/** * WAL-safe SQLite backup via better-sqlite3 backup API (not a raw file copy). * Usage: DB_PATH=./data/app.db BACKUP_DIR=./backups node scripts/sqlite-backup.mjs */ import Database from 'better-sqlite3' import { mkdirSync, existsSync, readdirSync, statSync, unlinkSync } from 'node:fs' import { join } from 'node:path' const srcPath = process.env.DB_PATH ?? join(process.cwd(), 'data', 'app.db') const backupDir = process.env.BACKUP_DIR ?? join(process.cwd(), 'backups') const retentionDays = Number(process.env.BACKUP_RETENTION_DAYS ?? '0') if (!existsSync(srcPath)) { console.error(`Source database not found: ${srcPath}`) process.exit(1) } mkdirSync(backupDir, { recursive: true }) const stamp = new Date().toISOString().replace(/[:.]/g, '-') const destPath = join(backupDir, `app-${stamp}.db`) const src = new Database(srcPath, { readonly: true }) try { await src.backup(destPath) } finally { src.close() } const verify = new Database(destPath, { readonly: true }) try { const integrity = verify.pragma('integrity_check', { simple: true }) if (integrity !== 'ok') { console.error(`Backup integrity check failed: ${integrity}`) process.exit(1) } } finally { verify.close() } console.log(`Backup written: ${destPath}`) if (retentionDays > 0) { const cutoff = Date.now() - retentionDays * 86_400_000 for (const name of readdirSync(backupDir)) { if (!name.startsWith('app-') || !name.endsWith('.db')) continue const full = join(backupDir, name) if (statSync(full).mtimeMs < cutoff) { unlinkSync(full) console.log(`Removed old backup: ${name}`) } } }