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 Binary files a/mobile/openapi/README.md and b/mobile/openapi/README.md differ diff --git a/mobile/openapi/doc/AssetApi.md b/mobile/openapi/doc/AssetApi.md index d6ad217420..dbff762d30 100644 Binary files a/mobile/openapi/doc/AssetApi.md and b/mobile/openapi/doc/AssetApi.md differ diff --git a/mobile/openapi/doc/TrashApi.md b/mobile/openapi/doc/TrashApi.md new file mode 100644 index 0000000000..d120d4da11 Binary files /dev/null and b/mobile/openapi/doc/TrashApi.md differ diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index d05488f306..fbe74168a3 100644 Binary files a/mobile/openapi/lib/api.dart and b/mobile/openapi/lib/api.dart differ diff --git a/mobile/openapi/lib/api/asset_api.dart b/mobile/openapi/lib/api/asset_api.dart index fba8ac7a49..67127392c6 100644 Binary files a/mobile/openapi/lib/api/asset_api.dart and b/mobile/openapi/lib/api/asset_api.dart differ 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 Binary files /dev/null and b/mobile/openapi/lib/api/trash_api.dart differ diff --git a/mobile/openapi/test/asset_api_test.dart b/mobile/openapi/test/asset_api_test.dart index 118b080329..8d0910f1f0 100644 Binary files a/mobile/openapi/test/asset_api_test.dart and b/mobile/openapi/test/asset_api_test.dart differ 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 Binary files /dev/null and b/mobile/openapi/test/trash_api_test.dart differ 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`,