mirror of
https://github.com/immich-app/immich.git
synced 2024-12-29 15:11:58 +00:00
fix: empty and restore over 1,000 items (#12751)
This commit is contained in:
parent
4f25cec6df
commit
6740c67ed8
39 changed files with 411 additions and 142 deletions
|
@ -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);
|
||||||
|
|
BIN
mobile/openapi/README.md
generated
BIN
mobile/openapi/README.md
generated
Binary file not shown.
BIN
mobile/openapi/lib/api.dart
generated
BIN
mobile/openapi/lib/api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api/trash_api.dart
generated
BIN
mobile/openapi/lib/api/trash_api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api_client.dart
generated
BIN
mobile/openapi/lib/api_client.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/trash_response_dto.dart
generated
Normal file
BIN
mobile/openapi/lib/model/trash_response_dto.dart
generated
Normal file
Binary file not shown.
|
@ -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": {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
6
server/src/dtos/trash.dto.ts
Normal file
6
server/src/dtos/trash.dto.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export class TrashResponseDto {
|
||||||
|
@ApiProperty({ type: 'integer' })
|
||||||
|
count!: number;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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[]>;
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
10
server/src/interfaces/trash.interface.ts
Normal file
10
server/src/interfaces/trash.interface.ts
Normal 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>;
|
||||||
|
}
|
16
server/src/migrations/1726593009549-AddAssetStatus.ts
Normal file
16
server/src/migrations/1726593009549-AddAssetStatus.ts
Normal 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"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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 },
|
||||||
];
|
];
|
||||||
|
|
|
@ -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()
|
||||||
|
|
49
server/src/repositories/trash.repository.ts
Normal file
49
server/src/repositories/trash.repository.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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] },
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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([]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 },
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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 });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
31
server/test/fixtures/asset.stub.ts
vendored
31
server/test/fixtures/asset.stub.ts
vendored
|
@ -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'),
|
||||||
|
|
3
server/test/fixtures/shared-link.stub.ts
vendored
3
server/test/fixtures/shared-link.stub.ts
vendored
|
@ -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',
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
11
server/test/repositories/trash.repository.mock.ts
Normal file
11
server/test/repositories/trash.repository.mock.ts
Normal 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(),
|
||||||
|
};
|
||||||
|
};
|
|
@ -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'));
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue