/** * Durable file backups under conductor state. * * Both the brief writer and the config writer keep a copy of every previous * version of the file they replace, so a bad write — or a process dying * mid-upgrade — never destroys the only copy of operator-owned bytes. * Retention is intentionally unbounded, matching the brief backups. */ import { randomUUID } from "node:crypto"; import { constants, copyFileSync, linkSync, mkdirSync, unlinkSync, } from "node:fs"; import { join } from "node:path"; /** A timestamp stem safe to embed in a file name. */ export function backupTimestamp(at: number = Date.now()): string { return new Date(at).toISOString().replace(/[:.]/g, "-"); } /** * Atomically publish a complete temp file as `stem` under `backupRoot`, * suffixing on collision, then remove the temp. Linking a complete file * publishes without ever overwriting a backup created concurrently at the * same millisecond, and it stays on one filesystem because the temp lives in * the same directory as the destination. * * Shared by {@link copyToUniqueBackup} and the store snapshot path, so every * durable copy under state uses the same temp-then-atomic-publish convention. */ export function publishTempFile(temporary: string, backupRoot: string, stem: string): string { try { for (let suffix = 0; ; suffix += 1) { const destination = join(backupRoot, suffix === 0 ? stem : `${stem}-${suffix}`); try { linkSync(temporary, destination); return destination; } catch (err) { if ((err as NodeJS.ErrnoException).code === "EEXIST") continue; throw err; } } } finally { unlinkSync(temporary); } } /** Publishes `source` under `backupRoot` as `stem`, suffixing on collision. */ export function copyToUniqueBackup(source: string, backupRoot: string, stem: string): string { mkdirSync(backupRoot, { recursive: true }); const temporary = join(backupRoot, `.${stem}.${process.pid}.${randomUUID()}.tmp`); copyFileSync(source, temporary, constants.COPYFILE_EXCL); return publishTempFile(temporary, backupRoot, stem); }