feat: write integrity report to database

This commit is contained in:
izzy
2025-11-27 12:53:04 +00:00
parent 15503b150a
commit 1e941f3f88
8 changed files with 105 additions and 16 deletions

View File

@@ -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',

View File

@@ -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,

View File

@@ -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<DB>) {}
create(dto: Insertable<IntegrityReportTable> | Insertable<IntegrityReportTable>[]) {
return this.db
.insertInto('integrity_report')
.values(dto)
.onConflict((oc) => oc.doNothing())
.returningAll()
.executeTakeFirst();
}
}

View File

@@ -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;

View File

@@ -0,0 +1,15 @@
import { Kysely, sql } from 'kysely';
export async function up(db: Kysely<any>): Promise<void> {
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<any>): Promise<void> {
await sql`DROP TABLE "integrity_report";`.execute(db);
}

View File

@@ -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<string>;
@Column()
type!: IntegrityReportType;
@Column()
path!: string;
}

View File

@@ -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,

View File

@@ -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++;