import { existsSync, readFileSync } from 'node:fs'; import { rename, writeFile } from 'node:fs/promises'; import { join } from 'node:path'; import { BACKUP_STATUS_FILE, RESTORE_STATUS_FILE, type BackupStatus, type RestoreStatus, } from '@dorfteich/shared'; /** * File I/O for `status.json` on the backups volume. The shape itself is a * shared contract (`@dorfteich/shared` backup-status.ts) because the api's * readiness check (#85) and admin panel (#86) read what this sidecar writes. */ export type { BackupRun, BackupSizes, BackupStatus } from '@dorfteich/shared'; export { BACKUP_STATUS_FILE as STATUS_FILE } from '@dorfteich/shared'; /** Reads the previous status; a missing or torn file is simply "no status". */ export function readStatus(backupsDir: string): BackupStatus | null { const path = join(backupsDir, BACKUP_STATUS_FILE); if (!existsSync(path)) return null; try { return JSON.parse(readFileSync(path, 'utf8')) as BackupStatus; } catch { return null; } } /** Atomic write (staging + rename) so readers never see a torn file. */ export async function writeStatus(backupsDir: string, status: BackupStatus): Promise { await writeJsonAtomic(join(backupsDir, BACKUP_STATUS_FILE), status); } /** The in-app restore progress file (issue #103) — same contract discipline * as status.json: the api's maintenance gate and admin card read it. */ export function readRestoreStatus(backupsDir: string): RestoreStatus | null { const path = join(backupsDir, RESTORE_STATUS_FILE); if (!existsSync(path)) return null; try { return JSON.parse(readFileSync(path, 'utf8')) as RestoreStatus; } catch { return null; } } export async function writeRestoreStatus(backupsDir: string, status: RestoreStatus): Promise { await writeJsonAtomic(join(backupsDir, RESTORE_STATUS_FILE), status); } async function writeJsonAtomic(path: string, value: unknown): Promise { const staging = `${path}.tmp-${process.pid}`; await writeFile(staging, JSON.stringify(value, null, 2) + '\n'); await rename(staging, path); }