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 | Bin 24582 -> 24879 bytes mobile/openapi/doc/AssetApi.md | Bin 71523 -> 71577 bytes mobile/openapi/doc/TrashApi.md | Bin 0 -> 5722 bytes mobile/openapi/lib/api.dart | Bin 8232 -> 8259 bytes mobile/openapi/lib/api/asset_api.dart | Bin 69019 -> 69046 bytes mobile/openapi/lib/api/trash_api.dart | Bin 0 -> 3291 bytes mobile/openapi/test/asset_api_test.dart | Bin 6678 -> 6696 bytes mobile/openapi/test/trash_api_test.dart | Bin 0 -> 765 bytes 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, 601 insertions(+), 104 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 f4b53eca3688c4445e1eb3a1d3b9111d653cd969..97a04aa376cfe9e5604e05549f4989572eea48cc 100644 GIT binary patch delta 216 zcmZoWz_|Vp4*_u<3o^A7Y80Ziv{G{mN-Du(T3WFhDf!9z`e5EEr^VzgaRvs6(^x22T=pHOCRD4B*oJuyGd diff --git a/mobile/openapi/doc/AssetApi.md b/mobile/openapi/doc/AssetApi.md index d6ad217420173d58c7a0bced14f776ce2ac1e13c..dbff762d30881b55e630987447c550d7737fea55 100644 GIT binary patch delta 283 zcmaF7j%DV0mJOXu3jR4MT3WFhj>W~PC5{D|dbugesksFul_f=q#TofIDU%hrL^f|{ z;$~$As+`QrAi&BeA z@{3X(i;GiBiZ|ccRnIaxZhz?H7ynu}TOatwD2S{G#Yo=Cb;o$1#-2XL12y&zm9K$hnv zj`^Z;)HU5opmf;;U59k(rznE*FSK`zOE26{n)ShE%QqZJ;a6zB>tCE*(LGnky?4|& zRn`m`yS`KdVN( z`li~q=jDf@yP~xH3n${q|7M1^oqJY_!qcu(xZOrKp@hEVU2YM6%HV>&&Ycl*0z)-J z`MeCibxzke#5Su9Qxd6fIz@rj25U)dwr;m~ettFVANNk1tk08d{@aj>zFyaHb(A+6)G zKzd*Sa5`EfUc;Fsd}2VKb**e2Ki;r=pr=cbCrDugWla@vNwarbU;t%G4NB6gs!*v& z9PRy;ojrJk@1@V~ea!_Pk_O!HIXX0!4X45a%2F$`45dN~Qn~PWfy*UPPE^fmG&>^z zFcRESBV!+wD9JmI^F$g}D@kp0qsk?LpTEa3K4!g{{{x zifz5MAEjQQZl|sNS{QHK;IAq2(#Oc4O;Al>>Ilo)LvlS$_u!PmB-j*^Kq@Hs2GwqR zi;YGHM7tjRoYI$)85D73s|WHVe%g>EH;Za^RN6zQc4W09s~<(@@%=tB>5i=aXJl2g*$OO( z%MKoqgV+p+iC2Q~zws0F_9Xm#3)gKJ{Sw`=QzAZc(USmut<5(;OQHm{UpMH`sf|I1 z5$>*1MMKRU&=|8ykDp;yE8@S_oxk~|kt^4_ii1b~r={ufMKf%iC07I+1iKM%Hv;~! eA$iBDFLVTqbQTfhI}l^A^d(;0je#$74EzTPjc@A! literal 0 HcmV?d00001 diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index d05488f306fbb8dfb5a04845a4e2e467edd44931..fbe74168a3d98047bc28220fdbf21e234cc12f31 100644 GIT binary patch delta 21 ccmZ4CaM)pkG%HIFqko9mRdnOJ}fjmZmEt4=~sT&JhaxYr%eo)KoK4o65)VsQrC QRGG;!GTfV$HdMv}01V?bi2wiq delta 101 zcmdlsn`QQFmJM~vn;Vt2nI=1|SK~^}EhwoBDM~EPnEWnr;p7QxI5+pGB{xkD7v$ai zf5kMW$^TZUP7V+eXDdoAF3B&N93UvN`QvIS#>tJ!37hlQo@WG_Vg%KyzS(v|Wh?-Y C#VkSq diff --git a/mobile/openapi/lib/api/trash_api.dart b/mobile/openapi/lib/api/trash_api.dart new file mode 100644 index 0000000000000000000000000000000000000000..91f1d1a747a8b7843e1f326321c6ab15f4aa670d GIT binary patch literal 3291 zcmds2ZBN@U5dNNDar@ME>Y9yDQ-OAcmQiV=9b}rOs)|hFP(x~GwgamW|9xlYwXAFv zV$$G4Q~PqS&pmf>UB`7{@YS2V`8YZmoesRw5RS&5Mr{~Q;AG;#=gDw<^!*j88O;wN zk$Qip^KQp6S9ZaLq<$o%AF-HrA?2x}p-*GFq+GX)J6k3~>Wvm8z9B2+C#m)&{gE<> z=A10?nGlKpno5PvrP^#B2(C2Y+GimKWD%p%nyZb$V#cZRW5F*_O)m)d-@o;P1(C{e zFq{Svfk-GP3G3i**KwR6CQ5-PiCPR22Dc8Bm*xy5hcUyzeL(f>IB!9>y1{Ez4_S z1W9!&xdOeQFgv9xL8R!sgW--+d_(sNSr4qeE%6)Ei({>mF^@#E1w^fQU~2{_+uemk z(g*>x*!v+xh_bi}ofp+4L@%&-*;vcb7PQHig#bGfkT~-)IlrU<;+hDh4@9`iEiK2H-!cTF16%z^BHxd#i-n z404Aix@jon=vSTWFn%qr=wLc-+V=nkpmu(?-E0)MT6Kk}K%uzT3oB$*{)METWrwMe zp$YVXr*VuNU3+UMEVFAFQ0`0_IZ>_!3+p7gM~ti`GG~|}{0(9HK~iOqvk01nNe3rb zr|M9I6ngt$_;+R{opTbN;!>bWw{jK&@>z--Gc2Q@0Zk0uH#utOSr~evU&Ek-m8PoH zZP_wSp?R@?A2ZBXLLe>HdQCmrT{NIZV3ku6op&tt?JBuQcx*hTAJD#05sYv`Ylt=ZyO zV`_67c0RCC)*P)-zT!|jAM0nlU$$Q?4=sg%{3*}-(0KgVKaWX&S#!PiG0$6QiL%}2 Pyl$~Jd;jOa-8sJi?QbL#xjWS=ZA1k}R>7T+8pbb%cJ$MG3f diff --git a/mobile/openapi/test/trash_api_test.dart b/mobile/openapi/test/trash_api_test.dart new file mode 100644 index 0000000000000000000000000000000000000000..e96c254e4ff4ee9fe27df47d2cc3da3edb1221cb GIT binary patch literal 765 zcmbV}O;3X`7{~8?ipLWynL~FnTb77{nI$?Bg9lGdg$LFQ+S(S)V)or_VOwT4jqyOC z@cX|!B@DeVgy=a-FYl9O@)TuB4D-c9GK4sVWtzcb8ZYKA7Z!8mZ9$cpjYrpG&uv-7 zg+fvaMM_rV1X|u|EC|-vAUDJPo;6Y^b5=p?54zDrwuUHtYnifgP8+*Qs_fmM($-w* z^YUD9Z74T{6*iES3{@xBXTxg7QIlHm%3@|kIk~wa`HCv-d3{IlCC%5g!ikW`sbrzO zMs8|+0tSvjZFq zrk+JAC0ZHyr)K^Tb}r6m-kMe+U?a_@i?iRT-tgR7Kv$*rI3jw34(iG{&=$(lIPFB+ vzQB>YvJ=@K9chh52X}3~UKBbu0$vY~Kcs$B{yX^B!FCZH^Tz}?;nw>CEY => { + 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`,