iducationducation
IndexArticlesFormationsProfilOutilsBibliothech
N°014 — 2026
Navigation
01Index02Articles03Formations04Profil05Outils06Bibliothech
N°014 — 2026
iducationducation
IndexArticlesFormationsProfilOutilsBibliothech
N°014 — 2026
Navigation
01Index02Articles03Formations04Profil05Outils06Bibliothech
N°014 — 2026
Formations
TypeScript · Avancé

OpenFiler

Construire une plateforme de gestion de fichiers complète — upload, authentification, analytics, sécurité fail2ban et déploiement Docker.

Next.js 15TypeScriptSQLiteBetter AuthTailwind CSS v4
GitHub
01Setup et configuration02Authentification avec Better Auth03Base de données SQLite04Système d'upload de fichiers05Dashboard et interface06Corbeille et liens de partage07Déploiement Docker
Chapitre 4·30 min

Système d'upload de fichiers

L'upload dans OpenFiler ne passe par aucun service tiers. Les fichiers vont directement sur le disque du serveur, classés par type, avec validation MIME et limites configurables.

Configuration des limites

lib/upload-config.ts
export const UPLOAD_CONFIG = {
  maxFileSize: 64 * 1024 * 1024, // 64 MB
  limits: {
    image: { count: 6, mimes: ['image/jpeg', 'image/png', 'image/svg+xml', 'image/webp', 'image/bmp', 'image/x-icon'] },
    video: { count: 2, mimes: ['video/mp4', 'video/avi', 'video/quicktime', 'video/x-ms-wmv', 'video/x-flv', 'video/webm', 'video/x-matroska'] },
    document: { count: 3, mimes: ['application/pdf', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'] },
  },
}
 
export function getFolderFromMime(mime: string): string | null {
  for (const [folder, config] of Object.entries(UPLOAD_CONFIG.limits)) {
    if (config.mimes.includes(mime)) return folder
  }
  return null
}

Route API d'upload

app/api/upload/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { writeFile, mkdir } from 'fs/promises'
import { join } from 'path'
import { requireSession } from '@/lib/auth/require-session'
import { getFolderFromMime } from '@/lib/upload-config'
import { slugify } from '@/lib/slug'
import db from '@/lib/db'
 
export async function POST(req: NextRequest) {
  const { session, error } = await requireSession()
  if (error) return error
 
  const formData = await req.formData()
  const files = formData.getAll('files') as File[]
 
  const results = []
 
  for (const file of files) {
    const folder = getFolderFromMime(file.type)
    if (!folder) {
      results.push({ name: file.name, error: 'Type non autorisé' })
      continue
    }
 
    if (file.size > 64 * 1024 * 1024) {
      results.push({ name: file.name, error: 'Fichier trop lourd' })
      continue
    }
 
    const filename = slugify(file.name)
    const uploadDir = join(process.cwd(), 'upload', folder)
    await mkdir(uploadDir, { recursive: true })
 
    const buffer = Buffer.from(await file.arrayBuffer())
    await writeFile(join(uploadDir, filename), buffer)
 
    db.prepare(
      `INSERT INTO file_metadata (filename, original_name, folder, size, mime_type, uploaded_by)
       VALUES (?, ?, ?, ?, ?, ?)`
    ).run(filename, file.name, folder, file.size, file.type, session.user.id)
 
    results.push({ name: file.name, filename, folder, success: true })
  }
 
  return NextResponse.json({ results })
}

Slugification des noms

Les noms de fichiers sont nettoyés avant stockage pour éviter les problèmes d'URL et de système de fichiers.

lib/slug.ts
export function slugify(filename: string): string {
  const ext = filename.split('.').pop() ?? ''
  const base = filename.slice(0, filename.lastIndexOf('.'))
  const clean = base
    .toLowerCase()
    .normalize('NFD')
    .replace(/[̀-ͯ]/g, '') // supprimer accents
    .replace(/[^a-z0-9]+/g, '-')
    .replace(/^-|-$/g, '')
  const timestamp = Date.now()
  return `${clean}-${timestamp}.${ext}`
}

Initialisation des dossiers

lib/ensure-dirs.ts
import { mkdir } from 'fs/promises'
import { join } from 'path'
 
export async function ensureUploadDirs() {
  const dirs = ['image', 'video', 'document', 'trash']
  await Promise.all(
    dirs.map(d => mkdir(join(process.cwd(), 'upload', d), { recursive: true }))
  )
}

Appelé dans instrumentation.ts au démarrage — garantit que les dossiers existent avant le premier upload.

Téléchargement en lot (ZIP)

app/api/download/batch/route.ts
import archiver from 'archiver'
import { createReadStream } from 'fs'
import { join } from 'path'
 
export async function POST(req: NextRequest) {
  const { files } = await req.json() // [{ folder, filename }]
 
  const archive = archiver('zip')
  const chunks: Buffer[] = []
 
  archive.on('data', chunk => chunks.push(chunk))
  await new Promise<void>((resolve, reject) => {
    archive.on('end', resolve)
    archive.on('error', reject)
 
    for (const { folder, filename } of files) {
      archive.file(join(process.cwd(), 'upload', folder, filename), { name: filename })
    }
    archive.finalize()
  })
 
  return new Response(Buffer.concat(chunks), {
    headers: {
      'Content-Type': 'application/zip',
      'Content-Disposition': 'attachment; filename="openfiler-export.zip"',
    },
  })
}
Précédent
Base de données SQLite
Suivant
Dashboard et interface

Développeur fullstack passionné. J'apprends en construisant et je documente tout — front, back, outils. Le code s'apprend mieux en public.

Naviguer

IndexTous les articlesFormationsProfilOutilsBibliothech

Ailleurs

GitHub RSS

Newsletter

Les articles, libs et découvertes. Une fois par semaine, pas plus.

© 2026 William LoreeConçu & codé à la main