Harden state persistence: atomic writes + backup fallback

- Atomic save: write to .tmp file then rename (prevents corruption
  if container is killed mid-write)
- Backup: .bak copy created on successful load, used as fallback
  if main file is corrupted
- Startup log shows loaded keys (verifies favorites survived)

Ensures radio_favorites and radio_volumes survive container
updates, crashes, and forced restarts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Daniel 2026-03-06 19:58:23 +01:00
parent 262cc9f213
commit 9aefc3d470

View file

@ -3,25 +3,41 @@ import path from 'node:path';
const DATA_DIR = process.env.DATA_DIR ?? '/data'; const DATA_DIR = process.env.DATA_DIR ?? '/data';
const stateFile = path.join(DATA_DIR, 'hub-state.json'); const stateFile = path.join(DATA_DIR, 'hub-state.json');
const backupFile = stateFile + '.bak';
const tmpFile = stateFile + '.tmp';
let state: Record<string, any> = {}; let state: Record<string, any> = {};
export function loadState(): void { export function loadState(): void {
try { // Try main file first, fall back to backup if corrupted
if (fs.existsSync(stateFile)) { for (const file of [stateFile, backupFile]) {
state = JSON.parse(fs.readFileSync(stateFile, 'utf-8')); try {
if (fs.existsSync(file)) {
state = JSON.parse(fs.readFileSync(file, 'utf-8'));
const keys = Object.keys(state);
console.log(`[State] Loaded from ${path.basename(file)} (${keys.length} keys: ${keys.join(', ')})`);
// Create backup on successful load from main file
if (file === stateFile) {
try { fs.copyFileSync(stateFile, backupFile); } catch {}
}
return;
}
} catch (e) {
console.error(`[State] Failed to load ${path.basename(file)}:`, e);
} }
} catch (e) {
console.error('Failed to load state:', e);
} }
console.log('[State] No existing state found, starting fresh');
} }
export function saveState(): void { export function saveState(): void {
try { try {
fs.mkdirSync(path.dirname(stateFile), { recursive: true }); fs.mkdirSync(path.dirname(stateFile), { recursive: true });
fs.writeFileSync(stateFile, JSON.stringify(state, null, 2)); // Atomic write: write to tmp file, then rename
// rename() is atomic on POSIX filesystems, prevents corruption on crash
fs.writeFileSync(tmpFile, JSON.stringify(state, null, 2));
fs.renameSync(tmpFile, stateFile);
} catch (e) { } catch (e) {
console.error('Failed to save state:', e); console.error('[State] Failed to save:', e);
} }
} }