#!/usr/bin/env node import fs from "node:fs"; import path from "node:path"; import { spawnSync } from "node:child_process"; import "dotenv/config"; const args = process.argv.slice(2); function getArg(name) { const index = args.indexOf(name); return index >= 0 ? args[index + 1] : undefined; } function hasFlag(name) { return args.includes(name); } function usage() { console.log(`Usage: node scripts/backup-production.mjs [--out ./backups] [--include-mosquitto] [--dry-run] Creates a timestamped PostgreSQL custom-format dump. When --include-mosquitto is set, copies Mosquitto passwd/ACL files if readable. `); } if (hasFlag("--help") || hasFlag("-h")) { usage(); process.exit(0); } const outDir = path.resolve(process.cwd(), getArg("--out") || process.env.BACKUP_DIR || "./backups"); const includeMosquitto = hasFlag("--include-mosquitto"); const dryRun = hasFlag("--dry-run"); const timestamp = new Date().toISOString().replace(/[:.]/g, "-"); const dbName = process.env.PGDATABASE || "qris_soundbox_platform"; const dbBackup = path.join(outDir, `${dbName}-${timestamp}.dump`); const passwdFile = process.env.MOSQUITTO_PASSWD_FILE || "/etc/mosquitto/passwd"; const aclFile = process.env.MOSQUITTO_ACL_FILE || "/etc/mosquitto/acl"; fs.mkdirSync(outDir, { recursive: true }); const pgDumpArgs = ["-Fc", "-f", dbBackup]; if (process.env.DATABASE_URL) { pgDumpArgs.push(process.env.DATABASE_URL); } else { pgDumpArgs.push("-h", process.env.PGHOST || "127.0.0.1"); pgDumpArgs.push("-p", String(process.env.PGPORT || 5432)); pgDumpArgs.push("-U", process.env.PGUSER || "postgres"); pgDumpArgs.push(dbName); } const plan = { out_dir: outDir, database_backup: dbBackup, include_mosquitto: includeMosquitto, commands: [`pg_dump ${pgDumpArgs.map((arg) => (arg.includes(" ") ? JSON.stringify(arg) : arg)).join(" ")}`] }; if (dryRun) { console.log(JSON.stringify({ dry_run: true, ...plan }, null, 2)); process.exit(0); } const result = spawnSync("pg_dump", pgDumpArgs, { stdio: "inherit", env: process.env }); if (result.status !== 0) { throw new Error(`pg_dump failed with status ${result.status}`); } const copied = []; if (includeMosquitto) { for (const source of [passwdFile, aclFile]) { if (!fs.existsSync(source)) { continue; } const target = path.join(outDir, `${path.basename(source)}-${timestamp}`); fs.copyFileSync(source, target); copied.push({ source, target }); } } console.log(JSON.stringify({ ok: true, ...plan, copied }, null, 2));