new raceday and backup
This commit is contained in:
84
scripts/restore-backup.js
Normal file
84
scripts/restore-backup.js
Normal file
@@ -0,0 +1,84 @@
|
||||
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 -- <backup-file.sqlite> --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)));
|
||||
Reference in New Issue
Block a user