From 9aefc3d470ec1e341117f2076bca1266739be8a3 Mon Sep 17 00:00:00 2001 From: Daniel Date: Fri, 6 Mar 2026 19:58:23 +0100 Subject: [PATCH] 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 --- server/src/core/persistence.ts | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/server/src/core/persistence.ts b/server/src/core/persistence.ts index c9273d9..04d7b10 100644 --- a/server/src/core/persistence.ts +++ b/server/src/core/persistence.ts @@ -3,25 +3,41 @@ import path from 'node:path'; const DATA_DIR = process.env.DATA_DIR ?? '/data'; const stateFile = path.join(DATA_DIR, 'hub-state.json'); +const backupFile = stateFile + '.bak'; +const tmpFile = stateFile + '.tmp'; let state: Record = {}; export function loadState(): void { - try { - if (fs.existsSync(stateFile)) { - state = JSON.parse(fs.readFileSync(stateFile, 'utf-8')); + // Try main file first, fall back to backup if corrupted + for (const file of [stateFile, backupFile]) { + 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 { try { 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) { - console.error('Failed to save state:', e); + console.error('[State] Failed to save:', e); } }