diff --git a/server/src/enum.ts b/server/src/enum.ts index 9c8c49c1e8..35fd98c53c 100644 --- a/server/src/enum.ts +++ b/server/src/enum.ts @@ -482,6 +482,12 @@ export enum CacheControl { None = 'none', } +export enum IntegrityReportType { + OrphanFile = 'orphan_file', + MissingFile = 'missing_file', + ChecksumFail = 'checksum_fail', +} + export enum ImmichEnvironment { Development = 'development', Testing = 'testing', diff --git a/server/src/repositories/index.ts b/server/src/repositories/index.ts index c59110d674..dc63d427a2 100644 --- a/server/src/repositories/index.ts +++ b/server/src/repositories/index.ts @@ -15,6 +15,7 @@ import { DownloadRepository } from 'src/repositories/download.repository'; import { DuplicateRepository } from 'src/repositories/duplicate.repository'; import { EmailRepository } from 'src/repositories/email.repository'; import { EventRepository } from 'src/repositories/event.repository'; +import { IntegrityReportRepository } from 'src/repositories/integrity-report.repository'; import { JobRepository } from 'src/repositories/job.repository'; import { LibraryRepository } from 'src/repositories/library.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; @@ -68,6 +69,7 @@ export const repositories = [ DuplicateRepository, EmailRepository, EventRepository, + IntegrityReportRepository, JobRepository, LibraryRepository, LoggingRepository, diff --git a/server/src/repositories/integrity-report.repository.ts b/server/src/repositories/integrity-report.repository.ts new file mode 100644 index 0000000000..17e85e78a3 --- /dev/null +++ b/server/src/repositories/integrity-report.repository.ts @@ -0,0 +1,19 @@ +import { Injectable } from '@nestjs/common'; +import { Insertable, Kysely } from 'kysely'; +import { InjectKysely } from 'nestjs-kysely'; +import { DB } from 'src/schema'; +import { IntegrityReportTable } from 'src/schema/tables/integrity-report.table'; + +@Injectable() +export class IntegrityReportRepository { + constructor(@InjectKysely() private db: Kysely) {} + + create(dto: Insertable | Insertable[]) { + return this.db + .insertInto('integrity_report') + .values(dto) + .onConflict((oc) => oc.doNothing()) + .returningAll() + .executeTakeFirst(); + } +} diff --git a/server/src/schema/index.ts b/server/src/schema/index.ts index 9e206826e6..cea30cded1 100644 --- a/server/src/schema/index.ts +++ b/server/src/schema/index.ts @@ -40,6 +40,7 @@ import { AssetTable } from 'src/schema/tables/asset.table'; import { AuditTable } from 'src/schema/tables/audit.table'; import { FaceSearchTable } from 'src/schema/tables/face-search.table'; import { GeodataPlacesTable } from 'src/schema/tables/geodata-places.table'; +import { IntegrityReportTable } from 'src/schema/tables/integrity-report.table'; import { LibraryTable } from 'src/schema/tables/library.table'; import { MemoryAssetAuditTable } from 'src/schema/tables/memory-asset-audit.table'; import { MemoryAssetTable } from 'src/schema/tables/memory-asset.table'; @@ -98,6 +99,7 @@ export class ImmichDatabase { AssetExifTable, FaceSearchTable, GeodataPlacesTable, + IntegrityReportTable, LibraryTable, MemoryTable, MemoryAuditTable, @@ -195,6 +197,8 @@ export interface DB { geodata_places: GeodataPlacesTable; + integrity_report: IntegrityReportTable; + library: LibraryTable; memory: MemoryTable; diff --git a/server/src/schema/migrations/1764246650423-CreateIntegrityReportTable.ts b/server/src/schema/migrations/1764246650423-CreateIntegrityReportTable.ts new file mode 100644 index 0000000000..a6f99ab755 --- /dev/null +++ b/server/src/schema/migrations/1764246650423-CreateIntegrityReportTable.ts @@ -0,0 +1,15 @@ +import { Kysely, sql } from 'kysely'; + +export async function up(db: Kysely): Promise { + await sql`CREATE TABLE "integrity_report" ( + "id" uuid NOT NULL DEFAULT uuid_generate_v4(), + "type" character varying NOT NULL, + "path" character varying NOT NULL, + CONSTRAINT "integrity_report_type_path_uq" UNIQUE ("type", "path"), + CONSTRAINT "integrity_report_pkey" PRIMARY KEY ("id") +);`.execute(db); +} + +export async function down(db: Kysely): Promise { + await sql`DROP TABLE "integrity_report";`.execute(db); +} diff --git a/server/src/schema/tables/integrity-report.table.ts b/server/src/schema/tables/integrity-report.table.ts new file mode 100644 index 0000000000..d789c534db --- /dev/null +++ b/server/src/schema/tables/integrity-report.table.ts @@ -0,0 +1,21 @@ +import { IntegrityReportType } from 'src/enum'; +import { + Column, + Generated, + PrimaryGeneratedColumn, + Table, + Unique +} from 'src/sql-tools'; + +@Table('integrity_report') +@Unique({ columns: ['type', 'path'] }) +export class IntegrityReportTable { + @PrimaryGeneratedColumn() + id!: Generated; + + @Column() + type!: IntegrityReportType; + + @Column() + path!: string; +} diff --git a/server/src/services/base.service.ts b/server/src/services/base.service.ts index 9c422818b3..e311a860e3 100644 --- a/server/src/services/base.service.ts +++ b/server/src/services/base.service.ts @@ -22,6 +22,7 @@ import { DownloadRepository } from 'src/repositories/download.repository'; import { DuplicateRepository } from 'src/repositories/duplicate.repository'; import { EmailRepository } from 'src/repositories/email.repository'; import { EventRepository } from 'src/repositories/event.repository'; +import { IntegrityReportRepository } from 'src/repositories/integrity-report.repository'; import { JobRepository } from 'src/repositories/job.repository'; import { LibraryRepository } from 'src/repositories/library.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; @@ -137,6 +138,7 @@ export class BaseService { protected duplicateRepository: DuplicateRepository, protected emailRepository: EmailRepository, protected eventRepository: EventRepository, + protected integrityReportRepository: IntegrityReportRepository, protected jobRepository: JobRepository, protected libraryRepository: LibraryRepository, protected machineLearningRepository: MachineLearningRepository, diff --git a/server/src/services/integrity.service.ts b/server/src/services/integrity.service.ts index 0347a06edf..c4db5d49e7 100644 --- a/server/src/services/integrity.service.ts +++ b/server/src/services/integrity.service.ts @@ -7,7 +7,15 @@ import { pipeline } from 'node:stream/promises'; import { JOBS_LIBRARY_PAGINATION_SIZE } from 'src/constants'; import { StorageCore } from 'src/cores/storage.core'; import { OnEvent, OnJob } from 'src/decorators'; -import { ImmichWorker, JobName, JobStatus, QueueName, StorageFolder, SystemMetadataKey } from 'src/enum'; +import { + ImmichWorker, + IntegrityReportType, + JobName, + JobStatus, + QueueName, + StorageFolder, + SystemMetadataKey, +} from 'src/enum'; import { ArgOf } from 'src/repositories/event.repository'; import { BaseService } from 'src/services/base.service'; import { IIntegrityMissingFilesJob, IIntegrityOrphanedFilesJob } from 'src/types'; @@ -31,16 +39,17 @@ export class IntegrityService extends BaseService { // start: database.enabled, // }); // } - setTimeout(() => { - // this.jobRepository.queue({ - // name: JobName.IntegrityOrphanedFilesQueueAll, - // data: {}, - // }); - // this.jobRepository.queue({ - // name: JobName.IntegrityMissingFilesQueueAll, - // data: {}, - // }); + setTimeout(() => { + this.jobRepository.queue({ + name: JobName.IntegrityOrphanedFilesQueueAll, + data: {}, + }); + + this.jobRepository.queue({ + name: JobName.IntegrityMissingFilesQueueAll, + data: {}, + }); this.jobRepository.queue({ name: JobName.IntegrityChecksumFiles, @@ -129,8 +138,12 @@ export class IntegrityService extends BaseService { } } - // todo: do something with orphanedFiles - console.info(orphanedFiles); + await this.integrityReportRepository.create( + [...orphanedFiles].map((path) => ({ + type: IntegrityReportType.OrphanFile, + path, + })), + ); this.logger.log(`Processed ${paths.length} and found ${orphanedFiles.size} orphaned file(s).`); return JobStatus.Success; @@ -201,10 +214,14 @@ export class IntegrityService extends BaseService { ), ); - const missingFiles = result.filter((path) => path); + const missingFiles = result.filter((path) => path) as string[]; - // todo: do something with missingFiles - console.info(missingFiles); + await this.integrityReportRepository.create( + missingFiles.map((path) => ({ + type: IntegrityReportType.MissingFile, + path, + })), + ); this.logger.log(`Processed ${paths.length} and found ${missingFiles.length} missing file(s).`); return JobStatus.Success; @@ -266,7 +283,10 @@ export class IntegrityService extends BaseService { } } catch (error) { this.logger.warn('Failed to process a file: ' + error); - // todo: do something with originalPath + await this.integrityReportRepository.create({ + path: originalPath, + type: IntegrityReportType.ChecksumFail, + }); } processed++;