Bläddra i källkod

refactor(server): download file (#1512)

* refactor(server): download file

* chore: generate open-api and remove unused refs

* chore(server): tests

* chore: remove unused code
Jason Rasmussen 2 år sedan
förälder
incheckning
2b0b2bb1ae
29 ändrade filer med 210 tillägg och 291 borttagningar
  1. 0 6
      mobile/lib/modules/asset_viewer/services/image_viewer.service.dart
  2. 0 2
      mobile/lib/shared/services/share.service.dart
  3. 1 1
      mobile/openapi/README.md
  4. 2 6
      mobile/openapi/doc/AssetApi.md
  5. 3 18
      mobile/openapi/lib/api/asset_api.dart
  6. 41 53
      mobile/openapi/lib/model/album_response_dto.dart
  7. 61 72
      mobile/openapi/lib/model/asset_response_dto.dart
  8. 1 1
      mobile/openapi/test/asset_api_test.dart
  9. 7 5
      server/apps/immich/src/api-v1/asset/asset.controller.ts
  10. 16 1
      server/apps/immich/src/api-v1/asset/asset.service.spec.ts
  11. 19 66
      server/apps/immich/src/api-v1/asset/asset.service.ts
  12. 0 18
      server/immich-openapi-specs.json
  13. 0 5
      server/libs/domain/src/auth/auth.core.ts
  14. 0 1
      server/libs/domain/src/auth/dto/index.ts
  15. 0 4
      server/libs/domain/src/auth/dto/jwt-payload.dto.ts
  16. 1 0
      server/libs/domain/src/index.ts
  17. 1 0
      server/libs/domain/src/storage/index.ts
  18. 13 0
      server/libs/domain/src/storage/storage.repository.ts
  19. 1 0
      server/libs/domain/test/index.ts
  20. 7 0
      server/libs/domain/test/storage.repository.mock.ts
  21. 3 0
      server/libs/infra/src/infra.module.ts
  22. 18 0
      server/libs/infra/src/storage/filesystem.provider.ts
  23. 1 0
      server/libs/infra/src/storage/index.ts
  24. 8 24
      web/src/api/open-api/api.ts
  25. 1 1
      web/src/api/open-api/base.ts
  26. 1 1
      web/src/api/open-api/common.ts
  27. 1 1
      web/src/api/open-api/configuration.ts
  28. 1 1
      web/src/api/open-api/index.ts
  29. 2 4
      web/src/lib/components/asset-viewer/asset-viewer.svelte

+ 0 - 6
mobile/lib/modules/asset_viewer/services/image_viewer.service.dart

@@ -26,14 +26,10 @@ class ImageViewerService {
       if (asset.type == AssetTypeEnum.IMAGE && asset.livePhotoVideoId != null) {
         var imageResponse = await _apiService.assetApi.downloadFileWithHttpInfo(
           asset.id,
-          isThumb: false,
-          isWeb: false,
         );
 
         var motionReponse = await _apiService.assetApi.downloadFileWithHttpInfo(
           asset.livePhotoVideoId!,
-          isThumb: false,
-          isWeb: false,
         );
 
         final AssetEntity? entity;
@@ -54,8 +50,6 @@ class ImageViewerService {
       } else {
         var res = await _apiService.assetApi.downloadFileWithHttpInfo(
           asset.id,
-          isThumb: false,
-          isWeb: false,
         );
 
         final AssetEntity? entity;

+ 0 - 2
mobile/lib/shared/services/share.service.dart

@@ -29,8 +29,6 @@ class ShareService {
         final tempFile = await File('${tempDir.path}/$fileName').create();
         final res = await _apiService.assetApi.downloadFileWithHttpInfo(
           asset.remote!.id,
-          isThumb: false,
-          isWeb: false,
         );
         tempFile.writeAsBytesSync(res.bodyBytes);
         return XFile(tempFile.path);

+ 1 - 1
mobile/openapi/README.md

@@ -3,7 +3,7 @@ Immich API
 
 This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
 
-- API version: 1.43.0
+- API version: 1.43.1
 - Build package: org.openapitools.codegen.languages.DartClientCodegen
 
 ## Requirements

+ 2 - 6
mobile/openapi/doc/AssetApi.md

@@ -230,7 +230,7 @@ Name | Type | Description  | Notes
 [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
 
 # **downloadFile**
-> Object downloadFile(assetId, isThumb, isWeb)
+> Object downloadFile(assetId)
 
 
 
@@ -248,11 +248,9 @@ import 'package:openapi/api.dart';
 
 final api_instance = AssetApi();
 final assetId = assetId_example; // String | 
-final isThumb = true; // bool | 
-final isWeb = true; // bool | 
 
 try {
-    final result = api_instance.downloadFile(assetId, isThumb, isWeb);
+    final result = api_instance.downloadFile(assetId);
     print(result);
 } catch (e) {
     print('Exception when calling AssetApi->downloadFile: $e\n');
@@ -264,8 +262,6 @@ try {
 Name | Type | Description  | Notes
 ------------- | ------------- | ------------- | -------------
  **assetId** | **String**|  | 
- **isThumb** | **bool**|  | [optional] 
- **isWeb** | **bool**|  | [optional] 
 
 ### Return type
 

+ 3 - 18
mobile/openapi/lib/api/asset_api.dart

@@ -234,11 +234,7 @@ class AssetApi {
   /// Parameters:
   ///
   /// * [String] assetId (required):
-  ///
-  /// * [bool] isThumb:
-  ///
-  /// * [bool] isWeb:
-  Future<Response> downloadFileWithHttpInfo(String assetId, { bool? isThumb, bool? isWeb, }) async {
+  Future<Response> downloadFileWithHttpInfo(String assetId,) async {
     // ignore: prefer_const_declarations
     final path = r'/asset/download/{assetId}'
       .replaceAll('{assetId}', assetId);
@@ -250,13 +246,6 @@ class AssetApi {
     final headerParams = <String, String>{};
     final formParams = <String, String>{};
 
-    if (isThumb != null) {
-      queryParams.addAll(_queryParams('', 'isThumb', isThumb));
-    }
-    if (isWeb != null) {
-      queryParams.addAll(_queryParams('', 'isWeb', isWeb));
-    }
-
     const contentTypes = <String>[];
 
 
@@ -276,12 +265,8 @@ class AssetApi {
   /// Parameters:
   ///
   /// * [String] assetId (required):
-  ///
-  /// * [bool] isThumb:
-  ///
-  /// * [bool] isWeb:
-  Future<Object?> downloadFile(String assetId, { bool? isThumb, bool? isWeb, }) async {
-    final response = await downloadFileWithHttpInfo(assetId,  isThumb: isThumb, isWeb: isWeb, );
+  Future<Object?> downloadFile(String assetId,) async {
+    final response = await downloadFileWithHttpInfo(assetId,);
     if (response.statusCode >= HttpStatus.badRequest) {
       throw ApiException(response.statusCode, await _decodeBodyBytes(response));
     }

+ 41 - 53
mobile/openapi/lib/model/album_response_dto.dart

@@ -43,51 +43,48 @@ class AlbumResponseDto {
   List<AssetResponseDto> assets;
 
   @override
-  bool operator ==(Object other) =>
-      identical(this, other) ||
-      other is AlbumResponseDto &&
-          other.assetCount == assetCount &&
-          other.id == id &&
-          other.ownerId == ownerId &&
-          other.albumName == albumName &&
-          other.createdAt == createdAt &&
-          other.albumThumbnailAssetId == albumThumbnailAssetId &&
-          other.shared == shared &&
-          other.sharedUsers == sharedUsers &&
-          other.assets == assets;
+  bool operator ==(Object other) => identical(this, other) || other is AlbumResponseDto &&
+     other.assetCount == assetCount &&
+     other.id == id &&
+     other.ownerId == ownerId &&
+     other.albumName == albumName &&
+     other.createdAt == createdAt &&
+     other.albumThumbnailAssetId == albumThumbnailAssetId &&
+     other.shared == shared &&
+     other.sharedUsers == sharedUsers &&
+     other.assets == assets;
 
   @override
   int get hashCode =>
-      // ignore: unnecessary_parenthesis
-      (assetCount.hashCode) +
-      (id.hashCode) +
-      (ownerId.hashCode) +
-      (albumName.hashCode) +
-      (createdAt.hashCode) +
-      (albumThumbnailAssetId == null ? 0 : albumThumbnailAssetId!.hashCode) +
-      (shared.hashCode) +
-      (sharedUsers.hashCode) +
-      (assets.hashCode);
+    // ignore: unnecessary_parenthesis
+    (assetCount.hashCode) +
+    (id.hashCode) +
+    (ownerId.hashCode) +
+    (albumName.hashCode) +
+    (createdAt.hashCode) +
+    (albumThumbnailAssetId == null ? 0 : albumThumbnailAssetId!.hashCode) +
+    (shared.hashCode) +
+    (sharedUsers.hashCode) +
+    (assets.hashCode);
 
   @override
-  String toString() =>
-      'AlbumResponseDto[assetCount=$assetCount, id=$id, ownerId=$ownerId, albumName=$albumName, createdAt=$createdAt, albumThumbnailAssetId=$albumThumbnailAssetId, shared=$shared, sharedUsers=$sharedUsers, assets=$assets]';
+  String toString() => 'AlbumResponseDto[assetCount=$assetCount, id=$id, ownerId=$ownerId, albumName=$albumName, createdAt=$createdAt, albumThumbnailAssetId=$albumThumbnailAssetId, shared=$shared, sharedUsers=$sharedUsers, assets=$assets]';
 
   Map<String, dynamic> toJson() {
     final json = <String, dynamic>{};
-    json[r'assetCount'] = this.assetCount;
-    json[r'id'] = this.id;
-    json[r'ownerId'] = this.ownerId;
-    json[r'albumName'] = this.albumName;
-    json[r'createdAt'] = this.createdAt;
+      json[r'assetCount'] = this.assetCount;
+      json[r'id'] = this.id;
+      json[r'ownerId'] = this.ownerId;
+      json[r'albumName'] = this.albumName;
+      json[r'createdAt'] = this.createdAt;
     if (this.albumThumbnailAssetId != null) {
       json[r'albumThumbnailAssetId'] = this.albumThumbnailAssetId;
     } else {
       // json[r'albumThumbnailAssetId'] = null;
     }
-    json[r'shared'] = this.shared;
-    json[r'sharedUsers'] = this.sharedUsers;
-    json[r'assets'] = this.assets;
+      json[r'shared'] = this.shared;
+      json[r'sharedUsers'] = this.sharedUsers;
+      json[r'assets'] = this.assets;
     return json;
   }
 
@@ -101,13 +98,13 @@ class AlbumResponseDto {
       // Ensure that the map contains the required keys.
       // Note 1: the values aren't checked for validity beyond being non-null.
       // Note 2: this code is stripped in release mode!
-      // assert(() {
-      //   requiredKeys.forEach((key) {
-      //     assert(json.containsKey(key), 'Required key "AlbumResponseDto[$key]" is missing from JSON.');
-      //     assert(json[key] != null, 'Required key "AlbumResponseDto[$key]" has a null value in JSON.');
-      //   });
-      //   return true;
-      // }());
+      assert(() {
+        requiredKeys.forEach((key) {
+          assert(json.containsKey(key), 'Required key "AlbumResponseDto[$key]" is missing from JSON.');
+          assert(json[key] != null, 'Required key "AlbumResponseDto[$key]" has a null value in JSON.');
+        });
+        return true;
+      }());
 
       return AlbumResponseDto(
         assetCount: mapValueOfType<int>(json, r'assetCount')!,
@@ -115,8 +112,7 @@ class AlbumResponseDto {
         ownerId: mapValueOfType<String>(json, r'ownerId')!,
         albumName: mapValueOfType<String>(json, r'albumName')!,
         createdAt: mapValueOfType<String>(json, r'createdAt')!,
-        albumThumbnailAssetId:
-            mapValueOfType<String>(json, r'albumThumbnailAssetId'),
+        albumThumbnailAssetId: mapValueOfType<String>(json, r'albumThumbnailAssetId'),
         shared: mapValueOfType<bool>(json, r'shared')!,
         sharedUsers: UserResponseDto.listFromJson(json[r'sharedUsers'])!,
         assets: AssetResponseDto.listFromJson(json[r'assets'])!,
@@ -125,10 +121,7 @@ class AlbumResponseDto {
     return null;
   }
 
-  static List<AlbumResponseDto>? listFromJson(
-    dynamic json, {
-    bool growable = false,
-  }) {
+  static List<AlbumResponseDto>? listFromJson(dynamic json, {bool growable = false,}) {
     final result = <AlbumResponseDto>[];
     if (json is List && json.isNotEmpty) {
       for (final row in json) {
@@ -156,18 +149,12 @@ class AlbumResponseDto {
   }
 
   // maps a json object with a list of AlbumResponseDto-objects as value to a dart map
-  static Map<String, List<AlbumResponseDto>> mapListFromJson(
-    dynamic json, {
-    bool growable = false,
-  }) {
+  static Map<String, List<AlbumResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
     final map = <String, List<AlbumResponseDto>>{};
     if (json is Map && json.isNotEmpty) {
       json = json.cast<String, dynamic>(); // ignore: parameter_assignments
       for (final entry in json.entries) {
-        final value = AlbumResponseDto.listFromJson(
-          entry.value,
-          growable: growable,
-        );
+        final value = AlbumResponseDto.listFromJson(entry.value, growable: growable,);
         if (value != null) {
           map[entry.key] = value;
         }
@@ -189,3 +176,4 @@ class AlbumResponseDto {
     'assets',
   };
 }
+

+ 61 - 72
mobile/openapi/lib/model/asset_response_dto.dart

@@ -82,76 +82,73 @@ class AssetResponseDto {
   List<TagResponseDto> tags;
 
   @override
-  bool operator ==(Object other) =>
-      identical(this, other) ||
-      other is AssetResponseDto &&
-          other.type == type &&
-          other.id == id &&
-          other.deviceAssetId == deviceAssetId &&
-          other.ownerId == ownerId &&
-          other.deviceId == deviceId &&
-          other.originalPath == originalPath &&
-          other.resizePath == resizePath &&
-          other.createdAt == createdAt &&
-          other.modifiedAt == modifiedAt &&
-          other.isFavorite == isFavorite &&
-          other.mimeType == mimeType &&
-          other.duration == duration &&
-          other.webpPath == webpPath &&
-          other.encodedVideoPath == encodedVideoPath &&
-          other.exifInfo == exifInfo &&
-          other.smartInfo == smartInfo &&
-          other.livePhotoVideoId == livePhotoVideoId &&
-          other.tags == tags;
+  bool operator ==(Object other) => identical(this, other) || other is AssetResponseDto &&
+     other.type == type &&
+     other.id == id &&
+     other.deviceAssetId == deviceAssetId &&
+     other.ownerId == ownerId &&
+     other.deviceId == deviceId &&
+     other.originalPath == originalPath &&
+     other.resizePath == resizePath &&
+     other.createdAt == createdAt &&
+     other.modifiedAt == modifiedAt &&
+     other.isFavorite == isFavorite &&
+     other.mimeType == mimeType &&
+     other.duration == duration &&
+     other.webpPath == webpPath &&
+     other.encodedVideoPath == encodedVideoPath &&
+     other.exifInfo == exifInfo &&
+     other.smartInfo == smartInfo &&
+     other.livePhotoVideoId == livePhotoVideoId &&
+     other.tags == tags;
 
   @override
   int get hashCode =>
-      // ignore: unnecessary_parenthesis
-      (type.hashCode) +
-      (id.hashCode) +
-      (deviceAssetId.hashCode) +
-      (ownerId.hashCode) +
-      (deviceId.hashCode) +
-      (originalPath.hashCode) +
-      (resizePath == null ? 0 : resizePath!.hashCode) +
-      (createdAt.hashCode) +
-      (modifiedAt.hashCode) +
-      (isFavorite.hashCode) +
-      (mimeType == null ? 0 : mimeType!.hashCode) +
-      (duration.hashCode) +
-      (webpPath == null ? 0 : webpPath!.hashCode) +
-      (encodedVideoPath == null ? 0 : encodedVideoPath!.hashCode) +
-      (exifInfo == null ? 0 : exifInfo!.hashCode) +
-      (smartInfo == null ? 0 : smartInfo!.hashCode) +
-      (livePhotoVideoId == null ? 0 : livePhotoVideoId!.hashCode) +
-      (tags.hashCode);
+    // ignore: unnecessary_parenthesis
+    (type.hashCode) +
+    (id.hashCode) +
+    (deviceAssetId.hashCode) +
+    (ownerId.hashCode) +
+    (deviceId.hashCode) +
+    (originalPath.hashCode) +
+    (resizePath == null ? 0 : resizePath!.hashCode) +
+    (createdAt.hashCode) +
+    (modifiedAt.hashCode) +
+    (isFavorite.hashCode) +
+    (mimeType == null ? 0 : mimeType!.hashCode) +
+    (duration.hashCode) +
+    (webpPath == null ? 0 : webpPath!.hashCode) +
+    (encodedVideoPath == null ? 0 : encodedVideoPath!.hashCode) +
+    (exifInfo == null ? 0 : exifInfo!.hashCode) +
+    (smartInfo == null ? 0 : smartInfo!.hashCode) +
+    (livePhotoVideoId == null ? 0 : livePhotoVideoId!.hashCode) +
+    (tags.hashCode);
 
   @override
-  String toString() =>
-      'AssetResponseDto[type=$type, id=$id, deviceAssetId=$deviceAssetId, ownerId=$ownerId, deviceId=$deviceId, originalPath=$originalPath, resizePath=$resizePath, createdAt=$createdAt, modifiedAt=$modifiedAt, isFavorite=$isFavorite, mimeType=$mimeType, duration=$duration, webpPath=$webpPath, encodedVideoPath=$encodedVideoPath, exifInfo=$exifInfo, smartInfo=$smartInfo, livePhotoVideoId=$livePhotoVideoId, tags=$tags]';
+  String toString() => 'AssetResponseDto[type=$type, id=$id, deviceAssetId=$deviceAssetId, ownerId=$ownerId, deviceId=$deviceId, originalPath=$originalPath, resizePath=$resizePath, createdAt=$createdAt, modifiedAt=$modifiedAt, isFavorite=$isFavorite, mimeType=$mimeType, duration=$duration, webpPath=$webpPath, encodedVideoPath=$encodedVideoPath, exifInfo=$exifInfo, smartInfo=$smartInfo, livePhotoVideoId=$livePhotoVideoId, tags=$tags]';
 
   Map<String, dynamic> toJson() {
     final json = <String, dynamic>{};
-    json[r'type'] = this.type;
-    json[r'id'] = this.id;
-    json[r'deviceAssetId'] = this.deviceAssetId;
-    json[r'ownerId'] = this.ownerId;
-    json[r'deviceId'] = this.deviceId;
-    json[r'originalPath'] = this.originalPath;
+      json[r'type'] = this.type;
+      json[r'id'] = this.id;
+      json[r'deviceAssetId'] = this.deviceAssetId;
+      json[r'ownerId'] = this.ownerId;
+      json[r'deviceId'] = this.deviceId;
+      json[r'originalPath'] = this.originalPath;
     if (this.resizePath != null) {
       json[r'resizePath'] = this.resizePath;
     } else {
       // json[r'resizePath'] = null;
     }
-    json[r'createdAt'] = this.createdAt;
-    json[r'modifiedAt'] = this.modifiedAt;
-    json[r'isFavorite'] = this.isFavorite;
+      json[r'createdAt'] = this.createdAt;
+      json[r'modifiedAt'] = this.modifiedAt;
+      json[r'isFavorite'] = this.isFavorite;
     if (this.mimeType != null) {
       json[r'mimeType'] = this.mimeType;
     } else {
       // json[r'mimeType'] = null;
     }
-    json[r'duration'] = this.duration;
+      json[r'duration'] = this.duration;
     if (this.webpPath != null) {
       json[r'webpPath'] = this.webpPath;
     } else {
@@ -177,7 +174,7 @@ class AssetResponseDto {
     } else {
       // json[r'livePhotoVideoId'] = null;
     }
-    json[r'tags'] = this.tags;
+      json[r'tags'] = this.tags;
     return json;
   }
 
@@ -191,13 +188,13 @@ class AssetResponseDto {
       // Ensure that the map contains the required keys.
       // Note 1: the values aren't checked for validity beyond being non-null.
       // Note 2: this code is stripped in release mode!
-      // assert(() {
-      //   requiredKeys.forEach((key) {
-      //     assert(json.containsKey(key), 'Required key "AssetResponseDto[$key]" is missing from JSON.');
-      //     assert(json[key] != null, 'Required key "AssetResponseDto[$key]" has a null value in JSON.');
-      //   });
-      //   return true;
-      // }());
+      assert(() {
+        requiredKeys.forEach((key) {
+          assert(json.containsKey(key), 'Required key "AssetResponseDto[$key]" is missing from JSON.');
+          assert(json[key] != null, 'Required key "AssetResponseDto[$key]" has a null value in JSON.');
+        });
+        return true;
+      }());
 
       return AssetResponseDto(
         type: AssetTypeEnum.fromJson(json[r'type'])!,
@@ -223,10 +220,7 @@ class AssetResponseDto {
     return null;
   }
 
-  static List<AssetResponseDto>? listFromJson(
-    dynamic json, {
-    bool growable = false,
-  }) {
+  static List<AssetResponseDto>? listFromJson(dynamic json, {bool growable = false,}) {
     final result = <AssetResponseDto>[];
     if (json is List && json.isNotEmpty) {
       for (final row in json) {
@@ -254,18 +248,12 @@ class AssetResponseDto {
   }
 
   // maps a json object with a list of AssetResponseDto-objects as value to a dart map
-  static Map<String, List<AssetResponseDto>> mapListFromJson(
-    dynamic json, {
-    bool growable = false,
-  }) {
+  static Map<String, List<AssetResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
     final map = <String, List<AssetResponseDto>>{};
     if (json is Map && json.isNotEmpty) {
       json = json.cast<String, dynamic>(); // ignore: parameter_assignments
       for (final entry in json.entries) {
-        final value = AssetResponseDto.listFromJson(
-          entry.value,
-          growable: growable,
-        );
+        final value = AssetResponseDto.listFromJson(entry.value, growable: growable,);
         if (value != null) {
           map[entry.key] = value;
         }
@@ -292,3 +280,4 @@ class AssetResponseDto {
     'tags',
   };
 }
+

+ 1 - 1
mobile/openapi/test/asset_api_test.dart

@@ -47,7 +47,7 @@ void main() {
 
     // 
     //
-    //Future<Object> downloadFile(String assetId, { bool isThumb, bool isWeb }) async
+    //Future<Object> downloadFile(String assetId) async
     test('test downloadFile', () async {
       // TODO
     });

+ 7 - 5
server/apps/immich/src/api-v1/asset/asset.controller.ts

@@ -15,6 +15,7 @@ import {
   Put,
   UploadedFiles,
   Patch,
+  StreamableFile,
 } from '@nestjs/common';
 import { Authenticated } from '../../decorators/authenticated.decorator';
 import { AssetService } from './asset.service';
@@ -28,7 +29,7 @@ import { CheckDuplicateAssetDto } from './dto/check-duplicate-asset.dto';
 import { ApiBearerAuth, ApiBody, ApiConsumes, ApiHeader, ApiTags } from '@nestjs/swagger';
 import { CuratedObjectsResponseDto } from './response-dto/curated-objects-response.dto';
 import { CuratedLocationsResponseDto } from './response-dto/curated-locations-response.dto';
-import { AssetResponseDto } from '@app/domain';
+import { AssetResponseDto, ImmichReadStream } from '@app/domain';
 import { CheckDuplicateAssetResponseDto } from './response-dto/check-duplicate-asset-response.dto';
 import { AssetFileUploadDto } from './dto/asset-file-upload.dto';
 import { CreateAssetDto, mapToUploadFile } from './dto/create-asset.dto';
@@ -55,6 +56,10 @@ import { UpdateAssetsToSharedLinkDto } from './dto/add-assets-to-shared-link.dto
 import { AssetSearchDto } from './dto/asset-search.dto';
 import { assetUploadOption, ImmichFile } from '../../config/asset-upload.config';
 
+function asStreamableFile({ stream, type, length }: ImmichReadStream) {
+  return new StreamableFile(stream, { type, length });
+}
+
 @ApiBearerAuth()
 @ApiTags('Asset')
 @Controller('asset')
@@ -103,12 +108,9 @@ export class AssetController {
   async downloadFile(
     @GetAuthUser() authUser: AuthUserDto,
     @Response({ passthrough: true }) res: Res,
-    @Query(new ValidationPipe({ transform: true })) query: ServeFileDto,
     @Param('assetId') assetId: string,
   ): Promise<any> {
-    this.assetService.checkDownloadAccess(authUser);
-    await this.assetService.checkAssetsAccess(authUser, [assetId]);
-    return this.assetService.downloadFile(query, assetId, res);
+    return this.assetService.downloadFile(authUser, assetId).then(asStreamableFile);
   }
 
   @Authenticated({ isShared: true })

+ 16 - 1
server/apps/immich/src/api-v1/asset/asset.service.spec.ts

@@ -9,12 +9,13 @@ import { AssetCountByUserIdResponseDto } from './response-dto/asset-count-by-use
 import { DownloadService } from '../../modules/download/download.service';
 import { AlbumRepository, IAlbumRepository } from '../album/album-repository';
 import { StorageService } from '@app/storage';
-import { ICryptoRepository, IJobRepository, ISharedLinkRepository, JobName } from '@app/domain';
+import { ICryptoRepository, IJobRepository, ISharedLinkRepository, IStorageRepository, JobName } from '@app/domain';
 import {
   authStub,
   newCryptoRepositoryMock,
   newJobRepositoryMock,
   newSharedLinkRepositoryMock,
+  newStorageRepositoryMock,
   sharedLinkResponseStub,
   sharedLinkStub,
 } from '@app/domain/../test';
@@ -110,6 +111,7 @@ describe('AssetService', () => {
   let sharedLinkRepositoryMock: jest.Mocked<ISharedLinkRepository>;
   let cryptoMock: jest.Mocked<ICryptoRepository>;
   let jobMock: jest.Mocked<IJobRepository>;
+  let storageMock: jest.Mocked<IStorageRepository>;
 
   beforeEach(() => {
     assetRepositoryMock = {
@@ -154,6 +156,7 @@ describe('AssetService', () => {
     sharedLinkRepositoryMock = newSharedLinkRepositoryMock();
     jobMock = newJobRepositoryMock();
     cryptoMock = newCryptoRepositoryMock();
+    storageMock = newStorageRepositoryMock();
 
     sut = new AssetService(
       assetRepositoryMock,
@@ -164,6 +167,7 @@ describe('AssetService', () => {
       sharedLinkRepositoryMock,
       jobMock,
       cryptoMock,
+      storageMock,
     );
   });
 
@@ -413,4 +417,15 @@ describe('AssetService', () => {
       expect(() => sut.checkDownloadAccess(authStub.readonlySharedLink)).toThrow(ForbiddenException);
     });
   });
+
+  describe('downloadFile', () => {
+    it('should download a single file', async () => {
+      assetRepositoryMock.countByIdAndUser.mockResolvedValue(1);
+      assetRepositoryMock.get.mockResolvedValue(_getAsset_1());
+
+      await sut.downloadFile(authStub.admin, 'id_1');
+
+      expect(storageMock.createReadStream).toHaveBeenCalledWith('fake_path/asset_1.jpeg', 'image/jpeg');
+    });
+  });
 });

+ 19 - 66
server/apps/immich/src/api-v1/asset/asset.service.ts

@@ -10,7 +10,6 @@ import {
   StreamableFile,
 } from '@nestjs/common';
 import { InjectRepository } from '@nestjs/typeorm';
-import { createHash } from 'node:crypto';
 import { QueryFailedError, Repository } from 'typeorm';
 import { AuthUserDto } from '../../decorators/auth-user.decorator';
 import { AssetEntity, AssetType, SharedLinkType } from '@app/infra';
@@ -23,7 +22,14 @@ import { SearchAssetDto } from './dto/search-asset.dto';
 import fs from 'fs/promises';
 import { CheckDuplicateAssetDto } from './dto/check-duplicate-asset.dto';
 import { CuratedObjectsResponseDto } from './response-dto/curated-objects-response.dto';
-import { AssetResponseDto, JobName, mapAsset, mapAssetWithoutExif } from '@app/domain';
+import {
+  AssetResponseDto,
+  ImmichReadStream,
+  IStorageRepository,
+  JobName,
+  mapAsset,
+  mapAssetWithoutExif,
+} from '@app/domain';
 import { CreateAssetDto, UploadFile } from './dto/create-asset.dto';
 import { DeleteAssetResponseDto, DeleteAssetStatusEnum } from './response-dto/delete-asset-response.dto';
 import { GetAssetThumbnailDto, GetAssetThumbnailFormatEnum } from './dto/get-asset-thumbnail.dto';
@@ -73,6 +79,7 @@ export class AssetService {
     @Inject(ISharedLinkRepository) sharedLinkRepository: ISharedLinkRepository,
     @Inject(IJobRepository) private jobRepository: IJobRepository,
     @Inject(ICryptoRepository) cryptoRepository: ICryptoRepository,
+    @Inject(IStorageRepository) private storage: IStorageRepository,
   ) {
     this.assetCore = new AssetCore(_assetRepository, jobRepository, storageService);
     this.shareCore = new ShareCore(sharedLinkRepository, cryptoRepository);
@@ -189,62 +196,21 @@ export class AssetService {
     return this.downloadService.downloadArchive(`immich-${now}`, assetToDownload);
   }
 
-  public async downloadFile(query: ServeFileDto, assetId: string, res: Res) {
-    try {
-      let fileReadStream = null;
-      const asset = await this._assetRepository.getById(assetId);
-
-      // Download Video
-      if (asset.type === AssetType.VIDEO) {
-        const { size } = await fileInfo(asset.originalPath);
-
-        res.set({
-          'Content-Type': asset.mimeType,
-          'Content-Length': size,
-        });
-
-        await fs.access(asset.originalPath, constants.R_OK | constants.W_OK);
-        fileReadStream = createReadStream(asset.originalPath);
-      } else {
-        // Download Image
-        if (!query.isThumb) {
-          /**
-           * Download Image Original File
-           */
-          const { size } = await fileInfo(asset.originalPath);
-
-          res.set({
-            'Content-Type': asset.mimeType,
-            'Content-Length': size,
-          });
-
-          await fs.access(asset.originalPath, constants.R_OK | constants.W_OK);
-          fileReadStream = createReadStream(asset.originalPath);
-        } else {
-          /**
-           * Download Image Resize File
-           */
-          if (!asset.resizePath) {
-            throw new NotFoundException('resizePath not set');
-          }
-
-          const { size } = await fileInfo(asset.resizePath);
-
-          res.set({
-            'Content-Type': 'image/jpeg',
-            'Content-Length': size,
-          });
+  public async downloadFile(authUser: AuthUserDto, assetId: string): Promise<ImmichReadStream> {
+    this.checkDownloadAccess(authUser);
+    await this.checkAssetsAccess(authUser, [assetId]);
 
-          await fs.access(asset.resizePath, constants.R_OK | constants.W_OK);
-          fileReadStream = createReadStream(asset.resizePath);
-        }
+    try {
+      const asset = await this._assetRepository.get(assetId);
+      if (asset && asset.originalPath && asset.mimeType) {
+        return this.storage.createReadStream(asset.originalPath, asset.mimeType);
       }
-
-      return new StreamableFile(fileReadStream);
     } catch (e) {
       Logger.error(`Error download asset ${e}`, 'downloadFile');
       throw new InternalServerErrorException(`Failed to download asset ${e}`, 'DownloadFile');
     }
+
+    throw new NotFoundException();
   }
 
   public async getAssetThumbnail(
@@ -255,8 +221,7 @@ export class AssetService {
   ) {
     let fileReadStream: ReadStream;
 
-    const asset = await this.assetRepository.findOne({ where: { id: assetId } });
-
+    const asset = await this._assetRepository.get(assetId);
     if (!asset) {
       throw new NotFoundException('Asset not found');
     }
@@ -584,18 +549,6 @@ export class AssetService {
     return this._assetRepository.getAssetByChecksum(userId, checksum);
   }
 
-  calculateChecksum(filePath: string): Promise<Buffer> {
-    const fileReadStream = createReadStream(filePath);
-    const sha1Hash = createHash('sha1');
-    const deferred = new Promise<Buffer>((resolve, reject) => {
-      sha1Hash.once('error', (err) => reject(err));
-      sha1Hash.once('finish', () => resolve(sha1Hash.read()));
-    });
-
-    fileReadStream.pipe(sha1Hash);
-    return deferred;
-  }
-
   getAssetCountByUserId(authUser: AuthUserDto): Promise<AssetCountByUserIdResponseDto> {
     return this._assetRepository.getAssetCountByUserId(authUser.id);
   }

+ 0 - 18
server/immich-openapi-specs.json

@@ -1109,24 +1109,6 @@
         "operationId": "downloadFile",
         "description": "",
         "parameters": [
-          {
-            "name": "isThumb",
-            "required": false,
-            "in": "query",
-            "schema": {
-              "title": "Is serve thumbnail (resize) file",
-              "type": "boolean"
-            }
-          },
-          {
-            "name": "isWeb",
-            "required": false,
-            "in": "query",
-            "schema": {
-              "title": "Is request made from web",
-              "type": "boolean"
-            }
-          },
           {
             "name": "assetId",
             "required": true,

+ 0 - 5
server/libs/domain/src/auth/auth.core.ts

@@ -6,11 +6,6 @@ import { ICryptoRepository } from '../crypto/crypto.repository';
 import { LoginResponseDto, mapLoginResponse } from './response-dto';
 import { IUserTokenRepository, UserTokenCore } from '../user-token';
 
-export type JwtValidationResult = {
-  status: boolean;
-  userId: string | null;
-};
-
 export class AuthCore {
   private userTokenCore: UserTokenCore;
   constructor(

+ 0 - 1
server/libs/domain/src/auth/dto/index.ts

@@ -1,5 +1,4 @@
 export * from './auth-user.dto';
 export * from './change-password.dto';
-export * from './jwt-payload.dto';
 export * from './login-credential.dto';
 export * from './sign-up.dto';

+ 0 - 4
server/libs/domain/src/auth/dto/jwt-payload.dto.ts

@@ -1,4 +0,0 @@
-export class JwtPayloadDto {
-  userId!: string;
-  email!: string;
-}

+ 1 - 0
server/libs/domain/src/index.ts

@@ -8,6 +8,7 @@ export * from './domain.module';
 export * from './job';
 export * from './oauth';
 export * from './share';
+export * from './storage';
 export * from './system-config';
 export * from './tag';
 export * from './user';

+ 1 - 0
server/libs/domain/src/storage/index.ts

@@ -0,0 +1 @@
+export * from './storage.repository';

+ 13 - 0
server/libs/domain/src/storage/storage.repository.ts

@@ -0,0 +1,13 @@
+import { ReadStream } from 'fs';
+
+export interface ImmichReadStream {
+  stream: ReadStream;
+  type: string;
+  length: number;
+}
+
+export const IStorageRepository = 'IStorageRepository';
+
+export interface IStorageRepository {
+  createReadStream(filepath: string, mimeType: string): Promise<ImmichReadStream>;
+}

+ 1 - 0
server/libs/domain/test/index.ts

@@ -4,6 +4,7 @@ export * from './device-info.repository.mock';
 export * from './fixtures';
 export * from './job.repository.mock';
 export * from './shared-link.repository.mock';
+export * from './storage.repository.mock';
 export * from './system-config.repository.mock';
 export * from './user-token.repository.mock';
 export * from './user.repository.mock';

+ 7 - 0
server/libs/domain/test/storage.repository.mock.ts

@@ -0,0 +1,7 @@
+import { IStorageRepository } from '../src';
+
+export const newStorageRepositoryMock = (): jest.Mocked<IStorageRepository> => {
+  return {
+    createReadStream: jest.fn(),
+  };
+};

+ 3 - 0
server/libs/infra/src/infra.module.ts

@@ -4,6 +4,7 @@ import {
   IJobRepository,
   IKeyRepository,
   ISharedLinkRepository,
+  IStorageRepository,
   ISystemConfigRepository,
   IUserRepository,
   QueueName,
@@ -29,6 +30,7 @@ import {
   UserTokenEntity,
 } from './db';
 import { JobRepository } from './job';
+import { FilesystemProvider } from './storage';
 
 const providers: Provider[] = [
   { provide: ICryptoRepository, useClass: CryptoRepository },
@@ -36,6 +38,7 @@ const providers: Provider[] = [
   { provide: IKeyRepository, useClass: APIKeyRepository },
   { provide: IJobRepository, useClass: JobRepository },
   { provide: ISharedLinkRepository, useClass: SharedLinkRepository },
+  { provide: IStorageRepository, useClass: FilesystemProvider },
   { provide: ISystemConfigRepository, useClass: SystemConfigRepository },
   { provide: IUserRepository, useClass: UserRepository },
   { provide: IUserTokenRepository, useClass: UserTokenRepository },

+ 18 - 0
server/libs/infra/src/storage/filesystem.provider.ts

@@ -0,0 +1,18 @@
+import { ImmichReadStream, IStorageRepository } from '@app/domain';
+import { constants, createReadStream, stat } from 'fs';
+import fs from 'fs/promises';
+import { promisify } from 'util';
+
+const fileInfo = promisify(stat);
+
+export class FilesystemProvider implements IStorageRepository {
+  async createReadStream(filepath: string, mimeType: string): Promise<ImmichReadStream> {
+    const { size } = await fileInfo(filepath);
+    await fs.access(filepath, constants.R_OK | constants.W_OK);
+    return {
+      stream: createReadStream(filepath),
+      length: size,
+      type: mimeType,
+    };
+  }
+}

+ 1 - 0
server/libs/infra/src/storage/index.ts

@@ -0,0 +1 @@
+export * from './filesystem.provider';

+ 8 - 24
web/src/api/open-api/api.ts

@@ -4,7 +4,7 @@
  * Immich
  * Immich API
  *
- * The version of the OpenAPI document: 1.43.0
+ * The version of the OpenAPI document: 1.43.1
  * 
  *
  * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
@@ -3729,12 +3729,10 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
         /**
          * 
          * @param {string} assetId 
-         * @param {boolean} [isThumb] 
-         * @param {boolean} [isWeb] 
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          */
-        downloadFile: async (assetId: string, isThumb?: boolean, isWeb?: boolean, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+        downloadFile: async (assetId: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
             // verify required parameter 'assetId' is not null or undefined
             assertParamExists('downloadFile', 'assetId', assetId)
             const localVarPath = `/asset/download/{assetId}`
@@ -3754,14 +3752,6 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
             // http bearer authentication required
             await setBearerAuthToObject(localVarHeaderParameter, configuration)
 
-            if (isThumb !== undefined) {
-                localVarQueryParameter['isThumb'] = isThumb;
-            }
-
-            if (isWeb !== undefined) {
-                localVarQueryParameter['isWeb'] = isWeb;
-            }
-
 
     
             setSearchParams(localVarUrlObj, localVarQueryParameter);
@@ -4489,13 +4479,11 @@ export const AssetApiFp = function(configuration?: Configuration) {
         /**
          * 
          * @param {string} assetId 
-         * @param {boolean} [isThumb] 
-         * @param {boolean} [isWeb] 
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          */
-        async downloadFile(assetId: string, isThumb?: boolean, isWeb?: boolean, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<object>> {
-            const localVarAxiosArgs = await localVarAxiosParamCreator.downloadFile(assetId, isThumb, isWeb, options);
+        async downloadFile(assetId: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<object>> {
+            const localVarAxiosArgs = await localVarAxiosParamCreator.downloadFile(assetId, options);
             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
         },
         /**
@@ -4719,13 +4707,11 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
         /**
          * 
          * @param {string} assetId 
-         * @param {boolean} [isThumb] 
-         * @param {boolean} [isWeb] 
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          */
-        downloadFile(assetId: string, isThumb?: boolean, isWeb?: boolean, options?: any): AxiosPromise<object> {
-            return localVarFp.downloadFile(assetId, isThumb, isWeb, options).then((request) => request(axios, basePath));
+        downloadFile(assetId: string, options?: any): AxiosPromise<object> {
+            return localVarFp.downloadFile(assetId, options).then((request) => request(axios, basePath));
         },
         /**
          * 
@@ -4939,14 +4925,12 @@ export class AssetApi extends BaseAPI {
     /**
      * 
      * @param {string} assetId 
-     * @param {boolean} [isThumb] 
-     * @param {boolean} [isWeb] 
      * @param {*} [options] Override http request option.
      * @throws {RequiredError}
      * @memberof AssetApi
      */
-    public downloadFile(assetId: string, isThumb?: boolean, isWeb?: boolean, options?: AxiosRequestConfig) {
-        return AssetApiFp(this.configuration).downloadFile(assetId, isThumb, isWeb, options).then((request) => request(this.axios, this.basePath));
+    public downloadFile(assetId: string, options?: AxiosRequestConfig) {
+        return AssetApiFp(this.configuration).downloadFile(assetId, options).then((request) => request(this.axios, this.basePath));
     }
 
     /**

+ 1 - 1
web/src/api/open-api/base.ts

@@ -4,7 +4,7 @@
  * Immich
  * Immich API
  *
- * The version of the OpenAPI document: 1.43.0
+ * The version of the OpenAPI document: 1.43.1
  * 
  *
  * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

+ 1 - 1
web/src/api/open-api/common.ts

@@ -4,7 +4,7 @@
  * Immich
  * Immich API
  *
- * The version of the OpenAPI document: 1.43.0
+ * The version of the OpenAPI document: 1.43.1
  * 
  *
  * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

+ 1 - 1
web/src/api/open-api/configuration.ts

@@ -4,7 +4,7 @@
  * Immich
  * Immich API
  *
- * The version of the OpenAPI document: 1.43.0
+ * The version of the OpenAPI document: 1.43.1
  * 
  *
  * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

+ 1 - 1
web/src/api/open-api/index.ts

@@ -4,7 +4,7 @@
  * Immich
  * Immich API
  *
- * The version of the OpenAPI document: 1.43.0
+ * The version of the OpenAPI document: 1.43.1
  * 
  *
  * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

+ 2 - 4
web/src/lib/components/asset-viewer/asset-viewer.svelte

@@ -136,10 +136,8 @@
 
 			$downloadAssets[imageFileName] = 0;
 
-			const { data, status } = await api.assetApi.downloadFile(assetId, false, false, {
-				params: {
-					key
-				},
+			const { data, status } = await api.assetApi.downloadFile(assetId, {
+				params: { key },
 				responseType: 'blob',
 				onDownloadProgress: (progressEvent) => {
 					if (progressEvent.lengthComputable) {