Browse Source

feat(server): return asset checksum (#2582)

* feat: return asset checksum

* chore: generate open api

* chore: coverage

* feat(server): support base64 hashes in bulk upload check:

* chore: generate open api
Jason Rasmussen 2 years ago
parent
commit
bca4626708

+ 1 - 1
mobile/openapi/doc/AssetBulkUploadCheckItem.md

@@ -9,7 +9,7 @@ import 'package:openapi/api.dart';
 Name | Type | Description | Notes
 Name | Type | Description | Notes
 ------------ | ------------- | ------------- | -------------
 ------------ | ------------- | ------------- | -------------
 **id** | **String** |  | 
 **id** | **String** |  | 
-**checksum** | **String** |  | 
+**checksum** | **String** | base64 or hex encoded sha1 hash | 
 
 
 [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
 [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
 
 

+ 2 - 3
mobile/openapi/doc/AssetResponseDto.md

@@ -15,7 +15,7 @@ Name | Type | Description | Notes
 **deviceId** | **String** |  | 
 **deviceId** | **String** |  | 
 **originalPath** | **String** |  | 
 **originalPath** | **String** |  | 
 **originalFileName** | **String** |  | 
 **originalFileName** | **String** |  | 
-**resizePath** | **String** |  | 
+**resized** | **bool** |  | 
 **fileCreatedAt** | **String** |  | 
 **fileCreatedAt** | **String** |  | 
 **fileModifiedAt** | **String** |  | 
 **fileModifiedAt** | **String** |  | 
 **updatedAt** | **String** |  | 
 **updatedAt** | **String** |  | 
@@ -23,13 +23,12 @@ Name | Type | Description | Notes
 **isArchived** | **bool** |  | 
 **isArchived** | **bool** |  | 
 **mimeType** | **String** |  | 
 **mimeType** | **String** |  | 
 **duration** | **String** |  | 
 **duration** | **String** |  | 
-**webpPath** | **String** |  | 
-**encodedVideoPath** | **String** |  | [optional] 
 **exifInfo** | [**ExifResponseDto**](ExifResponseDto.md) |  | [optional] 
 **exifInfo** | [**ExifResponseDto**](ExifResponseDto.md) |  | [optional] 
 **smartInfo** | [**SmartInfoResponseDto**](SmartInfoResponseDto.md) |  | [optional] 
 **smartInfo** | [**SmartInfoResponseDto**](SmartInfoResponseDto.md) |  | [optional] 
 **livePhotoVideoId** | **String** |  | [optional] 
 **livePhotoVideoId** | **String** |  | [optional] 
 **tags** | [**List<TagResponseDto>**](TagResponseDto.md) |  | [optional] [default to const []]
 **tags** | [**List<TagResponseDto>**](TagResponseDto.md) |  | [optional] [default to const []]
 **people** | [**List<PersonResponseDto>**](PersonResponseDto.md) |  | [optional] [default to const []]
 **people** | [**List<PersonResponseDto>**](PersonResponseDto.md) |  | [optional] [default to const []]
+**checksum** | **String** | base64 encoded sha1 hash | 
 
 
 [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
 [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
 
 

+ 1 - 0
mobile/openapi/lib/model/asset_bulk_upload_check_item.dart

@@ -19,6 +19,7 @@ class AssetBulkUploadCheckItem {
 
 
   String id;
   String id;
 
 
+  /// base64 or hex encoded sha1 hash
   String checksum;
   String checksum;
 
 
   @override
   @override

+ 19 - 37
mobile/openapi/lib/model/asset_response_dto.dart

@@ -20,7 +20,7 @@ class AssetResponseDto {
     required this.deviceId,
     required this.deviceId,
     required this.originalPath,
     required this.originalPath,
     required this.originalFileName,
     required this.originalFileName,
-    required this.resizePath,
+    required this.resized,
     required this.fileCreatedAt,
     required this.fileCreatedAt,
     required this.fileModifiedAt,
     required this.fileModifiedAt,
     required this.updatedAt,
     required this.updatedAt,
@@ -28,13 +28,12 @@ class AssetResponseDto {
     required this.isArchived,
     required this.isArchived,
     required this.mimeType,
     required this.mimeType,
     required this.duration,
     required this.duration,
-    required this.webpPath,
-    this.encodedVideoPath,
     this.exifInfo,
     this.exifInfo,
     this.smartInfo,
     this.smartInfo,
     this.livePhotoVideoId,
     this.livePhotoVideoId,
     this.tags = const [],
     this.tags = const [],
     this.people = const [],
     this.people = const [],
+    required this.checksum,
   });
   });
 
 
   AssetTypeEnum type;
   AssetTypeEnum type;
@@ -51,7 +50,7 @@ class AssetResponseDto {
 
 
   String originalFileName;
   String originalFileName;
 
 
-  String? resizePath;
+  bool resized;
 
 
   String fileCreatedAt;
   String fileCreatedAt;
 
 
@@ -67,10 +66,6 @@ class AssetResponseDto {
 
 
   String duration;
   String duration;
 
 
-  String? webpPath;
-
-  String? encodedVideoPath;
-
   ///
   ///
   /// Please note: This property should have been non-nullable! Since the specification file
   /// Please note: This property should have been non-nullable! Since the specification file
   /// does not include a default value (using the "default:" property), however, the generated
   /// does not include a default value (using the "default:" property), however, the generated
@@ -93,6 +88,9 @@ class AssetResponseDto {
 
 
   List<PersonResponseDto> people;
   List<PersonResponseDto> people;
 
 
+  /// base64 encoded sha1 hash
+  String checksum;
+
   @override
   @override
   bool operator ==(Object other) => identical(this, other) || other is AssetResponseDto &&
   bool operator ==(Object other) => identical(this, other) || other is AssetResponseDto &&
      other.type == type &&
      other.type == type &&
@@ -102,7 +100,7 @@ class AssetResponseDto {
      other.deviceId == deviceId &&
      other.deviceId == deviceId &&
      other.originalPath == originalPath &&
      other.originalPath == originalPath &&
      other.originalFileName == originalFileName &&
      other.originalFileName == originalFileName &&
-     other.resizePath == resizePath &&
+     other.resized == resized &&
      other.fileCreatedAt == fileCreatedAt &&
      other.fileCreatedAt == fileCreatedAt &&
      other.fileModifiedAt == fileModifiedAt &&
      other.fileModifiedAt == fileModifiedAt &&
      other.updatedAt == updatedAt &&
      other.updatedAt == updatedAt &&
@@ -110,13 +108,12 @@ class AssetResponseDto {
      other.isArchived == isArchived &&
      other.isArchived == isArchived &&
      other.mimeType == mimeType &&
      other.mimeType == mimeType &&
      other.duration == duration &&
      other.duration == duration &&
-     other.webpPath == webpPath &&
-     other.encodedVideoPath == encodedVideoPath &&
      other.exifInfo == exifInfo &&
      other.exifInfo == exifInfo &&
      other.smartInfo == smartInfo &&
      other.smartInfo == smartInfo &&
      other.livePhotoVideoId == livePhotoVideoId &&
      other.livePhotoVideoId == livePhotoVideoId &&
      other.tags == tags &&
      other.tags == tags &&
-     other.people == people;
+     other.people == people &&
+     other.checksum == checksum;
 
 
   @override
   @override
   int get hashCode =>
   int get hashCode =>
@@ -128,7 +125,7 @@ class AssetResponseDto {
     (deviceId.hashCode) +
     (deviceId.hashCode) +
     (originalPath.hashCode) +
     (originalPath.hashCode) +
     (originalFileName.hashCode) +
     (originalFileName.hashCode) +
-    (resizePath == null ? 0 : resizePath!.hashCode) +
+    (resized.hashCode) +
     (fileCreatedAt.hashCode) +
     (fileCreatedAt.hashCode) +
     (fileModifiedAt.hashCode) +
     (fileModifiedAt.hashCode) +
     (updatedAt.hashCode) +
     (updatedAt.hashCode) +
@@ -136,16 +133,15 @@ class AssetResponseDto {
     (isArchived.hashCode) +
     (isArchived.hashCode) +
     (mimeType == null ? 0 : mimeType!.hashCode) +
     (mimeType == null ? 0 : mimeType!.hashCode) +
     (duration.hashCode) +
     (duration.hashCode) +
-    (webpPath == null ? 0 : webpPath!.hashCode) +
-    (encodedVideoPath == null ? 0 : encodedVideoPath!.hashCode) +
     (exifInfo == null ? 0 : exifInfo!.hashCode) +
     (exifInfo == null ? 0 : exifInfo!.hashCode) +
     (smartInfo == null ? 0 : smartInfo!.hashCode) +
     (smartInfo == null ? 0 : smartInfo!.hashCode) +
     (livePhotoVideoId == null ? 0 : livePhotoVideoId!.hashCode) +
     (livePhotoVideoId == null ? 0 : livePhotoVideoId!.hashCode) +
     (tags.hashCode) +
     (tags.hashCode) +
-    (people.hashCode);
+    (people.hashCode) +
+    (checksum.hashCode);
 
 
   @override
   @override
-  String toString() => 'AssetResponseDto[type=$type, id=$id, deviceAssetId=$deviceAssetId, ownerId=$ownerId, deviceId=$deviceId, originalPath=$originalPath, originalFileName=$originalFileName, resizePath=$resizePath, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, updatedAt=$updatedAt, isFavorite=$isFavorite, isArchived=$isArchived, mimeType=$mimeType, duration=$duration, webpPath=$webpPath, encodedVideoPath=$encodedVideoPath, exifInfo=$exifInfo, smartInfo=$smartInfo, livePhotoVideoId=$livePhotoVideoId, tags=$tags, people=$people]';
+  String toString() => 'AssetResponseDto[type=$type, id=$id, deviceAssetId=$deviceAssetId, ownerId=$ownerId, deviceId=$deviceId, originalPath=$originalPath, originalFileName=$originalFileName, resized=$resized, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, updatedAt=$updatedAt, isFavorite=$isFavorite, isArchived=$isArchived, mimeType=$mimeType, duration=$duration, exifInfo=$exifInfo, smartInfo=$smartInfo, livePhotoVideoId=$livePhotoVideoId, tags=$tags, people=$people, checksum=$checksum]';
 
 
   Map<String, dynamic> toJson() {
   Map<String, dynamic> toJson() {
     final json = <String, dynamic>{};
     final json = <String, dynamic>{};
@@ -156,11 +152,7 @@ class AssetResponseDto {
       json[r'deviceId'] = this.deviceId;
       json[r'deviceId'] = this.deviceId;
       json[r'originalPath'] = this.originalPath;
       json[r'originalPath'] = this.originalPath;
       json[r'originalFileName'] = this.originalFileName;
       json[r'originalFileName'] = this.originalFileName;
-    if (this.resizePath != null) {
-      json[r'resizePath'] = this.resizePath;
-    } else {
-      // json[r'resizePath'] = null;
-    }
+      json[r'resized'] = this.resized;
       json[r'fileCreatedAt'] = this.fileCreatedAt;
       json[r'fileCreatedAt'] = this.fileCreatedAt;
       json[r'fileModifiedAt'] = this.fileModifiedAt;
       json[r'fileModifiedAt'] = this.fileModifiedAt;
       json[r'updatedAt'] = this.updatedAt;
       json[r'updatedAt'] = this.updatedAt;
@@ -172,16 +164,6 @@ class AssetResponseDto {
       // json[r'mimeType'] = null;
       // json[r'mimeType'] = null;
     }
     }
       json[r'duration'] = this.duration;
       json[r'duration'] = this.duration;
-    if (this.webpPath != null) {
-      json[r'webpPath'] = this.webpPath;
-    } else {
-      // json[r'webpPath'] = null;
-    }
-    if (this.encodedVideoPath != null) {
-      json[r'encodedVideoPath'] = this.encodedVideoPath;
-    } else {
-      // json[r'encodedVideoPath'] = null;
-    }
     if (this.exifInfo != null) {
     if (this.exifInfo != null) {
       json[r'exifInfo'] = this.exifInfo;
       json[r'exifInfo'] = this.exifInfo;
     } else {
     } else {
@@ -199,6 +181,7 @@ class AssetResponseDto {
     }
     }
       json[r'tags'] = this.tags;
       json[r'tags'] = this.tags;
       json[r'people'] = this.people;
       json[r'people'] = this.people;
+      json[r'checksum'] = this.checksum;
     return json;
     return json;
   }
   }
 
 
@@ -228,7 +211,7 @@ class AssetResponseDto {
         deviceId: mapValueOfType<String>(json, r'deviceId')!,
         deviceId: mapValueOfType<String>(json, r'deviceId')!,
         originalPath: mapValueOfType<String>(json, r'originalPath')!,
         originalPath: mapValueOfType<String>(json, r'originalPath')!,
         originalFileName: mapValueOfType<String>(json, r'originalFileName')!,
         originalFileName: mapValueOfType<String>(json, r'originalFileName')!,
-        resizePath: mapValueOfType<String>(json, r'resizePath'),
+        resized: mapValueOfType<bool>(json, r'resized')!,
         fileCreatedAt: mapValueOfType<String>(json, r'fileCreatedAt')!,
         fileCreatedAt: mapValueOfType<String>(json, r'fileCreatedAt')!,
         fileModifiedAt: mapValueOfType<String>(json, r'fileModifiedAt')!,
         fileModifiedAt: mapValueOfType<String>(json, r'fileModifiedAt')!,
         updatedAt: mapValueOfType<String>(json, r'updatedAt')!,
         updatedAt: mapValueOfType<String>(json, r'updatedAt')!,
@@ -236,13 +219,12 @@ class AssetResponseDto {
         isArchived: mapValueOfType<bool>(json, r'isArchived')!,
         isArchived: mapValueOfType<bool>(json, r'isArchived')!,
         mimeType: mapValueOfType<String>(json, r'mimeType'),
         mimeType: mapValueOfType<String>(json, r'mimeType'),
         duration: mapValueOfType<String>(json, r'duration')!,
         duration: mapValueOfType<String>(json, r'duration')!,
-        webpPath: mapValueOfType<String>(json, r'webpPath'),
-        encodedVideoPath: mapValueOfType<String>(json, r'encodedVideoPath'),
         exifInfo: ExifResponseDto.fromJson(json[r'exifInfo']),
         exifInfo: ExifResponseDto.fromJson(json[r'exifInfo']),
         smartInfo: SmartInfoResponseDto.fromJson(json[r'smartInfo']),
         smartInfo: SmartInfoResponseDto.fromJson(json[r'smartInfo']),
         livePhotoVideoId: mapValueOfType<String>(json, r'livePhotoVideoId'),
         livePhotoVideoId: mapValueOfType<String>(json, r'livePhotoVideoId'),
         tags: TagResponseDto.listFromJson(json[r'tags']),
         tags: TagResponseDto.listFromJson(json[r'tags']),
         people: PersonResponseDto.listFromJson(json[r'people']),
         people: PersonResponseDto.listFromJson(json[r'people']),
+        checksum: mapValueOfType<String>(json, r'checksum')!,
       );
       );
     }
     }
     return null;
     return null;
@@ -297,7 +279,7 @@ class AssetResponseDto {
     'deviceId',
     'deviceId',
     'originalPath',
     'originalPath',
     'originalFileName',
     'originalFileName',
-    'resizePath',
+    'resized',
     'fileCreatedAt',
     'fileCreatedAt',
     'fileModifiedAt',
     'fileModifiedAt',
     'updatedAt',
     'updatedAt',
@@ -305,7 +287,7 @@ class AssetResponseDto {
     'isArchived',
     'isArchived',
     'mimeType',
     'mimeType',
     'duration',
     'duration',
-    'webpPath',
+    'checksum',
   };
   };
 }
 }
 
 

+ 1 - 0
mobile/openapi/test/asset_bulk_upload_check_item_test.dart

@@ -21,6 +21,7 @@ void main() {
       // TODO
       // TODO
     });
     });
 
 
+    // base64 or hex encoded sha1 hash
     // String checksum
     // String checksum
     test('to test the property `checksum`', () async {
     test('to test the property `checksum`', () async {
       // TODO
       // TODO

+ 8 - 12
mobile/openapi/test/asset_response_dto_test.dart

@@ -51,8 +51,8 @@ void main() {
       // TODO
       // TODO
     });
     });
 
 
-    // String resizePath
-    test('to test the property `resizePath`', () async {
+    // bool resized
+    test('to test the property `resized`', () async {
       // TODO
       // TODO
     });
     });
 
 
@@ -91,16 +91,6 @@ void main() {
       // TODO
       // TODO
     });
     });
 
 
-    // String webpPath
-    test('to test the property `webpPath`', () async {
-      // TODO
-    });
-
-    // String encodedVideoPath
-    test('to test the property `encodedVideoPath`', () async {
-      // TODO
-    });
-
     // ExifResponseDto exifInfo
     // ExifResponseDto exifInfo
     test('to test the property `exifInfo`', () async {
     test('to test the property `exifInfo`', () async {
       // TODO
       // TODO
@@ -126,6 +116,12 @@ void main() {
       // TODO
       // TODO
     });
     });
 
 
+    // base64 encoded sha1 hash
+    // String checksum
+    test('to test the property `checksum`', () async {
+      // TODO
+    });
+
 
 
   });
   });
 
 

+ 29 - 0
server/apps/immich/src/api-v1/asset/asset.service.spec.ts

@@ -30,6 +30,7 @@ import {
 import { CreateAssetsShareLinkDto } from './dto/create-asset-shared-link.dto';
 import { CreateAssetsShareLinkDto } from './dto/create-asset-shared-link.dto';
 import { BadRequestException, ForbiddenException } from '@nestjs/common';
 import { BadRequestException, ForbiddenException } from '@nestjs/common';
 import { when } from 'jest-when';
 import { when } from 'jest-when';
+import { AssetRejectReason, AssetUploadAction } from './response-dto/asset-check-response.dto';
 
 
 const _getCreateAssetDto = (): CreateAssetDto => {
 const _getCreateAssetDto = (): CreateAssetDto => {
   const createAssetDto = new CreateAssetDto();
   const createAssetDto = new CreateAssetDto();
@@ -504,4 +505,32 @@ describe('AssetService', () => {
       expect(storageMock.createReadStream).toHaveBeenCalledWith('fake_path/asset_1.jpeg', 'image/jpeg');
       expect(storageMock.createReadStream).toHaveBeenCalledWith('fake_path/asset_1.jpeg', 'image/jpeg');
     });
     });
   });
   });
+
+  describe('bulkUploadCheck', () => {
+    it('should accept hex and base64 checksums', async () => {
+      const file1 = Buffer.from('d2947b871a706081be194569951b7db246907957', 'hex');
+      const file2 = Buffer.from('53be335e99f18a66ff12e9a901c7a6171dd76573', 'hex');
+
+      assetRepositoryMock.getAssetsByChecksums.mockResolvedValue([
+        { id: 'asset-1', checksum: file1 },
+        { id: 'asset-2', checksum: file2 },
+      ]);
+
+      await expect(
+        sut.bulkUploadCheck(authStub.admin, {
+          assets: [
+            { id: '1', checksum: file1.toString('hex') },
+            { id: '2', checksum: file2.toString('base64') },
+          ],
+        }),
+      ).resolves.toEqual({
+        results: [
+          { id: '1', assetId: 'asset-1', action: AssetUploadAction.REJECT, reason: AssetRejectReason.DUPLICATE },
+          { id: '2', assetId: 'asset-2', action: AssetUploadAction.REJECT, reason: AssetRejectReason.DUPLICATE },
+        ],
+      });
+
+      expect(assetRepositoryMock.getAssetsByChecksums).toHaveBeenCalledWith(authStub.admin.id, [file1, file2]);
+    });
+  });
 });
 });

+ 10 - 3
server/apps/immich/src/api-v1/asset/asset.service.ts

@@ -486,17 +486,24 @@ export class AssetService {
   }
   }
 
 
   async bulkUploadCheck(authUser: AuthUserDto, dto: AssetBulkUploadCheckDto): Promise<AssetBulkUploadCheckResponseDto> {
   async bulkUploadCheck(authUser: AuthUserDto, dto: AssetBulkUploadCheckDto): Promise<AssetBulkUploadCheckResponseDto> {
+    // support base64 and hex checksums
+    for (const asset of dto.assets) {
+      if (asset.checksum.length === 28) {
+        asset.checksum = Buffer.from(asset.checksum, 'base64').toString('hex');
+      }
+    }
+
     const checksums: Buffer[] = dto.assets.map((asset) => Buffer.from(asset.checksum, 'hex'));
     const checksums: Buffer[] = dto.assets.map((asset) => Buffer.from(asset.checksum, 'hex'));
     const results = await this._assetRepository.getAssetsByChecksums(authUser.id, checksums);
     const results = await this._assetRepository.getAssetsByChecksums(authUser.id, checksums);
-    const resultsMap: Record<string, string> = {};
+    const checksumMap: Record<string, string> = {};
 
 
     for (const { id, checksum } of results) {
     for (const { id, checksum } of results) {
-      resultsMap[checksum.toString('hex')] = id;
+      checksumMap[checksum.toString('hex')] = id;
     }
     }
 
 
     return {
     return {
       results: dto.assets.map(({ id, checksum }) => {
       results: dto.assets.map(({ id, checksum }) => {
-        const duplicate = resultsMap[checksum];
+        const duplicate = checksumMap[checksum];
         if (duplicate) {
         if (duplicate) {
           return {
           return {
             id,
             id,

+ 1 - 0
server/apps/immich/src/api-v1/asset/dto/asset-check.dto.ts

@@ -6,6 +6,7 @@ export class AssetBulkUploadCheckItem {
   @IsNotEmpty()
   @IsNotEmpty()
   id!: string;
   id!: string;
 
 
+  /** base64 or hex encoded sha1 hash */
   @IsString()
   @IsString()
   @IsNotEmpty()
   @IsNotEmpty()
   checksum!: string;
   checksum!: string;

+ 10 - 14
server/immich-openapi-specs.json

@@ -4446,9 +4446,8 @@
           "originalFileName": {
           "originalFileName": {
             "type": "string"
             "type": "string"
           },
           },
-          "resizePath": {
-            "type": "string",
-            "nullable": true
+          "resized": {
+            "type": "boolean"
           },
           },
           "fileCreatedAt": {
           "fileCreatedAt": {
             "type": "string"
             "type": "string"
@@ -4472,14 +4471,6 @@
           "duration": {
           "duration": {
             "type": "string"
             "type": "string"
           },
           },
-          "webpPath": {
-            "type": "string",
-            "nullable": true
-          },
-          "encodedVideoPath": {
-            "type": "string",
-            "nullable": true
-          },
           "exifInfo": {
           "exifInfo": {
             "$ref": "#/components/schemas/ExifResponseDto"
             "$ref": "#/components/schemas/ExifResponseDto"
           },
           },
@@ -4501,6 +4492,10 @@
             "items": {
             "items": {
               "$ref": "#/components/schemas/PersonResponseDto"
               "$ref": "#/components/schemas/PersonResponseDto"
             }
             }
+          },
+          "checksum": {
+            "type": "string",
+            "description": "base64 encoded sha1 hash"
           }
           }
         },
         },
         "required": [
         "required": [
@@ -4511,7 +4506,7 @@
           "deviceId",
           "deviceId",
           "originalPath",
           "originalPath",
           "originalFileName",
           "originalFileName",
-          "resizePath",
+          "resized",
           "fileCreatedAt",
           "fileCreatedAt",
           "fileModifiedAt",
           "fileModifiedAt",
           "updatedAt",
           "updatedAt",
@@ -4519,7 +4514,7 @@
           "isArchived",
           "isArchived",
           "mimeType",
           "mimeType",
           "duration",
           "duration",
-          "webpPath"
+          "checksum"
         ]
         ]
       },
       },
       "AlbumResponseDto": {
       "AlbumResponseDto": {
@@ -6173,7 +6168,8 @@
             "type": "string"
             "type": "string"
           },
           },
           "checksum": {
           "checksum": {
-            "type": "string"
+            "type": "string",
+            "description": "base64 or hex encoded sha1 hash"
           }
           }
         },
         },
         "required": [
         "required": [

+ 2 - 1
server/libs/domain/src/album/album.service.ts

@@ -1,6 +1,6 @@
 import { AlbumEntity, AssetEntity, UserEntity } from '@app/infra/entities';
 import { AlbumEntity, AssetEntity, UserEntity } from '@app/infra/entities';
 import { BadRequestException, ForbiddenException, Inject, Injectable } from '@nestjs/common';
 import { BadRequestException, ForbiddenException, Inject, Injectable } from '@nestjs/common';
-import { IAssetRepository } from '../asset';
+import { IAssetRepository, mapAsset } from '../asset';
 import { AuthUserDto } from '../auth';
 import { AuthUserDto } from '../auth';
 import { IJobRepository, JobName } from '../job';
 import { IJobRepository, JobName } from '../job';
 import { IAlbumRepository } from './album.repository';
 import { IAlbumRepository } from './album.repository';
@@ -40,6 +40,7 @@ export class AlbumService {
     return albums.map((album) => {
     return albums.map((album) => {
       return {
       return {
         ...album,
         ...album,
+        assets: album?.assets?.map(mapAsset),
         sharedLinks: undefined, // Don't return shared links
         sharedLinks: undefined, // Don't return shared links
         shared: album.sharedLinks?.length > 0 || album.sharedUsers?.length > 0,
         shared: album.sharedLinks?.length > 0 || album.sharedUsers?.length > 0,
         assetCount: albumsAssetCountObj[album.id],
         assetCount: albumsAssetCountObj[album.id],

+ 7 - 9
server/libs/domain/src/asset/response-dto/asset-response.dto.ts

@@ -15,7 +15,7 @@ export class AssetResponseDto {
   type!: AssetType;
   type!: AssetType;
   originalPath!: string;
   originalPath!: string;
   originalFileName!: string;
   originalFileName!: string;
-  resizePath!: string | null;
+  resized!: boolean;
   fileCreatedAt!: string;
   fileCreatedAt!: string;
   fileModifiedAt!: string;
   fileModifiedAt!: string;
   updatedAt!: string;
   updatedAt!: string;
@@ -23,13 +23,13 @@ export class AssetResponseDto {
   isArchived!: boolean;
   isArchived!: boolean;
   mimeType!: string | null;
   mimeType!: string | null;
   duration!: string;
   duration!: string;
-  webpPath!: string | null;
-  encodedVideoPath?: string | null;
   exifInfo?: ExifResponseDto;
   exifInfo?: ExifResponseDto;
   smartInfo?: SmartInfoResponseDto;
   smartInfo?: SmartInfoResponseDto;
   livePhotoVideoId?: string | null;
   livePhotoVideoId?: string | null;
   tags?: TagResponseDto[];
   tags?: TagResponseDto[];
   people?: PersonResponseDto[];
   people?: PersonResponseDto[];
+  /**base64 encoded sha1 hash */
+  checksum!: string;
 }
 }
 
 
 export function mapAsset(entity: AssetEntity): AssetResponseDto {
 export function mapAsset(entity: AssetEntity): AssetResponseDto {
@@ -41,21 +41,20 @@ export function mapAsset(entity: AssetEntity): AssetResponseDto {
     type: entity.type,
     type: entity.type,
     originalPath: entity.originalPath,
     originalPath: entity.originalPath,
     originalFileName: entity.originalFileName,
     originalFileName: entity.originalFileName,
-    resizePath: entity.resizePath,
+    resized: !!entity.resizePath,
     fileCreatedAt: entity.fileCreatedAt,
     fileCreatedAt: entity.fileCreatedAt,
     fileModifiedAt: entity.fileModifiedAt,
     fileModifiedAt: entity.fileModifiedAt,
     updatedAt: entity.updatedAt,
     updatedAt: entity.updatedAt,
     isFavorite: entity.isFavorite,
     isFavorite: entity.isFavorite,
     isArchived: entity.isArchived,
     isArchived: entity.isArchived,
     mimeType: entity.mimeType,
     mimeType: entity.mimeType,
-    webpPath: entity.webpPath,
-    encodedVideoPath: entity.encodedVideoPath,
     duration: entity.duration ?? '0:00:00.00000',
     duration: entity.duration ?? '0:00:00.00000',
     exifInfo: entity.exifInfo ? mapExif(entity.exifInfo) : undefined,
     exifInfo: entity.exifInfo ? mapExif(entity.exifInfo) : undefined,
     smartInfo: entity.smartInfo ? mapSmartInfo(entity.smartInfo) : undefined,
     smartInfo: entity.smartInfo ? mapSmartInfo(entity.smartInfo) : undefined,
     livePhotoVideoId: entity.livePhotoVideoId,
     livePhotoVideoId: entity.livePhotoVideoId,
     tags: entity.tags?.map(mapTag),
     tags: entity.tags?.map(mapTag),
     people: entity.faces?.map(mapFace),
     people: entity.faces?.map(mapFace),
+    checksum: entity.checksum.toString('base64'),
   };
   };
 }
 }
 
 
@@ -68,20 +67,19 @@ export function mapAssetWithoutExif(entity: AssetEntity): AssetResponseDto {
     type: entity.type,
     type: entity.type,
     originalPath: entity.originalPath,
     originalPath: entity.originalPath,
     originalFileName: entity.originalFileName,
     originalFileName: entity.originalFileName,
-    resizePath: entity.resizePath,
+    resized: !!entity.resizePath,
     fileCreatedAt: entity.fileCreatedAt,
     fileCreatedAt: entity.fileCreatedAt,
     fileModifiedAt: entity.fileModifiedAt,
     fileModifiedAt: entity.fileModifiedAt,
     updatedAt: entity.updatedAt,
     updatedAt: entity.updatedAt,
     isFavorite: entity.isFavorite,
     isFavorite: entity.isFavorite,
     isArchived: entity.isArchived,
     isArchived: entity.isArchived,
     mimeType: entity.mimeType,
     mimeType: entity.mimeType,
-    webpPath: entity.webpPath,
-    encodedVideoPath: entity.encodedVideoPath,
     duration: entity.duration ?? '0:00:00.00000',
     duration: entity.duration ?? '0:00:00.00000',
     exifInfo: undefined,
     exifInfo: undefined,
     smartInfo: entity.smartInfo ? mapSmartInfo(entity.smartInfo) : undefined,
     smartInfo: entity.smartInfo ? mapSmartInfo(entity.smartInfo) : undefined,
     livePhotoVideoId: entity.livePhotoVideoId,
     livePhotoVideoId: entity.livePhotoVideoId,
     tags: entity.tags?.map(mapTag),
     tags: entity.tags?.map(mapTag),
     people: entity.faces?.map(mapFace),
     people: entity.faces?.map(mapFace),
+    checksum: entity.checksum.toString('base64'),
   };
   };
 }
 }

+ 7 - 3
server/libs/domain/src/search/search.service.ts

@@ -3,7 +3,7 @@ import { BadRequestException, Inject, Injectable, Logger } from '@nestjs/common'
 import { ConfigService } from '@nestjs/config';
 import { ConfigService } from '@nestjs/config';
 import { mapAlbum } from '../album';
 import { mapAlbum } from '../album';
 import { IAlbumRepository } from '../album/album.repository';
 import { IAlbumRepository } from '../album/album.repository';
-import { mapAsset } from '../asset';
+import { AssetResponseDto, mapAsset } from '../asset';
 import { IAssetRepository } from '../asset/asset.repository';
 import { IAssetRepository } from '../asset/asset.repository';
 import { AuthUserDto } from '../auth';
 import { AuthUserDto } from '../auth';
 import { MACHINE_LEARNING_ENABLED } from '../domain.constant';
 import { MACHINE_LEARNING_ENABLED } from '../domain.constant';
@@ -103,9 +103,13 @@ export class SearchService {
     }
     }
   }
   }
 
 
-  async getExploreData(authUser: AuthUserDto): Promise<SearchExploreItem<AssetEntity>[]> {
+  async getExploreData(authUser: AuthUserDto): Promise<SearchExploreItem<AssetResponseDto>[]> {
     this.assertEnabled();
     this.assertEnabled();
-    return this.searchRepository.explore(authUser.id);
+    const results = await this.searchRepository.explore(authUser.id);
+    return results.map(({ fieldName, items }) => ({
+      fieldName,
+      items: items.map(({ value, data }) => ({ value, data: mapAsset(data) })),
+    }));
   }
   }
 
 
   async search(authUser: AuthUserDto, dto: SearchDto): Promise<SearchResponseDto> {
   async search(authUser: AuthUserDto, dto: SearchDto): Promise<SearchResponseDto> {

+ 2 - 3
server/libs/domain/test/fixtures.ts

@@ -446,7 +446,7 @@ const assetResponse: AssetResponseDto = {
   type: AssetType.VIDEO,
   type: AssetType.VIDEO,
   originalPath: 'fake_path/jpeg',
   originalPath: 'fake_path/jpeg',
   originalFileName: 'asset_1.jpeg',
   originalFileName: 'asset_1.jpeg',
-  resizePath: '',
+  resized: false,
   fileModifiedAt: today.toISOString(),
   fileModifiedAt: today.toISOString(),
   fileCreatedAt: today.toISOString(),
   fileCreatedAt: today.toISOString(),
   updatedAt: today.toISOString(),
   updatedAt: today.toISOString(),
@@ -457,13 +457,12 @@ const assetResponse: AssetResponseDto = {
     tags: [],
     tags: [],
     objects: ['a', 'b', 'c'],
     objects: ['a', 'b', 'c'],
   },
   },
-  webpPath: '',
-  encodedVideoPath: '',
   duration: '0:00:00.00000',
   duration: '0:00:00.00000',
   exifInfo: assetInfo,
   exifInfo: assetInfo,
   livePhotoVideoId: null,
   livePhotoVideoId: null,
   tags: [],
   tags: [],
   people: [],
   people: [],
+  checksum: 'ZmlsZSBoYXNo',
 };
 };
 
 
 const albumResponse: AlbumResponseDto = {
 const albumResponse: AlbumResponseDto = {

+ 9 - 15
web/src/api/open-api/api.ts

@@ -378,7 +378,7 @@ export interface AssetBulkUploadCheckItem {
      */
      */
     'id': string;
     'id': string;
     /**
     /**
-     * 
+     * base64 or hex encoded sha1 hash
      * @type {string}
      * @type {string}
      * @memberof AssetBulkUploadCheckItem
      * @memberof AssetBulkUploadCheckItem
      */
      */
@@ -586,10 +586,10 @@ export interface AssetResponseDto {
     'originalFileName': string;
     'originalFileName': string;
     /**
     /**
      * 
      * 
-     * @type {string}
+     * @type {boolean}
      * @memberof AssetResponseDto
      * @memberof AssetResponseDto
      */
      */
-    'resizePath': string | null;
+    'resized': boolean;
     /**
     /**
      * 
      * 
      * @type {string}
      * @type {string}
@@ -632,18 +632,6 @@ export interface AssetResponseDto {
      * @memberof AssetResponseDto
      * @memberof AssetResponseDto
      */
      */
     'duration': string;
     'duration': string;
-    /**
-     * 
-     * @type {string}
-     * @memberof AssetResponseDto
-     */
-    'webpPath': string | null;
-    /**
-     * 
-     * @type {string}
-     * @memberof AssetResponseDto
-     */
-    'encodedVideoPath'?: string | null;
     /**
     /**
      * 
      * 
      * @type {ExifResponseDto}
      * @type {ExifResponseDto}
@@ -674,6 +662,12 @@ export interface AssetResponseDto {
      * @memberof AssetResponseDto
      * @memberof AssetResponseDto
      */
      */
     'people'?: Array<PersonResponseDto>;
     'people'?: Array<PersonResponseDto>;
+    /**
+     * base64 encoded sha1 hash
+     * @type {string}
+     * @memberof AssetResponseDto
+     */
+    'checksum': string;
 }
 }
 
 
 
 

+ 1 - 1
web/src/lib/components/asset-viewer/asset-viewer.svelte

@@ -350,7 +350,7 @@
 
 
 	<div class="row-start-1 row-span-full col-start-1 col-span-4">
 	<div class="row-start-1 row-span-full col-start-1 col-span-4">
 		{#key asset.id}
 		{#key asset.id}
-			{#if !asset.resizePath}
+			{#if !asset.resized}
 				<div class="h-full w-full flex justify-center">
 				<div class="h-full w-full flex justify-center">
 					<div
 					<div
 						class="h-full bg-gray-100 dark:bg-immich-dark-gray flex items-center justify-center aspect-square px-auto"
 						class="h-full bg-gray-100 dark:bg-immich-dark-gray flex items-center justify-center aspect-square px-auto"

+ 1 - 1
web/src/lib/components/assets/thumbnail/thumbnail.svelte

@@ -123,7 +123,7 @@
 					</div>
 					</div>
 				{/if}
 				{/if}
 
 
-				{#if asset.resizePath}
+				{#if asset.resized}
 					<ImageThumbnail
 					<ImageThumbnail
 						url={api.getAssetThumbnailUrl(asset.id, format, publicSharedKey)}
 						url={api.getAssetThumbnailUrl(asset.id, format, publicSharedKey)}
 						altText={asset.originalFileName}
 						altText={asset.originalFileName}