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:
1 parent
5cfc8986d7
commit
f32c6e348d
49 files changed
+1992
-2
No files matched your search
@@ -0,0 +1,40 @@
|
||||
import { createReadStream, createWriteStream } from 'node:fs';
|
||||
import { mkdir, rm, stat } from 'node:fs/promises';
|
||||
import { dirname, resolve, sep } from 'node:path';
|
||||
import type { Readable } from 'node:stream';
|
||||
import { pipeline } from 'node:stream/promises';
|
||||
import type { IFileStorage } from '../../domain/ports/file-storage.port.js';
|
||||
|
||||
/** Хранилище на локальном диске — для разработки и тестов */
|
||||
export class LocalFileStorage implements IFileStorage {
|
||||
private readonly root: string;
|
||||
|
||||
constructor(root: string) {
|
||||
this.root = resolve(root);
|
||||
}
|
||||
|
||||
async put(key: string, body: Readable): Promise<void> {
|
||||
const path = this.resolveKey(key);
|
||||
await mkdir(dirname(path), { recursive: true });
|
||||
await pipeline(body, createWriteStream(path));
|
||||
}
|
||||
|
||||
async get(key: string): Promise<Readable> {
|
||||
const path = this.resolveKey(key);
|
||||
await stat(path);
|
||||
return createReadStream(path);
|
||||
}
|
||||
|
||||
async delete(key: string): Promise<void> {
|
||||
await rm(this.resolveKey(key), { force: true });
|
||||
}
|
||||
|
||||
/** Ключи генерирует приложение, но выход за пределы root всё равно запрещаем */
|
||||
private resolveKey(key: string): string {
|
||||
const path = resolve(this.root, key);
|
||||
if (!path.startsWith(this.root + sep)) {
|
||||
throw new Error(`Недопустимый ключ хранилища: ${key}`);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user