/** * Restore backup to a target path (service must be stopped). * Usage: node scripts/sqlite-restore.mjs */ import Database from 'better-sqlite3' import { copyFileSync, existsSync, mkdirSync, renameSync, rmSync, statSync, } from 'node:fs' import { dirname, join, resolve } from 'node:path' import { validateAppSqliteFile } from './sqlite-app-schema.mjs' const backupPath = resolve(process.argv[2] ?? '') const targetPath = resolve(process.argv[3] ?? '') if (!backupPath || !targetPath) { console.error('Usage: node scripts/sqlite-restore.mjs ') process.exit(1) } if (!existsSync(backupPath)) { console.error(`Backup not found: ${backupPath}`) process.exit(1) } const activeDb = resolve(process.env.DB_PATH ?? join(process.cwd(), 'data', 'app.db')) const targetExists = existsSync(targetPath) if (targetPath === activeDb) { console.error('Restore onto active DB_PATH is not allowed while it is configured as the live database.') console.error('Stop the backend service, point DB_PATH elsewhere, or restore to a temp file first.') process.exit(1) } if (targetExists && process.env.RESTORE_ACK_STOPPED !== 'YES') { console.error( 'Target database file already exists. Stop the backend service and set RESTORE_ACK_STOPPED=YES to continue.', ) process.exit(1) } const schema = validateAppSqliteFile(backupPath) if (!schema.ok) { console.error(`Backup rejected: ${schema.reason}`) process.exit(1) } const staging = `${targetPath}.restore-staging` const stagingWal = `${staging}-wal` const stagingShm = `${staging}-shm` function cleanupStaging() { for (const p of [staging, stagingWal, stagingShm]) { try { if (existsSync(p)) rmSync(p) } catch { // ignore } } } cleanupStaging() try { copyFileSync(backupPath, staging) const stagedCheck = validateAppSqliteFile(staging) if (!stagedCheck.ok) { console.error(`Staging validation failed: ${stagedCheck.reason}`) cleanupStaging() process.exit(1) } const verify = new Database(staging, { readonly: true }) try { const integrity = verify.pragma('integrity_check', { simple: true }) if (integrity !== 'ok') { console.error(`Staging integrity check failed: ${integrity}`) cleanupStaging() process.exit(1) } } finally { verify.close() } for (const sidecar of [`${targetPath}-wal`, `${targetPath}-shm`]) { try { if (existsSync(sidecar)) rmSync(sidecar) } catch { // ignore } } if (targetExists) { const backupOfTarget = `${targetPath}.pre-restore-${Date.now()}.bak` renameSync(targetPath, backupOfTarget) console.log(`Existing target moved to ${backupOfTarget}`) } renameSync(staging, targetPath) console.log(`Restored ${backupPath} -> ${targetPath}`) } catch (error) { cleanupStaging() console.error(error instanceof Error ? error.message : error) process.exit(1) }