Backend CI / type-check, build, tests (push) Successful in 1m0s
Co-authored-by: Cursor <cursoragent@cursor.com>
110 lines
2.9 KiB
JavaScript
110 lines
2.9 KiB
JavaScript
/**
|
|
* Restore backup to a target path (service must be stopped).
|
|
* Usage: node scripts/sqlite-restore.mjs <backup.db> <target.db>
|
|
*/
|
|
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 <backup.db> <target.db>')
|
|
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)
|
|
}
|