feat: add config options & cron entries for checks

This commit is contained in:
izzy
2025-11-27 16:05:26 +00:00
parent 1744237aeb
commit 93860238af
13 changed files with 583 additions and 23 deletions
+32
View File
@@ -46,6 +46,22 @@ export interface SystemConfig {
accelDecode: boolean;
tonemap: ToneMapping;
};
integrityChecks: {
missingFiles: {
enabled: boolean;
cronExpression: string;
};
orphanedFiles: {
enabled: boolean;
cronExpression: string;
};
checksumFiles: {
enabled: boolean;
cronExpression: string;
timeLimit: number;
percentageLimit: number;
};
};
job: Record<ConcurrentQueueName, { concurrency: number }>;
logging: {
enabled: boolean;
@@ -222,6 +238,22 @@ export const defaults = Object.freeze<SystemConfig>({
accel: TranscodeHardwareAcceleration.Disabled,
accelDecode: false,
},
integrityChecks: {
missingFiles: {
enabled: true,
cronExpression: CronExpression.EVERY_DAY_AT_3AM,
},
orphanedFiles: {
enabled: true,
cronExpression: CronExpression.EVERY_DAY_AT_3AM,
},
checksumFiles: {
enabled: true,
cronExpression: CronExpression.EVERY_DAY_AT_3AM,
timeLimit: 60 * 60 * 1000, // 1 hour
percentageLimit: 1.0, // 100% of assets
},
},
job: {
[QueueName.BackgroundTask]: { concurrency: 5 },
[QueueName.SmartSearch]: { concurrency: 2 },
+42
View File
@@ -38,6 +38,7 @@ const isOAuthEnabled = (config: SystemConfigOAuthDto) => config.enabled;
const isOAuthOverrideEnabled = (config: SystemConfigOAuthDto) => config.mobileOverrideEnabled;
const isEmailNotificationEnabled = (config: SystemConfigSmtpDto) => config.enabled;
const isDatabaseBackupEnabled = (config: DatabaseBackupConfig) => config.enabled;
const isEnabledProperty = (config: { enabled: boolean }) => config.enabled;
export class DatabaseBackupConfig {
@ValidateBoolean()
@@ -145,6 +146,42 @@ export class SystemConfigFFmpegDto {
tonemap!: ToneMapping;
}
class SystemConfigIntegrityJob {
@ValidateBoolean()
enabled!: boolean;
@ValidateIf(isEnabledProperty)
@IsNotEmpty()
@IsCronExpression()
@IsString()
cronExpression!: string;
}
class SystemConfigIntegrityChecksumJob extends SystemConfigIntegrityJob {
@IsInt()
timeLimit!: number;
@IsNumber()
percentageLimit!: number;
}
class SystemConfigIntegrityChecks {
@Type(() => SystemConfigIntegrityJob)
@ValidateNested()
@IsObject()
missingFiles!: SystemConfigIntegrityJob;
@Type(() => SystemConfigIntegrityJob)
@ValidateNested()
@IsObject()
orphanedFiles!: SystemConfigIntegrityJob;
@Type(() => SystemConfigIntegrityChecksumJob)
@ValidateNested()
@IsObject()
checksumFiles!: SystemConfigIntegrityChecksumJob;
}
class JobSettingsDto {
@IsInt()
@IsPositive()
@@ -649,6 +686,11 @@ export class SystemConfigDto implements SystemConfig {
@IsObject()
ffmpeg!: SystemConfigFFmpegDto;
@Type(() => SystemConfigIntegrityChecks)
@ValidateNested()
@IsObject()
integrityChecks!: SystemConfigIntegrityChecks;
@Type(() => SystemConfigLoggingDto)
@ValidateNested()
@IsObject()
+1
View File
@@ -694,6 +694,7 @@ export enum DatabaseLock {
GetSystemConfig = 69,
BackupDatabase = 42,
MemoryCreation = 777,
IntegrityCheck = 67,
}
export enum MaintenanceAction {
+65 -22
View File
@@ -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 {
DatabaseLock,
ImmichWorker,
IntegrityReportType,
JobName,
@@ -19,6 +20,7 @@ import {
import { ArgOf } from 'src/repositories/event.repository';
import { BaseService } from 'src/services/base.service';
import { IIntegrityOrphanedFilesJob, IIntegrityPathWithReportJob } from 'src/types';
import { handlePromiseError } from 'src/utils/misc';
async function* chunk<T>(generator: AsyncIterableIterator<T>, n: number) {
let chunk: T[] = [];
@@ -38,25 +40,49 @@ async function* chunk<T>(generator: AsyncIterableIterator<T>, n: number) {
@Injectable()
export class IntegrityService extends BaseService {
// private backupLock = false;
private integrityLock = false;
@OnEvent({ name: 'ConfigInit', workers: [ImmichWorker.Microservices] })
async onConfigInit({
newConfig: {
backup: { database },
integrityChecks: { orphanedFiles, missingFiles, checksumFiles },
},
}: ArgOf<'ConfigInit'>) {
// this.backupLock = await this.databaseRepository.tryLock(DatabaseLock.BackupDatabase);
// if (this.backupLock) {
// this.cronRepository.create({
// name: 'backupDatabase',
// expression: database.cronExpression,
// onTick: () => handlePromiseError(this.jobRepository.queue({ name: JobName.DatabaseBackup }), this.logger),
// start: database.enabled,
// });
// }
this.integrityLock = await this.databaseRepository.tryLock(DatabaseLock.IntegrityCheck);
if (this.integrityLock) {
this.cronRepository.create({
name: 'integrityOrphanedFiles',
expression: orphanedFiles.cronExpression,
onTick: () =>
handlePromiseError(
this.jobRepository.queue({ name: JobName.IntegrityOrphanedFilesQueueAll, data: {} }),
this.logger,
),
start: orphanedFiles.enabled,
});
setTimeout(() => {
this.cronRepository.create({
name: 'integrityMissingFiles',
expression: missingFiles.cronExpression,
onTick: () =>
handlePromiseError(
this.jobRepository.queue({ name: JobName.IntegrityMissingFilesQueueAll, data: {} }),
this.logger,
),
start: missingFiles.enabled,
});
this.cronRepository.create({
name: 'integrityChecksumFiles',
expression: checksumFiles.cronExpression,
onTick: () =>
handlePromiseError(this.jobRepository.queue({ name: JobName.IntegrityChecksumFiles, data: {} }), this.logger),
start: checksumFiles.enabled,
});
}
// debug: run on boot
setImmediate(() => {
this.jobRepository.queue({
name: JobName.IntegrityOrphanedFilesQueueAll,
data: {},
@@ -71,19 +97,36 @@ export class IntegrityService extends BaseService {
name: JobName.IntegrityChecksumFiles,
data: {},
});
}, 1000);
});
}
@OnEvent({ name: 'ConfigUpdate', server: true })
async onConfigUpdate({ newConfig: { backup } }: ArgOf<'ConfigUpdate'>) {
// if (!this.backupLock) {
// return;
// }
// this.cronRepository.update({
// name: 'backupDatabase',
// expression: backup.database.cronExpression,
// start: backup.database.enabled,
// });
async onConfigUpdate({
newConfig: {
integrityChecks: { orphanedFiles, missingFiles, checksumFiles },
},
}: ArgOf<'ConfigUpdate'>) {
if (!this.integrityLock) {
return;
}
this.cronRepository.update({
name: 'integrityOrphanedFiles',
expression: orphanedFiles.cronExpression,
start: orphanedFiles.enabled,
});
this.cronRepository.update({
name: 'integrityMissingFiles',
expression: missingFiles.cronExpression,
start: missingFiles.enabled,
});
this.cronRepository.update({
name: 'integrityChecksumFiles',
expression: checksumFiles.cronExpression,
start: checksumFiles.enabled,
});
}
@OnJob({ name: JobName.IntegrityOrphanedFilesQueueAll, queue: QueueName.BackgroundTask })