mediaDrop/src/app.ts
Nicolas 64a38b1e41
All checks were successful
Publier l'image Docker / test (push) Successful in 17s
Publier l'image Docker / publish (push) Successful in 11s
Upload fragmenté : pool de connexions partagé et configurable
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>
2026-09-10 19:12:04 +02:00

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;
}