diff --git a/server/src/nest/common/app-settings.registry.ts b/server/src/nest/common/app-settings.registry.ts index 74ad8de30..5dbf4d2f0 100644 --- a/server/src/nest/common/app-settings.registry.ts +++ b/server/src/nest/common/app-settings.registry.ts @@ -152,6 +152,10 @@ export const APP_SETTINGS = { service_key: { schema: text, default: null, encrypted: 'always', managedLocked: true }, /** The key's bookkeeping as JSON: when to renew, when to ask again after a refusal, a revocation. */ service_key_state: { schema: text, default: null }, + /** When every Synology photo was asked again for its own capture data (#3019); set once. */ + synology_capture_repair_3019: { schema: text, default: null }, + /** How many starts have tried that so far, while some photo could not be confirmed. */ + synology_capture_repair_3019_tries: { schema: intText, default: null }, } as const satisfies Record; export type AppSettingKey = keyof typeof APP_SETTINGS; diff --git a/server/src/nest/memories/memories.module.ts b/server/src/nest/memories/memories.module.ts index e8e9813dc..c7f624b60 100644 --- a/server/src/nest/memories/memories.module.ts +++ b/server/src/nest/memories/memories.module.ts @@ -1,4 +1,3 @@ -import { AppSettings } from '../../db/entities/AppSettings.entity'; import { JourneyContributors } from '../../db/entities/JourneyContributors.entity'; import { JourneyPhotos } from '../../db/entities/JourneyPhotos.entity'; import { Journeys } from '../../db/entities/Journeys.entity'; @@ -16,6 +15,7 @@ import { NotificationsModule } from '../notifications/notifications.module'; import { TrekPhotosModule } from '../photos/trek-photos.module'; import { RealtimeModule } from '../realtime/realtime.module'; import { SchedulingModule } from '../scheduling/scheduling.module'; +import { SettingsModule } from '../settings/settings.module'; import { StorageModule } from '../storage/storage.module'; import { ImmichMemoriesController } from './immich.controller'; import { ImmichService } from './immich.service'; @@ -83,10 +83,10 @@ import { Module } from '@nestjs/common'; TrekPhotosModule, RealtimeModule, SchedulingModule, + SettingsModule, StorageModule, UserConnectionRepositoriesModule, MikroOrmModule.forFeature([ - AppSettings, TripPhotos, TrekPhotos, TripAlbumLinks, diff --git a/server/src/nest/memories/synology-capture-repair.job.ts b/server/src/nest/memories/synology-capture-repair.job.ts index 8d8048467..0d0569883 100644 --- a/server/src/nest/memories/synology-capture-repair.job.ts +++ b/server/src/nest/memories/synology-capture-repair.job.ts @@ -1,17 +1,16 @@ -import { AppSettings } from '../../db/entities/AppSettings.entity'; import { TrekPhotos } from '../../db/entities/TrekPhotos.entity'; -import type { AppSettingsRepository } from '../../db/repositories/AppSettings.repository'; import type { TrekPhotosRepository } from '../../db/repositories/TrekPhotos.repository'; import { logError, logInfo } from '../audit/audit-log.logger'; import { CronRegistrarService } from '../scheduling/cron-registrar.service'; +import { SettingsService } from '../settings/settings.service'; import { PhotoCaptureBackfillService } from './photo-capture-backfill.service'; import { InjectRepository } from '@mikro-orm/nestjs'; import { Injectable, type OnApplicationBootstrap } from '@nestjs/common'; /** The app_settings row that says every Synology photo has been asked again. */ -export const SYNOLOGY_CAPTURE_REPAIR_DONE_KEY = 'synology_capture_repair_3019'; +export const SYNOLOGY_CAPTURE_REPAIR_DONE_KEY = 'synology_capture_repair_3019' as const; /** How many starts have tried so far, while some photo could not be confirmed. */ -export const SYNOLOGY_CAPTURE_REPAIR_TRIES_KEY = 'synology_capture_repair_3019_tries'; +export const SYNOLOGY_CAPTURE_REPAIR_TRIES_KEY = 'synology_capture_repair_3019_tries' as const; const PROVIDER = 'synologyphotos'; const PAGE = 200; @@ -43,7 +42,7 @@ export class SynologyCaptureRepairJob implements OnApplicationBootstrap { constructor( private readonly backfill: PhotoCaptureBackfillService, private readonly registrar: CronRegistrarService, - @InjectRepository(AppSettings) private readonly appSettings: AppSettingsRepository, + private readonly settings: SettingsService, @InjectRepository(TrekPhotos) private readonly trekPhotos: TrekPhotosRepository, ) {} @@ -69,16 +68,16 @@ export class SynologyCaptureRepairJob implements OnApplicationBootstrap { async runOnce(): Promise { try { - if ((await this.appSettings.countKeysPresent([SYNOLOGY_CAPTURE_REPAIR_DONE_KEY])) > 0) return; - const tries = Number(await this.appSettings.getValue(SYNOLOGY_CAPTURE_REPAIR_TRIES_KEY)) || 0; + if ((await this.settings.getAppSetting(SYNOLOGY_CAPTURE_REPAIR_DONE_KEY)) !== null) return; + const tries = Number(await this.settings.getAppSetting(SYNOLOGY_CAPTURE_REPAIR_TRIES_KEY)) || 0; const { changed, failed } = await this.walk(0); if (failed > 0 && tries + 1 < MAX_TRIES) { - await this.appSettings.upsertOrReplace(SYNOLOGY_CAPTURE_REPAIR_TRIES_KEY, String(tries + 1)); + await this.settings.setAppSettings({ [SYNOLOGY_CAPTURE_REPAIR_TRIES_KEY]: String(tries + 1) }); } else { - await this.appSettings.upsertOrReplace(SYNOLOGY_CAPTURE_REPAIR_DONE_KEY, new Date().toISOString()); - await this.appSettings.deleteValue(SYNOLOGY_CAPTURE_REPAIR_TRIES_KEY); + await this.settings.setAppSettings({ [SYNOLOGY_CAPTURE_REPAIR_DONE_KEY]: new Date().toISOString() }); + await this.settings.deleteAppSettings([SYNOLOGY_CAPTURE_REPAIR_TRIES_KEY]); } if (changed + failed > 0) { logInfo( diff --git a/server/tests/unit/nest/synology-capture-repair.job.test.ts b/server/tests/unit/nest/synology-capture-repair.job.test.ts index 57ee77a0c..ac2620488 100644 --- a/server/tests/unit/nest/synology-capture-repair.job.test.ts +++ b/server/tests/unit/nest/synology-capture-repair.job.test.ts @@ -1,5 +1,3 @@ -import { db } from '../../../src/db/database'; -import { AppSettings } from '../../../src/db/entities/AppSettings.entity'; import type { TrekPhotosRepository } from '../../../src/db/repositories/TrekPhotos.repository'; import type { PhotoCaptureBackfillService } from '../../../src/nest/memories/photo-capture-backfill.service'; import { @@ -8,9 +6,7 @@ import { SynologyCaptureRepairJob, } from '../../../src/nest/memories/synology-capture-repair.job'; import type { CronRegistrarService } from '../../../src/nest/scheduling/cron-registrar.service'; -import { findRow } from '../../helpers/factories/rows'; -import { resetTestDb } from '../../helpers/test-db'; -import { createTestAppSettingsRepo, sharedTestOrm } from '../../helpers/test-uow'; +import type { SettingsService } from '../../../src/nest/settings/settings.service'; import { beforeEach, describe, expect, it, vi } from 'vitest'; @@ -35,12 +31,23 @@ describe('SynologyCaptureRepairJob', () => { } as unknown as TrekPhotosRepository; const reconfirm = vi.fn(); const backfill = { reconfirm } as unknown as PhotoCaptureBackfillService; - const setting = async (key: string) => (await findRow(await sharedTestOrm(db), AppSettings, { key }))?.value; + // The instance settings the job keeps its progress in, as the settings domain answers them. + const stored = new Map(); + const settings = { + getAppSetting: async (key: string) => stored.get(key) ?? null, + setAppSettings: async (values: Record) => { + for (const [key, value] of Object.entries(values)) stored.set(key, value); + }, + deleteAppSettings: async (keys: string[]) => { + for (const key of keys) stored.delete(key); + }, + } as unknown as SettingsService; + const setting = async (key: string) => stored.get(key); const job = async (enabled = true) => - new SynologyCaptureRepairJob(backfill, registrar(enabled), await createTestAppSettingsRepo(db), trekPhotos); + new SynologyCaptureRepairJob(backfill, registrar(enabled), settings, trekPhotos); beforeEach(() => { - resetTestDb(db); + stored.clear(); reconfirm.mockReset(); vi.mocked(trekPhotos.listByProviderAfter).mockClear(); });