mirror of
https://github.com/immich-app/immich
synced 2025-11-14 17:36:12 +00:00
feat(server): trash asset (#4015)
* refactor(server): delete assets endpoint * fix: formatting * chore: cleanup * chore: open api * chore(mobile): replace DeleteAssetDTO with BulkIdsDTOs * feat: trash an asset * chore(server): formatting * chore: open api * chore: wording * chore: open-api * feat(server): add withDeleted to getAssets queries * WIP: mobile-recycle-bin * feat(server): recycle-bin to system config * feat(web): use recycle-bin system config * chore(server): domain assetcore removed * chore(server): rename recycle-bin to trash * chore(web): rename recycle-bin to trash * chore(server): always send soft deleted assets for getAllByUserId * chore(web): formatting * feat(server): permanent delete assets older than trashed period * feat(web): trash empty placeholder image * feat(server): empty trash * feat(web): empty trash * WIP: mobile-recycle-bin * refactor(server): empty / restore trash to separate endpoint * test(server): handle failures * test(server): fix e2e server-info test * test(server): deletion test refactor * feat(mobile): use map settings from server-config to enable / disable map * feat(mobile): trash asset * fix(server): operations on assets in trash * feat(web): show trash statistics * fix(web): handle trash enabled * fix(mobile): restore updates from trash * fix(server): ignore trashed assets for person * fix(server): add / remove search index when trashed / restored * chore(web): format * fix(server): asset service test * fix(server): include trashed assts for duplicates from uploads * feat(mobile): no dialog for trash, always dialog for permanent delete * refactor(mobile): use isar where instead of dart filter * refactor(mobile): asset provide - handle deletes in single db txn * chore(mobile): review changes * feat(web): confirmation before empty trash * server: review changes * fix(server): handle library changes * fix: filter external assets from getting trashed / deleted * fix(server): empty-bin * feat: broadcast config update events through ws * change order of trash button on mobile * styling * fix(mobile): do not show trashed toast for local only assets --------- Co-authored-by: Jason Rasmussen <jrasm91@gmail.com> Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
parent
fc93762230
commit
4a8887f37b
117 changed files with 3155 additions and 928 deletions
|
|
@ -1,6 +1,7 @@
|
|||
import { AssetEntity } from '@app/infra/entities';
|
||||
import { AssetEntity, LibraryType } from '@app/infra/entities';
|
||||
import { BadRequestException, Inject, Logger } from '@nestjs/common';
|
||||
import _ from 'lodash';
|
||||
import { DateTime, Duration } from 'luxon';
|
||||
import { extname } from 'path';
|
||||
import sanitize from 'sanitize-filename';
|
||||
import { AccessCore, IAccessRepository, Permission } from '../access';
|
||||
|
|
@ -8,10 +9,12 @@ import { AuthUserDto } from '../auth';
|
|||
import { ICryptoRepository } from '../crypto';
|
||||
import { mimeTypes } from '../domain.constant';
|
||||
import { HumanReadableSize, usePagination } from '../domain.util';
|
||||
import { IJobRepository, JobName } from '../job';
|
||||
import { IAssetDeletionJob, IJobRepository, JOBS_ASSET_PAGINATION_SIZE, JobName } from '../job';
|
||||
import { IStorageRepository, ImmichReadStream, StorageCore, StorageFolder } from '../storage';
|
||||
import { ISystemConfigRepository, SystemConfigCore } from '../system-config';
|
||||
import { IAssetRepository } from './asset.repository';
|
||||
import {
|
||||
AssetBulkDeleteDto,
|
||||
AssetBulkUpdateDto,
|
||||
AssetIdsDto,
|
||||
AssetJobName,
|
||||
|
|
@ -24,11 +27,13 @@ import {
|
|||
MemoryLaneDto,
|
||||
TimeBucketAssetDto,
|
||||
TimeBucketDto,
|
||||
TrashAction,
|
||||
UpdateAssetDto,
|
||||
mapStats,
|
||||
} from './dto';
|
||||
import {
|
||||
AssetResponseDto,
|
||||
BulkIdsDto,
|
||||
MapMarkerResponseDto,
|
||||
MemoryLaneResponseDto,
|
||||
TimeBucketResponseDto,
|
||||
|
|
@ -57,6 +62,7 @@ export interface UploadFile {
|
|||
export class AssetService {
|
||||
private logger = new Logger(AssetService.name);
|
||||
private access: AccessCore;
|
||||
private configCore: SystemConfigCore;
|
||||
private storageCore: StorageCore;
|
||||
|
||||
constructor(
|
||||
|
|
@ -64,10 +70,12 @@ export class AssetService {
|
|||
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
|
||||
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
|
||||
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
||||
@Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
|
||||
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
||||
) {
|
||||
this.access = new AccessCore(accessRepository);
|
||||
this.storageCore = new StorageCore(storageRepository);
|
||||
this.configCore = new SystemConfigCore(configRepository);
|
||||
}
|
||||
|
||||
canUploadFile({ authUser, fieldName, file }: UploadRequest): true {
|
||||
|
|
@ -274,7 +282,9 @@ export class AssetService {
|
|||
if (dto.userId) {
|
||||
const userId = dto.userId;
|
||||
await this.access.requirePermission(authUser, Permission.TIMELINE_DOWNLOAD, userId);
|
||||
return usePagination(PAGINATION_SIZE, (pagination) => this.assetRepository.getByUserId(pagination, userId));
|
||||
return usePagination(PAGINATION_SIZE, (pagination) =>
|
||||
this.assetRepository.getByUserId(pagination, userId, { isVisible: true }),
|
||||
);
|
||||
}
|
||||
|
||||
throw new BadRequestException('assetIds, albumId, or userId is required');
|
||||
|
|
@ -303,13 +313,119 @@ export class AssetService {
|
|||
return mapAsset(asset);
|
||||
}
|
||||
|
||||
async updateAll(authUser: AuthUserDto, dto: AssetBulkUpdateDto) {
|
||||
async updateAll(authUser: AuthUserDto, dto: AssetBulkUpdateDto): Promise<void> {
|
||||
const { ids, ...options } = dto;
|
||||
await this.access.requirePermission(authUser, Permission.ASSET_UPDATE, ids);
|
||||
await this.jobRepository.queue({ name: JobName.SEARCH_INDEX_ASSET, data: { ids } });
|
||||
await this.assetRepository.updateAll(ids, options);
|
||||
}
|
||||
|
||||
async handleAssetDeletionCheck() {
|
||||
const config = await this.configCore.getConfig();
|
||||
const trashedDays = config.trash.enabled ? config.trash.days : 0;
|
||||
const trashedBefore = DateTime.now()
|
||||
.minus(Duration.fromObject({ days: trashedDays }))
|
||||
.toJSDate();
|
||||
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) =>
|
||||
this.assetRepository.getAll(pagination, { trashedBefore }),
|
||||
);
|
||||
|
||||
for await (const assets of assetPagination) {
|
||||
for (const asset of assets) {
|
||||
await this.jobRepository.queue({ name: JobName.ASSET_DELETION, data: { id: asset.id } });
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async handleAssetDeletion(job: IAssetDeletionJob) {
|
||||
const { id, fromExternal } = job;
|
||||
|
||||
const asset = await this.assetRepository.getById(id);
|
||||
if (!asset) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ignore requests that are not from external library job but is for an external asset
|
||||
if (!fromExternal && (!asset.library || asset.library.type === LibraryType.EXTERNAL)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (asset.faces) {
|
||||
await Promise.all(
|
||||
asset.faces.map(({ assetId, personId }) =>
|
||||
this.jobRepository.queue({ name: JobName.SEARCH_REMOVE_FACE, data: { assetId, personId } }),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
await this.assetRepository.remove(asset);
|
||||
await this.jobRepository.queue({ name: JobName.SEARCH_REMOVE_ASSET, data: { ids: [asset.id] } });
|
||||
|
||||
// TODO refactor this to use cascades
|
||||
if (asset.livePhotoVideoId) {
|
||||
await this.jobRepository.queue({ name: JobName.ASSET_DELETION, data: { id: asset.livePhotoVideoId } });
|
||||
}
|
||||
|
||||
const files = [asset.webpPath, asset.resizePath, asset.encodedVideoPath, asset.sidecarPath];
|
||||
if (!fromExternal) {
|
||||
files.push(asset.originalPath);
|
||||
}
|
||||
|
||||
if (!asset.isReadOnly) {
|
||||
await this.jobRepository.queue({ name: JobName.DELETE_FILES, data: { files } });
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async deleteAll(authUser: AuthUserDto, dto: AssetBulkDeleteDto): Promise<void> {
|
||||
const { ids, force } = dto;
|
||||
|
||||
await this.access.requirePermission(authUser, Permission.ASSET_DELETE, ids);
|
||||
|
||||
if (force) {
|
||||
for (const id of ids) {
|
||||
await this.jobRepository.queue({ name: JobName.ASSET_DELETION, data: { id } });
|
||||
}
|
||||
} else {
|
||||
await this.assetRepository.softDeleteAll(ids);
|
||||
await this.jobRepository.queue({ name: JobName.SEARCH_REMOVE_ASSET, data: { ids } });
|
||||
}
|
||||
}
|
||||
|
||||
async handleTrashAction(authUser: AuthUserDto, action: TrashAction): Promise<void> {
|
||||
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) =>
|
||||
this.assetRepository.getByUserId(pagination, authUser.id, { trashedBefore: DateTime.now().toJSDate() }),
|
||||
);
|
||||
|
||||
if (action == TrashAction.RESTORE_ALL) {
|
||||
for await (const assets of assetPagination) {
|
||||
const ids = assets.map((a) => a.id);
|
||||
await this.assetRepository.restoreAll(ids);
|
||||
await this.jobRepository.queue({ name: JobName.SEARCH_INDEX_ASSET, data: { ids } });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (action == TrashAction.EMPTY_ALL) {
|
||||
for await (const assets of assetPagination) {
|
||||
for (const asset of assets) {
|
||||
await this.jobRepository.queue({ name: JobName.ASSET_DELETION, data: { id: asset.id } });
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
async restoreAll(authUser: AuthUserDto, dto: BulkIdsDto): Promise<void> {
|
||||
const { ids } = dto;
|
||||
await this.access.requirePermission(authUser, Permission.ASSET_RESTORE, ids);
|
||||
await this.assetRepository.restoreAll(ids);
|
||||
await this.jobRepository.queue({ name: JobName.SEARCH_INDEX_ASSET, data: { ids } });
|
||||
}
|
||||
|
||||
async run(authUser: AuthUserDto, dto: AssetJobsDto) {
|
||||
await this.access.requirePermission(authUser, Permission.ASSET_UPDATE, dto.assetIds);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue