import { createRequire } from 'node:module' import { resolve, dirname } from 'node:path' import { existsSync, mkdirSync, renameSync, rmSync, realpathSync } from 'node:fs' // Resolve through the production workspace package; pnpm isolates transitive deps. const require = createRequire(realpathSync(resolve('node_modules/@evofw/db/package.json'))) const Database = require('better-sqlite3') const [mode, input, output, offline] = process.argv.slice(2) if (!['backup', 'restore', 'verify'].includes(mode) || !input || (mode !== 'verify' && !output)) { throw new Error('Usage: sqlite-backup.mjs backup|verify|restore INPUT [OUTPUT] [--offline]') } if (mode === 'restore' && offline !== '--offline') throw new Error('Restore requires a stopped volume and --offline') if (output && resolve(input) === resolve(output)) throw new Error('Input and output must differ') if (mode === 'backup' && existsSync(output)) throw new Error('Backup destination already exists') const source = new Database(input, { readonly: true, fileMustExist: true }) let temporary try { if (source.pragma('integrity_check', { simple: true }) !== 'ok') throw new Error('Source integrity_check failed') if (mode !== 'verify') { mkdirSync(dirname(output), { recursive: true }) temporary = `${output}.partial-${process.pid}` // SQLite online backup includes committed WAL pages while the API runs. await source.backup(temporary) const check = new Database(temporary, { readonly: true, fileMustExist: true }) try { if (check.pragma('integrity_check', { simple: true }) !== 'ok') throw new Error('Backup integrity_check failed') if (check.pragma('foreign_key_check').length) throw new Error('Backup foreign_key_check failed') } finally { check.close() } if (mode === 'restore') { // Only safe after the caller has excluded every process using this volume. rmSync(`${output}-wal`, { force: true }) rmSync(`${output}-shm`, { force: true }) } renameSync(temporary, output) temporary = undefined } console.log(JSON.stringify({ mode, input, output, integrity: 'ok' })) } finally { source.close() if (temporary) rmSync(temporary, { force: true }) }