const fs = require("fs"); const path = require("path"); const Database = require("better-sqlite3"); const root = path.resolve(__dirname, ".."); const dataDir = process.env.RC_TIMING_DATA_DIR ? path.resolve(process.env.RC_TIMING_DATA_DIR) : path.join(root, "data"); const backupDir = path.join(dataDir, "backups"); const dbPath = path.join(dataDir, "rc_timing.sqlite"); const pidFile = process.env.RC_TIMING_PID_FILE || path.join(dataDir, "server.pid"); const fileName = String(process.argv[2] || ""); const confirmed = process.argv.includes("--confirm"); function fail(message) { console.error(`[restore] ${message}`); process.exit(1); } function isProcessRunning(pid) { if (!Number.isInteger(pid) || pid <= 0) return false; try { process.kill(pid, 0); return true; } catch (error) { return error?.code === "EPERM"; } } async function main() { if (!fileName || path.basename(fileName) !== fileName || !/^[a-zA-Z0-9_.-]+\.sqlite$/.test(fileName)) { fail("Usage: npm run backup:restore -- --confirm"); } if (!confirmed) fail("Add --confirm after the backup file to confirm the restore."); if (fs.existsSync(pidFile)) { const pid = Number(fs.readFileSync(pidFile, "utf8").trim()); if (isProcessRunning(pid)) fail(`RC RaceCore is running with PID ${pid}. Run npm stop first.`); } const sourcePath = path.join(backupDir, fileName); if (!fs.existsSync(sourcePath)) fail(`Backup not found: ${sourcePath}`); const sourceDb = new Database(sourcePath, { readonly: true, fileMustExist: true }); try { const quickCheck = sourceDb.pragma("quick_check", { simple: true }); if (quickCheck !== "ok") fail(`Backup failed SQLite quick_check: ${quickCheck}`); const state = sourceDb.prepare("SELECT state_json FROM app_state WHERE id = 1").get(); if (!state?.state_json) fail("Backup does not contain RC RaceCore application state."); JSON.parse(state.state_json); } finally { sourceDb.close(); } fs.mkdirSync(backupDir, { recursive: true }); let safetyPath = ""; if (fs.existsSync(dbPath)) { const currentDb = new Database(dbPath); const timestamp = new Date().toISOString().replace(/[:.]/g, "-"); safetyPath = path.join(backupDir, `${timestamp}__pre_restore__current.sqlite`); try { currentDb.pragma("wal_checkpoint(TRUNCATE)"); await currentDb.backup(safetyPath); } finally { currentDb.close(); } fs.writeFileSync(safetyPath.replace(/\.sqlite$/, ".json"), JSON.stringify({ fileName: path.basename(safetyPath), createdAt: new Date().toISOString(), reason: "pre_restore", sessionId: "", eventId: "", }, null, 2)); } const temporaryPath = `${dbPath}.restore.tmp`; fs.copyFileSync(sourcePath, temporaryPath); fs.rmSync(dbPath, { force: true }); fs.renameSync(temporaryPath, dbPath); fs.rmSync(`${dbPath}-wal`, { force: true }); fs.rmSync(`${dbPath}-shm`, { force: true }); console.log(`[restore] Restored: ${fileName}`); if (safetyPath) console.log(`[restore] Previous database backup: ${safetyPath}`); console.log("[restore] Start RC RaceCore with: npm start"); } main().catch((error) => fail(error instanceof Error ? error.message : String(error)));