diff --git a/server/apps/immich/src/api-v1/asset/asset.controller.ts b/server/apps/immich/src/api-v1/asset/asset.controller.ts index 2176ebe26a..5db2aee25b 100644 --- a/server/apps/immich/src/api-v1/asset/asset.controller.ts +++ b/server/apps/immich/src/api-v1/asset/asset.controller.ts @@ -19,7 +19,7 @@ import { import { Authenticated } from '../../decorators/authenticated.decorator'; import { AssetService } from './asset.service'; import { FileFieldsInterceptor } from '@nestjs/platform-express'; -import { assetUploadOption } from '../../config/asset-upload.config'; +import { assetUploadOption, ImmichFile } from '../../config/asset-upload.config'; import { AuthUserDto, GetAuthUser } from '../../decorators/auth-user.decorator'; import { ServeFileDto } from './dto/serve-file.dto'; import { Response as Res } from 'express'; @@ -80,7 +80,7 @@ export class AssetController { }) async uploadFile( @GetAuthUser() authUser: AuthUserDto, - @UploadedFiles() files: { assetData: Express.Multer.File[]; livePhotoData?: Express.Multer.File[] }, + @UploadedFiles() files: { assetData: ImmichFile[]; livePhotoData?: ImmichFile[] }, @Body(ValidationPipe) createAssetDto: CreateAssetDto, @Response({ passthrough: true }) res: Res, ): Promise { diff --git a/server/apps/immich/src/api-v1/asset/asset.service.ts b/server/apps/immich/src/api-v1/asset/asset.service.ts index 534cf5234a..b7765f49fd 100644 --- a/server/apps/immich/src/api-v1/asset/asset.service.ts +++ b/server/apps/immich/src/api-v1/asset/asset.service.ts @@ -55,6 +55,7 @@ import { CreateAssetsShareLinkDto } from './dto/create-asset-shared-link.dto'; import { mapSharedLink, SharedLinkResponseDto } from '@app/domain'; import { UpdateAssetsToSharedLinkDto } from './dto/add-assets-to-shared-link.dto'; import { AssetSearchDto } from './dto/asset-search.dto'; +import { ImmichFile } from '../../config/asset-upload.config'; const fileInfo = promisify(stat); @@ -82,16 +83,16 @@ export class AssetService { authUser: AuthUserDto, createAssetDto: CreateAssetDto, res: Res, - originalAssetData: Express.Multer.File, - livePhotoAssetData?: Express.Multer.File, + originalAssetData: ImmichFile, + livePhotoAssetData?: ImmichFile, ) { - const checksum = await this.calculateChecksum(originalAssetData.path); + const checksum = originalAssetData.checksum; const isLivePhoto = livePhotoAssetData !== undefined; let livePhotoAssetEntity: AssetEntity | undefined; try { if (isLivePhoto) { - const livePhotoChecksum = await this.calculateChecksum(livePhotoAssetData.path); + const livePhotoChecksum = livePhotoAssetData.checksum; livePhotoAssetEntity = await this.createUserAsset( authUser, createAssetDto, diff --git a/server/apps/immich/src/config/asset-upload.config.ts b/server/apps/immich/src/config/asset-upload.config.ts index de09d0811b..770ffc97d5 100644 --- a/server/apps/immich/src/config/asset-upload.config.ts +++ b/server/apps/immich/src/config/asset-upload.config.ts @@ -1,10 +1,10 @@ import { APP_UPLOAD_LOCATION } from '@app/common/constants'; import { BadRequestException, Logger, UnauthorizedException } from '@nestjs/common'; import { MulterOptions } from '@nestjs/platform-express/multer/interfaces/multer-options.interface'; -import { randomUUID } from 'crypto'; +import { createHash, randomUUID } from 'crypto'; import { Request } from 'express'; import { existsSync, mkdirSync } from 'fs'; -import { diskStorage } from 'multer'; +import { diskStorage, StorageEngine } from 'multer'; import { extname, join } from 'path'; import sanitize from 'sanitize-filename'; import { AuthUserDto } from '../decorators/auth-user.decorator'; @@ -12,14 +12,40 @@ import { patchFormData } from '../utils/path-form-data.util'; const logger = new Logger('AssetUploadConfig'); +export interface ImmichFile extends Express.Multer.File { + /** sha1 hash of file */ + checksum: Buffer; +} + export const assetUploadOption: MulterOptions = { fileFilter, - storage: diskStorage({ - destination, - filename, - }), + storage: customStorage(), }; +export function customStorage(): StorageEngine { + const storage = diskStorage({ destination, filename }); + + return { + _handleFile(req, file, callback) { + const hash = createHash('sha1'); + file.stream.on('data', (chunk) => hash.update(chunk)); + + storage._handleFile(req, file, (error, response) => { + if (error) { + hash.destroy(); + callback(error); + } else { + callback(null, { ...response, checksum: hash.digest() } as ImmichFile); + } + }); + }, + + _removeFile(req, file, callback) { + storage._removeFile(req, file, callback); + }, + }; +} + export const multerUtils = { fileFilter, filename, destination }; function fileFilter(req: Request, file: any, cb: any) {