mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-04 03:39:37 -05:00 
			
		
		
		
	refactor(server): download assets (#3032)
* refactor: download assets * chore: open api * chore: finish tests, make size configurable * chore: defualt to 4GiB * chore: open api * fix: optional archive size * fix: bugs * chore: cleanup
This commit is contained in:
		
							parent
							
								
									df9c05bef3
								
							
						
					
					
						commit
						ad343b7b32
					
				
							
								
								
									
										9
									
								
								mobile/openapi/.openapi-generator/FILES
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										9
									
								
								mobile/openapi/.openapi-generator/FILES
									
									
									
										generated
									
									
									
								
							@ -45,7 +45,8 @@ doc/CuratedObjectsResponseDto.md
 | 
				
			|||||||
doc/DeleteAssetDto.md
 | 
					doc/DeleteAssetDto.md
 | 
				
			||||||
doc/DeleteAssetResponseDto.md
 | 
					doc/DeleteAssetResponseDto.md
 | 
				
			||||||
doc/DeleteAssetStatus.md
 | 
					doc/DeleteAssetStatus.md
 | 
				
			||||||
doc/DownloadFilesDto.md
 | 
					doc/DownloadArchiveInfo.md
 | 
				
			||||||
 | 
					doc/DownloadResponseDto.md
 | 
				
			||||||
doc/ExifResponseDto.md
 | 
					doc/ExifResponseDto.md
 | 
				
			||||||
doc/GetAssetByTimeBucketDto.md
 | 
					doc/GetAssetByTimeBucketDto.md
 | 
				
			||||||
doc/GetAssetCountByTimeBucketDto.md
 | 
					doc/GetAssetCountByTimeBucketDto.md
 | 
				
			||||||
@ -178,7 +179,8 @@ lib/model/curated_objects_response_dto.dart
 | 
				
			|||||||
lib/model/delete_asset_dto.dart
 | 
					lib/model/delete_asset_dto.dart
 | 
				
			||||||
lib/model/delete_asset_response_dto.dart
 | 
					lib/model/delete_asset_response_dto.dart
 | 
				
			||||||
lib/model/delete_asset_status.dart
 | 
					lib/model/delete_asset_status.dart
 | 
				
			||||||
lib/model/download_files_dto.dart
 | 
					lib/model/download_archive_info.dart
 | 
				
			||||||
 | 
					lib/model/download_response_dto.dart
 | 
				
			||||||
lib/model/exif_response_dto.dart
 | 
					lib/model/exif_response_dto.dart
 | 
				
			||||||
lib/model/get_asset_by_time_bucket_dto.dart
 | 
					lib/model/get_asset_by_time_bucket_dto.dart
 | 
				
			||||||
lib/model/get_asset_count_by_time_bucket_dto.dart
 | 
					lib/model/get_asset_count_by_time_bucket_dto.dart
 | 
				
			||||||
@ -282,7 +284,8 @@ test/curated_objects_response_dto_test.dart
 | 
				
			|||||||
test/delete_asset_dto_test.dart
 | 
					test/delete_asset_dto_test.dart
 | 
				
			||||||
test/delete_asset_response_dto_test.dart
 | 
					test/delete_asset_response_dto_test.dart
 | 
				
			||||||
test/delete_asset_status_test.dart
 | 
					test/delete_asset_status_test.dart
 | 
				
			||||||
test/download_files_dto_test.dart
 | 
					test/download_archive_info_test.dart
 | 
				
			||||||
 | 
					test/download_response_dto_test.dart
 | 
				
			||||||
test/exif_response_dto_test.dart
 | 
					test/exif_response_dto_test.dart
 | 
				
			||||||
test/get_asset_by_time_bucket_dto_test.dart
 | 
					test/get_asset_by_time_bucket_dto_test.dart
 | 
				
			||||||
test/get_asset_count_by_time_bucket_dto_test.dart
 | 
					test/get_asset_count_by_time_bucket_dto_test.dart
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										10
									
								
								mobile/openapi/README.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										10
									
								
								mobile/openapi/README.md
									
									
									
										generated
									
									
									
								
							@ -81,7 +81,6 @@ Class | Method | HTTP request | Description
 | 
				
			|||||||
*AlbumApi* | [**addUsersToAlbum**](doc//AlbumApi.md#adduserstoalbum) | **PUT** /album/{id}/users | 
 | 
					*AlbumApi* | [**addUsersToAlbum**](doc//AlbumApi.md#adduserstoalbum) | **PUT** /album/{id}/users | 
 | 
				
			||||||
*AlbumApi* | [**createAlbum**](doc//AlbumApi.md#createalbum) | **POST** /album | 
 | 
					*AlbumApi* | [**createAlbum**](doc//AlbumApi.md#createalbum) | **POST** /album | 
 | 
				
			||||||
*AlbumApi* | [**deleteAlbum**](doc//AlbumApi.md#deletealbum) | **DELETE** /album/{id} | 
 | 
					*AlbumApi* | [**deleteAlbum**](doc//AlbumApi.md#deletealbum) | **DELETE** /album/{id} | 
 | 
				
			||||||
*AlbumApi* | [**downloadArchive**](doc//AlbumApi.md#downloadarchive) | **GET** /album/{id}/download | 
 | 
					 | 
				
			||||||
*AlbumApi* | [**getAlbumCount**](doc//AlbumApi.md#getalbumcount) | **GET** /album/count | 
 | 
					*AlbumApi* | [**getAlbumCount**](doc//AlbumApi.md#getalbumcount) | **GET** /album/count | 
 | 
				
			||||||
*AlbumApi* | [**getAlbumInfo**](doc//AlbumApi.md#getalbuminfo) | **GET** /album/{id} | 
 | 
					*AlbumApi* | [**getAlbumInfo**](doc//AlbumApi.md#getalbuminfo) | **GET** /album/{id} | 
 | 
				
			||||||
*AlbumApi* | [**getAllAlbums**](doc//AlbumApi.md#getallalbums) | **GET** /album | 
 | 
					*AlbumApi* | [**getAllAlbums**](doc//AlbumApi.md#getallalbums) | **GET** /album | 
 | 
				
			||||||
@ -92,9 +91,8 @@ Class | Method | HTTP request | Description
 | 
				
			|||||||
*AssetApi* | [**checkDuplicateAsset**](doc//AssetApi.md#checkduplicateasset) | **POST** /asset/check | 
 | 
					*AssetApi* | [**checkDuplicateAsset**](doc//AssetApi.md#checkduplicateasset) | **POST** /asset/check | 
 | 
				
			||||||
*AssetApi* | [**checkExistingAssets**](doc//AssetApi.md#checkexistingassets) | **POST** /asset/exist | 
 | 
					*AssetApi* | [**checkExistingAssets**](doc//AssetApi.md#checkexistingassets) | **POST** /asset/exist | 
 | 
				
			||||||
*AssetApi* | [**deleteAsset**](doc//AssetApi.md#deleteasset) | **DELETE** /asset | 
 | 
					*AssetApi* | [**deleteAsset**](doc//AssetApi.md#deleteasset) | **DELETE** /asset | 
 | 
				
			||||||
*AssetApi* | [**downloadFile**](doc//AssetApi.md#downloadfile) | **GET** /asset/download/{id} | 
 | 
					*AssetApi* | [**downloadArchive**](doc//AssetApi.md#downloadarchive) | **POST** /asset/download | 
 | 
				
			||||||
*AssetApi* | [**downloadFiles**](doc//AssetApi.md#downloadfiles) | **POST** /asset/download-files | 
 | 
					*AssetApi* | [**downloadFile**](doc//AssetApi.md#downloadfile) | **POST** /asset/download/{id} | 
 | 
				
			||||||
*AssetApi* | [**downloadLibrary**](doc//AssetApi.md#downloadlibrary) | **GET** /asset/download-library | 
 | 
					 | 
				
			||||||
*AssetApi* | [**getAllAssets**](doc//AssetApi.md#getallassets) | **GET** /asset | 
 | 
					*AssetApi* | [**getAllAssets**](doc//AssetApi.md#getallassets) | **GET** /asset | 
 | 
				
			||||||
*AssetApi* | [**getArchivedAssetCountByUserId**](doc//AssetApi.md#getarchivedassetcountbyuserid) | **GET** /asset/stat/archive | 
 | 
					*AssetApi* | [**getArchivedAssetCountByUserId**](doc//AssetApi.md#getarchivedassetcountbyuserid) | **GET** /asset/stat/archive | 
 | 
				
			||||||
*AssetApi* | [**getAssetById**](doc//AssetApi.md#getassetbyid) | **GET** /asset/assetById/{id} | 
 | 
					*AssetApi* | [**getAssetById**](doc//AssetApi.md#getassetbyid) | **GET** /asset/assetById/{id} | 
 | 
				
			||||||
@ -105,6 +103,7 @@ Class | Method | HTTP request | Description
 | 
				
			|||||||
*AssetApi* | [**getAssetThumbnail**](doc//AssetApi.md#getassetthumbnail) | **GET** /asset/thumbnail/{id} | 
 | 
					*AssetApi* | [**getAssetThumbnail**](doc//AssetApi.md#getassetthumbnail) | **GET** /asset/thumbnail/{id} | 
 | 
				
			||||||
*AssetApi* | [**getCuratedLocations**](doc//AssetApi.md#getcuratedlocations) | **GET** /asset/curated-locations | 
 | 
					*AssetApi* | [**getCuratedLocations**](doc//AssetApi.md#getcuratedlocations) | **GET** /asset/curated-locations | 
 | 
				
			||||||
*AssetApi* | [**getCuratedObjects**](doc//AssetApi.md#getcuratedobjects) | **GET** /asset/curated-objects | 
 | 
					*AssetApi* | [**getCuratedObjects**](doc//AssetApi.md#getcuratedobjects) | **GET** /asset/curated-objects | 
 | 
				
			||||||
 | 
					*AssetApi* | [**getDownloadInfo**](doc//AssetApi.md#getdownloadinfo) | **GET** /asset/download | 
 | 
				
			||||||
*AssetApi* | [**getMapMarkers**](doc//AssetApi.md#getmapmarkers) | **GET** /asset/map-marker | 
 | 
					*AssetApi* | [**getMapMarkers**](doc//AssetApi.md#getmapmarkers) | **GET** /asset/map-marker | 
 | 
				
			||||||
*AssetApi* | [**getMemoryLane**](doc//AssetApi.md#getmemorylane) | **GET** /asset/memory-lane | 
 | 
					*AssetApi* | [**getMemoryLane**](doc//AssetApi.md#getmemorylane) | **GET** /asset/memory-lane | 
 | 
				
			||||||
*AssetApi* | [**getUserAssetsByDeviceId**](doc//AssetApi.md#getuserassetsbydeviceid) | **GET** /asset/{deviceId} | 
 | 
					*AssetApi* | [**getUserAssetsByDeviceId**](doc//AssetApi.md#getuserassetsbydeviceid) | **GET** /asset/{deviceId} | 
 | 
				
			||||||
@ -215,7 +214,8 @@ Class | Method | HTTP request | Description
 | 
				
			|||||||
 - [DeleteAssetDto](doc//DeleteAssetDto.md)
 | 
					 - [DeleteAssetDto](doc//DeleteAssetDto.md)
 | 
				
			||||||
 - [DeleteAssetResponseDto](doc//DeleteAssetResponseDto.md)
 | 
					 - [DeleteAssetResponseDto](doc//DeleteAssetResponseDto.md)
 | 
				
			||||||
 - [DeleteAssetStatus](doc//DeleteAssetStatus.md)
 | 
					 - [DeleteAssetStatus](doc//DeleteAssetStatus.md)
 | 
				
			||||||
 - [DownloadFilesDto](doc//DownloadFilesDto.md)
 | 
					 - [DownloadArchiveInfo](doc//DownloadArchiveInfo.md)
 | 
				
			||||||
 | 
					 - [DownloadResponseDto](doc//DownloadResponseDto.md)
 | 
				
			||||||
 - [ExifResponseDto](doc//ExifResponseDto.md)
 | 
					 - [ExifResponseDto](doc//ExifResponseDto.md)
 | 
				
			||||||
 - [GetAssetByTimeBucketDto](doc//GetAssetByTimeBucketDto.md)
 | 
					 - [GetAssetByTimeBucketDto](doc//GetAssetByTimeBucketDto.md)
 | 
				
			||||||
 - [GetAssetCountByTimeBucketDto](doc//GetAssetCountByTimeBucketDto.md)
 | 
					 - [GetAssetCountByTimeBucketDto](doc//GetAssetCountByTimeBucketDto.md)
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										62
									
								
								mobile/openapi/doc/AlbumApi.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										62
									
								
								mobile/openapi/doc/AlbumApi.md
									
									
									
										generated
									
									
									
								
							@ -13,7 +13,6 @@ Method | HTTP request | Description
 | 
				
			|||||||
[**addUsersToAlbum**](AlbumApi.md#adduserstoalbum) | **PUT** /album/{id}/users | 
 | 
					[**addUsersToAlbum**](AlbumApi.md#adduserstoalbum) | **PUT** /album/{id}/users | 
 | 
				
			||||||
[**createAlbum**](AlbumApi.md#createalbum) | **POST** /album | 
 | 
					[**createAlbum**](AlbumApi.md#createalbum) | **POST** /album | 
 | 
				
			||||||
[**deleteAlbum**](AlbumApi.md#deletealbum) | **DELETE** /album/{id} | 
 | 
					[**deleteAlbum**](AlbumApi.md#deletealbum) | **DELETE** /album/{id} | 
 | 
				
			||||||
[**downloadArchive**](AlbumApi.md#downloadarchive) | **GET** /album/{id}/download | 
 | 
					 | 
				
			||||||
[**getAlbumCount**](AlbumApi.md#getalbumcount) | **GET** /album/count | 
 | 
					[**getAlbumCount**](AlbumApi.md#getalbumcount) | **GET** /album/count | 
 | 
				
			||||||
[**getAlbumInfo**](AlbumApi.md#getalbuminfo) | **GET** /album/{id} | 
 | 
					[**getAlbumInfo**](AlbumApi.md#getalbuminfo) | **GET** /album/{id} | 
 | 
				
			||||||
[**getAllAlbums**](AlbumApi.md#getallalbums) | **GET** /album | 
 | 
					[**getAllAlbums**](AlbumApi.md#getallalbums) | **GET** /album | 
 | 
				
			||||||
@ -247,67 +246,6 @@ 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)
 | 
					[[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(id, name, skip, key)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### Example
 | 
					 | 
				
			||||||
```dart
 | 
					 | 
				
			||||||
import 'package:openapi/api.dart';
 | 
					 | 
				
			||||||
// TODO Configure API key authorization: cookie
 | 
					 | 
				
			||||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
 | 
					 | 
				
			||||||
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
 | 
					 | 
				
			||||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
 | 
					 | 
				
			||||||
// TODO Configure API key authorization: api_key
 | 
					 | 
				
			||||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
 | 
					 | 
				
			||||||
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
 | 
					 | 
				
			||||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
 | 
					 | 
				
			||||||
// 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 = AlbumApi();
 | 
					 | 
				
			||||||
final id = 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(id, name, skip, key);
 | 
					 | 
				
			||||||
    print(result);
 | 
					 | 
				
			||||||
} catch (e) {
 | 
					 | 
				
			||||||
    print('Exception when calling AlbumApi->downloadArchive: $e\n');
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### Parameters
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Name | Type | Description  | Notes
 | 
					 | 
				
			||||||
------------- | ------------- | ------------- | -------------
 | 
					 | 
				
			||||||
 **id** | **String**|  | 
 | 
					 | 
				
			||||||
 **name** | **String**|  | [optional] 
 | 
					 | 
				
			||||||
 **skip** | **num**|  | [optional] 
 | 
					 | 
				
			||||||
 **key** | **String**|  | [optional] 
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### Return type
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[**MultipartFile**](MultipartFile.md)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### Authorization
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### HTTP request headers
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 - **Content-Type**: Not defined
 | 
					 | 
				
			||||||
 - **Accept**: application/zip
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[[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)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# **getAlbumCount**
 | 
					# **getAlbumCount**
 | 
				
			||||||
> AlbumCountResponseDto getAlbumCount()
 | 
					> AlbumCountResponseDto getAlbumCount()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										244
									
								
								mobile/openapi/doc/AssetApi.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										244
									
								
								mobile/openapi/doc/AssetApi.md
									
									
									
										generated
									
									
									
								
							@ -13,9 +13,8 @@ Method | HTTP request | Description
 | 
				
			|||||||
[**checkDuplicateAsset**](AssetApi.md#checkduplicateasset) | **POST** /asset/check | 
 | 
					[**checkDuplicateAsset**](AssetApi.md#checkduplicateasset) | **POST** /asset/check | 
 | 
				
			||||||
[**checkExistingAssets**](AssetApi.md#checkexistingassets) | **POST** /asset/exist | 
 | 
					[**checkExistingAssets**](AssetApi.md#checkexistingassets) | **POST** /asset/exist | 
 | 
				
			||||||
[**deleteAsset**](AssetApi.md#deleteasset) | **DELETE** /asset | 
 | 
					[**deleteAsset**](AssetApi.md#deleteasset) | **DELETE** /asset | 
 | 
				
			||||||
[**downloadFile**](AssetApi.md#downloadfile) | **GET** /asset/download/{id} | 
 | 
					[**downloadArchive**](AssetApi.md#downloadarchive) | **POST** /asset/download | 
 | 
				
			||||||
[**downloadFiles**](AssetApi.md#downloadfiles) | **POST** /asset/download-files | 
 | 
					[**downloadFile**](AssetApi.md#downloadfile) | **POST** /asset/download/{id} | 
 | 
				
			||||||
[**downloadLibrary**](AssetApi.md#downloadlibrary) | **GET** /asset/download-library | 
 | 
					 | 
				
			||||||
[**getAllAssets**](AssetApi.md#getallassets) | **GET** /asset | 
 | 
					[**getAllAssets**](AssetApi.md#getallassets) | **GET** /asset | 
 | 
				
			||||||
[**getArchivedAssetCountByUserId**](AssetApi.md#getarchivedassetcountbyuserid) | **GET** /asset/stat/archive | 
 | 
					[**getArchivedAssetCountByUserId**](AssetApi.md#getarchivedassetcountbyuserid) | **GET** /asset/stat/archive | 
 | 
				
			||||||
[**getAssetById**](AssetApi.md#getassetbyid) | **GET** /asset/assetById/{id} | 
 | 
					[**getAssetById**](AssetApi.md#getassetbyid) | **GET** /asset/assetById/{id} | 
 | 
				
			||||||
@ -26,6 +25,7 @@ Method | HTTP request | Description
 | 
				
			|||||||
[**getAssetThumbnail**](AssetApi.md#getassetthumbnail) | **GET** /asset/thumbnail/{id} | 
 | 
					[**getAssetThumbnail**](AssetApi.md#getassetthumbnail) | **GET** /asset/thumbnail/{id} | 
 | 
				
			||||||
[**getCuratedLocations**](AssetApi.md#getcuratedlocations) | **GET** /asset/curated-locations | 
 | 
					[**getCuratedLocations**](AssetApi.md#getcuratedlocations) | **GET** /asset/curated-locations | 
 | 
				
			||||||
[**getCuratedObjects**](AssetApi.md#getcuratedobjects) | **GET** /asset/curated-objects | 
 | 
					[**getCuratedObjects**](AssetApi.md#getcuratedobjects) | **GET** /asset/curated-objects | 
 | 
				
			||||||
 | 
					[**getDownloadInfo**](AssetApi.md#getdownloadinfo) | **GET** /asset/download | 
 | 
				
			||||||
[**getMapMarkers**](AssetApi.md#getmapmarkers) | **GET** /asset/map-marker | 
 | 
					[**getMapMarkers**](AssetApi.md#getmapmarkers) | **GET** /asset/map-marker | 
 | 
				
			||||||
[**getMemoryLane**](AssetApi.md#getmemorylane) | **GET** /asset/memory-lane | 
 | 
					[**getMemoryLane**](AssetApi.md#getmemorylane) | **GET** /asset/memory-lane | 
 | 
				
			||||||
[**getUserAssetsByDeviceId**](AssetApi.md#getuserassetsbydeviceid) | **GET** /asset/{deviceId} | 
 | 
					[**getUserAssetsByDeviceId**](AssetApi.md#getuserassetsbydeviceid) | **GET** /asset/{deviceId} | 
 | 
				
			||||||
@ -264,6 +264,63 @@ 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)
 | 
					[[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(assetIdsDto, key)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Example
 | 
				
			||||||
 | 
					```dart
 | 
				
			||||||
 | 
					import 'package:openapi/api.dart';
 | 
				
			||||||
 | 
					// TODO Configure API key authorization: cookie
 | 
				
			||||||
 | 
					//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
 | 
				
			||||||
 | 
					// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
 | 
				
			||||||
 | 
					//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
 | 
				
			||||||
 | 
					// TODO Configure API key authorization: api_key
 | 
				
			||||||
 | 
					//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
 | 
				
			||||||
 | 
					// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
 | 
				
			||||||
 | 
					//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
 | 
				
			||||||
 | 
					// 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 assetIdsDto = AssetIdsDto(); // AssetIdsDto | 
 | 
				
			||||||
 | 
					final key = key_example; // String | 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					try {
 | 
				
			||||||
 | 
					    final result = api_instance.downloadArchive(assetIdsDto, key);
 | 
				
			||||||
 | 
					    print(result);
 | 
				
			||||||
 | 
					} catch (e) {
 | 
				
			||||||
 | 
					    print('Exception when calling AssetApi->downloadArchive: $e\n');
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Parameters
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Name | Type | Description  | Notes
 | 
				
			||||||
 | 
					------------- | ------------- | ------------- | -------------
 | 
				
			||||||
 | 
					 **assetIdsDto** | [**AssetIdsDto**](AssetIdsDto.md)|  | 
 | 
				
			||||||
 | 
					 **key** | **String**|  | [optional] 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Return type
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[**MultipartFile**](MultipartFile.md)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Authorization
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### HTTP request headers
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 - **Content-Type**: application/json
 | 
				
			||||||
 | 
					 - **Accept**: application/octet-stream
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# **downloadFile**
 | 
					# **downloadFile**
 | 
				
			||||||
> MultipartFile downloadFile(id, key)
 | 
					> MultipartFile downloadFile(id, key)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -321,124 +378,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)
 | 
					[[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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# **downloadFiles**
 | 
					 | 
				
			||||||
> MultipartFile downloadFiles(downloadFilesDto, key)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### Example
 | 
					 | 
				
			||||||
```dart
 | 
					 | 
				
			||||||
import 'package:openapi/api.dart';
 | 
					 | 
				
			||||||
// TODO Configure API key authorization: cookie
 | 
					 | 
				
			||||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
 | 
					 | 
				
			||||||
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
 | 
					 | 
				
			||||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
 | 
					 | 
				
			||||||
// TODO Configure API key authorization: api_key
 | 
					 | 
				
			||||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
 | 
					 | 
				
			||||||
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
 | 
					 | 
				
			||||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
 | 
					 | 
				
			||||||
// 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 downloadFilesDto = DownloadFilesDto(); // DownloadFilesDto | 
 | 
					 | 
				
			||||||
final key = key_example; // String | 
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
try {
 | 
					 | 
				
			||||||
    final result = api_instance.downloadFiles(downloadFilesDto, key);
 | 
					 | 
				
			||||||
    print(result);
 | 
					 | 
				
			||||||
} catch (e) {
 | 
					 | 
				
			||||||
    print('Exception when calling AssetApi->downloadFiles: $e\n');
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### Parameters
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Name | Type | Description  | Notes
 | 
					 | 
				
			||||||
------------- | ------------- | ------------- | -------------
 | 
					 | 
				
			||||||
 **downloadFilesDto** | [**DownloadFilesDto**](DownloadFilesDto.md)|  | 
 | 
					 | 
				
			||||||
 **key** | **String**|  | [optional] 
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### Return type
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[**MultipartFile**](MultipartFile.md)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### Authorization
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### HTTP request headers
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 - **Content-Type**: application/json
 | 
					 | 
				
			||||||
 - **Accept**: application/octet-stream
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[[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(name, skip, key)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Current this is not used in any UI element
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### Example
 | 
					 | 
				
			||||||
```dart
 | 
					 | 
				
			||||||
import 'package:openapi/api.dart';
 | 
					 | 
				
			||||||
// TODO Configure API key authorization: cookie
 | 
					 | 
				
			||||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
 | 
					 | 
				
			||||||
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
 | 
					 | 
				
			||||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
 | 
					 | 
				
			||||||
// TODO Configure API key authorization: api_key
 | 
					 | 
				
			||||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
 | 
					 | 
				
			||||||
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
 | 
					 | 
				
			||||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
 | 
					 | 
				
			||||||
// 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 name = name_example; // String | 
 | 
					 | 
				
			||||||
final skip = 8.14; // num | 
 | 
					 | 
				
			||||||
final key = key_example; // String | 
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
try {
 | 
					 | 
				
			||||||
    final result = api_instance.downloadLibrary(name, skip, key);
 | 
					 | 
				
			||||||
    print(result);
 | 
					 | 
				
			||||||
} catch (e) {
 | 
					 | 
				
			||||||
    print('Exception when calling AssetApi->downloadLibrary: $e\n');
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### Parameters
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Name | Type | Description  | Notes
 | 
					 | 
				
			||||||
------------- | ------------- | ------------- | -------------
 | 
					 | 
				
			||||||
 **name** | **String**|  | [optional] 
 | 
					 | 
				
			||||||
 **skip** | **num**|  | [optional] 
 | 
					 | 
				
			||||||
 **key** | **String**|  | [optional] 
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### Return type
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[**MultipartFile**](MultipartFile.md)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### Authorization
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### HTTP request headers
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 - **Content-Type**: Not defined
 | 
					 | 
				
			||||||
 - **Accept**: application/octet-stream
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[[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)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# **getAllAssets**
 | 
					# **getAllAssets**
 | 
				
			||||||
> List<AssetResponseDto> getAllAssets(userId, isFavorite, isArchived, withoutThumbs, skip, ifNoneMatch)
 | 
					> List<AssetResponseDto> getAllAssets(userId, isFavorite, isArchived, withoutThumbs, skip, ifNoneMatch)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -989,6 +928,69 @@ This endpoint does not need any parameter.
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
[[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)
 | 
					[[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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# **getDownloadInfo**
 | 
				
			||||||
 | 
					> DownloadResponseDto getDownloadInfo(assetIds, albumId, userId, archiveSize, key)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Example
 | 
				
			||||||
 | 
					```dart
 | 
				
			||||||
 | 
					import 'package:openapi/api.dart';
 | 
				
			||||||
 | 
					// TODO Configure API key authorization: cookie
 | 
				
			||||||
 | 
					//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
 | 
				
			||||||
 | 
					// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
 | 
				
			||||||
 | 
					//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
 | 
				
			||||||
 | 
					// TODO Configure API key authorization: api_key
 | 
				
			||||||
 | 
					//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
 | 
				
			||||||
 | 
					// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
 | 
				
			||||||
 | 
					//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
 | 
				
			||||||
 | 
					// 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 assetIds = []; // List<String> | 
 | 
				
			||||||
 | 
					final albumId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | 
 | 
				
			||||||
 | 
					final userId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | 
 | 
				
			||||||
 | 
					final archiveSize = 8.14; // num | 
 | 
				
			||||||
 | 
					final key = key_example; // String | 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					try {
 | 
				
			||||||
 | 
					    final result = api_instance.getDownloadInfo(assetIds, albumId, userId, archiveSize, key);
 | 
				
			||||||
 | 
					    print(result);
 | 
				
			||||||
 | 
					} catch (e) {
 | 
				
			||||||
 | 
					    print('Exception when calling AssetApi->getDownloadInfo: $e\n');
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Parameters
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Name | Type | Description  | Notes
 | 
				
			||||||
 | 
					------------- | ------------- | ------------- | -------------
 | 
				
			||||||
 | 
					 **assetIds** | [**List<String>**](String.md)|  | [optional] [default to const []]
 | 
				
			||||||
 | 
					 **albumId** | **String**|  | [optional] 
 | 
				
			||||||
 | 
					 **userId** | **String**|  | [optional] 
 | 
				
			||||||
 | 
					 **archiveSize** | **num**|  | [optional] 
 | 
				
			||||||
 | 
					 **key** | **String**|  | [optional] 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Return type
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[**DownloadResponseDto**](DownloadResponseDto.md)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Authorization
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### HTTP request headers
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 - **Content-Type**: Not defined
 | 
				
			||||||
 | 
					 - **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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# **getMapMarkers**
 | 
					# **getMapMarkers**
 | 
				
			||||||
> List<MapMarkerResponseDto> getMapMarkers(isFavorite, fileCreatedAfter, fileCreatedBefore)
 | 
					> List<MapMarkerResponseDto> getMapMarkers(isFavorite, fileCreatedAfter, fileCreatedBefore)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,4 @@
 | 
				
			|||||||
# openapi.model.DownloadFilesDto
 | 
					# openapi.model.DownloadArchiveInfo
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Load the model package
 | 
					## Load the model package
 | 
				
			||||||
```dart
 | 
					```dart
 | 
				
			||||||
@ -8,6 +8,7 @@ import 'package:openapi/api.dart';
 | 
				
			|||||||
## Properties
 | 
					## Properties
 | 
				
			||||||
Name | Type | Description | Notes
 | 
					Name | Type | Description | Notes
 | 
				
			||||||
------------ | ------------- | ------------- | -------------
 | 
					------------ | ------------- | ------------- | -------------
 | 
				
			||||||
 | 
					**size** | **int** |  | 
 | 
				
			||||||
**assetIds** | **List<String>** |  | [default to const []]
 | 
					**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)
 | 
					[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
 | 
				
			||||||
							
								
								
									
										16
									
								
								mobile/openapi/doc/DownloadResponseDto.md
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								mobile/openapi/doc/DownloadResponseDto.md
									
									
									
										generated
									
									
									
										Normal file
									
								
							@ -0,0 +1,16 @@
 | 
				
			|||||||
 | 
					# openapi.model.DownloadResponseDto
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Load the model package
 | 
				
			||||||
 | 
					```dart
 | 
				
			||||||
 | 
					import 'package:openapi/api.dart';
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Properties
 | 
				
			||||||
 | 
					Name | Type | Description | Notes
 | 
				
			||||||
 | 
					------------ | ------------- | ------------- | -------------
 | 
				
			||||||
 | 
					**totalSize** | **int** |  | 
 | 
				
			||||||
 | 
					**archives** | [**List<DownloadArchiveInfo>**](DownloadArchiveInfo.md) |  | [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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										3
									
								
								mobile/openapi/lib/api.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										3
									
								
								mobile/openapi/lib/api.dart
									
									
									
										generated
									
									
									
								
							@ -81,7 +81,8 @@ part 'model/curated_objects_response_dto.dart';
 | 
				
			|||||||
part 'model/delete_asset_dto.dart';
 | 
					part 'model/delete_asset_dto.dart';
 | 
				
			||||||
part 'model/delete_asset_response_dto.dart';
 | 
					part 'model/delete_asset_response_dto.dart';
 | 
				
			||||||
part 'model/delete_asset_status.dart';
 | 
					part 'model/delete_asset_status.dart';
 | 
				
			||||||
part 'model/download_files_dto.dart';
 | 
					part 'model/download_archive_info.dart';
 | 
				
			||||||
 | 
					part 'model/download_response_dto.dart';
 | 
				
			||||||
part 'model/exif_response_dto.dart';
 | 
					part 'model/exif_response_dto.dart';
 | 
				
			||||||
part 'model/get_asset_by_time_bucket_dto.dart';
 | 
					part 'model/get_asset_by_time_bucket_dto.dart';
 | 
				
			||||||
part 'model/get_asset_count_by_time_bucket_dto.dart';
 | 
					part 'model/get_asset_count_by_time_bucket_dto.dart';
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										70
									
								
								mobile/openapi/lib/api/album_api.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										70
									
								
								mobile/openapi/lib/api/album_api.dart
									
									
									
										generated
									
									
									
								
							@ -215,76 +215,6 @@ class AlbumApi {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /// Performs an HTTP 'GET /album/{id}/download' operation and returns the [Response].
 | 
					 | 
				
			||||||
  /// Parameters:
 | 
					 | 
				
			||||||
  ///
 | 
					 | 
				
			||||||
  /// * [String] id (required):
 | 
					 | 
				
			||||||
  ///
 | 
					 | 
				
			||||||
  /// * [String] name:
 | 
					 | 
				
			||||||
  ///
 | 
					 | 
				
			||||||
  /// * [num] skip:
 | 
					 | 
				
			||||||
  ///
 | 
					 | 
				
			||||||
  /// * [String] key:
 | 
					 | 
				
			||||||
  Future<Response> downloadArchiveWithHttpInfo(String id, { String? name, num? skip, String? key, }) async {
 | 
					 | 
				
			||||||
    // ignore: prefer_const_declarations
 | 
					 | 
				
			||||||
    final path = r'/album/{id}/download'
 | 
					 | 
				
			||||||
      .replaceAll('{id}', id);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // ignore: prefer_final_locals
 | 
					 | 
				
			||||||
    Object? postBody;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    final queryParams = <QueryParam>[];
 | 
					 | 
				
			||||||
    final headerParams = <String, String>{};
 | 
					 | 
				
			||||||
    final formParams = <String, String>{};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (name != null) {
 | 
					 | 
				
			||||||
      queryParams.addAll(_queryParams('', 'name', name));
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    if (skip != null) {
 | 
					 | 
				
			||||||
      queryParams.addAll(_queryParams('', 'skip', skip));
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    if (key != null) {
 | 
					 | 
				
			||||||
      queryParams.addAll(_queryParams('', 'key', key));
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const contentTypes = <String>[];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return apiClient.invokeAPI(
 | 
					 | 
				
			||||||
      path,
 | 
					 | 
				
			||||||
      'GET',
 | 
					 | 
				
			||||||
      queryParams,
 | 
					 | 
				
			||||||
      postBody,
 | 
					 | 
				
			||||||
      headerParams,
 | 
					 | 
				
			||||||
      formParams,
 | 
					 | 
				
			||||||
      contentTypes.isEmpty ? null : contentTypes.first,
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  /// Parameters:
 | 
					 | 
				
			||||||
  ///
 | 
					 | 
				
			||||||
  /// * [String] id (required):
 | 
					 | 
				
			||||||
  ///
 | 
					 | 
				
			||||||
  /// * [String] name:
 | 
					 | 
				
			||||||
  ///
 | 
					 | 
				
			||||||
  /// * [num] skip:
 | 
					 | 
				
			||||||
  ///
 | 
					 | 
				
			||||||
  /// * [String] key:
 | 
					 | 
				
			||||||
  Future<MultipartFile?> downloadArchive(String id, { String? name, num? skip, String? key, }) async {
 | 
					 | 
				
			||||||
    final response = await downloadArchiveWithHttpInfo(id,  name: name, skip: skip, key: key, );
 | 
					 | 
				
			||||||
    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), 'MultipartFile',) as MultipartFile;
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    return null;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  /// Performs an HTTP 'GET /album/count' operation and returns the [Response].
 | 
					  /// Performs an HTTP 'GET /album/count' operation and returns the [Response].
 | 
				
			||||||
  Future<Response> getAlbumCountWithHttpInfo() async {
 | 
					  Future<Response> getAlbumCountWithHttpInfo() async {
 | 
				
			||||||
    // ignore: prefer_const_declarations
 | 
					    // ignore: prefer_const_declarations
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										263
									
								
								mobile/openapi/lib/api/asset_api.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										263
									
								
								mobile/openapi/lib/api/asset_api.dart
									
									
									
										generated
									
									
									
								
							@ -230,7 +230,62 @@ class AssetApi {
 | 
				
			|||||||
    return null;
 | 
					    return null;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /// Performs an HTTP 'GET /asset/download/{id}' operation and returns the [Response].
 | 
					  /// Performs an HTTP 'POST /asset/download' operation and returns the [Response].
 | 
				
			||||||
 | 
					  /// Parameters:
 | 
				
			||||||
 | 
					  ///
 | 
				
			||||||
 | 
					  /// * [AssetIdsDto] assetIdsDto (required):
 | 
				
			||||||
 | 
					  ///
 | 
				
			||||||
 | 
					  /// * [String] key:
 | 
				
			||||||
 | 
					  Future<Response> downloadArchiveWithHttpInfo(AssetIdsDto assetIdsDto, { String? key, }) async {
 | 
				
			||||||
 | 
					    // ignore: prefer_const_declarations
 | 
				
			||||||
 | 
					    final path = r'/asset/download';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // ignore: prefer_final_locals
 | 
				
			||||||
 | 
					    Object? postBody = assetIdsDto;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final queryParams = <QueryParam>[];
 | 
				
			||||||
 | 
					    final headerParams = <String, String>{};
 | 
				
			||||||
 | 
					    final formParams = <String, String>{};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (key != null) {
 | 
				
			||||||
 | 
					      queryParams.addAll(_queryParams('', 'key', key));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const contentTypes = <String>['application/json'];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return apiClient.invokeAPI(
 | 
				
			||||||
 | 
					      path,
 | 
				
			||||||
 | 
					      'POST',
 | 
				
			||||||
 | 
					      queryParams,
 | 
				
			||||||
 | 
					      postBody,
 | 
				
			||||||
 | 
					      headerParams,
 | 
				
			||||||
 | 
					      formParams,
 | 
				
			||||||
 | 
					      contentTypes.isEmpty ? null : contentTypes.first,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Parameters:
 | 
				
			||||||
 | 
					  ///
 | 
				
			||||||
 | 
					  /// * [AssetIdsDto] assetIdsDto (required):
 | 
				
			||||||
 | 
					  ///
 | 
				
			||||||
 | 
					  /// * [String] key:
 | 
				
			||||||
 | 
					  Future<MultipartFile?> downloadArchive(AssetIdsDto assetIdsDto, { String? key, }) async {
 | 
				
			||||||
 | 
					    final response = await downloadArchiveWithHttpInfo(assetIdsDto,  key: key, );
 | 
				
			||||||
 | 
					    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), 'MultipartFile',) as MultipartFile;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return null;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Performs an HTTP 'POST /asset/download/{id}' operation and returns the [Response].
 | 
				
			||||||
  /// Parameters:
 | 
					  /// Parameters:
 | 
				
			||||||
  ///
 | 
					  ///
 | 
				
			||||||
  /// * [String] id (required):
 | 
					  /// * [String] id (required):
 | 
				
			||||||
@ -257,7 +312,7 @@ class AssetApi {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    return apiClient.invokeAPI(
 | 
					    return apiClient.invokeAPI(
 | 
				
			||||||
      path,
 | 
					      path,
 | 
				
			||||||
      'GET',
 | 
					      'POST',
 | 
				
			||||||
      queryParams,
 | 
					      queryParams,
 | 
				
			||||||
      postBody,
 | 
					      postBody,
 | 
				
			||||||
      headerParams,
 | 
					      headerParams,
 | 
				
			||||||
@ -286,131 +341,6 @@ class AssetApi {
 | 
				
			|||||||
    return null;
 | 
					    return null;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /// Performs an HTTP 'POST /asset/download-files' operation and returns the [Response].
 | 
					 | 
				
			||||||
  /// Parameters:
 | 
					 | 
				
			||||||
  ///
 | 
					 | 
				
			||||||
  /// * [DownloadFilesDto] downloadFilesDto (required):
 | 
					 | 
				
			||||||
  ///
 | 
					 | 
				
			||||||
  /// * [String] key:
 | 
					 | 
				
			||||||
  Future<Response> downloadFilesWithHttpInfo(DownloadFilesDto downloadFilesDto, { String? key, }) async {
 | 
					 | 
				
			||||||
    // ignore: prefer_const_declarations
 | 
					 | 
				
			||||||
    final path = r'/asset/download-files';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // ignore: prefer_final_locals
 | 
					 | 
				
			||||||
    Object? postBody = downloadFilesDto;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    final queryParams = <QueryParam>[];
 | 
					 | 
				
			||||||
    final headerParams = <String, String>{};
 | 
					 | 
				
			||||||
    final formParams = <String, String>{};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (key != null) {
 | 
					 | 
				
			||||||
      queryParams.addAll(_queryParams('', 'key', key));
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const contentTypes = <String>['application/json'];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return apiClient.invokeAPI(
 | 
					 | 
				
			||||||
      path,
 | 
					 | 
				
			||||||
      'POST',
 | 
					 | 
				
			||||||
      queryParams,
 | 
					 | 
				
			||||||
      postBody,
 | 
					 | 
				
			||||||
      headerParams,
 | 
					 | 
				
			||||||
      formParams,
 | 
					 | 
				
			||||||
      contentTypes.isEmpty ? null : contentTypes.first,
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  /// Parameters:
 | 
					 | 
				
			||||||
  ///
 | 
					 | 
				
			||||||
  /// * [DownloadFilesDto] downloadFilesDto (required):
 | 
					 | 
				
			||||||
  ///
 | 
					 | 
				
			||||||
  /// * [String] key:
 | 
					 | 
				
			||||||
  Future<MultipartFile?> downloadFiles(DownloadFilesDto downloadFilesDto, { String? key, }) async {
 | 
					 | 
				
			||||||
    final response = await downloadFilesWithHttpInfo(downloadFilesDto,  key: key, );
 | 
					 | 
				
			||||||
    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), 'MultipartFile',) as MultipartFile;
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    return null;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  /// Current this is not used in any UI element
 | 
					 | 
				
			||||||
  ///
 | 
					 | 
				
			||||||
  /// Note: This method returns the HTTP [Response].
 | 
					 | 
				
			||||||
  ///
 | 
					 | 
				
			||||||
  /// Parameters:
 | 
					 | 
				
			||||||
  ///
 | 
					 | 
				
			||||||
  /// * [String] name:
 | 
					 | 
				
			||||||
  ///
 | 
					 | 
				
			||||||
  /// * [num] skip:
 | 
					 | 
				
			||||||
  ///
 | 
					 | 
				
			||||||
  /// * [String] key:
 | 
					 | 
				
			||||||
  Future<Response> downloadLibraryWithHttpInfo({ String? name, num? skip, String? key, }) async {
 | 
					 | 
				
			||||||
    // ignore: prefer_const_declarations
 | 
					 | 
				
			||||||
    final path = r'/asset/download-library';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // ignore: prefer_final_locals
 | 
					 | 
				
			||||||
    Object? postBody;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    final queryParams = <QueryParam>[];
 | 
					 | 
				
			||||||
    final headerParams = <String, String>{};
 | 
					 | 
				
			||||||
    final formParams = <String, String>{};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (name != null) {
 | 
					 | 
				
			||||||
      queryParams.addAll(_queryParams('', 'name', name));
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    if (skip != null) {
 | 
					 | 
				
			||||||
      queryParams.addAll(_queryParams('', 'skip', skip));
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    if (key != null) {
 | 
					 | 
				
			||||||
      queryParams.addAll(_queryParams('', 'key', key));
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const contentTypes = <String>[];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return apiClient.invokeAPI(
 | 
					 | 
				
			||||||
      path,
 | 
					 | 
				
			||||||
      'GET',
 | 
					 | 
				
			||||||
      queryParams,
 | 
					 | 
				
			||||||
      postBody,
 | 
					 | 
				
			||||||
      headerParams,
 | 
					 | 
				
			||||||
      formParams,
 | 
					 | 
				
			||||||
      contentTypes.isEmpty ? null : contentTypes.first,
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  /// Current this is not used in any UI element
 | 
					 | 
				
			||||||
  ///
 | 
					 | 
				
			||||||
  /// Parameters:
 | 
					 | 
				
			||||||
  ///
 | 
					 | 
				
			||||||
  /// * [String] name:
 | 
					 | 
				
			||||||
  ///
 | 
					 | 
				
			||||||
  /// * [num] skip:
 | 
					 | 
				
			||||||
  ///
 | 
					 | 
				
			||||||
  /// * [String] 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));
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    // 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), 'MultipartFile',) as MultipartFile;
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    return null;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  /// Get all AssetEntity belong to the user
 | 
					  /// Get all AssetEntity belong to the user
 | 
				
			||||||
  ///
 | 
					  ///
 | 
				
			||||||
  /// Note: This method returns the HTTP [Response].
 | 
					  /// Note: This method returns the HTTP [Response].
 | 
				
			||||||
@ -945,6 +875,85 @@ class AssetApi {
 | 
				
			|||||||
    return null;
 | 
					    return null;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Performs an HTTP 'GET /asset/download' operation and returns the [Response].
 | 
				
			||||||
 | 
					  /// Parameters:
 | 
				
			||||||
 | 
					  ///
 | 
				
			||||||
 | 
					  /// * [List<String>] assetIds:
 | 
				
			||||||
 | 
					  ///
 | 
				
			||||||
 | 
					  /// * [String] albumId:
 | 
				
			||||||
 | 
					  ///
 | 
				
			||||||
 | 
					  /// * [String] userId:
 | 
				
			||||||
 | 
					  ///
 | 
				
			||||||
 | 
					  /// * [num] archiveSize:
 | 
				
			||||||
 | 
					  ///
 | 
				
			||||||
 | 
					  /// * [String] key:
 | 
				
			||||||
 | 
					  Future<Response> getDownloadInfoWithHttpInfo({ List<String>? assetIds, String? albumId, String? userId, num? archiveSize, String? key, }) async {
 | 
				
			||||||
 | 
					    // ignore: prefer_const_declarations
 | 
				
			||||||
 | 
					    final path = r'/asset/download';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // ignore: prefer_final_locals
 | 
				
			||||||
 | 
					    Object? postBody;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final queryParams = <QueryParam>[];
 | 
				
			||||||
 | 
					    final headerParams = <String, String>{};
 | 
				
			||||||
 | 
					    final formParams = <String, String>{};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (assetIds != null) {
 | 
				
			||||||
 | 
					      queryParams.addAll(_queryParams('multi', 'assetIds', assetIds));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (albumId != null) {
 | 
				
			||||||
 | 
					      queryParams.addAll(_queryParams('', 'albumId', albumId));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (userId != null) {
 | 
				
			||||||
 | 
					      queryParams.addAll(_queryParams('', 'userId', userId));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (archiveSize != null) {
 | 
				
			||||||
 | 
					      queryParams.addAll(_queryParams('', 'archiveSize', archiveSize));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (key != null) {
 | 
				
			||||||
 | 
					      queryParams.addAll(_queryParams('', 'key', key));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const contentTypes = <String>[];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return apiClient.invokeAPI(
 | 
				
			||||||
 | 
					      path,
 | 
				
			||||||
 | 
					      'GET',
 | 
				
			||||||
 | 
					      queryParams,
 | 
				
			||||||
 | 
					      postBody,
 | 
				
			||||||
 | 
					      headerParams,
 | 
				
			||||||
 | 
					      formParams,
 | 
				
			||||||
 | 
					      contentTypes.isEmpty ? null : contentTypes.first,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Parameters:
 | 
				
			||||||
 | 
					  ///
 | 
				
			||||||
 | 
					  /// * [List<String>] assetIds:
 | 
				
			||||||
 | 
					  ///
 | 
				
			||||||
 | 
					  /// * [String] albumId:
 | 
				
			||||||
 | 
					  ///
 | 
				
			||||||
 | 
					  /// * [String] userId:
 | 
				
			||||||
 | 
					  ///
 | 
				
			||||||
 | 
					  /// * [num] archiveSize:
 | 
				
			||||||
 | 
					  ///
 | 
				
			||||||
 | 
					  /// * [String] key:
 | 
				
			||||||
 | 
					  Future<DownloadResponseDto?> getDownloadInfo({ List<String>? assetIds, String? albumId, String? userId, num? archiveSize, String? key, }) async {
 | 
				
			||||||
 | 
					    final response = await getDownloadInfoWithHttpInfo( assetIds: assetIds, albumId: albumId, userId: userId, archiveSize: archiveSize, key: key, );
 | 
				
			||||||
 | 
					    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), 'DownloadResponseDto',) as DownloadResponseDto;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return null;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /// Performs an HTTP 'GET /asset/map-marker' operation and returns the [Response].
 | 
					  /// Performs an HTTP 'GET /asset/map-marker' operation and returns the [Response].
 | 
				
			||||||
  /// Parameters:
 | 
					  /// Parameters:
 | 
				
			||||||
  ///
 | 
					  ///
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										6
									
								
								mobile/openapi/lib/api_client.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										6
									
								
								mobile/openapi/lib/api_client.dart
									
									
									
										generated
									
									
									
								
							@ -257,8 +257,10 @@ class ApiClient {
 | 
				
			|||||||
          return DeleteAssetResponseDto.fromJson(value);
 | 
					          return DeleteAssetResponseDto.fromJson(value);
 | 
				
			||||||
        case 'DeleteAssetStatus':
 | 
					        case 'DeleteAssetStatus':
 | 
				
			||||||
          return DeleteAssetStatusTypeTransformer().decode(value);
 | 
					          return DeleteAssetStatusTypeTransformer().decode(value);
 | 
				
			||||||
        case 'DownloadFilesDto':
 | 
					        case 'DownloadArchiveInfo':
 | 
				
			||||||
          return DownloadFilesDto.fromJson(value);
 | 
					          return DownloadArchiveInfo.fromJson(value);
 | 
				
			||||||
 | 
					        case 'DownloadResponseDto':
 | 
				
			||||||
 | 
					          return DownloadResponseDto.fromJson(value);
 | 
				
			||||||
        case 'ExifResponseDto':
 | 
					        case 'ExifResponseDto':
 | 
				
			||||||
          return ExifResponseDto.fromJson(value);
 | 
					          return ExifResponseDto.fromJson(value);
 | 
				
			||||||
        case 'GetAssetByTimeBucketDto':
 | 
					        case 'GetAssetByTimeBucketDto':
 | 
				
			||||||
 | 
				
			|||||||
@ -10,40 +10,47 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
part of openapi.api;
 | 
					part of openapi.api;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class DownloadFilesDto {
 | 
					class DownloadArchiveInfo {
 | 
				
			||||||
  /// Returns a new [DownloadFilesDto] instance.
 | 
					  /// Returns a new [DownloadArchiveInfo] instance.
 | 
				
			||||||
  DownloadFilesDto({
 | 
					  DownloadArchiveInfo({
 | 
				
			||||||
 | 
					    required this.size,
 | 
				
			||||||
    this.assetIds = const [],
 | 
					    this.assetIds = const [],
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  int size;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  List<String> assetIds;
 | 
					  List<String> assetIds;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  bool operator ==(Object other) => identical(this, other) || other is DownloadFilesDto &&
 | 
					  bool operator ==(Object other) => identical(this, other) || other is DownloadArchiveInfo &&
 | 
				
			||||||
 | 
					     other.size == size &&
 | 
				
			||||||
     other.assetIds == assetIds;
 | 
					     other.assetIds == assetIds;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  int get hashCode =>
 | 
					  int get hashCode =>
 | 
				
			||||||
    // ignore: unnecessary_parenthesis
 | 
					    // ignore: unnecessary_parenthesis
 | 
				
			||||||
 | 
					    (size.hashCode) +
 | 
				
			||||||
    (assetIds.hashCode);
 | 
					    (assetIds.hashCode);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  String toString() => 'DownloadFilesDto[assetIds=$assetIds]';
 | 
					  String toString() => 'DownloadArchiveInfo[size=$size, assetIds=$assetIds]';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Map<String, dynamic> toJson() {
 | 
					  Map<String, dynamic> toJson() {
 | 
				
			||||||
    final json = <String, dynamic>{};
 | 
					    final json = <String, dynamic>{};
 | 
				
			||||||
 | 
					      json[r'size'] = this.size;
 | 
				
			||||||
      json[r'assetIds'] = this.assetIds;
 | 
					      json[r'assetIds'] = this.assetIds;
 | 
				
			||||||
    return json;
 | 
					    return json;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /// Returns a new [DownloadFilesDto] instance and imports its values from
 | 
					  /// Returns a new [DownloadArchiveInfo] instance and imports its values from
 | 
				
			||||||
  /// [value] if it's a [Map], null otherwise.
 | 
					  /// [value] if it's a [Map], null otherwise.
 | 
				
			||||||
  // ignore: prefer_constructors_over_static_methods
 | 
					  // ignore: prefer_constructors_over_static_methods
 | 
				
			||||||
  static DownloadFilesDto? fromJson(dynamic value) {
 | 
					  static DownloadArchiveInfo? fromJson(dynamic value) {
 | 
				
			||||||
    if (value is Map) {
 | 
					    if (value is Map) {
 | 
				
			||||||
      final json = value.cast<String, dynamic>();
 | 
					      final json = value.cast<String, dynamic>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      return DownloadFilesDto(
 | 
					      return DownloadArchiveInfo(
 | 
				
			||||||
 | 
					        size: mapValueOfType<int>(json, r'size')!,
 | 
				
			||||||
        assetIds: json[r'assetIds'] is Iterable
 | 
					        assetIds: json[r'assetIds'] is Iterable
 | 
				
			||||||
            ? (json[r'assetIds'] as Iterable).cast<String>().toList(growable: false)
 | 
					            ? (json[r'assetIds'] as Iterable).cast<String>().toList(growable: false)
 | 
				
			||||||
            : const [],
 | 
					            : const [],
 | 
				
			||||||
@ -52,11 +59,11 @@ class DownloadFilesDto {
 | 
				
			|||||||
    return null;
 | 
					    return null;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  static List<DownloadFilesDto> listFromJson(dynamic json, {bool growable = false,}) {
 | 
					  static List<DownloadArchiveInfo> listFromJson(dynamic json, {bool growable = false,}) {
 | 
				
			||||||
    final result = <DownloadFilesDto>[];
 | 
					    final result = <DownloadArchiveInfo>[];
 | 
				
			||||||
    if (json is List && json.isNotEmpty) {
 | 
					    if (json is List && json.isNotEmpty) {
 | 
				
			||||||
      for (final row in json) {
 | 
					      for (final row in json) {
 | 
				
			||||||
        final value = DownloadFilesDto.fromJson(row);
 | 
					        final value = DownloadArchiveInfo.fromJson(row);
 | 
				
			||||||
        if (value != null) {
 | 
					        if (value != null) {
 | 
				
			||||||
          result.add(value);
 | 
					          result.add(value);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@ -65,12 +72,12 @@ class DownloadFilesDto {
 | 
				
			|||||||
    return result.toList(growable: growable);
 | 
					    return result.toList(growable: growable);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  static Map<String, DownloadFilesDto> mapFromJson(dynamic json) {
 | 
					  static Map<String, DownloadArchiveInfo> mapFromJson(dynamic json) {
 | 
				
			||||||
    final map = <String, DownloadFilesDto>{};
 | 
					    final map = <String, DownloadArchiveInfo>{};
 | 
				
			||||||
    if (json is Map && json.isNotEmpty) {
 | 
					    if (json is Map && json.isNotEmpty) {
 | 
				
			||||||
      json = json.cast<String, dynamic>(); // ignore: parameter_assignments
 | 
					      json = json.cast<String, dynamic>(); // ignore: parameter_assignments
 | 
				
			||||||
      for (final entry in json.entries) {
 | 
					      for (final entry in json.entries) {
 | 
				
			||||||
        final value = DownloadFilesDto.fromJson(entry.value);
 | 
					        final value = DownloadArchiveInfo.fromJson(entry.value);
 | 
				
			||||||
        if (value != null) {
 | 
					        if (value != null) {
 | 
				
			||||||
          map[entry.key] = value;
 | 
					          map[entry.key] = value;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@ -79,14 +86,14 @@ class DownloadFilesDto {
 | 
				
			|||||||
    return map;
 | 
					    return map;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // maps a json object with a list of DownloadFilesDto-objects as value to a dart map
 | 
					  // maps a json object with a list of DownloadArchiveInfo-objects as value to a dart map
 | 
				
			||||||
  static Map<String, List<DownloadFilesDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
 | 
					  static Map<String, List<DownloadArchiveInfo>> mapListFromJson(dynamic json, {bool growable = false,}) {
 | 
				
			||||||
    final map = <String, List<DownloadFilesDto>>{};
 | 
					    final map = <String, List<DownloadArchiveInfo>>{};
 | 
				
			||||||
    if (json is Map && json.isNotEmpty) {
 | 
					    if (json is Map && json.isNotEmpty) {
 | 
				
			||||||
      // ignore: parameter_assignments
 | 
					      // ignore: parameter_assignments
 | 
				
			||||||
      json = json.cast<String, dynamic>();
 | 
					      json = json.cast<String, dynamic>();
 | 
				
			||||||
      for (final entry in json.entries) {
 | 
					      for (final entry in json.entries) {
 | 
				
			||||||
        map[entry.key] = DownloadFilesDto.listFromJson(entry.value, growable: growable,);
 | 
					        map[entry.key] = DownloadArchiveInfo.listFromJson(entry.value, growable: growable,);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    return map;
 | 
					    return map;
 | 
				
			||||||
@ -94,6 +101,7 @@ class DownloadFilesDto {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  /// The list of required keys that must be present in a JSON.
 | 
					  /// The list of required keys that must be present in a JSON.
 | 
				
			||||||
  static const requiredKeys = <String>{
 | 
					  static const requiredKeys = <String>{
 | 
				
			||||||
 | 
					    'size',
 | 
				
			||||||
    'assetIds',
 | 
					    'assetIds',
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
							
								
								
									
										106
									
								
								mobile/openapi/lib/model/download_response_dto.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								mobile/openapi/lib/model/download_response_dto.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							@ -0,0 +1,106 @@
 | 
				
			|||||||
 | 
					//
 | 
				
			||||||
 | 
					// 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 DownloadResponseDto {
 | 
				
			||||||
 | 
					  /// Returns a new [DownloadResponseDto] instance.
 | 
				
			||||||
 | 
					  DownloadResponseDto({
 | 
				
			||||||
 | 
					    required this.totalSize,
 | 
				
			||||||
 | 
					    this.archives = const [],
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  int totalSize;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  List<DownloadArchiveInfo> archives;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  bool operator ==(Object other) => identical(this, other) || other is DownloadResponseDto &&
 | 
				
			||||||
 | 
					     other.totalSize == totalSize &&
 | 
				
			||||||
 | 
					     other.archives == archives;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  int get hashCode =>
 | 
				
			||||||
 | 
					    // ignore: unnecessary_parenthesis
 | 
				
			||||||
 | 
					    (totalSize.hashCode) +
 | 
				
			||||||
 | 
					    (archives.hashCode);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  String toString() => 'DownloadResponseDto[totalSize=$totalSize, archives=$archives]';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Map<String, dynamic> toJson() {
 | 
				
			||||||
 | 
					    final json = <String, dynamic>{};
 | 
				
			||||||
 | 
					      json[r'totalSize'] = this.totalSize;
 | 
				
			||||||
 | 
					      json[r'archives'] = this.archives;
 | 
				
			||||||
 | 
					    return json;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Returns a new [DownloadResponseDto] instance and imports its values from
 | 
				
			||||||
 | 
					  /// [value] if it's a [Map], null otherwise.
 | 
				
			||||||
 | 
					  // ignore: prefer_constructors_over_static_methods
 | 
				
			||||||
 | 
					  static DownloadResponseDto? fromJson(dynamic value) {
 | 
				
			||||||
 | 
					    if (value is Map) {
 | 
				
			||||||
 | 
					      final json = value.cast<String, dynamic>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return DownloadResponseDto(
 | 
				
			||||||
 | 
					        totalSize: mapValueOfType<int>(json, r'totalSize')!,
 | 
				
			||||||
 | 
					        archives: DownloadArchiveInfo.listFromJson(json[r'archives']),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return null;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static List<DownloadResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
 | 
				
			||||||
 | 
					    final result = <DownloadResponseDto>[];
 | 
				
			||||||
 | 
					    if (json is List && json.isNotEmpty) {
 | 
				
			||||||
 | 
					      for (final row in json) {
 | 
				
			||||||
 | 
					        final value = DownloadResponseDto.fromJson(row);
 | 
				
			||||||
 | 
					        if (value != null) {
 | 
				
			||||||
 | 
					          result.add(value);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return result.toList(growable: growable);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static Map<String, DownloadResponseDto> mapFromJson(dynamic json) {
 | 
				
			||||||
 | 
					    final map = <String, DownloadResponseDto>{};
 | 
				
			||||||
 | 
					    if (json is Map && json.isNotEmpty) {
 | 
				
			||||||
 | 
					      json = json.cast<String, dynamic>(); // ignore: parameter_assignments
 | 
				
			||||||
 | 
					      for (final entry in json.entries) {
 | 
				
			||||||
 | 
					        final value = DownloadResponseDto.fromJson(entry.value);
 | 
				
			||||||
 | 
					        if (value != null) {
 | 
				
			||||||
 | 
					          map[entry.key] = value;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return map;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // maps a json object with a list of DownloadResponseDto-objects as value to a dart map
 | 
				
			||||||
 | 
					  static Map<String, List<DownloadResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
 | 
				
			||||||
 | 
					    final map = <String, List<DownloadResponseDto>>{};
 | 
				
			||||||
 | 
					    if (json is Map && json.isNotEmpty) {
 | 
				
			||||||
 | 
					      // ignore: parameter_assignments
 | 
				
			||||||
 | 
					      json = json.cast<String, dynamic>();
 | 
				
			||||||
 | 
					      for (final entry in json.entries) {
 | 
				
			||||||
 | 
					        map[entry.key] = DownloadResponseDto.listFromJson(entry.value, growable: growable,);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return map;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// The list of required keys that must be present in a JSON.
 | 
				
			||||||
 | 
					  static const requiredKeys = <String>{
 | 
				
			||||||
 | 
					    'totalSize',
 | 
				
			||||||
 | 
					    'archives',
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										5
									
								
								mobile/openapi/test/album_api_test.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										5
									
								
								mobile/openapi/test/album_api_test.dart
									
									
									
										generated
									
									
									
								
							@ -37,11 +37,6 @@ void main() {
 | 
				
			|||||||
      // TODO
 | 
					      // TODO
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    //Future<MultipartFile> downloadArchive(String id, { String name, num skip, String key }) async
 | 
					 | 
				
			||||||
    test('test downloadArchive', () async {
 | 
					 | 
				
			||||||
      // TODO
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    //Future<AlbumCountResponseDto> getAlbumCount() async
 | 
					    //Future<AlbumCountResponseDto> getAlbumCount() async
 | 
				
			||||||
    test('test getAlbumCount', () async {
 | 
					    test('test getAlbumCount', () async {
 | 
				
			||||||
      // TODO
 | 
					      // TODO
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										22
									
								
								mobile/openapi/test/asset_api_test.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										22
									
								
								mobile/openapi/test/asset_api_test.dart
									
									
									
										generated
									
									
									
								
							@ -43,23 +43,16 @@ void main() {
 | 
				
			|||||||
      // TODO
 | 
					      // TODO
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    //Future<MultipartFile> downloadArchive(AssetIdsDto assetIdsDto, { String key }) async
 | 
				
			||||||
 | 
					    test('test downloadArchive', () async {
 | 
				
			||||||
 | 
					      // TODO
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    //Future<MultipartFile> downloadFile(String id, { String key }) async
 | 
					    //Future<MultipartFile> downloadFile(String id, { String key }) async
 | 
				
			||||||
    test('test downloadFile', () async {
 | 
					    test('test downloadFile', () async {
 | 
				
			||||||
      // TODO
 | 
					      // TODO
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    //Future<MultipartFile> downloadFiles(DownloadFilesDto downloadFilesDto, { String key }) async
 | 
					 | 
				
			||||||
    test('test downloadFiles', () async {
 | 
					 | 
				
			||||||
      // TODO
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // Current this is not used in any UI element
 | 
					 | 
				
			||||||
    //
 | 
					 | 
				
			||||||
    //Future<MultipartFile> downloadLibrary({ String name, num skip, String key }) async
 | 
					 | 
				
			||||||
    test('test downloadLibrary', () async {
 | 
					 | 
				
			||||||
      // TODO
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // Get all AssetEntity belong to the user
 | 
					    // Get all AssetEntity belong to the user
 | 
				
			||||||
    //
 | 
					    //
 | 
				
			||||||
    //Future<List<AssetResponseDto>> getAllAssets({ String userId, bool isFavorite, bool isArchived, bool withoutThumbs, num skip, String ifNoneMatch }) async
 | 
					    //Future<List<AssetResponseDto>> getAllAssets({ String userId, bool isFavorite, bool isArchived, bool withoutThumbs, num skip, String ifNoneMatch }) async
 | 
				
			||||||
@ -114,6 +107,11 @@ void main() {
 | 
				
			|||||||
      // TODO
 | 
					      // TODO
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    //Future<DownloadResponseDto> getDownloadInfo({ List<String> assetIds, String albumId, String userId, num archiveSize, String key }) async
 | 
				
			||||||
 | 
					    test('test getDownloadInfo', () async {
 | 
				
			||||||
 | 
					      // TODO
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    //Future<List<MapMarkerResponseDto>> getMapMarkers({ bool isFavorite, DateTime fileCreatedAfter, DateTime fileCreatedBefore }) async
 | 
					    //Future<List<MapMarkerResponseDto>> getMapMarkers({ bool isFavorite, DateTime fileCreatedAfter, DateTime fileCreatedBefore }) async
 | 
				
			||||||
    test('test getMapMarkers', () async {
 | 
					    test('test getMapMarkers', () async {
 | 
				
			||||||
      // TODO
 | 
					      // TODO
 | 
				
			||||||
 | 
				
			|||||||
@ -11,11 +11,16 @@
 | 
				
			|||||||
import 'package:openapi/api.dart';
 | 
					import 'package:openapi/api.dart';
 | 
				
			||||||
import 'package:test/test.dart';
 | 
					import 'package:test/test.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// tests for DownloadFilesDto
 | 
					// tests for DownloadArchiveInfo
 | 
				
			||||||
void main() {
 | 
					void main() {
 | 
				
			||||||
  // final instance = DownloadFilesDto();
 | 
					  // final instance = DownloadArchiveInfo();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  group('test DownloadArchiveInfo', () {
 | 
				
			||||||
 | 
					    // int size
 | 
				
			||||||
 | 
					    test('to test the property `size`', () async {
 | 
				
			||||||
 | 
					      // TODO
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  group('test DownloadFilesDto', () {
 | 
					 | 
				
			||||||
    // List<String> assetIds (default value: const [])
 | 
					    // List<String> assetIds (default value: const [])
 | 
				
			||||||
    test('to test the property `assetIds`', () async {
 | 
					    test('to test the property `assetIds`', () async {
 | 
				
			||||||
      // TODO
 | 
					      // TODO
 | 
				
			||||||
							
								
								
									
										32
									
								
								mobile/openapi/test/download_response_dto_test.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								mobile/openapi/test/download_response_dto_test.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							@ -0,0 +1,32 @@
 | 
				
			|||||||
 | 
					//
 | 
				
			||||||
 | 
					// 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 DownloadResponseDto
 | 
				
			||||||
 | 
					void main() {
 | 
				
			||||||
 | 
					  // final instance = DownloadResponseDto();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  group('test DownloadResponseDto', () {
 | 
				
			||||||
 | 
					    // int totalSize
 | 
				
			||||||
 | 
					    test('to test the property `totalSize`', () async {
 | 
				
			||||||
 | 
					      // TODO
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // List<DownloadArchiveInfo> archives (default value: const [])
 | 
				
			||||||
 | 
					    test('to test the property `archives`', () async {
 | 
				
			||||||
 | 
					      // TODO
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -370,73 +370,6 @@
 | 
				
			|||||||
        ]
 | 
					        ]
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "/album/{id}/download": {
 | 
					 | 
				
			||||||
      "get": {
 | 
					 | 
				
			||||||
        "operationId": "downloadArchive",
 | 
					 | 
				
			||||||
        "parameters": [
 | 
					 | 
				
			||||||
          {
 | 
					 | 
				
			||||||
            "name": "id",
 | 
					 | 
				
			||||||
            "required": true,
 | 
					 | 
				
			||||||
            "in": "path",
 | 
					 | 
				
			||||||
            "schema": {
 | 
					 | 
				
			||||||
              "format": "uuid",
 | 
					 | 
				
			||||||
              "type": "string"
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          {
 | 
					 | 
				
			||||||
            "name": "name",
 | 
					 | 
				
			||||||
            "required": false,
 | 
					 | 
				
			||||||
            "in": "query",
 | 
					 | 
				
			||||||
            "schema": {
 | 
					 | 
				
			||||||
              "type": "string"
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          {
 | 
					 | 
				
			||||||
            "name": "skip",
 | 
					 | 
				
			||||||
            "required": false,
 | 
					 | 
				
			||||||
            "in": "query",
 | 
					 | 
				
			||||||
            "schema": {
 | 
					 | 
				
			||||||
              "type": "number"
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          {
 | 
					 | 
				
			||||||
            "name": "key",
 | 
					 | 
				
			||||||
            "required": false,
 | 
					 | 
				
			||||||
            "in": "query",
 | 
					 | 
				
			||||||
            "schema": {
 | 
					 | 
				
			||||||
              "type": "string"
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        ],
 | 
					 | 
				
			||||||
        "responses": {
 | 
					 | 
				
			||||||
          "200": {
 | 
					 | 
				
			||||||
            "content": {
 | 
					 | 
				
			||||||
              "application/zip": {
 | 
					 | 
				
			||||||
                "schema": {
 | 
					 | 
				
			||||||
                  "type": "string",
 | 
					 | 
				
			||||||
                  "format": "binary"
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
              }
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            "description": ""
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        "tags": [
 | 
					 | 
				
			||||||
          "Album"
 | 
					 | 
				
			||||||
        ],
 | 
					 | 
				
			||||||
        "security": [
 | 
					 | 
				
			||||||
          {
 | 
					 | 
				
			||||||
            "bearer": []
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          {
 | 
					 | 
				
			||||||
            "cookie": []
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          {
 | 
					 | 
				
			||||||
            "api_key": []
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        ]
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "/album/{id}/user/{userId}": {
 | 
					    "/album/{id}/user/{userId}": {
 | 
				
			||||||
      "delete": {
 | 
					      "delete": {
 | 
				
			||||||
        "operationId": "removeUserFromAlbum",
 | 
					        "operationId": "removeUserFromAlbum",
 | 
				
			||||||
@ -1153,10 +1086,48 @@
 | 
				
			|||||||
        ]
 | 
					        ]
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "/asset/download-files": {
 | 
					    "/asset/download": {
 | 
				
			||||||
      "post": {
 | 
					      "get": {
 | 
				
			||||||
        "operationId": "downloadFiles",
 | 
					        "operationId": "getDownloadInfo",
 | 
				
			||||||
        "parameters": [
 | 
					        "parameters": [
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            "name": "assetIds",
 | 
				
			||||||
 | 
					            "required": false,
 | 
				
			||||||
 | 
					            "in": "query",
 | 
				
			||||||
 | 
					            "schema": {
 | 
				
			||||||
 | 
					              "format": "uuid",
 | 
				
			||||||
 | 
					              "type": "array",
 | 
				
			||||||
 | 
					              "items": {
 | 
				
			||||||
 | 
					                "type": "string"
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            "name": "albumId",
 | 
				
			||||||
 | 
					            "required": false,
 | 
				
			||||||
 | 
					            "in": "query",
 | 
				
			||||||
 | 
					            "schema": {
 | 
				
			||||||
 | 
					              "format": "uuid",
 | 
				
			||||||
 | 
					              "type": "string"
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            "name": "userId",
 | 
				
			||||||
 | 
					            "required": false,
 | 
				
			||||||
 | 
					            "in": "query",
 | 
				
			||||||
 | 
					            "schema": {
 | 
				
			||||||
 | 
					              "format": "uuid",
 | 
				
			||||||
 | 
					              "type": "string"
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            "name": "archiveSize",
 | 
				
			||||||
 | 
					            "required": false,
 | 
				
			||||||
 | 
					            "in": "query",
 | 
				
			||||||
 | 
					            "schema": {
 | 
				
			||||||
 | 
					              "type": "number"
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
          {
 | 
					          {
 | 
				
			||||||
            "name": "key",
 | 
					            "name": "key",
 | 
				
			||||||
            "required": false,
 | 
					            "required": false,
 | 
				
			||||||
@ -1166,30 +1137,16 @@
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        ],
 | 
					        ],
 | 
				
			||||||
        "requestBody": {
 | 
					        "responses": {
 | 
				
			||||||
          "required": true,
 | 
					          "200": {
 | 
				
			||||||
 | 
					            "description": "",
 | 
				
			||||||
            "content": {
 | 
					            "content": {
 | 
				
			||||||
              "application/json": {
 | 
					              "application/json": {
 | 
				
			||||||
                "schema": {
 | 
					                "schema": {
 | 
				
			||||||
                "$ref": "#/components/schemas/DownloadFilesDto"
 | 
					                  "$ref": "#/components/schemas/DownloadResponseDto"
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
              }
 | 
					              }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        "responses": {
 | 
					 | 
				
			||||||
          "200": {
 | 
					 | 
				
			||||||
            "content": {
 | 
					 | 
				
			||||||
              "application/octet-stream": {
 | 
					 | 
				
			||||||
                "schema": {
 | 
					 | 
				
			||||||
                  "type": "string",
 | 
					 | 
				
			||||||
                  "format": "binary"
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
              }
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            "description": ""
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          "201": {
 | 
					 | 
				
			||||||
            "description": ""
 | 
					 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "tags": [
 | 
					        "tags": [
 | 
				
			||||||
@ -1206,29 +1163,10 @@
 | 
				
			|||||||
            "api_key": []
 | 
					            "api_key": []
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    "/asset/download-library": {
 | 
					      "post": {
 | 
				
			||||||
      "get": {
 | 
					        "operationId": "downloadArchive",
 | 
				
			||||||
        "operationId": "downloadLibrary",
 | 
					 | 
				
			||||||
        "description": "Current this is not used in any UI element",
 | 
					 | 
				
			||||||
        "parameters": [
 | 
					        "parameters": [
 | 
				
			||||||
          {
 | 
					 | 
				
			||||||
            "name": "name",
 | 
					 | 
				
			||||||
            "required": false,
 | 
					 | 
				
			||||||
            "in": "query",
 | 
					 | 
				
			||||||
            "schema": {
 | 
					 | 
				
			||||||
              "type": "string"
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          {
 | 
					 | 
				
			||||||
            "name": "skip",
 | 
					 | 
				
			||||||
            "required": false,
 | 
					 | 
				
			||||||
            "in": "query",
 | 
					 | 
				
			||||||
            "schema": {
 | 
					 | 
				
			||||||
              "type": "number"
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          {
 | 
					          {
 | 
				
			||||||
            "name": "key",
 | 
					            "name": "key",
 | 
				
			||||||
            "required": false,
 | 
					            "required": false,
 | 
				
			||||||
@ -1238,6 +1176,16 @@
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        ],
 | 
					        ],
 | 
				
			||||||
 | 
					        "requestBody": {
 | 
				
			||||||
 | 
					          "required": true,
 | 
				
			||||||
 | 
					          "content": {
 | 
				
			||||||
 | 
					            "application/json": {
 | 
				
			||||||
 | 
					              "schema": {
 | 
				
			||||||
 | 
					                "$ref": "#/components/schemas/AssetIdsDto"
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
        "responses": {
 | 
					        "responses": {
 | 
				
			||||||
          "200": {
 | 
					          "200": {
 | 
				
			||||||
            "content": {
 | 
					            "content": {
 | 
				
			||||||
@ -1268,7 +1216,7 @@
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "/asset/download/{id}": {
 | 
					    "/asset/download/{id}": {
 | 
				
			||||||
      "get": {
 | 
					      "post": {
 | 
				
			||||||
        "operationId": "downloadFile",
 | 
					        "operationId": "downloadFile",
 | 
				
			||||||
        "parameters": [
 | 
					        "parameters": [
 | 
				
			||||||
          {
 | 
					          {
 | 
				
			||||||
@ -5341,11 +5289,13 @@
 | 
				
			|||||||
          "FAILED"
 | 
					          "FAILED"
 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      "DownloadFilesDto": {
 | 
					      "DownloadArchiveInfo": {
 | 
				
			||||||
        "type": "object",
 | 
					        "type": "object",
 | 
				
			||||||
        "properties": {
 | 
					        "properties": {
 | 
				
			||||||
 | 
					          "size": {
 | 
				
			||||||
 | 
					            "type": "integer"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
          "assetIds": {
 | 
					          "assetIds": {
 | 
				
			||||||
            "title": "Array of asset ids to be downloaded",
 | 
					 | 
				
			||||||
            "type": "array",
 | 
					            "type": "array",
 | 
				
			||||||
            "items": {
 | 
					            "items": {
 | 
				
			||||||
              "type": "string"
 | 
					              "type": "string"
 | 
				
			||||||
@ -5353,9 +5303,28 @@
 | 
				
			|||||||
          }
 | 
					          }
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "required": [
 | 
					        "required": [
 | 
				
			||||||
 | 
					          "size",
 | 
				
			||||||
          "assetIds"
 | 
					          "assetIds"
 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
 | 
					      "DownloadResponseDto": {
 | 
				
			||||||
 | 
					        "type": "object",
 | 
				
			||||||
 | 
					        "properties": {
 | 
				
			||||||
 | 
					          "totalSize": {
 | 
				
			||||||
 | 
					            "type": "integer"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "archives": {
 | 
				
			||||||
 | 
					            "type": "array",
 | 
				
			||||||
 | 
					            "items": {
 | 
				
			||||||
 | 
					              "$ref": "#/components/schemas/DownloadArchiveInfo"
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "required": [
 | 
				
			||||||
 | 
					          "totalSize",
 | 
				
			||||||
 | 
					          "archives"
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
      "ExifResponseDto": {
 | 
					      "ExifResponseDto": {
 | 
				
			||||||
        "type": "object",
 | 
					        "type": "object",
 | 
				
			||||||
        "properties": {
 | 
					        "properties": {
 | 
				
			||||||
 | 
				
			|||||||
@ -16,6 +16,7 @@ export enum Permission {
 | 
				
			|||||||
  ALBUM_UPDATE = 'album.update',
 | 
					  ALBUM_UPDATE = 'album.update',
 | 
				
			||||||
  ALBUM_DELETE = 'album.delete',
 | 
					  ALBUM_DELETE = 'album.delete',
 | 
				
			||||||
  ALBUM_SHARE = 'album.share',
 | 
					  ALBUM_SHARE = 'album.share',
 | 
				
			||||||
 | 
					  ALBUM_DOWNLOAD = 'album.download',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  LIBRARY_READ = 'library.read',
 | 
					  LIBRARY_READ = 'library.read',
 | 
				
			||||||
  LIBRARY_DOWNLOAD = 'library.download',
 | 
					  LIBRARY_DOWNLOAD = 'library.download',
 | 
				
			||||||
@ -68,6 +69,10 @@ export class AccessCore {
 | 
				
			|||||||
        // TODO: fix this to not use authUser.id for shared link access control
 | 
					        // TODO: fix this to not use authUser.id for shared link access control
 | 
				
			||||||
        return this.repository.asset.hasOwnerAccess(authUser.id, id);
 | 
					        return this.repository.asset.hasOwnerAccess(authUser.id, id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      case Permission.ALBUM_DOWNLOAD: {
 | 
				
			||||||
 | 
					        return !!authUser.isAllowDownload && (await this.repository.album.hasSharedLinkAccess(sharedLinkId, id));
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // case Permission.ALBUM_READ:
 | 
					      // case Permission.ALBUM_READ:
 | 
				
			||||||
      //   return this.repository.album.hasSharedLinkAccess(sharedLinkId, id);
 | 
					      //   return this.repository.album.hasSharedLinkAccess(sharedLinkId, id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -122,6 +127,12 @@ export class AccessCore {
 | 
				
			|||||||
      case Permission.ALBUM_SHARE:
 | 
					      case Permission.ALBUM_SHARE:
 | 
				
			||||||
        return this.repository.album.hasOwnerAccess(authUser.id, id);
 | 
					        return this.repository.album.hasOwnerAccess(authUser.id, id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      case Permission.ALBUM_DOWNLOAD:
 | 
				
			||||||
 | 
					        return (
 | 
				
			||||||
 | 
					          (await this.repository.album.hasOwnerAccess(authUser.id, id)) ||
 | 
				
			||||||
 | 
					          (await this.repository.album.hasSharedAlbumAccess(authUser.id, id))
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      case Permission.LIBRARY_READ:
 | 
					      case Permission.LIBRARY_READ:
 | 
				
			||||||
        return authUser.id === id || (await this.repository.library.hasPartnerAccess(authUser.id, id));
 | 
					        return authUser.id === id || (await this.repository.library.hasPartnerAccess(authUser.id, id));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -44,6 +44,8 @@ export const IAssetRepository = 'IAssetRepository';
 | 
				
			|||||||
export interface IAssetRepository {
 | 
					export interface IAssetRepository {
 | 
				
			||||||
  getByDate(ownerId: string, date: Date): Promise<AssetEntity[]>;
 | 
					  getByDate(ownerId: string, date: Date): Promise<AssetEntity[]>;
 | 
				
			||||||
  getByIds(ids: string[]): Promise<AssetEntity[]>;
 | 
					  getByIds(ids: string[]): Promise<AssetEntity[]>;
 | 
				
			||||||
 | 
					  getByAlbumId(pagination: PaginationOptions, albumId: string): Paginated<AssetEntity>;
 | 
				
			||||||
 | 
					  getByUserId(pagination: PaginationOptions, userId: string): Paginated<AssetEntity>;
 | 
				
			||||||
  getWithout(pagination: PaginationOptions, property: WithoutProperty): Paginated<AssetEntity>;
 | 
					  getWithout(pagination: PaginationOptions, property: WithoutProperty): Paginated<AssetEntity>;
 | 
				
			||||||
  getWith(pagination: PaginationOptions, property: WithProperty): Paginated<AssetEntity>;
 | 
					  getWith(pagination: PaginationOptions, property: WithProperty): Paginated<AssetEntity>;
 | 
				
			||||||
  getFirstAssetForAlbumId(albumId: string): Promise<AssetEntity | null>;
 | 
					  getFirstAssetForAlbumId(albumId: string): Promise<AssetEntity | null>;
 | 
				
			||||||
 | 
				
			|||||||
@ -1,21 +1,48 @@
 | 
				
			|||||||
import { assetEntityStub, authStub, newAssetRepositoryMock } from '@test';
 | 
					import { BadRequestException } from '@nestjs/common';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  assetEntityStub,
 | 
				
			||||||
 | 
					  authStub,
 | 
				
			||||||
 | 
					  IAccessRepositoryMock,
 | 
				
			||||||
 | 
					  newAccessRepositoryMock,
 | 
				
			||||||
 | 
					  newAssetRepositoryMock,
 | 
				
			||||||
 | 
					  newStorageRepositoryMock,
 | 
				
			||||||
 | 
					} from '@test';
 | 
				
			||||||
import { when } from 'jest-when';
 | 
					import { when } from 'jest-when';
 | 
				
			||||||
import { AssetService, IAssetRepository, mapAsset } from '.';
 | 
					import { Readable } from 'stream';
 | 
				
			||||||
 | 
					import { IStorageRepository } from '../storage';
 | 
				
			||||||
 | 
					import { IAssetRepository } from './asset.repository';
 | 
				
			||||||
 | 
					import { AssetService } from './asset.service';
 | 
				
			||||||
 | 
					import { DownloadResponseDto } from './index';
 | 
				
			||||||
 | 
					import { mapAsset } from './response-dto';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const downloadResponse: DownloadResponseDto = {
 | 
				
			||||||
 | 
					  totalSize: 105_000,
 | 
				
			||||||
 | 
					  archives: [
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      assetIds: ['asset-id', 'asset-id'],
 | 
				
			||||||
 | 
					      size: 105_000,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  ],
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
describe(AssetService.name, () => {
 | 
					describe(AssetService.name, () => {
 | 
				
			||||||
  let sut: AssetService;
 | 
					  let sut: AssetService;
 | 
				
			||||||
 | 
					  let accessMock: IAccessRepositoryMock;
 | 
				
			||||||
  let assetMock: jest.Mocked<IAssetRepository>;
 | 
					  let assetMock: jest.Mocked<IAssetRepository>;
 | 
				
			||||||
 | 
					  let storageMock: jest.Mocked<IStorageRepository>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  it('should work', () => {
 | 
					  it('should work', () => {
 | 
				
			||||||
    expect(sut).toBeDefined();
 | 
					    expect(sut).toBeDefined();
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  beforeEach(async () => {
 | 
					  beforeEach(async () => {
 | 
				
			||||||
 | 
					    accessMock = newAccessRepositoryMock();
 | 
				
			||||||
    assetMock = newAssetRepositoryMock();
 | 
					    assetMock = newAssetRepositoryMock();
 | 
				
			||||||
    sut = new AssetService(assetMock);
 | 
					    storageMock = newStorageRepositoryMock();
 | 
				
			||||||
 | 
					    sut = new AssetService(accessMock, assetMock, storageMock);
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  describe('get map markers', () => {
 | 
					  describe('getMapMarkers', () => {
 | 
				
			||||||
    it('should get geo information of assets', async () => {
 | 
					    it('should get geo information of assets', async () => {
 | 
				
			||||||
      assetMock.getMapMarkers.mockResolvedValue(
 | 
					      assetMock.getMapMarkers.mockResolvedValue(
 | 
				
			||||||
        [assetEntityStub.withLocation].map((asset) => ({
 | 
					        [assetEntityStub.withLocation].map((asset) => ({
 | 
				
			||||||
@ -76,7 +103,6 @@ describe(AssetService.name, () => {
 | 
				
			|||||||
        [authStub.admin.id, new Date('2021-06-15T05:00:00.000Z')],
 | 
					        [authStub.admin.id, new Date('2021-06-15T05:00:00.000Z')],
 | 
				
			||||||
      ]);
 | 
					      ]);
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('should set the title correctly', async () => {
 | 
					    it('should set the title correctly', async () => {
 | 
				
			||||||
      when(assetMock.getByDate)
 | 
					      when(assetMock.getByDate)
 | 
				
			||||||
@ -97,4 +123,171 @@ describe(AssetService.name, () => {
 | 
				
			|||||||
        [authStub.admin.id, new Date('2021-06-15T00:00:00.000Z')],
 | 
					        [authStub.admin.id, new Date('2021-06-15T00:00:00.000Z')],
 | 
				
			||||||
      ]);
 | 
					      ]);
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe('downloadFile', () => {
 | 
				
			||||||
 | 
					    it('should require the asset.download permission', async () => {
 | 
				
			||||||
 | 
					      accessMock.asset.hasOwnerAccess.mockResolvedValue(false);
 | 
				
			||||||
 | 
					      accessMock.asset.hasAlbumAccess.mockResolvedValue(false);
 | 
				
			||||||
 | 
					      accessMock.asset.hasPartnerAccess.mockResolvedValue(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await expect(sut.downloadFile(authStub.admin, 'asset-1')).rejects.toBeInstanceOf(BadRequestException);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(accessMock.asset.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'asset-1');
 | 
				
			||||||
 | 
					      expect(accessMock.asset.hasAlbumAccess).toHaveBeenCalledWith(authStub.admin.id, 'asset-1');
 | 
				
			||||||
 | 
					      expect(accessMock.asset.hasPartnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'asset-1');
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('should throw an error if the asset is not found', async () => {
 | 
				
			||||||
 | 
					      accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
 | 
				
			||||||
 | 
					      assetMock.getByIds.mockResolvedValue([]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await expect(sut.downloadFile(authStub.admin, 'asset-1')).rejects.toBeInstanceOf(BadRequestException);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(assetMock.getByIds).toHaveBeenCalledWith(['asset-1']);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('should download a file', async () => {
 | 
				
			||||||
 | 
					      const stream = new Readable();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
 | 
				
			||||||
 | 
					      assetMock.getByIds.mockResolvedValue([assetEntityStub.image]);
 | 
				
			||||||
 | 
					      storageMock.createReadStream.mockResolvedValue({ stream });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await expect(sut.downloadFile(authStub.admin, 'asset-1')).resolves.toEqual({ stream });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(storageMock.createReadStream).toHaveBeenCalledWith(
 | 
				
			||||||
 | 
					        assetEntityStub.image.originalPath,
 | 
				
			||||||
 | 
					        assetEntityStub.image.mimeType,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('should download an archive', async () => {
 | 
				
			||||||
 | 
					      const archiveMock = {
 | 
				
			||||||
 | 
					        addFile: jest.fn(),
 | 
				
			||||||
 | 
					        finalize: jest.fn(),
 | 
				
			||||||
 | 
					        stream: new Readable(),
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
 | 
				
			||||||
 | 
					      assetMock.getByIds.mockResolvedValue([assetEntityStub.noResizePath, assetEntityStub.noWebpPath]);
 | 
				
			||||||
 | 
					      storageMock.createZipStream.mockReturnValue(archiveMock);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await expect(sut.downloadArchive(authStub.admin, { assetIds: ['asset-1', 'asset-2'] })).resolves.toEqual({
 | 
				
			||||||
 | 
					        stream: archiveMock.stream,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(archiveMock.addFile).toHaveBeenCalledTimes(2);
 | 
				
			||||||
 | 
					      expect(archiveMock.addFile).toHaveBeenNthCalledWith(1, 'upload/library/IMG_123.jpg', 'IMG_123.jpg');
 | 
				
			||||||
 | 
					      expect(archiveMock.addFile).toHaveBeenNthCalledWith(2, 'upload/library/IMG_456.jpg', 'IMG_456.jpg');
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('should handle duplicate file names', async () => {
 | 
				
			||||||
 | 
					      const archiveMock = {
 | 
				
			||||||
 | 
					        addFile: jest.fn(),
 | 
				
			||||||
 | 
					        finalize: jest.fn(),
 | 
				
			||||||
 | 
					        stream: new Readable(),
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
 | 
				
			||||||
 | 
					      assetMock.getByIds.mockResolvedValue([assetEntityStub.noResizePath, assetEntityStub.noResizePath]);
 | 
				
			||||||
 | 
					      storageMock.createZipStream.mockReturnValue(archiveMock);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await expect(sut.downloadArchive(authStub.admin, { assetIds: ['asset-1', 'asset-2'] })).resolves.toEqual({
 | 
				
			||||||
 | 
					        stream: archiveMock.stream,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(archiveMock.addFile).toHaveBeenCalledTimes(2);
 | 
				
			||||||
 | 
					      expect(archiveMock.addFile).toHaveBeenNthCalledWith(1, 'upload/library/IMG_123.jpg', 'IMG_123.jpg');
 | 
				
			||||||
 | 
					      expect(archiveMock.addFile).toHaveBeenNthCalledWith(2, 'upload/library/IMG_123.jpg', 'IMG_123+1.jpg');
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe('getDownloadInfo', () => {
 | 
				
			||||||
 | 
					    it('should throw an error for an invalid dto', async () => {
 | 
				
			||||||
 | 
					      await expect(sut.getDownloadInfo(authStub.admin, {})).rejects.toBeInstanceOf(BadRequestException);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('should return a list of archives (assetIds)', async () => {
 | 
				
			||||||
 | 
					      accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
 | 
				
			||||||
 | 
					      assetMock.getByIds.mockResolvedValue([assetEntityStub.image, assetEntityStub.video]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const assetIds = ['asset-1', 'asset-2'];
 | 
				
			||||||
 | 
					      await expect(sut.getDownloadInfo(authStub.admin, { assetIds })).resolves.toEqual(downloadResponse);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(assetMock.getByIds).toHaveBeenCalledWith(['asset-1', 'asset-2']);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('should return a list of archives (albumId)', async () => {
 | 
				
			||||||
 | 
					      accessMock.album.hasOwnerAccess.mockResolvedValue(true);
 | 
				
			||||||
 | 
					      assetMock.getByAlbumId.mockResolvedValue({
 | 
				
			||||||
 | 
					        items: [assetEntityStub.image, assetEntityStub.video],
 | 
				
			||||||
 | 
					        hasNextPage: false,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await expect(sut.getDownloadInfo(authStub.admin, { albumId: 'album-1' })).resolves.toEqual(downloadResponse);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(accessMock.album.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'album-1');
 | 
				
			||||||
 | 
					      expect(assetMock.getByAlbumId).toHaveBeenCalledWith({ take: 2500, skip: 0 }, 'album-1');
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('should return a list of archives (userId)', async () => {
 | 
				
			||||||
 | 
					      assetMock.getByUserId.mockResolvedValue({
 | 
				
			||||||
 | 
					        items: [assetEntityStub.image, assetEntityStub.video],
 | 
				
			||||||
 | 
					        hasNextPage: false,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await expect(sut.getDownloadInfo(authStub.admin, { userId: authStub.admin.id })).resolves.toEqual(
 | 
				
			||||||
 | 
					        downloadResponse,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(assetMock.getByUserId).toHaveBeenCalledWith({ take: 2500, skip: 0 }, authStub.admin.id);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('should split archives by size', async () => {
 | 
				
			||||||
 | 
					      assetMock.getByUserId.mockResolvedValue({
 | 
				
			||||||
 | 
					        items: [
 | 
				
			||||||
 | 
					          { ...assetEntityStub.image, id: 'asset-1' },
 | 
				
			||||||
 | 
					          { ...assetEntityStub.video, id: 'asset-2' },
 | 
				
			||||||
 | 
					          { ...assetEntityStub.withLocation, id: 'asset-3' },
 | 
				
			||||||
 | 
					          { ...assetEntityStub.noWebpPath, id: 'asset-4' },
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        hasNextPage: false,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await expect(
 | 
				
			||||||
 | 
					        sut.getDownloadInfo(authStub.admin, {
 | 
				
			||||||
 | 
					          userId: authStub.admin.id,
 | 
				
			||||||
 | 
					          archiveSize: 30_000,
 | 
				
			||||||
 | 
					        }),
 | 
				
			||||||
 | 
					      ).resolves.toEqual({
 | 
				
			||||||
 | 
					        totalSize: 251_456,
 | 
				
			||||||
 | 
					        archives: [
 | 
				
			||||||
 | 
					          { assetIds: ['asset-1', 'asset-2'], size: 105_000 },
 | 
				
			||||||
 | 
					          { assetIds: ['asset-3', 'asset-4'], size: 146_456 },
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('should include the video portion of a live photo', async () => {
 | 
				
			||||||
 | 
					      accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
 | 
				
			||||||
 | 
					      when(assetMock.getByIds)
 | 
				
			||||||
 | 
					        .calledWith([assetEntityStub.livePhotoStillAsset.id])
 | 
				
			||||||
 | 
					        .mockResolvedValue([assetEntityStub.livePhotoStillAsset]);
 | 
				
			||||||
 | 
					      when(assetMock.getByIds)
 | 
				
			||||||
 | 
					        .calledWith([assetEntityStub.livePhotoMotionAsset.id])
 | 
				
			||||||
 | 
					        .mockResolvedValue([assetEntityStub.livePhotoMotionAsset]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const assetIds = [assetEntityStub.livePhotoStillAsset.id];
 | 
				
			||||||
 | 
					      await expect(sut.getDownloadInfo(authStub.admin, { assetIds })).resolves.toEqual({
 | 
				
			||||||
 | 
					        totalSize: 125_000,
 | 
				
			||||||
 | 
					        archives: [
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            assetIds: [assetEntityStub.livePhotoStillAsset.id, assetEntityStub.livePhotoMotionAsset.id],
 | 
				
			||||||
 | 
					            size: 125_000,
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
				
			|||||||
@ -1,14 +1,27 @@
 | 
				
			|||||||
import { Inject } from '@nestjs/common';
 | 
					import { BadRequestException, Inject } from '@nestjs/common';
 | 
				
			||||||
import { DateTime } from 'luxon';
 | 
					import { DateTime } from 'luxon';
 | 
				
			||||||
 | 
					import { extname } from 'path';
 | 
				
			||||||
 | 
					import { AssetEntity } from '../../infra/entities/asset.entity';
 | 
				
			||||||
import { AuthUserDto } from '../auth';
 | 
					import { AuthUserDto } from '../auth';
 | 
				
			||||||
 | 
					import { HumanReadableSize, usePagination } from '../domain.util';
 | 
				
			||||||
 | 
					import { AccessCore, IAccessRepository, Permission } from '../index';
 | 
				
			||||||
 | 
					import { ImmichReadStream, IStorageRepository } from '../storage';
 | 
				
			||||||
import { IAssetRepository } from './asset.repository';
 | 
					import { IAssetRepository } from './asset.repository';
 | 
				
			||||||
import { MemoryLaneDto } from './dto';
 | 
					import { AssetIdsDto, DownloadArchiveInfo, DownloadDto, DownloadResponseDto, MemoryLaneDto } from './dto';
 | 
				
			||||||
import { MapMarkerDto } from './dto/map-marker.dto';
 | 
					import { MapMarkerDto } from './dto/map-marker.dto';
 | 
				
			||||||
import { mapAsset, MapMarkerResponseDto } from './response-dto';
 | 
					import { mapAsset, MapMarkerResponseDto } from './response-dto';
 | 
				
			||||||
import { MemoryLaneResponseDto } from './response-dto/memory-lane-response.dto';
 | 
					import { MemoryLaneResponseDto } from './response-dto/memory-lane-response.dto';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class AssetService {
 | 
					export class AssetService {
 | 
				
			||||||
  constructor(@Inject(IAssetRepository) private assetRepository: IAssetRepository) {}
 | 
					  private access: AccessCore;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  constructor(
 | 
				
			||||||
 | 
					    @Inject(IAccessRepository) accessRepository: IAccessRepository,
 | 
				
			||||||
 | 
					    @Inject(IAssetRepository) private assetRepository: IAssetRepository,
 | 
				
			||||||
 | 
					    @Inject(IStorageRepository) private storageRepository: IStorageRepository,
 | 
				
			||||||
 | 
					  ) {
 | 
				
			||||||
 | 
					    this.access = new AccessCore(accessRepository);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  getMapMarkers(authUser: AuthUserDto, options: MapMarkerDto): Promise<MapMarkerResponseDto[]> {
 | 
					  getMapMarkers(authUser: AuthUserDto, options: MapMarkerDto): Promise<MapMarkerResponseDto[]> {
 | 
				
			||||||
    return this.assetRepository.getMapMarkers(authUser.id, options);
 | 
					    return this.assetRepository.getMapMarkers(authUser.id, options);
 | 
				
			||||||
@ -32,4 +45,102 @@ export class AssetService {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    return Promise.all(requests).then((results) => results.filter((result) => result.assets.length > 0));
 | 
					    return Promise.all(requests).then((results) => results.filter((result) => result.assets.length > 0));
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async downloadFile(authUser: AuthUserDto, id: string): Promise<ImmichReadStream> {
 | 
				
			||||||
 | 
					    await this.access.requirePermission(authUser, Permission.ASSET_DOWNLOAD, id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const [asset] = await this.assetRepository.getByIds([id]);
 | 
				
			||||||
 | 
					    if (!asset) {
 | 
				
			||||||
 | 
					      throw new BadRequestException('Asset not found');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return this.storageRepository.createReadStream(asset.originalPath, asset.mimeType);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async getDownloadInfo(authUser: AuthUserDto, dto: DownloadDto): Promise<DownloadResponseDto> {
 | 
				
			||||||
 | 
					    const targetSize = dto.archiveSize || HumanReadableSize.GiB * 4;
 | 
				
			||||||
 | 
					    const archives: DownloadArchiveInfo[] = [];
 | 
				
			||||||
 | 
					    let archive: DownloadArchiveInfo = { size: 0, assetIds: [] };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const assetPagination = await this.getDownloadAssets(authUser, dto);
 | 
				
			||||||
 | 
					    for await (const assets of assetPagination) {
 | 
				
			||||||
 | 
					      // motion part of live photos
 | 
				
			||||||
 | 
					      const motionIds = assets.map((asset) => asset.livePhotoVideoId).filter<string>((id): id is string => !!id);
 | 
				
			||||||
 | 
					      if (motionIds.length > 0) {
 | 
				
			||||||
 | 
					        assets.push(...(await this.assetRepository.getByIds(motionIds)));
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      for (const asset of assets) {
 | 
				
			||||||
 | 
					        archive.size += Number(asset.exifInfo?.fileSizeInByte || 0);
 | 
				
			||||||
 | 
					        archive.assetIds.push(asset.id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (archive.size > targetSize) {
 | 
				
			||||||
 | 
					          archives.push(archive);
 | 
				
			||||||
 | 
					          archive = { size: 0, assetIds: [] };
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (archive.assetIds.length > 0) {
 | 
				
			||||||
 | 
					        archives.push(archive);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      totalSize: archives.reduce((total, item) => (total += item.size), 0),
 | 
				
			||||||
 | 
					      archives,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async downloadArchive(authUser: AuthUserDto, dto: AssetIdsDto): Promise<ImmichReadStream> {
 | 
				
			||||||
 | 
					    await this.access.requirePermission(authUser, Permission.ASSET_DOWNLOAD, dto.assetIds);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const zip = this.storageRepository.createZipStream();
 | 
				
			||||||
 | 
					    const assets = await this.assetRepository.getByIds(dto.assetIds);
 | 
				
			||||||
 | 
					    const paths: Record<string, boolean> = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (const { originalPath, originalFileName } of assets) {
 | 
				
			||||||
 | 
					      const ext = extname(originalPath);
 | 
				
			||||||
 | 
					      let filename = `${originalFileName}${ext}`;
 | 
				
			||||||
 | 
					      for (let i = 0; i < 10_000; i++) {
 | 
				
			||||||
 | 
					        if (!paths[filename]) {
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        filename = `${originalFileName}+${i + 1}${ext}`;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      paths[filename] = true;
 | 
				
			||||||
 | 
					      zip.addFile(originalPath, filename);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    zip.finalize();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return { stream: zip.stream };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private async getDownloadAssets(authUser: AuthUserDto, dto: DownloadDto): Promise<AsyncGenerator<AssetEntity[]>> {
 | 
				
			||||||
 | 
					    const PAGINATION_SIZE = 2500;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (dto.assetIds) {
 | 
				
			||||||
 | 
					      const assetIds = dto.assetIds;
 | 
				
			||||||
 | 
					      await this.access.requirePermission(authUser, Permission.ASSET_DOWNLOAD, assetIds);
 | 
				
			||||||
 | 
					      const assets = await this.assetRepository.getByIds(assetIds);
 | 
				
			||||||
 | 
					      return (async function* () {
 | 
				
			||||||
 | 
					        yield assets;
 | 
				
			||||||
 | 
					      })();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (dto.albumId) {
 | 
				
			||||||
 | 
					      const albumId = dto.albumId;
 | 
				
			||||||
 | 
					      await this.access.requirePermission(authUser, Permission.ALBUM_DOWNLOAD, albumId);
 | 
				
			||||||
 | 
					      return usePagination(PAGINATION_SIZE, (pagination) => this.assetRepository.getByAlbumId(pagination, albumId));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (dto.userId) {
 | 
				
			||||||
 | 
					      const userId = dto.userId;
 | 
				
			||||||
 | 
					      await this.access.requirePermission(authUser, Permission.LIBRARY_DOWNLOAD, userId);
 | 
				
			||||||
 | 
					      return usePagination(PAGINATION_SIZE, (pagination) => this.assetRepository.getByUserId(pagination, userId));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    throw new BadRequestException('assetIds, albumId, or userId is required');
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										31
									
								
								server/src/domain/asset/dto/download.dto.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								server/src/domain/asset/dto/download.dto.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,31 @@
 | 
				
			|||||||
 | 
					import { ValidateUUID } from '@app/immich/decorators/validate-uuid.decorator';
 | 
				
			||||||
 | 
					import { ApiProperty } from '@nestjs/swagger';
 | 
				
			||||||
 | 
					import { IsInt, IsOptional, IsPositive } from 'class-validator';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class DownloadDto {
 | 
				
			||||||
 | 
					  @ValidateUUID({ each: true, optional: true })
 | 
				
			||||||
 | 
					  assetIds?: string[];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @ValidateUUID({ optional: true })
 | 
				
			||||||
 | 
					  albumId?: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @ValidateUUID({ optional: true })
 | 
				
			||||||
 | 
					  userId?: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @IsInt()
 | 
				
			||||||
 | 
					  @IsPositive()
 | 
				
			||||||
 | 
					  @IsOptional()
 | 
				
			||||||
 | 
					  archiveSize?: number;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class DownloadResponseDto {
 | 
				
			||||||
 | 
					  @ApiProperty({ type: 'integer' })
 | 
				
			||||||
 | 
					  totalSize!: number;
 | 
				
			||||||
 | 
					  archives!: DownloadArchiveInfo[];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class DownloadArchiveInfo {
 | 
				
			||||||
 | 
					  @ApiProperty({ type: 'integer' })
 | 
				
			||||||
 | 
					  size!: number;
 | 
				
			||||||
 | 
					  assetIds!: string[];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,3 +1,4 @@
 | 
				
			|||||||
export * from './asset-ids.dto';
 | 
					export * from './asset-ids.dto';
 | 
				
			||||||
 | 
					export * from './download.dto';
 | 
				
			||||||
export * from './map-marker.dto';
 | 
					export * from './map-marker.dto';
 | 
				
			||||||
export * from './memory-lane.dto';
 | 
					export * from './memory-lane.dto';
 | 
				
			||||||
 | 
				
			|||||||
@ -1,9 +1,14 @@
 | 
				
			|||||||
import { ReadStream } from 'fs';
 | 
					import { Readable } from 'stream';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface ImmichReadStream {
 | 
					export interface ImmichReadStream {
 | 
				
			||||||
  stream: ReadStream;
 | 
					  stream: Readable;
 | 
				
			||||||
  type: string;
 | 
					  type?: string;
 | 
				
			||||||
  length: number;
 | 
					  length?: number;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface ImmichZipStream extends ImmichReadStream {
 | 
				
			||||||
 | 
					  addFile: (inputPath: string, filename: string) => void;
 | 
				
			||||||
 | 
					  finalize: () => Promise<void>;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface DiskUsage {
 | 
					export interface DiskUsage {
 | 
				
			||||||
@ -15,7 +20,8 @@ export interface DiskUsage {
 | 
				
			|||||||
export const IStorageRepository = 'IStorageRepository';
 | 
					export const IStorageRepository = 'IStorageRepository';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface IStorageRepository {
 | 
					export interface IStorageRepository {
 | 
				
			||||||
  createReadStream(filepath: string, mimeType: string): Promise<ImmichReadStream>;
 | 
					  createZipStream(): ImmichZipStream;
 | 
				
			||||||
 | 
					  createReadStream(filepath: string, mimeType?: string | null): Promise<ImmichReadStream>;
 | 
				
			||||||
  unlink(filepath: string): Promise<void>;
 | 
					  unlink(filepath: string): Promise<void>;
 | 
				
			||||||
  unlinkDir(folder: string, options?: { recursive?: boolean; force?: boolean }): Promise<void>;
 | 
					  unlinkDir(folder: string, options?: { recursive?: boolean; force?: boolean }): Promise<void>;
 | 
				
			||||||
  removeEmptyDirs(folder: string): Promise<void>;
 | 
					  removeEmptyDirs(folder: string): Promise<void>;
 | 
				
			||||||
 | 
				
			|||||||
@ -1,13 +1,10 @@
 | 
				
			|||||||
import { AlbumResponseDto } from '@app/domain';
 | 
					import { AlbumResponseDto } from '@app/domain';
 | 
				
			||||||
import { Body, Controller, Delete, Get, Param, Put, Query, Response } from '@nestjs/common';
 | 
					import { Body, Controller, Delete, Get, Param, Put } from '@nestjs/common';
 | 
				
			||||||
import { ApiOkResponse, ApiTags } from '@nestjs/swagger';
 | 
					import { ApiTags } from '@nestjs/swagger';
 | 
				
			||||||
import { Response as Res } from 'express';
 | 
					 | 
				
			||||||
import { handleDownload } from '../../app.utils';
 | 
					 | 
				
			||||||
import { UUIDParamDto } from '../../controllers/dto/uuid-param.dto';
 | 
					import { UUIDParamDto } from '../../controllers/dto/uuid-param.dto';
 | 
				
			||||||
import { AuthUser, AuthUserDto } from '../../decorators/auth-user.decorator';
 | 
					import { AuthUser, AuthUserDto } from '../../decorators/auth-user.decorator';
 | 
				
			||||||
import { Authenticated, SharedLinkRoute } from '../../decorators/authenticated.decorator';
 | 
					import { Authenticated, SharedLinkRoute } from '../../decorators/authenticated.decorator';
 | 
				
			||||||
import { UseValidation } from '../../decorators/use-validation.decorator';
 | 
					import { UseValidation } from '../../decorators/use-validation.decorator';
 | 
				
			||||||
import { DownloadDto } from '../asset/dto/download-library.dto';
 | 
					 | 
				
			||||||
import { AlbumService } from './album.service';
 | 
					import { AlbumService } from './album.service';
 | 
				
			||||||
import { AddAssetsDto } from './dto/add-assets.dto';
 | 
					import { AddAssetsDto } from './dto/add-assets.dto';
 | 
				
			||||||
import { RemoveAssetsDto } from './dto/remove-assets.dto';
 | 
					import { RemoveAssetsDto } from './dto/remove-assets.dto';
 | 
				
			||||||
@ -18,7 +15,7 @@ import { AddAssetsResponseDto } from './response-dto/add-assets-response.dto';
 | 
				
			|||||||
@Authenticated()
 | 
					@Authenticated()
 | 
				
			||||||
@UseValidation()
 | 
					@UseValidation()
 | 
				
			||||||
export class AlbumController {
 | 
					export class AlbumController {
 | 
				
			||||||
  constructor(private readonly service: AlbumService) {}
 | 
					  constructor(private service: AlbumService) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @SharedLinkRoute()
 | 
					  @SharedLinkRoute()
 | 
				
			||||||
  @Put(':id/assets')
 | 
					  @Put(':id/assets')
 | 
				
			||||||
@ -46,16 +43,4 @@ export class AlbumController {
 | 
				
			|||||||
  ): Promise<AlbumResponseDto> {
 | 
					  ): Promise<AlbumResponseDto> {
 | 
				
			||||||
    return this.service.removeAssets(authUser, id, dto);
 | 
					    return this.service.removeAssets(authUser, id, dto);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					 | 
				
			||||||
  @SharedLinkRoute()
 | 
					 | 
				
			||||||
  @Get(':id/download')
 | 
					 | 
				
			||||||
  @ApiOkResponse({ content: { 'application/zip': { schema: { type: 'string', format: 'binary' } } } })
 | 
					 | 
				
			||||||
  downloadArchive(
 | 
					 | 
				
			||||||
    @AuthUser() authUser: AuthUserDto,
 | 
					 | 
				
			||||||
    @Param() { id }: UUIDParamDto,
 | 
					 | 
				
			||||||
    @Query() dto: DownloadDto,
 | 
					 | 
				
			||||||
    @Response({ passthrough: true }) res: Res,
 | 
					 | 
				
			||||||
  ) {
 | 
					 | 
				
			||||||
    return this.service.downloadArchive(authUser, id, dto).then((download) => handleDownload(download, res));
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,13 +1,12 @@
 | 
				
			|||||||
import { AlbumEntity, AssetEntity } from '@app/infra/entities';
 | 
					import { AlbumEntity, AssetEntity } from '@app/infra/entities';
 | 
				
			||||||
import { Module } from '@nestjs/common';
 | 
					import { Module } from '@nestjs/common';
 | 
				
			||||||
import { TypeOrmModule } from '@nestjs/typeorm';
 | 
					import { TypeOrmModule } from '@nestjs/typeorm';
 | 
				
			||||||
import { DownloadModule } from '../../modules/download/download.module';
 | 
					 | 
				
			||||||
import { AlbumRepository, IAlbumRepository } from './album-repository';
 | 
					import { AlbumRepository, IAlbumRepository } from './album-repository';
 | 
				
			||||||
import { AlbumController } from './album.controller';
 | 
					import { AlbumController } from './album.controller';
 | 
				
			||||||
import { AlbumService } from './album.service';
 | 
					import { AlbumService } from './album.service';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Module({
 | 
					@Module({
 | 
				
			||||||
  imports: [TypeOrmModule.forFeature([AlbumEntity, AssetEntity]), DownloadModule],
 | 
					  imports: [TypeOrmModule.forFeature([AlbumEntity, AssetEntity])],
 | 
				
			||||||
  controllers: [AlbumController],
 | 
					  controllers: [AlbumController],
 | 
				
			||||||
  providers: [AlbumService, { provide: IAlbumRepository, useClass: AlbumRepository }],
 | 
					  providers: [AlbumService, { provide: IAlbumRepository, useClass: AlbumRepository }],
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
 | 
				
			|||||||
@ -3,7 +3,6 @@ import { AlbumEntity, UserEntity } from '@app/infra/entities';
 | 
				
			|||||||
import { ForbiddenException, NotFoundException } from '@nestjs/common';
 | 
					import { ForbiddenException, NotFoundException } from '@nestjs/common';
 | 
				
			||||||
import { userEntityStub } from '@test';
 | 
					import { userEntityStub } from '@test';
 | 
				
			||||||
import { AuthUserDto } from '../../decorators/auth-user.decorator';
 | 
					import { AuthUserDto } from '../../decorators/auth-user.decorator';
 | 
				
			||||||
import { DownloadService } from '../../modules/download/download.service';
 | 
					 | 
				
			||||||
import { IAlbumRepository } from './album-repository';
 | 
					import { IAlbumRepository } from './album-repository';
 | 
				
			||||||
import { AlbumService } from './album.service';
 | 
					import { AlbumService } from './album.service';
 | 
				
			||||||
import { AddAssetsResponseDto } from './response-dto/add-assets-response.dto';
 | 
					import { AddAssetsResponseDto } from './response-dto/add-assets-response.dto';
 | 
				
			||||||
@ -11,7 +10,6 @@ import { AddAssetsResponseDto } from './response-dto/add-assets-response.dto';
 | 
				
			|||||||
describe('Album service', () => {
 | 
					describe('Album service', () => {
 | 
				
			||||||
  let sut: AlbumService;
 | 
					  let sut: AlbumService;
 | 
				
			||||||
  let albumRepositoryMock: jest.Mocked<IAlbumRepository>;
 | 
					  let albumRepositoryMock: jest.Mocked<IAlbumRepository>;
 | 
				
			||||||
  let downloadServiceMock: jest.Mocked<Partial<DownloadService>>;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const authUser: AuthUserDto = Object.freeze({
 | 
					  const authUser: AuthUserDto = Object.freeze({
 | 
				
			||||||
    id: '1111',
 | 
					    id: '1111',
 | 
				
			||||||
@ -98,11 +96,7 @@ describe('Album service', () => {
 | 
				
			|||||||
      updateThumbnails: jest.fn(),
 | 
					      updateThumbnails: jest.fn(),
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    downloadServiceMock = {
 | 
					    sut = new AlbumService(albumRepositoryMock);
 | 
				
			||||||
      downloadArchive: jest.fn(),
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    sut = new AlbumService(albumRepositoryMock, downloadServiceMock as DownloadService);
 | 
					 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  it('gets an owned album', async () => {
 | 
					  it('gets an owned album', async () => {
 | 
				
			||||||
 | 
				
			|||||||
@ -2,8 +2,6 @@ import { AlbumResponseDto, mapAlbum } from '@app/domain';
 | 
				
			|||||||
import { AlbumEntity } from '@app/infra/entities';
 | 
					import { AlbumEntity } from '@app/infra/entities';
 | 
				
			||||||
import { BadRequestException, ForbiddenException, Inject, Injectable, Logger, NotFoundException } from '@nestjs/common';
 | 
					import { BadRequestException, ForbiddenException, Inject, Injectable, Logger, NotFoundException } from '@nestjs/common';
 | 
				
			||||||
import { AuthUserDto } from '../../decorators/auth-user.decorator';
 | 
					import { AuthUserDto } from '../../decorators/auth-user.decorator';
 | 
				
			||||||
import { DownloadService } from '../../modules/download/download.service';
 | 
					 | 
				
			||||||
import { DownloadDto } from '../asset/dto/download-library.dto';
 | 
					 | 
				
			||||||
import { IAlbumRepository } from './album-repository';
 | 
					import { IAlbumRepository } from './album-repository';
 | 
				
			||||||
import { AddAssetsDto } from './dto/add-assets.dto';
 | 
					import { AddAssetsDto } from './dto/add-assets.dto';
 | 
				
			||||||
import { RemoveAssetsDto } from './dto/remove-assets.dto';
 | 
					import { RemoveAssetsDto } from './dto/remove-assets.dto';
 | 
				
			||||||
@ -13,10 +11,7 @@ import { AddAssetsResponseDto } from './response-dto/add-assets-response.dto';
 | 
				
			|||||||
export class AlbumService {
 | 
					export class AlbumService {
 | 
				
			||||||
  private logger = new Logger(AlbumService.name);
 | 
					  private logger = new Logger(AlbumService.name);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  constructor(
 | 
					  constructor(@Inject(IAlbumRepository) private repository: IAlbumRepository) {}
 | 
				
			||||||
    @Inject(IAlbumRepository) private albumRepository: IAlbumRepository,
 | 
					 | 
				
			||||||
    private downloadService: DownloadService,
 | 
					 | 
				
			||||||
  ) {}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private async _getAlbum({
 | 
					  private async _getAlbum({
 | 
				
			||||||
    authUser,
 | 
					    authUser,
 | 
				
			||||||
@ -27,9 +22,9 @@ export class AlbumService {
 | 
				
			|||||||
    albumId: string;
 | 
					    albumId: string;
 | 
				
			||||||
    validateIsOwner?: boolean;
 | 
					    validateIsOwner?: boolean;
 | 
				
			||||||
  }): Promise<AlbumEntity> {
 | 
					  }): Promise<AlbumEntity> {
 | 
				
			||||||
    await this.albumRepository.updateThumbnails();
 | 
					    await this.repository.updateThumbnails();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const album = await this.albumRepository.get(albumId);
 | 
					    const album = await this.repository.get(albumId);
 | 
				
			||||||
    if (!album) {
 | 
					    if (!album) {
 | 
				
			||||||
      throw new NotFoundException('Album Not Found');
 | 
					      throw new NotFoundException('Album Not Found');
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -50,7 +45,7 @@ export class AlbumService {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  async removeAssets(authUser: AuthUserDto, albumId: string, dto: RemoveAssetsDto): Promise<AlbumResponseDto> {
 | 
					  async removeAssets(authUser: AuthUserDto, albumId: string, dto: RemoveAssetsDto): Promise<AlbumResponseDto> {
 | 
				
			||||||
    const album = await this._getAlbum({ authUser, albumId });
 | 
					    const album = await this._getAlbum({ authUser, albumId });
 | 
				
			||||||
    const deletedCount = await this.albumRepository.removeAssets(album, dto);
 | 
					    const deletedCount = await this.repository.removeAssets(album, dto);
 | 
				
			||||||
    const newAlbum = await this._getAlbum({ authUser, albumId });
 | 
					    const newAlbum = await this._getAlbum({ authUser, albumId });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (deletedCount !== dto.assetIds.length) {
 | 
					    if (deletedCount !== dto.assetIds.length) {
 | 
				
			||||||
@ -67,7 +62,7 @@ export class AlbumService {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const album = await this._getAlbum({ authUser, albumId, validateIsOwner: false });
 | 
					    const album = await this._getAlbum({ authUser, albumId, validateIsOwner: false });
 | 
				
			||||||
    const result = await this.albumRepository.addAssets(album, dto);
 | 
					    const result = await this.repository.addAssets(album, dto);
 | 
				
			||||||
    const newAlbum = await this._getAlbum({ authUser, albumId, validateIsOwner: false });
 | 
					    const newAlbum = await this._getAlbum({ authUser, albumId, validateIsOwner: false });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
@ -75,19 +70,4 @@ export class AlbumService {
 | 
				
			|||||||
      album: mapAlbum(newAlbum),
 | 
					      album: mapAlbum(newAlbum),
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					 | 
				
			||||||
  async downloadArchive(authUser: AuthUserDto, albumId: string, dto: DownloadDto) {
 | 
					 | 
				
			||||||
    this.checkDownloadAccess(authUser);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const album = await this._getAlbum({ authUser, albumId, validateIsOwner: false });
 | 
					 | 
				
			||||||
    const assets = (album.assets || []).map((asset) => asset).slice(dto.skip || 0);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return this.downloadService.downloadArchive(album.albumName, assets);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private checkDownloadAccess(authUser: AuthUserDto) {
 | 
					 | 
				
			||||||
    if (authUser.isPublicUser && !authUser.isAllowDownload) {
 | 
					 | 
				
			||||||
      throw new ForbiddenException();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,4 @@
 | 
				
			|||||||
import { AssetResponseDto, ImmichReadStream } from '@app/domain';
 | 
					import { AssetResponseDto } from '@app/domain';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  Body,
 | 
					  Body,
 | 
				
			||||||
  Controller,
 | 
					  Controller,
 | 
				
			||||||
@ -14,7 +14,6 @@ import {
 | 
				
			|||||||
  Put,
 | 
					  Put,
 | 
				
			||||||
  Query,
 | 
					  Query,
 | 
				
			||||||
  Response,
 | 
					  Response,
 | 
				
			||||||
  StreamableFile,
 | 
					 | 
				
			||||||
  UploadedFiles,
 | 
					  UploadedFiles,
 | 
				
			||||||
  UseInterceptors,
 | 
					  UseInterceptors,
 | 
				
			||||||
  ValidationPipe,
 | 
					  ValidationPipe,
 | 
				
			||||||
@ -22,7 +21,6 @@ import {
 | 
				
			|||||||
import { FileFieldsInterceptor } from '@nestjs/platform-express';
 | 
					import { FileFieldsInterceptor } from '@nestjs/platform-express';
 | 
				
			||||||
import { ApiBody, ApiConsumes, ApiHeader, ApiOkResponse, ApiTags } from '@nestjs/swagger';
 | 
					import { ApiBody, ApiConsumes, ApiHeader, ApiOkResponse, ApiTags } from '@nestjs/swagger';
 | 
				
			||||||
import { Response as Res } from 'express';
 | 
					import { Response as Res } from 'express';
 | 
				
			||||||
import { handleDownload } from '../../app.utils';
 | 
					 | 
				
			||||||
import { assetUploadOption, ImmichFile } from '../../config/asset-upload.config';
 | 
					import { assetUploadOption, ImmichFile } from '../../config/asset-upload.config';
 | 
				
			||||||
import { UUIDParamDto } from '../../controllers/dto/uuid-param.dto';
 | 
					import { UUIDParamDto } from '../../controllers/dto/uuid-param.dto';
 | 
				
			||||||
import { AuthUser, AuthUserDto } from '../../decorators/auth-user.decorator';
 | 
					import { AuthUser, AuthUserDto } from '../../decorators/auth-user.decorator';
 | 
				
			||||||
@ -36,8 +34,6 @@ import { CheckExistingAssetsDto } from './dto/check-existing-assets.dto';
 | 
				
			|||||||
import { CreateAssetDto, ImportAssetDto, mapToUploadFile } from './dto/create-asset.dto';
 | 
					import { CreateAssetDto, ImportAssetDto, mapToUploadFile } from './dto/create-asset.dto';
 | 
				
			||||||
import { DeleteAssetDto } from './dto/delete-asset.dto';
 | 
					import { DeleteAssetDto } from './dto/delete-asset.dto';
 | 
				
			||||||
import { DeviceIdDto } from './dto/device-id.dto';
 | 
					import { DeviceIdDto } from './dto/device-id.dto';
 | 
				
			||||||
import { DownloadFilesDto } from './dto/download-files.dto';
 | 
					 | 
				
			||||||
import { DownloadDto } from './dto/download-library.dto';
 | 
					 | 
				
			||||||
import { GetAssetByTimeBucketDto } from './dto/get-asset-by-time-bucket.dto';
 | 
					import { GetAssetByTimeBucketDto } from './dto/get-asset-by-time-bucket.dto';
 | 
				
			||||||
import { GetAssetCountByTimeBucketDto } from './dto/get-asset-count-by-time-bucket.dto';
 | 
					import { GetAssetCountByTimeBucketDto } from './dto/get-asset-count-by-time-bucket.dto';
 | 
				
			||||||
import { GetAssetThumbnailDto } from './dto/get-asset-thumbnail.dto';
 | 
					import { GetAssetThumbnailDto } from './dto/get-asset-thumbnail.dto';
 | 
				
			||||||
@ -54,10 +50,6 @@ import { CuratedLocationsResponseDto } from './response-dto/curated-locations-re
 | 
				
			|||||||
import { CuratedObjectsResponseDto } from './response-dto/curated-objects-response.dto';
 | 
					import { CuratedObjectsResponseDto } from './response-dto/curated-objects-response.dto';
 | 
				
			||||||
import { DeleteAssetResponseDto } from './response-dto/delete-asset-response.dto';
 | 
					import { DeleteAssetResponseDto } from './response-dto/delete-asset-response.dto';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function asStreamableFile({ stream, type, length }: ImmichReadStream) {
 | 
					 | 
				
			||||||
  return new StreamableFile(stream, { type, length });
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
interface UploadFiles {
 | 
					interface UploadFiles {
 | 
				
			||||||
  assetData: ImmichFile[];
 | 
					  assetData: ImmichFile[];
 | 
				
			||||||
  livePhotoData?: ImmichFile[];
 | 
					  livePhotoData?: ImmichFile[];
 | 
				
			||||||
@ -128,38 +120,6 @@ export class AssetController {
 | 
				
			|||||||
    return responseDto;
 | 
					    return responseDto;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @SharedLinkRoute()
 | 
					 | 
				
			||||||
  @Get('/download/:id')
 | 
					 | 
				
			||||||
  @ApiOkResponse({ content: { 'application/octet-stream': { schema: { type: 'string', format: 'binary' } } } })
 | 
					 | 
				
			||||||
  downloadFile(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto) {
 | 
					 | 
				
			||||||
    return this.assetService.downloadFile(authUser, id).then(asStreamableFile);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @SharedLinkRoute()
 | 
					 | 
				
			||||||
  @Post('/download-files')
 | 
					 | 
				
			||||||
  @ApiOkResponse({ content: { 'application/octet-stream': { schema: { type: 'string', format: 'binary' } } } })
 | 
					 | 
				
			||||||
  downloadFiles(
 | 
					 | 
				
			||||||
    @AuthUser() authUser: AuthUserDto,
 | 
					 | 
				
			||||||
    @Response({ passthrough: true }) res: Res,
 | 
					 | 
				
			||||||
    @Body(new ValidationPipe()) dto: DownloadFilesDto,
 | 
					 | 
				
			||||||
  ) {
 | 
					 | 
				
			||||||
    return this.assetService.downloadFiles(authUser, dto).then((download) => handleDownload(download, res));
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  /**
 | 
					 | 
				
			||||||
   * Current this is not used in any UI element
 | 
					 | 
				
			||||||
   */
 | 
					 | 
				
			||||||
  @SharedLinkRoute()
 | 
					 | 
				
			||||||
  @Get('/download-library')
 | 
					 | 
				
			||||||
  @ApiOkResponse({ content: { 'application/octet-stream': { schema: { type: 'string', format: 'binary' } } } })
 | 
					 | 
				
			||||||
  downloadLibrary(
 | 
					 | 
				
			||||||
    @AuthUser() authUser: AuthUserDto,
 | 
					 | 
				
			||||||
    @Query(new ValidationPipe({ transform: true })) dto: DownloadDto,
 | 
					 | 
				
			||||||
    @Response({ passthrough: true }) res: Res,
 | 
					 | 
				
			||||||
  ) {
 | 
					 | 
				
			||||||
    return this.assetService.downloadLibrary(authUser, dto).then((download) => handleDownload(download, res));
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @SharedLinkRoute()
 | 
					  @SharedLinkRoute()
 | 
				
			||||||
  @Get('/file/:id')
 | 
					  @Get('/file/:id')
 | 
				
			||||||
  @Header('Cache-Control', 'private, max-age=86400, no-transform')
 | 
					  @Header('Cache-Control', 'private, max-age=86400, no-transform')
 | 
				
			||||||
 | 
				
			|||||||
@ -1,17 +1,12 @@
 | 
				
			|||||||
import { AssetEntity, ExifEntity } from '@app/infra/entities';
 | 
					import { AssetEntity, ExifEntity } from '@app/infra/entities';
 | 
				
			||||||
import { Module } from '@nestjs/common';
 | 
					import { Module } from '@nestjs/common';
 | 
				
			||||||
import { TypeOrmModule } from '@nestjs/typeorm';
 | 
					import { TypeOrmModule } from '@nestjs/typeorm';
 | 
				
			||||||
import { DownloadModule } from '../../modules/download/download.module';
 | 
					 | 
				
			||||||
import { AssetRepository, IAssetRepository } from './asset-repository';
 | 
					import { AssetRepository, IAssetRepository } from './asset-repository';
 | 
				
			||||||
import { AssetController } from './asset.controller';
 | 
					import { AssetController } from './asset.controller';
 | 
				
			||||||
import { AssetService } from './asset.service';
 | 
					import { AssetService } from './asset.service';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Module({
 | 
					@Module({
 | 
				
			||||||
  imports: [
 | 
					  imports: [TypeOrmModule.forFeature([AssetEntity, ExifEntity])],
 | 
				
			||||||
    //
 | 
					 | 
				
			||||||
    TypeOrmModule.forFeature([AssetEntity, ExifEntity]),
 | 
					 | 
				
			||||||
    DownloadModule,
 | 
					 | 
				
			||||||
  ],
 | 
					 | 
				
			||||||
  controllers: [AssetController],
 | 
					  controllers: [AssetController],
 | 
				
			||||||
  providers: [AssetService, { provide: IAssetRepository, useClass: AssetRepository }],
 | 
					  providers: [AssetService, { provide: IAssetRepository, useClass: AssetRepository }],
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
 | 
				
			|||||||
@ -13,7 +13,6 @@ import {
 | 
				
			|||||||
} from '@test';
 | 
					} from '@test';
 | 
				
			||||||
import { when } from 'jest-when';
 | 
					import { when } from 'jest-when';
 | 
				
			||||||
import { QueryFailedError, Repository } from 'typeorm';
 | 
					import { QueryFailedError, Repository } from 'typeorm';
 | 
				
			||||||
import { DownloadService } from '../../modules/download/download.service';
 | 
					 | 
				
			||||||
import { IAssetRepository } from './asset-repository';
 | 
					import { IAssetRepository } from './asset-repository';
 | 
				
			||||||
import { AssetService } from './asset.service';
 | 
					import { AssetService } from './asset.service';
 | 
				
			||||||
import { CreateAssetDto } from './dto/create-asset.dto';
 | 
					import { CreateAssetDto } from './dto/create-asset.dto';
 | 
				
			||||||
@ -124,7 +123,6 @@ describe('AssetService', () => {
 | 
				
			|||||||
  let accessMock: IAccessRepositoryMock;
 | 
					  let accessMock: IAccessRepositoryMock;
 | 
				
			||||||
  let assetRepositoryMock: jest.Mocked<IAssetRepository>;
 | 
					  let assetRepositoryMock: jest.Mocked<IAssetRepository>;
 | 
				
			||||||
  let cryptoMock: jest.Mocked<ICryptoRepository>;
 | 
					  let cryptoMock: jest.Mocked<ICryptoRepository>;
 | 
				
			||||||
  let downloadServiceMock: jest.Mocked<Partial<DownloadService>>;
 | 
					 | 
				
			||||||
  let jobMock: jest.Mocked<IJobRepository>;
 | 
					  let jobMock: jest.Mocked<IJobRepository>;
 | 
				
			||||||
  let storageMock: jest.Mocked<IStorageRepository>;
 | 
					  let storageMock: jest.Mocked<IStorageRepository>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -152,24 +150,12 @@ describe('AssetService', () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    cryptoMock = newCryptoRepositoryMock();
 | 
					    cryptoMock = newCryptoRepositoryMock();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    downloadServiceMock = {
 | 
					 | 
				
			||||||
      downloadArchive: jest.fn(),
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    accessMock = newAccessRepositoryMock();
 | 
					    accessMock = newAccessRepositoryMock();
 | 
				
			||||||
    cryptoMock = newCryptoRepositoryMock();
 | 
					    cryptoMock = newCryptoRepositoryMock();
 | 
				
			||||||
    jobMock = newJobRepositoryMock();
 | 
					    jobMock = newJobRepositoryMock();
 | 
				
			||||||
    storageMock = newStorageRepositoryMock();
 | 
					    storageMock = newStorageRepositoryMock();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    sut = new AssetService(
 | 
					    sut = new AssetService(accessMock, assetRepositoryMock, a, cryptoMock, jobMock, storageMock);
 | 
				
			||||||
      accessMock,
 | 
					 | 
				
			||||||
      assetRepositoryMock,
 | 
					 | 
				
			||||||
      a,
 | 
					 | 
				
			||||||
      cryptoMock,
 | 
					 | 
				
			||||||
      downloadServiceMock as DownloadService,
 | 
					 | 
				
			||||||
      jobMock,
 | 
					 | 
				
			||||||
      storageMock,
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    when(assetRepositoryMock.get)
 | 
					    when(assetRepositoryMock.get)
 | 
				
			||||||
      .calledWith(assetEntityStub.livePhotoStillAsset.id)
 | 
					      .calledWith(assetEntityStub.livePhotoStillAsset.id)
 | 
				
			||||||
@ -398,27 +384,6 @@ describe('AssetService', () => {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // describe('checkDownloadAccess', () => {
 | 
					 | 
				
			||||||
  //   it('should validate download access', async () => {
 | 
					 | 
				
			||||||
  //     await sut.checkDownloadAccess(authStub.adminSharedLink);
 | 
					 | 
				
			||||||
  //   });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  //   it('should not allow when user is not allowed to download', async () => {
 | 
					 | 
				
			||||||
  //     expect(() => sut.checkDownloadAccess(authStub.readonlySharedLink)).toThrow(ForbiddenException);
 | 
					 | 
				
			||||||
  //   });
 | 
					 | 
				
			||||||
  // });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  describe('downloadFile', () => {
 | 
					 | 
				
			||||||
    it('should download a single file', async () => {
 | 
					 | 
				
			||||||
      accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
 | 
					 | 
				
			||||||
      assetRepositoryMock.get.mockResolvedValue(_getAsset_1());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      await sut.downloadFile(authStub.admin, 'id_1');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      expect(storageMock.createReadStream).toHaveBeenCalledWith('fake_path/asset_1.jpeg', 'image/jpeg');
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  describe('bulkUploadCheck', () => {
 | 
					  describe('bulkUploadCheck', () => {
 | 
				
			||||||
    it('should accept hex and base64 checksums', async () => {
 | 
					    it('should accept hex and base64 checksums', async () => {
 | 
				
			||||||
      const file1 = Buffer.from('d2947b871a706081be194569951b7db246907957', 'hex');
 | 
					      const file1 = Buffer.from('d2947b871a706081be194569951b7db246907957', 'hex');
 | 
				
			||||||
 | 
				
			|||||||
@ -6,7 +6,6 @@ import {
 | 
				
			|||||||
  IAccessRepository,
 | 
					  IAccessRepository,
 | 
				
			||||||
  ICryptoRepository,
 | 
					  ICryptoRepository,
 | 
				
			||||||
  IJobRepository,
 | 
					  IJobRepository,
 | 
				
			||||||
  ImmichReadStream,
 | 
					 | 
				
			||||||
  isSupportedFileType,
 | 
					  isSupportedFileType,
 | 
				
			||||||
  IStorageRepository,
 | 
					  IStorageRepository,
 | 
				
			||||||
  JobName,
 | 
					  JobName,
 | 
				
			||||||
@ -33,7 +32,6 @@ import mime from 'mime-types';
 | 
				
			|||||||
import path from 'path';
 | 
					import path from 'path';
 | 
				
			||||||
import { QueryFailedError, Repository } from 'typeorm';
 | 
					import { QueryFailedError, Repository } from 'typeorm';
 | 
				
			||||||
import { promisify } from 'util';
 | 
					import { promisify } from 'util';
 | 
				
			||||||
import { DownloadService } from '../../modules/download/download.service';
 | 
					 | 
				
			||||||
import { IAssetRepository } from './asset-repository';
 | 
					import { IAssetRepository } from './asset-repository';
 | 
				
			||||||
import { AssetCore } from './asset.core';
 | 
					import { AssetCore } from './asset.core';
 | 
				
			||||||
import { AssetBulkUploadCheckDto } from './dto/asset-check.dto';
 | 
					import { AssetBulkUploadCheckDto } from './dto/asset-check.dto';
 | 
				
			||||||
@ -42,8 +40,6 @@ import { CheckDuplicateAssetDto } from './dto/check-duplicate-asset.dto';
 | 
				
			|||||||
import { CheckExistingAssetsDto } from './dto/check-existing-assets.dto';
 | 
					import { CheckExistingAssetsDto } from './dto/check-existing-assets.dto';
 | 
				
			||||||
import { CreateAssetDto, ImportAssetDto, UploadFile } from './dto/create-asset.dto';
 | 
					import { CreateAssetDto, ImportAssetDto, UploadFile } from './dto/create-asset.dto';
 | 
				
			||||||
import { DeleteAssetDto } from './dto/delete-asset.dto';
 | 
					import { DeleteAssetDto } from './dto/delete-asset.dto';
 | 
				
			||||||
import { DownloadFilesDto } from './dto/download-files.dto';
 | 
					 | 
				
			||||||
import { DownloadDto } from './dto/download-library.dto';
 | 
					 | 
				
			||||||
import { GetAssetByTimeBucketDto } from './dto/get-asset-by-time-bucket.dto';
 | 
					import { GetAssetByTimeBucketDto } from './dto/get-asset-by-time-bucket.dto';
 | 
				
			||||||
import { GetAssetCountByTimeBucketDto } from './dto/get-asset-count-by-time-bucket.dto';
 | 
					import { GetAssetCountByTimeBucketDto } from './dto/get-asset-count-by-time-bucket.dto';
 | 
				
			||||||
import { GetAssetThumbnailDto, GetAssetThumbnailFormatEnum } from './dto/get-asset-thumbnail.dto';
 | 
					import { GetAssetThumbnailDto, GetAssetThumbnailFormatEnum } from './dto/get-asset-thumbnail.dto';
 | 
				
			||||||
@ -86,7 +82,6 @@ export class AssetService {
 | 
				
			|||||||
    @Inject(IAssetRepository) private _assetRepository: IAssetRepository,
 | 
					    @Inject(IAssetRepository) private _assetRepository: IAssetRepository,
 | 
				
			||||||
    @InjectRepository(AssetEntity) private assetRepository: Repository<AssetEntity>,
 | 
					    @InjectRepository(AssetEntity) private assetRepository: Repository<AssetEntity>,
 | 
				
			||||||
    @Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
 | 
					    @Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
 | 
				
			||||||
    private downloadService: DownloadService,
 | 
					 | 
				
			||||||
    @Inject(IJobRepository) private jobRepository: IJobRepository,
 | 
					    @Inject(IJobRepository) private jobRepository: IJobRepository,
 | 
				
			||||||
    @Inject(IStorageRepository) private storageRepository: IStorageRepository,
 | 
					    @Inject(IStorageRepository) private storageRepository: IStorageRepository,
 | 
				
			||||||
  ) {
 | 
					  ) {
 | 
				
			||||||
@ -250,50 +245,6 @@ export class AssetService {
 | 
				
			|||||||
    return mapAsset(updatedAsset);
 | 
					    return mapAsset(updatedAsset);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public async downloadLibrary(authUser: AuthUserDto, dto: DownloadDto) {
 | 
					 | 
				
			||||||
    await this.access.requirePermission(authUser, Permission.LIBRARY_DOWNLOAD, authUser.id);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const assets = await this._assetRepository.getAllByUserId(authUser.id, dto);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return this.downloadService.downloadArchive(dto.name || `library`, assets);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  public async downloadFiles(authUser: AuthUserDto, dto: DownloadFilesDto) {
 | 
					 | 
				
			||||||
    await this.access.requirePermission(authUser, Permission.ASSET_DOWNLOAD, dto.assetIds);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const assetToDownload = [];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    for (const assetId of dto.assetIds) {
 | 
					 | 
				
			||||||
      const asset = await this._assetRepository.getById(assetId);
 | 
					 | 
				
			||||||
      assetToDownload.push(asset);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      // Get live photo asset
 | 
					 | 
				
			||||||
      if (asset.livePhotoVideoId) {
 | 
					 | 
				
			||||||
        const livePhotoAsset = await this._assetRepository.getById(asset.livePhotoVideoId);
 | 
					 | 
				
			||||||
        assetToDownload.push(livePhotoAsset);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const now = new Date().toISOString();
 | 
					 | 
				
			||||||
    return this.downloadService.downloadArchive(`immich-${now}`, assetToDownload);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  public async downloadFile(authUser: AuthUserDto, assetId: string): Promise<ImmichReadStream> {
 | 
					 | 
				
			||||||
    await this.access.requirePermission(authUser, Permission.ASSET_DOWNLOAD, assetId);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      const asset = await this._assetRepository.get(assetId);
 | 
					 | 
				
			||||||
      if (asset && asset.originalPath && asset.mimeType) {
 | 
					 | 
				
			||||||
        return this.storageRepository.createReadStream(asset.originalPath, asset.mimeType);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    } catch (e) {
 | 
					 | 
				
			||||||
      Logger.error(`Error download asset ${e}`, 'downloadFile');
 | 
					 | 
				
			||||||
      throw new InternalServerErrorException(`Failed to download asset ${e}`, 'DownloadFile');
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    throw new NotFoundException();
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  async getAssetThumbnail(
 | 
					  async getAssetThumbnail(
 | 
				
			||||||
    authUser: AuthUserDto,
 | 
					    authUser: AuthUserDto,
 | 
				
			||||||
    assetId: string,
 | 
					    assetId: string,
 | 
				
			||||||
 | 
				
			|||||||
@ -1,12 +0,0 @@
 | 
				
			|||||||
import { ApiProperty } from '@nestjs/swagger';
 | 
					 | 
				
			||||||
import { IsNotEmpty } from 'class-validator';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export class DownloadFilesDto {
 | 
					 | 
				
			||||||
  @IsNotEmpty()
 | 
					 | 
				
			||||||
  @ApiProperty({
 | 
					 | 
				
			||||||
    isArray: true,
 | 
					 | 
				
			||||||
    type: String,
 | 
					 | 
				
			||||||
    title: 'Array of asset ids to be downloaded',
 | 
					 | 
				
			||||||
  })
 | 
					 | 
				
			||||||
  assetIds!: string[];
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,14 +0,0 @@
 | 
				
			|||||||
import { Type } from 'class-transformer';
 | 
					 | 
				
			||||||
import { IsNumber, IsOptional, IsPositive, IsString } from 'class-validator';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export class DownloadDto {
 | 
					 | 
				
			||||||
  @IsOptional()
 | 
					 | 
				
			||||||
  @IsString()
 | 
					 | 
				
			||||||
  name?: string;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @IsOptional()
 | 
					 | 
				
			||||||
  @IsPositive()
 | 
					 | 
				
			||||||
  @IsNumber()
 | 
					 | 
				
			||||||
  @Type(() => Number)
 | 
					 | 
				
			||||||
  skip?: number;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,5 +1,11 @@
 | 
				
			|||||||
import { IMMICH_ACCESS_COOKIE, IMMICH_API_KEY_HEADER, IMMICH_API_KEY_NAME, SERVER_VERSION } from '@app/domain';
 | 
					import {
 | 
				
			||||||
import { INestApplication } from '@nestjs/common';
 | 
					  ImmichReadStream,
 | 
				
			||||||
 | 
					  IMMICH_ACCESS_COOKIE,
 | 
				
			||||||
 | 
					  IMMICH_API_KEY_HEADER,
 | 
				
			||||||
 | 
					  IMMICH_API_KEY_NAME,
 | 
				
			||||||
 | 
					  SERVER_VERSION,
 | 
				
			||||||
 | 
					} from '@app/domain';
 | 
				
			||||||
 | 
					import { INestApplication, StreamableFile } from '@nestjs/common';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  DocumentBuilder,
 | 
					  DocumentBuilder,
 | 
				
			||||||
  OpenAPIObject,
 | 
					  OpenAPIObject,
 | 
				
			||||||
@ -7,18 +13,12 @@ import {
 | 
				
			|||||||
  SwaggerDocumentOptions,
 | 
					  SwaggerDocumentOptions,
 | 
				
			||||||
  SwaggerModule,
 | 
					  SwaggerModule,
 | 
				
			||||||
} from '@nestjs/swagger';
 | 
					} from '@nestjs/swagger';
 | 
				
			||||||
import { Response } from 'express';
 | 
					 | 
				
			||||||
import { writeFileSync } from 'fs';
 | 
					import { writeFileSync } from 'fs';
 | 
				
			||||||
import path from 'path';
 | 
					import path from 'path';
 | 
				
			||||||
import { Metadata } from './decorators/authenticated.decorator';
 | 
					import { Metadata } from './decorators/authenticated.decorator';
 | 
				
			||||||
import { DownloadArchive } from './modules/download/download.service';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const handleDownload = (download: DownloadArchive, res: Response) => {
 | 
					export const asStreamableFile = ({ stream, type, length }: ImmichReadStream) => {
 | 
				
			||||||
  res.attachment(download.fileName);
 | 
					  return new StreamableFile(stream, { type, length });
 | 
				
			||||||
  res.setHeader('X-Immich-Content-Length-Hint', download.fileSize);
 | 
					 | 
				
			||||||
  res.setHeader('X-Immich-Archive-File-Count', download.fileCount);
 | 
					 | 
				
			||||||
  res.setHeader('X-Immich-Archive-Complete', `${download.complete}`);
 | 
					 | 
				
			||||||
  return download.stream;
 | 
					 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function sortKeys<T extends object>(obj: T): T {
 | 
					function sortKeys<T extends object>(obj: T): T {
 | 
				
			||||||
 | 
				
			|||||||
@ -1,11 +1,21 @@
 | 
				
			|||||||
import { AssetService, AuthUserDto, MapMarkerResponseDto, MemoryLaneDto } from '@app/domain';
 | 
					import {
 | 
				
			||||||
 | 
					  AssetIdsDto,
 | 
				
			||||||
 | 
					  AssetService,
 | 
				
			||||||
 | 
					  AuthUserDto,
 | 
				
			||||||
 | 
					  DownloadDto,
 | 
				
			||||||
 | 
					  DownloadResponseDto,
 | 
				
			||||||
 | 
					  MapMarkerResponseDto,
 | 
				
			||||||
 | 
					  MemoryLaneDto,
 | 
				
			||||||
 | 
					} from '@app/domain';
 | 
				
			||||||
import { MapMarkerDto } from '@app/domain/asset/dto/map-marker.dto';
 | 
					import { MapMarkerDto } from '@app/domain/asset/dto/map-marker.dto';
 | 
				
			||||||
import { MemoryLaneResponseDto } from '@app/domain/asset/response-dto/memory-lane-response.dto';
 | 
					import { MemoryLaneResponseDto } from '@app/domain/asset/response-dto/memory-lane-response.dto';
 | 
				
			||||||
import { Controller, Get, Query } from '@nestjs/common';
 | 
					import { Body, Controller, Get, HttpCode, HttpStatus, Param, Post, Query, StreamableFile } from '@nestjs/common';
 | 
				
			||||||
import { ApiTags } from '@nestjs/swagger';
 | 
					import { ApiOkResponse, ApiTags } from '@nestjs/swagger';
 | 
				
			||||||
 | 
					import { asStreamableFile } from '../app.utils';
 | 
				
			||||||
import { AuthUser } from '../decorators/auth-user.decorator';
 | 
					import { AuthUser } from '../decorators/auth-user.decorator';
 | 
				
			||||||
import { Authenticated } from '../decorators/authenticated.decorator';
 | 
					import { Authenticated, SharedLinkRoute } from '../decorators/authenticated.decorator';
 | 
				
			||||||
import { UseValidation } from '../decorators/use-validation.decorator';
 | 
					import { UseValidation } from '../decorators/use-validation.decorator';
 | 
				
			||||||
 | 
					import { UUIDParamDto } from './dto/uuid-param.dto';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ApiTags('Asset')
 | 
					@ApiTags('Asset')
 | 
				
			||||||
@Controller('asset')
 | 
					@Controller('asset')
 | 
				
			||||||
@ -23,4 +33,26 @@ export class AssetController {
 | 
				
			|||||||
  getMemoryLane(@AuthUser() authUser: AuthUserDto, @Query() dto: MemoryLaneDto): Promise<MemoryLaneResponseDto[]> {
 | 
					  getMemoryLane(@AuthUser() authUser: AuthUserDto, @Query() dto: MemoryLaneDto): Promise<MemoryLaneResponseDto[]> {
 | 
				
			||||||
    return this.service.getMemoryLane(authUser, dto);
 | 
					    return this.service.getMemoryLane(authUser, dto);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @SharedLinkRoute()
 | 
				
			||||||
 | 
					  @Get('download')
 | 
				
			||||||
 | 
					  getDownloadInfo(@AuthUser() authUser: AuthUserDto, @Query() dto: DownloadDto): Promise<DownloadResponseDto> {
 | 
				
			||||||
 | 
					    return this.service.getDownloadInfo(authUser, dto);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @SharedLinkRoute()
 | 
				
			||||||
 | 
					  @Post('download')
 | 
				
			||||||
 | 
					  @HttpCode(HttpStatus.OK)
 | 
				
			||||||
 | 
					  @ApiOkResponse({ content: { 'application/octet-stream': { schema: { type: 'string', format: 'binary' } } } })
 | 
				
			||||||
 | 
					  downloadArchive(@AuthUser() authUser: AuthUserDto, @Body() dto: AssetIdsDto): Promise<StreamableFile> {
 | 
				
			||||||
 | 
					    return this.service.downloadArchive(authUser, dto).then(asStreamableFile);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @SharedLinkRoute()
 | 
				
			||||||
 | 
					  @Post('download/:id')
 | 
				
			||||||
 | 
					  @HttpCode(HttpStatus.OK)
 | 
				
			||||||
 | 
					  @ApiOkResponse({ content: { 'application/octet-stream': { schema: { type: 'string', format: 'binary' } } } })
 | 
				
			||||||
 | 
					  downloadFile(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto) {
 | 
				
			||||||
 | 
					    return this.service.downloadFile(authUser, id).then(asStreamableFile);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,8 +0,0 @@
 | 
				
			|||||||
import { Module } from '@nestjs/common';
 | 
					 | 
				
			||||||
import { DownloadService } from './download.service';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@Module({
 | 
					 | 
				
			||||||
  providers: [DownloadService],
 | 
					 | 
				
			||||||
  exports: [DownloadService],
 | 
					 | 
				
			||||||
})
 | 
					 | 
				
			||||||
export class DownloadModule {}
 | 
					 | 
				
			||||||
@ -1,63 +0,0 @@
 | 
				
			|||||||
import { asHumanReadable, HumanReadableSize } from '@app/domain';
 | 
					 | 
				
			||||||
import { AssetEntity } from '@app/infra/entities';
 | 
					 | 
				
			||||||
import { BadRequestException, Injectable, InternalServerErrorException, Logger, StreamableFile } from '@nestjs/common';
 | 
					 | 
				
			||||||
import archiver from 'archiver';
 | 
					 | 
				
			||||||
import { extname } from 'path';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export interface DownloadArchive {
 | 
					 | 
				
			||||||
  stream: StreamableFile;
 | 
					 | 
				
			||||||
  fileName: string;
 | 
					 | 
				
			||||||
  fileSize: number;
 | 
					 | 
				
			||||||
  fileCount: number;
 | 
					 | 
				
			||||||
  complete: boolean;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@Injectable()
 | 
					 | 
				
			||||||
export class DownloadService {
 | 
					 | 
				
			||||||
  private readonly logger = new Logger(DownloadService.name);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  public async downloadArchive(name: string, assets: AssetEntity[]): Promise<DownloadArchive> {
 | 
					 | 
				
			||||||
    if (!assets || assets.length === 0) {
 | 
					 | 
				
			||||||
      throw new BadRequestException('No assets to download.');
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      const archive = archiver('zip', { store: true });
 | 
					 | 
				
			||||||
      const stream = new StreamableFile(archive);
 | 
					 | 
				
			||||||
      let totalSize = 0;
 | 
					 | 
				
			||||||
      let fileCount = 0;
 | 
					 | 
				
			||||||
      let complete = true;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      for (const { originalPath, exifInfo, originalFileName } of assets) {
 | 
					 | 
				
			||||||
        const name = `${originalFileName}${extname(originalPath)}`;
 | 
					 | 
				
			||||||
        archive.file(originalPath, { name });
 | 
					 | 
				
			||||||
        totalSize += Number(exifInfo?.fileSizeInByte || 0);
 | 
					 | 
				
			||||||
        fileCount++;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // for easier testing, can be changed before merging.
 | 
					 | 
				
			||||||
        if (totalSize > HumanReadableSize.GiB * 20) {
 | 
					 | 
				
			||||||
          complete = false;
 | 
					 | 
				
			||||||
          this.logger.log(
 | 
					 | 
				
			||||||
            `Archive size exceeded after ${fileCount} files, capping at ${totalSize} bytes (${asHumanReadable(
 | 
					 | 
				
			||||||
              totalSize,
 | 
					 | 
				
			||||||
            )})`,
 | 
					 | 
				
			||||||
          );
 | 
					 | 
				
			||||||
          break;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      archive.finalize();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      return {
 | 
					 | 
				
			||||||
        stream,
 | 
					 | 
				
			||||||
        fileName: `${name}.zip`,
 | 
					 | 
				
			||||||
        fileSize: totalSize,
 | 
					 | 
				
			||||||
        fileCount,
 | 
					 | 
				
			||||||
        complete,
 | 
					 | 
				
			||||||
      };
 | 
					 | 
				
			||||||
    } catch (error) {
 | 
					 | 
				
			||||||
      this.logger.error(`Error creating download archive ${error}`);
 | 
					 | 
				
			||||||
      throw new InternalServerErrorException(`Failed to download ${name}: ${error}`, 'DownloadArchive');
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -140,7 +140,9 @@ export class AccessRepository implements IAccessRepository {
 | 
				
			|||||||
      return this.albumRepository.exist({
 | 
					      return this.albumRepository.exist({
 | 
				
			||||||
        where: {
 | 
					        where: {
 | 
				
			||||||
          id: albumId,
 | 
					          id: albumId,
 | 
				
			||||||
          ownerId: userId,
 | 
					          sharedUsers: {
 | 
				
			||||||
 | 
					            id: userId,
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
				
			|||||||
@ -72,6 +72,32 @@ export class AssetRepository implements IAssetRepository {
 | 
				
			|||||||
    await this.repository.delete({ ownerId });
 | 
					    await this.repository.delete({ ownerId });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getByAlbumId(pagination: PaginationOptions, albumId: string): Paginated<AssetEntity> {
 | 
				
			||||||
 | 
					    return paginate(this.repository, pagination, {
 | 
				
			||||||
 | 
					      where: {
 | 
				
			||||||
 | 
					        albums: {
 | 
				
			||||||
 | 
					          id: albumId,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      relations: {
 | 
				
			||||||
 | 
					        albums: true,
 | 
				
			||||||
 | 
					        exifInfo: true,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getByUserId(pagination: PaginationOptions, userId: string): Paginated<AssetEntity> {
 | 
				
			||||||
 | 
					    return paginate(this.repository, pagination, {
 | 
				
			||||||
 | 
					      where: {
 | 
				
			||||||
 | 
					        ownerId: userId,
 | 
				
			||||||
 | 
					        isVisible: true,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      relations: {
 | 
				
			||||||
 | 
					        exifInfo: true,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  getAll(pagination: PaginationOptions, options: AssetSearchOptions = {}): Paginated<AssetEntity> {
 | 
					  getAll(pagination: PaginationOptions, options: AssetSearchOptions = {}): Paginated<AssetEntity> {
 | 
				
			||||||
    return paginate(this.repository, pagination, {
 | 
					    return paginate(this.repository, pagination, {
 | 
				
			||||||
      where: {
 | 
					      where: {
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,5 @@
 | 
				
			|||||||
import { DiskUsage, ImmichReadStream, IStorageRepository } from '@app/domain';
 | 
					import { DiskUsage, ImmichReadStream, ImmichZipStream, IStorageRepository } from '@app/domain';
 | 
				
			||||||
 | 
					import archiver from 'archiver';
 | 
				
			||||||
import { constants, createReadStream, existsSync, mkdirSync } from 'fs';
 | 
					import { constants, createReadStream, existsSync, mkdirSync } from 'fs';
 | 
				
			||||||
import fs from 'fs/promises';
 | 
					import fs from 'fs/promises';
 | 
				
			||||||
import mv from 'mv';
 | 
					import mv from 'mv';
 | 
				
			||||||
@ -8,13 +9,25 @@ import path from 'path';
 | 
				
			|||||||
const moveFile = promisify<string, string, mv.Options>(mv);
 | 
					const moveFile = promisify<string, string, mv.Options>(mv);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class FilesystemProvider implements IStorageRepository {
 | 
					export class FilesystemProvider implements IStorageRepository {
 | 
				
			||||||
  async createReadStream(filepath: string, mimeType: string): Promise<ImmichReadStream> {
 | 
					  createZipStream(): ImmichZipStream {
 | 
				
			||||||
 | 
					    const archive = archiver('zip', { store: true });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const addFile = (input: string, filename: string) => {
 | 
				
			||||||
 | 
					      archive.file(input, { name: filename });
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const finalize = () => archive.finalize();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return { stream: archive, addFile, finalize };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async createReadStream(filepath: string, mimeType?: string | null): Promise<ImmichReadStream> {
 | 
				
			||||||
    const { size } = await fs.stat(filepath);
 | 
					    const { size } = await fs.stat(filepath);
 | 
				
			||||||
    await fs.access(filepath, constants.R_OK | constants.W_OK);
 | 
					    await fs.access(filepath, constants.R_OK | constants.W_OK);
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
      stream: createReadStream(filepath),
 | 
					      stream: createReadStream(filepath),
 | 
				
			||||||
      length: size,
 | 
					      length: size,
 | 
				
			||||||
      type: mimeType,
 | 
					      type: mimeType || undefined,
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -203,14 +203,14 @@ export const fileStub = {
 | 
				
			|||||||
export const assetEntityStub = {
 | 
					export const assetEntityStub = {
 | 
				
			||||||
  noResizePath: Object.freeze<AssetEntity>({
 | 
					  noResizePath: Object.freeze<AssetEntity>({
 | 
				
			||||||
    id: 'asset-id',
 | 
					    id: 'asset-id',
 | 
				
			||||||
    originalFileName: 'asset_1.jpeg',
 | 
					    originalFileName: 'IMG_123',
 | 
				
			||||||
    deviceAssetId: 'device-asset-id',
 | 
					    deviceAssetId: 'device-asset-id',
 | 
				
			||||||
    fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'),
 | 
					    fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'),
 | 
				
			||||||
    fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'),
 | 
					    fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'),
 | 
				
			||||||
    owner: userEntityStub.user1,
 | 
					    owner: userEntityStub.user1,
 | 
				
			||||||
    ownerId: 'user-id',
 | 
					    ownerId: 'user-id',
 | 
				
			||||||
    deviceId: 'device-id',
 | 
					    deviceId: 'device-id',
 | 
				
			||||||
    originalPath: 'upload/upload/path.ext',
 | 
					    originalPath: 'upload/library/IMG_123.jpg',
 | 
				
			||||||
    resizePath: null,
 | 
					    resizePath: null,
 | 
				
			||||||
    checksum: Buffer.from('file hash', 'utf8'),
 | 
					    checksum: Buffer.from('file hash', 'utf8'),
 | 
				
			||||||
    type: AssetType.IMAGE,
 | 
					    type: AssetType.IMAGE,
 | 
				
			||||||
@ -240,7 +240,7 @@ export const assetEntityStub = {
 | 
				
			|||||||
    owner: userEntityStub.user1,
 | 
					    owner: userEntityStub.user1,
 | 
				
			||||||
    ownerId: 'user-id',
 | 
					    ownerId: 'user-id',
 | 
				
			||||||
    deviceId: 'device-id',
 | 
					    deviceId: 'device-id',
 | 
				
			||||||
    originalPath: '/original/path.ext',
 | 
					    originalPath: 'upload/library/IMG_456.jpg',
 | 
				
			||||||
    resizePath: '/uploads/user-id/thumbs/path.ext',
 | 
					    resizePath: '/uploads/user-id/thumbs/path.ext',
 | 
				
			||||||
    checksum: Buffer.from('file hash', 'utf8'),
 | 
					    checksum: Buffer.from('file hash', 'utf8'),
 | 
				
			||||||
    type: AssetType.IMAGE,
 | 
					    type: AssetType.IMAGE,
 | 
				
			||||||
@ -258,10 +258,13 @@ export const assetEntityStub = {
 | 
				
			|||||||
    livePhotoVideoId: null,
 | 
					    livePhotoVideoId: null,
 | 
				
			||||||
    tags: [],
 | 
					    tags: [],
 | 
				
			||||||
    sharedLinks: [],
 | 
					    sharedLinks: [],
 | 
				
			||||||
    originalFileName: 'asset-id.ext',
 | 
					    originalFileName: 'IMG_456',
 | 
				
			||||||
    faces: [],
 | 
					    faces: [],
 | 
				
			||||||
    sidecarPath: null,
 | 
					    sidecarPath: null,
 | 
				
			||||||
    isReadOnly: false,
 | 
					    isReadOnly: false,
 | 
				
			||||||
 | 
					    exifInfo: {
 | 
				
			||||||
 | 
					      fileSizeInByte: 123_000,
 | 
				
			||||||
 | 
					    } as ExifEntity,
 | 
				
			||||||
  }),
 | 
					  }),
 | 
				
			||||||
  noThumbhash: Object.freeze<AssetEntity>({
 | 
					  noThumbhash: Object.freeze<AssetEntity>({
 | 
				
			||||||
    id: 'asset-id',
 | 
					    id: 'asset-id',
 | 
				
			||||||
@ -324,6 +327,9 @@ export const assetEntityStub = {
 | 
				
			|||||||
    originalFileName: 'asset-id.ext',
 | 
					    originalFileName: 'asset-id.ext',
 | 
				
			||||||
    faces: [],
 | 
					    faces: [],
 | 
				
			||||||
    sidecarPath: null,
 | 
					    sidecarPath: null,
 | 
				
			||||||
 | 
					    exifInfo: {
 | 
				
			||||||
 | 
					      fileSizeInByte: 5_000,
 | 
				
			||||||
 | 
					    } as ExifEntity,
 | 
				
			||||||
  }),
 | 
					  }),
 | 
				
			||||||
  video: Object.freeze<AssetEntity>({
 | 
					  video: Object.freeze<AssetEntity>({
 | 
				
			||||||
    id: 'asset-id',
 | 
					    id: 'asset-id',
 | 
				
			||||||
@ -355,6 +361,9 @@ export const assetEntityStub = {
 | 
				
			|||||||
    sharedLinks: [],
 | 
					    sharedLinks: [],
 | 
				
			||||||
    faces: [],
 | 
					    faces: [],
 | 
				
			||||||
    sidecarPath: null,
 | 
					    sidecarPath: null,
 | 
				
			||||||
 | 
					    exifInfo: {
 | 
				
			||||||
 | 
					      fileSizeInByte: 100_000,
 | 
				
			||||||
 | 
					    } as ExifEntity,
 | 
				
			||||||
  }),
 | 
					  }),
 | 
				
			||||||
  livePhotoMotionAsset: Object.freeze({
 | 
					  livePhotoMotionAsset: Object.freeze({
 | 
				
			||||||
    id: 'live-photo-motion-asset',
 | 
					    id: 'live-photo-motion-asset',
 | 
				
			||||||
@ -364,6 +373,9 @@ export const assetEntityStub = {
 | 
				
			|||||||
    isVisible: false,
 | 
					    isVisible: false,
 | 
				
			||||||
    fileModifiedAt: new Date('2022-06-19T23:41:36.910Z'),
 | 
					    fileModifiedAt: new Date('2022-06-19T23:41:36.910Z'),
 | 
				
			||||||
    fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'),
 | 
					    fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'),
 | 
				
			||||||
 | 
					    exifInfo: {
 | 
				
			||||||
 | 
					      fileSizeInByte: 100_000,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
  } as AssetEntity),
 | 
					  } as AssetEntity),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  livePhotoStillAsset: Object.freeze({
 | 
					  livePhotoStillAsset: Object.freeze({
 | 
				
			||||||
@ -375,6 +387,9 @@ export const assetEntityStub = {
 | 
				
			|||||||
    isVisible: true,
 | 
					    isVisible: true,
 | 
				
			||||||
    fileModifiedAt: new Date('2022-06-19T23:41:36.910Z'),
 | 
					    fileModifiedAt: new Date('2022-06-19T23:41:36.910Z'),
 | 
				
			||||||
    fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'),
 | 
					    fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'),
 | 
				
			||||||
 | 
					    exifInfo: {
 | 
				
			||||||
 | 
					      fileSizeInByte: 25_000,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
  } as AssetEntity),
 | 
					  } as AssetEntity),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  withLocation: Object.freeze<AssetEntity>({
 | 
					  withLocation: Object.freeze<AssetEntity>({
 | 
				
			||||||
@ -410,6 +425,7 @@ export const assetEntityStub = {
 | 
				
			|||||||
    exifInfo: {
 | 
					    exifInfo: {
 | 
				
			||||||
      latitude: 100,
 | 
					      latitude: 100,
 | 
				
			||||||
      longitude: 100,
 | 
					      longitude: 100,
 | 
				
			||||||
 | 
					      fileSizeInByte: 23_456,
 | 
				
			||||||
    } as ExifEntity,
 | 
					    } as ExifEntity,
 | 
				
			||||||
  }),
 | 
					  }),
 | 
				
			||||||
  sidecar: Object.freeze<AssetEntity>({
 | 
					  sidecar: Object.freeze<AssetEntity>({
 | 
				
			||||||
 | 
				
			|||||||
@ -4,6 +4,8 @@ export const newAssetRepositoryMock = (): jest.Mocked<IAssetRepository> => {
 | 
				
			|||||||
  return {
 | 
					  return {
 | 
				
			||||||
    getByDate: jest.fn(),
 | 
					    getByDate: jest.fn(),
 | 
				
			||||||
    getByIds: jest.fn().mockResolvedValue([]),
 | 
					    getByIds: jest.fn().mockResolvedValue([]),
 | 
				
			||||||
 | 
					    getByAlbumId: jest.fn(),
 | 
				
			||||||
 | 
					    getByUserId: jest.fn(),
 | 
				
			||||||
    getWithout: jest.fn(),
 | 
					    getWithout: jest.fn(),
 | 
				
			||||||
    getWith: jest.fn(),
 | 
					    getWith: jest.fn(),
 | 
				
			||||||
    getFirstAssetForAlbumId: jest.fn(),
 | 
					    getFirstAssetForAlbumId: jest.fn(),
 | 
				
			||||||
 | 
				
			|||||||
@ -2,6 +2,7 @@ import { IStorageRepository } from '@app/domain';
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export const newStorageRepositoryMock = (): jest.Mocked<IStorageRepository> => {
 | 
					export const newStorageRepositoryMock = (): jest.Mocked<IStorageRepository> => {
 | 
				
			||||||
  return {
 | 
					  return {
 | 
				
			||||||
 | 
					    createZipStream: jest.fn(),
 | 
				
			||||||
    createReadStream: jest.fn(),
 | 
					    createReadStream: jest.fn(),
 | 
				
			||||||
    unlink: jest.fn(),
 | 
					    unlink: jest.fn(),
 | 
				
			||||||
    unlinkDir: jest.fn().mockResolvedValue(true),
 | 
					    unlinkDir: jest.fn().mockResolvedValue(true),
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										551
									
								
								web/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										551
									
								
								web/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							@ -1111,16 +1111,41 @@ export type DeleteAssetStatus = typeof DeleteAssetStatus[keyof typeof DeleteAsse
 | 
				
			|||||||
/**
 | 
					/**
 | 
				
			||||||
 * 
 | 
					 * 
 | 
				
			||||||
 * @export
 | 
					 * @export
 | 
				
			||||||
 * @interface DownloadFilesDto
 | 
					 * @interface DownloadArchiveInfo
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
export interface DownloadFilesDto {
 | 
					export interface DownloadArchiveInfo {
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @type {number}
 | 
				
			||||||
 | 
					     * @memberof DownloadArchiveInfo
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    'size': number;
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * 
 | 
					     * 
 | 
				
			||||||
     * @type {Array<string>}
 | 
					     * @type {Array<string>}
 | 
				
			||||||
     * @memberof DownloadFilesDto
 | 
					     * @memberof DownloadArchiveInfo
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    'assetIds': Array<string>;
 | 
					    'assetIds': Array<string>;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * 
 | 
				
			||||||
 | 
					 * @export
 | 
				
			||||||
 | 
					 * @interface DownloadResponseDto
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export interface DownloadResponseDto {
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @type {number}
 | 
				
			||||||
 | 
					     * @memberof DownloadResponseDto
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    'totalSize': number;
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @type {Array<DownloadArchiveInfo>}
 | 
				
			||||||
 | 
					     * @memberof DownloadResponseDto
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    'archives': Array<DownloadArchiveInfo>;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * 
 | 
					 * 
 | 
				
			||||||
 * @export
 | 
					 * @export
 | 
				
			||||||
@ -3645,63 +3670,6 @@ export const AlbumApiAxiosParamCreator = function (configuration?: Configuration
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
            setSearchParams(localVarUrlObj, localVarQueryParameter);
 | 
					 | 
				
			||||||
            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
 | 
					 | 
				
			||||||
            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            return {
 | 
					 | 
				
			||||||
                url: toPathString(localVarUrlObj),
 | 
					 | 
				
			||||||
                options: localVarRequestOptions,
 | 
					 | 
				
			||||||
            };
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        /**
 | 
					 | 
				
			||||||
         * 
 | 
					 | 
				
			||||||
         * @param {string} id 
 | 
					 | 
				
			||||||
         * @param {string} [name] 
 | 
					 | 
				
			||||||
         * @param {number} [skip] 
 | 
					 | 
				
			||||||
         * @param {string} [key] 
 | 
					 | 
				
			||||||
         * @param {*} [options] Override http request option.
 | 
					 | 
				
			||||||
         * @throws {RequiredError}
 | 
					 | 
				
			||||||
         */
 | 
					 | 
				
			||||||
        downloadArchive: async (id: string, name?: string, skip?: number, key?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
 | 
					 | 
				
			||||||
            // verify required parameter 'id' is not null or undefined
 | 
					 | 
				
			||||||
            assertParamExists('downloadArchive', 'id', id)
 | 
					 | 
				
			||||||
            const localVarPath = `/album/{id}/download`
 | 
					 | 
				
			||||||
                .replace(`{${"id"}}`, encodeURIComponent(String(id)));
 | 
					 | 
				
			||||||
            // 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: 'GET', ...baseOptions, ...options};
 | 
					 | 
				
			||||||
            const localVarHeaderParameter = {} as any;
 | 
					 | 
				
			||||||
            const localVarQueryParameter = {} as any;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            // authentication cookie required
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            // authentication api_key required
 | 
					 | 
				
			||||||
            await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            // authentication bearer required
 | 
					 | 
				
			||||||
            // http bearer authentication required
 | 
					 | 
				
			||||||
            await setBearerAuthToObject(localVarHeaderParameter, configuration)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (name !== undefined) {
 | 
					 | 
				
			||||||
                localVarQueryParameter['name'] = name;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (skip !== undefined) {
 | 
					 | 
				
			||||||
                localVarQueryParameter['skip'] = skip;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (key !== undefined) {
 | 
					 | 
				
			||||||
                localVarQueryParameter['key'] = key;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
            setSearchParams(localVarUrlObj, localVarQueryParameter);
 | 
					            setSearchParams(localVarUrlObj, localVarQueryParameter);
 | 
				
			||||||
            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
 | 
					            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
 | 
				
			||||||
            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
 | 
					            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
 | 
				
			||||||
@ -4039,19 +4007,6 @@ export const AlbumApiFp = function(configuration?: Configuration) {
 | 
				
			|||||||
            const localVarAxiosArgs = await localVarAxiosParamCreator.deleteAlbum(id, options);
 | 
					            const localVarAxiosArgs = await localVarAxiosParamCreator.deleteAlbum(id, options);
 | 
				
			||||||
            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
 | 
					            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        /**
 | 
					 | 
				
			||||||
         * 
 | 
					 | 
				
			||||||
         * @param {string} id 
 | 
					 | 
				
			||||||
         * @param {string} [name] 
 | 
					 | 
				
			||||||
         * @param {number} [skip] 
 | 
					 | 
				
			||||||
         * @param {string} [key] 
 | 
					 | 
				
			||||||
         * @param {*} [options] Override http request option.
 | 
					 | 
				
			||||||
         * @throws {RequiredError}
 | 
					 | 
				
			||||||
         */
 | 
					 | 
				
			||||||
        async downloadArchive(id: string, name?: string, skip?: number, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<File>> {
 | 
					 | 
				
			||||||
            const localVarAxiosArgs = await localVarAxiosParamCreator.downloadArchive(id, name, skip, key, options);
 | 
					 | 
				
			||||||
            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        /**
 | 
					        /**
 | 
				
			||||||
         * 
 | 
					         * 
 | 
				
			||||||
         * @param {*} [options] Override http request option.
 | 
					         * @param {*} [options] Override http request option.
 | 
				
			||||||
@ -4165,18 +4120,6 @@ export const AlbumApiFactory = function (configuration?: Configuration, basePath
 | 
				
			|||||||
        deleteAlbum(id: string, options?: any): AxiosPromise<void> {
 | 
					        deleteAlbum(id: string, options?: any): AxiosPromise<void> {
 | 
				
			||||||
            return localVarFp.deleteAlbum(id, options).then((request) => request(axios, basePath));
 | 
					            return localVarFp.deleteAlbum(id, options).then((request) => request(axios, basePath));
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        /**
 | 
					 | 
				
			||||||
         * 
 | 
					 | 
				
			||||||
         * @param {string} id 
 | 
					 | 
				
			||||||
         * @param {string} [name] 
 | 
					 | 
				
			||||||
         * @param {number} [skip] 
 | 
					 | 
				
			||||||
         * @param {string} [key] 
 | 
					 | 
				
			||||||
         * @param {*} [options] Override http request option.
 | 
					 | 
				
			||||||
         * @throws {RequiredError}
 | 
					 | 
				
			||||||
         */
 | 
					 | 
				
			||||||
        downloadArchive(id: string, name?: string, skip?: number, key?: string, options?: any): AxiosPromise<File> {
 | 
					 | 
				
			||||||
            return localVarFp.downloadArchive(id, name, skip, key, options).then((request) => request(axios, basePath));
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        /**
 | 
					        /**
 | 
				
			||||||
         * 
 | 
					         * 
 | 
				
			||||||
         * @param {*} [options] Override http request option.
 | 
					         * @param {*} [options] Override http request option.
 | 
				
			||||||
@ -4315,41 +4258,6 @@ export interface AlbumApiDeleteAlbumRequest {
 | 
				
			|||||||
    readonly id: string
 | 
					    readonly id: string
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * Request parameters for downloadArchive operation in AlbumApi.
 | 
					 | 
				
			||||||
 * @export
 | 
					 | 
				
			||||||
 * @interface AlbumApiDownloadArchiveRequest
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
export interface AlbumApiDownloadArchiveRequest {
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * 
 | 
					 | 
				
			||||||
     * @type {string}
 | 
					 | 
				
			||||||
     * @memberof AlbumApiDownloadArchive
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    readonly id: string
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * 
 | 
					 | 
				
			||||||
     * @type {string}
 | 
					 | 
				
			||||||
     * @memberof AlbumApiDownloadArchive
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    readonly name?: string
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * 
 | 
					 | 
				
			||||||
     * @type {number}
 | 
					 | 
				
			||||||
     * @memberof AlbumApiDownloadArchive
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    readonly skip?: number
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * 
 | 
					 | 
				
			||||||
     * @type {string}
 | 
					 | 
				
			||||||
     * @memberof AlbumApiDownloadArchive
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    readonly key?: string
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Request parameters for getAlbumInfo operation in AlbumApi.
 | 
					 * Request parameters for getAlbumInfo operation in AlbumApi.
 | 
				
			||||||
 * @export
 | 
					 * @export
 | 
				
			||||||
@ -4506,17 +4414,6 @@ export class AlbumApi extends BaseAPI {
 | 
				
			|||||||
        return AlbumApiFp(this.configuration).deleteAlbum(requestParameters.id, options).then((request) => request(this.axios, this.basePath));
 | 
					        return AlbumApiFp(this.configuration).deleteAlbum(requestParameters.id, options).then((request) => request(this.axios, this.basePath));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * 
 | 
					 | 
				
			||||||
     * @param {AlbumApiDownloadArchiveRequest} requestParameters Request parameters.
 | 
					 | 
				
			||||||
     * @param {*} [options] Override http request option.
 | 
					 | 
				
			||||||
     * @throws {RequiredError}
 | 
					 | 
				
			||||||
     * @memberof AlbumApi
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public downloadArchive(requestParameters: AlbumApiDownloadArchiveRequest, options?: AxiosRequestConfig) {
 | 
					 | 
				
			||||||
        return AlbumApiFp(this.configuration).downloadArchive(requestParameters.id, requestParameters.name, requestParameters.skip, requestParameters.key, options).then((request) => request(this.axios, this.basePath));
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * 
 | 
					     * 
 | 
				
			||||||
     * @param {*} [options] Override http request option.
 | 
					     * @param {*} [options] Override http request option.
 | 
				
			||||||
@ -4773,62 +4670,15 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
 | 
				
			|||||||
        },
 | 
					        },
 | 
				
			||||||
        /**
 | 
					        /**
 | 
				
			||||||
         * 
 | 
					         * 
 | 
				
			||||||
         * @param {string} id 
 | 
					         * @param {AssetIdsDto} assetIdsDto 
 | 
				
			||||||
         * @param {string} [key] 
 | 
					         * @param {string} [key] 
 | 
				
			||||||
         * @param {*} [options] Override http request option.
 | 
					         * @param {*} [options] Override http request option.
 | 
				
			||||||
         * @throws {RequiredError}
 | 
					         * @throws {RequiredError}
 | 
				
			||||||
         */
 | 
					         */
 | 
				
			||||||
        downloadFile: async (id: string, key?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
 | 
					        downloadArchive: async (assetIdsDto: AssetIdsDto, key?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
 | 
				
			||||||
            // verify required parameter 'id' is not null or undefined
 | 
					            // verify required parameter 'assetIdsDto' is not null or undefined
 | 
				
			||||||
            assertParamExists('downloadFile', 'id', id)
 | 
					            assertParamExists('downloadArchive', 'assetIdsDto', assetIdsDto)
 | 
				
			||||||
            const localVarPath = `/asset/download/{id}`
 | 
					            const localVarPath = `/asset/download`;
 | 
				
			||||||
                .replace(`{${"id"}}`, encodeURIComponent(String(id)));
 | 
					 | 
				
			||||||
            // 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: 'GET', ...baseOptions, ...options};
 | 
					 | 
				
			||||||
            const localVarHeaderParameter = {} as any;
 | 
					 | 
				
			||||||
            const localVarQueryParameter = {} as any;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            // authentication cookie required
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            // authentication api_key required
 | 
					 | 
				
			||||||
            await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            // authentication bearer required
 | 
					 | 
				
			||||||
            // http bearer authentication required
 | 
					 | 
				
			||||||
            await setBearerAuthToObject(localVarHeaderParameter, configuration)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (key !== undefined) {
 | 
					 | 
				
			||||||
                localVarQueryParameter['key'] = key;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
            setSearchParams(localVarUrlObj, localVarQueryParameter);
 | 
					 | 
				
			||||||
            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
 | 
					 | 
				
			||||||
            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            return {
 | 
					 | 
				
			||||||
                url: toPathString(localVarUrlObj),
 | 
					 | 
				
			||||||
                options: localVarRequestOptions,
 | 
					 | 
				
			||||||
            };
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        /**
 | 
					 | 
				
			||||||
         * 
 | 
					 | 
				
			||||||
         * @param {DownloadFilesDto} downloadFilesDto 
 | 
					 | 
				
			||||||
         * @param {string} [key] 
 | 
					 | 
				
			||||||
         * @param {*} [options] Override http request option.
 | 
					 | 
				
			||||||
         * @throws {RequiredError}
 | 
					 | 
				
			||||||
         */
 | 
					 | 
				
			||||||
        downloadFiles: async (downloadFilesDto: DownloadFilesDto, key?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
 | 
					 | 
				
			||||||
            // verify required parameter 'downloadFilesDto' is not null or undefined
 | 
					 | 
				
			||||||
            assertParamExists('downloadFiles', 'downloadFilesDto', downloadFilesDto)
 | 
					 | 
				
			||||||
            const localVarPath = `/asset/download-files`;
 | 
					 | 
				
			||||||
            // use dummy base URL string because the URL constructor only accepts absolute URLs.
 | 
					            // use dummy base URL string because the URL constructor only accepts absolute URLs.
 | 
				
			||||||
            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
 | 
					            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
 | 
				
			||||||
            let baseOptions;
 | 
					            let baseOptions;
 | 
				
			||||||
@ -4860,7 +4710,7 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
 | 
				
			|||||||
            setSearchParams(localVarUrlObj, localVarQueryParameter);
 | 
					            setSearchParams(localVarUrlObj, localVarQueryParameter);
 | 
				
			||||||
            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
 | 
					            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
 | 
				
			||||||
            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
 | 
					            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
 | 
				
			||||||
            localVarRequestOptions.data = serializeDataIfNeeded(downloadFilesDto, localVarRequestOptions, configuration)
 | 
					            localVarRequestOptions.data = serializeDataIfNeeded(assetIdsDto, localVarRequestOptions, configuration)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return {
 | 
					            return {
 | 
				
			||||||
                url: toPathString(localVarUrlObj),
 | 
					                url: toPathString(localVarUrlObj),
 | 
				
			||||||
@ -4868,15 +4718,17 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
 | 
				
			|||||||
            };
 | 
					            };
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        /**
 | 
					        /**
 | 
				
			||||||
         * Current this is not used in any UI element
 | 
					         * 
 | 
				
			||||||
         * @param {string} [name] 
 | 
					         * @param {string} id 
 | 
				
			||||||
         * @param {number} [skip] 
 | 
					 | 
				
			||||||
         * @param {string} [key] 
 | 
					         * @param {string} [key] 
 | 
				
			||||||
         * @param {*} [options] Override http request option.
 | 
					         * @param {*} [options] Override http request option.
 | 
				
			||||||
         * @throws {RequiredError}
 | 
					         * @throws {RequiredError}
 | 
				
			||||||
         */
 | 
					         */
 | 
				
			||||||
        downloadLibrary: async (name?: string, skip?: number, key?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
 | 
					        downloadFile: async (id: string, key?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
 | 
				
			||||||
            const localVarPath = `/asset/download-library`;
 | 
					            // verify required parameter 'id' is not null or undefined
 | 
				
			||||||
 | 
					            assertParamExists('downloadFile', 'id', id)
 | 
				
			||||||
 | 
					            const localVarPath = `/asset/download/{id}`
 | 
				
			||||||
 | 
					                .replace(`{${"id"}}`, encodeURIComponent(String(id)));
 | 
				
			||||||
            // use dummy base URL string because the URL constructor only accepts absolute URLs.
 | 
					            // use dummy base URL string because the URL constructor only accepts absolute URLs.
 | 
				
			||||||
            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
 | 
					            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
 | 
				
			||||||
            let baseOptions;
 | 
					            let baseOptions;
 | 
				
			||||||
@ -4884,7 +4736,7 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
 | 
				
			|||||||
                baseOptions = configuration.baseOptions;
 | 
					                baseOptions = configuration.baseOptions;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
 | 
					            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
 | 
				
			||||||
            const localVarHeaderParameter = {} as any;
 | 
					            const localVarHeaderParameter = {} as any;
 | 
				
			||||||
            const localVarQueryParameter = {} as any;
 | 
					            const localVarQueryParameter = {} as any;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -4897,14 +4749,6 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
 | 
				
			|||||||
            // http bearer authentication required
 | 
					            // http bearer authentication required
 | 
				
			||||||
            await setBearerAuthToObject(localVarHeaderParameter, configuration)
 | 
					            await setBearerAuthToObject(localVarHeaderParameter, configuration)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (name !== undefined) {
 | 
					 | 
				
			||||||
                localVarQueryParameter['name'] = name;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (skip !== undefined) {
 | 
					 | 
				
			||||||
                localVarQueryParameter['skip'] = skip;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (key !== undefined) {
 | 
					            if (key !== undefined) {
 | 
				
			||||||
                localVarQueryParameter['key'] = key;
 | 
					                localVarQueryParameter['key'] = key;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
@ -5356,6 +5200,69 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
 | 
					            setSearchParams(localVarUrlObj, localVarQueryParameter);
 | 
				
			||||||
 | 
					            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
 | 
				
			||||||
 | 
					            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return {
 | 
				
			||||||
 | 
					                url: toPathString(localVarUrlObj),
 | 
				
			||||||
 | 
					                options: localVarRequestOptions,
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        /**
 | 
				
			||||||
 | 
					         * 
 | 
				
			||||||
 | 
					         * @param {Array<string>} [assetIds] 
 | 
				
			||||||
 | 
					         * @param {string} [albumId] 
 | 
				
			||||||
 | 
					         * @param {string} [userId] 
 | 
				
			||||||
 | 
					         * @param {number} [archiveSize] 
 | 
				
			||||||
 | 
					         * @param {string} [key] 
 | 
				
			||||||
 | 
					         * @param {*} [options] Override http request option.
 | 
				
			||||||
 | 
					         * @throws {RequiredError}
 | 
				
			||||||
 | 
					         */
 | 
				
			||||||
 | 
					        getDownloadInfo: async (assetIds?: Array<string>, albumId?: string, userId?: string, archiveSize?: number, key?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
 | 
				
			||||||
 | 
					            const localVarPath = `/asset/download`;
 | 
				
			||||||
 | 
					            // 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: 'GET', ...baseOptions, ...options};
 | 
				
			||||||
 | 
					            const localVarHeaderParameter = {} as any;
 | 
				
			||||||
 | 
					            const localVarQueryParameter = {} as any;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // authentication cookie required
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // authentication api_key required
 | 
				
			||||||
 | 
					            await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // authentication bearer required
 | 
				
			||||||
 | 
					            // http bearer authentication required
 | 
				
			||||||
 | 
					            await setBearerAuthToObject(localVarHeaderParameter, configuration)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (assetIds) {
 | 
				
			||||||
 | 
					                localVarQueryParameter['assetIds'] = assetIds;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (albumId !== undefined) {
 | 
				
			||||||
 | 
					                localVarQueryParameter['albumId'] = albumId;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (userId !== undefined) {
 | 
				
			||||||
 | 
					                localVarQueryParameter['userId'] = userId;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (archiveSize !== undefined) {
 | 
				
			||||||
 | 
					                localVarQueryParameter['archiveSize'] = archiveSize;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (key !== undefined) {
 | 
				
			||||||
 | 
					                localVarQueryParameter['key'] = key;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
            setSearchParams(localVarUrlObj, localVarQueryParameter);
 | 
					            setSearchParams(localVarUrlObj, localVarQueryParameter);
 | 
				
			||||||
            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
 | 
					            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
 | 
				
			||||||
            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
 | 
					            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
 | 
				
			||||||
@ -5888,6 +5795,17 @@ export const AssetApiFp = function(configuration?: Configuration) {
 | 
				
			|||||||
            const localVarAxiosArgs = await localVarAxiosParamCreator.deleteAsset(deleteAssetDto, options);
 | 
					            const localVarAxiosArgs = await localVarAxiosParamCreator.deleteAsset(deleteAssetDto, options);
 | 
				
			||||||
            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
 | 
					            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					        /**
 | 
				
			||||||
 | 
					         * 
 | 
				
			||||||
 | 
					         * @param {AssetIdsDto} assetIdsDto 
 | 
				
			||||||
 | 
					         * @param {string} [key] 
 | 
				
			||||||
 | 
					         * @param {*} [options] Override http request option.
 | 
				
			||||||
 | 
					         * @throws {RequiredError}
 | 
				
			||||||
 | 
					         */
 | 
				
			||||||
 | 
					        async downloadArchive(assetIdsDto: AssetIdsDto, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<File>> {
 | 
				
			||||||
 | 
					            const localVarAxiosArgs = await localVarAxiosParamCreator.downloadArchive(assetIdsDto, key, options);
 | 
				
			||||||
 | 
					            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
        /**
 | 
					        /**
 | 
				
			||||||
         * 
 | 
					         * 
 | 
				
			||||||
         * @param {string} id 
 | 
					         * @param {string} id 
 | 
				
			||||||
@ -5899,29 +5817,6 @@ export const AssetApiFp = function(configuration?: Configuration) {
 | 
				
			|||||||
            const localVarAxiosArgs = await localVarAxiosParamCreator.downloadFile(id, key, options);
 | 
					            const localVarAxiosArgs = await localVarAxiosParamCreator.downloadFile(id, key, options);
 | 
				
			||||||
            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
 | 
					            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        /**
 | 
					 | 
				
			||||||
         * 
 | 
					 | 
				
			||||||
         * @param {DownloadFilesDto} downloadFilesDto 
 | 
					 | 
				
			||||||
         * @param {string} [key] 
 | 
					 | 
				
			||||||
         * @param {*} [options] Override http request option.
 | 
					 | 
				
			||||||
         * @throws {RequiredError}
 | 
					 | 
				
			||||||
         */
 | 
					 | 
				
			||||||
        async downloadFiles(downloadFilesDto: DownloadFilesDto, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<File>> {
 | 
					 | 
				
			||||||
            const localVarAxiosArgs = await localVarAxiosParamCreator.downloadFiles(downloadFilesDto, key, options);
 | 
					 | 
				
			||||||
            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, 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(name?: string, skip?: number, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<File>> {
 | 
					 | 
				
			||||||
            const localVarAxiosArgs = await localVarAxiosParamCreator.downloadLibrary(name, skip, key, options);
 | 
					 | 
				
			||||||
            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        /**
 | 
					        /**
 | 
				
			||||||
         * Get all AssetEntity belong to the user
 | 
					         * Get all AssetEntity belong to the user
 | 
				
			||||||
         * @param {string} [userId] 
 | 
					         * @param {string} [userId] 
 | 
				
			||||||
@ -6025,6 +5920,20 @@ export const AssetApiFp = function(configuration?: Configuration) {
 | 
				
			|||||||
            const localVarAxiosArgs = await localVarAxiosParamCreator.getCuratedObjects(options);
 | 
					            const localVarAxiosArgs = await localVarAxiosParamCreator.getCuratedObjects(options);
 | 
				
			||||||
            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
 | 
					            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					        /**
 | 
				
			||||||
 | 
					         * 
 | 
				
			||||||
 | 
					         * @param {Array<string>} [assetIds] 
 | 
				
			||||||
 | 
					         * @param {string} [albumId] 
 | 
				
			||||||
 | 
					         * @param {string} [userId] 
 | 
				
			||||||
 | 
					         * @param {number} [archiveSize] 
 | 
				
			||||||
 | 
					         * @param {string} [key] 
 | 
				
			||||||
 | 
					         * @param {*} [options] Override http request option.
 | 
				
			||||||
 | 
					         * @throws {RequiredError}
 | 
				
			||||||
 | 
					         */
 | 
				
			||||||
 | 
					        async getDownloadInfo(assetIds?: Array<string>, albumId?: string, userId?: string, archiveSize?: number, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<DownloadResponseDto>> {
 | 
				
			||||||
 | 
					            const localVarAxiosArgs = await localVarAxiosParamCreator.getDownloadInfo(assetIds, albumId, userId, archiveSize, key, options);
 | 
				
			||||||
 | 
					            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
        /**
 | 
					        /**
 | 
				
			||||||
         * 
 | 
					         * 
 | 
				
			||||||
         * @param {boolean} [isFavorite] 
 | 
					         * @param {boolean} [isFavorite] 
 | 
				
			||||||
@ -6172,6 +6081,16 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
 | 
				
			|||||||
        deleteAsset(deleteAssetDto: DeleteAssetDto, options?: any): AxiosPromise<Array<DeleteAssetResponseDto>> {
 | 
					        deleteAsset(deleteAssetDto: DeleteAssetDto, options?: any): AxiosPromise<Array<DeleteAssetResponseDto>> {
 | 
				
			||||||
            return localVarFp.deleteAsset(deleteAssetDto, options).then((request) => request(axios, basePath));
 | 
					            return localVarFp.deleteAsset(deleteAssetDto, options).then((request) => request(axios, basePath));
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					        /**
 | 
				
			||||||
 | 
					         * 
 | 
				
			||||||
 | 
					         * @param {AssetIdsDto} assetIdsDto 
 | 
				
			||||||
 | 
					         * @param {string} [key] 
 | 
				
			||||||
 | 
					         * @param {*} [options] Override http request option.
 | 
				
			||||||
 | 
					         * @throws {RequiredError}
 | 
				
			||||||
 | 
					         */
 | 
				
			||||||
 | 
					        downloadArchive(assetIdsDto: AssetIdsDto, key?: string, options?: any): AxiosPromise<File> {
 | 
				
			||||||
 | 
					            return localVarFp.downloadArchive(assetIdsDto, key, options).then((request) => request(axios, basePath));
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
        /**
 | 
					        /**
 | 
				
			||||||
         * 
 | 
					         * 
 | 
				
			||||||
         * @param {string} id 
 | 
					         * @param {string} id 
 | 
				
			||||||
@ -6182,27 +6101,6 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
 | 
				
			|||||||
        downloadFile(id: string, key?: string, options?: any): AxiosPromise<File> {
 | 
					        downloadFile(id: string, key?: string, options?: any): AxiosPromise<File> {
 | 
				
			||||||
            return localVarFp.downloadFile(id, key, options).then((request) => request(axios, basePath));
 | 
					            return localVarFp.downloadFile(id, key, options).then((request) => request(axios, basePath));
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        /**
 | 
					 | 
				
			||||||
         * 
 | 
					 | 
				
			||||||
         * @param {DownloadFilesDto} downloadFilesDto 
 | 
					 | 
				
			||||||
         * @param {string} [key] 
 | 
					 | 
				
			||||||
         * @param {*} [options] Override http request option.
 | 
					 | 
				
			||||||
         * @throws {RequiredError}
 | 
					 | 
				
			||||||
         */
 | 
					 | 
				
			||||||
        downloadFiles(downloadFilesDto: DownloadFilesDto, key?: string, options?: any): AxiosPromise<File> {
 | 
					 | 
				
			||||||
            return localVarFp.downloadFiles(downloadFilesDto, key, options).then((request) => request(axios, 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(name?: string, skip?: number, key?: string, options?: any): AxiosPromise<File> {
 | 
					 | 
				
			||||||
            return localVarFp.downloadLibrary(name, skip, key, options).then((request) => request(axios, basePath));
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        /**
 | 
					        /**
 | 
				
			||||||
         * Get all AssetEntity belong to the user
 | 
					         * Get all AssetEntity belong to the user
 | 
				
			||||||
         * @param {string} [userId] 
 | 
					         * @param {string} [userId] 
 | 
				
			||||||
@ -6296,6 +6194,19 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
 | 
				
			|||||||
        getCuratedObjects(options?: any): AxiosPromise<Array<CuratedObjectsResponseDto>> {
 | 
					        getCuratedObjects(options?: any): AxiosPromise<Array<CuratedObjectsResponseDto>> {
 | 
				
			||||||
            return localVarFp.getCuratedObjects(options).then((request) => request(axios, basePath));
 | 
					            return localVarFp.getCuratedObjects(options).then((request) => request(axios, basePath));
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					        /**
 | 
				
			||||||
 | 
					         * 
 | 
				
			||||||
 | 
					         * @param {Array<string>} [assetIds] 
 | 
				
			||||||
 | 
					         * @param {string} [albumId] 
 | 
				
			||||||
 | 
					         * @param {string} [userId] 
 | 
				
			||||||
 | 
					         * @param {number} [archiveSize] 
 | 
				
			||||||
 | 
					         * @param {string} [key] 
 | 
				
			||||||
 | 
					         * @param {*} [options] Override http request option.
 | 
				
			||||||
 | 
					         * @throws {RequiredError}
 | 
				
			||||||
 | 
					         */
 | 
				
			||||||
 | 
					        getDownloadInfo(assetIds?: Array<string>, albumId?: string, userId?: string, archiveSize?: number, key?: string, options?: any): AxiosPromise<DownloadResponseDto> {
 | 
				
			||||||
 | 
					            return localVarFp.getDownloadInfo(assetIds, albumId, userId, archiveSize, key, options).then((request) => request(axios, basePath));
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
        /**
 | 
					        /**
 | 
				
			||||||
         * 
 | 
					         * 
 | 
				
			||||||
         * @param {boolean} [isFavorite] 
 | 
					         * @param {boolean} [isFavorite] 
 | 
				
			||||||
@ -6454,6 +6365,27 @@ export interface AssetApiDeleteAssetRequest {
 | 
				
			|||||||
    readonly deleteAssetDto: DeleteAssetDto
 | 
					    readonly deleteAssetDto: DeleteAssetDto
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Request parameters for downloadArchive operation in AssetApi.
 | 
				
			||||||
 | 
					 * @export
 | 
				
			||||||
 | 
					 * @interface AssetApiDownloadArchiveRequest
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export interface AssetApiDownloadArchiveRequest {
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @type {AssetIdsDto}
 | 
				
			||||||
 | 
					     * @memberof AssetApiDownloadArchive
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    readonly assetIdsDto: AssetIdsDto
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @type {string}
 | 
				
			||||||
 | 
					     * @memberof AssetApiDownloadArchive
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    readonly key?: string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Request parameters for downloadFile operation in AssetApi.
 | 
					 * Request parameters for downloadFile operation in AssetApi.
 | 
				
			||||||
 * @export
 | 
					 * @export
 | 
				
			||||||
@ -6475,55 +6407,6 @@ export interface AssetApiDownloadFileRequest {
 | 
				
			|||||||
    readonly key?: string
 | 
					    readonly key?: string
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * Request parameters for downloadFiles operation in AssetApi.
 | 
					 | 
				
			||||||
 * @export
 | 
					 | 
				
			||||||
 * @interface AssetApiDownloadFilesRequest
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
export interface AssetApiDownloadFilesRequest {
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * 
 | 
					 | 
				
			||||||
     * @type {DownloadFilesDto}
 | 
					 | 
				
			||||||
     * @memberof AssetApiDownloadFiles
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    readonly downloadFilesDto: DownloadFilesDto
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * 
 | 
					 | 
				
			||||||
     * @type {string}
 | 
					 | 
				
			||||||
     * @memberof AssetApiDownloadFiles
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    readonly key?: string
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * Request parameters for downloadLibrary operation in AssetApi.
 | 
					 | 
				
			||||||
 * @export
 | 
					 | 
				
			||||||
 * @interface AssetApiDownloadLibraryRequest
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
export interface AssetApiDownloadLibraryRequest {
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * 
 | 
					 | 
				
			||||||
     * @type {string}
 | 
					 | 
				
			||||||
     * @memberof AssetApiDownloadLibrary
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    readonly name?: string
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * 
 | 
					 | 
				
			||||||
     * @type {number}
 | 
					 | 
				
			||||||
     * @memberof AssetApiDownloadLibrary
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    readonly skip?: number
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * 
 | 
					 | 
				
			||||||
     * @type {string}
 | 
					 | 
				
			||||||
     * @memberof AssetApiDownloadLibrary
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    readonly key?: string
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Request parameters for getAllAssets operation in AssetApi.
 | 
					 * Request parameters for getAllAssets operation in AssetApi.
 | 
				
			||||||
 * @export
 | 
					 * @export
 | 
				
			||||||
@ -6650,6 +6533,48 @@ export interface AssetApiGetAssetThumbnailRequest {
 | 
				
			|||||||
    readonly key?: string
 | 
					    readonly key?: string
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Request parameters for getDownloadInfo operation in AssetApi.
 | 
				
			||||||
 | 
					 * @export
 | 
				
			||||||
 | 
					 * @interface AssetApiGetDownloadInfoRequest
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export interface AssetApiGetDownloadInfoRequest {
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @type {Array<string>}
 | 
				
			||||||
 | 
					     * @memberof AssetApiGetDownloadInfo
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    readonly assetIds?: Array<string>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @type {string}
 | 
				
			||||||
 | 
					     * @memberof AssetApiGetDownloadInfo
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    readonly albumId?: string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @type {string}
 | 
				
			||||||
 | 
					     * @memberof AssetApiGetDownloadInfo
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    readonly userId?: string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @type {number}
 | 
				
			||||||
 | 
					     * @memberof AssetApiGetDownloadInfo
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    readonly archiveSize?: number
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @type {string}
 | 
				
			||||||
 | 
					     * @memberof AssetApiGetDownloadInfo
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    readonly key?: string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Request parameters for getMapMarkers operation in AssetApi.
 | 
					 * Request parameters for getMapMarkers operation in AssetApi.
 | 
				
			||||||
 * @export
 | 
					 * @export
 | 
				
			||||||
@ -6953,6 +6878,17 @@ export class AssetApi extends BaseAPI {
 | 
				
			|||||||
        return AssetApiFp(this.configuration).deleteAsset(requestParameters.deleteAssetDto, options).then((request) => request(this.axios, this.basePath));
 | 
					        return AssetApiFp(this.configuration).deleteAsset(requestParameters.deleteAssetDto, options).then((request) => request(this.axios, this.basePath));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @param {AssetApiDownloadArchiveRequest} requestParameters Request parameters.
 | 
				
			||||||
 | 
					     * @param {*} [options] Override http request option.
 | 
				
			||||||
 | 
					     * @throws {RequiredError}
 | 
				
			||||||
 | 
					     * @memberof AssetApi
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public downloadArchive(requestParameters: AssetApiDownloadArchiveRequest, options?: AxiosRequestConfig) {
 | 
				
			||||||
 | 
					        return AssetApiFp(this.configuration).downloadArchive(requestParameters.assetIdsDto, requestParameters.key, options).then((request) => request(this.axios, this.basePath));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * 
 | 
					     * 
 | 
				
			||||||
     * @param {AssetApiDownloadFileRequest} requestParameters Request parameters.
 | 
					     * @param {AssetApiDownloadFileRequest} requestParameters Request parameters.
 | 
				
			||||||
@ -6964,28 +6900,6 @@ export class AssetApi extends BaseAPI {
 | 
				
			|||||||
        return AssetApiFp(this.configuration).downloadFile(requestParameters.id, requestParameters.key, options).then((request) => request(this.axios, this.basePath));
 | 
					        return AssetApiFp(this.configuration).downloadFile(requestParameters.id, requestParameters.key, options).then((request) => request(this.axios, this.basePath));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * 
 | 
					 | 
				
			||||||
     * @param {AssetApiDownloadFilesRequest} requestParameters Request parameters.
 | 
					 | 
				
			||||||
     * @param {*} [options] Override http request option.
 | 
					 | 
				
			||||||
     * @throws {RequiredError}
 | 
					 | 
				
			||||||
     * @memberof AssetApi
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public downloadFiles(requestParameters: AssetApiDownloadFilesRequest, options?: AxiosRequestConfig) {
 | 
					 | 
				
			||||||
        return AssetApiFp(this.configuration).downloadFiles(requestParameters.downloadFilesDto, requestParameters.key, options).then((request) => request(this.axios, this.basePath));
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * Current this is not used in any UI element
 | 
					 | 
				
			||||||
     * @param {AssetApiDownloadLibraryRequest} requestParameters Request parameters.
 | 
					 | 
				
			||||||
     * @param {*} [options] Override http request option.
 | 
					 | 
				
			||||||
     * @throws {RequiredError}
 | 
					 | 
				
			||||||
     * @memberof AssetApi
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public downloadLibrary(requestParameters: AssetApiDownloadLibraryRequest = {}, options?: AxiosRequestConfig) {
 | 
					 | 
				
			||||||
        return AssetApiFp(this.configuration).downloadLibrary(requestParameters.name, requestParameters.skip, requestParameters.key, options).then((request) => request(this.axios, this.basePath));
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Get all AssetEntity belong to the user
 | 
					     * Get all AssetEntity belong to the user
 | 
				
			||||||
     * @param {AssetApiGetAllAssetsRequest} requestParameters Request parameters.
 | 
					     * @param {AssetApiGetAllAssetsRequest} requestParameters Request parameters.
 | 
				
			||||||
@ -7091,6 +7005,17 @@ export class AssetApi extends BaseAPI {
 | 
				
			|||||||
        return AssetApiFp(this.configuration).getCuratedObjects(options).then((request) => request(this.axios, this.basePath));
 | 
					        return AssetApiFp(this.configuration).getCuratedObjects(options).then((request) => request(this.axios, this.basePath));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @param {AssetApiGetDownloadInfoRequest} requestParameters Request parameters.
 | 
				
			||||||
 | 
					     * @param {*} [options] Override http request option.
 | 
				
			||||||
 | 
					     * @throws {RequiredError}
 | 
				
			||||||
 | 
					     * @memberof AssetApi
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public getDownloadInfo(requestParameters: AssetApiGetDownloadInfoRequest = {}, options?: AxiosRequestConfig) {
 | 
				
			||||||
 | 
					        return AssetApiFp(this.configuration).getDownloadInfo(requestParameters.assetIds, requestParameters.albumId, requestParameters.userId, requestParameters.archiveSize, requestParameters.key, options).then((request) => request(this.axios, this.basePath));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * 
 | 
					     * 
 | 
				
			||||||
     * @param {AssetApiGetMapMarkersRequest} requestParameters Request parameters.
 | 
					     * @param {AssetApiGetMapMarkersRequest} requestParameters Request parameters.
 | 
				
			||||||
 | 
				
			|||||||
@ -3,7 +3,6 @@
 | 
				
			|||||||
	import { afterNavigate, goto } from '$app/navigation';
 | 
						import { afterNavigate, goto } from '$app/navigation';
 | 
				
			||||||
	import { albumAssetSelectionStore } from '$lib/stores/album-asset-selection.store';
 | 
						import { albumAssetSelectionStore } from '$lib/stores/album-asset-selection.store';
 | 
				
			||||||
	import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store';
 | 
						import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store';
 | 
				
			||||||
	import { downloadAssets } from '$lib/stores/download';
 | 
					 | 
				
			||||||
	import { locale } from '$lib/stores/preferences.store';
 | 
						import { locale } from '$lib/stores/preferences.store';
 | 
				
			||||||
	import { fileUploadHandler, openFileUploadDialog } from '$lib/utils/file-uploader';
 | 
						import { fileUploadHandler, openFileUploadDialog } from '$lib/utils/file-uploader';
 | 
				
			||||||
	import {
 | 
						import {
 | 
				
			||||||
@ -45,6 +44,7 @@
 | 
				
			|||||||
	import ThumbnailSelection from './thumbnail-selection.svelte';
 | 
						import ThumbnailSelection from './thumbnail-selection.svelte';
 | 
				
			||||||
	import UserSelectionModal from './user-selection-modal.svelte';
 | 
						import UserSelectionModal from './user-selection-modal.svelte';
 | 
				
			||||||
	import { handleError } from '../../utils/handle-error';
 | 
						import { handleError } from '../../utils/handle-error';
 | 
				
			||||||
 | 
						import { downloadArchive } from '../../utils/asset-utils';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	export let album: AlbumResponseDto;
 | 
						export let album: AlbumResponseDto;
 | 
				
			||||||
	export let sharedLink: SharedLinkResponseDto | undefined = undefined;
 | 
						export let sharedLink: SharedLinkResponseDto | undefined = undefined;
 | 
				
			||||||
@ -242,78 +242,12 @@
 | 
				
			|||||||
	};
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const downloadAlbum = async () => {
 | 
						const downloadAlbum = async () => {
 | 
				
			||||||
		try {
 | 
							await downloadArchive(
 | 
				
			||||||
			let skip = 0;
 | 
								`${album.albumName}.zip`,
 | 
				
			||||||
			let count = 0;
 | 
								{ albumId: album.id },
 | 
				
			||||||
			let done = false;
 | 
								undefined,
 | 
				
			||||||
 | 
								sharedLink?.key
 | 
				
			||||||
			while (!done) {
 | 
					 | 
				
			||||||
				count++;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				const fileName = album.albumName + `${count === 1 ? '' : count}.zip`;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				$downloadAssets[fileName] = 0;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				let total = 0;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				const { data, status, headers } = await api.albumApi.downloadArchive(
 | 
					 | 
				
			||||||
					{ id: album.id, skip: skip || undefined, key: sharedLink?.key },
 | 
					 | 
				
			||||||
					{
 | 
					 | 
				
			||||||
						responseType: 'blob',
 | 
					 | 
				
			||||||
						onDownloadProgress: function (progressEvent) {
 | 
					 | 
				
			||||||
							const request = this as XMLHttpRequest;
 | 
					 | 
				
			||||||
							if (!total) {
 | 
					 | 
				
			||||||
								total = Number(request.getResponseHeader('X-Immich-Content-Length-Hint')) || 0;
 | 
					 | 
				
			||||||
							}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
							if (total) {
 | 
					 | 
				
			||||||
								const current = progressEvent.loaded;
 | 
					 | 
				
			||||||
								$downloadAssets[fileName] = Math.floor((current / total) * 100);
 | 
					 | 
				
			||||||
							}
 | 
					 | 
				
			||||||
						}
 | 
					 | 
				
			||||||
					}
 | 
					 | 
				
			||||||
		);
 | 
							);
 | 
				
			||||||
 | 
					 | 
				
			||||||
				const isNotComplete = headers['x-immich-archive-complete'] === 'false';
 | 
					 | 
				
			||||||
				const fileCount = Number(headers['x-immich-archive-file-count']) || 0;
 | 
					 | 
				
			||||||
				if (isNotComplete && fileCount > 0) {
 | 
					 | 
				
			||||||
					skip += fileCount;
 | 
					 | 
				
			||||||
				} else {
 | 
					 | 
				
			||||||
					done = true;
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				if (!(data instanceof Blob)) {
 | 
					 | 
				
			||||||
					return;
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				if (status === 200) {
 | 
					 | 
				
			||||||
					const fileUrl = URL.createObjectURL(data);
 | 
					 | 
				
			||||||
					const anchor = document.createElement('a');
 | 
					 | 
				
			||||||
					anchor.href = fileUrl;
 | 
					 | 
				
			||||||
					anchor.download = fileName;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
					document.body.appendChild(anchor);
 | 
					 | 
				
			||||||
					anchor.click();
 | 
					 | 
				
			||||||
					document.body.removeChild(anchor);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
					URL.revokeObjectURL(fileUrl);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
					// Remove item from download list
 | 
					 | 
				
			||||||
					setTimeout(() => {
 | 
					 | 
				
			||||||
						const copy = $downloadAssets;
 | 
					 | 
				
			||||||
						delete copy[fileName];
 | 
					 | 
				
			||||||
						$downloadAssets = copy;
 | 
					 | 
				
			||||||
					}, 2000);
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		} catch (e) {
 | 
					 | 
				
			||||||
			$downloadAssets = {};
 | 
					 | 
				
			||||||
			console.error('Error downloading file ', e);
 | 
					 | 
				
			||||||
			notificationController.show({
 | 
					 | 
				
			||||||
				type: NotificationType.Error,
 | 
					 | 
				
			||||||
				message: 'Error downloading file, check console for more details.'
 | 
					 | 
				
			||||||
			});
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const showAlbumOptionsMenu = ({ x, y }: MouseEvent) => {
 | 
						const showAlbumOptionsMenu = ({ x, y }: MouseEvent) => {
 | 
				
			||||||
@ -360,7 +294,7 @@
 | 
				
			|||||||
		>
 | 
							>
 | 
				
			||||||
			<CircleIconButton title="Select all" logo={SelectAll} on:click={handleSelectAll} />
 | 
								<CircleIconButton title="Select all" logo={SelectAll} on:click={handleSelectAll} />
 | 
				
			||||||
			{#if sharedLink?.allowDownload || !isPublicShared}
 | 
								{#if sharedLink?.allowDownload || !isPublicShared}
 | 
				
			||||||
				<DownloadAction filename={album.albumName} sharedLinkKey={sharedLink?.key} />
 | 
									<DownloadAction filename="{album.albumName}.zip" sharedLinkKey={sharedLink?.key} />
 | 
				
			||||||
			{/if}
 | 
								{/if}
 | 
				
			||||||
			{#if isOwned}
 | 
								{#if isOwned}
 | 
				
			||||||
				<RemoveFromAlbum bind:album />
 | 
									<RemoveFromAlbum bind:album />
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,5 @@
 | 
				
			|||||||
<script lang="ts">
 | 
					<script lang="ts">
 | 
				
			||||||
	import { goto } from '$app/navigation';
 | 
						import { goto } from '$app/navigation';
 | 
				
			||||||
	import { downloadAssets } from '$lib/stores/download';
 | 
					 | 
				
			||||||
	import {
 | 
						import {
 | 
				
			||||||
		AlbumResponseDto,
 | 
							AlbumResponseDto,
 | 
				
			||||||
		api,
 | 
							api,
 | 
				
			||||||
@ -25,7 +24,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	import { assetStore } from '$lib/stores/assets.store';
 | 
						import { assetStore } from '$lib/stores/assets.store';
 | 
				
			||||||
	import { isShowDetail } from '$lib/stores/preferences.store';
 | 
						import { isShowDetail } from '$lib/stores/preferences.store';
 | 
				
			||||||
	import { addAssetsToAlbum, getFilenameExtension } from '$lib/utils/asset-utils';
 | 
						import { addAssetsToAlbum, downloadFile } from '$lib/utils/asset-utils';
 | 
				
			||||||
	import { browser } from '$app/environment';
 | 
						import { browser } from '$app/environment';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	export let asset: AssetResponseDto;
 | 
						export let asset: AssetResponseDto;
 | 
				
			||||||
@ -115,75 +114,6 @@
 | 
				
			|||||||
		$isShowDetail = !$isShowDetail;
 | 
							$isShowDetail = !$isShowDetail;
 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const handleDownload = () => {
 | 
					 | 
				
			||||||
		if (asset.livePhotoVideoId) {
 | 
					 | 
				
			||||||
			downloadFile(asset.livePhotoVideoId, true, publicSharedKey);
 | 
					 | 
				
			||||||
			downloadFile(asset.id, false, publicSharedKey);
 | 
					 | 
				
			||||||
			return;
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		downloadFile(asset.id, false, publicSharedKey);
 | 
					 | 
				
			||||||
	};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	const downloadFile = async (assetId: string, isLivePhoto: boolean, key: string) => {
 | 
					 | 
				
			||||||
		try {
 | 
					 | 
				
			||||||
			const imageExtension = isLivePhoto ? 'mov' : getFilenameExtension(asset.originalPath);
 | 
					 | 
				
			||||||
			const imageFileName = asset.originalFileName + '.' + imageExtension;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			// If assets is already download -> return;
 | 
					 | 
				
			||||||
			if ($downloadAssets[imageFileName]) {
 | 
					 | 
				
			||||||
				return;
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			$downloadAssets[imageFileName] = 0;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			const { data, status } = await api.assetApi.downloadFile(
 | 
					 | 
				
			||||||
				{ id: assetId, key },
 | 
					 | 
				
			||||||
				{
 | 
					 | 
				
			||||||
					responseType: 'blob',
 | 
					 | 
				
			||||||
					onDownloadProgress: (progressEvent) => {
 | 
					 | 
				
			||||||
						if (progressEvent.lengthComputable) {
 | 
					 | 
				
			||||||
							const total = progressEvent.total;
 | 
					 | 
				
			||||||
							const current = progressEvent.loaded;
 | 
					 | 
				
			||||||
							$downloadAssets[imageFileName] = Math.floor((current / total) * 100);
 | 
					 | 
				
			||||||
						}
 | 
					 | 
				
			||||||
					}
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			if (!(data instanceof Blob)) {
 | 
					 | 
				
			||||||
				return;
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			if (status === 200) {
 | 
					 | 
				
			||||||
				const fileUrl = URL.createObjectURL(data);
 | 
					 | 
				
			||||||
				const anchor = document.createElement('a');
 | 
					 | 
				
			||||||
				anchor.href = fileUrl;
 | 
					 | 
				
			||||||
				anchor.download = imageFileName;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				document.body.appendChild(anchor);
 | 
					 | 
				
			||||||
				anchor.click();
 | 
					 | 
				
			||||||
				document.body.removeChild(anchor);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				URL.revokeObjectURL(fileUrl);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				// Remove item from download list
 | 
					 | 
				
			||||||
				setTimeout(() => {
 | 
					 | 
				
			||||||
					const copy = $downloadAssets;
 | 
					 | 
				
			||||||
					delete copy[imageFileName];
 | 
					 | 
				
			||||||
					$downloadAssets = copy;
 | 
					 | 
				
			||||||
				}, 2000);
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		} catch (e) {
 | 
					 | 
				
			||||||
			$downloadAssets = {};
 | 
					 | 
				
			||||||
			console.error('Error downloading file ', e);
 | 
					 | 
				
			||||||
			notificationController.show({
 | 
					 | 
				
			||||||
				type: NotificationType.Error,
 | 
					 | 
				
			||||||
				message: 'Error downloading file, check console for more details.'
 | 
					 | 
				
			||||||
			});
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	const deleteAsset = async () => {
 | 
						const deleteAsset = async () => {
 | 
				
			||||||
		try {
 | 
							try {
 | 
				
			||||||
			if (
 | 
								if (
 | 
				
			||||||
@ -313,7 +243,7 @@
 | 
				
			|||||||
			showDownloadButton={shouldShowDownloadButton}
 | 
								showDownloadButton={shouldShowDownloadButton}
 | 
				
			||||||
			on:goBack={closeViewer}
 | 
								on:goBack={closeViewer}
 | 
				
			||||||
			on:showDetail={showDetailInfoHandler}
 | 
								on:showDetail={showDetailInfoHandler}
 | 
				
			||||||
			on:download={handleDownload}
 | 
								on:download={() => downloadFile(asset, publicSharedKey)}
 | 
				
			||||||
			on:delete={deleteAsset}
 | 
								on:delete={deleteAsset}
 | 
				
			||||||
			on:favorite={toggleFavorite}
 | 
								on:favorite={toggleFavorite}
 | 
				
			||||||
			on:addToAlbum={() => openAlbumPicker(false)}
 | 
								on:addToAlbum={() => openAlbumPicker(false)}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,18 +1,30 @@
 | 
				
			|||||||
<script lang="ts">
 | 
					<script lang="ts">
 | 
				
			||||||
	import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
 | 
						import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
 | 
				
			||||||
	import { bulkDownload } from '$lib/utils/asset-utils';
 | 
						import { downloadArchive, downloadFile } from '$lib/utils/asset-utils';
 | 
				
			||||||
	import CloudDownloadOutline from 'svelte-material-icons/CloudDownloadOutline.svelte';
 | 
						import CloudDownloadOutline from 'svelte-material-icons/CloudDownloadOutline.svelte';
 | 
				
			||||||
	import MenuOption from '../../shared-components/context-menu/menu-option.svelte';
 | 
						import MenuOption from '../../shared-components/context-menu/menu-option.svelte';
 | 
				
			||||||
	import { getAssetControlContext } from '../asset-select-control-bar.svelte';
 | 
						import { getAssetControlContext } from '../asset-select-control-bar.svelte';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	export let filename = 'immich';
 | 
						export let filename = 'immich.zip';
 | 
				
			||||||
	export let sharedLinkKey: string | undefined = undefined;
 | 
						export let sharedLinkKey: string | undefined = undefined;
 | 
				
			||||||
	export let menuItem = false;
 | 
						export let menuItem = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const { getAssets, clearSelect } = getAssetControlContext();
 | 
						const { getAssets, clearSelect } = getAssetControlContext();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const handleDownloadFiles = async () => {
 | 
						const handleDownloadFiles = async () => {
 | 
				
			||||||
		await bulkDownload(filename, Array.from(getAssets()), clearSelect, sharedLinkKey);
 | 
							const assets = Array.from(getAssets());
 | 
				
			||||||
 | 
							if (assets.length === 1) {
 | 
				
			||||||
 | 
								await downloadFile(assets[0], sharedLinkKey);
 | 
				
			||||||
 | 
								clearSelect();
 | 
				
			||||||
 | 
								return;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							await downloadArchive(
 | 
				
			||||||
 | 
								filename,
 | 
				
			||||||
 | 
								{ assetIds: assets.map((asset) => asset.id) },
 | 
				
			||||||
 | 
								clearSelect,
 | 
				
			||||||
 | 
								sharedLinkKey
 | 
				
			||||||
 | 
							);
 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -1,7 +1,7 @@
 | 
				
			|||||||
<script lang="ts">
 | 
					<script lang="ts">
 | 
				
			||||||
	import { goto } from '$app/navigation';
 | 
						import { goto } from '$app/navigation';
 | 
				
			||||||
	import { bulkDownload } from '$lib/utils/asset-utils';
 | 
					 | 
				
			||||||
	import { fileUploadHandler, openFileUploadDialog } from '$lib/utils/file-uploader';
 | 
						import { fileUploadHandler, openFileUploadDialog } from '$lib/utils/file-uploader';
 | 
				
			||||||
 | 
						import { downloadArchive } from '$lib/utils/asset-utils';
 | 
				
			||||||
	import { api, AssetResponseDto, SharedLinkResponseDto } from '@api';
 | 
						import { api, AssetResponseDto, SharedLinkResponseDto } from '@api';
 | 
				
			||||||
	import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store';
 | 
						import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store';
 | 
				
			||||||
	import ArrowLeft from 'svelte-material-icons/ArrowLeft.svelte';
 | 
						import ArrowLeft from 'svelte-material-icons/ArrowLeft.svelte';
 | 
				
			||||||
@ -38,7 +38,12 @@
 | 
				
			|||||||
	});
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const downloadAssets = async () => {
 | 
						const downloadAssets = async () => {
 | 
				
			||||||
		await bulkDownload('immich-shared', assets, undefined, sharedLink.key);
 | 
							await downloadArchive(
 | 
				
			||||||
 | 
								`immich-shared.zip`,
 | 
				
			||||||
 | 
								{ assetIds: assets.map((asset) => asset.id) },
 | 
				
			||||||
 | 
								undefined,
 | 
				
			||||||
 | 
								sharedLink.key
 | 
				
			||||||
 | 
							);
 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const handleUploadAssets = async (files: File[] = []) => {
 | 
						const handleUploadAssets = async (files: File[] = []) => {
 | 
				
			||||||
@ -78,7 +83,7 @@
 | 
				
			|||||||
		<AssetSelectControlBar assets={selectedAssets} clearSelect={() => (selectedAssets = new Set())}>
 | 
							<AssetSelectControlBar assets={selectedAssets} clearSelect={() => (selectedAssets = new Set())}>
 | 
				
			||||||
			<CircleIconButton title="Select all" logo={SelectAll} on:click={handleSelectAll} />
 | 
								<CircleIconButton title="Select all" logo={SelectAll} on:click={handleSelectAll} />
 | 
				
			||||||
			{#if sharedLink?.allowDownload}
 | 
								{#if sharedLink?.allowDownload}
 | 
				
			||||||
				<DownloadAction filename="immich-shared" sharedLinkKey={sharedLink.key} />
 | 
									<DownloadAction filename="immich-shared.zip" sharedLinkKey={sharedLink.key} />
 | 
				
			||||||
			{/if}
 | 
								{/if}
 | 
				
			||||||
			{#if isOwned}
 | 
								{#if isOwned}
 | 
				
			||||||
				<RemoveFromSharedLink bind:sharedLink />
 | 
									<RemoveFromSharedLink bind:sharedLink />
 | 
				
			||||||
 | 
				
			|||||||
@ -9,3 +9,18 @@ export const isDownloading = derived(downloadAssets, ($downloadAssets) => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	return true;
 | 
						return true;
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const update = (key: string, value: number | null) => {
 | 
				
			||||||
 | 
						downloadAssets.update((state) => {
 | 
				
			||||||
 | 
							const newState = { ...state };
 | 
				
			||||||
 | 
							if (value === null) {
 | 
				
			||||||
 | 
								delete newState[key];
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								newState[key] = value;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return newState;
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const clearDownload = (key: string) => update(key, null);
 | 
				
			||||||
 | 
					export const updateDownload = (key: string, value: number) => update(key, value);
 | 
				
			||||||
 | 
				
			|||||||
@ -1,9 +1,16 @@
 | 
				
			|||||||
import { api, AddAssetsResponseDto, AssetResponseDto } from '@api';
 | 
					 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
	notificationController,
 | 
						notificationController,
 | 
				
			||||||
	NotificationType
 | 
						NotificationType
 | 
				
			||||||
} from '$lib/components/shared-components/notification/notification';
 | 
					} from '$lib/components/shared-components/notification/notification';
 | 
				
			||||||
import { downloadAssets } from '$lib/stores/download';
 | 
					import { clearDownload, updateDownload } from '$lib/stores/download';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
						AddAssetsResponseDto,
 | 
				
			||||||
 | 
						api,
 | 
				
			||||||
 | 
						AssetApiGetDownloadInfoRequest,
 | 
				
			||||||
 | 
						AssetResponseDto,
 | 
				
			||||||
 | 
						DownloadResponseDto
 | 
				
			||||||
 | 
					} from '@api';
 | 
				
			||||||
 | 
					import { handleError } from './handle-error';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const addAssetsToAlbum = async (
 | 
					export const addAssetsToAlbum = async (
 | 
				
			||||||
	albumId: string,
 | 
						albumId: string,
 | 
				
			||||||
@ -24,84 +31,104 @@ export const addAssetsToAlbum = async (
 | 
				
			|||||||
			return dto;
 | 
								return dto;
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function bulkDownload(
 | 
					const downloadBlob = (data: Blob, filename: string) => {
 | 
				
			||||||
	fileName: string,
 | 
						const url = URL.createObjectURL(data);
 | 
				
			||||||
	assets: AssetResponseDto[],
 | 
					 | 
				
			||||||
	onDone?: () => void,
 | 
					 | 
				
			||||||
	key?: string
 | 
					 | 
				
			||||||
) {
 | 
					 | 
				
			||||||
	const assetIds = assets.map((asset) => asset.id);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	try {
 | 
					 | 
				
			||||||
		// let skip = 0;
 | 
					 | 
				
			||||||
		let count = 0;
 | 
					 | 
				
			||||||
		let done = false;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		while (!done) {
 | 
					 | 
				
			||||||
			count++;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			const downloadFileName = fileName + `${count === 1 ? '' : count}.zip`;
 | 
					 | 
				
			||||||
			downloadAssets.set({ [downloadFileName]: 0 });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			let total = 0;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			const { data, status, headers } = await api.assetApi.downloadFiles(
 | 
					 | 
				
			||||||
				{ downloadFilesDto: { assetIds }, key },
 | 
					 | 
				
			||||||
				{
 | 
					 | 
				
			||||||
					responseType: 'blob',
 | 
					 | 
				
			||||||
					onDownloadProgress: function (progressEvent) {
 | 
					 | 
				
			||||||
						const request = this as XMLHttpRequest;
 | 
					 | 
				
			||||||
						if (!total) {
 | 
					 | 
				
			||||||
							total = Number(request.getResponseHeader('X-Immich-Content-Length-Hint')) || 0;
 | 
					 | 
				
			||||||
						}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
						if (total) {
 | 
					 | 
				
			||||||
							const current = progressEvent.loaded;
 | 
					 | 
				
			||||||
							downloadAssets.set({ [downloadFileName]: Math.floor((current / total) * 100) });
 | 
					 | 
				
			||||||
						}
 | 
					 | 
				
			||||||
					}
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			const isNotComplete = headers['x-immich-archive-complete'] === 'false';
 | 
					 | 
				
			||||||
			const fileCount = Number(headers['x-immich-archive-file-count']) || 0;
 | 
					 | 
				
			||||||
			if (isNotComplete && fileCount > 0) {
 | 
					 | 
				
			||||||
				// skip += fileCount;
 | 
					 | 
				
			||||||
			} else {
 | 
					 | 
				
			||||||
				onDone?.();
 | 
					 | 
				
			||||||
				done = true;
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			if (!(data instanceof Blob)) {
 | 
					 | 
				
			||||||
				return;
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			if (status === 201) {
 | 
					 | 
				
			||||||
				const fileUrl = URL.createObjectURL(data);
 | 
					 | 
				
			||||||
	const anchor = document.createElement('a');
 | 
						const anchor = document.createElement('a');
 | 
				
			||||||
				anchor.href = fileUrl;
 | 
						anchor.href = url;
 | 
				
			||||||
				anchor.download = downloadFileName;
 | 
						anchor.download = filename;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	document.body.appendChild(anchor);
 | 
						document.body.appendChild(anchor);
 | 
				
			||||||
	anchor.click();
 | 
						anchor.click();
 | 
				
			||||||
	document.body.removeChild(anchor);
 | 
						document.body.removeChild(anchor);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				URL.revokeObjectURL(fileUrl);
 | 
						URL.revokeObjectURL(url);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				// Remove item from download list
 | 
					export const downloadArchive = async (
 | 
				
			||||||
				setTimeout(() => {
 | 
						fileName: string,
 | 
				
			||||||
					downloadAssets.set({});
 | 
						options: Omit<AssetApiGetDownloadInfoRequest, 'key'>,
 | 
				
			||||||
				}, 2000);
 | 
						onDone?: () => void,
 | 
				
			||||||
 | 
						key?: string
 | 
				
			||||||
 | 
					) => {
 | 
				
			||||||
 | 
						let downloadInfo: DownloadResponseDto | null = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						try {
 | 
				
			||||||
 | 
							const { data } = await api.assetApi.getDownloadInfo({ ...options, key });
 | 
				
			||||||
 | 
							downloadInfo = data;
 | 
				
			||||||
 | 
						} catch (error) {
 | 
				
			||||||
 | 
							handleError(error, 'Unable to download files');
 | 
				
			||||||
 | 
							return;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// TODO: prompt for big download
 | 
				
			||||||
 | 
						// const total = downloadInfo.totalSize;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for (let i = 0; i < downloadInfo.archives.length; i++) {
 | 
				
			||||||
 | 
							const archive = downloadInfo.archives[i];
 | 
				
			||||||
 | 
							const suffix = downloadInfo.archives.length === 1 ? '' : `+${i + 1}`;
 | 
				
			||||||
 | 
							const archiveName = fileName.replace('.zip', `${suffix}.zip`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							let downloadKey = `${archiveName}`;
 | 
				
			||||||
 | 
							if (downloadInfo.archives.length > 1) {
 | 
				
			||||||
 | 
								downloadKey = `${archiveName} (${i + 1}/${downloadInfo.archives.length})`;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							updateDownload(downloadKey, 0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							try {
 | 
				
			||||||
 | 
								const { data } = await api.assetApi.downloadArchive(
 | 
				
			||||||
 | 
									{ assetIdsDto: { assetIds: archive.assetIds }, key },
 | 
				
			||||||
 | 
									{
 | 
				
			||||||
 | 
										responseType: 'blob',
 | 
				
			||||||
 | 
										onDownloadProgress: (event) =>
 | 
				
			||||||
 | 
											updateDownload(downloadKey, Math.floor((event.loaded / archive.size) * 100))
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								downloadBlob(data, archiveName);
 | 
				
			||||||
		} catch (e) {
 | 
							} catch (e) {
 | 
				
			||||||
		console.error('Error downloading file ', e);
 | 
								handleError(e, 'Unable to download files');
 | 
				
			||||||
		notificationController.show({
 | 
								clearDownload(downloadKey);
 | 
				
			||||||
			type: NotificationType.Error,
 | 
								return;
 | 
				
			||||||
			message: 'Error downloading file, check console for more details.'
 | 
							} finally {
 | 
				
			||||||
		});
 | 
								setTimeout(() => clearDownload(downloadKey), 3_000);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						onDone?.();
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const downloadFile = async (asset: AssetResponseDto, key?: string) => {
 | 
				
			||||||
 | 
						const filenames = [`${asset.originalFileName}.${getFilenameExtension(asset.originalPath)}`];
 | 
				
			||||||
 | 
						if (asset.livePhotoVideoId) {
 | 
				
			||||||
 | 
							filenames.push(`${asset.originalFileName}.mov`);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for (const filename of filenames) {
 | 
				
			||||||
 | 
							try {
 | 
				
			||||||
 | 
								updateDownload(filename, 0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								const { data } = await api.assetApi.downloadFile(
 | 
				
			||||||
 | 
									{ id: asset.id, key },
 | 
				
			||||||
 | 
									{
 | 
				
			||||||
 | 
										responseType: 'blob',
 | 
				
			||||||
 | 
										onDownloadProgress: (event: ProgressEvent) => {
 | 
				
			||||||
 | 
											if (event.lengthComputable) {
 | 
				
			||||||
 | 
												updateDownload(filename, Math.floor((event.loaded / event.total) * 100));
 | 
				
			||||||
 | 
											}
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								downloadBlob(data, filename);
 | 
				
			||||||
 | 
							} catch (e) {
 | 
				
			||||||
 | 
								handleError(e, `Error downloading ${filename}`);
 | 
				
			||||||
 | 
							} finally {
 | 
				
			||||||
 | 
								setTimeout(() => clearDownload(filename), 3_000);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Returns the lowercase filename extension without a dot (.) and
 | 
					 * Returns the lowercase filename extension without a dot (.) and
 | 
				
			||||||
 | 
				
			|||||||
@ -4,10 +4,20 @@ import {
 | 
				
			|||||||
	NotificationType
 | 
						NotificationType
 | 
				
			||||||
} from '../components/shared-components/notification/notification';
 | 
					} from '../components/shared-components/notification/notification';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function handleError(error: unknown, message: string) {
 | 
					export async function handleError(error: unknown, message: string) {
 | 
				
			||||||
	console.error(`[handleError]: ${message}`, error);
 | 
						console.error(`[handleError]: ${message}`, error);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	let serverMessage = (error as ApiError)?.response?.data?.message;
 | 
						let data = (error as ApiError)?.response?.data;
 | 
				
			||||||
 | 
						if (data instanceof Blob) {
 | 
				
			||||||
 | 
							const response = await data.text();
 | 
				
			||||||
 | 
							try {
 | 
				
			||||||
 | 
								data = JSON.parse(response);
 | 
				
			||||||
 | 
							} catch {
 | 
				
			||||||
 | 
								data = { message: response };
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						let serverMessage = data?.message;
 | 
				
			||||||
	if (serverMessage) {
 | 
						if (serverMessage) {
 | 
				
			||||||
		serverMessage = `${String(serverMessage).slice(0, 75)}\n(Immich Server Error)`;
 | 
							serverMessage = `${String(serverMessage).slice(0, 75)}\n(Immich Server Error)`;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
				
			|||||||
@ -67,7 +67,7 @@
 | 
				
			|||||||
		</AssetSelectContextMenu>
 | 
							</AssetSelectContextMenu>
 | 
				
			||||||
		<DeleteAssets {onAssetDelete} />
 | 
							<DeleteAssets {onAssetDelete} />
 | 
				
			||||||
		<AssetSelectContextMenu icon={DotsVertical} title="Add">
 | 
							<AssetSelectContextMenu icon={DotsVertical} title="Add">
 | 
				
			||||||
			<DownloadAction menuItem />
 | 
								<DownloadAction menuItem filename="{data.person.name || 'immich'}.zip" />
 | 
				
			||||||
			<FavoriteAction menuItem removeFavorite={isAllFavorite} />
 | 
								<FavoriteAction menuItem removeFavorite={isAllFavorite} />
 | 
				
			||||||
			<ArchiveAction
 | 
								<ArchiveAction
 | 
				
			||||||
				menuItem
 | 
									menuItem
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user