Sfoglia il codice sorgente

feat(server): improve validation of albums (#2188)

* feat(server): improve validation of albums

* regenerate openapi + fix downloadArchive for web
Michel Heusschen 2 anni fa
parent
commit
8e3a7caebd

+ 4 - 2
mobile/openapi/doc/AlbumApi.md

@@ -294,7 +294,7 @@ void (empty response body)
 [[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)
 
 # **downloadArchive**
-> MultipartFile downloadArchive(albumId, skip, key)
+> MultipartFile downloadArchive(albumId, name, skip, key)
 
 
 
@@ -316,11 +316,12 @@ import 'package:openapi/api.dart';
 
 final api_instance = AlbumApi();
 final albumId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | 
+final name = name_example; // String | 
 final skip = 8.14; // num | 
 final key = key_example; // String | 
 
 try {
-    final result = api_instance.downloadArchive(albumId, skip, key);
+    final result = api_instance.downloadArchive(albumId, name, skip, key);
     print(result);
 } catch (e) {
     print('Exception when calling AlbumApi->downloadArchive: $e\n');
@@ -332,6 +333,7 @@ try {
 Name | Type | Description  | Notes
 ------------- | ------------- | ------------- | -------------
  **albumId** | **String**|  | 
+ **name** | **String**|  | [optional] 
  **skip** | **num**|  | [optional] 
  **key** | **String**|  | [optional] 
 

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

@@ -414,7 +414,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)
 
 # **downloadLibrary**
-> MultipartFile downloadLibrary(skip, key)
+> MultipartFile downloadLibrary(name, skip, key)
 
 
 
@@ -435,11 +435,12 @@ import 'package:openapi/api.dart';
 //defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
 
 final api_instance = AssetApi();
+final name = name_example; // String | 
 final skip = 8.14; // num | 
 final key = key_example; // String | 
 
 try {
-    final result = api_instance.downloadLibrary(skip, key);
+    final result = api_instance.downloadLibrary(name, skip, key);
     print(result);
 } catch (e) {
     print('Exception when calling AssetApi->downloadLibrary: $e\n');
@@ -450,6 +451,7 @@ try {
 
 Name | Type | Description  | Notes
 ------------- | ------------- | ------------- | -------------
+ **name** | **String**|  | [optional] 
  **skip** | **num**|  | [optional] 
  **key** | **String**|  | [optional] 
 

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

@@ -9,7 +9,7 @@ import 'package:openapi/api.dart';
 Name | Type | Description | Notes
 ------------ | ------------- | ------------- | -------------
 **albumId** | **String** |  | 
-**expiresAt** | **String** |  | [optional] 
+**expiresAt** | [**DateTime**](DateTime.md) |  | [optional] 
 **allowUpload** | **bool** |  | [optional] 
 **allowDownload** | **bool** |  | [optional] 
 **showExif** | **bool** |  | [optional] 

+ 10 - 3
mobile/openapi/lib/api/album_api.dart

@@ -295,10 +295,12 @@ class AlbumApi {
   ///
   /// * [String] albumId (required):
   ///
+  /// * [String] name:
+  ///
   /// * [num] skip:
   ///
   /// * [String] key:
-  Future<Response> downloadArchiveWithHttpInfo(String albumId, { num? skip, String? key, }) async {
+  Future<Response> downloadArchiveWithHttpInfo(String albumId, { String? name, num? skip, String? key, }) async {
     // ignore: prefer_const_declarations
     final path = r'/album/{albumId}/download'
       .replaceAll('{albumId}', albumId);
@@ -310,6 +312,9 @@ class AlbumApi {
     final headerParams = <String, String>{};
     final formParams = <String, String>{};
 
+    if (name != null) {
+      queryParams.addAll(_queryParams('', 'name', name));
+    }
     if (skip != null) {
       queryParams.addAll(_queryParams('', 'skip', skip));
     }
@@ -337,11 +342,13 @@ class AlbumApi {
   ///
   /// * [String] albumId (required):
   ///
+  /// * [String] name:
+  ///
   /// * [num] skip:
   ///
   /// * [String] key:
-  Future<MultipartFile?> downloadArchive(String albumId, { num? skip, String? key, }) async {
-    final response = await downloadArchiveWithHttpInfo(albumId,  skip: skip, key: key, );
+  Future<MultipartFile?> downloadArchive(String albumId, { String? name, num? skip, String? key, }) async {
+    final response = await downloadArchiveWithHttpInfo(albumId,  name: name, skip: skip, key: key, );
     if (response.statusCode >= HttpStatus.badRequest) {
       throw ApiException(response.statusCode, await _decodeBodyBytes(response));
     }

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

@@ -422,10 +422,12 @@ class AssetApi {
   ///
   /// Parameters:
   ///
+  /// * [String] name:
+  ///
   /// * [num] skip:
   ///
   /// * [String] key:
-  Future<Response> downloadLibraryWithHttpInfo({ num? skip, String? key, }) async {
+  Future<Response> downloadLibraryWithHttpInfo({ String? name, num? skip, String? key, }) async {
     // ignore: prefer_const_declarations
     final path = r'/asset/download-library';
 
@@ -436,6 +438,9 @@ class AssetApi {
     final headerParams = <String, String>{};
     final formParams = <String, String>{};
 
+    if (name != null) {
+      queryParams.addAll(_queryParams('', 'name', name));
+    }
     if (skip != null) {
       queryParams.addAll(_queryParams('', 'skip', skip));
     }
@@ -461,11 +466,13 @@ class AssetApi {
   ///
   /// Parameters:
   ///
+  /// * [String] name:
+  ///
   /// * [num] skip:
   ///
   /// * [String] key:
-  Future<MultipartFile?> downloadLibrary({ num? skip, String? key, }) async {
-    final response = await downloadLibraryWithHttpInfo( skip: skip, key: key, );
+  Future<MultipartFile?> downloadLibrary({ String? name, num? skip, String? key, }) async {
+    final response = await downloadLibraryWithHttpInfo( name: name, skip: skip, key: key, );
     if (response.statusCode >= HttpStatus.badRequest) {
       throw ApiException(response.statusCode, await _decodeBodyBytes(response));
     }

+ 3 - 3
mobile/openapi/lib/model/create_album_share_link_dto.dart

@@ -29,7 +29,7 @@ class CreateAlbumShareLinkDto {
   /// source code must fall back to having a nullable type.
   /// Consider adding a "default:" property in the specification file to hide this note.
   ///
-  String? expiresAt;
+  DateTime? expiresAt;
 
   ///
   /// Please note: This property should have been non-nullable! Since the specification file
@@ -89,7 +89,7 @@ class CreateAlbumShareLinkDto {
     final json = <String, dynamic>{};
       json[r'albumId'] = this.albumId;
     if (this.expiresAt != null) {
-      json[r'expiresAt'] = this.expiresAt;
+      json[r'expiresAt'] = this.expiresAt!.toUtc().toIso8601String();
     } else {
       // json[r'expiresAt'] = null;
     }
@@ -136,7 +136,7 @@ class CreateAlbumShareLinkDto {
 
       return CreateAlbumShareLinkDto(
         albumId: mapValueOfType<String>(json, r'albumId')!,
-        expiresAt: mapValueOfType<String>(json, r'expiresAt'),
+        expiresAt: mapDateTime(json, r'expiresAt', ''),
         allowUpload: mapValueOfType<bool>(json, r'allowUpload'),
         allowDownload: mapValueOfType<bool>(json, r'allowDownload'),
         showExif: mapValueOfType<bool>(json, r'showExif'),

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

@@ -54,7 +54,7 @@ void main() {
 
     // 
     //
-    //Future<MultipartFile> downloadArchive(String albumId, { num skip, String key }) async
+    //Future<MultipartFile> downloadArchive(String albumId, { String name, num skip, String key }) async
     test('test downloadArchive', () async {
       // TODO
     });

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

@@ -68,7 +68,7 @@ void main() {
 
     // Current this is not used in any UI element
     //
-    //Future<MultipartFile> downloadLibrary({ num skip, String key }) async
+    //Future<MultipartFile> downloadLibrary({ String name, num skip, String key }) async
     test('test downloadLibrary', () async {
       // TODO
     });

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

@@ -21,7 +21,7 @@ void main() {
       // TODO
     });
 
-    // String expiresAt
+    // DateTime expiresAt
     test('to test the property `expiresAt`', () async {
       // TODO
     });

+ 16 - 20
server/apps/immich/src/api-v1/album/album.controller.ts

@@ -1,16 +1,4 @@
-import {
-  Controller,
-  Get,
-  Post,
-  Body,
-  Patch,
-  Param,
-  Delete,
-  ValidationPipe,
-  Put,
-  Query,
-  Response,
-} from '@nestjs/common';
+import { Controller, Get, Post, Body, Patch, Param, Delete, Put, Query, Response } from '@nestjs/common';
 import { ParseMeUUIDPipe } from '../validation/parse-me-uuid-pipe';
 import { AlbumService } from './album.service';
 import { CreateAlbumDto } from './dto/create-album.dto';
@@ -33,9 +21,11 @@ import {
 import { DownloadDto } from '../asset/dto/download-library.dto';
 import { CreateAlbumShareLinkDto as CreateAlbumSharedLinkDto } from './dto/create-album-shared-link.dto';
 import { AlbumIdDto } from './dto/album-id.dto';
+import { UseValidation } from '../../decorators/use-validation.decorator';
 
 @ApiTags('Album')
 @Controller('album')
+@UseValidation()
 export class AlbumController {
   constructor(private readonly albumService: AlbumService) {}
 
@@ -47,7 +37,8 @@ export class AlbumController {
 
   @Authenticated()
   @Post()
-  async createAlbum(@GetAuthUser() authUser: AuthUserDto, @Body(ValidationPipe) createAlbumDto: CreateAlbumDto) {
+  async createAlbum(@GetAuthUser() authUser: AuthUserDto, @Body() createAlbumDto: CreateAlbumDto) {
+    // TODO: Handle nonexistent sharedWithUserIds and assetIds.
     return this.albumService.create(authUser, createAlbumDto);
   }
 
@@ -55,9 +46,10 @@ export class AlbumController {
   @Put('/:albumId/users')
   async addUsersToAlbum(
     @GetAuthUser() authUser: AuthUserDto,
-    @Body(ValidationPipe) addUsersDto: AddUsersDto,
+    @Body() addUsersDto: AddUsersDto,
     @Param() { albumId }: AlbumIdDto,
   ) {
+    // TODO: Handle nonexistent sharedUserIds.
     return this.albumService.addUsersToAlbum(authUser, addUsersDto, albumId);
   }
 
@@ -65,9 +57,11 @@ export class AlbumController {
   @Put('/:albumId/assets')
   async addAssetsToAlbum(
     @GetAuthUser() authUser: AuthUserDto,
-    @Body(ValidationPipe) addAssetsDto: AddAssetsDto,
+    @Body() addAssetsDto: AddAssetsDto,
     @Param() { albumId }: AlbumIdDto,
   ): Promise<AddAssetsResponseDto> {
+    // TODO: Handle nonexistent assetIds.
+    // TODO: Disallow adding assets of another user to an album.
     return this.albumService.addAssetsToAlbum(authUser, addAssetsDto, albumId);
   }
 
@@ -81,7 +75,7 @@ export class AlbumController {
   @Delete('/:albumId/assets')
   async removeAssetFromAlbum(
     @GetAuthUser() authUser: AuthUserDto,
-    @Body(ValidationPipe) removeAssetsDto: RemoveAssetsDto,
+    @Body() removeAssetsDto: RemoveAssetsDto,
     @Param() { albumId }: AlbumIdDto,
   ): Promise<AlbumResponseDto> {
     return this.albumService.removeAssetsFromAlbum(authUser, removeAssetsDto, albumId);
@@ -107,9 +101,11 @@ export class AlbumController {
   @Patch('/:albumId')
   async updateAlbumInfo(
     @GetAuthUser() authUser: AuthUserDto,
-    @Body(ValidationPipe) updateAlbumInfoDto: UpdateAlbumDto,
+    @Body() updateAlbumInfoDto: UpdateAlbumDto,
     @Param() { albumId }: AlbumIdDto,
   ) {
+    // TODO: Handle nonexistent albumThumbnailAssetId.
+    // TODO: Disallow setting asset from other user as albumThumbnailAssetId.
     return this.albumService.updateAlbumInfo(authUser, updateAlbumInfoDto, albumId);
   }
 
@@ -119,7 +115,7 @@ export class AlbumController {
   async downloadArchive(
     @GetAuthUser() authUser: AuthUserDto,
     @Param() { albumId }: AlbumIdDto,
-    @Query(new ValidationPipe({ transform: true })) dto: DownloadDto,
+    @Query() dto: DownloadDto,
     @Response({ passthrough: true }) res: Res,
   ) {
     this.albumService.checkDownloadAccess(authUser);
@@ -140,7 +136,7 @@ export class AlbumController {
   @Post('/create-shared-link')
   async createAlbumSharedLink(
     @GetAuthUser() authUser: AuthUserDto,
-    @Body(ValidationPipe) createAlbumShareLinkDto: CreateAlbumSharedLinkDto,
+    @Body() createAlbumShareLinkDto: CreateAlbumSharedLinkDto,
   ) {
     return this.albumService.createAlbumSharedLink(authUser, createAlbumShareLinkDto);
   }

+ 2 - 2
server/apps/immich/src/api-v1/album/dto/add-assets.dto.ts

@@ -1,6 +1,6 @@
-import { IsNotEmpty } from 'class-validator';
+import { ValidateUUID } from 'apps/immich/src/decorators/validate-uuid.decorator';
 
 export class AddAssetsDto {
-  @IsNotEmpty()
+  @ValidateUUID({ each: true })
   assetIds!: string[];
 }

+ 2 - 2
server/apps/immich/src/api-v1/album/dto/add-users.dto.ts

@@ -1,6 +1,6 @@
-import { IsNotEmpty } from 'class-validator';
+import { ValidateUUID } from 'apps/immich/src/decorators/validate-uuid.decorator';
 
 export class AddUsersDto {
-  @IsNotEmpty()
+  @ValidateUUID({ each: true })
   sharedUserIds!: string[];
 }

+ 2 - 5
server/apps/immich/src/api-v1/album/dto/album-id.dto.ts

@@ -1,9 +1,6 @@
-import { ApiProperty } from '@nestjs/swagger';
-import { IsNotEmpty, IsUUID } from 'class-validator';
+import { ValidateUUID } from 'apps/immich/src/decorators/validate-uuid.decorator';
 
 export class AlbumIdDto {
-  @IsNotEmpty()
-  @IsUUID('4')
-  @ApiProperty({ format: 'uuid' })
+  @ValidateUUID()
   albumId!: string;
 }

+ 10 - 4
server/apps/immich/src/api-v1/album/dto/create-album-shared-link.dto.ts

@@ -1,27 +1,33 @@
-import { IsBoolean, IsNotEmpty, IsOptional, IsString } from 'class-validator';
+import { ApiProperty } from '@nestjs/swagger';
+import { ValidateUUID } from 'apps/immich/src/decorators/validate-uuid.decorator';
+import { IsBoolean, IsISO8601, IsOptional, IsString } from 'class-validator';
 
 export class CreateAlbumShareLinkDto {
-  @IsString()
-  @IsNotEmpty()
+  @ValidateUUID()
   albumId!: string;
 
-  @IsString()
+  @IsISO8601()
   @IsOptional()
+  @ApiProperty({ format: 'date-time' })
   expiresAt?: string;
 
   @IsBoolean()
   @IsOptional()
+  @ApiProperty()
   allowUpload?: boolean;
 
   @IsBoolean()
   @IsOptional()
+  @ApiProperty()
   allowDownload?: boolean;
 
   @IsBoolean()
   @IsOptional()
+  @ApiProperty()
   showExif?: boolean;
 
   @IsString()
   @IsOptional()
+  @ApiProperty()
   description?: string;
 }

+ 7 - 3
server/apps/immich/src/api-v1/album/dto/create-album.dto.ts

@@ -1,12 +1,16 @@
-import { IsNotEmpty, IsOptional } from 'class-validator';
+import { ApiProperty } from '@nestjs/swagger';
+import { ValidateUUID } from 'apps/immich/src/decorators/validate-uuid.decorator';
+import { IsNotEmpty, IsString } from 'class-validator';
 
 export class CreateAlbumDto {
   @IsNotEmpty()
+  @IsString()
+  @ApiProperty()
   albumName!: string;
 
-  @IsOptional()
+  @ValidateUUID({ optional: true, each: true })
   sharedWithUserIds?: string[];
 
-  @IsOptional()
+  @ValidateUUID({ optional: true, each: true })
   assetIds?: string[];
 }

+ 2 - 2
server/apps/immich/src/api-v1/album/dto/remove-assets.dto.ts

@@ -1,6 +1,6 @@
-import { IsNotEmpty } from 'class-validator';
+import { ValidateUUID } from 'apps/immich/src/decorators/validate-uuid.decorator';
 
 export class RemoveAssetsDto {
-  @IsNotEmpty()
+  @ValidateUUID({ each: true })
   assetIds!: string[];
 }

+ 4 - 1
server/apps/immich/src/api-v1/album/dto/update-album.dto.ts

@@ -1,9 +1,12 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { ValidateUUID } from 'apps/immich/src/decorators/validate-uuid.decorator';
 import { IsOptional } from 'class-validator';
 
 export class UpdateAlbumDto {
   @IsOptional()
+  @ApiProperty()
   albumName?: string;
 
-  @IsOptional()
+  @ValidateUUID({ optional: true })
   albumThumbnailAssetId?: string;
 }

+ 1 - 1
server/apps/immich/src/api-v1/asset/dto/download-library.dto.ts

@@ -4,7 +4,7 @@ import { IsNumber, IsOptional, IsPositive, IsString } from 'class-validator';
 export class DownloadDto {
   @IsOptional()
   @IsString()
-  name = '';
+  name?: string;
 
   @IsOptional()
   @IsPositive()

+ 17 - 0
server/apps/immich/src/decorators/validate-uuid.decorator.ts

@@ -0,0 +1,17 @@
+import { applyDecorators } from '@nestjs/common';
+import { ApiProperty } from '@nestjs/swagger';
+import { IsArray, IsNotEmpty, IsOptional, IsString, IsUUID } from 'class-validator';
+
+export type Options = {
+  optional?: boolean;
+  each?: boolean;
+};
+
+export function ValidateUUID({ optional, each }: Options = { optional: false, each: false }) {
+  return applyDecorators(
+    IsUUID('4', { each }),
+    ApiProperty({ format: 'uuid' }),
+    optional ? IsOptional() : IsNotEmpty(),
+    each ? IsArray() : IsString(),
+  );
+}

+ 32 - 8
server/immich-openapi-specs.json

@@ -1895,6 +1895,14 @@
         "operationId": "downloadLibrary",
         "description": "Current this is not used in any UI element",
         "parameters": [
+          {
+            "name": "name",
+            "required": false,
+            "in": "query",
+            "schema": {
+              "type": "string"
+            }
+          },
           {
             "name": "skip",
             "required": false,
@@ -3343,6 +3351,14 @@
               "type": "string"
             }
           },
+          {
+            "name": "name",
+            "required": false,
+            "in": "query",
+            "schema": {
+              "type": "string"
+            }
+          },
           {
             "name": "skip",
             "required": false,
@@ -5359,7 +5375,8 @@
           "assetIds": {
             "type": "array",
             "items": {
-              "type": "string"
+              "type": "string",
+              "format": "uuid"
             }
           }
         },
@@ -5373,7 +5390,8 @@
           "assetIds": {
             "type": "array",
             "items": {
-              "type": "string"
+              "type": "string",
+              "format": "uuid"
             }
           }
         },
@@ -5435,13 +5453,15 @@
           "sharedWithUserIds": {
             "type": "array",
             "items": {
-              "type": "string"
+              "type": "string",
+              "format": "uuid"
             }
           },
           "assetIds": {
             "type": "array",
             "items": {
-              "type": "string"
+              "type": "string",
+              "format": "uuid"
             }
           }
         },
@@ -5455,7 +5475,8 @@
           "sharedUserIds": {
             "type": "array",
             "items": {
-              "type": "string"
+              "type": "string",
+              "format": "uuid"
             }
           }
         },
@@ -5491,7 +5512,8 @@
             "type": "string"
           },
           "albumThumbnailAssetId": {
-            "type": "string"
+            "type": "string",
+            "format": "uuid"
           }
         }
       },
@@ -5499,10 +5521,12 @@
         "type": "object",
         "properties": {
           "albumId": {
-            "type": "string"
+            "type": "string",
+            "format": "uuid"
           },
           "expiresAt": {
-            "type": "string"
+            "type": "string",
+            "format": "date-time"
           },
           "allowUpload": {
             "type": "boolean"

+ 3 - 4
server/libs/domain/src/album/dto/get-albums.dto.ts

@@ -1,7 +1,8 @@
 import { Transform } from 'class-transformer';
-import { IsBoolean, IsOptional, IsUUID } from 'class-validator';
+import { IsBoolean, IsOptional } from 'class-validator';
 import { toBoolean } from 'apps/immich/src/utils/transform.util';
 import { ApiProperty } from '@nestjs/swagger';
+import { ValidateUUID } from 'apps/immich/src/decorators/validate-uuid.decorator';
 
 export class GetAlbumsDto {
   @IsOptional()
@@ -20,8 +21,6 @@ export class GetAlbumsDto {
    * Ignores the shared parameter
    * undefined: get all albums
    */
-  @IsOptional()
-  @IsUUID(4)
-  @ApiProperty({ format: 'uuid' })
+  @ValidateUUID({ optional: true })
   assetId?: string;
 }

+ 30 - 14
web/src/api/open-api/api.ts

@@ -3160,12 +3160,13 @@ export const AlbumApiAxiosParamCreator = function (configuration?: Configuration
         /**
          * 
          * @param {string} albumId 
+         * @param {string} [name] 
          * @param {number} [skip] 
          * @param {string} [key] 
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          */
-        downloadArchive: async (albumId: string, skip?: number, key?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+        downloadArchive: async (albumId: string, name?: string, skip?: number, key?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
             // verify required parameter 'albumId' is not null or undefined
             assertParamExists('downloadArchive', 'albumId', albumId)
             const localVarPath = `/album/{albumId}/download`
@@ -3187,6 +3188,10 @@ export const AlbumApiAxiosParamCreator = function (configuration?: Configuration
 
             // authentication cookie required
 
+            if (name !== undefined) {
+                localVarQueryParameter['name'] = name;
+            }
+
             if (skip !== undefined) {
                 localVarQueryParameter['skip'] = skip;
             }
@@ -3529,13 +3534,14 @@ export const AlbumApiFp = function(configuration?: Configuration) {
         /**
          * 
          * @param {string} albumId 
+         * @param {string} [name] 
          * @param {number} [skip] 
          * @param {string} [key] 
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          */
-        async downloadArchive(albumId: string, skip?: number, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<any>> {
-            const localVarAxiosArgs = await localVarAxiosParamCreator.downloadArchive(albumId, skip, key, options);
+        async downloadArchive(albumId: string, name?: string, skip?: number, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<any>> {
+            const localVarAxiosArgs = await localVarAxiosParamCreator.downloadArchive(albumId, name, skip, key, options);
             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
         },
         /**
@@ -3663,13 +3669,14 @@ export const AlbumApiFactory = function (configuration?: Configuration, basePath
         /**
          * 
          * @param {string} albumId 
+         * @param {string} [name] 
          * @param {number} [skip] 
          * @param {string} [key] 
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          */
-        downloadArchive(albumId: string, skip?: number, key?: string, options?: any): AxiosPromise<any> {
-            return localVarFp.downloadArchive(albumId, skip, key, options).then((request) => request(axios, basePath));
+        downloadArchive(albumId: string, name?: string, skip?: number, key?: string, options?: any): AxiosPromise<any> {
+            return localVarFp.downloadArchive(albumId, name, skip, key, options).then((request) => request(axios, basePath));
         },
         /**
          * 
@@ -3800,14 +3807,15 @@ export class AlbumApi extends BaseAPI {
     /**
      * 
      * @param {string} albumId 
+     * @param {string} [name] 
      * @param {number} [skip] 
      * @param {string} [key] 
      * @param {*} [options] Override http request option.
      * @throws {RequiredError}
      * @memberof AlbumApi
      */
-    public downloadArchive(albumId: string, skip?: number, key?: string, options?: AxiosRequestConfig) {
-        return AlbumApiFp(this.configuration).downloadArchive(albumId, skip, key, options).then((request) => request(this.axios, this.basePath));
+    public downloadArchive(albumId: string, name?: string, skip?: number, key?: string, options?: AxiosRequestConfig) {
+        return AlbumApiFp(this.configuration).downloadArchive(albumId, name, skip, key, options).then((request) => request(this.axios, this.basePath));
     }
 
     /**
@@ -4195,12 +4203,13 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
         },
         /**
          * Current this is not used in any UI element
+         * @param {string} [name] 
          * @param {number} [skip] 
          * @param {string} [key] 
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          */
-        downloadLibrary: async (skip?: number, key?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+        downloadLibrary: async (name?: string, skip?: number, key?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
             const localVarPath = `/asset/download-library`;
             // use dummy base URL string because the URL constructor only accepts absolute URLs.
             const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
@@ -4219,6 +4228,10 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
 
             // authentication cookie required
 
+            if (name !== undefined) {
+                localVarQueryParameter['name'] = name;
+            }
+
             if (skip !== undefined) {
                 localVarQueryParameter['skip'] = skip;
             }
@@ -5029,13 +5042,14 @@ export const AssetApiFp = function(configuration?: Configuration) {
         },
         /**
          * Current this is not used in any UI element
+         * @param {string} [name] 
          * @param {number} [skip] 
          * @param {string} [key] 
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          */
-        async downloadLibrary(skip?: number, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<any>> {
-            const localVarAxiosArgs = await localVarAxiosParamCreator.downloadLibrary(skip, key, options);
+        async downloadLibrary(name?: string, skip?: number, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<any>> {
+            const localVarAxiosArgs = await localVarAxiosParamCreator.downloadLibrary(name, skip, key, options);
             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
         },
         /**
@@ -5284,13 +5298,14 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
         },
         /**
          * Current this is not used in any UI element
+         * @param {string} [name] 
          * @param {number} [skip] 
          * @param {string} [key] 
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          */
-        downloadLibrary(skip?: number, key?: string, options?: any): AxiosPromise<any> {
-            return localVarFp.downloadLibrary(skip, key, options).then((request) => request(axios, basePath));
+        downloadLibrary(name?: string, skip?: number, key?: string, options?: any): AxiosPromise<any> {
+            return localVarFp.downloadLibrary(name, skip, key, options).then((request) => request(axios, basePath));
         },
         /**
          * Get all AssetEntity belong to the user
@@ -5537,14 +5552,15 @@ export class AssetApi extends BaseAPI {
 
     /**
      * Current this is not used in any UI element
+     * @param {string} [name] 
      * @param {number} [skip] 
      * @param {string} [key] 
      * @param {*} [options] Override http request option.
      * @throws {RequiredError}
      * @memberof AssetApi
      */
-    public downloadLibrary(skip?: number, key?: string, options?: AxiosRequestConfig) {
-        return AssetApiFp(this.configuration).downloadLibrary(skip, key, options).then((request) => request(this.axios, this.basePath));
+    public downloadLibrary(name?: string, skip?: number, key?: string, options?: AxiosRequestConfig) {
+        return AssetApiFp(this.configuration).downloadLibrary(name, skip, key, options).then((request) => request(this.axios, this.basePath));
     }
 
     /**

+ 1 - 0
web/src/lib/components/album-page/album-viewer.svelte

@@ -264,6 +264,7 @@
 
 				const { data, status, headers } = await api.albumApi.downloadArchive(
 					album.id,
+					undefined,
 					skip || undefined,
 					sharedLink?.key,
 					{