- 新增 PostgreSQL 备份、fresh DB 恢复和 server_data volume 备份脚本\n- 恢复默认拒绝覆盖,必须显式 --confirm-overwrite\n- 补充 package scripts、deploy verify 校验和部署文档\n\nCo-Authored-By: GPT-5 Codex <codex@openai.com>
140 lines
3.8 KiB
JavaScript
140 lines
3.8 KiB
JavaScript
#!/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 <path> Compose env file (default: .env.production)
|
|
--compose-file <path> Compose file (default: docker-compose.prod.yml)
|
|
--service <name> Postgres service name (default: postgres)
|
|
--user <name> Database user (default: POSTGRES_USER or postgres)
|
|
--db <name> Database name (default: POSTGRES_DB or ftb_pm)
|
|
--backup-dir <path> Directory for generated backups (default: backups/postgres)
|
|
--output <path> 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);
|
|
});
|
|
}
|
|
|