Files
ulitka/src/modules/file/infrastructure/storage/local-file-storage.ts
T
vadimandClaude Opus 5.5 f32c6e348d 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>
2026-10-02 09:17:55 +03:00

41 lines
1.4 KiB
TypeScript

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