- 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>
45 lines
1.9 KiB
TypeScript
45 lines
1.9 KiB
TypeScript
import { Inject, Injectable } from '@nestjs/common';
|
|
import { FILE_REPOSITORY, type IFileRepository } from '../../domain/repositories/file.repository.interface.js';
|
|
import { StoredFile } from '../../domain/entities/stored-file.entity.js';
|
|
import { FileNotFoundException } from '../../domain/exceptions/file-not-found.exception.js';
|
|
import { FileOwnerRegistry } from './file-owner.registry.js';
|
|
import { FileDto } from '../dto/file.dto.js';
|
|
import { FileApplicationMapper } from '../mappers/file-application.mapper.js';
|
|
|
|
@Injectable()
|
|
export class FileAccessService {
|
|
constructor(
|
|
@Inject(FILE_REPOSITORY)
|
|
private readonly fileRepository: IFileRepository,
|
|
private readonly fileOwners: FileOwnerRegistry,
|
|
) {}
|
|
|
|
/**
|
|
* Файл виден загрузившему и всем, кому виден хоть один его владелец
|
|
* (например, задача, к которой он прикреплён). Иначе — FileNotFoundException,
|
|
* чтобы не раскрывать существование чужих файлов.
|
|
*/
|
|
async getVisibleFile(id: string, userId: string): Promise<StoredFile> {
|
|
const file = await this.fileRepository.findById(id);
|
|
if (!file) {
|
|
throw new FileNotFoundException(id);
|
|
}
|
|
|
|
if (file.isUploadedBy(userId) || (await this.fileOwners.isVisibleThroughAnyOwner(id, userId))) {
|
|
return file;
|
|
}
|
|
|
|
throw new FileNotFoundException(id);
|
|
}
|
|
|
|
/**
|
|
* Файл, который пользователь может прикрепить к задаче/комментарию:
|
|
* невидимый — FileNotFoundException, видимый, но чужой — FileForbiddenException.
|
|
*/
|
|
async getAttachableFile(id: string, userId: string): Promise<FileDto> {
|
|
const file = await this.getVisibleFile(id, userId);
|
|
file.assertOwnedBy(userId);
|
|
return FileApplicationMapper.toDto(file);
|
|
}
|
|
}
|