feat: prepare six-lesson trial backend with reliable rewards and operations
Backend CI / type-check, build, tests (push) Successful in 1m0s
Backend CI / type-check, build, tests (push) Successful in 1m0s
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
Reference in New Issue
Block a user