fix(memories): keep the Synology repair's progress through the settings domain

The repair job wrote its marker rows through the AppSettings repository,
which the settings domain owns. It now registers both keys and goes through
SettingsService, as the import boundary check asks.
This commit is contained in:
Maurice
2026-11-09 12:03:27 +01:00
parent a9bfcd0fd5
commit d3bdc6de62
4 changed files with 30 additions and 20 deletions
@@ -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<string, AppSettingDef>;
export type AppSettingKey = keyof typeof APP_SETTINGS;
+2 -2
View File
@@ -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,
@@ -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<void> {
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(
@@ -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<string, string>();
const settings = {
getAppSetting: async (key: string) => stored.get(key) ?? null,
setAppSettings: async (values: Record<string, string>) => {
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();
});