import type { FastifyPluginCallback } from 'fastify'; import type { DropTarget } from '../config.js'; import { ValidationError } from '../errors.js'; import { isAllowedMedia } from '../lib/media-types.js'; import { resolvePlacement, resolveTarget } from '../lib/targets.js'; import { requireSession } from '../session.js'; import { saveFile, type StoredFile } from '../lib/storage.js'; const FIELD_NAME = 'file'; interface UploadResponse { target: string; storedName: string; originalName: string; size: number; mimeType: string; } interface TargetSummary { name: string; label: string; allowDirectories: boolean; } interface ClientConfig { /** Taille du pool de connexions pour l'upload fragmenté (réglage client). */ chunkPoolSize: number; } interface UploadQuery { target?: unknown; path?: unknown; } function toResponse(target: DropTarget, stored: StoredFile): UploadResponse { return { target: target.name, storedName: stored.storedName, originalName: stored.originalName, size: stored.size, mimeType: stored.mimeType, }; } /** * - `GET /config` — réglages client non sensibles (taille du pool de morceaux) * - `GET /targets` — liste des cibles (nom, libellé, dépôt de dossier autorisé) * - `POST /upload?target=[&path=]` — dépôt d'un fichier multipart */ export const uploadRoutes: FastifyPluginCallback = (app, _options, done) => { app.get('/config', (): ClientConfig => ({ chunkPoolSize: app.appConfig.chunkPoolSize })); app.get('/targets', { preHandler: requireSession }, (): TargetSummary[] => app.appConfig.targets.map((target) => ({ name: target.name, label: target.label, allowDirectories: target.allowDirectories, })), ); app.post( '/upload', { preHandler: requireSession }, async (request, reply): Promise => { const query = request.query as UploadQuery; const target = resolveTarget(app.appConfig.targets, query.target); const placement = resolvePlacement(target, query.path); if (!request.isMultipart()) { throw new ValidationError('Requête multipart/form-data attendue'); } const part = await request.file(); if (!part) { throw new ValidationError(`Aucun fichier reçu dans le champ « ${FIELD_NAME} »`); } if (part.fieldname !== FIELD_NAME) { part.file.resume(); throw new ValidationError(`Le fichier doit être envoyé dans le champ « ${FIELD_NAME} »`); } if (!isAllowedMedia(part.mimetype, app.appConfig.allowedMimeTypes)) { part.file.resume(); throw new ValidationError(`Type de média non autorisé : ${part.mimetype}`); } const stored = await saveFile({ source: part.file, originalName: part.filename, mimeType: part.mimetype, targetDir: target.dir, maxFileSizeBytes: app.appConfig.maxFileSizeBytes, ...(placement !== undefined ? { placement } : {}), }); reply.status(201); return toResponse(target, stored); }, ); done(); };