You've already forked immich
							
							
				mirror of
				https://github.com/immich-app/immich.git
				synced 2025-10-31 00:18:28 +02:00 
			
		
		
		
	fix(web/server) uploaded asset in shared link not loaded (#1766)
* fix(web/server): Uploaded asset to shared link does not get added to the shared link/album * remove unused code * Add endpoints for each remove and add assets to shared link * Update api * Added deletion logic * Convert callback to async/await * Fix linter * Fix test * Fix server test * added test * Test coverage * modify DTO * Add notification * fix test
This commit is contained in:
		
							
								
								
									
										3
									
								
								mobile/openapi/.openapi-generator/FILES
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										3
									
								
								mobile/openapi/.openapi-generator/FILES
									
									
									
										generated
									
									
									
								
							| @@ -86,7 +86,6 @@ doc/ThumbnailFormat.md | ||||
| doc/TimeGroupEnum.md | ||||
| doc/UpdateAlbumDto.md | ||||
| doc/UpdateAssetDto.md | ||||
| doc/UpdateAssetsToSharedLinkDto.md | ||||
| doc/UpdateTagDto.md | ||||
| doc/UpdateUserDto.md | ||||
| doc/UpsertDeviceInfoDto.md | ||||
| @@ -189,7 +188,6 @@ lib/model/thumbnail_format.dart | ||||
| lib/model/time_group_enum.dart | ||||
| lib/model/update_album_dto.dart | ||||
| lib/model/update_asset_dto.dart | ||||
| lib/model/update_assets_to_shared_link_dto.dart | ||||
| lib/model/update_tag_dto.dart | ||||
| lib/model/update_user_dto.dart | ||||
| lib/model/upsert_device_info_dto.dart | ||||
| @@ -281,7 +279,6 @@ test/thumbnail_format_test.dart | ||||
| test/time_group_enum_test.dart | ||||
| test/update_album_dto_test.dart | ||||
| test/update_asset_dto_test.dart | ||||
| test/update_assets_to_shared_link_dto_test.dart | ||||
| test/update_tag_dto_test.dart | ||||
| test/update_user_dto_test.dart | ||||
| test/upsert_device_info_dto_test.dart | ||||
|   | ||||
							
								
								
									
										6
									
								
								mobile/openapi/README.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										6
									
								
								mobile/openapi/README.md
									
									
									
										generated
									
									
									
								
							| @@ -3,7 +3,7 @@ Immich API | ||||
| 
 | ||||
| This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project: | ||||
| 
 | ||||
| - API version: 1.46.1 | ||||
| - API version: 1.47.2 | ||||
| - Build package: org.openapitools.codegen.languages.DartClientCodegen | ||||
| 
 | ||||
| ## Requirements | ||||
| @@ -75,6 +75,7 @@ Class | Method | HTTP request | Description | ||||
| *AlbumApi* | [**removeAssetFromAlbum**](doc//AlbumApi.md#removeassetfromalbum) | **DELETE** /album/{albumId}/assets |  | ||||
| *AlbumApi* | [**removeUserFromAlbum**](doc//AlbumApi.md#removeuserfromalbum) | **DELETE** /album/{albumId}/user/{userId} |  | ||||
| *AlbumApi* | [**updateAlbumInfo**](doc//AlbumApi.md#updatealbuminfo) | **PATCH** /album/{albumId} |  | ||||
| *AssetApi* | [**addAssetsToSharedLink**](doc//AssetApi.md#addassetstosharedlink) | **PATCH** /asset/shared-link/add |  | ||||
| *AssetApi* | [**checkDuplicateAsset**](doc//AssetApi.md#checkduplicateasset) | **POST** /asset/check |  | ||||
| *AssetApi* | [**checkExistingAssets**](doc//AssetApi.md#checkexistingassets) | **POST** /asset/exist |  | ||||
| *AssetApi* | [**createAssetsSharedLink**](doc//AssetApi.md#createassetssharedlink) | **POST** /asset/shared-link |  | ||||
| @@ -92,10 +93,10 @@ Class | Method | HTTP request | Description | ||||
| *AssetApi* | [**getCuratedLocations**](doc//AssetApi.md#getcuratedlocations) | **GET** /asset/curated-locations |  | ||||
| *AssetApi* | [**getCuratedObjects**](doc//AssetApi.md#getcuratedobjects) | **GET** /asset/curated-objects |  | ||||
| *AssetApi* | [**getUserAssetsByDeviceId**](doc//AssetApi.md#getuserassetsbydeviceid) | **GET** /asset/{deviceId} |  | ||||
| *AssetApi* | [**removeAssetsFromSharedLink**](doc//AssetApi.md#removeassetsfromsharedlink) | **PATCH** /asset/shared-link/remove |  | ||||
| *AssetApi* | [**searchAsset**](doc//AssetApi.md#searchasset) | **POST** /asset/search |  | ||||
| *AssetApi* | [**serveFile**](doc//AssetApi.md#servefile) | **GET** /asset/file/{assetId} |  | ||||
| *AssetApi* | [**updateAsset**](doc//AssetApi.md#updateasset) | **PUT** /asset/{assetId} |  | ||||
| *AssetApi* | [**updateAssetsInSharedLink**](doc//AssetApi.md#updateassetsinsharedlink) | **PATCH** /asset/shared-link |  | ||||
| *AssetApi* | [**uploadFile**](doc//AssetApi.md#uploadfile) | **POST** /asset/upload |  | ||||
| *AuthenticationApi* | [**adminSignUp**](doc//AuthenticationApi.md#adminsignup) | **POST** /auth/admin-sign-up |  | ||||
| *AuthenticationApi* | [**changePassword**](doc//AuthenticationApi.md#changepassword) | **POST** /auth/change-password |  | ||||
| @@ -214,7 +215,6 @@ Class | Method | HTTP request | Description | ||||
|  - [TimeGroupEnum](doc//TimeGroupEnum.md) | ||||
|  - [UpdateAlbumDto](doc//UpdateAlbumDto.md) | ||||
|  - [UpdateAssetDto](doc//UpdateAssetDto.md) | ||||
|  - [UpdateAssetsToSharedLinkDto](doc//UpdateAssetsToSharedLinkDto.md) | ||||
|  - [UpdateTagDto](doc//UpdateTagDto.md) | ||||
|  - [UpdateUserDto](doc//UpdateUserDto.md) | ||||
|  - [UpsertDeviceInfoDto](doc//UpsertDeviceInfoDto.md) | ||||
|   | ||||
							
								
								
									
										150
									
								
								mobile/openapi/doc/AssetApi.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										150
									
								
								mobile/openapi/doc/AssetApi.md
									
									
									
										generated
									
									
									
								
							| @@ -9,6 +9,7 @@ All URIs are relative to */api* | ||||
| 
 | ||||
| Method | HTTP request | Description | ||||
| ------------- | ------------- | ------------- | ||||
| [**addAssetsToSharedLink**](AssetApi.md#addassetstosharedlink) | **PATCH** /asset/shared-link/add |  | ||||
| [**checkDuplicateAsset**](AssetApi.md#checkduplicateasset) | **POST** /asset/check |  | ||||
| [**checkExistingAssets**](AssetApi.md#checkexistingassets) | **POST** /asset/exist |  | ||||
| [**createAssetsSharedLink**](AssetApi.md#createassetssharedlink) | **POST** /asset/shared-link |  | ||||
| @@ -26,13 +27,62 @@ Method | HTTP request | Description | ||||
| [**getCuratedLocations**](AssetApi.md#getcuratedlocations) | **GET** /asset/curated-locations |  | ||||
| [**getCuratedObjects**](AssetApi.md#getcuratedobjects) | **GET** /asset/curated-objects |  | ||||
| [**getUserAssetsByDeviceId**](AssetApi.md#getuserassetsbydeviceid) | **GET** /asset/{deviceId} |  | ||||
| [**removeAssetsFromSharedLink**](AssetApi.md#removeassetsfromsharedlink) | **PATCH** /asset/shared-link/remove |  | ||||
| [**searchAsset**](AssetApi.md#searchasset) | **POST** /asset/search |  | ||||
| [**serveFile**](AssetApi.md#servefile) | **GET** /asset/file/{assetId} |  | ||||
| [**updateAsset**](AssetApi.md#updateasset) | **PUT** /asset/{assetId} |  | ||||
| [**updateAssetsInSharedLink**](AssetApi.md#updateassetsinsharedlink) | **PATCH** /asset/shared-link |  | ||||
| [**uploadFile**](AssetApi.md#uploadfile) | **POST** /asset/upload |  | ||||
| 
 | ||||
| 
 | ||||
| # **addAssetsToSharedLink** | ||||
| > SharedLinkResponseDto addAssetsToSharedLink(addAssetsDto) | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| ### Example | ||||
| ```dart | ||||
| import 'package:openapi/api.dart'; | ||||
| // TODO Configure HTTP Bearer authorization: bearer | ||||
| // Case 1. Use String Token | ||||
| //defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); | ||||
| // Case 2. Use Function which generate token. | ||||
| // String yourTokenGeneratorFunction() { ... } | ||||
| //defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction); | ||||
| 
 | ||||
| final api_instance = AssetApi(); | ||||
| final addAssetsDto = AddAssetsDto(); // AddAssetsDto |  | ||||
| 
 | ||||
| try { | ||||
|     final result = api_instance.addAssetsToSharedLink(addAssetsDto); | ||||
|     print(result); | ||||
| } catch (e) { | ||||
|     print('Exception when calling AssetApi->addAssetsToSharedLink: $e\n'); | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| ### Parameters | ||||
| 
 | ||||
| Name | Type | Description  | Notes | ||||
| ------------- | ------------- | ------------- | ------------- | ||||
|  **addAssetsDto** | [**AddAssetsDto**](AddAssetsDto.md)|  |  | ||||
| 
 | ||||
| ### Return type | ||||
| 
 | ||||
| [**SharedLinkResponseDto**](SharedLinkResponseDto.md) | ||||
| 
 | ||||
| ### Authorization | ||||
| 
 | ||||
| [bearer](../README.md#bearer) | ||||
| 
 | ||||
| ### HTTP request headers | ||||
| 
 | ||||
|  - **Content-Type**: application/json | ||||
|  - **Accept**: application/json | ||||
| 
 | ||||
| [[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) | ||||
| 
 | ||||
| # **checkDuplicateAsset** | ||||
| > CheckDuplicateAssetResponseDto checkDuplicateAsset(checkDuplicateAssetDto) | ||||
| 
 | ||||
| @@ -856,6 +906,55 @@ 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) | ||||
| 
 | ||||
| # **removeAssetsFromSharedLink** | ||||
| > SharedLinkResponseDto removeAssetsFromSharedLink(removeAssetsDto) | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| ### Example | ||||
| ```dart | ||||
| import 'package:openapi/api.dart'; | ||||
| // TODO Configure HTTP Bearer authorization: bearer | ||||
| // Case 1. Use String Token | ||||
| //defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); | ||||
| // Case 2. Use Function which generate token. | ||||
| // String yourTokenGeneratorFunction() { ... } | ||||
| //defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction); | ||||
| 
 | ||||
| final api_instance = AssetApi(); | ||||
| final removeAssetsDto = RemoveAssetsDto(); // RemoveAssetsDto |  | ||||
| 
 | ||||
| try { | ||||
|     final result = api_instance.removeAssetsFromSharedLink(removeAssetsDto); | ||||
|     print(result); | ||||
| } catch (e) { | ||||
|     print('Exception when calling AssetApi->removeAssetsFromSharedLink: $e\n'); | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| ### Parameters | ||||
| 
 | ||||
| Name | Type | Description  | Notes | ||||
| ------------- | ------------- | ------------- | ------------- | ||||
|  **removeAssetsDto** | [**RemoveAssetsDto**](RemoveAssetsDto.md)|  |  | ||||
| 
 | ||||
| ### Return type | ||||
| 
 | ||||
| [**SharedLinkResponseDto**](SharedLinkResponseDto.md) | ||||
| 
 | ||||
| ### Authorization | ||||
| 
 | ||||
| [bearer](../README.md#bearer) | ||||
| 
 | ||||
| ### HTTP request headers | ||||
| 
 | ||||
|  - **Content-Type**: application/json | ||||
|  - **Accept**: application/json | ||||
| 
 | ||||
| [[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) | ||||
| 
 | ||||
| # **searchAsset** | ||||
| > List<AssetResponseDto> searchAsset(searchAssetDto) | ||||
| 
 | ||||
| @@ -1009,55 +1108,6 @@ 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) | ||||
| 
 | ||||
| # **updateAssetsInSharedLink** | ||||
| > SharedLinkResponseDto updateAssetsInSharedLink(updateAssetsToSharedLinkDto) | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| ### Example | ||||
| ```dart | ||||
| import 'package:openapi/api.dart'; | ||||
| // TODO Configure HTTP Bearer authorization: bearer | ||||
| // Case 1. Use String Token | ||||
| //defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); | ||||
| // Case 2. Use Function which generate token. | ||||
| // String yourTokenGeneratorFunction() { ... } | ||||
| //defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction); | ||||
| 
 | ||||
| final api_instance = AssetApi(); | ||||
| final updateAssetsToSharedLinkDto = UpdateAssetsToSharedLinkDto(); // UpdateAssetsToSharedLinkDto |  | ||||
| 
 | ||||
| try { | ||||
|     final result = api_instance.updateAssetsInSharedLink(updateAssetsToSharedLinkDto); | ||||
|     print(result); | ||||
| } catch (e) { | ||||
|     print('Exception when calling AssetApi->updateAssetsInSharedLink: $e\n'); | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| ### Parameters | ||||
| 
 | ||||
| Name | Type | Description  | Notes | ||||
| ------------- | ------------- | ------------- | ------------- | ||||
|  **updateAssetsToSharedLinkDto** | [**UpdateAssetsToSharedLinkDto**](UpdateAssetsToSharedLinkDto.md)|  |  | ||||
| 
 | ||||
| ### Return type | ||||
| 
 | ||||
| [**SharedLinkResponseDto**](SharedLinkResponseDto.md) | ||||
| 
 | ||||
| ### Authorization | ||||
| 
 | ||||
| [bearer](../README.md#bearer) | ||||
| 
 | ||||
| ### HTTP request headers | ||||
| 
 | ||||
|  - **Content-Type**: application/json | ||||
|  - **Accept**: application/json | ||||
| 
 | ||||
| [[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) | ||||
| 
 | ||||
| # **uploadFile** | ||||
| > AssetFileUploadResponseDto uploadFile(assetType, assetData, deviceAssetId, deviceId, createdAt, modifiedAt, isFavorite, fileExtension, livePhotoData, isVisible, duration) | ||||
| 
 | ||||
|   | ||||
							
								
								
									
										15
									
								
								mobile/openapi/doc/UpdateAssetsToSharedLinkDto.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										15
									
								
								mobile/openapi/doc/UpdateAssetsToSharedLinkDto.md
									
									
									
										generated
									
									
									
								
							| @@ -1,15 +0,0 @@ | ||||
| # openapi.model.UpdateAssetsToSharedLinkDto | ||||
| 
 | ||||
| ## Load the model package | ||||
| ```dart | ||||
| import 'package:openapi/api.dart'; | ||||
| ``` | ||||
| 
 | ||||
| ## Properties | ||||
| Name | Type | Description | Notes | ||||
| ------------ | ------------- | ------------- | ------------- | ||||
| **assetIds** | **List<String>** |  | [default to const []] | ||||
| 
 | ||||
| [[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
									
								
								mobile/openapi/lib/api.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1
									
								
								mobile/openapi/lib/api.dart
									
									
									
										generated
									
									
									
								
							| @@ -113,7 +113,6 @@ part 'model/thumbnail_format.dart'; | ||||
| part 'model/time_group_enum.dart'; | ||||
| part 'model/update_album_dto.dart'; | ||||
| part 'model/update_asset_dto.dart'; | ||||
| part 'model/update_assets_to_shared_link_dto.dart'; | ||||
| part 'model/update_tag_dto.dart'; | ||||
| part 'model/update_user_dto.dart'; | ||||
| part 'model/upsert_device_info_dto.dart'; | ||||
|   | ||||
							
								
								
									
										156
									
								
								mobile/openapi/lib/api/asset_api.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										156
									
								
								mobile/openapi/lib/api/asset_api.dart
									
									
									
										generated
									
									
									
								
							| @@ -16,6 +16,58 @@ class AssetApi { | ||||
| 
 | ||||
|   final ApiClient apiClient; | ||||
| 
 | ||||
|   ///  | ||||
|   /// | ||||
|   /// Note: This method returns the HTTP [Response]. | ||||
|   /// | ||||
|   /// Parameters: | ||||
|   /// | ||||
|   /// * [AddAssetsDto] addAssetsDto (required): | ||||
|   Future<Response> addAssetsToSharedLinkWithHttpInfo(AddAssetsDto addAssetsDto,) async { | ||||
|     // ignore: prefer_const_declarations | ||||
|     final path = r'/asset/shared-link/add'; | ||||
| 
 | ||||
|     // ignore: prefer_final_locals | ||||
|     Object? postBody = addAssetsDto; | ||||
| 
 | ||||
|     final queryParams = <QueryParam>[]; | ||||
|     final headerParams = <String, String>{}; | ||||
|     final formParams = <String, String>{}; | ||||
| 
 | ||||
|     const contentTypes = <String>['application/json']; | ||||
| 
 | ||||
| 
 | ||||
|     return apiClient.invokeAPI( | ||||
|       path, | ||||
|       'PATCH', | ||||
|       queryParams, | ||||
|       postBody, | ||||
|       headerParams, | ||||
|       formParams, | ||||
|       contentTypes.isEmpty ? null : contentTypes.first, | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   ///  | ||||
|   /// | ||||
|   /// Parameters: | ||||
|   /// | ||||
|   /// * [AddAssetsDto] addAssetsDto (required): | ||||
|   Future<SharedLinkResponseDto?> addAssetsToSharedLink(AddAssetsDto addAssetsDto,) async { | ||||
|     final response = await addAssetsToSharedLinkWithHttpInfo(addAssetsDto,); | ||||
|     if (response.statusCode >= HttpStatus.badRequest) { | ||||
|       throw ApiException(response.statusCode, await _decodeBodyBytes(response)); | ||||
|     } | ||||
|     // When a remote server returns no body with a status of 204, we shall not decode it. | ||||
|     // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" | ||||
|     // FormatException when trying to decode an empty string. | ||||
|     if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { | ||||
|       return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'SharedLinkResponseDto',) as SharedLinkResponseDto; | ||||
|      | ||||
|     } | ||||
|     return null; | ||||
|   } | ||||
| 
 | ||||
|   /// Check duplicated asset before uploading - for Web upload used | ||||
|   /// | ||||
|   /// Note: This method returns the HTTP [Response]. | ||||
| @@ -926,6 +978,58 @@ class AssetApi { | ||||
|     return null; | ||||
|   } | ||||
| 
 | ||||
|   ///  | ||||
|   /// | ||||
|   /// Note: This method returns the HTTP [Response]. | ||||
|   /// | ||||
|   /// Parameters: | ||||
|   /// | ||||
|   /// * [RemoveAssetsDto] removeAssetsDto (required): | ||||
|   Future<Response> removeAssetsFromSharedLinkWithHttpInfo(RemoveAssetsDto removeAssetsDto,) async { | ||||
|     // ignore: prefer_const_declarations | ||||
|     final path = r'/asset/shared-link/remove'; | ||||
| 
 | ||||
|     // ignore: prefer_final_locals | ||||
|     Object? postBody = removeAssetsDto; | ||||
| 
 | ||||
|     final queryParams = <QueryParam>[]; | ||||
|     final headerParams = <String, String>{}; | ||||
|     final formParams = <String, String>{}; | ||||
| 
 | ||||
|     const contentTypes = <String>['application/json']; | ||||
| 
 | ||||
| 
 | ||||
|     return apiClient.invokeAPI( | ||||
|       path, | ||||
|       'PATCH', | ||||
|       queryParams, | ||||
|       postBody, | ||||
|       headerParams, | ||||
|       formParams, | ||||
|       contentTypes.isEmpty ? null : contentTypes.first, | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   ///  | ||||
|   /// | ||||
|   /// Parameters: | ||||
|   /// | ||||
|   /// * [RemoveAssetsDto] removeAssetsDto (required): | ||||
|   Future<SharedLinkResponseDto?> removeAssetsFromSharedLink(RemoveAssetsDto removeAssetsDto,) async { | ||||
|     final response = await removeAssetsFromSharedLinkWithHttpInfo(removeAssetsDto,); | ||||
|     if (response.statusCode >= HttpStatus.badRequest) { | ||||
|       throw ApiException(response.statusCode, await _decodeBodyBytes(response)); | ||||
|     } | ||||
|     // When a remote server returns no body with a status of 204, we shall not decode it. | ||||
|     // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" | ||||
|     // FormatException when trying to decode an empty string. | ||||
|     if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { | ||||
|       return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'SharedLinkResponseDto',) as SharedLinkResponseDto; | ||||
|      | ||||
|     } | ||||
|     return null; | ||||
|   } | ||||
| 
 | ||||
|   ///  | ||||
|   /// | ||||
|   /// Note: This method returns the HTTP [Response]. | ||||
| @@ -1106,58 +1210,6 @@ class AssetApi { | ||||
|     return null; | ||||
|   } | ||||
| 
 | ||||
|   ///  | ||||
|   /// | ||||
|   /// Note: This method returns the HTTP [Response]. | ||||
|   /// | ||||
|   /// Parameters: | ||||
|   /// | ||||
|   /// * [UpdateAssetsToSharedLinkDto] updateAssetsToSharedLinkDto (required): | ||||
|   Future<Response> updateAssetsInSharedLinkWithHttpInfo(UpdateAssetsToSharedLinkDto updateAssetsToSharedLinkDto,) async { | ||||
|     // ignore: prefer_const_declarations | ||||
|     final path = r'/asset/shared-link'; | ||||
| 
 | ||||
|     // ignore: prefer_final_locals | ||||
|     Object? postBody = updateAssetsToSharedLinkDto; | ||||
| 
 | ||||
|     final queryParams = <QueryParam>[]; | ||||
|     final headerParams = <String, String>{}; | ||||
|     final formParams = <String, String>{}; | ||||
| 
 | ||||
|     const contentTypes = <String>['application/json']; | ||||
| 
 | ||||
| 
 | ||||
|     return apiClient.invokeAPI( | ||||
|       path, | ||||
|       'PATCH', | ||||
|       queryParams, | ||||
|       postBody, | ||||
|       headerParams, | ||||
|       formParams, | ||||
|       contentTypes.isEmpty ? null : contentTypes.first, | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   ///  | ||||
|   /// | ||||
|   /// Parameters: | ||||
|   /// | ||||
|   /// * [UpdateAssetsToSharedLinkDto] updateAssetsToSharedLinkDto (required): | ||||
|   Future<SharedLinkResponseDto?> updateAssetsInSharedLink(UpdateAssetsToSharedLinkDto updateAssetsToSharedLinkDto,) async { | ||||
|     final response = await updateAssetsInSharedLinkWithHttpInfo(updateAssetsToSharedLinkDto,); | ||||
|     if (response.statusCode >= HttpStatus.badRequest) { | ||||
|       throw ApiException(response.statusCode, await _decodeBodyBytes(response)); | ||||
|     } | ||||
|     // When a remote server returns no body with a status of 204, we shall not decode it. | ||||
|     // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" | ||||
|     // FormatException when trying to decode an empty string. | ||||
|     if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { | ||||
|       return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'SharedLinkResponseDto',) as SharedLinkResponseDto; | ||||
|      | ||||
|     } | ||||
|     return null; | ||||
|   } | ||||
| 
 | ||||
|   ///  | ||||
|   /// | ||||
|   /// Note: This method returns the HTTP [Response]. | ||||
|   | ||||
							
								
								
									
										2
									
								
								mobile/openapi/lib/api_client.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								mobile/openapi/lib/api_client.dart
									
									
									
										generated
									
									
									
								
							| @@ -336,8 +336,6 @@ class ApiClient { | ||||
|           return UpdateAlbumDto.fromJson(value); | ||||
|         case 'UpdateAssetDto': | ||||
|           return UpdateAssetDto.fromJson(value); | ||||
|         case 'UpdateAssetsToSharedLinkDto': | ||||
|           return UpdateAssetsToSharedLinkDto.fromJson(value); | ||||
|         case 'UpdateTagDto': | ||||
|           return UpdateTagDto.fromJson(value); | ||||
|         case 'UpdateUserDto': | ||||
|   | ||||
| @@ -1,113 +0,0 @@ | ||||
| // | ||||
| // AUTO-GENERATED FILE, DO NOT MODIFY! | ||||
| // | ||||
| // @dart=2.12 | ||||
| 
 | ||||
| // ignore_for_file: unused_element, unused_import | ||||
| // ignore_for_file: always_put_required_named_parameters_first | ||||
| // ignore_for_file: constant_identifier_names | ||||
| // ignore_for_file: lines_longer_than_80_chars | ||||
| 
 | ||||
| part of openapi.api; | ||||
| 
 | ||||
| class UpdateAssetsToSharedLinkDto { | ||||
|   /// Returns a new [UpdateAssetsToSharedLinkDto] instance. | ||||
|   UpdateAssetsToSharedLinkDto({ | ||||
|     this.assetIds = const [], | ||||
|   }); | ||||
| 
 | ||||
|   List<String> assetIds; | ||||
| 
 | ||||
|   @override | ||||
|   bool operator ==(Object other) => identical(this, other) || other is UpdateAssetsToSharedLinkDto && | ||||
|      other.assetIds == assetIds; | ||||
| 
 | ||||
|   @override | ||||
|   int get hashCode => | ||||
|     // ignore: unnecessary_parenthesis | ||||
|     (assetIds.hashCode); | ||||
| 
 | ||||
|   @override | ||||
|   String toString() => 'UpdateAssetsToSharedLinkDto[assetIds=$assetIds]'; | ||||
| 
 | ||||
|   Map<String, dynamic> toJson() { | ||||
|     final json = <String, dynamic>{}; | ||||
|       json[r'assetIds'] = this.assetIds; | ||||
|     return json; | ||||
|   } | ||||
| 
 | ||||
|   /// Returns a new [UpdateAssetsToSharedLinkDto] instance and imports its values from | ||||
|   /// [value] if it's a [Map], null otherwise. | ||||
|   // ignore: prefer_constructors_over_static_methods | ||||
|   static UpdateAssetsToSharedLinkDto? fromJson(dynamic value) { | ||||
|     if (value is Map) { | ||||
|       final json = value.cast<String, dynamic>(); | ||||
| 
 | ||||
|       // 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 "UpdateAssetsToSharedLinkDto[$key]" is missing from JSON.'); | ||||
|           assert(json[key] != null, 'Required key "UpdateAssetsToSharedLinkDto[$key]" has a null value in JSON.'); | ||||
|         }); | ||||
|         return true; | ||||
|       }()); | ||||
| 
 | ||||
|       return UpdateAssetsToSharedLinkDto( | ||||
|         assetIds: json[r'assetIds'] is List | ||||
|             ? (json[r'assetIds'] as List).cast<String>() | ||||
|             : const [], | ||||
|       ); | ||||
|     } | ||||
|     return null; | ||||
|   } | ||||
| 
 | ||||
|   static List<UpdateAssetsToSharedLinkDto>? listFromJson(dynamic json, {bool growable = false,}) { | ||||
|     final result = <UpdateAssetsToSharedLinkDto>[]; | ||||
|     if (json is List && json.isNotEmpty) { | ||||
|       for (final row in json) { | ||||
|         final value = UpdateAssetsToSharedLinkDto.fromJson(row); | ||||
|         if (value != null) { | ||||
|           result.add(value); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     return result.toList(growable: growable); | ||||
|   } | ||||
| 
 | ||||
|   static Map<String, UpdateAssetsToSharedLinkDto> mapFromJson(dynamic json) { | ||||
|     final map = <String, UpdateAssetsToSharedLinkDto>{}; | ||||
|     if (json is Map && json.isNotEmpty) { | ||||
|       json = json.cast<String, dynamic>(); // ignore: parameter_assignments | ||||
|       for (final entry in json.entries) { | ||||
|         final value = UpdateAssetsToSharedLinkDto.fromJson(entry.value); | ||||
|         if (value != null) { | ||||
|           map[entry.key] = value; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     return map; | ||||
|   } | ||||
| 
 | ||||
|   // maps a json object with a list of UpdateAssetsToSharedLinkDto-objects as value to a dart map | ||||
|   static Map<String, List<UpdateAssetsToSharedLinkDto>> mapListFromJson(dynamic json, {bool growable = false,}) { | ||||
|     final map = <String, List<UpdateAssetsToSharedLinkDto>>{}; | ||||
|     if (json is Map && json.isNotEmpty) { | ||||
|       json = json.cast<String, dynamic>(); // ignore: parameter_assignments | ||||
|       for (final entry in json.entries) { | ||||
|         final value = UpdateAssetsToSharedLinkDto.listFromJson(entry.value, growable: growable,); | ||||
|         if (value != null) { | ||||
|           map[entry.key] = value; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     return map; | ||||
|   } | ||||
| 
 | ||||
|   /// The list of required keys that must be present in a JSON. | ||||
|   static const requiredKeys = <String>{ | ||||
|     'assetIds', | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
							
								
								
									
										21
									
								
								mobile/openapi/test/asset_api_test.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										21
									
								
								mobile/openapi/test/asset_api_test.dart
									
									
									
										generated
									
									
									
								
							| @@ -17,6 +17,13 @@ void main() { | ||||
|   // final instance = AssetApi(); | ||||
| 
 | ||||
|   group('tests for AssetApi', () { | ||||
|     //  | ||||
|     // | ||||
|     //Future<SharedLinkResponseDto> addAssetsToSharedLink(AddAssetsDto addAssetsDto) async | ||||
|     test('test addAssetsToSharedLink', () async { | ||||
|       // TODO | ||||
|     }); | ||||
| 
 | ||||
|     // Check duplicated asset before uploading - for Web upload used | ||||
|     // | ||||
|     //Future<CheckDuplicateAssetResponseDto> checkDuplicateAsset(CheckDuplicateAssetDto checkDuplicateAssetDto) async | ||||
| @@ -136,6 +143,13 @@ void main() { | ||||
|       // TODO | ||||
|     }); | ||||
| 
 | ||||
|     //  | ||||
|     // | ||||
|     //Future<SharedLinkResponseDto> removeAssetsFromSharedLink(RemoveAssetsDto removeAssetsDto) async | ||||
|     test('test removeAssetsFromSharedLink', () async { | ||||
|       // TODO | ||||
|     }); | ||||
| 
 | ||||
|     //  | ||||
|     // | ||||
|     //Future<List<AssetResponseDto>> searchAsset(SearchAssetDto searchAssetDto) async | ||||
| @@ -157,13 +171,6 @@ void main() { | ||||
|       // TODO | ||||
|     }); | ||||
| 
 | ||||
|     //  | ||||
|     // | ||||
|     //Future<SharedLinkResponseDto> updateAssetsInSharedLink(UpdateAssetsToSharedLinkDto updateAssetsToSharedLinkDto) async | ||||
|     test('test updateAssetsInSharedLink', () async { | ||||
|       // TODO | ||||
|     }); | ||||
| 
 | ||||
|     //  | ||||
|     // | ||||
|     //Future<AssetFileUploadResponseDto> uploadFile(AssetTypeEnum assetType, MultipartFile assetData, String deviceAssetId, String deviceId, String createdAt, String modifiedAt, bool isFavorite, String fileExtension, { MultipartFile livePhotoData, bool isVisible, String duration }) async | ||||
|   | ||||
| @@ -1,27 +0,0 @@ | ||||
| // | ||||
| // AUTO-GENERATED FILE, DO NOT MODIFY! | ||||
| // | ||||
| // @dart=2.12 | ||||
| 
 | ||||
| // ignore_for_file: unused_element, unused_import | ||||
| // ignore_for_file: always_put_required_named_parameters_first | ||||
| // ignore_for_file: constant_identifier_names | ||||
| // ignore_for_file: lines_longer_than_80_chars | ||||
| 
 | ||||
| import 'package:openapi/api.dart'; | ||||
| import 'package:test/test.dart'; | ||||
| 
 | ||||
| // tests for UpdateAssetsToSharedLinkDto | ||||
| void main() { | ||||
|   // final instance = UpdateAssetsToSharedLinkDto(); | ||||
| 
 | ||||
|   group('test UpdateAssetsToSharedLinkDto', () { | ||||
|     // List<String> assetIds (default value: const []) | ||||
|     test('to test the property `assetIds`', () async { | ||||
|       // TODO | ||||
|     }); | ||||
| 
 | ||||
| 
 | ||||
|   }); | ||||
| 
 | ||||
| } | ||||
| @@ -1,3 +1,4 @@ | ||||
| import { AddAssetsDto } from './../album/dto/add-assets.dto'; | ||||
| import { | ||||
|   Controller, | ||||
|   Post, | ||||
| @@ -52,10 +53,10 @@ import { | ||||
| import { DownloadFilesDto } from './dto/download-files.dto'; | ||||
| import { CreateAssetsShareLinkDto } from './dto/create-asset-shared-link.dto'; | ||||
| import { SharedLinkResponseDto } from '@app/domain'; | ||||
| 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'; | ||||
| import FileNotEmptyValidator from '../validation/file-not-empty-validator'; | ||||
| import { RemoveAssetsDto } from '../album/dto/remove-assets.dto'; | ||||
|  | ||||
| function asStreamableFile({ stream, type, length }: ImmichReadStream) { | ||||
|   return new StreamableFile(stream, { type, length }); | ||||
| @@ -330,11 +331,20 @@ export class AssetController { | ||||
|   } | ||||
|  | ||||
|   @Authenticated({ isShared: true }) | ||||
|   @Patch('/shared-link') | ||||
|   async updateAssetsInSharedLink( | ||||
|   @Patch('/shared-link/add') | ||||
|   async addAssetsToSharedLink( | ||||
|     @GetAuthUser() authUser: AuthUserDto, | ||||
|     @Body(ValidationPipe) dto: UpdateAssetsToSharedLinkDto, | ||||
|     @Body(ValidationPipe) dto: AddAssetsDto, | ||||
|   ): Promise<SharedLinkResponseDto> { | ||||
|     return await this.assetService.updateAssetsInSharedLink(authUser, dto); | ||||
|     return await this.assetService.addAssetsToSharedLink(authUser, dto); | ||||
|   } | ||||
|  | ||||
|   @Authenticated({ isShared: true }) | ||||
|   @Patch('/shared-link/remove') | ||||
|   async removeAssetsFromSharedLink( | ||||
|     @GetAuthUser() authUser: AuthUserDto, | ||||
|     @Body(ValidationPipe) dto: RemoveAssetsDto, | ||||
|   ): Promise<SharedLinkResponseDto> { | ||||
|     return await this.assetService.removeAssetsFromSharedLink(authUser, dto); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -198,14 +198,31 @@ describe('AssetService', () => { | ||||
|       sharedLinkRepositoryMock.get.mockResolvedValue(null); | ||||
|       sharedLinkRepositoryMock.hasAssetAccess.mockResolvedValue(true); | ||||
|  | ||||
|       await expect(sut.updateAssetsInSharedLink(authDto, dto)).rejects.toBeInstanceOf(BadRequestException); | ||||
|       await expect(sut.addAssetsToSharedLink(authDto, dto)).rejects.toBeInstanceOf(BadRequestException); | ||||
|  | ||||
|       expect(assetRepositoryMock.getById).toHaveBeenCalledWith(asset1.id); | ||||
|       expect(sharedLinkRepositoryMock.get).toHaveBeenCalledWith(authDto.id, authDto.sharedLinkId); | ||||
|       expect(sharedLinkRepositoryMock.hasAssetAccess).toHaveBeenCalledWith(authDto.sharedLinkId, asset1.id); | ||||
|       expect(sharedLinkRepositoryMock.save).not.toHaveBeenCalled(); | ||||
|     }); | ||||
|  | ||||
|     it('should add assets to a shared link', async () => { | ||||
|       const asset1 = _getAsset_1(); | ||||
|  | ||||
|       const authDto = authStub.adminSharedLink; | ||||
|       const dto = { assetIds: [asset1.id] }; | ||||
|  | ||||
|       assetRepositoryMock.getById.mockResolvedValue(asset1); | ||||
|       sharedLinkRepositoryMock.get.mockResolvedValue(sharedLinkStub.valid); | ||||
|       sharedLinkRepositoryMock.hasAssetAccess.mockResolvedValue(true); | ||||
|       sharedLinkRepositoryMock.save.mockResolvedValue(sharedLinkStub.valid); | ||||
|  | ||||
|       await expect(sut.addAssetsToSharedLink(authDto, dto)).resolves.toEqual(sharedLinkResponseStub.valid); | ||||
|  | ||||
|       expect(assetRepositoryMock.getById).toHaveBeenCalledWith(asset1.id); | ||||
|       expect(sharedLinkRepositoryMock.get).toHaveBeenCalledWith(authDto.id, authDto.sharedLinkId); | ||||
|       expect(sharedLinkRepositoryMock.save).toHaveBeenCalled(); | ||||
|     }); | ||||
|  | ||||
|     it('should remove assets from a shared link', async () => { | ||||
|       const asset1 = _getAsset_1(); | ||||
|  | ||||
| @@ -217,11 +234,11 @@ describe('AssetService', () => { | ||||
|       sharedLinkRepositoryMock.hasAssetAccess.mockResolvedValue(true); | ||||
|       sharedLinkRepositoryMock.save.mockResolvedValue(sharedLinkStub.valid); | ||||
|  | ||||
|       await expect(sut.updateAssetsInSharedLink(authDto, dto)).resolves.toEqual(sharedLinkResponseStub.valid); | ||||
|       await expect(sut.removeAssetsFromSharedLink(authDto, dto)).resolves.toEqual(sharedLinkResponseStub.valid); | ||||
|  | ||||
|       expect(assetRepositoryMock.getById).toHaveBeenCalledWith(asset1.id); | ||||
|       expect(sharedLinkRepositoryMock.get).toHaveBeenCalledWith(authDto.id, authDto.sharedLinkId); | ||||
|       expect(sharedLinkRepositoryMock.hasAssetAccess).toHaveBeenCalledWith(authDto.sharedLinkId, asset1.id); | ||||
|       expect(sharedLinkRepositoryMock.save).toHaveBeenCalled(); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   | ||||
| @@ -58,8 +58,9 @@ import { ISharedLinkRepository } from '@app/domain'; | ||||
| import { DownloadFilesDto } from './dto/download-files.dto'; | ||||
| import { CreateAssetsShareLinkDto } from './dto/create-asset-shared-link.dto'; | ||||
| import { mapSharedLink, SharedLinkResponseDto } from '@app/domain'; | ||||
| import { UpdateAssetsToSharedLinkDto } from './dto/add-assets-to-shared-link.dto'; | ||||
| import { AssetSearchDto } from './dto/asset-search.dto'; | ||||
| import { AddAssetsDto } from '../album/dto/add-assets.dto'; | ||||
| import { RemoveAssetsDto } from '../album/dto/remove-assets.dto'; | ||||
|  | ||||
| const fileInfo = promisify(stat); | ||||
|  | ||||
| @@ -606,23 +607,35 @@ export class AssetService { | ||||
|     return mapSharedLink(sharedLink); | ||||
|   } | ||||
|  | ||||
|   async updateAssetsInSharedLink( | ||||
|     authUser: AuthUserDto, | ||||
|     dto: UpdateAssetsToSharedLinkDto, | ||||
|   ): Promise<SharedLinkResponseDto> { | ||||
|   async addAssetsToSharedLink(authUser: AuthUserDto, dto: AddAssetsDto): Promise<SharedLinkResponseDto> { | ||||
|     if (!authUser.sharedLinkId) { | ||||
|       throw new ForbiddenException(); | ||||
|     } | ||||
|  | ||||
|     const assets = []; | ||||
|  | ||||
|     await this.checkAssetsAccess(authUser, dto.assetIds); | ||||
|     for (const assetId of dto.assetIds) { | ||||
|       const asset = await this._assetRepository.getById(assetId); | ||||
|       assets.push(asset); | ||||
|     } | ||||
|  | ||||
|     const updatedLink = await this.shareCore.updateAssets(authUser.id, authUser.sharedLinkId, assets); | ||||
|     const updatedLink = await this.shareCore.addAssets(authUser.id, authUser.sharedLinkId, assets); | ||||
|     return mapSharedLink(updatedLink); | ||||
|   } | ||||
|  | ||||
|   async removeAssetsFromSharedLink(authUser: AuthUserDto, dto: RemoveAssetsDto): Promise<SharedLinkResponseDto> { | ||||
|     if (!authUser.sharedLinkId) { | ||||
|       throw new ForbiddenException(); | ||||
|     } | ||||
|  | ||||
|     const assets = []; | ||||
|  | ||||
|     for (const assetId of dto.assetIds) { | ||||
|       const asset = await this._assetRepository.getById(assetId); | ||||
|       assets.push(asset); | ||||
|     } | ||||
|  | ||||
|     const updatedLink = await this.shareCore.removeAssets(authUser.id, authUser.sharedLinkId, assets); | ||||
|     return mapSharedLink(updatedLink); | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -1,6 +0,0 @@ | ||||
| import { IsNotEmpty } from 'class-validator'; | ||||
|  | ||||
| export class UpdateAssetsToSharedLinkDto { | ||||
|   @IsNotEmpty() | ||||
|   assetIds!: string[]; | ||||
| } | ||||
| @@ -1869,9 +1869,11 @@ | ||||
|             "bearer": [] | ||||
|           } | ||||
|         ] | ||||
|       }, | ||||
|       } | ||||
|     }, | ||||
|     "/asset/shared-link/add": { | ||||
|       "patch": { | ||||
|         "operationId": "updateAssetsInSharedLink", | ||||
|         "operationId": "addAssetsToSharedLink", | ||||
|         "description": "", | ||||
|         "parameters": [], | ||||
|         "requestBody": { | ||||
| @@ -1879,7 +1881,44 @@ | ||||
|           "content": { | ||||
|             "application/json": { | ||||
|               "schema": { | ||||
|                 "$ref": "#/components/schemas/UpdateAssetsToSharedLinkDto" | ||||
|                 "$ref": "#/components/schemas/AddAssetsDto" | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|         }, | ||||
|         "responses": { | ||||
|           "200": { | ||||
|             "description": "", | ||||
|             "content": { | ||||
|               "application/json": { | ||||
|                 "schema": { | ||||
|                   "$ref": "#/components/schemas/SharedLinkResponseDto" | ||||
|                 } | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|         }, | ||||
|         "tags": [ | ||||
|           "Asset" | ||||
|         ], | ||||
|         "security": [ | ||||
|           { | ||||
|             "bearer": [] | ||||
|           } | ||||
|         ] | ||||
|       } | ||||
|     }, | ||||
|     "/asset/shared-link/remove": { | ||||
|       "patch": { | ||||
|         "operationId": "removeAssetsFromSharedLink", | ||||
|         "description": "", | ||||
|         "parameters": [], | ||||
|         "requestBody": { | ||||
|           "required": true, | ||||
|           "content": { | ||||
|             "application/json": { | ||||
|               "schema": { | ||||
|                 "$ref": "#/components/schemas/RemoveAssetsDto" | ||||
|               } | ||||
|             } | ||||
|           } | ||||
| @@ -4171,7 +4210,21 @@ | ||||
|           "assetIds" | ||||
|         ] | ||||
|       }, | ||||
|       "UpdateAssetsToSharedLinkDto": { | ||||
|       "AddAssetsDto": { | ||||
|         "type": "object", | ||||
|         "properties": { | ||||
|           "assetIds": { | ||||
|             "type": "array", | ||||
|             "items": { | ||||
|               "type": "string" | ||||
|             } | ||||
|           } | ||||
|         }, | ||||
|         "required": [ | ||||
|           "assetIds" | ||||
|         ] | ||||
|       }, | ||||
|       "RemoveAssetsDto": { | ||||
|         "type": "object", | ||||
|         "properties": { | ||||
|           "assetIds": { | ||||
| @@ -4267,20 +4320,6 @@ | ||||
|           "sharedUserIds" | ||||
|         ] | ||||
|       }, | ||||
|       "AddAssetsDto": { | ||||
|         "type": "object", | ||||
|         "properties": { | ||||
|           "assetIds": { | ||||
|             "type": "array", | ||||
|             "items": { | ||||
|               "type": "string" | ||||
|             } | ||||
|           } | ||||
|         }, | ||||
|         "required": [ | ||||
|           "assetIds" | ||||
|         ] | ||||
|       }, | ||||
|       "AddAssetsResponseDto": { | ||||
|         "type": "object", | ||||
|         "properties": { | ||||
| @@ -4302,20 +4341,6 @@ | ||||
|           "alreadyInAlbum" | ||||
|         ] | ||||
|       }, | ||||
|       "RemoveAssetsDto": { | ||||
|         "type": "object", | ||||
|         "properties": { | ||||
|           "assetIds": { | ||||
|             "type": "array", | ||||
|             "items": { | ||||
|               "type": "string" | ||||
|             } | ||||
|           } | ||||
|         }, | ||||
|         "required": [ | ||||
|           "assetIds" | ||||
|         ] | ||||
|       }, | ||||
|       "UpdateAlbumDto": { | ||||
|         "type": "object", | ||||
|         "properties": { | ||||
|   | ||||
| @@ -63,13 +63,24 @@ export class ShareCore { | ||||
|     return this.repository.remove(link); | ||||
|   } | ||||
|  | ||||
|   async updateAssets(userId: string, id: string, assets: AssetEntity[]) { | ||||
|   async addAssets(userId: string, id: string, assets: AssetEntity[]) { | ||||
|     const link = await this.get(userId, id); | ||||
|     if (!link) { | ||||
|       throw new BadRequestException('Shared link not found'); | ||||
|     } | ||||
|  | ||||
|     return this.repository.save({ ...link, assets }); | ||||
|     return this.repository.save({ ...link, assets: [...link.assets, ...assets] }); | ||||
|   } | ||||
|  | ||||
|   async removeAssets(userId: string, id: string, assets: AssetEntity[]) { | ||||
|     const link = await this.get(userId, id); | ||||
|     if (!link) { | ||||
|       throw new BadRequestException('Shared link not found'); | ||||
|     } | ||||
|  | ||||
|     const newAssets = link.assets.filter((asset) => assets.find((a) => a.id === asset.id)); | ||||
|  | ||||
|     return this.repository.save({ ...link, assets: newAssets }); | ||||
|   } | ||||
|  | ||||
|   async hasAssetAccess(id: string, assetId: string): Promise<boolean> { | ||||
|   | ||||
| @@ -140,9 +140,9 @@ | ||||
|       }, | ||||
|       "./libs/domain/": { | ||||
|         "branches": 80, | ||||
|         "functions": 89, | ||||
|         "functions": 88, | ||||
|         "lines": 95, | ||||
|         "statements": 95 | ||||
|         "statements": 94 | ||||
|       } | ||||
|     }, | ||||
|     "testEnvironment": "node", | ||||
|   | ||||
							
								
								
									
										222
									
								
								web/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										222
									
								
								web/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							| @@ -4,7 +4,7 @@ | ||||
|  * Immich | ||||
|  * Immich API | ||||
|  * | ||||
|  * The version of the OpenAPI document: 1.46.1 | ||||
|  * The version of the OpenAPI document: 1.47.2 | ||||
|  *  | ||||
|  * | ||||
|  * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
 | ||||
| @@ -2083,19 +2083,6 @@ export interface UpdateAssetDto { | ||||
|      */ | ||||
|     'isFavorite'?: boolean; | ||||
| } | ||||
| /** | ||||
|  *  | ||||
|  * @export | ||||
|  * @interface UpdateAssetsToSharedLinkDto | ||||
|  */ | ||||
| export interface UpdateAssetsToSharedLinkDto { | ||||
|     /** | ||||
|      *  | ||||
|      * @type {Array<string>} | ||||
|      * @memberof UpdateAssetsToSharedLinkDto | ||||
|      */ | ||||
|     'assetIds': Array<string>; | ||||
| } | ||||
| /** | ||||
|  *  | ||||
|  * @export | ||||
| @@ -3588,6 +3575,45 @@ export class AlbumApi extends BaseAPI { | ||||
|  */ | ||||
| export const AssetApiAxiosParamCreator = function (configuration?: Configuration) { | ||||
|     return { | ||||
|         /** | ||||
|          *  | ||||
|          * @param {AddAssetsDto} addAssetsDto  | ||||
|          * @param {*} [options] Override http request option. | ||||
|          * @throws {RequiredError} | ||||
|          */ | ||||
|         addAssetsToSharedLink: async (addAssetsDto: AddAssetsDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => { | ||||
|             // verify required parameter 'addAssetsDto' is not null or undefined
 | ||||
|             assertParamExists('addAssetsToSharedLink', 'addAssetsDto', addAssetsDto) | ||||
|             const localVarPath = `/asset/shared-link/add`; | ||||
|             // use dummy base URL string because the URL constructor only accepts absolute URLs.
 | ||||
|             const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); | ||||
|             let baseOptions; | ||||
|             if (configuration) { | ||||
|                 baseOptions = configuration.baseOptions; | ||||
|             } | ||||
| 
 | ||||
|             const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options}; | ||||
|             const localVarHeaderParameter = {} as any; | ||||
|             const localVarQueryParameter = {} as any; | ||||
| 
 | ||||
|             // authentication bearer required
 | ||||
|             // http bearer authentication required
 | ||||
|             await setBearerAuthToObject(localVarHeaderParameter, configuration) | ||||
| 
 | ||||
| 
 | ||||
|      | ||||
|             localVarHeaderParameter['Content-Type'] = 'application/json'; | ||||
| 
 | ||||
|             setSearchParams(localVarUrlObj, localVarQueryParameter); | ||||
|             let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; | ||||
|             localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; | ||||
|             localVarRequestOptions.data = serializeDataIfNeeded(addAssetsDto, localVarRequestOptions, configuration) | ||||
| 
 | ||||
|             return { | ||||
|                 url: toPathString(localVarUrlObj), | ||||
|                 options: localVarRequestOptions, | ||||
|             }; | ||||
|         }, | ||||
|         /** | ||||
|          * Check duplicated asset before uploading - for Web upload used | ||||
|          * @param {CheckDuplicateAssetDto} checkDuplicateAssetDto  | ||||
| @@ -4232,6 +4258,45 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration | ||||
|                 options: localVarRequestOptions, | ||||
|             }; | ||||
|         }, | ||||
|         /** | ||||
|          *  | ||||
|          * @param {RemoveAssetsDto} removeAssetsDto  | ||||
|          * @param {*} [options] Override http request option. | ||||
|          * @throws {RequiredError} | ||||
|          */ | ||||
|         removeAssetsFromSharedLink: async (removeAssetsDto: RemoveAssetsDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => { | ||||
|             // verify required parameter 'removeAssetsDto' is not null or undefined
 | ||||
|             assertParamExists('removeAssetsFromSharedLink', 'removeAssetsDto', removeAssetsDto) | ||||
|             const localVarPath = `/asset/shared-link/remove`; | ||||
|             // use dummy base URL string because the URL constructor only accepts absolute URLs.
 | ||||
|             const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); | ||||
|             let baseOptions; | ||||
|             if (configuration) { | ||||
|                 baseOptions = configuration.baseOptions; | ||||
|             } | ||||
| 
 | ||||
|             const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options}; | ||||
|             const localVarHeaderParameter = {} as any; | ||||
|             const localVarQueryParameter = {} as any; | ||||
| 
 | ||||
|             // authentication bearer required
 | ||||
|             // http bearer authentication required
 | ||||
|             await setBearerAuthToObject(localVarHeaderParameter, configuration) | ||||
| 
 | ||||
| 
 | ||||
|      | ||||
|             localVarHeaderParameter['Content-Type'] = 'application/json'; | ||||
| 
 | ||||
|             setSearchParams(localVarUrlObj, localVarQueryParameter); | ||||
|             let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; | ||||
|             localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; | ||||
|             localVarRequestOptions.data = serializeDataIfNeeded(removeAssetsDto, localVarRequestOptions, configuration) | ||||
| 
 | ||||
|             return { | ||||
|                 url: toPathString(localVarUrlObj), | ||||
|                 options: localVarRequestOptions, | ||||
|             }; | ||||
|         }, | ||||
|         /** | ||||
|          *  | ||||
|          * @param {SearchAssetDto} searchAssetDto  | ||||
| @@ -4361,45 +4426,6 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration | ||||
|                 options: localVarRequestOptions, | ||||
|             }; | ||||
|         }, | ||||
|         /** | ||||
|          *  | ||||
|          * @param {UpdateAssetsToSharedLinkDto} updateAssetsToSharedLinkDto  | ||||
|          * @param {*} [options] Override http request option. | ||||
|          * @throws {RequiredError} | ||||
|          */ | ||||
|         updateAssetsInSharedLink: async (updateAssetsToSharedLinkDto: UpdateAssetsToSharedLinkDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => { | ||||
|             // verify required parameter 'updateAssetsToSharedLinkDto' is not null or undefined
 | ||||
|             assertParamExists('updateAssetsInSharedLink', 'updateAssetsToSharedLinkDto', updateAssetsToSharedLinkDto) | ||||
|             const localVarPath = `/asset/shared-link`; | ||||
|             // use dummy base URL string because the URL constructor only accepts absolute URLs.
 | ||||
|             const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); | ||||
|             let baseOptions; | ||||
|             if (configuration) { | ||||
|                 baseOptions = configuration.baseOptions; | ||||
|             } | ||||
| 
 | ||||
|             const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options}; | ||||
|             const localVarHeaderParameter = {} as any; | ||||
|             const localVarQueryParameter = {} as any; | ||||
| 
 | ||||
|             // authentication bearer required
 | ||||
|             // http bearer authentication required
 | ||||
|             await setBearerAuthToObject(localVarHeaderParameter, configuration) | ||||
| 
 | ||||
| 
 | ||||
|      | ||||
|             localVarHeaderParameter['Content-Type'] = 'application/json'; | ||||
| 
 | ||||
|             setSearchParams(localVarUrlObj, localVarQueryParameter); | ||||
|             let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; | ||||
|             localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; | ||||
|             localVarRequestOptions.data = serializeDataIfNeeded(updateAssetsToSharedLinkDto, localVarRequestOptions, configuration) | ||||
| 
 | ||||
|             return { | ||||
|                 url: toPathString(localVarUrlObj), | ||||
|                 options: localVarRequestOptions, | ||||
|             }; | ||||
|         }, | ||||
|         /** | ||||
|          *  | ||||
|          * @param {AssetTypeEnum} assetType  | ||||
| @@ -4518,6 +4544,16 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration | ||||
| export const AssetApiFp = function(configuration?: Configuration) { | ||||
|     const localVarAxiosParamCreator = AssetApiAxiosParamCreator(configuration) | ||||
|     return { | ||||
|         /** | ||||
|          *  | ||||
|          * @param {AddAssetsDto} addAssetsDto  | ||||
|          * @param {*} [options] Override http request option. | ||||
|          * @throws {RequiredError} | ||||
|          */ | ||||
|         async addAssetsToSharedLink(addAssetsDto: AddAssetsDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SharedLinkResponseDto>> { | ||||
|             const localVarAxiosArgs = await localVarAxiosParamCreator.addAssetsToSharedLink(addAssetsDto, options); | ||||
|             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); | ||||
|         }, | ||||
|         /** | ||||
|          * Check duplicated asset before uploading - for Web upload used | ||||
|          * @param {CheckDuplicateAssetDto} checkDuplicateAssetDto  | ||||
| @@ -4687,6 +4723,16 @@ export const AssetApiFp = function(configuration?: Configuration) { | ||||
|             const localVarAxiosArgs = await localVarAxiosParamCreator.getUserAssetsByDeviceId(deviceId, options); | ||||
|             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); | ||||
|         }, | ||||
|         /** | ||||
|          *  | ||||
|          * @param {RemoveAssetsDto} removeAssetsDto  | ||||
|          * @param {*} [options] Override http request option. | ||||
|          * @throws {RequiredError} | ||||
|          */ | ||||
|         async removeAssetsFromSharedLink(removeAssetsDto: RemoveAssetsDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SharedLinkResponseDto>> { | ||||
|             const localVarAxiosArgs = await localVarAxiosParamCreator.removeAssetsFromSharedLink(removeAssetsDto, options); | ||||
|             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); | ||||
|         }, | ||||
|         /** | ||||
|          *  | ||||
|          * @param {SearchAssetDto} searchAssetDto  | ||||
| @@ -4720,16 +4766,6 @@ export const AssetApiFp = function(configuration?: Configuration) { | ||||
|             const localVarAxiosArgs = await localVarAxiosParamCreator.updateAsset(assetId, updateAssetDto, options); | ||||
|             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); | ||||
|         }, | ||||
|         /** | ||||
|          *  | ||||
|          * @param {UpdateAssetsToSharedLinkDto} updateAssetsToSharedLinkDto  | ||||
|          * @param {*} [options] Override http request option. | ||||
|          * @throws {RequiredError} | ||||
|          */ | ||||
|         async updateAssetsInSharedLink(updateAssetsToSharedLinkDto: UpdateAssetsToSharedLinkDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SharedLinkResponseDto>> { | ||||
|             const localVarAxiosArgs = await localVarAxiosParamCreator.updateAssetsInSharedLink(updateAssetsToSharedLinkDto, options); | ||||
|             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); | ||||
|         }, | ||||
|         /** | ||||
|          *  | ||||
|          * @param {AssetTypeEnum} assetType  | ||||
| @@ -4760,6 +4796,15 @@ export const AssetApiFp = function(configuration?: Configuration) { | ||||
| export const AssetApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { | ||||
|     const localVarFp = AssetApiFp(configuration) | ||||
|     return { | ||||
|         /** | ||||
|          *  | ||||
|          * @param {AddAssetsDto} addAssetsDto  | ||||
|          * @param {*} [options] Override http request option. | ||||
|          * @throws {RequiredError} | ||||
|          */ | ||||
|         addAssetsToSharedLink(addAssetsDto: AddAssetsDto, options?: any): AxiosPromise<SharedLinkResponseDto> { | ||||
|             return localVarFp.addAssetsToSharedLink(addAssetsDto, options).then((request) => request(axios, basePath)); | ||||
|         }, | ||||
|         /** | ||||
|          * Check duplicated asset before uploading - for Web upload used | ||||
|          * @param {CheckDuplicateAssetDto} checkDuplicateAssetDto  | ||||
| @@ -4912,6 +4957,15 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath | ||||
|         getUserAssetsByDeviceId(deviceId: string, options?: any): AxiosPromise<Array<string>> { | ||||
|             return localVarFp.getUserAssetsByDeviceId(deviceId, options).then((request) => request(axios, basePath)); | ||||
|         }, | ||||
|         /** | ||||
|          *  | ||||
|          * @param {RemoveAssetsDto} removeAssetsDto  | ||||
|          * @param {*} [options] Override http request option. | ||||
|          * @throws {RequiredError} | ||||
|          */ | ||||
|         removeAssetsFromSharedLink(removeAssetsDto: RemoveAssetsDto, options?: any): AxiosPromise<SharedLinkResponseDto> { | ||||
|             return localVarFp.removeAssetsFromSharedLink(removeAssetsDto, options).then((request) => request(axios, basePath)); | ||||
|         }, | ||||
|         /** | ||||
|          *  | ||||
|          * @param {SearchAssetDto} searchAssetDto  | ||||
| @@ -4942,15 +4996,6 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath | ||||
|         updateAsset(assetId: string, updateAssetDto: UpdateAssetDto, options?: any): AxiosPromise<AssetResponseDto> { | ||||
|             return localVarFp.updateAsset(assetId, updateAssetDto, options).then((request) => request(axios, basePath)); | ||||
|         }, | ||||
|         /** | ||||
|          *  | ||||
|          * @param {UpdateAssetsToSharedLinkDto} updateAssetsToSharedLinkDto  | ||||
|          * @param {*} [options] Override http request option. | ||||
|          * @throws {RequiredError} | ||||
|          */ | ||||
|         updateAssetsInSharedLink(updateAssetsToSharedLinkDto: UpdateAssetsToSharedLinkDto, options?: any): AxiosPromise<SharedLinkResponseDto> { | ||||
|             return localVarFp.updateAssetsInSharedLink(updateAssetsToSharedLinkDto, options).then((request) => request(axios, basePath)); | ||||
|         }, | ||||
|         /** | ||||
|          *  | ||||
|          * @param {AssetTypeEnum} assetType  | ||||
| @@ -4980,6 +5025,17 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath | ||||
|  * @extends {BaseAPI} | ||||
|  */ | ||||
| export class AssetApi extends BaseAPI { | ||||
|     /** | ||||
|      *  | ||||
|      * @param {AddAssetsDto} addAssetsDto  | ||||
|      * @param {*} [options] Override http request option. | ||||
|      * @throws {RequiredError} | ||||
|      * @memberof AssetApi | ||||
|      */ | ||||
|     public addAssetsToSharedLink(addAssetsDto: AddAssetsDto, options?: AxiosRequestConfig) { | ||||
|         return AssetApiFp(this.configuration).addAssetsToSharedLink(addAssetsDto, options).then((request) => request(this.axios, this.basePath)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check duplicated asset before uploading - for Web upload used | ||||
|      * @param {CheckDuplicateAssetDto} checkDuplicateAssetDto  | ||||
| @@ -5166,6 +5222,17 @@ export class AssetApi extends BaseAPI { | ||||
|         return AssetApiFp(this.configuration).getUserAssetsByDeviceId(deviceId, options).then((request) => request(this.axios, this.basePath)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      *  | ||||
|      * @param {RemoveAssetsDto} removeAssetsDto  | ||||
|      * @param {*} [options] Override http request option. | ||||
|      * @throws {RequiredError} | ||||
|      * @memberof AssetApi | ||||
|      */ | ||||
|     public removeAssetsFromSharedLink(removeAssetsDto: RemoveAssetsDto, options?: AxiosRequestConfig) { | ||||
|         return AssetApiFp(this.configuration).removeAssetsFromSharedLink(removeAssetsDto, options).then((request) => request(this.axios, this.basePath)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      *  | ||||
|      * @param {SearchAssetDto} searchAssetDto  | ||||
| @@ -5202,17 +5269,6 @@ export class AssetApi extends BaseAPI { | ||||
|         return AssetApiFp(this.configuration).updateAsset(assetId, updateAssetDto, options).then((request) => request(this.axios, this.basePath)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      *  | ||||
|      * @param {UpdateAssetsToSharedLinkDto} updateAssetsToSharedLinkDto  | ||||
|      * @param {*} [options] Override http request option. | ||||
|      * @throws {RequiredError} | ||||
|      * @memberof AssetApi | ||||
|      */ | ||||
|     public updateAssetsInSharedLink(updateAssetsToSharedLinkDto: UpdateAssetsToSharedLinkDto, options?: AxiosRequestConfig) { | ||||
|         return AssetApiFp(this.configuration).updateAssetsInSharedLink(updateAssetsToSharedLinkDto, options).then((request) => request(this.axios, this.basePath)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      *  | ||||
|      * @param {AssetTypeEnum} assetType  | ||||
|   | ||||
							
								
								
									
										2
									
								
								web/src/api/open-api/base.ts
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								web/src/api/open-api/base.ts
									
									
									
										generated
									
									
									
								
							| @@ -4,7 +4,7 @@ | ||||
|  * Immich | ||||
|  * Immich API | ||||
|  * | ||||
|  * The version of the OpenAPI document: 1.46.1 | ||||
|  * The version of the OpenAPI document: 1.47.2 | ||||
|  *  | ||||
|  * | ||||
|  * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
 | ||||
|   | ||||
							
								
								
									
										2
									
								
								web/src/api/open-api/common.ts
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								web/src/api/open-api/common.ts
									
									
									
										generated
									
									
									
								
							| @@ -4,7 +4,7 @@ | ||||
|  * Immich | ||||
|  * Immich API | ||||
|  * | ||||
|  * The version of the OpenAPI document: 1.46.1 | ||||
|  * The version of the OpenAPI document: 1.47.2 | ||||
|  *  | ||||
|  * | ||||
|  * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
 | ||||
|   | ||||
							
								
								
									
										2
									
								
								web/src/api/open-api/configuration.ts
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								web/src/api/open-api/configuration.ts
									
									
									
										generated
									
									
									
								
							| @@ -4,7 +4,7 @@ | ||||
|  * Immich | ||||
|  * Immich API | ||||
|  * | ||||
|  * The version of the OpenAPI document: 1.46.1 | ||||
|  * The version of the OpenAPI document: 1.47.2 | ||||
|  *  | ||||
|  * | ||||
|  * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
 | ||||
|   | ||||
							
								
								
									
										2
									
								
								web/src/api/open-api/index.ts
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								web/src/api/open-api/index.ts
									
									
									
										generated
									
									
									
								
							| @@ -4,7 +4,7 @@ | ||||
|  * Immich | ||||
|  * Immich API | ||||
|  * | ||||
|  * The version of the OpenAPI document: 1.46.1 | ||||
|  * The version of the OpenAPI document: 1.47.2 | ||||
|  *  | ||||
|  * | ||||
|  * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
 | ||||
|   | ||||
| @@ -16,6 +16,7 @@ | ||||
|  | ||||
| 	export let albumId: string; | ||||
| 	export let assetsInAlbum: AssetResponseDto[]; | ||||
| 	const locale = navigator.language; | ||||
|  | ||||
| 	onMount(() => { | ||||
| 		$assetsInAlbumStoreState = assetsInAlbum; | ||||
| @@ -28,8 +29,11 @@ | ||||
|  | ||||
| 		assetInteractionStore.clearMultiselect(); | ||||
| 	}; | ||||
|  | ||||
| 	const locale = navigator.language; | ||||
| 	const handleSelectFromComputerClicked = async () => { | ||||
| 		await openFileUploadDialog(albumId, ''); | ||||
| 		assetInteractionStore.clearMultiselect(); | ||||
| 		dispatch('go-back'); | ||||
| 	}; | ||||
| </script> | ||||
|  | ||||
| <section | ||||
| @@ -54,11 +58,7 @@ | ||||
|  | ||||
| 		<svelte:fragment slot="trailing"> | ||||
| 			<button | ||||
| 				on:click={() => | ||||
| 					openFileUploadDialog(albumId, '', () => { | ||||
| 						assetInteractionStore.clearMultiselect(); | ||||
| 						dispatch('go-back'); | ||||
| 					})} | ||||
| 				on:click={handleSelectFromComputerClicked} | ||||
| 				class="text-immich-primary dark:text-immich-dark-primary text-sm hover:bg-immich-primary/10 dark:hover:bg-immich-dark-primary/25 transition-all px-6 py-2 rounded-lg font-medium" | ||||
| 			> | ||||
| 				Select from computer | ||||
|   | ||||
| @@ -13,11 +13,11 @@ | ||||
| 	import CloudDownloadOutline from 'svelte-material-icons/CloudDownloadOutline.svelte'; | ||||
| 	import GalleryViewer from '../shared-components/gallery-viewer/gallery-viewer.svelte'; | ||||
| 	import DeleteOutline from 'svelte-material-icons/DeleteOutline.svelte'; | ||||
| 	import ImmichLogo from '../shared-components/immich-logo.svelte'; | ||||
| 	import { | ||||
| 		notificationController, | ||||
| 		NotificationType | ||||
| 	} from '../shared-components/notification/notification'; | ||||
| 	import ImmichLogo from '../shared-components/immich-logo.svelte'; | ||||
|  | ||||
| 	export let sharedLink: SharedLinkResponseDto; | ||||
| 	export let isOwned: boolean; | ||||
| @@ -43,11 +43,15 @@ | ||||
| 		); | ||||
| 	}; | ||||
|  | ||||
| 	const handleUploadAssets = () => { | ||||
| 		openFileUploadDialog(undefined, sharedLink?.key, async (assetId) => { | ||||
| 			await api.assetApi.updateAssetsInSharedLink( | ||||
| 	const handleUploadAssets = async () => { | ||||
| 		try { | ||||
| 			const results = await openFileUploadDialog(undefined, sharedLink?.key); | ||||
|  | ||||
| 			const assetIds = results.filter((id) => !!id) as string[]; | ||||
|  | ||||
| 			await api.assetApi.addAssetsToSharedLink( | ||||
| 				{ | ||||
| 					assetIds: [...assets.map((a) => a.id), assetId] | ||||
| 					assetIds | ||||
| 				}, | ||||
| 				{ | ||||
| 					params: { | ||||
| @@ -57,15 +61,17 @@ | ||||
| 			); | ||||
|  | ||||
| 			notificationController.show({ | ||||
| 				message: 'Add asset to shared link successfully', | ||||
| 				message: `Successfully add ${assetIds.length} to the shared link`, | ||||
| 				type: NotificationType.Info | ||||
| 			}); | ||||
| 		}); | ||||
| 		} catch (e) { | ||||
| 			console.error('handleUploadAssets', e); | ||||
| 		} | ||||
| 	}; | ||||
|  | ||||
| 	const handleRemoveAssetsFromSharedLink = async () => { | ||||
| 		if (window.confirm('Do you want to remove selected assets from the shared link?')) { | ||||
| 			await api.assetApi.updateAssetsInSharedLink( | ||||
| 			await api.assetApi.removeAssetsFromSharedLink( | ||||
| 				{ | ||||
| 					assetIds: assets.filter((a) => !selectedAssets.has(a)).map((a) => a.id) | ||||
| 				}, | ||||
|   | ||||
| @@ -6,71 +6,65 @@ import { uploadAssetsStore } from '$lib/stores/upload'; | ||||
| import type { UploadAsset } from '../models/upload-asset'; | ||||
| import { api, AssetFileUploadResponseDto } from '@api'; | ||||
| import { addAssetsToAlbum, getFileMimeType, getFilenameExtension } from '$lib/utils/asset-utils'; | ||||
| import { Subject, mergeMap } from 'rxjs'; | ||||
| import { mergeMap, filter, firstValueFrom, from, of, combineLatestAll } from 'rxjs'; | ||||
| import axios from 'axios'; | ||||
|  | ||||
| export const openFileUploadDialog = ( | ||||
| export const openFileUploadDialog = async ( | ||||
| 	albumId: string | undefined = undefined, | ||||
| 	sharedKey: string | undefined = undefined, | ||||
| 	onDone?: (id: string) => void | ||||
| 	sharedKey: string | undefined = undefined | ||||
| ) => { | ||||
| 	try { | ||||
| 		const fileSelector = document.createElement('input'); | ||||
| 	return new Promise<(string | undefined)[]>((resolve, reject) => { | ||||
| 		try { | ||||
| 			const fileSelector = document.createElement('input'); | ||||
|  | ||||
| 		fileSelector.type = 'file'; | ||||
| 		fileSelector.multiple = true; | ||||
| 			fileSelector.type = 'file'; | ||||
| 			fileSelector.multiple = true; | ||||
|  | ||||
| 		// When adding a content type that is unsupported by browsers, make sure | ||||
| 		// to also add it to getFileMimeType() otherwise the upload will fail. | ||||
| 		fileSelector.accept = 'image/*,video/*,.heic,.heif,.dng,.3gp,.nef,.srw,.raf'; | ||||
| 			// When adding a content type that is unsupported by browsers, make sure | ||||
| 			// to also add it to getFileMimeType() otherwise the upload will fail. | ||||
| 			fileSelector.accept = 'image/*,video/*,.heic,.heif,.dng,.3gp,.nef,.srw,.raf'; | ||||
|  | ||||
| 		fileSelector.onchange = async (e: Event) => { | ||||
| 			const target = e.target as HTMLInputElement; | ||||
| 			if (!target.files) { | ||||
| 				return; | ||||
| 			} | ||||
| 			const files = Array.from<File>(target.files); | ||||
| 			fileSelector.onchange = async (e: Event) => { | ||||
| 				const target = e.target as HTMLInputElement; | ||||
| 				if (!target.files) { | ||||
| 					return; | ||||
| 				} | ||||
| 				const files = Array.from<File>(target.files); | ||||
|  | ||||
| 			await fileUploadHandler(files, albumId, sharedKey, onDone); | ||||
| 		}; | ||||
| 				resolve(await fileUploadHandler(files, albumId, sharedKey)); | ||||
| 			}; | ||||
|  | ||||
| 		fileSelector.click(); | ||||
| 	} catch (e) { | ||||
| 		console.log('Error selecting file', e); | ||||
| 	} | ||||
| 			fileSelector.click(); | ||||
| 		} catch (e) { | ||||
| 			console.log('Error selecting file', e); | ||||
| 			reject(e); | ||||
| 		} | ||||
| 	}); | ||||
| }; | ||||
|  | ||||
| export const fileUploadHandler = async ( | ||||
| 	files: File[], | ||||
| 	albumId: string | undefined = undefined, | ||||
| 	sharedKey: string | undefined = undefined, | ||||
| 	onDone?: (id: string) => void | ||||
| 	sharedKey: string | undefined = undefined | ||||
| ) => { | ||||
| 	const files$ = new Subject<File>(); | ||||
| 	files$ | ||||
| 		.pipe( | ||||
| 			mergeMap(async (file) => { | ||||
| 				await fileUploader(file, albumId, sharedKey, onDone); | ||||
| 			}, 2) | ||||
| 	return firstValueFrom( | ||||
| 		from(files).pipe( | ||||
| 			filter((file) => { | ||||
| 				const assetType = getFileMimeType(file).split('/')[0]; | ||||
| 				return assetType === 'video' || assetType === 'image'; | ||||
| 			}), | ||||
| 			mergeMap(async (file) => of(await fileUploader(file, albumId, sharedKey)), 2), | ||||
| 			combineLatestAll() | ||||
| 		) | ||||
| 		.subscribe(); | ||||
|  | ||||
| 	const acceptedFile = files.filter((file) => { | ||||
| 		const assetType = getFileMimeType(file).split('/')[0]; | ||||
| 		return assetType === 'video' || assetType === 'image'; | ||||
| 	}); | ||||
| 	for (const file of acceptedFile) { | ||||
| 		files$.next(file); | ||||
| 	} | ||||
| 	); | ||||
| }; | ||||
|  | ||||
| //TODO: should probably use the @api SDK | ||||
| async function fileUploader( | ||||
| 	asset: File, | ||||
| 	albumId: string | undefined = undefined, | ||||
| 	sharedKey: string | undefined = undefined, | ||||
| 	onDone?: (id: string) => void | ||||
| ) { | ||||
| 	console.log('uploading', asset.name); | ||||
| 	sharedKey: string | undefined = undefined | ||||
| ): Promise<string | undefined> { | ||||
| 	const mimeType = getFileMimeType(asset); | ||||
| 	const assetType = mimeType.split('/')[0].toUpperCase(); | ||||
| 	const fileExtension = getFilenameExtension(asset.name); | ||||
| @@ -121,67 +115,50 @@ async function fileUploader( | ||||
| 			} | ||||
| 		); | ||||
|  | ||||
| 		if (status === 200) { | ||||
| 			if (data.isExist) { | ||||
| 				const dataId = data.id; | ||||
| 				if (albumId && dataId) { | ||||
| 					addAssetsToAlbum(albumId, [dataId], sharedKey); | ||||
| 				} | ||||
| 				onDone && dataId && onDone(dataId); | ||||
| 				return; | ||||
| 		if (status === 200 && data.isExist && data.id) { | ||||
| 			if (albumId) { | ||||
| 				await addAssetsToAlbum(albumId, [data.id], sharedKey); | ||||
| 			} | ||||
|  | ||||
| 			return data.id; | ||||
| 		} | ||||
|  | ||||
| 		const request = new XMLHttpRequest(); | ||||
| 		request.upload.onloadstart = () => { | ||||
| 			const newUploadAsset: UploadAsset = { | ||||
| 				id: deviceAssetId, | ||||
| 				file: asset, | ||||
| 				progress: 0, | ||||
| 				fileExtension: fileExtension | ||||
| 			}; | ||||
|  | ||||
| 			uploadAssetsStore.addNewUploadAsset(newUploadAsset); | ||||
| 		const newUploadAsset: UploadAsset = { | ||||
| 			id: deviceAssetId, | ||||
| 			file: asset, | ||||
| 			progress: 0, | ||||
| 			fileExtension: fileExtension | ||||
| 		}; | ||||
|  | ||||
| 		request.upload.onload = () => { | ||||
| 			uploadAssetsStore.removeUploadAsset(deviceAssetId); | ||||
| 			const res: AssetFileUploadResponseDto = JSON.parse(request.response || '{}'); | ||||
| 			if (albumId) { | ||||
| 				try { | ||||
| 					if (res.id) { | ||||
| 						addAssetsToAlbum(albumId, [res.id], sharedKey); | ||||
| 					} | ||||
| 				} catch (e) { | ||||
| 					console.error('ERROR parsing data JSON in upload onload'); | ||||
| 				} | ||||
| 		uploadAssetsStore.addNewUploadAsset(newUploadAsset); | ||||
|  | ||||
| 		const response = await axios.post(`/api/asset/upload`, formData, { | ||||
| 			params: { | ||||
| 				key: sharedKey | ||||
| 			}, | ||||
| 			onUploadProgress: (event) => { | ||||
| 				const percentComplete = Math.floor((event.loaded / event.total) * 100); | ||||
| 				uploadAssetsStore.updateProgress(deviceAssetId, percentComplete); | ||||
| 			} | ||||
| 			onDone && onDone(res.id); | ||||
| 		}; | ||||
| 		}); | ||||
|  | ||||
| 		// listen for `error` event | ||||
| 		request.upload.onerror = () => { | ||||
| 			uploadAssetsStore.removeUploadAsset(deviceAssetId); | ||||
| 			handleUploadError(asset, request.response); | ||||
| 		}; | ||||
| 		if (response.status == 200 || response.status == 201) { | ||||
| 			const res: AssetFileUploadResponseDto = response.data; | ||||
|  | ||||
| 		// listen for `abort` event | ||||
| 		request.upload.onabort = () => { | ||||
| 			uploadAssetsStore.removeUploadAsset(deviceAssetId); | ||||
| 			handleUploadError(asset, request.response); | ||||
| 		}; | ||||
| 			if (albumId && res.id) { | ||||
| 				await addAssetsToAlbum(albumId, [res.id], sharedKey); | ||||
| 			} | ||||
|  | ||||
| 		// listen for `progress` event | ||||
| 		request.upload.onprogress = (event) => { | ||||
| 			const percentComplete = Math.floor((event.loaded / event.total) * 100); | ||||
| 			uploadAssetsStore.updateProgress(deviceAssetId, percentComplete); | ||||
| 		}; | ||||
| 			setTimeout(() => { | ||||
| 				uploadAssetsStore.removeUploadAsset(deviceAssetId); | ||||
| 			}, 1000); | ||||
|  | ||||
| 		request.open('POST', `/api/asset/upload?key=${sharedKey ?? ''}`); | ||||
|  | ||||
| 		request.send(formData); | ||||
| 			return res.id; | ||||
| 		} | ||||
| 	} catch (e) { | ||||
| 		console.log('error uploading file ', e); | ||||
| 		handleUploadError(asset, JSON.stringify(e)); | ||||
| 		uploadAssetsStore.removeUploadAsset(deviceAssetId); | ||||
| 	} | ||||
| } | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user