feat(ops): 添加生产备份恢复脚本
- 新增 PostgreSQL 备份、fresh DB 恢复和 server_data volume 备份脚本\n- 恢复默认拒绝覆盖,必须显式 --confirm-overwrite\n- 补充 package scripts、deploy verify 校验和部署文档\n\nCo-Authored-By: GPT-5 Codex <codex@openai.com>
This commit is contained in:
116
scripts/ops-utils.mjs
Normal file
116
scripts/ops-utils.mjs
Normal file
@@ -0,0 +1,116 @@
|
||||
import { existsSync, mkdirSync, readFileSync } from 'node:fs';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import { spawn } from 'node:child_process';
|
||||
|
||||
export function parseArgs(argv) {
|
||||
const parsed = { flags: new Set(), values: new Map(), positionals: [] };
|
||||
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const arg = argv[index];
|
||||
if (arg === '--') continue;
|
||||
if (!arg.startsWith('--')) {
|
||||
parsed.positionals.push(arg);
|
||||
continue;
|
||||
}
|
||||
|
||||
const eq = arg.indexOf('=');
|
||||
if (eq !== -1) {
|
||||
parsed.values.set(arg.slice(2, eq), arg.slice(eq + 1));
|
||||
continue;
|
||||
}
|
||||
|
||||
const key = arg.slice(2);
|
||||
const next = argv[index + 1];
|
||||
if (next && !next.startsWith('--')) {
|
||||
parsed.values.set(key, next);
|
||||
index += 1;
|
||||
} else {
|
||||
parsed.flags.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
export function option(args, name, fallback = undefined) {
|
||||
return args.values.has(name) ? args.values.get(name) : fallback;
|
||||
}
|
||||
|
||||
export function flag(args, name) {
|
||||
return args.flags.has(name);
|
||||
}
|
||||
|
||||
export function readEnvFile(filePath, { optional = false } = {}) {
|
||||
if (!existsSync(filePath)) {
|
||||
if (optional) return {};
|
||||
throw new Error(`Env file not found: ${filePath}`);
|
||||
}
|
||||
|
||||
const env = {};
|
||||
const content = readFileSync(filePath, 'utf8');
|
||||
for (const rawLine of content.split(/\r?\n/)) {
|
||||
const line = rawLine.trim();
|
||||
if (!line || line.startsWith('#')) continue;
|
||||
const eq = line.indexOf('=');
|
||||
if (eq === -1) continue;
|
||||
const key = line.slice(0, eq).trim();
|
||||
let value = line.slice(eq + 1).trim();
|
||||
if (
|
||||
(value.startsWith('"') && value.endsWith('"')) ||
|
||||
(value.startsWith("'") && value.endsWith("'"))
|
||||
) {
|
||||
value = value.slice(1, -1);
|
||||
}
|
||||
env[key] = value;
|
||||
}
|
||||
return env;
|
||||
}
|
||||
|
||||
export function ensureParentDir(filePath) {
|
||||
mkdirSync(dirname(filePath), { recursive: true });
|
||||
}
|
||||
|
||||
export function timestampSlug(date = new Date()) {
|
||||
return date.toISOString().replace(/[-:]/g, '').replace(/\.\d{3}Z$/, 'Z');
|
||||
}
|
||||
|
||||
export function shellQuote(value) {
|
||||
if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(value)) return value;
|
||||
return `'${value.replace(/'/g, "'\\''")}'`;
|
||||
}
|
||||
|
||||
export function commandToString(command, { stdin, stdout } = {}) {
|
||||
const rendered = command.map((part) => shellQuote(part)).join(' ');
|
||||
const withStdin = stdin ? `${rendered} < ${shellQuote(stdin)}` : rendered;
|
||||
return stdout ? `${withStdin} > ${shellQuote(stdout)}` : withStdin;
|
||||
}
|
||||
|
||||
export function composePrefix({ envFile, composeFile }) {
|
||||
return ['docker', 'compose', '--env-file', envFile, '-f', composeFile];
|
||||
}
|
||||
|
||||
export function resolvePath(path) {
|
||||
return resolve(process.cwd(), path);
|
||||
}
|
||||
|
||||
export function runCommand(command, options = {}) {
|
||||
return new Promise((resolveRun, rejectRun) => {
|
||||
const child = spawn(command[0], command.slice(1), {
|
||||
stdio: options.stdio ?? 'inherit',
|
||||
cwd: options.cwd ?? process.cwd(),
|
||||
});
|
||||
|
||||
child.on('error', rejectRun);
|
||||
child.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
resolveRun();
|
||||
} else {
|
||||
rejectRun(new Error(`Command failed (${code}): ${commandToString(command)}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function quotePgLiteral(value) {
|
||||
return `'${value.replace(/'/g, "''")}'`;
|
||||
}
|
||||
Reference in New Issue
Block a user