Remplace les 3 workers par fichier par un pool global de morceaux en vol, réparti équitablement entre les fichiers fragmentés actifs : 1 fichier utilise tout le pool, N fichiers en reçoivent une part chacun (ceil(taille / N)), la capacité libérée par un fichier terminé est redistribuée. Un fichier en cours d'init compte dans la part pour ne pas être affamé ; les morceaux déjà en vol ne sont jamais interrompus. La taille du pool est réglable via `chunkPoolSize` (config.json, défaut 30, borné à 256) et transmise au navigateur par un nouveau `GET /config`. Corps des morceaux repassé en streaming (writeChunkAt sur un flux + garde `maxBytes`) : mémoire ~constante même à pool élevé, plus de bufferisation de 16 Mo par requête. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
102 lines
3.2 KiB
TypeScript
102 lines
3.2 KiB
TypeScript
import { readFileSync } from 'node:fs';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
import Fastify, { type FastifyInstance, type FastifyServerOptions } from 'fastify';
|
|
import multipart from '@fastify/multipart';
|
|
import secureSession from '@fastify/secure-session';
|
|
|
|
import type { AppConfig } from './config.js';
|
|
import { registerErrorHandler } from './error-handler.js';
|
|
import { ensureUploadDir, removeFile } from './lib/storage.js';
|
|
import { createUploadSessionStore } from './lib/upload-sessions.js';
|
|
import { createOidcClient, type OidcClient } from './oidc.js';
|
|
import { authRoutes } from './routes/auth.js';
|
|
import { chunkedUploadRoutes } from './routes/upload-chunked.js';
|
|
import { healthRoutes } from './routes/health.js';
|
|
import { uploadRoutes } from './routes/upload.js';
|
|
|
|
const INDEX_HTML = readFileSync(
|
|
fileURLToPath(new URL('../public/index.html', import.meta.url)),
|
|
'utf8',
|
|
);
|
|
|
|
const SESSION_COOKIE_NAME = 'mediadrop_session';
|
|
|
|
export interface BuildAppOptions {
|
|
/** Passer `false` pour couper les logs (utile en test). */
|
|
logger?: FastifyServerOptions['logger'];
|
|
/** Façade OIDC injectée (test). Par défaut : découverte réelle de l'issuer. */
|
|
oidcClient?: OidcClient;
|
|
}
|
|
|
|
/**
|
|
* Construit l'instance Fastify : session chiffrée, façade OIDC, plugins, dossier
|
|
* de dépôt, gestionnaire d'erreurs, page (`GET /`) et routes (`/auth/*`,
|
|
* `/health`, `/upload`). Exportée pour les tests.
|
|
*/
|
|
export async function buildApp(
|
|
config: AppConfig,
|
|
options: BuildAppOptions = {},
|
|
): Promise<FastifyInstance> {
|
|
const oidcClient = options.oidcClient ?? (await createOidcClient(config));
|
|
|
|
const app = Fastify({
|
|
logger: options.logger ?? { level: config.logLevel },
|
|
// Uploads pouvant durer longtemps (fichiers de plusieurs Go) : pas de
|
|
// timeout serveur imposé. Régler les timeouts sur le reverse proxy.
|
|
requestTimeout: 0,
|
|
connectionTimeout: 0,
|
|
});
|
|
|
|
const uploadSessions = createUploadSessionStore((session) => {
|
|
void removeFile(session.partPath);
|
|
});
|
|
|
|
app.decorate('appConfig', config);
|
|
app.decorate('oidcClient', oidcClient);
|
|
app.decorate('uploadSessions', uploadSessions);
|
|
app.addHook('onClose', () => {
|
|
uploadSessions.stop();
|
|
});
|
|
|
|
// Corps brut (flux) pour les morceaux d'upload fragmenté ; écrit en streaming.
|
|
app.addContentTypeParser('application/octet-stream', (_request, payload, taskDone) => {
|
|
taskDone(null, payload);
|
|
});
|
|
|
|
await app.register(secureSession, {
|
|
key: config.session.key,
|
|
cookieName: SESSION_COOKIE_NAME,
|
|
cookie: {
|
|
path: '/',
|
|
httpOnly: true,
|
|
sameSite: 'lax',
|
|
secure: config.publicUrl.startsWith('https://'),
|
|
maxAge: config.session.maxAgeSeconds,
|
|
},
|
|
});
|
|
|
|
await app.register(multipart, {
|
|
throwFileSizeLimit: true,
|
|
limits: {
|
|
files: 1,
|
|
fileSize: config.maxFileSizeBytes,
|
|
},
|
|
});
|
|
|
|
for (const target of config.targets) {
|
|
await ensureUploadDir(target.dir);
|
|
}
|
|
registerErrorHandler(app);
|
|
|
|
app.get('/', (_request, reply): void => {
|
|
reply.type('text/html; charset=utf-8').send(INDEX_HTML);
|
|
});
|
|
|
|
await app.register(authRoutes);
|
|
await app.register(healthRoutes);
|
|
await app.register(uploadRoutes);
|
|
await app.register(chunkedUploadRoutes);
|
|
|
|
return app;
|
|
}
|