diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 93cd0c714a..1c28bbdd43 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -161,7 +161,7 @@ Class | Method | HTTP request | Description *LibrariesApi* | [**scanLibrary**](doc//LibrariesApi.md#scanlibrary) | **POST** /libraries/{id}/scan | Scan a library *LibrariesApi* | [**updateLibrary**](doc//LibrariesApi.md#updatelibrary) | **PUT** /libraries/{id} | Update a library *LibrariesApi* | [**validate**](doc//LibrariesApi.md#validate) | **POST** /libraries/{id}/validate | Validate library settings -*MaintenanceAdminApi* | [**deleteIntegrityReportFile**](doc//MaintenanceAdminApi.md#deleteintegrityreportfile) | **DELETE** /admin/maintenance/integrity/report/{id}/file | Delete associated file if it exists +*MaintenanceAdminApi* | [**deleteIntegrityReport**](doc//MaintenanceAdminApi.md#deleteintegrityreport) | **DELETE** /admin/maintenance/integrity/report/{id} | Delete report entry and perform corresponding deletion action *MaintenanceAdminApi* | [**getIntegrityReport**](doc//MaintenanceAdminApi.md#getintegrityreport) | **POST** /admin/maintenance/integrity/report | Get integrity report by type *MaintenanceAdminApi* | [**getIntegrityReportCsv**](doc//MaintenanceAdminApi.md#getintegrityreportcsv) | **GET** /admin/maintenance/integrity/report/{type}/csv | Export integrity report by type as CSV *MaintenanceAdminApi* | [**getIntegrityReportFile**](doc//MaintenanceAdminApi.md#getintegrityreportfile) | **GET** /admin/maintenance/integrity/report/{id}/file | Download the orphan/broken file if one exists diff --git a/mobile/openapi/lib/api/maintenance_admin_api.dart b/mobile/openapi/lib/api/maintenance_admin_api.dart index 96fb5920ff..3dcbea1426 100644 --- a/mobile/openapi/lib/api/maintenance_admin_api.dart +++ b/mobile/openapi/lib/api/maintenance_admin_api.dart @@ -16,7 +16,7 @@ class MaintenanceAdminApi { final ApiClient apiClient; - /// Delete associated file if it exists + /// Delete report entry and perform corresponding deletion action /// /// ... /// @@ -25,9 +25,9 @@ class MaintenanceAdminApi { /// Parameters: /// /// * [String] id (required): - Future deleteIntegrityReportFileWithHttpInfo(String id,) async { + Future deleteIntegrityReportWithHttpInfo(String id,) async { // ignore: prefer_const_declarations - final apiPath = r'/admin/maintenance/integrity/report/{id}/file' + final apiPath = r'/admin/maintenance/integrity/report/{id}' .replaceAll('{id}', id); // ignore: prefer_final_locals @@ -51,15 +51,15 @@ class MaintenanceAdminApi { ); } - /// Delete associated file if it exists + /// Delete report entry and perform corresponding deletion action /// /// ... /// /// Parameters: /// /// * [String] id (required): - Future deleteIntegrityReportFile(String id,) async { - final response = await deleteIntegrityReportFileWithHttpInfo(id,); + Future deleteIntegrityReport(String id,) async { + final response = await deleteIntegrityReportWithHttpInfo(id,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/model/job_name.dart b/mobile/openapi/lib/model/job_name.dart index a624f6b035..4572ccf7a4 100644 --- a/mobile/openapi/lib/model/job_name.dart +++ b/mobile/openapi/lib/model/job_name.dart @@ -86,6 +86,7 @@ class JobName { static const integrityMissingFilesRefresh = JobName._(r'IntegrityMissingFilesRefresh'); static const integrityChecksumFiles = JobName._(r'IntegrityChecksumFiles'); static const integrityChecksumFilesRefresh = JobName._(r'IntegrityChecksumFilesRefresh'); + static const integrityReportDelete = JobName._(r'IntegrityReportDelete'); /// List of all possible values in this [enum][JobName]. static const values = [ @@ -152,6 +153,7 @@ class JobName { integrityMissingFilesRefresh, integrityChecksumFiles, integrityChecksumFilesRefresh, + integrityReportDelete, ]; static JobName? fromJson(dynamic value) => JobNameTypeTransformer().decode(value); @@ -253,6 +255,7 @@ class JobNameTypeTransformer { case r'IntegrityMissingFilesRefresh': return JobName.integrityMissingFilesRefresh; case r'IntegrityChecksumFiles': return JobName.integrityChecksumFiles; case r'IntegrityChecksumFilesRefresh': return JobName.integrityChecksumFilesRefresh; + case r'IntegrityReportDelete': return JobName.integrityReportDelete; default: if (!allowNull) { throw ArgumentError('Unknown enum value to decode: $data'); diff --git a/mobile/openapi/lib/model/manual_job_name.dart b/mobile/openapi/lib/model/manual_job_name.dart index 424dc60e42..60d14e6ef7 100644 --- a/mobile/openapi/lib/model/manual_job_name.dart +++ b/mobile/openapi/lib/model/manual_job_name.dart @@ -35,6 +35,9 @@ class ManualJobName { static const integrityMissingFilesRefresh = ManualJobName._(r'integrity-missing-files-refresh'); static const integrityOrphanFilesRefresh = ManualJobName._(r'integrity-orphan-files-refresh'); static const integrityChecksumMismatchRefresh = ManualJobName._(r'integrity-checksum-mismatch-refresh'); + static const integrityMissingFilesDeleteAll = ManualJobName._(r'integrity-missing-files-delete-all'); + static const integrityOrphanFilesDeleteAll = ManualJobName._(r'integrity-orphan-files-delete-all'); + static const integrityChecksumMismatchDeleteAll = ManualJobName._(r'integrity-checksum-mismatch-delete-all'); /// List of all possible values in this [enum][ManualJobName]. static const values = [ @@ -50,6 +53,9 @@ class ManualJobName { integrityMissingFilesRefresh, integrityOrphanFilesRefresh, integrityChecksumMismatchRefresh, + integrityMissingFilesDeleteAll, + integrityOrphanFilesDeleteAll, + integrityChecksumMismatchDeleteAll, ]; static ManualJobName? fromJson(dynamic value) => ManualJobNameTypeTransformer().decode(value); @@ -100,6 +106,9 @@ class ManualJobNameTypeTransformer { case r'integrity-missing-files-refresh': return ManualJobName.integrityMissingFilesRefresh; case r'integrity-orphan-files-refresh': return ManualJobName.integrityOrphanFilesRefresh; case r'integrity-checksum-mismatch-refresh': return ManualJobName.integrityChecksumMismatchRefresh; + case r'integrity-missing-files-delete-all': return ManualJobName.integrityMissingFilesDeleteAll; + case r'integrity-orphan-files-delete-all': return ManualJobName.integrityOrphanFilesDeleteAll; + case r'integrity-checksum-mismatch-delete-all': return ManualJobName.integrityChecksumMismatchDeleteAll; default: if (!allowNull) { throw ArgumentError('Unknown enum value to decode: $data'); diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 928294b757..59bf9d58d7 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -429,10 +429,10 @@ "x-immich-state": "Alpha" } }, - "/admin/maintenance/integrity/report/{id}/file": { + "/admin/maintenance/integrity/report/{id}": { "delete": { "description": "...", - "operationId": "deleteIntegrityReportFile", + "operationId": "deleteIntegrityReport", "parameters": [ { "name": "id", @@ -460,7 +460,7 @@ "api_key": [] } ], - "summary": "Delete associated file if it exists", + "summary": "Delete report entry and perform corresponding deletion action", "tags": [ "Maintenance (admin)" ], @@ -477,7 +477,9 @@ ], "x-immich-permission": "maintenance", "x-immich-state": "Alpha" - }, + } + }, + "/admin/maintenance/integrity/report/{id}/file": { "get": { "description": "...", "operationId": "getIntegrityReportFile", @@ -16943,7 +16945,8 @@ "IntegrityMissingFiles", "IntegrityMissingFilesRefresh", "IntegrityChecksumFiles", - "IntegrityChecksumFilesRefresh" + "IntegrityChecksumFilesRefresh", + "IntegrityReportDelete" ], "type": "string" }, @@ -17289,7 +17292,10 @@ "integrity-checksum-mismatch", "integrity-missing-files-refresh", "integrity-orphan-files-refresh", - "integrity-checksum-mismatch-refresh" + "integrity-checksum-mismatch-refresh", + "integrity-missing-files-delete-all", + "integrity-orphan-files-delete-all", + "integrity-checksum-mismatch-delete-all" ], "type": "string" }, diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 9d1b69146f..8f50cb1664 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -1910,12 +1910,12 @@ export function getIntegrityReport({ maintenanceGetIntegrityReportDto }: { }))); } /** - * Delete associated file if it exists + * Delete report entry and perform corresponding deletion action */ -export function deleteIntegrityReportFile({ id }: { +export function deleteIntegrityReport({ id }: { id: string; }, opts?: Oazapfts.RequestOpts) { - return oazapfts.ok(oazapfts.fetchText(`/admin/maintenance/integrity/report/${encodeURIComponent(id)}/file`, { + return oazapfts.ok(oazapfts.fetchText(`/admin/maintenance/integrity/report/${encodeURIComponent(id)}`, { ...opts, method: "DELETE" })); @@ -5484,7 +5484,10 @@ export enum ManualJobName { IntegrityChecksumMismatch = "integrity-checksum-mismatch", IntegrityMissingFilesRefresh = "integrity-missing-files-refresh", IntegrityOrphanFilesRefresh = "integrity-orphan-files-refresh", - IntegrityChecksumMismatchRefresh = "integrity-checksum-mismatch-refresh" + IntegrityChecksumMismatchRefresh = "integrity-checksum-mismatch-refresh", + IntegrityMissingFilesDeleteAll = "integrity-missing-files-delete-all", + IntegrityOrphanFilesDeleteAll = "integrity-orphan-files-delete-all", + IntegrityChecksumMismatchDeleteAll = "integrity-checksum-mismatch-delete-all" } export enum QueueName { ThumbnailGeneration = "thumbnailGeneration", @@ -5600,7 +5603,8 @@ export enum JobName { IntegrityMissingFiles = "IntegrityMissingFiles", IntegrityMissingFilesRefresh = "IntegrityMissingFilesRefresh", IntegrityChecksumFiles = "IntegrityChecksumFiles", - IntegrityChecksumFilesRefresh = "IntegrityChecksumFilesRefresh" + IntegrityChecksumFilesRefresh = "IntegrityChecksumFilesRefresh", + IntegrityReportDelete = "IntegrityReportDelete" } export enum SearchSuggestionType { Country = "country", diff --git a/server/src/controllers/maintenance.controller.ts b/server/src/controllers/maintenance.controller.ts index a930d1c6e1..553dfeea6b 100644 --- a/server/src/controllers/maintenance.controller.ts +++ b/server/src/controllers/maintenance.controller.ts @@ -82,6 +82,17 @@ export class MaintenanceController { return this.service.getIntegrityReport(dto); } + @Delete('integrity/report/:id') + @Endpoint({ + summary: 'Delete report entry and perform corresponding deletion action', + description: '...', + history: new HistoryBuilder().added('v9.9.9').alpha('v9.9.9'), + }) + @Authenticated({ permission: Permission.Maintenance, admin: true }) + async deleteIntegrityReport(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { + await this.service.deleteIntegrityReport(auth, id); + } + @Get('integrity/report/:type/csv') @Endpoint({ summary: 'Export integrity report by type as CSV', @@ -113,15 +124,4 @@ export class MaintenanceController { ): Promise { await sendFile(res, next, () => this.service.getIntegrityReportFile(id), this.logger); } - - @Delete('integrity/report/:id/file') - @Endpoint({ - summary: 'Delete associated file if it exists', - description: '...', - history: new HistoryBuilder().added('v9.9.9').alpha('v9.9.9'), - }) - @Authenticated({ permission: Permission.Maintenance, admin: true }) - async deleteIntegrityReportFile(@Param() { id }: UUIDParamDto): Promise { - await this.service.deleteIntegrityReportFile(id); - } } diff --git a/server/src/dtos/maintenance.dto.ts b/server/src/dtos/maintenance.dto.ts index c4bf26a315..e76a40dd9b 100644 --- a/server/src/dtos/maintenance.dto.ts +++ b/server/src/dtos/maintenance.dto.ts @@ -37,6 +37,11 @@ export class MaintenanceGetIntegrityReportDto { // page?: number; } +export class MaintenanceDeleteIntegrityReportDto { + @ValidateEnum({ enum: IntegrityReportType, name: 'IntegrityReportType' }) + type!: IntegrityReportType; +} + class MaintenanceIntegrityReportDto { id!: string; @ValidateEnum({ enum: IntegrityReportType, name: 'IntegrityReportType' }) diff --git a/server/src/enum.ts b/server/src/enum.ts index 35f724f985..6203e4f892 100644 --- a/server/src/enum.ts +++ b/server/src/enum.ts @@ -364,6 +364,9 @@ export enum ManualJobName { IntegrityMissingFilesRefresh = `integrity-missing-files-refresh`, IntegrityOrphanFilesRefresh = `integrity-orphan-files-refresh`, IntegrityChecksumFilesRefresh = `integrity-checksum-mismatch-refresh`, + IntegrityMissingFilesDeleteAll = `integrity-missing-files-delete-all`, + IntegrityOrphanFilesDeleteAll = `integrity-orphan-files-delete-all`, + IntegrityChecksumFilesDeleteAll = `integrity-checksum-mismatch-delete-all`, } export enum AssetPathType { @@ -660,6 +663,7 @@ export enum JobName { IntegrityMissingFilesRefresh = 'IntegrityMissingFilesRefresh', IntegrityChecksumFiles = 'IntegrityChecksumFiles', IntegrityChecksumFilesRefresh = 'IntegrityChecksumFilesRefresh', + IntegrityReportDelete = 'IntegrityReportDelete', } export enum QueueCommand { diff --git a/server/src/repositories/integrity-report.repository.ts b/server/src/repositories/integrity-report.repository.ts index fe8c99818f..5bdf31eedd 100644 --- a/server/src/repositories/integrity-report.repository.ts +++ b/server/src/repositories/integrity-report.repository.ts @@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common'; import { Insertable, Kysely } from 'kysely'; import { InjectKysely } from 'nestjs-kysely'; import { Readable } from 'node:stream'; +import { DummyValue, GenerateSql } from 'src/decorators'; import { MaintenanceGetIntegrityReportDto, MaintenanceIntegrityReportResponseDto, @@ -101,4 +102,15 @@ export class IntegrityReportRepository { deleteByIds(ids: string[]) { return this.db.deleteFrom('integrity_report').where('id', 'in', ids).execute(); } + + @GenerateSql({ params: [DummyValue.STRING], stream: true }) + streamIntegrityReportsByProperty(property?: 'assetId' | 'fileAssetId', filterType?: IntegrityReportType) { + return this.db + .selectFrom('integrity_report') + .select(['id', 'path', 'assetId', 'fileAssetId']) + .$if(filterType !== undefined, (eb) => eb.where('type', '=', filterType!)) + .$if(property === undefined, (eb) => eb.where('assetId', 'is', null).where('fileAssetId', 'is', null)) + .$if(property !== undefined, (eb) => eb.where(property!, 'is not', null)) + .stream(); + } } diff --git a/server/src/services/integrity.service.ts b/server/src/services/integrity.service.ts index e161b40662..27f797bf15 100644 --- a/server/src/services/integrity.service.ts +++ b/server/src/services/integrity.service.ts @@ -8,6 +8,7 @@ import { JOBS_LIBRARY_PAGINATION_SIZE } from 'src/constants'; import { StorageCore } from 'src/cores/storage.core'; import { OnEvent, OnJob } from 'src/decorators'; import { + AssetStatus, DatabaseLock, ImmichWorker, IntegrityReportType, @@ -20,6 +21,7 @@ import { import { ArgOf } from 'src/repositories/event.repository'; import { BaseService } from 'src/services/base.service'; import { + IIntegrityDeleteReportJob, IIntegrityJob, IIntegrityMissingFilesJob, IIntegrityOrphanedFilesJob, @@ -42,7 +44,7 @@ import { handlePromiseError } from 'src/utils/misc'; * Check whether files exist on disk * * * Reports must include origin (asset or asset_file) & ID for further action - * * Can perform trash (asset) or dereference (asset_file) + * * Can perform trash (asset) or delete (asset_file) * * Checksum Mismatch: * Paths & checksums are queried from asset(originalPath, checksum) @@ -548,6 +550,68 @@ export class IntegrityService extends BaseService { this.logger.log(`Processed ${paths.length} paths and found ${reportIds.length} report(s) out of date.`); return JobStatus.Success; } + + @OnJob({ name: JobName.IntegrityReportDelete, queue: QueueName.BackgroundTask }) + async handleDeleteIntegrityReport({ type }: IIntegrityDeleteReportJob): Promise { + this.logger.log(`Deleting all entries for ${type ?? 'all types of'} integrity report`); + + let properties; + switch (type) { + case IntegrityReportType.ChecksumFail: { + properties = ['assetId'] as const; + break; + } + case IntegrityReportType.MissingFile: { + properties = ['assetId', 'fileAssetId'] as const; + break; + } + case IntegrityReportType.OrphanFile: { + properties = [void 0] as const; + break; + } + default: { + properties = [void 0, 'assetId', 'fileAssetId'] as const; + break; + } + } + + for (const property of properties) { + const reports = this.integrityReportRepository.streamIntegrityReportsByProperty(property, type); + for await (const report of chunk(reports, JOBS_LIBRARY_PAGINATION_SIZE)) { + // todo: queue sub-job here instead? + + switch (property) { + case 'assetId': { + const ids = report.map(({ assetId }) => assetId!); + await this.assetRepository.updateAll(ids, { + deletedAt: new Date(), + status: AssetStatus.Trashed, + }); + + await this.eventRepository.emit('AssetTrashAll', { + assetIds: ids, + userId: '', // ??? + }); + + await this.integrityReportRepository.deleteByIds(report.map(({ id }) => id)); + break; + } + case 'fileAssetId': { + await this.assetRepository.deleteFiles(report.map(({ fileAssetId }) => ({ id: fileAssetId! }))); + break; + } + default: { + await Promise.all(report.map(({ path }) => this.storageRepository.unlink(path).catch(() => void 0))); + await this.integrityReportRepository.deleteByIds(report.map(({ id }) => id)); + break; + } + } + } + } + + this.logger.log('Finished deleting integrity report.'); + return JobStatus.Success; + } } async function* chunk(generator: AsyncIterableIterator, n: number) { diff --git a/server/src/services/job.service.ts b/server/src/services/job.service.ts index cdb7f06e4e..d0b41572b6 100644 --- a/server/src/services/job.service.ts +++ b/server/src/services/job.service.ts @@ -2,7 +2,7 @@ import { BadRequestException, Injectable } from '@nestjs/common'; import { OnEvent } from 'src/decorators'; import { mapAsset } from 'src/dtos/asset-response.dto'; import { JobCreateDto } from 'src/dtos/job.dto'; -import { AssetType, AssetVisibility, JobName, JobStatus, ManualJobName } from 'src/enum'; +import { AssetType, AssetVisibility, IntegrityReportType, JobName, JobStatus, ManualJobName } from 'src/enum'; import { ArgsOf } from 'src/repositories/event.repository'; import { BaseService } from 'src/services/base.service'; import { JobItem } from 'src/types'; @@ -58,6 +58,18 @@ const asJobItem = (dto: JobCreateDto): JobItem => { return { name: JobName.IntegrityChecksumFiles, data: { refreshOnly: true } }; } + case ManualJobName.IntegrityMissingFilesDeleteAll: { + return { name: JobName.IntegrityReportDelete, data: { type: IntegrityReportType.MissingFile } }; + } + + case ManualJobName.IntegrityOrphanFilesDeleteAll: { + return { name: JobName.IntegrityReportDelete, data: { type: IntegrityReportType.OrphanFile } }; + } + + case ManualJobName.IntegrityChecksumFilesDeleteAll: { + return { name: JobName.IntegrityReportDelete, data: { type: IntegrityReportType.ChecksumFail } }; + } + default: { throw new BadRequestException('Invalid job name'); } diff --git a/server/src/services/maintenance.service.ts b/server/src/services/maintenance.service.ts index 0d13702363..844d5b8a42 100644 --- a/server/src/services/maintenance.service.ts +++ b/server/src/services/maintenance.service.ts @@ -2,13 +2,14 @@ import { Injectable } from '@nestjs/common'; import { basename } from 'node:path'; import { Readable } from 'node:stream'; import { OnEvent } from 'src/decorators'; +import { AuthDto } from 'src/dtos/auth.dto'; import { MaintenanceAuthDto, MaintenanceGetIntegrityReportDto, MaintenanceIntegrityReportResponseDto, MaintenanceIntegrityReportSummaryResponseDto, } from 'src/dtos/maintenance.dto'; -import { CacheControl, IntegrityReportType, SystemMetadataKey } from 'src/enum'; +import { AssetStatus, CacheControl, IntegrityReportType, SystemMetadataKey } from 'src/enum'; import { BaseService } from 'src/services/base.service'; import { MaintenanceModeState } from 'src/types'; import { ImmichFileResponse } from 'src/utils/file'; @@ -82,9 +83,26 @@ export class MaintenanceService extends BaseService { }); } - async deleteIntegrityReportFile(id: string): Promise { - const { path } = await this.integrityReportRepository.getById(id); - await this.storageRepository.unlink(path); - await this.integrityReportRepository.deleteById(id); + async deleteIntegrityReport(auth: AuthDto, id: string): Promise { + const { path, assetId, fileAssetId } = await this.integrityReportRepository.getById(id); + + if (assetId) { + await this.assetRepository.updateAll([assetId], { + deletedAt: new Date(), + status: AssetStatus.Trashed, + }); + + await this.eventRepository.emit('AssetTrashAll', { + assetIds: [assetId], + userId: auth.user.id, + }); + + await this.integrityReportRepository.deleteById(id); + } else if (fileAssetId) { + await this.assetRepository.deleteFiles([{ id: fileAssetId }]); + } else { + await this.storageRepository.unlink(path); + await this.integrityReportRepository.deleteById(id); + } } } diff --git a/server/src/types.ts b/server/src/types.ts index e0ed847e8d..7c0539fd23 100644 --- a/server/src/types.ts +++ b/server/src/types.ts @@ -10,6 +10,7 @@ import { DatabaseSslMode, ExifOrientation, ImageFormat, + IntegrityReportType, JobName, MemoryType, PluginTriggerType, @@ -286,6 +287,10 @@ export interface IIntegrityJob { refreshOnly?: boolean; } +export interface IIntegrityDeleteReportJob { + type?: IntegrityReportType; +} + export interface IIntegrityOrphanedFilesJob { type: 'asset' | 'asset_file'; paths: string[]; @@ -427,7 +432,8 @@ export type JobItem = | { name: JobName.IntegrityMissingFiles; data: IIntegrityPathWithReportJob } | { name: JobName.IntegrityMissingFilesRefresh; data: IIntegrityPathWithReportJob } | { name: JobName.IntegrityChecksumFiles; data?: IIntegrityJob } - | { name: JobName.IntegrityChecksumFilesRefresh; data?: IIntegrityPathWithChecksumJob }; + | { name: JobName.IntegrityChecksumFilesRefresh; data?: IIntegrityPathWithChecksumJob } + | { name: JobName.IntegrityReportDelete; data: IIntegrityDeleteReportJob }; export type VectorExtension = (typeof VECTOR_EXTENSIONS)[number]; diff --git a/web/src/routes/admin/maintenance/integrity-report/[type]/+page.svelte b/web/src/routes/admin/maintenance/integrity-report/[type]/+page.svelte index da0ea3d509..924567d5ea 100644 --- a/web/src/routes/admin/maintenance/integrity-report/[type]/+page.svelte +++ b/web/src/routes/admin/maintenance/integrity-report/[type]/+page.svelte @@ -2,7 +2,7 @@ import AdminPageLayout from '$lib/components/layouts/AdminPageLayout.svelte'; import { AppRoute } from '$lib/constants'; import { handleError } from '$lib/utils/handle-error'; - import { deleteIntegrityReportFile, getBaseUrl, IntegrityReportType } from '@immich/sdk'; + import { createJob, deleteIntegrityReport, getBaseUrl, IntegrityReportType, ManualJobName } from '@immich/sdk'; import { Button, HStack, @@ -10,6 +10,7 @@ menuManager, modalManager, Text, + toastManager, type ContextMenuBaseProps, type MenuItems, } from '@immich/ui'; @@ -27,6 +28,38 @@ let deleting = new SvelteSet(); let integrityReport = $state(data.integrityReport.items); + async function removeAll() { + const confirm = await modalManager.showDialog({ + confirmText: $t('delete'), + }); + + if (confirm) { + let name: ManualJobName; + switch (data.type) { + case IntegrityReportType.OrphanFile: { + name = ManualJobName.IntegrityOrphanFilesDeleteAll; + break; + } + case IntegrityReportType.MissingFile: { + name = ManualJobName.IntegrityMissingFilesDeleteAll; + break; + } + case IntegrityReportType.ChecksumMismatch: { + name = ManualJobName.IntegrityChecksumMismatchDeleteAll; + break; + } + } + + try { + deleting.add('all'); + await createJob({ jobCreateDto: { name } }); + toastManager.success($t('admin.job_created')); + } catch (error) { + handleError(error, 'Failed to delete file!'); + } + } + } + async function remove(id: string) { const confirm = await modalManager.showDialog({ confirmText: $t('delete'), @@ -35,7 +68,7 @@ if (confirm) { try { deleting.add(id); - await deleteIntegrityReportFile({ + await deleteIntegrityReport({ id, }); integrityReport = integrityReport.filter((report) => report.id !== id); @@ -64,21 +97,20 @@ }); } - if (data.type === IntegrityReportType.OrphanFile) { - items.push({ - title: $t('delete'), - icon: mdiTrashCanOutline, - color: 'danger', - onAction() { - void remove(reportId); - }, - }); - } - await menuManager.show({ ...props, target: event.currentTarget as HTMLElement, - items, + items: [ + ...items, + { + title: $t('delete'), + icon: mdiTrashCanOutline, + color: 'danger', + onAction() { + void remove(reportId); + }, + }, + ], }); }; @@ -96,10 +128,14 @@ size="small" variant="ghost" color="secondary" + leadingIcon={mdiDownload} href={`${getBaseUrl()}/admin/maintenance/integrity/report/${data.type}/csv`} > + {/snippet} @@ -119,7 +155,7 @@ > {#each integrityReport as { id, path } (id)} {path} @@ -129,7 +165,7 @@ variant="ghost" onclick={(event: Event) => handleOpen(event, { position: 'top-right' }, id)} aria-label={$t('open')} - disabled={deleting.has(id)} + disabled={deleting.has(id) || deleting.has('all')} />