feat(d33): database backup script (pg_dump + rotation)
scripts/backup.mjs (npm run backup): pg_dump the DB to a timestamped compressed pg_restore-able file under ./backups (gitignored), keep newest HQ_BACKUP_KEEP (14). Locates pg_dump on PATH or the Windows PostgreSQL install dir. Verified against live data: 304 KB dump, 32 tables, valid archive. Backups hold plaintext creds (D32) so backups/ is gitignored + documented as access-controlled. Pending: schedule + off-box copy + DB password out of server.ts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>feat/client-detail-redesign
parent
583b663aa6
commit
62eb937012
@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* SiMS HQ database backup (ops slice). Dumps the Postgres database to a timestamped,
|
||||
* compressed file under ./backups (or $HQ_BACKUP_DIR), then rotates to the newest N.
|
||||
*
|
||||
* DATABASE_URL=postgres://user:pass@host:5432/db node scripts/backup.mjs
|
||||
* npm run backup (from apps/hq — passes DATABASE_URL through)
|
||||
*
|
||||
* Restore a dump with:
|
||||
* pg_restore --clean --if-exists --no-owner -d "$DATABASE_URL" backups/hq-<ts>.dump
|
||||
*
|
||||
* SECURITY (D32): credentials are stored in the CLEAR, so these dumps contain plaintext
|
||||
* logins. Keep the backups/ directory access-controlled and off any shared drive; it is
|
||||
* gitignored so it can never be committed.
|
||||
*
|
||||
* Env: DATABASE_URL (required), HQ_BACKUP_DIR (default ./backups), HQ_BACKUP_KEEP (default 14).
|
||||
*/
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { mkdirSync, readdirSync, statSync, unlinkSync, existsSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
|
||||
const url = process.env.DATABASE_URL
|
||||
if (!url || url.trim() === '') {
|
||||
console.error('DATABASE_URL is not set. Run: DATABASE_URL=postgres://… node scripts/backup.mjs')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
/** Locate pg_dump: PATH first, then a standard Windows PostgreSQL install. */
|
||||
function findPgDump() {
|
||||
try {
|
||||
execFileSync(process.platform === 'win32' ? 'where' : 'which', ['pg_dump'], { stdio: 'ignore' })
|
||||
return 'pg_dump'
|
||||
} catch { /* not on PATH */ }
|
||||
if (process.platform === 'win32') {
|
||||
const base = 'C:/Program Files/PostgreSQL'
|
||||
try {
|
||||
for (const v of readdirSync(base).sort().reverse()) {
|
||||
const cand = join(base, v, 'bin', 'pg_dump.exe')
|
||||
if (existsSync(cand)) return cand
|
||||
}
|
||||
} catch { /* no install dir */ }
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const pgDump = findPgDump()
|
||||
if (pgDump === null) {
|
||||
console.error('pg_dump not found. Install the PostgreSQL client tools, or add pg_dump to PATH.')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const OUT = process.env.HQ_BACKUP_DIR || join(process.cwd(), 'backups')
|
||||
const KEEP = Math.max(1, Number(process.env.HQ_BACKUP_KEEP || 14))
|
||||
mkdirSync(OUT, { recursive: true })
|
||||
|
||||
const d = new Date()
|
||||
const p2 = (n) => String(n).padStart(2, '0')
|
||||
const ts = `${d.getFullYear()}${p2(d.getMonth() + 1)}${p2(d.getDate())}-${p2(d.getHours())}${p2(d.getMinutes())}${p2(d.getSeconds())}`
|
||||
const file = join(OUT, `hq-${ts}.dump`)
|
||||
|
||||
// -Fc = custom, compressed, restorable with pg_restore. --no-owner keeps it portable.
|
||||
try {
|
||||
execFileSync(pgDump, ['-Fc', '--no-owner', '-f', file, url], { stdio: ['ignore', 'inherit', 'inherit'] })
|
||||
} catch (e) {
|
||||
console.error(`pg_dump failed: ${e instanceof Error ? e.message : String(e)}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const kb = (statSync(file).size / 1024).toFixed(1)
|
||||
console.log(`✓ backup written: ${file} (${kb} KB)`)
|
||||
|
||||
// Rotation: keep the newest KEEP hq-YYYYMMDD-HHMMSS.dump files.
|
||||
const dumps = readdirSync(OUT).filter((f) => /^hq-\d{8}-\d{6}\.dump$/.test(f)).sort()
|
||||
for (const f of dumps.slice(0, Math.max(0, dumps.length - KEEP))) {
|
||||
unlinkSync(join(OUT, f))
|
||||
console.log(` rotated out: ${f}`)
|
||||
}
|
||||
console.log(`retained ${Math.min(dumps.length, KEEP)} backup(s) in ${OUT} (HQ_BACKUP_KEEP=${KEEP})`)
|
||||
Loading…
Reference in New Issue