1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2024-12-28 14:41:59 +00:00

refactor(server): trash endpoints (#6652)

* refactor(server): trash endpoints

* chore: open api

* chore: fix wrong rename
This commit is contained in:
Jason Rasmussen 2024-01-26 11:48:37 -05:00 committed by GitHub
parent 33757689fe
commit 96b7885583
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 601 additions and 104 deletions

View file

@ -22,7 +22,7 @@ class TrashService {
try {
List<String> 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<void> 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<void> restoreTrash() async {
try {
await _apiService.assetApi.restoreTrash();
await _apiService.assetApi.restoreTrashOld();
} catch (error, stack) {
_log.severe("Cannot restore trash ${error.toString()}", error, stack);
}

View file

@ -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

BIN
mobile/openapi/README.md generated

Binary file not shown.

Binary file not shown.

BIN
mobile/openapi/doc/TrashApi.md generated Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
mobile/openapi/lib/api/trash_api.dart generated Normal file

Binary file not shown.

Binary file not shown.

BIN
mobile/openapi/test/trash_api_test.dart generated Normal file

Binary file not shown.

View file

@ -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",

View file

@ -7026,7 +7026,7 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
emptyTrash: async (options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
emptyTrashOld: async (options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
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<RequestArgs> => {
restoreAssetsOld: async (bulkIdsDto: BulkIdsDto, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
// 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<RequestArgs> => {
restoreTrashOld: async (options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
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<void>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.emptyTrash(options);
async emptyTrashOld(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
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<void>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.restoreAssets(bulkIdsDto, options);
async restoreAssetsOld(bulkIdsDto: BulkIdsDto, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
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<void>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.restoreTrash(options);
async restoreTrashOld(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
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<void> {
return localVarFp.emptyTrash(options).then((request) => request(axios, basePath));
emptyTrashOld(options?: RawAxiosRequestConfig): AxiosPromise<void> {
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<void> {
return localVarFp.restoreAssets(requestParameters.bulkIdsDto, options).then((request) => request(axios, basePath));
restoreAssetsOld(requestParameters: AssetApiRestoreAssetsOldRequest, options?: RawAxiosRequestConfig): AxiosPromise<void> {
return localVarFp.restoreAssetsOld(requestParameters.bulkIdsDto, options).then((request) => request(axios, basePath));
},
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
restoreTrash(options?: RawAxiosRequestConfig): AxiosPromise<void> {
return localVarFp.restoreTrash(options).then((request) => request(axios, basePath));
restoreTrashOld(options?: RawAxiosRequestConfig): AxiosPromise<void> {
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<RequestArgs> => {
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<RequestArgs> => {
// 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<RequestArgs> => {
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<void>> {
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<void>> {
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<void>> {
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<void> {
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<void> {
return localVarFp.restoreAssets(requestParameters.bulkIdsDto, options).then((request) => request(axios, basePath));
},
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
restoreTrash(options?: RawAxiosRequestConfig): AxiosPromise<void> {
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

View file

@ -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)

View file

@ -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<void> {
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<void> {
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<void> {
const { oldParentId, newParentId } = dto;
await this.access.requirePermission(auth, Permission.ASSET_READ, oldParentId);

View file

@ -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()

View file

@ -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,
];

View file

@ -26,4 +26,5 @@ export * from './storage';
export * from './storage-template';
export * from './system-config';
export * from './tag';
export * from './trash';
export * from './user';

View file

@ -0,0 +1 @@
export * from './trash.service';

View file

@ -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<IAssetRepository>;
let jobMock: jest.Mocked<IJobRepository>;
let communicationMock: jest.Mocked<ICommunicationRepository>;
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 } },
]);
});
});
});

View file

@ -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<void> {
const { ids } = dto;
await this.access.requirePermission(auth, Permission.ASSET_RESTORE, ids);
await this.restoreAndSend(auth, ids);
}
async restore(auth: AuthDto): Promise<void> {
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<void> {
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);
}
}

View file

@ -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,
],

View file

@ -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<void> {
return this.service.restoreAll(auth, dto);
restoreAssetsOld(@Auth() auth: AuthDto, @Body() dto: BulkIdsDto): Promise<void> {
return this.trashService.restoreAssets(auth, dto);
}
/**
* @deprecated use `POST /trash/empty`
*/
@Post('trash/empty')
@HttpCode(HttpStatus.NO_CONTENT)
emptyTrash(@Auth() auth: AuthDto): Promise<void> {
return this.service.handleTrashAction(auth, TrashAction.EMPTY_ALL);
emptyTrashOld(@Auth() auth: AuthDto): Promise<void> {
return this.trashService.empty(auth);
}
/**
* @deprecated use `POST /trash/restore`
*/
@Post('trash/restore')
@HttpCode(HttpStatus.NO_CONTENT)
restoreTrash(@Auth() auth: AuthDto): Promise<void> {
return this.service.handleTrashAction(auth, TrashAction.RESTORE_ALL);
restoreTrashOld(@Auth() auth: AuthDto): Promise<void> {
return this.trashService.restore(auth);
}
@Put('stack/parent')

View file

@ -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';

View file

@ -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<void> {
return this.service.empty(auth);
}
@Post('restore')
@HttpCode(HttpStatus.NO_CONTENT)
restoreTrash(@Auth() auth: AuthDto): Promise<void> {
return this.service.restore(auth);
}
@Post('restore/assets')
@HttpCode(HttpStatus.NO_CONTENT)
restoreAssets(@Auth() auth: AuthDto, @Body() dto: BulkIdsDto): Promise<void> {
return this.service.restoreAssets(auth, dto);
}
}

View file

@ -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<string, unknown>) {

View file

@ -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({

View file

@ -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`,