fix: empty and restore over 1,000 items (#12751)

This commit is contained in:
Jason Rasmussen 2024-09-18 09:57:52 -04:00 committed by GitHub
parent 4f25cec6df
commit 6740c67ed8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
39 changed files with 540 additions and 145 deletions

View File

@ -34,8 +34,11 @@ describe('/trash', () => {
const before = await getAssetInfo({ id: assetId }, { headers: asBearerAuth(admin.accessToken) }); const before = await getAssetInfo({ id: assetId }, { headers: asBearerAuth(admin.accessToken) });
expect(before).toStrictEqual(expect.objectContaining({ id: assetId, isTrashed: true })); expect(before).toStrictEqual(expect.objectContaining({ id: assetId, isTrashed: true }));
const { status } = await request(app).post('/trash/empty').set('Authorization', `Bearer ${admin.accessToken}`); const { status, body } = await request(app)
expect(status).toBe(204); .post('/trash/empty')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual({ count: 1 });
await utils.waitForWebsocketEvent({ event: 'assetDelete', id: assetId }); await utils.waitForWebsocketEvent({ event: 'assetDelete', id: assetId });
@ -51,8 +54,11 @@ describe('/trash', () => {
const before = await getAssetInfo({ id: assetId }, { headers: asBearerAuth(admin.accessToken) }); const before = await getAssetInfo({ id: assetId }, { headers: asBearerAuth(admin.accessToken) });
expect(before).toStrictEqual(expect.objectContaining({ id: assetId, isTrashed: true, isArchived: true })); expect(before).toStrictEqual(expect.objectContaining({ id: assetId, isTrashed: true, isArchived: true }));
const { status } = await request(app).post('/trash/empty').set('Authorization', `Bearer ${admin.accessToken}`); const { status, body } = await request(app)
expect(status).toBe(204); .post('/trash/empty')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual({ count: 1 });
await utils.waitForWebsocketEvent({ event: 'assetDelete', id: assetId }); await utils.waitForWebsocketEvent({ event: 'assetDelete', id: assetId });
@ -76,8 +82,11 @@ describe('/trash', () => {
const before = await getAssetInfo({ id: assetId }, { headers: asBearerAuth(admin.accessToken) }); const before = await getAssetInfo({ id: assetId }, { headers: asBearerAuth(admin.accessToken) });
expect(before).toStrictEqual(expect.objectContaining({ id: assetId, isTrashed: true })); expect(before).toStrictEqual(expect.objectContaining({ id: assetId, isTrashed: true }));
const { status } = await request(app).post('/trash/restore').set('Authorization', `Bearer ${admin.accessToken}`); const { status, body } = await request(app)
expect(status).toBe(204); .post('/trash/restore')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual({ count: 1 });
const after = await getAssetInfo({ id: assetId }, { headers: asBearerAuth(admin.accessToken) }); const after = await getAssetInfo({ id: assetId }, { headers: asBearerAuth(admin.accessToken) });
expect(after).toStrictEqual(expect.objectContaining({ id: assetId, isTrashed: false })); expect(after).toStrictEqual(expect.objectContaining({ id: assetId, isTrashed: false }));
@ -99,11 +108,12 @@ describe('/trash', () => {
const before = await utils.getAssetInfo(admin.accessToken, assetId); const before = await utils.getAssetInfo(admin.accessToken, assetId);
expect(before.isTrashed).toBe(true); expect(before.isTrashed).toBe(true);
const { status } = await request(app) const { status, body } = await request(app)
.post('/trash/restore/assets') .post('/trash/restore/assets')
.set('Authorization', `Bearer ${admin.accessToken}`) .set('Authorization', `Bearer ${admin.accessToken}`)
.send({ ids: [assetId] }); .send({ ids: [assetId] });
expect(status).toBe(204); expect(status).toBe(200);
expect(body).toEqual({ count: 1 });
const after = await utils.getAssetInfo(admin.accessToken, assetId); const after = await utils.getAssetInfo(admin.accessToken, assetId);
expect(after.isTrashed).toBe(false); expect(after.isTrashed).toBe(false);

View File

@ -452,6 +452,7 @@ Class | Method | HTTP request | Description
- [ToneMapping](doc//ToneMapping.md) - [ToneMapping](doc//ToneMapping.md)
- [TranscodeHWAccel](doc//TranscodeHWAccel.md) - [TranscodeHWAccel](doc//TranscodeHWAccel.md)
- [TranscodePolicy](doc//TranscodePolicy.md) - [TranscodePolicy](doc//TranscodePolicy.md)
- [TrashResponseDto](doc//TrashResponseDto.md)
- [UpdateAlbumDto](doc//UpdateAlbumDto.md) - [UpdateAlbumDto](doc//UpdateAlbumDto.md)
- [UpdateAlbumUserDto](doc//UpdateAlbumUserDto.md) - [UpdateAlbumUserDto](doc//UpdateAlbumUserDto.md)
- [UpdateAssetDto](doc//UpdateAssetDto.md) - [UpdateAssetDto](doc//UpdateAssetDto.md)

View File

@ -265,6 +265,7 @@ part 'model/time_bucket_size.dart';
part 'model/tone_mapping.dart'; part 'model/tone_mapping.dart';
part 'model/transcode_hw_accel.dart'; part 'model/transcode_hw_accel.dart';
part 'model/transcode_policy.dart'; part 'model/transcode_policy.dart';
part 'model/trash_response_dto.dart';
part 'model/update_album_dto.dart'; part 'model/update_album_dto.dart';
part 'model/update_album_user_dto.dart'; part 'model/update_album_user_dto.dart';
part 'model/update_asset_dto.dart'; part 'model/update_asset_dto.dart';

View File

@ -42,11 +42,19 @@ class TrashApi {
); );
} }
Future<void> emptyTrash() async { Future<TrashResponseDto?> emptyTrash() async {
final response = await emptyTrashWithHttpInfo(); final response = await emptyTrashWithHttpInfo();
if (response.statusCode >= HttpStatus.badRequest) { if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response)); throw ApiException(response.statusCode, await _decodeBodyBytes(response));
} }
// When a remote server returns no body with a status of 204, we shall not decode it.
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'TrashResponseDto',) as TrashResponseDto;
}
return null;
} }
/// Performs an HTTP 'POST /trash/restore/assets' operation and returns the [Response]. /// Performs an HTTP 'POST /trash/restore/assets' operation and returns the [Response].
@ -81,11 +89,19 @@ class TrashApi {
/// Parameters: /// Parameters:
/// ///
/// * [BulkIdsDto] bulkIdsDto (required): /// * [BulkIdsDto] bulkIdsDto (required):
Future<void> restoreAssets(BulkIdsDto bulkIdsDto,) async { Future<TrashResponseDto?> restoreAssets(BulkIdsDto bulkIdsDto,) async {
final response = await restoreAssetsWithHttpInfo(bulkIdsDto,); final response = await restoreAssetsWithHttpInfo(bulkIdsDto,);
if (response.statusCode >= HttpStatus.badRequest) { if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response)); throw ApiException(response.statusCode, await _decodeBodyBytes(response));
} }
// When a remote server returns no body with a status of 204, we shall not decode it.
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'TrashResponseDto',) as TrashResponseDto;
}
return null;
} }
/// Performs an HTTP 'POST /trash/restore' operation and returns the [Response]. /// Performs an HTTP 'POST /trash/restore' operation and returns the [Response].
@ -114,10 +130,18 @@ class TrashApi {
); );
} }
Future<void> restoreTrash() async { Future<TrashResponseDto?> restoreTrash() async {
final response = await restoreTrashWithHttpInfo(); final response = await restoreTrashWithHttpInfo();
if (response.statusCode >= HttpStatus.badRequest) { if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response)); throw ApiException(response.statusCode, await _decodeBodyBytes(response));
} }
// When a remote server returns no body with a status of 204, we shall not decode it.
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'TrashResponseDto',) as TrashResponseDto;
}
return null;
} }
} }

View File

@ -585,6 +585,8 @@ class ApiClient {
return TranscodeHWAccelTypeTransformer().decode(value); return TranscodeHWAccelTypeTransformer().decode(value);
case 'TranscodePolicy': case 'TranscodePolicy':
return TranscodePolicyTypeTransformer().decode(value); return TranscodePolicyTypeTransformer().decode(value);
case 'TrashResponseDto':
return TrashResponseDto.fromJson(value);
case 'UpdateAlbumDto': case 'UpdateAlbumDto':
return UpdateAlbumDto.fromJson(value); return UpdateAlbumDto.fromJson(value);
case 'UpdateAlbumUserDto': case 'UpdateAlbumUserDto':

View File

@ -0,0 +1,98 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class TrashResponseDto {
/// Returns a new [TrashResponseDto] instance.
TrashResponseDto({
required this.count,
});
int count;
@override
bool operator ==(Object other) => identical(this, other) || other is TrashResponseDto &&
other.count == count;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(count.hashCode);
@override
String toString() => 'TrashResponseDto[count=$count]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'count'] = this.count;
return json;
}
/// Returns a new [TrashResponseDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static TrashResponseDto? fromJson(dynamic value) {
if (value is Map) {
final json = value.cast<String, dynamic>();
return TrashResponseDto(
count: mapValueOfType<int>(json, r'count')!,
);
}
return null;
}
static List<TrashResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <TrashResponseDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = TrashResponseDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, TrashResponseDto> mapFromJson(dynamic json) {
final map = <String, TrashResponseDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = TrashResponseDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of TrashResponseDto-objects as value to a dart map
static Map<String, List<TrashResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<TrashResponseDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = TrashResponseDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'count',
};
}

View File

@ -6839,7 +6839,14 @@
"operationId": "emptyTrash", "operationId": "emptyTrash",
"parameters": [], "parameters": [],
"responses": { "responses": {
"204": { "200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TrashResponseDto"
}
}
},
"description": "" "description": ""
} }
}, },
@ -6864,7 +6871,14 @@
"operationId": "restoreTrash", "operationId": "restoreTrash",
"parameters": [], "parameters": [],
"responses": { "responses": {
"204": { "200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TrashResponseDto"
}
}
},
"description": "" "description": ""
} }
}, },
@ -6899,7 +6913,14 @@
"required": true "required": true
}, },
"responses": { "responses": {
"204": { "200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TrashResponseDto"
}
}
},
"description": "" "description": ""
} }
}, },
@ -12254,6 +12275,17 @@
], ],
"type": "string" "type": "string"
}, },
"TrashResponseDto": {
"properties": {
"count": {
"type": "integer"
}
},
"required": [
"count"
],
"type": "object"
},
"UpdateAlbumDto": { "UpdateAlbumDto": {
"properties": { "properties": {
"albumName": { "albumName": {

View File

@ -1246,6 +1246,9 @@ export type TimeBucketResponseDto = {
count: number; count: number;
timeBucket: string; timeBucket: string;
}; };
export type TrashResponseDto = {
count: number;
};
export type UserUpdateMeDto = { export type UserUpdateMeDto = {
email?: string; email?: string;
name?: string; name?: string;
@ -3073,13 +3076,19 @@ export function getTimeBuckets({ albumId, isArchived, isFavorite, isTrashed, key
})); }));
} }
export function emptyTrash(opts?: Oazapfts.RequestOpts) { export function emptyTrash(opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchText("/trash/empty", { return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
data: TrashResponseDto;
}>("/trash/empty", {
...opts, ...opts,
method: "POST" method: "POST"
})); }));
} }
export function restoreTrash(opts?: Oazapfts.RequestOpts) { export function restoreTrash(opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchText("/trash/restore", { return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
data: TrashResponseDto;
}>("/trash/restore", {
...opts, ...opts,
method: "POST" method: "POST"
})); }));
@ -3087,7 +3096,10 @@ export function restoreTrash(opts?: Oazapfts.RequestOpts) {
export function restoreAssets({ bulkIdsDto }: { export function restoreAssets({ bulkIdsDto }: {
bulkIdsDto: BulkIdsDto; bulkIdsDto: BulkIdsDto;
}, opts?: Oazapfts.RequestOpts) { }, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchText("/trash/restore/assets", oazapfts.json({ return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
data: TrashResponseDto;
}>("/trash/restore/assets", oazapfts.json({
...opts, ...opts,
method: "POST", method: "POST",
body: bulkIdsDto body: bulkIdsDto

View File

@ -2,6 +2,7 @@ import { Body, Controller, HttpCode, HttpStatus, Post } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiTags } from '@nestjs/swagger';
import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { TrashResponseDto } from 'src/dtos/trash.dto';
import { Permission } from 'src/enum'; import { Permission } from 'src/enum';
import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { Auth, Authenticated } from 'src/middleware/auth.guard';
import { TrashService } from 'src/services/trash.service'; import { TrashService } from 'src/services/trash.service';
@ -12,23 +13,23 @@ export class TrashController {
constructor(private service: TrashService) {} constructor(private service: TrashService) {}
@Post('empty') @Post('empty')
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.OK)
@Authenticated({ permission: Permission.ASSET_DELETE }) @Authenticated({ permission: Permission.ASSET_DELETE })
emptyTrash(@Auth() auth: AuthDto): Promise<void> { emptyTrash(@Auth() auth: AuthDto): Promise<TrashResponseDto> {
return this.service.empty(auth); return this.service.empty(auth);
} }
@Post('restore') @Post('restore')
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.OK)
@Authenticated({ permission: Permission.ASSET_DELETE }) @Authenticated({ permission: Permission.ASSET_DELETE })
restoreTrash(@Auth() auth: AuthDto): Promise<void> { restoreTrash(@Auth() auth: AuthDto): Promise<TrashResponseDto> {
return this.service.restore(auth); return this.service.restore(auth);
} }
@Post('restore/assets') @Post('restore/assets')
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.OK)
@Authenticated({ permission: Permission.ASSET_DELETE }) @Authenticated({ permission: Permission.ASSET_DELETE })
restoreAssets(@Auth() auth: AuthDto, @Body() dto: BulkIdsDto): Promise<void> { restoreAssets(@Auth() auth: AuthDto, @Body() dto: BulkIdsDto): Promise<TrashResponseDto> {
return this.service.restoreAssets(auth, dto); return this.service.restoreAssets(auth, dto);
} }
} }

View File

@ -0,0 +1,6 @@
import { ApiProperty } from '@nestjs/swagger';
export class TrashResponseDto {
@ApiProperty({ type: 'integer' })
count!: number;
}

View File

@ -10,7 +10,7 @@ import { SmartSearchEntity } from 'src/entities/smart-search.entity';
import { StackEntity } from 'src/entities/stack.entity'; import { StackEntity } from 'src/entities/stack.entity';
import { TagEntity } from 'src/entities/tag.entity'; import { TagEntity } from 'src/entities/tag.entity';
import { UserEntity } from 'src/entities/user.entity'; import { UserEntity } from 'src/entities/user.entity';
import { AssetType } from 'src/enum'; import { AssetStatus, AssetType } from 'src/enum';
import { import {
Column, Column,
CreateDateColumn, CreateDateColumn,
@ -70,6 +70,9 @@ export class AssetEntity {
@Column() @Column()
type!: AssetType; type!: AssetType;
@Column({ type: 'enum', enum: AssetStatus, default: AssetStatus.ACTIVE })
status!: AssetStatus;
@Column() @Column()
originalPath!: string; originalPath!: string;

View File

@ -182,6 +182,12 @@ export enum UserStatus {
DELETED = 'deleted', DELETED = 'deleted',
} }
export enum AssetStatus {
ACTIVE = 'active',
TRASHED = 'trashed',
DELETED = 'deleted',
}
export enum SourceType { export enum SourceType {
MACHINE_LEARNING = 'machine-learning', MACHINE_LEARNING = 'machine-learning',
EXIF = 'exif', EXIF = 'exif',

View File

@ -1,7 +1,7 @@
import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity'; import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity';
import { AssetEntity } from 'src/entities/asset.entity'; import { AssetEntity } from 'src/entities/asset.entity';
import { ExifEntity } from 'src/entities/exif.entity'; import { ExifEntity } from 'src/entities/exif.entity';
import { AssetFileType, AssetOrder, AssetType } from 'src/enum'; import { AssetFileType, AssetOrder, AssetStatus, AssetType } from 'src/enum';
import { AssetSearchOptions, SearchExploreItem } from 'src/interfaces/search.interface'; import { AssetSearchOptions, SearchExploreItem } from 'src/interfaces/search.interface';
import { Paginated, PaginationOptions } from 'src/utils/pagination'; import { Paginated, PaginationOptions } from 'src/utils/pagination';
import { FindOptionsOrder, FindOptionsRelations, FindOptionsSelect } from 'typeorm'; import { FindOptionsOrder, FindOptionsRelations, FindOptionsSelect } from 'typeorm';
@ -56,6 +56,7 @@ export interface AssetBuilderOptions {
userIds?: string[]; userIds?: string[];
withStacked?: boolean; withStacked?: boolean;
exifInfo?: boolean; exifInfo?: boolean;
status?: AssetStatus;
assetType?: AssetType; assetType?: AssetType;
} }
@ -185,8 +186,6 @@ export interface IAssetRepository {
updateDuplicates(options: AssetUpdateDuplicateOptions): Promise<void>; updateDuplicates(options: AssetUpdateDuplicateOptions): Promise<void>;
update(asset: AssetUpdateOptions): Promise<void>; update(asset: AssetUpdateOptions): Promise<void>;
remove(asset: AssetEntity): Promise<void>; remove(asset: AssetEntity): Promise<void>;
softDeleteAll(ids: string[]): Promise<void>;
restoreAll(ids: string[]): Promise<void>;
findLivePhotoMatch(options: LivePhotoSearchOptions): Promise<AssetEntity | null>; findLivePhotoMatch(options: LivePhotoSearchOptions): Promise<AssetEntity | null>;
getStatistics(ownerId: string, options: AssetStatsOptions): Promise<AssetStats>; getStatistics(ownerId: string, options: AssetStatsOptions): Promise<AssetStats>;
getTimeBuckets(options: TimeBucketOptions): Promise<TimeBucketItem[]>; getTimeBuckets(options: TimeBucketOptions): Promise<TimeBucketItem[]>;

View File

@ -27,6 +27,7 @@ type EmitEventMap = {
// asset bulk events // asset bulk events
'assets.trash': [{ assetIds: string[]; userId: string }]; 'assets.trash': [{ assetIds: string[]; userId: string }];
'assets.delete': [{ assetIds: string[]; userId: string }];
'assets.restore': [{ assetIds: string[]; userId: string }]; 'assets.restore': [{ assetIds: string[]; userId: string }];
// session events // session events

View File

@ -93,6 +93,8 @@ export enum JobName {
QUEUE_SMART_SEARCH = 'queue-smart-search', QUEUE_SMART_SEARCH = 'queue-smart-search',
SMART_SEARCH = 'smart-search', SMART_SEARCH = 'smart-search',
QUEUE_TRASH_EMPTY = 'queue-trash-empty',
// duplicate detection // duplicate detection
QUEUE_DUPLICATE_DETECTION = 'queue-duplicate-detection', QUEUE_DUPLICATE_DETECTION = 'queue-duplicate-detection',
DUPLICATE_DETECTION = 'duplicate-detection', DUPLICATE_DETECTION = 'duplicate-detection',
@ -253,6 +255,7 @@ export type JobItem =
// Smart Search // Smart Search
| { name: JobName.QUEUE_SMART_SEARCH; data: IBaseJob } | { name: JobName.QUEUE_SMART_SEARCH; data: IBaseJob }
| { name: JobName.SMART_SEARCH; data: IEntityJob } | { name: JobName.SMART_SEARCH; data: IEntityJob }
| { name: JobName.QUEUE_TRASH_EMPTY; data?: IBaseJob }
// Duplicate Detection // Duplicate Detection
| { name: JobName.QUEUE_DUPLICATE_DETECTION; data: IBaseJob } | { name: JobName.QUEUE_DUPLICATE_DETECTION; data: IBaseJob }

View File

@ -1,7 +1,7 @@
import { AssetFaceEntity } from 'src/entities/asset-face.entity'; import { AssetFaceEntity } from 'src/entities/asset-face.entity';
import { AssetEntity } from 'src/entities/asset.entity'; import { AssetEntity } from 'src/entities/asset.entity';
import { GeodataPlacesEntity } from 'src/entities/geodata-places.entity'; import { GeodataPlacesEntity } from 'src/entities/geodata-places.entity';
import { AssetType } from 'src/enum'; import { AssetStatus, AssetType } from 'src/enum';
import { Paginated } from 'src/utils/pagination'; import { Paginated } from 'src/utils/pagination';
export const ISearchRepository = 'ISearchRepository'; export const ISearchRepository = 'ISearchRepository';
@ -61,6 +61,7 @@ export interface SearchStatusOptions {
isVisible?: boolean; isVisible?: boolean;
isNotInAlbum?: boolean; isNotInAlbum?: boolean;
type?: AssetType; type?: AssetType;
status?: AssetStatus;
withArchived?: boolean; withArchived?: boolean;
withDeleted?: boolean; withDeleted?: boolean;
} }

View File

@ -0,0 +1,10 @@
import { Paginated, PaginationOptions } from 'src/utils/pagination';
export const ITrashRepository = 'ITrashRepository';
export interface ITrashRepository {
empty(userId: string): Promise<number>;
restore(userId: string): Promise<number>;
restoreAll(assetIds: string[]): Promise<number>;
getDeletedIds(pagination: PaginationOptions): Paginated<string>;
}

View File

@ -0,0 +1,16 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class AddAssetStatus1726593009549 implements MigrationInterface {
name = 'AddAssetStatus1726593009549'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TYPE "assets_status_enum" AS ENUM('active', 'trashed', 'deleted')`);
await queryRunner.query(`ALTER TABLE "assets" ADD "status" "assets_status_enum" NOT NULL DEFAULT 'active'`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "assets" DROP COLUMN "status"`);
await queryRunner.query(`DROP TYPE "assets_status_enum"`);
}
}

View File

@ -8,6 +8,7 @@ SELECT
"entity"."libraryId" AS "entity_libraryId", "entity"."libraryId" AS "entity_libraryId",
"entity"."deviceId" AS "entity_deviceId", "entity"."deviceId" AS "entity_deviceId",
"entity"."type" AS "entity_type", "entity"."type" AS "entity_type",
"entity"."status" AS "entity_status",
"entity"."originalPath" AS "entity_originalPath", "entity"."originalPath" AS "entity_originalPath",
"entity"."thumbhash" AS "entity_thumbhash", "entity"."thumbhash" AS "entity_thumbhash",
"entity"."encodedVideoPath" AS "entity_encodedVideoPath", "entity"."encodedVideoPath" AS "entity_encodedVideoPath",
@ -96,6 +97,7 @@ SELECT
"AssetEntity"."libraryId" AS "AssetEntity_libraryId", "AssetEntity"."libraryId" AS "AssetEntity_libraryId",
"AssetEntity"."deviceId" AS "AssetEntity_deviceId", "AssetEntity"."deviceId" AS "AssetEntity_deviceId",
"AssetEntity"."type" AS "AssetEntity_type", "AssetEntity"."type" AS "AssetEntity_type",
"AssetEntity"."status" AS "AssetEntity_status",
"AssetEntity"."originalPath" AS "AssetEntity_originalPath", "AssetEntity"."originalPath" AS "AssetEntity_originalPath",
"AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash",
"AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath",
@ -130,6 +132,7 @@ SELECT
"AssetEntity"."libraryId" AS "AssetEntity_libraryId", "AssetEntity"."libraryId" AS "AssetEntity_libraryId",
"AssetEntity"."deviceId" AS "AssetEntity_deviceId", "AssetEntity"."deviceId" AS "AssetEntity_deviceId",
"AssetEntity"."type" AS "AssetEntity_type", "AssetEntity"."type" AS "AssetEntity_type",
"AssetEntity"."status" AS "AssetEntity_status",
"AssetEntity"."originalPath" AS "AssetEntity_originalPath", "AssetEntity"."originalPath" AS "AssetEntity_originalPath",
"AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash",
"AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath",
@ -218,6 +221,7 @@ SELECT
"bd93d5747511a4dad4923546c51365bf1a803774"."libraryId" AS "bd93d5747511a4dad4923546c51365bf1a803774_libraryId", "bd93d5747511a4dad4923546c51365bf1a803774"."libraryId" AS "bd93d5747511a4dad4923546c51365bf1a803774_libraryId",
"bd93d5747511a4dad4923546c51365bf1a803774"."deviceId" AS "bd93d5747511a4dad4923546c51365bf1a803774_deviceId", "bd93d5747511a4dad4923546c51365bf1a803774"."deviceId" AS "bd93d5747511a4dad4923546c51365bf1a803774_deviceId",
"bd93d5747511a4dad4923546c51365bf1a803774"."type" AS "bd93d5747511a4dad4923546c51365bf1a803774_type", "bd93d5747511a4dad4923546c51365bf1a803774"."type" AS "bd93d5747511a4dad4923546c51365bf1a803774_type",
"bd93d5747511a4dad4923546c51365bf1a803774"."status" AS "bd93d5747511a4dad4923546c51365bf1a803774_status",
"bd93d5747511a4dad4923546c51365bf1a803774"."originalPath" AS "bd93d5747511a4dad4923546c51365bf1a803774_originalPath", "bd93d5747511a4dad4923546c51365bf1a803774"."originalPath" AS "bd93d5747511a4dad4923546c51365bf1a803774_originalPath",
"bd93d5747511a4dad4923546c51365bf1a803774"."thumbhash" AS "bd93d5747511a4dad4923546c51365bf1a803774_thumbhash", "bd93d5747511a4dad4923546c51365bf1a803774"."thumbhash" AS "bd93d5747511a4dad4923546c51365bf1a803774_thumbhash",
"bd93d5747511a4dad4923546c51365bf1a803774"."encodedVideoPath" AS "bd93d5747511a4dad4923546c51365bf1a803774_encodedVideoPath", "bd93d5747511a4dad4923546c51365bf1a803774"."encodedVideoPath" AS "bd93d5747511a4dad4923546c51365bf1a803774_encodedVideoPath",
@ -305,6 +309,7 @@ FROM
"AssetEntity"."libraryId" AS "AssetEntity_libraryId", "AssetEntity"."libraryId" AS "AssetEntity_libraryId",
"AssetEntity"."deviceId" AS "AssetEntity_deviceId", "AssetEntity"."deviceId" AS "AssetEntity_deviceId",
"AssetEntity"."type" AS "AssetEntity_type", "AssetEntity"."type" AS "AssetEntity_type",
"AssetEntity"."status" AS "AssetEntity_status",
"AssetEntity"."originalPath" AS "AssetEntity_originalPath", "AssetEntity"."originalPath" AS "AssetEntity_originalPath",
"AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash",
"AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath",
@ -402,6 +407,7 @@ SELECT
"AssetEntity"."libraryId" AS "AssetEntity_libraryId", "AssetEntity"."libraryId" AS "AssetEntity_libraryId",
"AssetEntity"."deviceId" AS "AssetEntity_deviceId", "AssetEntity"."deviceId" AS "AssetEntity_deviceId",
"AssetEntity"."type" AS "AssetEntity_type", "AssetEntity"."type" AS "AssetEntity_type",
"AssetEntity"."status" AS "AssetEntity_status",
"AssetEntity"."originalPath" AS "AssetEntity_originalPath", "AssetEntity"."originalPath" AS "AssetEntity_originalPath",
"AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash",
"AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath",
@ -455,6 +461,7 @@ SELECT
"AssetEntity"."libraryId" AS "AssetEntity_libraryId", "AssetEntity"."libraryId" AS "AssetEntity_libraryId",
"AssetEntity"."deviceId" AS "AssetEntity_deviceId", "AssetEntity"."deviceId" AS "AssetEntity_deviceId",
"AssetEntity"."type" AS "AssetEntity_type", "AssetEntity"."type" AS "AssetEntity_type",
"AssetEntity"."status" AS "AssetEntity_status",
"AssetEntity"."originalPath" AS "AssetEntity_originalPath", "AssetEntity"."originalPath" AS "AssetEntity_originalPath",
"AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash",
"AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath",
@ -527,6 +534,7 @@ SELECT
"AssetEntity"."libraryId" AS "AssetEntity_libraryId", "AssetEntity"."libraryId" AS "AssetEntity_libraryId",
"AssetEntity"."deviceId" AS "AssetEntity_deviceId", "AssetEntity"."deviceId" AS "AssetEntity_deviceId",
"AssetEntity"."type" AS "AssetEntity_type", "AssetEntity"."type" AS "AssetEntity_type",
"AssetEntity"."status" AS "AssetEntity_status",
"AssetEntity"."originalPath" AS "AssetEntity_originalPath", "AssetEntity"."originalPath" AS "AssetEntity_originalPath",
"AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash",
"AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath",
@ -581,6 +589,7 @@ SELECT
"asset"."libraryId" AS "asset_libraryId", "asset"."libraryId" AS "asset_libraryId",
"asset"."deviceId" AS "asset_deviceId", "asset"."deviceId" AS "asset_deviceId",
"asset"."type" AS "asset_type", "asset"."type" AS "asset_type",
"asset"."status" AS "asset_status",
"asset"."originalPath" AS "asset_originalPath", "asset"."originalPath" AS "asset_originalPath",
"asset"."thumbhash" AS "asset_thumbhash", "asset"."thumbhash" AS "asset_thumbhash",
"asset"."encodedVideoPath" AS "asset_encodedVideoPath", "asset"."encodedVideoPath" AS "asset_encodedVideoPath",
@ -640,6 +649,7 @@ SELECT
"stackedAssets"."libraryId" AS "stackedAssets_libraryId", "stackedAssets"."libraryId" AS "stackedAssets_libraryId",
"stackedAssets"."deviceId" AS "stackedAssets_deviceId", "stackedAssets"."deviceId" AS "stackedAssets_deviceId",
"stackedAssets"."type" AS "stackedAssets_type", "stackedAssets"."type" AS "stackedAssets_type",
"stackedAssets"."status" AS "stackedAssets_status",
"stackedAssets"."originalPath" AS "stackedAssets_originalPath", "stackedAssets"."originalPath" AS "stackedAssets_originalPath",
"stackedAssets"."thumbhash" AS "stackedAssets_thumbhash", "stackedAssets"."thumbhash" AS "stackedAssets_thumbhash",
"stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath", "stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath",
@ -719,6 +729,7 @@ SELECT
"asset"."libraryId" AS "asset_libraryId", "asset"."libraryId" AS "asset_libraryId",
"asset"."deviceId" AS "asset_deviceId", "asset"."deviceId" AS "asset_deviceId",
"asset"."type" AS "asset_type", "asset"."type" AS "asset_type",
"asset"."status" AS "asset_status",
"asset"."originalPath" AS "asset_originalPath", "asset"."originalPath" AS "asset_originalPath",
"asset"."thumbhash" AS "asset_thumbhash", "asset"."thumbhash" AS "asset_thumbhash",
"asset"."encodedVideoPath" AS "asset_encodedVideoPath", "asset"."encodedVideoPath" AS "asset_encodedVideoPath",
@ -778,6 +789,7 @@ SELECT
"stackedAssets"."libraryId" AS "stackedAssets_libraryId", "stackedAssets"."libraryId" AS "stackedAssets_libraryId",
"stackedAssets"."deviceId" AS "stackedAssets_deviceId", "stackedAssets"."deviceId" AS "stackedAssets_deviceId",
"stackedAssets"."type" AS "stackedAssets_type", "stackedAssets"."type" AS "stackedAssets_type",
"stackedAssets"."status" AS "stackedAssets_status",
"stackedAssets"."originalPath" AS "stackedAssets_originalPath", "stackedAssets"."originalPath" AS "stackedAssets_originalPath",
"stackedAssets"."thumbhash" AS "stackedAssets_thumbhash", "stackedAssets"."thumbhash" AS "stackedAssets_thumbhash",
"stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath", "stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath",
@ -833,6 +845,7 @@ SELECT
"asset"."libraryId" AS "asset_libraryId", "asset"."libraryId" AS "asset_libraryId",
"asset"."deviceId" AS "asset_deviceId", "asset"."deviceId" AS "asset_deviceId",
"asset"."type" AS "asset_type", "asset"."type" AS "asset_type",
"asset"."status" AS "asset_status",
"asset"."originalPath" AS "asset_originalPath", "asset"."originalPath" AS "asset_originalPath",
"asset"."thumbhash" AS "asset_thumbhash", "asset"."thumbhash" AS "asset_thumbhash",
"asset"."encodedVideoPath" AS "asset_encodedVideoPath", "asset"."encodedVideoPath" AS "asset_encodedVideoPath",
@ -892,6 +905,7 @@ SELECT
"stackedAssets"."libraryId" AS "stackedAssets_libraryId", "stackedAssets"."libraryId" AS "stackedAssets_libraryId",
"stackedAssets"."deviceId" AS "stackedAssets_deviceId", "stackedAssets"."deviceId" AS "stackedAssets_deviceId",
"stackedAssets"."type" AS "stackedAssets_type", "stackedAssets"."type" AS "stackedAssets_type",
"stackedAssets"."status" AS "stackedAssets_status",
"stackedAssets"."originalPath" AS "stackedAssets_originalPath", "stackedAssets"."originalPath" AS "stackedAssets_originalPath",
"stackedAssets"."thumbhash" AS "stackedAssets_thumbhash", "stackedAssets"."thumbhash" AS "stackedAssets_thumbhash",
"stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath", "stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath",
@ -997,6 +1011,7 @@ SELECT
"asset"."libraryId" AS "asset_libraryId", "asset"."libraryId" AS "asset_libraryId",
"asset"."deviceId" AS "asset_deviceId", "asset"."deviceId" AS "asset_deviceId",
"asset"."type" AS "asset_type", "asset"."type" AS "asset_type",
"asset"."status" AS "asset_status",
"asset"."originalPath" AS "asset_originalPath", "asset"."originalPath" AS "asset_originalPath",
"asset"."thumbhash" AS "asset_thumbhash", "asset"."thumbhash" AS "asset_thumbhash",
"asset"."encodedVideoPath" AS "asset_encodedVideoPath", "asset"."encodedVideoPath" AS "asset_encodedVideoPath",
@ -1072,6 +1087,7 @@ SELECT
"asset"."libraryId" AS "asset_libraryId", "asset"."libraryId" AS "asset_libraryId",
"asset"."deviceId" AS "asset_deviceId", "asset"."deviceId" AS "asset_deviceId",
"asset"."type" AS "asset_type", "asset"."type" AS "asset_type",
"asset"."status" AS "asset_status",
"asset"."originalPath" AS "asset_originalPath", "asset"."originalPath" AS "asset_originalPath",
"asset"."thumbhash" AS "asset_thumbhash", "asset"."thumbhash" AS "asset_thumbhash",
"asset"."encodedVideoPath" AS "asset_encodedVideoPath", "asset"."encodedVideoPath" AS "asset_encodedVideoPath",

View File

@ -159,6 +159,7 @@ FROM
"AssetFaceEntity__AssetFaceEntity_asset"."libraryId" AS "AssetFaceEntity__AssetFaceEntity_asset_libraryId", "AssetFaceEntity__AssetFaceEntity_asset"."libraryId" AS "AssetFaceEntity__AssetFaceEntity_asset_libraryId",
"AssetFaceEntity__AssetFaceEntity_asset"."deviceId" AS "AssetFaceEntity__AssetFaceEntity_asset_deviceId", "AssetFaceEntity__AssetFaceEntity_asset"."deviceId" AS "AssetFaceEntity__AssetFaceEntity_asset_deviceId",
"AssetFaceEntity__AssetFaceEntity_asset"."type" AS "AssetFaceEntity__AssetFaceEntity_asset_type", "AssetFaceEntity__AssetFaceEntity_asset"."type" AS "AssetFaceEntity__AssetFaceEntity_asset_type",
"AssetFaceEntity__AssetFaceEntity_asset"."status" AS "AssetFaceEntity__AssetFaceEntity_asset_status",
"AssetFaceEntity__AssetFaceEntity_asset"."originalPath" AS "AssetFaceEntity__AssetFaceEntity_asset_originalPath", "AssetFaceEntity__AssetFaceEntity_asset"."originalPath" AS "AssetFaceEntity__AssetFaceEntity_asset_originalPath",
"AssetFaceEntity__AssetFaceEntity_asset"."thumbhash" AS "AssetFaceEntity__AssetFaceEntity_asset_thumbhash", "AssetFaceEntity__AssetFaceEntity_asset"."thumbhash" AS "AssetFaceEntity__AssetFaceEntity_asset_thumbhash",
"AssetFaceEntity__AssetFaceEntity_asset"."encodedVideoPath" AS "AssetFaceEntity__AssetFaceEntity_asset_encodedVideoPath", "AssetFaceEntity__AssetFaceEntity_asset"."encodedVideoPath" AS "AssetFaceEntity__AssetFaceEntity_asset_encodedVideoPath",
@ -260,6 +261,7 @@ FROM
"AssetEntity"."libraryId" AS "AssetEntity_libraryId", "AssetEntity"."libraryId" AS "AssetEntity_libraryId",
"AssetEntity"."deviceId" AS "AssetEntity_deviceId", "AssetEntity"."deviceId" AS "AssetEntity_deviceId",
"AssetEntity"."type" AS "AssetEntity_type", "AssetEntity"."type" AS "AssetEntity_type",
"AssetEntity"."status" AS "AssetEntity_status",
"AssetEntity"."originalPath" AS "AssetEntity_originalPath", "AssetEntity"."originalPath" AS "AssetEntity_originalPath",
"AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash",
"AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath",
@ -391,6 +393,7 @@ SELECT
"AssetFaceEntity__AssetFaceEntity_asset"."libraryId" AS "AssetFaceEntity__AssetFaceEntity_asset_libraryId", "AssetFaceEntity__AssetFaceEntity_asset"."libraryId" AS "AssetFaceEntity__AssetFaceEntity_asset_libraryId",
"AssetFaceEntity__AssetFaceEntity_asset"."deviceId" AS "AssetFaceEntity__AssetFaceEntity_asset_deviceId", "AssetFaceEntity__AssetFaceEntity_asset"."deviceId" AS "AssetFaceEntity__AssetFaceEntity_asset_deviceId",
"AssetFaceEntity__AssetFaceEntity_asset"."type" AS "AssetFaceEntity__AssetFaceEntity_asset_type", "AssetFaceEntity__AssetFaceEntity_asset"."type" AS "AssetFaceEntity__AssetFaceEntity_asset_type",
"AssetFaceEntity__AssetFaceEntity_asset"."status" AS "AssetFaceEntity__AssetFaceEntity_asset_status",
"AssetFaceEntity__AssetFaceEntity_asset"."originalPath" AS "AssetFaceEntity__AssetFaceEntity_asset_originalPath", "AssetFaceEntity__AssetFaceEntity_asset"."originalPath" AS "AssetFaceEntity__AssetFaceEntity_asset_originalPath",
"AssetFaceEntity__AssetFaceEntity_asset"."thumbhash" AS "AssetFaceEntity__AssetFaceEntity_asset_thumbhash", "AssetFaceEntity__AssetFaceEntity_asset"."thumbhash" AS "AssetFaceEntity__AssetFaceEntity_asset_thumbhash",
"AssetFaceEntity__AssetFaceEntity_asset"."encodedVideoPath" AS "AssetFaceEntity__AssetFaceEntity_asset_encodedVideoPath", "AssetFaceEntity__AssetFaceEntity_asset"."encodedVideoPath" AS "AssetFaceEntity__AssetFaceEntity_asset_encodedVideoPath",

View File

@ -13,6 +13,7 @@ FROM
"asset"."libraryId" AS "asset_libraryId", "asset"."libraryId" AS "asset_libraryId",
"asset"."deviceId" AS "asset_deviceId", "asset"."deviceId" AS "asset_deviceId",
"asset"."type" AS "asset_type", "asset"."type" AS "asset_type",
"asset"."status" AS "asset_status",
"asset"."originalPath" AS "asset_originalPath", "asset"."originalPath" AS "asset_originalPath",
"asset"."thumbhash" AS "asset_thumbhash", "asset"."thumbhash" AS "asset_thumbhash",
"asset"."encodedVideoPath" AS "asset_encodedVideoPath", "asset"."encodedVideoPath" AS "asset_encodedVideoPath",
@ -43,6 +44,7 @@ FROM
"stackedAssets"."libraryId" AS "stackedAssets_libraryId", "stackedAssets"."libraryId" AS "stackedAssets_libraryId",
"stackedAssets"."deviceId" AS "stackedAssets_deviceId", "stackedAssets"."deviceId" AS "stackedAssets_deviceId",
"stackedAssets"."type" AS "stackedAssets_type", "stackedAssets"."type" AS "stackedAssets_type",
"stackedAssets"."status" AS "stackedAssets_status",
"stackedAssets"."originalPath" AS "stackedAssets_originalPath", "stackedAssets"."originalPath" AS "stackedAssets_originalPath",
"stackedAssets"."thumbhash" AS "stackedAssets_thumbhash", "stackedAssets"."thumbhash" AS "stackedAssets_thumbhash",
"stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath", "stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath",
@ -106,6 +108,7 @@ SELECT
"asset"."libraryId" AS "asset_libraryId", "asset"."libraryId" AS "asset_libraryId",
"asset"."deviceId" AS "asset_deviceId", "asset"."deviceId" AS "asset_deviceId",
"asset"."type" AS "asset_type", "asset"."type" AS "asset_type",
"asset"."status" AS "asset_status",
"asset"."originalPath" AS "asset_originalPath", "asset"."originalPath" AS "asset_originalPath",
"asset"."thumbhash" AS "asset_thumbhash", "asset"."thumbhash" AS "asset_thumbhash",
"asset"."encodedVideoPath" AS "asset_encodedVideoPath", "asset"."encodedVideoPath" AS "asset_encodedVideoPath",
@ -136,6 +139,7 @@ SELECT
"stackedAssets"."libraryId" AS "stackedAssets_libraryId", "stackedAssets"."libraryId" AS "stackedAssets_libraryId",
"stackedAssets"."deviceId" AS "stackedAssets_deviceId", "stackedAssets"."deviceId" AS "stackedAssets_deviceId",
"stackedAssets"."type" AS "stackedAssets_type", "stackedAssets"."type" AS "stackedAssets_type",
"stackedAssets"."status" AS "stackedAssets_status",
"stackedAssets"."originalPath" AS "stackedAssets_originalPath", "stackedAssets"."originalPath" AS "stackedAssets_originalPath",
"stackedAssets"."thumbhash" AS "stackedAssets_thumbhash", "stackedAssets"."thumbhash" AS "stackedAssets_thumbhash",
"stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath", "stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath",
@ -345,6 +349,7 @@ SELECT
"asset"."libraryId" AS "asset_libraryId", "asset"."libraryId" AS "asset_libraryId",
"asset"."deviceId" AS "asset_deviceId", "asset"."deviceId" AS "asset_deviceId",
"asset"."type" AS "asset_type", "asset"."type" AS "asset_type",
"asset"."status" AS "asset_status",
"asset"."originalPath" AS "asset_originalPath", "asset"."originalPath" AS "asset_originalPath",
"asset"."thumbhash" AS "asset_thumbhash", "asset"."thumbhash" AS "asset_thumbhash",
"asset"."encodedVideoPath" AS "asset_encodedVideoPath", "asset"."encodedVideoPath" AS "asset_encodedVideoPath",

View File

@ -27,6 +27,7 @@ FROM
"SharedLinkEntity__SharedLinkEntity_assets"."libraryId" AS "SharedLinkEntity__SharedLinkEntity_assets_libraryId", "SharedLinkEntity__SharedLinkEntity_assets"."libraryId" AS "SharedLinkEntity__SharedLinkEntity_assets_libraryId",
"SharedLinkEntity__SharedLinkEntity_assets"."deviceId" AS "SharedLinkEntity__SharedLinkEntity_assets_deviceId", "SharedLinkEntity__SharedLinkEntity_assets"."deviceId" AS "SharedLinkEntity__SharedLinkEntity_assets_deviceId",
"SharedLinkEntity__SharedLinkEntity_assets"."type" AS "SharedLinkEntity__SharedLinkEntity_assets_type", "SharedLinkEntity__SharedLinkEntity_assets"."type" AS "SharedLinkEntity__SharedLinkEntity_assets_type",
"SharedLinkEntity__SharedLinkEntity_assets"."status" AS "SharedLinkEntity__SharedLinkEntity_assets_status",
"SharedLinkEntity__SharedLinkEntity_assets"."originalPath" AS "SharedLinkEntity__SharedLinkEntity_assets_originalPath", "SharedLinkEntity__SharedLinkEntity_assets"."originalPath" AS "SharedLinkEntity__SharedLinkEntity_assets_originalPath",
"SharedLinkEntity__SharedLinkEntity_assets"."thumbhash" AS "SharedLinkEntity__SharedLinkEntity_assets_thumbhash", "SharedLinkEntity__SharedLinkEntity_assets"."thumbhash" AS "SharedLinkEntity__SharedLinkEntity_assets_thumbhash",
"SharedLinkEntity__SharedLinkEntity_assets"."encodedVideoPath" AS "SharedLinkEntity__SharedLinkEntity_assets_encodedVideoPath", "SharedLinkEntity__SharedLinkEntity_assets"."encodedVideoPath" AS "SharedLinkEntity__SharedLinkEntity_assets_encodedVideoPath",
@ -93,6 +94,7 @@ FROM
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."libraryId" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_libraryId", "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."libraryId" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_libraryId",
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."deviceId" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_deviceId", "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."deviceId" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_deviceId",
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."type" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_type", "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."type" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_type",
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."status" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_status",
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."originalPath" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_originalPath", "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."originalPath" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_originalPath",
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."thumbhash" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_thumbhash", "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."thumbhash" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_thumbhash",
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."encodedVideoPath" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_encodedVideoPath", "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."encodedVideoPath" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_encodedVideoPath",
@ -214,6 +216,7 @@ SELECT
"SharedLinkEntity__SharedLinkEntity_assets"."libraryId" AS "SharedLinkEntity__SharedLinkEntity_assets_libraryId", "SharedLinkEntity__SharedLinkEntity_assets"."libraryId" AS "SharedLinkEntity__SharedLinkEntity_assets_libraryId",
"SharedLinkEntity__SharedLinkEntity_assets"."deviceId" AS "SharedLinkEntity__SharedLinkEntity_assets_deviceId", "SharedLinkEntity__SharedLinkEntity_assets"."deviceId" AS "SharedLinkEntity__SharedLinkEntity_assets_deviceId",
"SharedLinkEntity__SharedLinkEntity_assets"."type" AS "SharedLinkEntity__SharedLinkEntity_assets_type", "SharedLinkEntity__SharedLinkEntity_assets"."type" AS "SharedLinkEntity__SharedLinkEntity_assets_type",
"SharedLinkEntity__SharedLinkEntity_assets"."status" AS "SharedLinkEntity__SharedLinkEntity_assets_status",
"SharedLinkEntity__SharedLinkEntity_assets"."originalPath" AS "SharedLinkEntity__SharedLinkEntity_assets_originalPath", "SharedLinkEntity__SharedLinkEntity_assets"."originalPath" AS "SharedLinkEntity__SharedLinkEntity_assets_originalPath",
"SharedLinkEntity__SharedLinkEntity_assets"."thumbhash" AS "SharedLinkEntity__SharedLinkEntity_assets_thumbhash", "SharedLinkEntity__SharedLinkEntity_assets"."thumbhash" AS "SharedLinkEntity__SharedLinkEntity_assets_thumbhash",
"SharedLinkEntity__SharedLinkEntity_assets"."encodedVideoPath" AS "SharedLinkEntity__SharedLinkEntity_assets_encodedVideoPath", "SharedLinkEntity__SharedLinkEntity_assets"."encodedVideoPath" AS "SharedLinkEntity__SharedLinkEntity_assets_encodedVideoPath",

View File

@ -8,6 +8,7 @@ SELECT
"asset"."libraryId" AS "asset_libraryId", "asset"."libraryId" AS "asset_libraryId",
"asset"."deviceId" AS "asset_deviceId", "asset"."deviceId" AS "asset_deviceId",
"asset"."type" AS "asset_type", "asset"."type" AS "asset_type",
"asset"."status" AS "asset_status",
"asset"."originalPath" AS "asset_originalPath", "asset"."originalPath" AS "asset_originalPath",
"asset"."thumbhash" AS "asset_thumbhash", "asset"."thumbhash" AS "asset_thumbhash",
"asset"."encodedVideoPath" AS "asset_encodedVideoPath", "asset"."encodedVideoPath" AS "asset_encodedVideoPath",

View File

@ -6,7 +6,7 @@ import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity';
import { AssetEntity } from 'src/entities/asset.entity'; import { AssetEntity } from 'src/entities/asset.entity';
import { ExifEntity } from 'src/entities/exif.entity'; import { ExifEntity } from 'src/entities/exif.entity';
import { SmartInfoEntity } from 'src/entities/smart-info.entity'; import { SmartInfoEntity } from 'src/entities/smart-info.entity';
import { AssetFileType, AssetOrder, AssetType } from 'src/enum'; import { AssetFileType, AssetOrder, AssetStatus, AssetType } from 'src/enum';
import { import {
AssetBuilderOptions, AssetBuilderOptions,
AssetCreate, AssetCreate,
@ -295,16 +295,6 @@ export class AssetRepository implements IAssetRepository {
.execute(); .execute();
} }
@Chunked()
async softDeleteAll(ids: string[]): Promise<void> {
await this.repository.softDelete({ id: In(ids) });
}
@Chunked()
async restoreAll(ids: string[]): Promise<void> {
await this.repository.restore({ id: In(ids) });
}
async update(asset: AssetUpdateOptions): Promise<void> { async update(asset: AssetUpdateOptions): Promise<void> {
await this.repository.update(asset.id, asset); await this.repository.update(asset.id, asset);
} }
@ -597,7 +587,10 @@ export class AssetRepository implements IAssetRepository {
} }
if (isTrashed !== undefined) { if (isTrashed !== undefined) {
builder.withDeleted().andWhere(`asset.deletedAt is not null`); builder
.withDeleted()
.andWhere(`asset.deletedAt is not null`)
.andWhere('asset.status = :status', { status: AssetStatus.TRASHED });
} }
const items = await builder.getRawMany(); const items = await builder.getRawMany();
@ -755,6 +748,10 @@ export class AssetRepository implements IAssetRepository {
if (options.isTrashed !== undefined) { if (options.isTrashed !== undefined) {
builder.andWhere(`asset.deletedAt ${options.isTrashed ? 'IS NOT NULL' : 'IS NULL'}`).withDeleted(); builder.andWhere(`asset.deletedAt ${options.isTrashed ? 'IS NOT NULL' : 'IS NULL'}`).withDeleted();
if (options.isTrashed) {
builder.andWhere('asset.status = :status', { status: AssetStatus.TRASHED });
}
} }
if (options.isDuplicate !== undefined) { if (options.isDuplicate !== undefined) {

View File

@ -29,6 +29,7 @@ import { IStackRepository } from 'src/interfaces/stack.interface';
import { IStorageRepository } from 'src/interfaces/storage.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { ITagRepository } from 'src/interfaces/tag.interface'; import { ITagRepository } from 'src/interfaces/tag.interface';
import { ITrashRepository } from 'src/interfaces/trash.interface';
import { IUserRepository } from 'src/interfaces/user.interface'; import { IUserRepository } from 'src/interfaces/user.interface';
import { IViewRepository } from 'src/interfaces/view.interface'; import { IViewRepository } from 'src/interfaces/view.interface';
import { AccessRepository } from 'src/repositories/access.repository'; import { AccessRepository } from 'src/repositories/access.repository';
@ -62,6 +63,7 @@ import { StackRepository } from 'src/repositories/stack.repository';
import { StorageRepository } from 'src/repositories/storage.repository'; import { StorageRepository } from 'src/repositories/storage.repository';
import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository'; import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository';
import { TagRepository } from 'src/repositories/tag.repository'; import { TagRepository } from 'src/repositories/tag.repository';
import { TrashRepository } from 'src/repositories/trash.repository';
import { UserRepository } from 'src/repositories/user.repository'; import { UserRepository } from 'src/repositories/user.repository';
import { ViewRepository } from 'src/repositories/view-repository'; import { ViewRepository } from 'src/repositories/view-repository';
@ -97,6 +99,7 @@ export const repositories = [
{ provide: IStorageRepository, useClass: StorageRepository }, { provide: IStorageRepository, useClass: StorageRepository },
{ provide: ISystemMetadataRepository, useClass: SystemMetadataRepository }, { provide: ISystemMetadataRepository, useClass: SystemMetadataRepository },
{ provide: ITagRepository, useClass: TagRepository }, { provide: ITagRepository, useClass: TagRepository },
{ provide: ITrashRepository, useClass: TrashRepository },
{ provide: IUserRepository, useClass: UserRepository }, { provide: IUserRepository, useClass: UserRepository },
{ provide: IViewRepository, useClass: ViewRepository }, { provide: IViewRepository, useClass: ViewRepository },
]; ];

View File

@ -95,6 +95,9 @@ export const JOBS_TO_QUEUE: Record<JobName, QueueName> = {
// Version check // Version check
[JobName.VERSION_CHECK]: QueueName.BACKGROUND_TASK, [JobName.VERSION_CHECK]: QueueName.BACKGROUND_TASK,
// Trash
[JobName.QUEUE_TRASH_EMPTY]: QueueName.BACKGROUND_TASK,
}; };
@Instrumentation() @Instrumentation()

View File

@ -0,0 +1,49 @@
import { InjectRepository } from '@nestjs/typeorm';
import { AssetEntity } from 'src/entities/asset.entity';
import { AssetStatus } from 'src/enum';
import { ITrashRepository } from 'src/interfaces/trash.interface';
import { Paginated, paginatedBuilder, PaginationOptions } from 'src/utils/pagination';
import { In, IsNull, Not, Repository } from 'typeorm';
export class TrashRepository implements ITrashRepository {
constructor(@InjectRepository(AssetEntity) private assetRepository: Repository<AssetEntity>) {}
async getDeletedIds(pagination: PaginationOptions): Paginated<string> {
const { hasNextPage, items } = await paginatedBuilder(
this.assetRepository
.createQueryBuilder('asset')
.select('asset.id')
.where({ status: AssetStatus.DELETED })
.withDeleted(),
pagination,
);
return {
hasNextPage,
items: items.map((asset) => asset.id),
};
}
async restore(userId: string): Promise<number> {
const result = await this.assetRepository.update(
{ ownerId: userId, deletedAt: Not(IsNull()) },
{ status: AssetStatus.ACTIVE, deletedAt: null },
);
return result.affected || 0;
}
async empty(userId: string): Promise<number> {
const result = await this.assetRepository.update(
{ ownerId: userId, deletedAt: Not(IsNull()), status: AssetStatus.TRASHED },
{ status: AssetStatus.DELETED },
);
return result.affected || 0;
}
async restoreAll(ids: string[]): Promise<number> {
const result = await this.assetRepository.update({ id: In(ids) }, { status: AssetStatus.ACTIVE, deletedAt: null });
return result.affected ?? 0;
}
}

View File

@ -4,7 +4,7 @@ import { AssetMediaStatus, AssetRejectReason, AssetUploadAction } from 'src/dtos
import { AssetMediaCreateDto, AssetMediaReplaceDto, UploadFieldName } from 'src/dtos/asset-media.dto'; import { AssetMediaCreateDto, AssetMediaReplaceDto, UploadFieldName } from 'src/dtos/asset-media.dto';
import { AssetFileEntity } from 'src/entities/asset-files.entity'; import { AssetFileEntity } from 'src/entities/asset-files.entity';
import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity } from 'src/entities/asset.entity'; import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity } from 'src/entities/asset.entity';
import { AssetType } from 'src/enum'; import { AssetStatus, AssetType } from 'src/enum';
import { IAssetRepository } from 'src/interfaces/asset.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface';
import { IEventRepository } from 'src/interfaces/event.interface'; import { IEventRepository } from 'src/interfaces/event.interface';
import { IJobRepository, JobName } from 'src/interfaces/job.interface'; import { IJobRepository, JobName } from 'src/interfaces/job.interface';
@ -478,7 +478,10 @@ describe(AssetMediaService.name, () => {
}), }),
); );
expect(assetMock.softDeleteAll).toHaveBeenCalledWith([copiedAsset.id]); expect(assetMock.updateAll).toHaveBeenCalledWith([copiedAsset.id], {
deletedAt: expect.any(Date),
status: AssetStatus.TRASHED,
});
expect(userMock.updateUsage).toHaveBeenCalledWith(authStub.user1.user.id, updatedFile.size); expect(userMock.updateUsage).toHaveBeenCalledWith(authStub.user1.user.id, updatedFile.size);
expect(storageMock.utimes).toHaveBeenCalledWith( expect(storageMock.utimes).toHaveBeenCalledWith(
updatedFile.originalPath, updatedFile.originalPath,
@ -506,7 +509,10 @@ describe(AssetMediaService.name, () => {
id: 'copied-asset', id: 'copied-asset',
}); });
expect(assetMock.softDeleteAll).toHaveBeenCalledWith(['copied-asset']); expect(assetMock.updateAll).toHaveBeenCalledWith([copiedAsset.id], {
deletedAt: expect.any(Date),
status: AssetStatus.TRASHED,
});
expect(userMock.updateUsage).toHaveBeenCalledWith(authStub.user1.user.id, updatedFile.size); expect(userMock.updateUsage).toHaveBeenCalledWith(authStub.user1.user.id, updatedFile.size);
expect(storageMock.utimes).toHaveBeenCalledWith( expect(storageMock.utimes).toHaveBeenCalledWith(
updatedFile.originalPath, updatedFile.originalPath,
@ -532,7 +538,10 @@ describe(AssetMediaService.name, () => {
id: 'copied-asset', id: 'copied-asset',
}); });
expect(assetMock.softDeleteAll).toHaveBeenCalledWith(['copied-asset']); expect(assetMock.updateAll).toHaveBeenCalledWith([copiedAsset.id], {
deletedAt: expect.any(Date),
status: AssetStatus.TRASHED,
});
expect(userMock.updateUsage).toHaveBeenCalledWith(authStub.user1.user.id, updatedFile.size); expect(userMock.updateUsage).toHaveBeenCalledWith(authStub.user1.user.id, updatedFile.size);
expect(storageMock.utimes).toHaveBeenCalledWith( expect(storageMock.utimes).toHaveBeenCalledWith(
updatedFile.originalPath, updatedFile.originalPath,
@ -561,7 +570,7 @@ describe(AssetMediaService.name, () => {
}); });
expect(assetMock.create).not.toHaveBeenCalled(); expect(assetMock.create).not.toHaveBeenCalled();
expect(assetMock.softDeleteAll).not.toHaveBeenCalled(); expect(assetMock.updateAll).not.toHaveBeenCalled();
expect(jobMock.queue).toHaveBeenCalledWith({ expect(jobMock.queue).toHaveBeenCalledWith({
name: JobName.DELETE_FILES, name: JobName.DELETE_FILES,
data: { files: [updatedFile.originalPath, undefined] }, data: { files: [updatedFile.originalPath, undefined] },

View File

@ -27,7 +27,7 @@ import {
} from 'src/dtos/asset-media.dto'; } from 'src/dtos/asset-media.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity } from 'src/entities/asset.entity'; import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity } from 'src/entities/asset.entity';
import { AssetType, Permission } from 'src/enum'; import { AssetStatus, AssetType, Permission } from 'src/enum';
import { IAccessRepository } from 'src/interfaces/access.interface'; import { IAccessRepository } from 'src/interfaces/access.interface';
import { IAssetRepository } from 'src/interfaces/asset.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface';
import { IEventRepository } from 'src/interfaces/event.interface'; import { IEventRepository } from 'src/interfaces/event.interface';
@ -193,7 +193,7 @@ export class AssetMediaService {
// but the local variable holds the original file data paths. // but the local variable holds the original file data paths.
const copiedPhoto = await this.createCopy(asset); const copiedPhoto = await this.createCopy(asset);
// and immediate trash it // and immediate trash it
await this.assetRepository.softDeleteAll([copiedPhoto.id]); await this.assetRepository.updateAll([copiedPhoto.id], { deletedAt: new Date(), status: AssetStatus.TRASHED });
await this.eventRepository.emit('asset.trash', { assetId: copiedPhoto.id, userId: auth.user.id }); await this.eventRepository.emit('asset.trash', { assetId: copiedPhoto.id, userId: auth.user.id });
await this.userRepository.updateUsage(auth.user.id, file.size); await this.userRepository.updateUsage(auth.user.id, file.size);

View File

@ -2,7 +2,7 @@ import { BadRequestException } from '@nestjs/common';
import { mapAsset } from 'src/dtos/asset-response.dto'; import { mapAsset } from 'src/dtos/asset-response.dto';
import { AssetJobName, AssetStatsResponseDto } from 'src/dtos/asset.dto'; import { AssetJobName, AssetStatsResponseDto } from 'src/dtos/asset.dto';
import { AssetEntity } from 'src/entities/asset.entity'; import { AssetEntity } from 'src/entities/asset.entity';
import { AssetType } from 'src/enum'; import { AssetStatus, AssetType } from 'src/enum';
import { AssetStats, IAssetRepository } from 'src/interfaces/asset.interface'; import { AssetStats, IAssetRepository } from 'src/interfaces/asset.interface';
import { IEventRepository } from 'src/interfaces/event.interface'; import { IEventRepository } from 'src/interfaces/event.interface';
import { IJobRepository, JobName } from 'src/interfaces/job.interface'; import { IJobRepository, JobName } from 'src/interfaces/job.interface';
@ -269,10 +269,10 @@ describe(AssetService.name, () => {
await sut.deleteAll(authStub.user1, { ids: ['asset1', 'asset2'], force: true }); await sut.deleteAll(authStub.user1, { ids: ['asset1', 'asset2'], force: true });
expect(jobMock.queueAll).toHaveBeenCalledWith([ expect(eventMock.emit).toHaveBeenCalledWith('assets.delete', {
{ name: JobName.ASSET_DELETION, data: { id: 'asset1', deleteOnDisk: true } }, assetIds: ['asset1', 'asset2'],
{ name: JobName.ASSET_DELETION, data: { id: 'asset2', deleteOnDisk: true } }, userId: 'user-id',
]); });
}); });
it('should soft delete a batch of assets', async () => { it('should soft delete a batch of assets', async () => {
@ -280,7 +280,10 @@ describe(AssetService.name, () => {
await sut.deleteAll(authStub.user1, { ids: ['asset1', 'asset2'], force: false }); await sut.deleteAll(authStub.user1, { ids: ['asset1', 'asset2'], force: false });
expect(assetMock.softDeleteAll).toHaveBeenCalledWith(['asset1', 'asset2']); expect(assetMock.updateAll).toHaveBeenCalledWith(['asset1', 'asset2'], {
deletedAt: expect.any(Date),
status: AssetStatus.TRASHED,
});
expect(jobMock.queue.mock.calls).toEqual([]); expect(jobMock.queue.mock.calls).toEqual([]);
}); });
}); });

View File

@ -20,7 +20,7 @@ import {
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { MemoryLaneDto } from 'src/dtos/search.dto'; import { MemoryLaneDto } from 'src/dtos/search.dto';
import { AssetEntity } from 'src/entities/asset.entity'; import { AssetEntity } from 'src/entities/asset.entity';
import { Permission } from 'src/enum'; import { AssetStatus, Permission } from 'src/enum';
import { IAccessRepository } from 'src/interfaces/access.interface'; import { IAccessRepository } from 'src/interfaces/access.interface';
import { IAssetRepository } from 'src/interfaces/asset.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface';
import { IEventRepository } from 'src/interfaces/event.interface'; import { IEventRepository } from 'src/interfaces/event.interface';
@ -302,18 +302,11 @@ export class AssetService {
const { ids, force } = dto; const { ids, force } = dto;
await requireAccess(this.access, { auth, permission: Permission.ASSET_DELETE, ids }); await requireAccess(this.access, { auth, permission: Permission.ASSET_DELETE, ids });
await this.assetRepository.updateAll(ids, {
if (force) { deletedAt: new Date(),
await this.jobRepository.queueAll( status: force ? AssetStatus.DELETED : AssetStatus.TRASHED,
ids.map((id) => ({ });
name: JobName.ASSET_DELETION, await this.eventRepository.emit(force ? 'assets.delete' : 'assets.trash', { assetIds: ids, userId: auth.user.id });
data: { id, deleteOnDisk: true },
})),
);
} else {
await this.assetRepository.softDeleteAll(ids);
await this.eventRepository.emit('assets.trash', { assetIds: ids, userId: auth.user.id });
}
} }
async run(auth: AuthDto, dto: AssetJobsDto) { async run(auth: AuthDto, dto: AssetJobsDto) {

View File

@ -16,6 +16,7 @@ import { SmartInfoService } from 'src/services/smart-info.service';
import { StorageTemplateService } from 'src/services/storage-template.service'; import { StorageTemplateService } from 'src/services/storage-template.service';
import { StorageService } from 'src/services/storage.service'; import { StorageService } from 'src/services/storage.service';
import { TagService } from 'src/services/tag.service'; import { TagService } from 'src/services/tag.service';
import { TrashService } from 'src/services/trash.service';
import { UserService } from 'src/services/user.service'; import { UserService } from 'src/services/user.service';
import { VersionService } from 'src/services/version.service'; import { VersionService } from 'src/services/version.service';
import { otelShutdown } from 'src/utils/instrumentation'; import { otelShutdown } from 'src/utils/instrumentation';
@ -36,6 +37,7 @@ export class MicroservicesService {
private storageTemplateService: StorageTemplateService, private storageTemplateService: StorageTemplateService,
private storageService: StorageService, private storageService: StorageService,
private tagService: TagService, private tagService: TagService,
private trashService: TrashService,
private userService: UserService, private userService: UserService,
private duplicateService: DuplicateService, private duplicateService: DuplicateService,
private versionService: VersionService, private versionService: VersionService,
@ -97,6 +99,7 @@ export class MicroservicesService {
[JobName.NOTIFY_SIGNUP]: (data) => this.notificationService.handleUserSignup(data), [JobName.NOTIFY_SIGNUP]: (data) => this.notificationService.handleUserSignup(data),
[JobName.TAG_CLEANUP]: () => this.tagService.handleTagCleanup(), [JobName.TAG_CLEANUP]: () => this.tagService.handleTagCleanup(),
[JobName.VERSION_CHECK]: () => this.versionService.handleVersionCheck(), [JobName.VERSION_CHECK]: () => this.versionService.handleVersionCheck(),
[JobName.QUEUE_TRASH_EMPTY]: () => this.trashService.handleQueueEmptyTrash(),
}); });
} }

View File

@ -1,22 +1,24 @@
import { BadRequestException } from '@nestjs/common'; import { BadRequestException } from '@nestjs/common';
import { IAssetRepository } from 'src/interfaces/asset.interface';
import { IEventRepository } from 'src/interfaces/event.interface'; import { IEventRepository } from 'src/interfaces/event.interface';
import { IJobRepository, JobName } from 'src/interfaces/job.interface'; import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { ITrashRepository } from 'src/interfaces/trash.interface';
import { TrashService } from 'src/services/trash.service'; import { TrashService } from 'src/services/trash.service';
import { assetStub } from 'test/fixtures/asset.stub';
import { authStub } from 'test/fixtures/auth.stub'; import { authStub } from 'test/fixtures/auth.stub';
import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock'; import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock';
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
import { newEventRepositoryMock } from 'test/repositories/event.repository.mock'; import { newEventRepositoryMock } from 'test/repositories/event.repository.mock';
import { newJobRepositoryMock } from 'test/repositories/job.repository.mock'; import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
import { newTrashRepositoryMock } from 'test/repositories/trash.repository.mock';
import { Mocked } from 'vitest'; import { Mocked } from 'vitest';
describe(TrashService.name, () => { describe(TrashService.name, () => {
let sut: TrashService; let sut: TrashService;
let accessMock: IAccessRepositoryMock; let accessMock: IAccessRepositoryMock;
let assetMock: Mocked<IAssetRepository>;
let jobMock: Mocked<IJobRepository>;
let eventMock: Mocked<IEventRepository>; let eventMock: Mocked<IEventRepository>;
let jobMock: Mocked<IJobRepository>;
let trashMock: Mocked<ITrashRepository>;
let loggerMock: Mocked<ILoggerRepository>;
it('should work', () => { it('should work', () => {
expect(sut).toBeDefined(); expect(sut).toBeDefined();
@ -24,11 +26,12 @@ describe(TrashService.name, () => {
beforeEach(() => { beforeEach(() => {
accessMock = newAccessRepositoryMock(); accessMock = newAccessRepositoryMock();
assetMock = newAssetRepositoryMock();
eventMock = newEventRepositoryMock(); eventMock = newEventRepositoryMock();
jobMock = newJobRepositoryMock(); jobMock = newJobRepositoryMock();
trashMock = newTrashRepositoryMock();
loggerMock = newLoggerRepositoryMock();
sut = new TrashService(accessMock, assetMock, jobMock, eventMock); sut = new TrashService(accessMock, eventMock, jobMock, trashMock, loggerMock);
}); });
describe('restoreAssets', () => { describe('restoreAssets', () => {
@ -40,44 +43,70 @@ describe(TrashService.name, () => {
).rejects.toBeInstanceOf(BadRequestException); ).rejects.toBeInstanceOf(BadRequestException);
}); });
it('should handle an empty list', async () => {
await expect(sut.restoreAssets(authStub.user1, { ids: [] })).resolves.toEqual({ count: 0 });
expect(accessMock.asset.checkOwnerAccess).not.toHaveBeenCalled();
});
it('should restore a batch of assets', async () => { it('should restore a batch of assets', async () => {
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset1', 'asset2'])); accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset1', 'asset2']));
await sut.restoreAssets(authStub.user1, { ids: ['asset1', 'asset2'] }); await sut.restoreAssets(authStub.user1, { ids: ['asset1', 'asset2'] });
expect(assetMock.restoreAll).toHaveBeenCalledWith(['asset1', 'asset2']); expect(trashMock.restoreAll).toHaveBeenCalledWith(['asset1', 'asset2']);
expect(jobMock.queue.mock.calls).toEqual([]); expect(jobMock.queue.mock.calls).toEqual([]);
}); });
}); });
describe('restore', () => { describe('restore', () => {
it('should handle an empty trash', async () => { it('should handle an empty trash', async () => {
assetMock.getByUserId.mockResolvedValue({ items: [], hasNextPage: false }); trashMock.getDeletedIds.mockResolvedValue({ items: [], hasNextPage: false });
await expect(sut.restore(authStub.user1)).resolves.toBeUndefined(); trashMock.restore.mockResolvedValue(0);
expect(assetMock.restoreAll).not.toHaveBeenCalled(); await expect(sut.restore(authStub.user1)).resolves.toEqual({ count: 0 });
expect(eventMock.clientSend).not.toHaveBeenCalled(); expect(trashMock.restore).toHaveBeenCalledWith('user-id');
}); });
it('should restore and notify', async () => { it('should restore', async () => {
assetMock.getByUserId.mockResolvedValue({ items: [assetStub.image], hasNextPage: false }); trashMock.getDeletedIds.mockResolvedValue({ items: ['asset-id'], hasNextPage: false });
await expect(sut.restore(authStub.user1)).resolves.toBeUndefined(); trashMock.restore.mockResolvedValue(1);
expect(assetMock.restoreAll).toHaveBeenCalledWith([assetStub.image.id]); await expect(sut.restore(authStub.user1)).resolves.toEqual({ count: 1 });
expect(eventMock.emit).toHaveBeenCalledWith('assets.restore', { assetIds: ['asset-id'], userId: 'user-id' }); expect(trashMock.restore).toHaveBeenCalledWith('user-id');
}); });
}); });
describe('empty', () => { describe('empty', () => {
it('should handle an empty trash', async () => { it('should handle an empty trash', async () => {
assetMock.getByUserId.mockResolvedValue({ items: [], hasNextPage: false }); trashMock.getDeletedIds.mockResolvedValue({ items: [], hasNextPage: false });
await expect(sut.empty(authStub.user1)).resolves.toBeUndefined(); trashMock.empty.mockResolvedValue(0);
expect(jobMock.queueAll).toHaveBeenCalledWith([]); await expect(sut.empty(authStub.user1)).resolves.toEqual({ count: 0 });
expect(jobMock.queue).not.toHaveBeenCalled();
}); });
it('should empty the trash', async () => { it('should empty the trash', async () => {
assetMock.getByUserId.mockResolvedValue({ items: [assetStub.image], hasNextPage: false }); trashMock.getDeletedIds.mockResolvedValue({ items: ['asset-id'], hasNextPage: false });
await expect(sut.empty(authStub.user1)).resolves.toBeUndefined(); trashMock.empty.mockResolvedValue(1);
await expect(sut.empty(authStub.user1)).resolves.toEqual({ count: 1 });
expect(trashMock.empty).toHaveBeenCalledWith('user-id');
expect(jobMock.queue).toHaveBeenCalledWith({ name: JobName.QUEUE_TRASH_EMPTY, data: {} });
});
});
describe('onAssetsDelete', () => {
it('should queue the empty trash job', async () => {
await expect(sut.onAssetsDelete()).resolves.toBeUndefined();
expect(jobMock.queue).toHaveBeenCalledWith({ name: JobName.QUEUE_TRASH_EMPTY, data: {} });
});
});
describe('handleQueueEmptyTrash', () => {
it('should queue asset delete jobs', async () => {
trashMock.getDeletedIds.mockResolvedValue({ items: ['asset-1'], hasNextPage: false });
await expect(sut.handleQueueEmptyTrash()).resolves.toEqual(JobStatus.SUCCESS);
expect(jobMock.queueAll).toHaveBeenCalledWith([ expect(jobMock.queueAll).toHaveBeenCalledWith([
{ name: JobName.ASSET_DELETION, data: { id: assetStub.image.id, deleteOnDisk: true } }, {
name: JobName.ASSET_DELETION,
data: { id: 'asset-1', deleteOnDisk: true },
},
]); ]);
}); });
}); });

View File

@ -1,69 +1,86 @@
import { Inject } from '@nestjs/common'; import { Inject } from '@nestjs/common';
import { DateTime } from 'luxon'; import { OnEmit } from 'src/decorators';
import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { TrashResponseDto } from 'src/dtos/trash.dto';
import { Permission } from 'src/enum'; import { Permission } from 'src/enum';
import { IAccessRepository } from 'src/interfaces/access.interface'; import { IAccessRepository } from 'src/interfaces/access.interface';
import { IAssetRepository } from 'src/interfaces/asset.interface';
import { IEventRepository } from 'src/interfaces/event.interface'; import { IEventRepository } from 'src/interfaces/event.interface';
import { IJobRepository, JOBS_ASSET_PAGINATION_SIZE, JobName } from 'src/interfaces/job.interface'; import { IJobRepository, JOBS_ASSET_PAGINATION_SIZE, JobName, JobStatus } from 'src/interfaces/job.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { ITrashRepository } from 'src/interfaces/trash.interface';
import { requireAccess } from 'src/utils/access'; import { requireAccess } from 'src/utils/access';
import { usePagination } from 'src/utils/pagination'; import { usePagination } from 'src/utils/pagination';
export class TrashService { export class TrashService {
constructor( constructor(
@Inject(IAccessRepository) private access: IAccessRepository, @Inject(IAccessRepository) private access: IAccessRepository,
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
@Inject(IJobRepository) private jobRepository: IJobRepository,
@Inject(IEventRepository) private eventRepository: IEventRepository, @Inject(IEventRepository) private eventRepository: IEventRepository,
) {} @Inject(IJobRepository) private jobRepository: IJobRepository,
@Inject(ITrashRepository) private trashRepository: ITrashRepository,
@Inject(ILoggerRepository) private logger: ILoggerRepository,
) {
this.logger.setContext(TrashService.name);
}
async restoreAssets(auth: AuthDto, dto: BulkIdsDto): Promise<void> { async restoreAssets(auth: AuthDto, dto: BulkIdsDto): Promise<TrashResponseDto> {
const { ids } = dto; const { ids } = dto;
await requireAccess(this.access, { auth, permission: Permission.ASSET_DELETE, ids }); if (ids.length === 0) {
await this.restoreAndSend(auth, ids); return { count: 0 };
}
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);
} }
await requireAccess(this.access, { auth, permission: Permission.ASSET_DELETE, ids });
await this.trashRepository.restoreAll(ids);
await this.eventRepository.emit('assets.restore', { assetIds: ids, userId: auth.user.id });
this.logger.log(`Restored ${ids.length} assets from trash`);
return { count: ids.length };
} }
async empty(auth: AuthDto): Promise<void> { async restore(auth: AuthDto): Promise<TrashResponseDto> {
const count = await this.trashRepository.restore(auth.user.id);
if (count > 0) {
this.logger.log(`Restored ${count} assets from trash`);
}
return { count };
}
async empty(auth: AuthDto): Promise<TrashResponseDto> {
const count = await this.trashRepository.empty(auth.user.id);
if (count > 0) {
await this.jobRepository.queue({ name: JobName.QUEUE_TRASH_EMPTY, data: {} });
}
return { count };
}
@OnEmit({ event: 'assets.delete' })
async onAssetsDelete() {
await this.jobRepository.queue({ name: JobName.QUEUE_TRASH_EMPTY, data: {} });
}
async handleQueueEmptyTrash() {
let count = 0;
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) =>
this.assetRepository.getByUserId(pagination, auth.user.id, { this.trashRepository.getDeletedIds(pagination),
trashedBefore: DateTime.now().toJSDate(),
withArchived: true,
}),
); );
for await (const assets of assetPagination) { for await (const assetIds of assetPagination) {
this.logger.debug(`Queueing ${assetIds.length} assets for deletion from the trash`);
count += assetIds.length;
await this.jobRepository.queueAll( await this.jobRepository.queueAll(
assets.map((asset) => ({ assetIds.map((assetId) => ({
name: JobName.ASSET_DELETION, name: JobName.ASSET_DELETION,
data: { data: {
id: asset.id, id: assetId,
deleteOnDisk: true, deleteOnDisk: true,
}, },
})), })),
); );
} }
}
private async restoreAndSend(auth: AuthDto, ids: string[]) { this.logger.log(`Queued ${count} assets for deletion from the trash`);
if (ids.length === 0) {
return;
}
await this.assetRepository.restoreAll(ids); return JobStatus.SUCCESS;
await this.eventRepository.emit('assets.restore', { assetIds: ids, userId: auth.user.id });
} }
} }

View File

@ -2,7 +2,7 @@ import { AssetFileEntity } from 'src/entities/asset-files.entity';
import { AssetEntity } from 'src/entities/asset.entity'; import { AssetEntity } from 'src/entities/asset.entity';
import { ExifEntity } from 'src/entities/exif.entity'; import { ExifEntity } from 'src/entities/exif.entity';
import { StackEntity } from 'src/entities/stack.entity'; import { StackEntity } from 'src/entities/stack.entity';
import { AssetFileType, AssetType } from 'src/enum'; import { AssetFileType, AssetStatus, AssetType } from 'src/enum';
import { authStub } from 'test/fixtures/auth.stub'; import { authStub } from 'test/fixtures/auth.stub';
import { fileStub } from 'test/fixtures/file.stub'; import { fileStub } from 'test/fixtures/file.stub';
import { libraryStub } from 'test/fixtures/library.stub'; import { libraryStub } from 'test/fixtures/library.stub';
@ -42,6 +42,7 @@ export const stackStub = (stackId: string, assets: AssetEntity[]): StackEntity =
export const assetStub = { export const assetStub = {
noResizePath: Object.freeze<AssetEntity>({ noResizePath: Object.freeze<AssetEntity>({
id: 'asset-id', id: 'asset-id',
status: AssetStatus.ACTIVE,
originalFileName: 'IMG_123.jpg', originalFileName: 'IMG_123.jpg',
deviceAssetId: 'device-asset-id', deviceAssetId: 'device-asset-id',
fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'),
@ -76,6 +77,7 @@ export const assetStub = {
noWebpPath: Object.freeze<AssetEntity>({ noWebpPath: Object.freeze<AssetEntity>({
id: 'asset-id', id: 'asset-id',
status: AssetStatus.ACTIVE,
deviceAssetId: 'device-asset-id', deviceAssetId: 'device-asset-id',
fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'),
fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'),
@ -83,7 +85,6 @@ export const assetStub = {
ownerId: 'user-id', ownerId: 'user-id',
deviceId: 'device-id', deviceId: 'device-id',
originalPath: 'upload/library/IMG_456.jpg', originalPath: 'upload/library/IMG_456.jpg',
files: [previewFile], files: [previewFile],
checksum: Buffer.from('file hash', 'utf8'), checksum: Buffer.from('file hash', 'utf8'),
type: AssetType.IMAGE, type: AssetType.IMAGE,
@ -114,6 +115,7 @@ export const assetStub = {
noThumbhash: Object.freeze<AssetEntity>({ noThumbhash: Object.freeze<AssetEntity>({
id: 'asset-id', id: 'asset-id',
status: AssetStatus.ACTIVE,
deviceAssetId: 'device-asset-id', deviceAssetId: 'device-asset-id',
fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'),
fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'),
@ -148,6 +150,7 @@ export const assetStub = {
primaryImage: Object.freeze<AssetEntity>({ primaryImage: Object.freeze<AssetEntity>({
id: 'primary-asset-id', id: 'primary-asset-id',
status: AssetStatus.ACTIVE,
deviceAssetId: 'device-asset-id', deviceAssetId: 'device-asset-id',
fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'),
fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'),
@ -192,6 +195,7 @@ export const assetStub = {
image: Object.freeze<AssetEntity>({ image: Object.freeze<AssetEntity>({
id: 'asset-id', id: 'asset-id',
status: AssetStatus.ACTIVE,
deviceAssetId: 'device-asset-id', deviceAssetId: 'device-asset-id',
fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'),
fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'),
@ -231,6 +235,7 @@ export const assetStub = {
trashed: Object.freeze<AssetEntity>({ trashed: Object.freeze<AssetEntity>({
id: 'asset-id', id: 'asset-id',
status: AssetStatus.ACTIVE,
deviceAssetId: 'device-asset-id', deviceAssetId: 'device-asset-id',
fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'),
fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'),
@ -270,6 +275,7 @@ export const assetStub = {
archived: Object.freeze<AssetEntity>({ archived: Object.freeze<AssetEntity>({
id: 'asset-id', id: 'asset-id',
status: AssetStatus.ACTIVE,
deviceAssetId: 'device-asset-id', deviceAssetId: 'device-asset-id',
fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'),
fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'),
@ -309,6 +315,7 @@ export const assetStub = {
external: Object.freeze<AssetEntity>({ external: Object.freeze<AssetEntity>({
id: 'asset-id', id: 'asset-id',
status: AssetStatus.ACTIVE,
deviceAssetId: 'device-asset-id', deviceAssetId: 'device-asset-id',
fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'),
fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'),
@ -348,6 +355,7 @@ export const assetStub = {
offline: Object.freeze<AssetEntity>({ offline: Object.freeze<AssetEntity>({
id: 'asset-id', id: 'asset-id',
status: AssetStatus.ACTIVE,
deviceAssetId: 'device-asset-id', deviceAssetId: 'device-asset-id',
fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'),
fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'),
@ -385,6 +393,7 @@ export const assetStub = {
externalOffline: Object.freeze<AssetEntity>({ externalOffline: Object.freeze<AssetEntity>({
id: 'asset-id', id: 'asset-id',
status: AssetStatus.ACTIVE,
deviceAssetId: 'device-asset-id', deviceAssetId: 'device-asset-id',
fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'),
fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'),
@ -424,6 +433,7 @@ export const assetStub = {
image1: Object.freeze<AssetEntity>({ image1: Object.freeze<AssetEntity>({
id: 'asset-id-1', id: 'asset-id-1',
status: AssetStatus.ACTIVE,
deviceAssetId: 'device-asset-id', deviceAssetId: 'device-asset-id',
fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'),
fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'),
@ -461,6 +471,7 @@ export const assetStub = {
imageFrom2015: Object.freeze<AssetEntity>({ imageFrom2015: Object.freeze<AssetEntity>({
id: 'asset-id-1', id: 'asset-id-1',
status: AssetStatus.ACTIVE,
deviceAssetId: 'device-asset-id', deviceAssetId: 'device-asset-id',
fileModifiedAt: new Date('2015-02-23T05:06:29.716Z'), fileModifiedAt: new Date('2015-02-23T05:06:29.716Z'),
fileCreatedAt: new Date('2015-02-23T05:06:29.716Z'), fileCreatedAt: new Date('2015-02-23T05:06:29.716Z'),
@ -498,6 +509,7 @@ export const assetStub = {
video: Object.freeze<AssetEntity>({ video: Object.freeze<AssetEntity>({
id: 'asset-id', id: 'asset-id',
status: AssetStatus.ACTIVE,
originalFileName: 'asset-id.ext', originalFileName: 'asset-id.ext',
deviceAssetId: 'device-asset-id', deviceAssetId: 'device-asset-id',
fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'),
@ -536,6 +548,7 @@ export const assetStub = {
}), }),
livePhotoMotionAsset: Object.freeze({ livePhotoMotionAsset: Object.freeze({
status: AssetStatus.ACTIVE,
id: fileStub.livePhotoMotion.uuid, id: fileStub.livePhotoMotion.uuid,
originalPath: fileStub.livePhotoMotion.originalPath, originalPath: fileStub.livePhotoMotion.originalPath,
ownerId: authStub.user1.user.id, ownerId: authStub.user1.user.id,
@ -551,6 +564,7 @@ export const assetStub = {
liveMotionWithThumb: Object.freeze({ liveMotionWithThumb: Object.freeze({
id: fileStub.livePhotoMotion.uuid, id: fileStub.livePhotoMotion.uuid,
status: AssetStatus.ACTIVE,
originalPath: fileStub.livePhotoMotion.originalPath, originalPath: fileStub.livePhotoMotion.originalPath,
ownerId: authStub.user1.user.id, ownerId: authStub.user1.user.id,
type: AssetType.VIDEO, type: AssetType.VIDEO,
@ -581,6 +595,7 @@ export const assetStub = {
livePhotoStillAsset: Object.freeze({ livePhotoStillAsset: Object.freeze({
id: 'live-photo-still-asset', id: 'live-photo-still-asset',
status: AssetStatus.ACTIVE,
originalPath: fileStub.livePhotoStill.originalPath, originalPath: fileStub.livePhotoStill.originalPath,
ownerId: authStub.user1.user.id, ownerId: authStub.user1.user.id,
type: AssetType.IMAGE, type: AssetType.IMAGE,
@ -596,6 +611,7 @@ export const assetStub = {
livePhotoStillAssetWithTheSameLivePhotoMotionAsset: Object.freeze({ livePhotoStillAssetWithTheSameLivePhotoMotionAsset: Object.freeze({
id: 'live-photo-still-asset-1', id: 'live-photo-still-asset-1',
status: AssetStatus.ACTIVE,
originalPath: fileStub.livePhotoStill.originalPath, originalPath: fileStub.livePhotoStill.originalPath,
ownerId: authStub.user1.user.id, ownerId: authStub.user1.user.id,
type: AssetType.IMAGE, type: AssetType.IMAGE,
@ -611,6 +627,7 @@ export const assetStub = {
livePhotoWithOriginalFileName: Object.freeze({ livePhotoWithOriginalFileName: Object.freeze({
id: 'live-photo-still-asset', id: 'live-photo-still-asset',
status: AssetStatus.ACTIVE,
originalPath: fileStub.livePhotoStill.originalPath, originalPath: fileStub.livePhotoStill.originalPath,
originalFileName: fileStub.livePhotoStill.originalName, originalFileName: fileStub.livePhotoStill.originalName,
ownerId: authStub.user1.user.id, ownerId: authStub.user1.user.id,
@ -627,6 +644,7 @@ export const assetStub = {
withLocation: Object.freeze<AssetEntity>({ withLocation: Object.freeze<AssetEntity>({
id: 'asset-with-favorite-id', id: 'asset-with-favorite-id',
status: AssetStatus.ACTIVE,
deviceAssetId: 'device-asset-id', deviceAssetId: 'device-asset-id',
fileModifiedAt: new Date('2023-02-22T05:06:29.716Z'), fileModifiedAt: new Date('2023-02-22T05:06:29.716Z'),
fileCreatedAt: new Date('2023-02-22T05:06:29.716Z'), fileCreatedAt: new Date('2023-02-22T05:06:29.716Z'),
@ -668,6 +686,7 @@ export const assetStub = {
}), }),
sidecar: Object.freeze<AssetEntity>({ sidecar: Object.freeze<AssetEntity>({
id: 'asset-id', id: 'asset-id',
status: AssetStatus.ACTIVE,
deviceAssetId: 'device-asset-id', deviceAssetId: 'device-asset-id',
fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'),
fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'),
@ -701,6 +720,7 @@ export const assetStub = {
}), }),
sidecarWithoutExt: Object.freeze<AssetEntity>({ sidecarWithoutExt: Object.freeze<AssetEntity>({
id: 'asset-id', id: 'asset-id',
status: AssetStatus.ACTIVE,
deviceAssetId: 'device-asset-id', deviceAssetId: 'device-asset-id',
fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'),
fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'),
@ -735,6 +755,7 @@ export const assetStub = {
readOnly: Object.freeze<AssetEntity>({ readOnly: Object.freeze<AssetEntity>({
id: 'read-only-asset', id: 'read-only-asset',
status: AssetStatus.ACTIVE,
deviceAssetId: 'device-asset-id', deviceAssetId: 'device-asset-id',
fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'),
fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'),
@ -769,6 +790,7 @@ export const assetStub = {
hasEncodedVideo: Object.freeze<AssetEntity>({ hasEncodedVideo: Object.freeze<AssetEntity>({
id: 'asset-id', id: 'asset-id',
status: AssetStatus.ACTIVE,
originalFileName: 'asset-id.ext', originalFileName: 'asset-id.ext',
deviceAssetId: 'device-asset-id', deviceAssetId: 'device-asset-id',
fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'),
@ -805,6 +827,7 @@ export const assetStub = {
}), }),
missingFileExtension: Object.freeze<AssetEntity>({ missingFileExtension: Object.freeze<AssetEntity>({
id: 'asset-id', id: 'asset-id',
status: AssetStatus.ACTIVE,
deviceAssetId: 'device-asset-id', deviceAssetId: 'device-asset-id',
fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'),
fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'),
@ -843,6 +866,7 @@ export const assetStub = {
}), }),
hasFileExtension: Object.freeze<AssetEntity>({ hasFileExtension: Object.freeze<AssetEntity>({
id: 'asset-id', id: 'asset-id',
status: AssetStatus.ACTIVE,
deviceAssetId: 'device-asset-id', deviceAssetId: 'device-asset-id',
fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'),
fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'),
@ -881,6 +905,7 @@ export const assetStub = {
}), }),
imageDng: Object.freeze<AssetEntity>({ imageDng: Object.freeze<AssetEntity>({
id: 'asset-id', id: 'asset-id',
status: AssetStatus.ACTIVE,
deviceAssetId: 'device-asset-id', deviceAssetId: 'device-asset-id',
fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'),
fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'),
@ -919,6 +944,7 @@ export const assetStub = {
}), }),
hasEmbedding: Object.freeze<AssetEntity>({ hasEmbedding: Object.freeze<AssetEntity>({
id: 'asset-id-embedding', id: 'asset-id-embedding',
status: AssetStatus.ACTIVE,
deviceAssetId: 'device-asset-id', deviceAssetId: 'device-asset-id',
fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'),
fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'),
@ -959,6 +985,7 @@ export const assetStub = {
}), }),
hasDupe: Object.freeze<AssetEntity>({ hasDupe: Object.freeze<AssetEntity>({
id: 'asset-id-dupe', id: 'asset-id-dupe',
status: AssetStatus.ACTIVE,
deviceAssetId: 'device-asset-id', deviceAssetId: 'device-asset-id',
fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'),
fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'),

View File

@ -5,7 +5,7 @@ import { SharedLinkResponseDto } from 'src/dtos/shared-link.dto';
import { mapUser } from 'src/dtos/user.dto'; import { mapUser } from 'src/dtos/user.dto';
import { SharedLinkEntity } from 'src/entities/shared-link.entity'; import { SharedLinkEntity } from 'src/entities/shared-link.entity';
import { UserEntity } from 'src/entities/user.entity'; import { UserEntity } from 'src/entities/user.entity';
import { AssetOrder, AssetType, SharedLinkType } from 'src/enum'; import { AssetOrder, AssetStatus, AssetType, SharedLinkType } from 'src/enum';
import { assetStub } from 'test/fixtures/asset.stub'; import { assetStub } from 'test/fixtures/asset.stub';
import { authStub } from 'test/fixtures/auth.stub'; import { authStub } from 'test/fixtures/auth.stub';
import { userStub } from 'test/fixtures/user.stub'; import { userStub } from 'test/fixtures/user.stub';
@ -188,6 +188,7 @@ export const sharedLinkStub = {
assets: [ assets: [
{ {
id: 'id_1', id: 'id_1',
status: AssetStatus.ACTIVE,
owner: undefined as unknown as UserEntity, owner: undefined as unknown as UserEntity,
ownerId: 'user_id_1', ownerId: 'user_id_1',
deviceAssetId: 'device_asset_id_1', deviceAssetId: 'device_asset_id_1',

View File

@ -34,8 +34,6 @@ export const newAssetRepositoryMock = (): Mocked<IAssetRepository> => {
getStatistics: vitest.fn(), getStatistics: vitest.fn(),
getTimeBucket: vitest.fn(), getTimeBucket: vitest.fn(),
getTimeBuckets: vitest.fn(), getTimeBuckets: vitest.fn(),
restoreAll: vitest.fn(),
softDeleteAll: vitest.fn(),
getAssetIdByCity: vitest.fn(), getAssetIdByCity: vitest.fn(),
getAssetIdByTag: vitest.fn(), getAssetIdByTag: vitest.fn(),
getAllForUserFullSync: vitest.fn(), getAllForUserFullSync: vitest.fn(),

View File

@ -0,0 +1,11 @@
import { ITrashRepository } from 'src/interfaces/trash.interface';
import { Mocked, vitest } from 'vitest';
export const newTrashRepositoryMock = (): Mocked<ITrashRepository> => {
return {
empty: vitest.fn(),
restore: vitest.fn(),
restoreAll: vitest.fn(),
getDeletedIds: vitest.fn(),
};
};

View File

@ -33,7 +33,8 @@
handlePromiseError(goto(AppRoute.PHOTOS)); handlePromiseError(goto(AppRoute.PHOTOS));
} }
const assetStore = new AssetStore({ isTrashed: true }); const options = { isTrashed: true };
const assetStore = new AssetStore(options);
const assetInteractionStore = createAssetInteractionStore(); const assetInteractionStore = createAssetInteractionStore();
const { isMultiSelectState, selectedAssets } = assetInteractionStore; const { isMultiSelectState, selectedAssets } = assetInteractionStore;
@ -47,16 +48,15 @@
} }
try { try {
await emptyTrash(); const { count } = await emptyTrash();
const deletedAssetIds = assetStore.assets.map((a) => a.id);
const numberOfAssets = deletedAssetIds.length;
assetStore.removeAssets(deletedAssetIds);
notificationController.show({ notificationController.show({
message: $t('assets_permanently_deleted_count', { values: { count: numberOfAssets } }), message: $t('assets_permanently_deleted_count', { values: { count } }),
type: NotificationType.Info, type: NotificationType.Info,
}); });
// reset asset grid (TODO fix in asset store that it should reset when it is empty)
await assetStore.updateOptions(options);
} catch (error) { } catch (error) {
handleError(error, $t('errors.unable_to_empty_trash')); handleError(error, $t('errors.unable_to_empty_trash'));
} }
@ -71,16 +71,14 @@
return; return;
} }
try { try {
await restoreTrash(); const { count } = await restoreTrash();
const restoredAssetIds = assetStore.assets.map((a) => a.id);
const numberOfAssets = restoredAssetIds.length;
assetStore.removeAssets(restoredAssetIds);
notificationController.show({ notificationController.show({
message: $t('assets_restored_count', { values: { count: numberOfAssets } }), message: $t('assets_restored_count', { values: { count } }),
type: NotificationType.Info, type: NotificationType.Info,
}); });
// reset asset grid (TODO fix in asset store that it should reset when it is empty)
await assetStore.updateOptions(options);
} catch (error) { } catch (error) {
handleError(error, $t('errors.unable_to_restore_trash')); handleError(error, $t('errors.unable_to_restore_trash'));
} }