Files
ulitka/src/modules/file/application/services/file-access.service.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

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