Construire une plateforme de gestion de fichiers complète — upload, authentification, analytics, sécurité fail2ban et déploiement Docker.
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.
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
}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 })
}Les noms de fichiers sont nettoyés avant stockage pour éviter les problèmes d'URL et de système de fichiers.
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}`
}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.
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"',
},
})
}