Add file module for uploads and storage

- File model + migration: file metadata, content lives in object storage
- IFileStorage port with local disk and S3-compatible (RustFS/MinIO) adapters
- POST /files, GET /files/:id/download, DELETE /files/:id
- MIME sniffed from content; only safe images served inline, nosniff header
- FileOwnerRegistry: owner modules register to grant visibility and keep
  their files from GC; FileGcService removes orphaned files hourly
- TransformResponseInterceptor passes StreamableFile through

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
vadimandClaude Opus 5.5 committed 2026-10-02 09:17:55 +03:00
1 parent 5cfc8986d7
commit f32c6e348d
49 files changed
+1992 -2

No files matched your search

@@ -0,0 +1,26 @@
import { ConfigService } from '@nestjs/config';
import type { IFileStorage } from '../../domain/ports/file-storage.port.js';
import { LocalFileStorage } from './local-file-storage.js';
import { S3FileStorage } from './s3-file-storage.js';
/** FILE_STORAGE=s3 — S3-совместимое хранилище, иначе локальный диск */
export function createFileStorage(config: ConfigService): IFileStorage {
const driver = config.get<string>('FILE_STORAGE', 'local');
if (driver === 's3') {
return new S3FileStorage({
endpoint: config.get<string>('S3_ENDPOINT') || undefined,
region: config.get<string>('S3_REGION', 'us-east-1'),
bucket: config.getOrThrow<string>('S3_BUCKET'),
accessKeyId: config.getOrThrow<string>('S3_ACCESS_KEY_ID'),
secretAccessKey: config.getOrThrow<string>('S3_SECRET_ACCESS_KEY'),
forcePathStyle: config.get<string>('S3_FORCE_PATH_STYLE', 'true') !== 'false',
});
}
if (driver !== 'local') {
throw new Error(`Неизвестный FILE_STORAGE=${driver}, ожидается local | s3`);
}
return new LocalFileStorage(config.get<string>('FILE_STORAGE_LOCAL_DIR', './storage'));
}