#!/usr/bin/env node import { createWriteStream, unlinkSync } from 'node:fs'; import { dirname, join, resolve } from 'node:path'; import { pathToFileURL } from 'node:url'; import { spawn } from 'node:child_process'; import { commandToString, composePrefix, ensureParentDir, flag, option, parseArgs, readEnvFile, timestampSlug, } from './ops-utils.mjs'; function usage() { return `Usage: node scripts/backup-postgres.mjs [options] Options: --env-file Compose env file (default: .env.production) --compose-file Compose file (default: docker-compose.prod.yml) --service Postgres service name (default: postgres) --user Database user (default: POSTGRES_USER or postgres) --db Database name (default: POSTGRES_DB or ftb_pm) --backup-dir Directory for generated backups (default: backups/postgres) --output Exact output dump path --dry-run Print the pg_dump command without writing a file --help Show this help `; } function buildOptions(argv) { const args = parseArgs(argv); if (flag(args, 'help')) return { help: true }; const envFile = option(args, 'env-file', '.env.production'); const env = readEnvFile(envFile, { optional: flag(args, 'dry-run') }); const db = option(args, 'db', env.POSTGRES_DB || 'ftb_pm'); const user = option(args, 'user', env.POSTGRES_USER || 'postgres'); const backupDir = option(args, 'backup-dir', 'backups/postgres'); const output = option( args, 'output', join(backupDir, `${db}-postgres-${timestampSlug()}.dump`), ); return { help: false, dryRun: flag(args, 'dry-run'), envFile, composeFile: option(args, 'compose-file', 'docker-compose.prod.yml'), service: option(args, 'service', 'postgres'), db, user, output: resolve(output), }; } export function buildPgDumpCommand(options) { return [ ...composePrefix(options), 'exec', '-T', options.service, 'pg_dump', '-U', options.user, '-d', options.db, '--format=custom', '--no-owner', '--no-acl', ]; } async function writeBackup(command, output) { ensureParentDir(output); await new Promise((resolveWrite, rejectWrite) => { const file = createWriteStream(output, { flags: 'wx' }); const child = spawn(command[0], command.slice(1), { cwd: process.cwd(), stdio: ['ignore', 'pipe', 'inherit'], }); let settled = false; const rejectOnce = (error) => { if (settled) return; settled = true; try { unlinkSync(output); } catch { // Best effort cleanup of a partial dump. } rejectWrite(error); }; child.stdout.pipe(file); child.on('error', rejectOnce); file.on('error', rejectOnce); child.on('close', (code) => { file.end(() => { if (settled) return; settled = true; if (code === 0) { resolveWrite(); } else { rejectOnce(new Error(`pg_dump failed with exit code ${code}`)); } }); }); }); } export async function main(argv = process.argv.slice(2)) { const options = buildOptions(argv); if (options.help) { process.stdout.write(usage()); return; } const command = buildPgDumpCommand(options); if (options.dryRun) { process.stdout.write(`[dry-run] PostgreSQL backup would write: ${options.output}\n`); process.stdout.write(`${commandToString(command, { stdout: options.output })}\n`); return; } await writeBackup(command, options.output); process.stdout.write(`PostgreSQL backup written: ${options.output}\n`); } if (import.meta.url === pathToFileURL(process.argv[1]).href) { main().catch((error) => { process.stderr.write(`${error.message}\n`); process.exit(1); }); }