diff --git a/README.md b/README.md index b2bbbc5..b2f054b 100644 --- a/README.md +++ b/README.md @@ -72,3 +72,8 @@ APP_FILE_STORAGE_S3_ROOT_URL=https://storage.yandexcloud.net/arm-supervisor - APP_FILE_STORAGE_S3_USE_SSL - использовать SSL для подключения - APP_FILE_STORAGE_S3_ROOT_URL - адрес S3 хранилища, включая бакет - APP_FILE_STORAGE_S3_REGION - регион S3 хранилища + +### Параметры загрузки файла в определенное хранилище + +В проекте по токену GET_FILE_STORAGE_PARAMS_USE_CASE_TOKEN можно положить в DI-контейнер юзкейс, возвращающий параметры загрузки в определенный тип хранилища для конкретного fileType. +Данный юзкейс будет вызван при загрузке файла в FileService и FileImageService diff --git a/src/domain/dtos/FileSaveDto.ts b/src/domain/dtos/FileSaveDto.ts index 95ff549..6e356e5 100644 --- a/src/domain/dtos/FileSaveDto.ts +++ b/src/domain/dtos/FileSaveDto.ts @@ -25,6 +25,9 @@ export class FileSaveDto { @ExtendField(FileModel) folder: string; + @ExtendField(FileModel) + fileType?: string; + @ExtendField(FileModel) md5: string; } diff --git a/src/domain/interfaces/IFileStorage.ts b/src/domain/interfaces/IFileStorage.ts index 1728440..1bd5b97 100644 --- a/src/domain/interfaces/IFileStorage.ts +++ b/src/domain/interfaces/IFileStorage.ts @@ -4,9 +4,13 @@ import {IFileReadable} from './IFileReadable'; import {IFileWritable} from './IFileWritable'; export interface IFileStorage { - init(config: any) - read(file: IFileReadable): Promise - write(file: IFileWritable, source: Readable | Buffer): Promise - getUrl(file: IFileReadable): string - deleteFile(fileName: string): void | Promise; + init(config: any), + read(file: IFileReadable): Promise, + write( + file: IFileWritable, + source: Readable | Buffer, + fileStorageParams?: Record | null, + ): Promise, + getUrl(file: IFileReadable): string, + deleteFile(fileName: string): void | Promise, } diff --git a/src/domain/models/FileModel.ts b/src/domain/models/FileModel.ts index 11e6dcd..fc9914b 100644 --- a/src/domain/models/FileModel.ts +++ b/src/domain/models/FileModel.ts @@ -4,8 +4,8 @@ import { StringField, CreateTimeField, IntegerField, UidField, EnumField, } from '@steroidsjs/nest/infrastructure/decorators/fields'; -import {FileImageModel} from './FileImageModel'; import FileStorageEnum from '../enums/FileStorageEnum'; +import {FileImageModel} from './FileImageModel'; /** * Файлы @@ -66,6 +66,12 @@ export class FileModel { }) folder: string; + @StringField({ + label: 'Тип файла', + nullable: true, + }) + fileType: string; + @CreateTimeField({ label: 'Создан', }) diff --git a/src/domain/services/FileImageService.ts b/src/domain/services/FileImageService.ts index ed9c5a9..8c0d7db 100644 --- a/src/domain/services/FileImageService.ts +++ b/src/domain/services/FileImageService.ts @@ -1,9 +1,9 @@ import * as sharp from 'sharp'; import {DataMapper} from '@steroidsjs/nest/usecases/helpers/DataMapper'; import {ContextDto} from '@steroidsjs/nest/usecases/dtos/ContextDto'; +import {Inject, Optional} from '@nestjs/common'; import {IFileImageRepository} from '../interfaces/IFileImageRepository'; import {FileImageModel} from '../models/FileImageModel'; -import {FileConfigService} from './FileConfigService'; import {FileModel} from '../models/FileModel'; import {FileHelper} from '../helpers/FileHelper'; import FilePreviewEnum from '../enums/FilePreviewEnum'; @@ -12,8 +12,13 @@ import {SharpHelper} from '../helpers/SharpHelper'; import {IFilePreviewOptions} from '../interfaces/IFilePreviewOptions'; import {FileRemovedEventDto} from '../dtos/events/FileRemovedEventDto'; import {IEventEmitter} from '../interfaces/IEventEmitter'; -import { IFileStorageFactory } from '../interfaces/IFileStorageFactory'; +import {IFileStorageFactory} from '../interfaces/IFileStorageFactory'; import FileStorageEnum from '../enums/FileStorageEnum'; +import { + GET_FILE_STORAGE_PARAMS_USE_CASE_TOKEN, + IGetFileStorageParamsUseCase, +} from '../../usecases/getFileStorageParams/interfaces/IGetFileStorageParamsUseCase'; +import {FileConfigService} from './FileConfigService'; const SVG_MIME_TYPE = 'image/svg+xml'; @@ -23,6 +28,9 @@ export class FileImageService { protected readonly fileConfigService: FileConfigService, protected readonly fileStorageFactory: IFileStorageFactory, protected readonly eventEmitter: IEventEmitter, + @Optional() + @Inject(GET_FILE_STORAGE_PARAMS_USE_CASE_TOKEN) + protected readonly getFileStorageParamsUseCase?: IGetFileStorageParamsUseCase, ) { } @@ -113,15 +121,22 @@ export class FileImageService { }); if (hasChanges) { - await this.fileStorageFactory.get(file.storageName).write( - DataMapper.create(FileSaveDto, { - uid: file.uid, - folder: imageModel.folder, - fileName: imageModel.fileName, - fileMimeType: file.fileMimeType, - }), - data, - ); + const fileStorageParams = this.getFileStorageParamsUseCase + ? await this.getFileStorageParamsUseCase.handle(file.fileType, file.storageName) + : null; + + await this.fileStorageFactory + .get(file.storageName) + .write( + DataMapper.create(FileSaveDto, { + uid: file.uid, + folder: imageModel.folder, + fileName: imageModel.fileName, + fileMimeType: file.fileMimeType, + }), + data, + fileStorageParams, + ); } return this.repository.create(imageModel); diff --git a/src/domain/services/FileService.ts b/src/domain/services/FileService.ts index cd2b0ca..be9d06d 100644 --- a/src/domain/services/FileService.ts +++ b/src/domain/services/FileService.ts @@ -25,6 +25,9 @@ import {FileRemovedEventDto} from '../dtos/events/FileRemovedEventDto'; import {IFileTypeService} from '../interfaces/IFileTypeService'; import {IFileStorageFactory} from '../interfaces/IFileStorageFactory'; import FileStorageEnum from '../enums/FileStorageEnum'; +import { + IGetFileStorageParamsUseCase, +} from '../../usecases/getFileStorageParams/interfaces/IGetFileStorageParamsUseCase'; import {FileConfigService} from './FileConfigService'; import {FileImageService} from './FileImageService'; @@ -38,13 +41,14 @@ function isFileExpressOrLocalSource( export class FileService extends ReadService { constructor( - public repository: IFileRepository, + protected readonly repository: IFileRepository, protected readonly fileImageService: FileImageService, protected readonly fileConfigService: FileConfigService, protected readonly fileStorageFactory: IFileStorageFactory, protected readonly eventEmitter: IEventEmitter, protected readonly fileTypeService: IFileTypeService, public validators: IValidator[], + protected readonly getFileStorageParamsUseCase?: IGetFileStorageParamsUseCase, ) { super(); } @@ -122,8 +126,14 @@ export class FileService extends ReadService { // Get file stream from source const stream = await this.createStreamFromSource(options.source); + const fileStorageParams = this.getFileStorageParamsUseCase + ? await this.getFileStorageParamsUseCase.handle(options.fileType, options.storageName) + : null; + // Save original file via storage - const writeResult = await this.fileStorageFactory.get(options.storageName).write(fileDto, stream); + const writeResult = await this.fileStorageFactory + .get(options.storageName) + .write(fileDto, stream, fileStorageParams); // Delete temporary file const shouldDeleteTemporaryFile = !this.fileConfigService.saveTemporaryFileAfterUpload; diff --git a/src/domain/storages/FileLocalStorage.ts b/src/domain/storages/FileLocalStorage.ts index 5496b13..629d615 100644 --- a/src/domain/storages/FileLocalStorage.ts +++ b/src/domain/storages/FileLocalStorage.ts @@ -9,6 +9,8 @@ import {IFileLocalStorage} from '../interfaces/IFileLocalStorage'; import {IFileReadable} from '../interfaces/IFileReadable'; import {IFileWritable} from '../interfaces/IFileWritable'; +const DEFAULT_FILE_ENCODING: BufferEncoding = 'utf8'; + export class FileLocalStorage implements IFileLocalStorage { /** * Absolute path to root user files dir @@ -34,7 +36,11 @@ export class FileLocalStorage implements IFileLocalStorage { return fs.promises.readFile(filePath); } - public async write(file: IFileWritable, source: Readable | Buffer): Promise { + public async write( + file: IFileWritable, + source: Readable | Buffer, + fileStorageParams: Record | null = {}, + ): Promise { const dir = join(...[this.rootPath, file.folder].filter(Boolean)); // Create dir @@ -43,7 +49,13 @@ export class FileLocalStorage implements IFileLocalStorage { } const filePath = join(dir, file.fileName); - await fs.promises.writeFile(filePath, source, 'utf8'); + + const options = { + encoding: DEFAULT_FILE_ENCODING, + ...fileStorageParams, + }; + + await fs.promises.writeFile(filePath, source, options); return DataMapper.create(FileWriteResult, { md5: await md5File(filePath), diff --git a/src/domain/storages/MinioS3Storage.ts b/src/domain/storages/MinioS3Storage.ts index e18d3c0..085d4f4 100644 --- a/src/domain/storages/MinioS3Storage.ts +++ b/src/domain/storages/MinioS3Storage.ts @@ -1,5 +1,5 @@ -import {toInteger as _toInteger} from 'lodash'; import {Readable} from 'stream'; +import {toInteger as _toInteger} from 'lodash'; import * as Minio from 'minio'; import {DataMapper} from '@steroidsjs/nest/usecases/helpers/DataMapper'; import {normalizeBoolean} from '@steroidsjs/nest/infrastructure/decorators/fields/BooleanField'; @@ -73,16 +73,25 @@ export class MinioS3Storage implements IFileStorage { }); } - public async write(file: IFileWritable, source: Readable | Buffer): Promise { + public async write( + file: IFileWritable, + source: Readable | Buffer, + fileStorageParams: Record | null = {}, + ): Promise { await this.makeMainBucket(); + const metaData = { + 'Content-Type': file.fileMimeType, + ...fileStorageParams, + }; + return new Promise((resolve, reject) => { this.getClient().putObject( this.mainBucket, [file.folder, file.fileName].filter(Boolean).join('/'), source, file.fileSize, - {'Content-Type': file.fileMimeType}, + metaData, (err, {etag}) => { if (err) { reject(err); diff --git a/src/usecases/getFileStorageParams/interfaces/IGetFileStorageParamsUseCase.ts b/src/usecases/getFileStorageParams/interfaces/IGetFileStorageParamsUseCase.ts new file mode 100644 index 0000000..639788d --- /dev/null +++ b/src/usecases/getFileStorageParams/interfaces/IGetFileStorageParamsUseCase.ts @@ -0,0 +1,8 @@ +export const GET_FILE_STORAGE_PARAMS_USE_CASE_TOKEN = 'get_file_storage_params_use_case_token'; + +/* + Позволяет задать параметры загрузки файла в определенный тип хранилища + */ +export interface IGetFileStorageParamsUseCase { + handle: (fileType: string, storageName: string) => Promise>, +}