import { randomUUID } from 'node:crypto'; import type { DropTarget } from '../config.js'; import { TooManyUploadsError, UploadSessionNotFoundError } from '../errors.js'; /** Un upload fragmenté en cours : morceaux écrits dans `partPath`, finalisé en `finalPath`. */ export interface UploadSession { id: string; target: DropTarget; /** Nom de fichier annoncé par le client (pour les notifications). */ originalName: string; /** Nom simple, ou chemin relatif `dossier/sous/fichier.ext` (mode dossier). */ storedName: string; mimeType: string; partPath: string; finalPath: string; totalSize: number; chunkSize: number; chunkCount: number; received: Set; lastActivity: number; } export type NewUploadSession = Omit; const IDLE_TTL_MS = 30 * 60_000; const SWEEP_INTERVAL_MS = 60_000; const MAX_SESSIONS = 100; export interface UploadSessionStore { create(input: NewUploadSession): UploadSession; /** Récupère et rafraîchit la session ; lève `UploadSessionNotFoundError` si absente. */ use(id: string): UploadSession; remove(id: string): UploadSession | undefined; stop(): void; } /** * Registre en mémoire des uploads fragmentés. Les sessions inactives depuis plus * de 30 min sont purgées et `onExpire` est appelé (pour supprimer le `.part`). */ export function createUploadSessionStore( onExpire: (session: UploadSession) => void, ): UploadSessionStore { const sessions = new Map(); const timer = setInterval(() => { const now = Date.now(); for (const session of sessions.values()) { if (now - session.lastActivity > IDLE_TTL_MS) { sessions.delete(session.id); onExpire(session); } } }, SWEEP_INTERVAL_MS); timer.unref(); return { create(input: NewUploadSession): UploadSession { if (sessions.size >= MAX_SESSIONS) { throw new TooManyUploadsError(); } const session: UploadSession = { ...input, id: randomUUID(), received: new Set(), lastActivity: Date.now(), }; sessions.set(session.id, session); return session; }, use(id: string): UploadSession { const session = sessions.get(id); if (session === undefined) { throw new UploadSessionNotFoundError(); } session.lastActivity = Date.now(); return session; }, remove(id: string): UploadSession | undefined { const session = sessions.get(id); sessions.delete(id); return session; }, stop(): void { clearInterval(timer); }, }; }