Browse Source

feature(server): compute sha1 during upload (#1424)

* feature(server): compute sha1 during upload

* fix: clean up stream on error
Jason Rasmussen 2 years ago
parent
commit
c4e1bc35b4

+ 2 - 2
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<AssetFileUploadResponseDto> {

+ 5 - 4
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,

+ 32 - 6
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) {