fix: move hub-state.json to /data/sounds/ for persistence
Root cause: only /data/sounds/ survives container recreation (it's the volume-mounted directory). /data/hub-state.json was written to the container's ephemeral layer and lost on every redeploy. - State file now saved to /data/sounds/hub-state.json - Auto-migrates from legacy /data/hub-state.json if found - Favorites and radio volumes will now persist across deploys Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
55f311c612
commit
24b4dadb0f
1 changed files with 30 additions and 32 deletions
|
|
@ -2,54 +2,64 @@ import fs from 'node:fs';
|
||||||
import path from 'node:path';
|
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');
|
// Save state inside /data/sounds/ — this directory is volume-mounted and
|
||||||
|
// survives container recreation. The old /data/hub-state.json was lost on
|
||||||
|
// every redeploy because only /data/sounds/ is persistently mounted.
|
||||||
|
const SOUNDS_DIR = path.join(DATA_DIR, 'sounds');
|
||||||
|
const stateFile = path.join(SOUNDS_DIR, 'hub-state.json');
|
||||||
const backupFile = stateFile + '.bak';
|
const backupFile = stateFile + '.bak';
|
||||||
const tmpFile = stateFile + '.tmp';
|
const tmpFile = stateFile + '.tmp';
|
||||||
|
|
||||||
|
// Legacy location (will be migrated automatically)
|
||||||
|
const legacyStateFile = path.join(DATA_DIR, 'hub-state.json');
|
||||||
|
|
||||||
let state: Record<string, any> = {};
|
let state: Record<string, any> = {};
|
||||||
let lastSaveOk = false;
|
let lastSaveOk = false;
|
||||||
|
|
||||||
export function loadState(): void {
|
export function loadState(): void {
|
||||||
console.log(`[State] DATA_DIR=${DATA_DIR}, stateFile=${stateFile}`);
|
console.log(`[State] DATA_DIR=${DATA_DIR}, stateFile=${stateFile}`);
|
||||||
|
|
||||||
// Check if DATA_DIR exists and is writable
|
// Check if sounds dir exists and is writable
|
||||||
try {
|
try {
|
||||||
fs.mkdirSync(DATA_DIR, { recursive: true });
|
fs.mkdirSync(SOUNDS_DIR, { recursive: true });
|
||||||
const testFile = path.join(DATA_DIR, '.write-test');
|
const testFile = path.join(SOUNDS_DIR, '.write-test');
|
||||||
fs.writeFileSync(testFile, 'ok');
|
fs.writeFileSync(testFile, 'ok');
|
||||||
fs.unlinkSync(testFile);
|
fs.unlinkSync(testFile);
|
||||||
console.log(`[State] DATA_DIR is writable`);
|
console.log(`[State] sounds dir is writable`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`[State] ⚠ DATA_DIR is NOT writable:`, e);
|
console.error(`[State] ⚠ sounds dir is NOT writable:`, e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// List existing files in DATA_DIR for diagnostics
|
// List existing state files for diagnostics
|
||||||
try {
|
try {
|
||||||
const files = fs.readdirSync(DATA_DIR);
|
const files = fs.readdirSync(SOUNDS_DIR).filter(f => f.includes('state') || f.includes('hub'));
|
||||||
console.log(`[State] Files in DATA_DIR: ${files.join(', ') || '(empty)'}`);
|
console.log(`[State] State files in sounds/: ${files.join(', ') || '(none)'}`);
|
||||||
} catch (e) {
|
} catch {}
|
||||||
console.error(`[State] Cannot list DATA_DIR:`, e);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try main file first, fall back to backup if corrupted
|
// Try new location, then backup, then legacy location
|
||||||
for (const file of [stateFile, backupFile]) {
|
const candidates = [stateFile, backupFile, legacyStateFile];
|
||||||
|
for (const file of candidates) {
|
||||||
try {
|
try {
|
||||||
if (fs.existsSync(file)) {
|
if (fs.existsSync(file)) {
|
||||||
const raw = fs.readFileSync(file, 'utf-8');
|
const raw = fs.readFileSync(file, 'utf-8');
|
||||||
state = JSON.parse(raw);
|
state = JSON.parse(raw);
|
||||||
const keys = Object.keys(state);
|
const keys = Object.keys(state);
|
||||||
const favCount = Array.isArray(state.radio_favorites) ? state.radio_favorites.length : 0;
|
const favCount = Array.isArray(state.radio_favorites) ? state.radio_favorites.length : 0;
|
||||||
console.log(`[State] ✓ Loaded from ${path.basename(file)} (${keys.length} keys: ${keys.join(', ')})`);
|
console.log(`[State] ✓ Loaded from ${file} (${keys.length} keys: ${keys.join(', ')})`);
|
||||||
console.log(`[State] radio_favorites: ${favCount} entries`);
|
console.log(`[State] radio_favorites: ${favCount} entries`);
|
||||||
// Create backup on successful load from main file
|
// If loaded from legacy location, migrate to new location
|
||||||
if (file === stateFile) {
|
if (file === legacyStateFile) {
|
||||||
try { fs.copyFileSync(stateFile, backupFile); } catch {}
|
console.log(`[State] Migrating from legacy ${legacyStateFile} → ${stateFile}`);
|
||||||
|
saveState();
|
||||||
|
try { fs.unlinkSync(legacyStateFile); } catch {}
|
||||||
}
|
}
|
||||||
|
// Create backup on successful load
|
||||||
|
try { fs.copyFileSync(stateFile, backupFile); } catch {}
|
||||||
lastSaveOk = true;
|
lastSaveOk = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`[State] Failed to load ${path.basename(file)}:`, e);
|
console.error(`[State] Failed to load ${file}:`, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log('[State] No existing state found, starting fresh');
|
console.log('[State] No existing state found, starting fresh');
|
||||||
|
|
@ -62,14 +72,7 @@ export function saveState(): void {
|
||||||
// Atomic write: write to tmp file, then rename
|
// Atomic write: write to tmp file, then rename
|
||||||
fs.writeFileSync(tmpFile, json);
|
fs.writeFileSync(tmpFile, json);
|
||||||
fs.renameSync(tmpFile, stateFile);
|
fs.renameSync(tmpFile, stateFile);
|
||||||
// Verify write by reading back
|
|
||||||
const verify = fs.readFileSync(stateFile, 'utf-8');
|
|
||||||
if (verify.length !== json.length) {
|
|
||||||
console.error(`[State] ⚠ Write verification FAILED: wrote ${json.length} bytes, read back ${verify.length}`);
|
|
||||||
lastSaveOk = false;
|
|
||||||
} else {
|
|
||||||
lastSaveOk = true;
|
lastSaveOk = true;
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('[State] ✗ Failed to save:', e);
|
console.error('[State] ✗ Failed to save:', e);
|
||||||
lastSaveOk = false;
|
lastSaveOk = false;
|
||||||
|
|
@ -101,7 +104,6 @@ export function getFullState(): Record<string, any> {
|
||||||
export function getStateDiag(): Record<string, any> {
|
export function getStateDiag(): Record<string, any> {
|
||||||
let fileExists = false;
|
let fileExists = false;
|
||||||
let fileSize = 0;
|
let fileSize = 0;
|
||||||
let fileKeys: string[] = [];
|
|
||||||
let fileFavCount = 0;
|
let fileFavCount = 0;
|
||||||
try {
|
try {
|
||||||
if (fs.existsSync(stateFile)) {
|
if (fs.existsSync(stateFile)) {
|
||||||
|
|
@ -109,19 +111,15 @@ export function getStateDiag(): Record<string, any> {
|
||||||
const stat = fs.statSync(stateFile);
|
const stat = fs.statSync(stateFile);
|
||||||
fileSize = stat.size;
|
fileSize = stat.size;
|
||||||
const parsed = JSON.parse(fs.readFileSync(stateFile, 'utf-8'));
|
const parsed = JSON.parse(fs.readFileSync(stateFile, 'utf-8'));
|
||||||
fileKeys = Object.keys(parsed);
|
|
||||||
fileFavCount = Array.isArray(parsed.radio_favorites) ? parsed.radio_favorites.length : 0;
|
fileFavCount = Array.isArray(parsed.radio_favorites) ? parsed.radio_favorites.length : 0;
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
const memKeys = Object.keys(state);
|
|
||||||
const memFavCount = Array.isArray(state.radio_favorites) ? state.radio_favorites.length : 0;
|
const memFavCount = Array.isArray(state.radio_favorites) ? state.radio_favorites.length : 0;
|
||||||
return {
|
return {
|
||||||
stateFile,
|
stateFile,
|
||||||
fileExists,
|
fileExists,
|
||||||
fileSize,
|
fileSize,
|
||||||
fileKeys,
|
|
||||||
fileFavCount,
|
fileFavCount,
|
||||||
memoryKeys: memKeys,
|
|
||||||
memoryFavCount: memFavCount,
|
memoryFavCount: memFavCount,
|
||||||
lastSaveOk,
|
lastSaveOk,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue