From 96b7885583adb4b434c7df1565ad793c3e793007 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Fri, 26 Jan 2024 11:48:37 -0500 Subject: [PATCH] refactor(server): trash endpoints (#6652) * refactor(server): trash endpoints * chore: open api * chore: fix wrong rename --- .../modules/trash/services/trash.service.dart | 6 +- mobile/openapi/.openapi-generator/FILES | 3 + mobile/openapi/README.md | 9 +- mobile/openapi/doc/AssetApi.md | 30 +- mobile/openapi/doc/TrashApi.md | 170 +++++++++ mobile/openapi/lib/api.dart | 1 + mobile/openapi/lib/api/asset_api.dart | 18 +- mobile/openapi/lib/api/trash_api.dart | 123 +++++++ mobile/openapi/test/asset_api_test.dart | 12 +- mobile/openapi/test/trash_api_test.dart | 36 ++ open-api/immich-openapi-specs.json | 91 ++++- open-api/typescript-sdk/client/api.ts | 325 ++++++++++++++++-- server/src/domain/asset/asset.service.spec.ts | 19 - server/src/domain/asset/asset.service.ts | 33 -- server/src/domain/asset/dto/asset.dto.ts | 5 - server/src/domain/domain.module.ts | 2 + server/src/domain/index.ts | 1 + server/src/domain/trash/index.ts | 1 + server/src/domain/trash/trash.service.spec.ts | 87 +++++ server/src/domain/trash/trash.service.ts | 65 ++++ server/src/immich/app.module.ts | 2 + .../immich/controllers/asset.controller.ts | 24 +- server/src/immich/controllers/index.ts | 1 + .../immich/controllers/trash.controller.ts | 31 ++ web/src/api/api.ts | 3 + .../photos-page/actions/restore-assets.svelte | 2 +- web/src/routes/(user)/trash/+page.svelte | 4 +- 27 files changed, 967 insertions(+), 137 deletions(-) create mode 100644 mobile/openapi/doc/TrashApi.md create mode 100644 mobile/openapi/lib/api/trash_api.dart create mode 100644 mobile/openapi/test/trash_api_test.dart create mode 100644 server/src/domain/trash/index.ts create mode 100644 server/src/domain/trash/trash.service.spec.ts create mode 100644 server/src/domain/trash/trash.service.ts create mode 100644 server/src/immich/controllers/trash.controller.ts diff --git a/mobile/lib/modules/trash/services/trash.service.dart b/mobile/lib/modules/trash/services/trash.service.dart index 1accff4ec4..cfcae27653 100644 --- a/mobile/lib/modules/trash/services/trash.service.dart +++ b/mobile/lib/modules/trash/services/trash.service.dart @@ -22,7 +22,7 @@ class TrashService { try { List remoteIds = assetList.where((a) => a.isRemote).map((e) => e.remoteId!).toList(); - await _apiService.assetApi.restoreAssets(BulkIdsDto(ids: remoteIds)); + await _apiService.assetApi.restoreAssetsOld(BulkIdsDto(ids: remoteIds)); return true; } catch (error, stack) { _log.severe("Cannot restore assets ${error.toString()}", error, stack); @@ -32,7 +32,7 @@ class TrashService { Future emptyTrash() async { try { - await _apiService.assetApi.emptyTrash(); + await _apiService.assetApi.emptyTrashOld(); } catch (error, stack) { _log.severe("Cannot empty trash ${error.toString()}", error, stack); } @@ -40,7 +40,7 @@ class TrashService { Future restoreTrash() async { try { - await _apiService.assetApi.restoreTrash(); + await _apiService.assetApi.restoreTrashOld(); } catch (error, stack) { _log.severe("Cannot restore trash ${error.toString()}", error, stack); } diff --git a/mobile/openapi/.openapi-generator/FILES b/mobile/openapi/.openapi-generator/FILES index 7a8a69467d..3abf82a93d 100644 --- a/mobile/openapi/.openapi-generator/FILES +++ b/mobile/openapi/.openapi-generator/FILES @@ -165,6 +165,7 @@ doc/TimeBucketSize.md doc/ToneMapping.md doc/TranscodeHWAccel.md doc/TranscodePolicy.md +doc/TrashApi.md doc/UpdateAlbumDto.md doc/UpdateAssetDto.md doc/UpdateLibraryDto.md @@ -199,6 +200,7 @@ lib/api/server_info_api.dart lib/api/shared_link_api.dart lib/api/system_config_api.dart lib/api/tag_api.dart +lib/api/trash_api.dart lib/api/user_api.dart lib/api_client.dart lib/api_exception.dart @@ -528,6 +530,7 @@ test/time_bucket_size_test.dart test/tone_mapping_test.dart test/transcode_hw_accel_test.dart test/transcode_policy_test.dart +test/trash_api_test.dart test/update_album_dto_test.dart test/update_asset_dto_test.dart test/update_library_dto_test.dart diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index f4b53eca36..97a04aa376 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -96,7 +96,7 @@ Class | Method | HTTP request | Description *AssetApi* | [**deleteAssets**](doc//AssetApi.md#deleteassets) | **DELETE** /asset | *AssetApi* | [**downloadArchiveOld**](doc//AssetApi.md#downloadarchiveold) | **POST** /asset/download/archive | *AssetApi* | [**downloadFileOld**](doc//AssetApi.md#downloadfileold) | **POST** /asset/download/{id} | -*AssetApi* | [**emptyTrash**](doc//AssetApi.md#emptytrash) | **POST** /asset/trash/empty | +*AssetApi* | [**emptyTrashOld**](doc//AssetApi.md#emptytrashold) | **POST** /asset/trash/empty | *AssetApi* | [**getAllAssets**](doc//AssetApi.md#getallassets) | **GET** /asset | *AssetApi* | [**getAllUserAssetsByDeviceId**](doc//AssetApi.md#getalluserassetsbydeviceid) | **GET** /asset/device/{deviceId} | *AssetApi* | [**getAssetById**](doc//AssetApi.md#getassetbyid) | **GET** /asset/assetById/{id} | @@ -112,8 +112,8 @@ Class | Method | HTTP request | Description *AssetApi* | [**getRandom**](doc//AssetApi.md#getrandom) | **GET** /asset/random | *AssetApi* | [**getTimeBucket**](doc//AssetApi.md#gettimebucket) | **GET** /asset/time-bucket | *AssetApi* | [**getTimeBuckets**](doc//AssetApi.md#gettimebuckets) | **GET** /asset/time-buckets | -*AssetApi* | [**restoreAssets**](doc//AssetApi.md#restoreassets) | **POST** /asset/restore | -*AssetApi* | [**restoreTrash**](doc//AssetApi.md#restoretrash) | **POST** /asset/trash/restore | +*AssetApi* | [**restoreAssetsOld**](doc//AssetApi.md#restoreassetsold) | **POST** /asset/restore | +*AssetApi* | [**restoreTrashOld**](doc//AssetApi.md#restoretrashold) | **POST** /asset/trash/restore | *AssetApi* | [**runAssetJobs**](doc//AssetApi.md#runassetjobs) | **POST** /asset/jobs | *AssetApi* | [**searchAssets**](doc//AssetApi.md#searchassets) | **GET** /assets | *AssetApi* | [**serveFile**](doc//AssetApi.md#servefile) | **GET** /asset/file/{id} | @@ -201,6 +201,9 @@ Class | Method | HTTP request | Description *TagApi* | [**tagAssets**](doc//TagApi.md#tagassets) | **PUT** /tag/{id}/assets | *TagApi* | [**untagAssets**](doc//TagApi.md#untagassets) | **DELETE** /tag/{id}/assets | *TagApi* | [**updateTag**](doc//TagApi.md#updatetag) | **PATCH** /tag/{id} | +*TrashApi* | [**emptyTrash**](doc//TrashApi.md#emptytrash) | **POST** /trash/empty | +*TrashApi* | [**restoreAssets**](doc//TrashApi.md#restoreassets) | **POST** /trash/restore/assets | +*TrashApi* | [**restoreTrash**](doc//TrashApi.md#restoretrash) | **POST** /trash/restore | *UserApi* | [**createProfileImage**](doc//UserApi.md#createprofileimage) | **POST** /user/profile-image | *UserApi* | [**createUser**](doc//UserApi.md#createuser) | **POST** /user | *UserApi* | [**deleteProfileImage**](doc//UserApi.md#deleteprofileimage) | **DELETE** /user/profile-image | diff --git a/mobile/openapi/doc/AssetApi.md b/mobile/openapi/doc/AssetApi.md index d6ad217420..dbff762d30 100644 --- a/mobile/openapi/doc/AssetApi.md +++ b/mobile/openapi/doc/AssetApi.md @@ -14,7 +14,7 @@ Method | HTTP request | Description [**deleteAssets**](AssetApi.md#deleteassets) | **DELETE** /asset | [**downloadArchiveOld**](AssetApi.md#downloadarchiveold) | **POST** /asset/download/archive | [**downloadFileOld**](AssetApi.md#downloadfileold) | **POST** /asset/download/{id} | -[**emptyTrash**](AssetApi.md#emptytrash) | **POST** /asset/trash/empty | +[**emptyTrashOld**](AssetApi.md#emptytrashold) | **POST** /asset/trash/empty | [**getAllAssets**](AssetApi.md#getallassets) | **GET** /asset | [**getAllUserAssetsByDeviceId**](AssetApi.md#getalluserassetsbydeviceid) | **GET** /asset/device/{deviceId} | [**getAssetById**](AssetApi.md#getassetbyid) | **GET** /asset/assetById/{id} | @@ -30,8 +30,8 @@ Method | HTTP request | Description [**getRandom**](AssetApi.md#getrandom) | **GET** /asset/random | [**getTimeBucket**](AssetApi.md#gettimebucket) | **GET** /asset/time-bucket | [**getTimeBuckets**](AssetApi.md#gettimebuckets) | **GET** /asset/time-buckets | -[**restoreAssets**](AssetApi.md#restoreassets) | **POST** /asset/restore | -[**restoreTrash**](AssetApi.md#restoretrash) | **POST** /asset/trash/restore | +[**restoreAssetsOld**](AssetApi.md#restoreassetsold) | **POST** /asset/restore | +[**restoreTrashOld**](AssetApi.md#restoretrashold) | **POST** /asset/trash/restore | [**runAssetJobs**](AssetApi.md#runassetjobs) | **POST** /asset/jobs | [**searchAssets**](AssetApi.md#searchassets) | **GET** /assets | [**serveFile**](AssetApi.md#servefile) | **GET** /asset/file/{id} | @@ -323,8 +323,8 @@ 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) -# **emptyTrash** -> emptyTrash() +# **emptyTrashOld** +> emptyTrashOld() @@ -349,9 +349,9 @@ import 'package:openapi/api.dart'; final api_instance = AssetApi(); try { - api_instance.emptyTrash(); + api_instance.emptyTrashOld(); } catch (e) { - print('Exception when calling AssetApi->emptyTrash: $e\n'); + print('Exception when calling AssetApi->emptyTrashOld: $e\n'); } ``` @@ -1266,8 +1266,8 @@ 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) -# **restoreAssets** -> restoreAssets(bulkIdsDto) +# **restoreAssetsOld** +> restoreAssetsOld(bulkIdsDto) @@ -1293,9 +1293,9 @@ final api_instance = AssetApi(); final bulkIdsDto = BulkIdsDto(); // BulkIdsDto | try { - api_instance.restoreAssets(bulkIdsDto); + api_instance.restoreAssetsOld(bulkIdsDto); } catch (e) { - print('Exception when calling AssetApi->restoreAssets: $e\n'); + print('Exception when calling AssetApi->restoreAssetsOld: $e\n'); } ``` @@ -1320,8 +1320,8 @@ 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) -# **restoreTrash** -> restoreTrash() +# **restoreTrashOld** +> restoreTrashOld() @@ -1346,9 +1346,9 @@ import 'package:openapi/api.dart'; final api_instance = AssetApi(); try { - api_instance.restoreTrash(); + api_instance.restoreTrashOld(); } catch (e) { - print('Exception when calling AssetApi->restoreTrash: $e\n'); + print('Exception when calling AssetApi->restoreTrashOld: $e\n'); } ``` diff --git a/mobile/openapi/doc/TrashApi.md b/mobile/openapi/doc/TrashApi.md new file mode 100644 index 0000000000..d120d4da11 --- /dev/null +++ b/mobile/openapi/doc/TrashApi.md @@ -0,0 +1,170 @@ +# openapi.api.TrashApi + +## Load the API package +```dart +import 'package:openapi/api.dart'; +``` + +All URIs are relative to */api* + +Method | HTTP request | Description +------------- | ------------- | ------------- +[**emptyTrash**](TrashApi.md#emptytrash) | **POST** /trash/empty | +[**restoreAssets**](TrashApi.md#restoreassets) | **POST** /trash/restore/assets | +[**restoreTrash**](TrashApi.md#restoretrash) | **POST** /trash/restore | + + +# **emptyTrash** +> emptyTrash() + + + +### Example +```dart +import 'package:openapi/api.dart'; +// TODO Configure API key authorization: cookie +//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; +// TODO Configure API key authorization: api_key +//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; +// TODO Configure HTTP Bearer authorization: bearer +// Case 1. Use String Token +//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); +// Case 2. Use Function which generate token. +// String yourTokenGeneratorFunction() { ... } +//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); + +final api_instance = TrashApi(); + +try { + api_instance.emptyTrash(); +} catch (e) { + print('Exception when calling TrashApi->emptyTrash: $e\n'); +} +``` + +### Parameters +This endpoint does not need any parameter. + +### Return type + +void (empty response body) + +### Authorization + +[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: Not defined + +[[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) + +# **restoreAssets** +> restoreAssets(bulkIdsDto) + + + +### Example +```dart +import 'package:openapi/api.dart'; +// TODO Configure API key authorization: cookie +//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; +// TODO Configure API key authorization: api_key +//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; +// TODO Configure HTTP Bearer authorization: bearer +// Case 1. Use String Token +//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); +// Case 2. Use Function which generate token. +// String yourTokenGeneratorFunction() { ... } +//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); + +final api_instance = TrashApi(); +final bulkIdsDto = BulkIdsDto(); // BulkIdsDto | + +try { + api_instance.restoreAssets(bulkIdsDto); +} catch (e) { + print('Exception when calling TrashApi->restoreAssets: $e\n'); +} +``` + +### Parameters + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **bulkIdsDto** | [**BulkIdsDto**](BulkIdsDto.md)| | + +### Return type + +void (empty response body) + +### Authorization + +[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) + +### HTTP request headers + + - **Content-Type**: application/json + - **Accept**: Not defined + +[[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) + +# **restoreTrash** +> restoreTrash() + + + +### Example +```dart +import 'package:openapi/api.dart'; +// TODO Configure API key authorization: cookie +//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; +// TODO Configure API key authorization: api_key +//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; +// TODO Configure HTTP Bearer authorization: bearer +// Case 1. Use String Token +//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); +// Case 2. Use Function which generate token. +// String yourTokenGeneratorFunction() { ... } +//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); + +final api_instance = TrashApi(); + +try { + api_instance.restoreTrash(); +} catch (e) { + print('Exception when calling TrashApi->restoreTrash: $e\n'); +} +``` + +### Parameters +This endpoint does not need any parameter. + +### Return type + +void (empty response body) + +### Authorization + +[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: Not defined + +[[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) + diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index d05488f306..fbe74168a3 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -47,6 +47,7 @@ part 'api/server_info_api.dart'; part 'api/shared_link_api.dart'; part 'api/system_config_api.dart'; part 'api/tag_api.dart'; +part 'api/trash_api.dart'; part 'api/user_api.dart'; part 'model/api_key_create_dto.dart'; diff --git a/mobile/openapi/lib/api/asset_api.dart b/mobile/openapi/lib/api/asset_api.dart index fba8ac7a49..67127392c6 100644 --- a/mobile/openapi/lib/api/asset_api.dart +++ b/mobile/openapi/lib/api/asset_api.dart @@ -271,7 +271,7 @@ class AssetApi { } /// Performs an HTTP 'POST /asset/trash/empty' operation and returns the [Response]. - Future emptyTrashWithHttpInfo() async { + Future emptyTrashOldWithHttpInfo() async { // ignore: prefer_const_declarations final path = r'/asset/trash/empty'; @@ -296,8 +296,8 @@ class AssetApi { ); } - Future emptyTrash() async { - final response = await emptyTrashWithHttpInfo(); + Future emptyTrashOld() async { + final response = await emptyTrashOldWithHttpInfo(); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -1327,7 +1327,7 @@ class AssetApi { /// Parameters: /// /// * [BulkIdsDto] bulkIdsDto (required): - Future restoreAssetsWithHttpInfo(BulkIdsDto bulkIdsDto,) async { + Future restoreAssetsOldWithHttpInfo(BulkIdsDto bulkIdsDto,) async { // ignore: prefer_const_declarations final path = r'/asset/restore'; @@ -1355,15 +1355,15 @@ class AssetApi { /// Parameters: /// /// * [BulkIdsDto] bulkIdsDto (required): - Future restoreAssets(BulkIdsDto bulkIdsDto,) async { - final response = await restoreAssetsWithHttpInfo(bulkIdsDto,); + Future restoreAssetsOld(BulkIdsDto bulkIdsDto,) async { + final response = await restoreAssetsOldWithHttpInfo(bulkIdsDto,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } } /// Performs an HTTP 'POST /asset/trash/restore' operation and returns the [Response]. - Future restoreTrashWithHttpInfo() async { + Future restoreTrashOldWithHttpInfo() async { // ignore: prefer_const_declarations final path = r'/asset/trash/restore'; @@ -1388,8 +1388,8 @@ class AssetApi { ); } - Future restoreTrash() async { - final response = await restoreTrashWithHttpInfo(); + Future restoreTrashOld() async { + final response = await restoreTrashOldWithHttpInfo(); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/trash_api.dart b/mobile/openapi/lib/api/trash_api.dart new file mode 100644 index 0000000000..91f1d1a747 --- /dev/null +++ b/mobile/openapi/lib/api/trash_api.dart @@ -0,0 +1,123 @@ +// +// 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 TrashApi { + TrashApi([ApiClient? apiClient]) : apiClient = apiClient ?? defaultApiClient; + + final ApiClient apiClient; + + /// Performs an HTTP 'POST /trash/empty' operation and returns the [Response]. + Future emptyTrashWithHttpInfo() async { + // ignore: prefer_const_declarations + final path = r'/trash/empty'; + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = []; + + + return apiClient.invokeAPI( + path, + 'POST', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + Future emptyTrash() async { + final response = await emptyTrashWithHttpInfo(); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + } + + /// Performs an HTTP 'POST /trash/restore/assets' operation and returns the [Response]. + /// Parameters: + /// + /// * [BulkIdsDto] bulkIdsDto (required): + Future restoreAssetsWithHttpInfo(BulkIdsDto bulkIdsDto,) async { + // ignore: prefer_const_declarations + final path = r'/trash/restore/assets'; + + // ignore: prefer_final_locals + Object? postBody = bulkIdsDto; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = ['application/json']; + + + return apiClient.invokeAPI( + path, + 'POST', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [BulkIdsDto] bulkIdsDto (required): + Future restoreAssets(BulkIdsDto bulkIdsDto,) async { + final response = await restoreAssetsWithHttpInfo(bulkIdsDto,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + } + + /// Performs an HTTP 'POST /trash/restore' operation and returns the [Response]. + Future restoreTrashWithHttpInfo() async { + // ignore: prefer_const_declarations + final path = r'/trash/restore'; + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = []; + + + return apiClient.invokeAPI( + path, + 'POST', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + Future restoreTrash() async { + final response = await restoreTrashWithHttpInfo(); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + } +} diff --git a/mobile/openapi/test/asset_api_test.dart b/mobile/openapi/test/asset_api_test.dart index 118b080329..8d0910f1f0 100644 --- a/mobile/openapi/test/asset_api_test.dart +++ b/mobile/openapi/test/asset_api_test.dart @@ -46,8 +46,8 @@ void main() { // TODO }); - //Future emptyTrash() async - test('test emptyTrash', () async { + //Future emptyTrashOld() async + test('test emptyTrashOld', () async { // TODO }); @@ -132,13 +132,13 @@ void main() { // TODO }); - //Future restoreAssets(BulkIdsDto bulkIdsDto) async - test('test restoreAssets', () async { + //Future restoreAssetsOld(BulkIdsDto bulkIdsDto) async + test('test restoreAssetsOld', () async { // TODO }); - //Future restoreTrash() async - test('test restoreTrash', () async { + //Future restoreTrashOld() async + test('test restoreTrashOld', () async { // TODO }); diff --git a/mobile/openapi/test/trash_api_test.dart b/mobile/openapi/test/trash_api_test.dart new file mode 100644 index 0000000000..e96c254e4f --- /dev/null +++ b/mobile/openapi/test/trash_api_test.dart @@ -0,0 +1,36 @@ +// +// 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 TrashApi +void main() { + // final instance = TrashApi(); + + group('tests for TrashApi', () { + //Future emptyTrash() async + test('test emptyTrash', () async { + // TODO + }); + + //Future restoreAssets(BulkIdsDto bulkIdsDto) async + test('test restoreAssets', () async { + // TODO + }); + + //Future restoreTrash() async + test('test restoreTrash', () async { + // TODO + }); + + }); +} diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index f32ca7a855..03f6811bb2 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -1734,7 +1734,7 @@ }, "/asset/restore": { "post": { - "operationId": "restoreAssets", + "operationId": "restoreAssetsOld", "parameters": [], "requestBody": { "content": { @@ -2219,7 +2219,7 @@ }, "/asset/trash/empty": { "post": { - "operationId": "emptyTrash", + "operationId": "emptyTrashOld", "parameters": [], "responses": { "204": { @@ -2244,7 +2244,7 @@ }, "/asset/trash/restore": { "post": { - "operationId": "restoreTrash", + "operationId": "restoreTrashOld", "parameters": [], "responses": { "204": { @@ -5983,6 +5983,91 @@ ] } }, + "/trash/empty": { + "post": { + "operationId": "emptyTrash", + "parameters": [], + "responses": { + "204": { + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Trash" + ] + } + }, + "/trash/restore": { + "post": { + "operationId": "restoreTrash", + "parameters": [], + "responses": { + "204": { + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Trash" + ] + } + }, + "/trash/restore/assets": { + "post": { + "operationId": "restoreAssets", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BulkIdsDto" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Trash" + ] + } + }, "/user": { "get": { "operationId": "getAllUsers", diff --git a/open-api/typescript-sdk/client/api.ts b/open-api/typescript-sdk/client/api.ts index c9408d8cec..5be129b3a2 100644 --- a/open-api/typescript-sdk/client/api.ts +++ b/open-api/typescript-sdk/client/api.ts @@ -7026,7 +7026,7 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration * @param {*} [options] Override http request option. * @throws {RequiredError} */ - emptyTrash: async (options: RawAxiosRequestConfig = {}): Promise => { + emptyTrashOld: async (options: RawAxiosRequestConfig = {}): Promise => { const localVarPath = `/asset/trash/empty`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); @@ -7896,9 +7896,9 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration * @param {*} [options] Override http request option. * @throws {RequiredError} */ - restoreAssets: async (bulkIdsDto: BulkIdsDto, options: RawAxiosRequestConfig = {}): Promise => { + restoreAssetsOld: async (bulkIdsDto: BulkIdsDto, options: RawAxiosRequestConfig = {}): Promise => { // verify required parameter 'bulkIdsDto' is not null or undefined - assertParamExists('restoreAssets', 'bulkIdsDto', bulkIdsDto) + assertParamExists('restoreAssetsOld', 'bulkIdsDto', bulkIdsDto) const localVarPath = `/asset/restore`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); @@ -7939,7 +7939,7 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration * @param {*} [options] Override http request option. * @throws {RequiredError} */ - restoreTrash: async (options: RawAxiosRequestConfig = {}): Promise => { + restoreTrashOld: async (options: RawAxiosRequestConfig = {}): Promise => { const localVarPath = `/asset/trash/restore`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); @@ -8672,10 +8672,10 @@ export const AssetApiFp = function(configuration?: Configuration) { * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async emptyTrash(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.emptyTrash(options); + async emptyTrashOld(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.emptyTrashOld(options); const index = configuration?.serverIndex ?? 0; - const operationBasePath = operationServerMap['AssetApi.emptyTrash']?.[index]?.url; + const operationBasePath = operationServerMap['AssetApi.emptyTrashOld']?.[index]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); }, /** @@ -8899,10 +8899,10 @@ export const AssetApiFp = function(configuration?: Configuration) { * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async restoreAssets(bulkIdsDto: BulkIdsDto, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.restoreAssets(bulkIdsDto, options); + async restoreAssetsOld(bulkIdsDto: BulkIdsDto, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.restoreAssetsOld(bulkIdsDto, options); const index = configuration?.serverIndex ?? 0; - const operationBasePath = operationServerMap['AssetApi.restoreAssets']?.[index]?.url; + const operationBasePath = operationServerMap['AssetApi.restoreAssetsOld']?.[index]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); }, /** @@ -8910,10 +8910,10 @@ export const AssetApiFp = function(configuration?: Configuration) { * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async restoreTrash(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.restoreTrash(options); + async restoreTrashOld(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.restoreTrashOld(options); const index = configuration?.serverIndex ?? 0; - const operationBasePath = operationServerMap['AssetApi.restoreTrash']?.[index]?.url; + const operationBasePath = operationServerMap['AssetApi.restoreTrashOld']?.[index]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); }, /** @@ -9118,8 +9118,8 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath * @param {*} [options] Override http request option. * @throws {RequiredError} */ - emptyTrash(options?: RawAxiosRequestConfig): AxiosPromise { - return localVarFp.emptyTrash(options).then((request) => request(axios, basePath)); + emptyTrashOld(options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.emptyTrashOld(options).then((request) => request(axios, basePath)); }, /** * Get all AssetEntity belong to the user @@ -9256,20 +9256,20 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath }, /** * - * @param {AssetApiRestoreAssetsRequest} requestParameters Request parameters. + * @param {AssetApiRestoreAssetsOldRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} */ - restoreAssets(requestParameters: AssetApiRestoreAssetsRequest, options?: RawAxiosRequestConfig): AxiosPromise { - return localVarFp.restoreAssets(requestParameters.bulkIdsDto, options).then((request) => request(axios, basePath)); + restoreAssetsOld(requestParameters: AssetApiRestoreAssetsOldRequest, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.restoreAssetsOld(requestParameters.bulkIdsDto, options).then((request) => request(axios, basePath)); }, /** * * @param {*} [options] Override http request option. * @throws {RequiredError} */ - restoreTrash(options?: RawAxiosRequestConfig): AxiosPromise { - return localVarFp.restoreTrash(options).then((request) => request(axios, basePath)); + restoreTrashOld(options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.restoreTrashOld(options).then((request) => request(axios, basePath)); }, /** * @@ -9849,15 +9849,15 @@ export interface AssetApiGetTimeBucketsRequest { } /** - * Request parameters for restoreAssets operation in AssetApi. + * Request parameters for restoreAssetsOld operation in AssetApi. * @export - * @interface AssetApiRestoreAssetsRequest + * @interface AssetApiRestoreAssetsOldRequest */ -export interface AssetApiRestoreAssetsRequest { +export interface AssetApiRestoreAssetsOldRequest { /** * * @type {BulkIdsDto} - * @memberof AssetApiRestoreAssets + * @memberof AssetApiRestoreAssetsOld */ readonly bulkIdsDto: BulkIdsDto } @@ -10434,8 +10434,8 @@ export class AssetApi extends BaseAPI { * @throws {RequiredError} * @memberof AssetApi */ - public emptyTrash(options?: RawAxiosRequestConfig) { - return AssetApiFp(this.configuration).emptyTrash(options).then((request) => request(this.axios, this.basePath)); + public emptyTrashOld(options?: RawAxiosRequestConfig) { + return AssetApiFp(this.configuration).emptyTrashOld(options).then((request) => request(this.axios, this.basePath)); } /** @@ -10603,13 +10603,13 @@ export class AssetApi extends BaseAPI { /** * - * @param {AssetApiRestoreAssetsRequest} requestParameters Request parameters. + * @param {AssetApiRestoreAssetsOldRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof AssetApi */ - public restoreAssets(requestParameters: AssetApiRestoreAssetsRequest, options?: RawAxiosRequestConfig) { - return AssetApiFp(this.configuration).restoreAssets(requestParameters.bulkIdsDto, options).then((request) => request(this.axios, this.basePath)); + public restoreAssetsOld(requestParameters: AssetApiRestoreAssetsOldRequest, options?: RawAxiosRequestConfig) { + return AssetApiFp(this.configuration).restoreAssetsOld(requestParameters.bulkIdsDto, options).then((request) => request(this.axios, this.basePath)); } /** @@ -10618,8 +10618,8 @@ export class AssetApi extends BaseAPI { * @throws {RequiredError} * @memberof AssetApi */ - public restoreTrash(options?: RawAxiosRequestConfig) { - return AssetApiFp(this.configuration).restoreTrash(options).then((request) => request(this.axios, this.basePath)); + public restoreTrashOld(options?: RawAxiosRequestConfig) { + return AssetApiFp(this.configuration).restoreTrashOld(options).then((request) => request(this.axios, this.basePath)); } /** @@ -18135,6 +18135,269 @@ export class TagApi extends BaseAPI { +/** + * TrashApi - axios parameter creator + * @export + */ +export const TrashApiAxiosParamCreator = function (configuration?: Configuration) { + return { + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + emptyTrash: async (options: RawAxiosRequestConfig = {}): Promise => { + const localVarPath = `/trash/empty`; + // 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: 'POST', ...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) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @param {BulkIdsDto} bulkIdsDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + restoreAssets: async (bulkIdsDto: BulkIdsDto, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'bulkIdsDto' is not null or undefined + assertParamExists('restoreAssets', 'bulkIdsDto', bulkIdsDto) + const localVarPath = `/trash/restore/assets`; + // 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: 'POST', ...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) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(bulkIdsDto, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + restoreTrash: async (options: RawAxiosRequestConfig = {}): Promise => { + const localVarPath = `/trash/restore`; + // 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: 'POST', ...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) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + } +}; + +/** + * TrashApi - functional programming interface + * @export + */ +export const TrashApiFp = function(configuration?: Configuration) { + const localVarAxiosParamCreator = TrashApiAxiosParamCreator(configuration) + return { + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async emptyTrash(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.emptyTrash(options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['TrashApi.emptyTrash']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * + * @param {BulkIdsDto} bulkIdsDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async restoreAssets(bulkIdsDto: BulkIdsDto, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.restoreAssets(bulkIdsDto, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['TrashApi.restoreAssets']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async restoreTrash(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.restoreTrash(options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['TrashApi.restoreTrash']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + } +}; + +/** + * TrashApi - factory interface + * @export + */ +export const TrashApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { + const localVarFp = TrashApiFp(configuration) + return { + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + emptyTrash(options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.emptyTrash(options).then((request) => request(axios, basePath)); + }, + /** + * + * @param {TrashApiRestoreAssetsRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + restoreAssets(requestParameters: TrashApiRestoreAssetsRequest, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.restoreAssets(requestParameters.bulkIdsDto, options).then((request) => request(axios, basePath)); + }, + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + restoreTrash(options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.restoreTrash(options).then((request) => request(axios, basePath)); + }, + }; +}; + +/** + * Request parameters for restoreAssets operation in TrashApi. + * @export + * @interface TrashApiRestoreAssetsRequest + */ +export interface TrashApiRestoreAssetsRequest { + /** + * + * @type {BulkIdsDto} + * @memberof TrashApiRestoreAssets + */ + readonly bulkIdsDto: BulkIdsDto +} + +/** + * TrashApi - object-oriented interface + * @export + * @class TrashApi + * @extends {BaseAPI} + */ +export class TrashApi extends BaseAPI { + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof TrashApi + */ + public emptyTrash(options?: RawAxiosRequestConfig) { + return TrashApiFp(this.configuration).emptyTrash(options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @param {TrashApiRestoreAssetsRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof TrashApi + */ + public restoreAssets(requestParameters: TrashApiRestoreAssetsRequest, options?: RawAxiosRequestConfig) { + return TrashApiFp(this.configuration).restoreAssets(requestParameters.bulkIdsDto, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof TrashApi + */ + public restoreTrash(options?: RawAxiosRequestConfig) { + return TrashApiFp(this.configuration).restoreTrash(options).then((request) => request(this.axios, this.basePath)); + } +} + + + /** * UserApi - axios parameter creator * @export diff --git a/server/src/domain/asset/asset.service.spec.ts b/server/src/domain/asset/asset.service.spec.ts index ac3e3d58d0..0fed93c46e 100644 --- a/server/src/domain/asset/asset.service.spec.ts +++ b/server/src/domain/asset/asset.service.spec.ts @@ -679,25 +679,6 @@ describe(AssetService.name, () => { }); }); - describe('restoreAll', () => { - it('should require asset restore access for all ids', async () => { - await expect( - sut.deleteAll(authStub.user1, { - ids: ['asset-1'], - }), - ).rejects.toBeInstanceOf(BadRequestException); - }); - - it('should restore a batch of assets', async () => { - accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset1', 'asset2'])); - - await sut.restoreAll(authStub.user1, { ids: ['asset1', 'asset2'] }); - - expect(assetMock.restoreAll).toHaveBeenCalledWith(['asset1', 'asset2']); - expect(jobMock.queue.mock.calls).toEqual([]); - }); - }); - describe('handleAssetDeletion', () => { beforeEach(() => { when(jobMock.queue) diff --git a/server/src/domain/asset/asset.service.ts b/server/src/domain/asset/asset.service.ts index 24297dce1c..684270e232 100644 --- a/server/src/domain/asset/asset.service.ts +++ b/server/src/domain/asset/asset.service.ts @@ -37,14 +37,12 @@ import { MemoryLaneDto, TimeBucketAssetDto, TimeBucketDto, - TrashAction, UpdateAssetDto, UpdateStackParentDto, mapStats, } from './dto'; import { AssetResponseDto, - BulkIdsDto, MapMarkerResponseDto, MemoryLaneResponseDto, SanitizedAssetResponseDto, @@ -451,37 +449,6 @@ export class AssetService { } } - async handleTrashAction(auth: AuthDto, action: TrashAction): Promise { - const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => - this.assetRepository.getByUserId(pagination, auth.user.id, { trashedBefore: DateTime.now().toJSDate() }), - ); - - if (action == TrashAction.RESTORE_ALL) { - for await (const assets of assetPagination) { - const ids = assets.map((a) => a.id); - await this.assetRepository.restoreAll(ids); - this.communicationRepository.send(ClientEvent.ASSET_RESTORE, auth.user.id, ids); - } - return; - } - - if (action == TrashAction.EMPTY_ALL) { - for await (const assets of assetPagination) { - await this.jobRepository.queueAll( - assets.map((asset) => ({ name: JobName.ASSET_DELETION, data: { id: asset.id } })), - ); - } - return; - } - } - - async restoreAll(auth: AuthDto, dto: BulkIdsDto): Promise { - const { ids } = dto; - await this.access.requirePermission(auth, Permission.ASSET_RESTORE, ids); - await this.assetRepository.restoreAll(ids); - this.communicationRepository.send(ClientEvent.ASSET_RESTORE, auth.user.id, ids); - } - async updateStackParent(auth: AuthDto, dto: UpdateStackParentDto): Promise { const { oldParentId, newParentId } = dto; await this.access.requirePermission(auth, Permission.ASSET_READ, oldParentId); diff --git a/server/src/domain/asset/dto/asset.dto.ts b/server/src/domain/asset/dto/asset.dto.ts index ec3f5df478..bd4cf93ba6 100644 --- a/server/src/domain/asset/dto/asset.dto.ts +++ b/server/src/domain/asset/dto/asset.dto.ts @@ -246,11 +246,6 @@ export class RandomAssetsDto { count?: number; } -export enum TrashAction { - EMPTY_ALL = 'empty-all', - RESTORE_ALL = 'restore-all', -} - export class AssetBulkDeleteDto extends BulkIdsDto { @Optional() @IsBoolean() diff --git a/server/src/domain/domain.module.ts b/server/src/domain/domain.module.ts index 805664e11f..37faa09c9f 100644 --- a/server/src/domain/domain.module.ts +++ b/server/src/domain/domain.module.ts @@ -22,6 +22,7 @@ import { StorageService } from './storage'; import { StorageTemplateService } from './storage-template'; import { SystemConfigService } from './system-config'; import { TagService } from './tag'; +import { TrashService } from './trash'; import { UserService } from './user'; const providers: Provider[] = [ @@ -48,6 +49,7 @@ const providers: Provider[] = [ StorageTemplateService, SystemConfigService, TagService, + TrashService, UserService, ]; diff --git a/server/src/domain/index.ts b/server/src/domain/index.ts index 341245c16a..dce2fa696d 100644 --- a/server/src/domain/index.ts +++ b/server/src/domain/index.ts @@ -26,4 +26,5 @@ export * from './storage'; export * from './storage-template'; export * from './system-config'; export * from './tag'; +export * from './trash'; export * from './user'; diff --git a/server/src/domain/trash/index.ts b/server/src/domain/trash/index.ts new file mode 100644 index 0000000000..3cd00e1912 --- /dev/null +++ b/server/src/domain/trash/index.ts @@ -0,0 +1 @@ +export * from './trash.service'; diff --git a/server/src/domain/trash/trash.service.spec.ts b/server/src/domain/trash/trash.service.spec.ts new file mode 100644 index 0000000000..1b200a1bd8 --- /dev/null +++ b/server/src/domain/trash/trash.service.spec.ts @@ -0,0 +1,87 @@ +import { BadRequestException } from '@nestjs/common'; +import { + IAccessRepositoryMock, + assetStub, + authStub, + newAccessRepositoryMock, + newAssetRepositoryMock, + newCommunicationRepositoryMock, + newJobRepositoryMock, +} from '@test'; +import { JobName } from '..'; +import { ClientEvent, IAssetRepository, ICommunicationRepository, IJobRepository } from '../repositories'; +import { TrashService } from './trash.service'; + +describe(TrashService.name, () => { + let sut: TrashService; + let accessMock: IAccessRepositoryMock; + let assetMock: jest.Mocked; + let jobMock: jest.Mocked; + let communicationMock: jest.Mocked; + + it('should work', () => { + expect(sut).toBeDefined(); + }); + + beforeEach(async () => { + accessMock = newAccessRepositoryMock(); + assetMock = newAssetRepositoryMock(); + communicationMock = newCommunicationRepositoryMock(); + jobMock = newJobRepositoryMock(); + + sut = new TrashService(accessMock, assetMock, jobMock, communicationMock); + }); + + describe('restoreAssets', () => { + it('should require asset restore access for all ids', async () => { + await expect( + sut.restoreAssets(authStub.user1, { + ids: ['asset-1'], + }), + ).rejects.toBeInstanceOf(BadRequestException); + }); + + it('should restore a batch of assets', async () => { + accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset1', 'asset2'])); + + await sut.restoreAssets(authStub.user1, { ids: ['asset1', 'asset2'] }); + + expect(assetMock.restoreAll).toHaveBeenCalledWith(['asset1', 'asset2']); + expect(jobMock.queue.mock.calls).toEqual([]); + }); + }); + + describe('restore', () => { + it('should handle an empty trash', async () => { + assetMock.getByUserId.mockResolvedValue({ items: [], hasNextPage: false }); + await expect(sut.restore(authStub.user1)).resolves.toBeUndefined(); + expect(assetMock.restoreAll).not.toHaveBeenCalled(); + expect(communicationMock.send).not.toHaveBeenCalled(); + }); + + it('should restore and notify', async () => { + assetMock.getByUserId.mockResolvedValue({ items: [assetStub.image], hasNextPage: false }); + await expect(sut.restore(authStub.user1)).resolves.toBeUndefined(); + expect(assetMock.restoreAll).toHaveBeenCalledWith([assetStub.image.id]); + expect(communicationMock.send).toHaveBeenCalledWith(ClientEvent.ASSET_RESTORE, authStub.user1.user.id, [ + assetStub.image.id, + ]); + }); + }); + + describe('empty', () => { + it('should handle an empty trash', async () => { + assetMock.getByUserId.mockResolvedValue({ items: [], hasNextPage: false }); + await expect(sut.empty(authStub.user1)).resolves.toBeUndefined(); + expect(jobMock.queueAll).toHaveBeenCalledWith([]); + }); + + it('should empty the trash', async () => { + assetMock.getByUserId.mockResolvedValue({ items: [assetStub.image], hasNextPage: false }); + await expect(sut.empty(authStub.user1)).resolves.toBeUndefined(); + expect(jobMock.queueAll).toHaveBeenCalledWith([ + { name: JobName.ASSET_DELETION, data: { id: assetStub.image.id } }, + ]); + }); + }); +}); diff --git a/server/src/domain/trash/trash.service.ts b/server/src/domain/trash/trash.service.ts new file mode 100644 index 0000000000..b1a38f72c9 --- /dev/null +++ b/server/src/domain/trash/trash.service.ts @@ -0,0 +1,65 @@ +import { Inject } from '@nestjs/common'; +import { DateTime } from 'luxon'; +import { AccessCore, Permission } from '../access'; +import { BulkIdsDto } from '../asset'; +import { AuthDto } from '../auth'; +import { usePagination } from '../domain.util'; +import { JOBS_ASSET_PAGINATION_SIZE, JobName } from '../job'; +import { + ClientEvent, + IAccessRepository, + IAssetRepository, + ICommunicationRepository, + IJobRepository, +} from '../repositories'; + +export class TrashService { + private access: AccessCore; + + constructor( + @Inject(IAccessRepository) accessRepository: IAccessRepository, + @Inject(IAssetRepository) private assetRepository: IAssetRepository, + @Inject(IJobRepository) private jobRepository: IJobRepository, + @Inject(ICommunicationRepository) private communicationRepository: ICommunicationRepository, + ) { + this.access = AccessCore.create(accessRepository); + } + + async restoreAssets(auth: AuthDto, dto: BulkIdsDto): Promise { + const { ids } = dto; + await this.access.requirePermission(auth, Permission.ASSET_RESTORE, ids); + await this.restoreAndSend(auth, ids); + } + + async restore(auth: AuthDto): Promise { + const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => + this.assetRepository.getByUserId(pagination, auth.user.id, { trashedBefore: DateTime.now().toJSDate() }), + ); + + for await (const assets of assetPagination) { + const ids = assets.map((a) => a.id); + await this.restoreAndSend(auth, ids); + } + } + + async empty(auth: AuthDto): Promise { + const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => + this.assetRepository.getByUserId(pagination, auth.user.id, { trashedBefore: DateTime.now().toJSDate() }), + ); + + for await (const assets of assetPagination) { + await this.jobRepository.queueAll( + assets.map((asset) => ({ name: JobName.ASSET_DELETION, data: { id: asset.id } })), + ); + } + } + + private async restoreAndSend(auth: AuthDto, ids: string[]) { + if (ids.length === 0) { + return; + } + + await this.assetRepository.restoreAll(ids); + this.communicationRepository.send(ClientEvent.ASSET_RESTORE, auth.user.id, ids); + } +} diff --git a/server/src/immich/app.module.ts b/server/src/immich/app.module.ts index 07a8183a39..8d02a44a91 100644 --- a/server/src/immich/app.module.ts +++ b/server/src/immich/app.module.ts @@ -31,6 +31,7 @@ import { SharedLinkController, SystemConfigController, TagController, + TrashController, UserController, } from './controllers'; import { ErrorInterceptor, FileUploadInterceptor } from './interceptors'; @@ -64,6 +65,7 @@ import { ErrorInterceptor, FileUploadInterceptor } from './interceptors'; SharedLinkController, SystemConfigController, TagController, + TrashController, UserController, PersonController, ], diff --git a/server/src/immich/controllers/asset.controller.ts b/server/src/immich/controllers/asset.controller.ts index 59685fb993..86a2b155ab 100644 --- a/server/src/immich/controllers/asset.controller.ts +++ b/server/src/immich/controllers/asset.controller.ts @@ -22,7 +22,7 @@ import { TimeBucketAssetDto, TimeBucketDto, TimeBucketResponseDto, - TrashAction, + TrashService, UpdateAssetDto as UpdateDto, UpdateStackParentDto, } from '@app/domain'; @@ -69,6 +69,7 @@ export class AssetController { constructor( private service: AssetService, private downloadService: DownloadService, + private trashService: TrashService, ) {} @Get('map-marker') @@ -165,22 +166,31 @@ export class AssetController { return this.service.deleteAll(auth, dto); } + /** + * @deprecated use `POST /trash/restore/assets` + */ @Post('restore') @HttpCode(HttpStatus.NO_CONTENT) - restoreAssets(@Auth() auth: AuthDto, @Body() dto: BulkIdsDto): Promise { - return this.service.restoreAll(auth, dto); + restoreAssetsOld(@Auth() auth: AuthDto, @Body() dto: BulkIdsDto): Promise { + return this.trashService.restoreAssets(auth, dto); } + /** + * @deprecated use `POST /trash/empty` + */ @Post('trash/empty') @HttpCode(HttpStatus.NO_CONTENT) - emptyTrash(@Auth() auth: AuthDto): Promise { - return this.service.handleTrashAction(auth, TrashAction.EMPTY_ALL); + emptyTrashOld(@Auth() auth: AuthDto): Promise { + return this.trashService.empty(auth); } + /** + * @deprecated use `POST /trash/restore` + */ @Post('trash/restore') @HttpCode(HttpStatus.NO_CONTENT) - restoreTrash(@Auth() auth: AuthDto): Promise { - return this.service.handleTrashAction(auth, TrashAction.RESTORE_ALL); + restoreTrashOld(@Auth() auth: AuthDto): Promise { + return this.trashService.restore(auth); } @Put('stack/parent') diff --git a/server/src/immich/controllers/index.ts b/server/src/immich/controllers/index.ts index d6e2938ef3..f4e4730917 100644 --- a/server/src/immich/controllers/index.ts +++ b/server/src/immich/controllers/index.ts @@ -17,4 +17,5 @@ export * from './server-info.controller'; export * from './shared-link.controller'; export * from './system-config.controller'; export * from './tag.controller'; +export * from './trash.controller'; export * from './user.controller'; diff --git a/server/src/immich/controllers/trash.controller.ts b/server/src/immich/controllers/trash.controller.ts new file mode 100644 index 0000000000..9f7abe3116 --- /dev/null +++ b/server/src/immich/controllers/trash.controller.ts @@ -0,0 +1,31 @@ +import { AuthDto, BulkIdsDto, TrashService } from '@app/domain'; +import { Body, Controller, HttpCode, HttpStatus, Post } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { Auth, Authenticated } from '../app.guard'; +import { UseValidation } from '../app.utils'; + +@ApiTags('Trash') +@Controller('trash') +@Authenticated() +@UseValidation() +export class TrashController { + constructor(private service: TrashService) {} + + @Post('empty') + @HttpCode(HttpStatus.NO_CONTENT) + emptyTrash(@Auth() auth: AuthDto): Promise { + return this.service.empty(auth); + } + + @Post('restore') + @HttpCode(HttpStatus.NO_CONTENT) + restoreTrash(@Auth() auth: AuthDto): Promise { + return this.service.restore(auth); + } + + @Post('restore/assets') + @HttpCode(HttpStatus.NO_CONTENT) + restoreAssets(@Auth() auth: AuthDto, @Body() dto: BulkIdsDto): Promise { + return this.service.restoreAssets(auth, dto); + } +} diff --git a/web/src/api/api.ts b/web/src/api/api.ts index cb43fa8f38..387c754b25 100644 --- a/web/src/api/api.ts +++ b/web/src/api/api.ts @@ -19,6 +19,7 @@ import { ServerInfoApi, SharedLinkApi, SystemConfigApi, + TrashApi, UserApi, UserApiFp, base, @@ -46,6 +47,7 @@ class ImmichApi { public personApi: PersonApi; public systemConfigApi: SystemConfigApi; public userApi: UserApi; + public trashApi: TrashApi; private config: configuration.Configuration; private key?: string; @@ -75,6 +77,7 @@ class ImmichApi { this.personApi = new PersonApi(this.config); this.systemConfigApi = new SystemConfigApi(this.config); this.userApi = new UserApi(this.config); + this.trashApi = new TrashApi(this.config); } private createUrl(path: string, params?: Record) { diff --git a/web/src/lib/components/photos-page/actions/restore-assets.svelte b/web/src/lib/components/photos-page/actions/restore-assets.svelte index fef340891e..4efbbda532 100644 --- a/web/src/lib/components/photos-page/actions/restore-assets.svelte +++ b/web/src/lib/components/photos-page/actions/restore-assets.svelte @@ -22,7 +22,7 @@ try { const ids = Array.from(getAssets()).map((a) => a.id); - await api.assetApi.restoreAssets({ bulkIdsDto: { ids } }); + await api.trashApi.restoreAssets({ bulkIdsDto: { ids } }); onRestore?.(ids); notificationController.show({ diff --git a/web/src/routes/(user)/trash/+page.svelte b/web/src/routes/(user)/trash/+page.svelte index 87e3190e66..d5e22a79ca 100644 --- a/web/src/routes/(user)/trash/+page.svelte +++ b/web/src/routes/(user)/trash/+page.svelte @@ -37,7 +37,7 @@ const handleEmptyTrash = async () => { isShowEmptyConfirmation = false; try { - await api.assetApi.emptyTrash(); + await api.trashApi.emptyTrash(); notificationController.show({ message: `Empty trash initiated. Refresh the page to see the changes`, @@ -50,7 +50,7 @@ const handleRestoreTrash = async () => { try { - await api.assetApi.restoreTrash(); + await api.trashApi.restoreTrash(); notificationController.show({ message: `Restore trash initiated. Refresh the page to see the changes`,