import { Injectable , Inject , ForbiddenException } from "@nestjs/common" ; import * as _ from "lodash" ; import { ReadStream } from "fs" ; import { PassThrough } from "stream" ; import { IFindOptions } from "sequelize-typescript" ; import { WhereOptions , Op } from "sequelize" ; import { FileDto } from "../dto" ; import { BaseService } from "../../../base.service" ; import { FileDiToken } from "../file.di" ; import { S3FileStorage } from "../storage/file-storage" ; import { DbDiToken , File , FileType } from "../../db" ; import { TypeMapperDiToken , TypeMapper } from "../../shared" ; import { S3FileStorageConfigurationFactory } from "../storage" ; import { ValidatorService , NotNilValidator , EnumValueValidator , NotEmptyValidator } from "../../shared/validator" ; import { JwtUserData , AuthDiToken , PermissionContext , CurrentUserService , PermissionContextService , CurrentUserNotDefinedException , } from "../../auth" ; export interface FileUploadRequestModel { data : { originalname : string ; buffer : Buffer | ReadableStream ; } ; file : FileParams ; } export interface FileParams { title : string ; name ? : string ; type : FileType ; description ? : string ; dealId : number ; } /**
 *  Abstractaion layer of in-system file representation manipulations.
 *  Files are assumed to be presented in `file` table.
 *  FileDto - in-system file representation.
 */ @ Injectable ( ) export class FileService extends BaseService { public constructor ( @ Inject ( FileDiToken . FILE_STORAGE ) private readonly storage , @ Inject ( FileDiToken . FILE_STORAGE_CONFIGURATION_FACTORY ) private readonly configFactory , @ Inject ( DbDiToken . FILE_REPOSITORY ) private readonly repository , @ Inject ( AuthDiToken . CURRENT_USER_SERVICE ) private readonly currentUserService , @ Inject ( AuthDiToken . PERMISSION_CONTEXT_SERVICE ) private readonly permissionService , @ Inject ( TypeMapperDiToken . MAPPER ) private readonly typeMapper ) { super ( ) ; } param { ReadStream } readStream param { FileParams } params returns { FileDto } description public async multipartUpload ( readStream , params ) { const passTransformStream = new PassThrough ( ) ; const contextData = await this . getFileUploadPermissionContext ( params ) ; const storagePath = this . configFactory . getStoragePath ( { context : contextData , type : params . type } ) ; const key = this . configFactory . generateKey ( storagePath , params . title ) ; const config = this . configFactory . getUploadConfiguration ( key , passTransformStream , params . name ) ; const currentUser = this . currentUserService . get ( ) ; const file = new FileDto ( { description : params . description || "" , name : params . name , title : params . title , uploadDate : new Date ( ) , type : params . type , uploadedByUserId : currentUser . id , context : contextData , key , } ) ; this . validateFile ( file ) ; if ( ! currentUser ) { throw new CurrentUserNotDefinedException ( ) ; } return new Promise < FileDto > ( ( resolve , reject ) => { const multipartUpload$ = this . storage . upload ( config ) ; readStream . pipe ( passTransformStream ) ; readStream . on ( "error" , ( err ) => { reject ( err ) ; } ) ; readStream . on ( "end" , async ( ) => { await multipartUpload$ ; const createdFile = ( await this . repository . create ( file ) ) . toJSON ( ) ; createdFile . directUrl = this . storage . getDirectUrl ( this . configFactory . getDownloadConfiguration ( key ) ) ; resolve ( createdFile ) ; } ) ; } ) ; } param { FileUploadRequestModel } request returns { FileDto } description public async upload ( request ) { const fileOriginalName = request . data . originalname ; const contextData = await this . getFileUploadPermissionContext ( request . file ) ; const storagePath = this . configFactory . getStoragePath ( { context : contextData , type : request . file . type } ) ; const key = this . configFactory . generateKey ( storagePath , fileOriginalName ) ; const config = this . configFactory . getUploadConfiguration ( key , request . data . buffer , fileOriginalName ) ; const currentUser = this . currentUserService . get ( ) ; if ( ! currentUser ) { throw new CurrentUserNotDefinedException ( ) ; } const file = new FileDto ( { description : request . file . description || "" , name : fileOriginalName , title : request . file . title , uploadDate : new Date ( ) , type : request . file . type , uploadedByUserId : currentUser . id , context : contextData , key , } ) ; this . validateFile ( file ) ; await this . storage . upload ( config ) ; const createdFile = ( await this . repository . create ( file ) ) . toJSON ( ) ; createdFile . directUrl = this . storage . getDirectUrl ( this . configFactory . getDownloadConfiguration ( key ) ) ; return createdFile ; } param { Number [ ] } ids description public async multipleDelete ( ids ) { ValidatorService . compose ( [ new NotEmptyValidator ( "ids" ) ] ) ( ids ) ; const fileLookupWhereOptions = { id : { [ Op . in ] : ids } } ; const secureFileLookupOptions = await this . permissionService . getFilePermissionContext ( fileLookupWhereOptions ) ; const files = await this . repository . findAll ( { where : secureFileLookupOptions } ) ; const filesDtos = _ . map ( files , ( file ) => this . typeMapper . map ( File , FileDto , file . toJSON ( ) ) ) ; if ( files . length !== ids . length ) { throw new ForbiddenException ( `File delete unauthorized attempt` ) ; } await this . multipleDestroyBy ( this . repository , ids ) ; await this . storage . multipleDelete ( this . configFactory . getMultipleDeleteConfiguration ( _ . map ( filesDtos , file => file . key ) ) ) ; } param { FileDto [ ] } files public async multipleUpdate ( files ) { const updatedFiles = await Promise . map ( files , file => { return this . updateBy < File > ( this . repository , File , file as any , "id" , true , [ "title" , "description" ] ) ; } ) as File [ ] ; return _ . map ( updatedFiles , file => this . typeMapper . map ( File , FileDto , file ) ) ; } param { FileDto } file returns { String } description Methods returns 15-mins valid direct url.
     *  Validity duration can be overriten in  S3FileStorageConfigurationFactory `getDownloadConfiguration` method. Refer to AWS S3 SDK docs.
      public getDirectUrl ( file ) { ValidatorService . compose ( [ new NotNilValidator ( "Key" , "key" ) , ] ) ( file ) ; return this . storage . getDirectUrl ( this . configFactory . getDownloadConfiguration ( file . key ) ) ; } ignore private validateFile ( file ) { ValidatorService . compose ( [ new EnumValueValidator < typeof FileType > ( FileType , "type" , "type" ) , new NotNilValidator ( "title" , "title" ) , new NotNilValidator ( "name" , "name" ) , ] ) ( file ) ; } ignore private async getFileUploadPermissionContext ( params ) { return { } ; } }