- 新增 PostgreSQL 备份、fresh DB 恢复和 server_data volume 备份脚本\n- 恢复默认拒绝覆盖,必须显式 --confirm-overwrite\n- 补充 package scripts、deploy verify 校验和部署文档\n\nCo-Authored-By: GPT-5 Codex <codex@openai.com>
100 lines
2.7 KiB
JavaScript
100 lines
2.7 KiB
JavaScript
#!/usr/bin/env node
|
|
import { basename, dirname, join, resolve } from 'node:path';
|
|
import { pathToFileURL } from 'node:url';
|
|
import {
|
|
commandToString,
|
|
ensureParentDir,
|
|
flag,
|
|
option,
|
|
parseArgs,
|
|
readEnvFile,
|
|
runCommand,
|
|
timestampSlug,
|
|
} from './ops-utils.mjs';
|
|
|
|
function usage() {
|
|
return `Usage: node scripts/backup-server-data.mjs [options]
|
|
|
|
Backs up the server_data Docker volume that stores runtime AI provider config.
|
|
|
|
Options:
|
|
--env-file <path> Compose env file used to derive COMPOSE_PROJECT_NAME
|
|
(default: .env.production)
|
|
--volume <name> Explicit Docker volume name
|
|
--backup-dir <path> Directory for generated backups (default: backups/server-data)
|
|
--output <path> Exact output .tgz path
|
|
--image <name> Utility image (default: alpine:3.20)
|
|
--dry-run Print the docker run 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 projectName = env.COMPOSE_PROJECT_NAME || 'ftb_pm';
|
|
const volume = option(args, 'volume', `${projectName}_server_data`);
|
|
const backupDir = option(args, 'backup-dir', 'backups/server-data');
|
|
const output = option(
|
|
args,
|
|
'output',
|
|
join(backupDir, `${volume}-${timestampSlug()}.tgz`),
|
|
);
|
|
|
|
return {
|
|
help: false,
|
|
dryRun: flag(args, 'dry-run'),
|
|
image: option(args, 'image', 'alpine:3.20'),
|
|
volume,
|
|
output: resolve(output),
|
|
};
|
|
}
|
|
|
|
export function buildServerDataBackupCommand(options) {
|
|
const outputDir = dirname(options.output);
|
|
const outputName = basename(options.output);
|
|
return [
|
|
'docker',
|
|
'run',
|
|
'--rm',
|
|
'-v',
|
|
`${options.volume}:/data:ro`,
|
|
'-v',
|
|
`${outputDir}:/backup`,
|
|
options.image,
|
|
'sh',
|
|
'-lc',
|
|
`tar -czf /backup/${outputName} -C /data .`,
|
|
];
|
|
}
|
|
|
|
export async function main(argv = process.argv.slice(2)) {
|
|
const options = buildOptions(argv);
|
|
if (options.help) {
|
|
process.stdout.write(usage());
|
|
return;
|
|
}
|
|
|
|
const command = buildServerDataBackupCommand(options);
|
|
if (options.dryRun) {
|
|
process.stdout.write(`[dry-run] server_data backup would write: ${options.output}\n`);
|
|
process.stdout.write(`${commandToString(command)}\n`);
|
|
return;
|
|
}
|
|
|
|
ensureParentDir(options.output);
|
|
await runCommand(command);
|
|
process.stdout.write(`server_data 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);
|
|
});
|
|
}
|
|
|