import { Client } from 'pg'; import { z } from 'zod'; /** * The sidecar's read-only view of the backup-related instance settings * (issue #103). The api owns the `instance_settings` registry; the sidecar * reads the raw rows with the same defaults, so admin changes apply on the * next run without a restart or a config channel. A row that is missing or * fails validation falls back to its default — never to a crash. */ export const NEXTCLOUD_PASSWORD_SECRET_KEY = 'BACKUP_NEXTCLOUD_PASSWORD'; const SETTING_SCHEMAS = { // Local retention: `null` means "no admin override" — the env value // (BACKUP_RETENTION_DAYS, stage-specific) stays authoritative until a // Site Admin saves the setting once. 'backup.localRetentionDays': z.number().int().min(1).nullable().default(null), 'backup.remoteRetentionDays': z.number().int().min(1).default(30), 'backup.nextcloud.enabled': z.boolean().default(false), 'backup.nextcloud.baseUrl': z.string().default(''), 'backup.nextcloud.username': z.string().default(''), 'backup.nextcloud.folder': z.string().default('dorfteich-backups'), 'backup.nextcloud.uploadSchedule': z.enum(['off', 'daily', 'weekly']).default('daily'), } as const; export interface BackupDbSettings { localRetentionDays: number | null; remoteRetentionDays: number; nextcloud: { enabled: boolean; baseUrl: string; username: string; folder: string; uploadSchedule: 'off' | 'daily' | 'weekly'; }; } export function parseBackupSettings(rows: { key: string; value: unknown }[]): BackupDbSettings { const byKey = new Map(rows.map((row) => [row.key, row.value])); const get = ( key: K, ): z.infer<(typeof SETTING_SCHEMAS)[K]> => { const parsed = SETTING_SCHEMAS[key].safeParse(byKey.get(key)); return parsed.success ? parsed.data : SETTING_SCHEMAS[key].parse(undefined); }; return { localRetentionDays: get('backup.localRetentionDays'), remoteRetentionDays: get('backup.remoteRetentionDays'), nextcloud: { enabled: get('backup.nextcloud.enabled'), baseUrl: get('backup.nextcloud.baseUrl'), username: get('backup.nextcloud.username'), folder: get('backup.nextcloud.folder'), uploadSchedule: get('backup.nextcloud.uploadSchedule'), }, }; } /** * Reads the settings rows with a short-lived connection. A database that is * unreachable or predates the settings keys yields plain defaults — the * local backup must still run when the instance is at its most broken. */ export async function readBackupDbSettings(databaseUrl: string): Promise { const client = new Client({ connectionString: databaseUrl }); try { await client.connect(); const result = await client.query<{ key: string; value: unknown }>( `SELECT key, value FROM instance_settings WHERE key LIKE 'backup.%'`, ); return parseBackupSettings(result.rows); } catch { return parseBackupSettings([]); } finally { await client.end().catch(() => undefined); } }