121 lines
3.9 KiB
JavaScript
121 lines
3.9 KiB
JavaScript
#!/usr/bin/env node
|
|
import { spawn } from 'node:child_process';
|
|
import fs from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
|
|
function parseArgs(argv) {
|
|
const [action = '', ...rest] = argv;
|
|
const options = { action };
|
|
for (let index = 0; index < rest.length; index += 1) {
|
|
const item = rest[index];
|
|
if (item === '--output') options.output = rest[++index];
|
|
else if (item === '--input') options.input = rest[++index];
|
|
else if (item === '--database-url') options.databaseUrl = rest[++index];
|
|
else if (item === '--confirm-empty-target') options.confirmEmptyTarget = true;
|
|
else if (item === '--allow-remote-target') options.allowRemoteTarget = true;
|
|
}
|
|
return options;
|
|
}
|
|
|
|
function run(command, args, { env = process.env } = {}) {
|
|
return new Promise((resolve, reject) => {
|
|
const child = spawn(command, args, {
|
|
env,
|
|
stdio: ['ignore', 'inherit', 'inherit'],
|
|
shell: false,
|
|
});
|
|
child.once('error', reject);
|
|
child.once('exit', (code, signal) => {
|
|
if (code === 0) resolve();
|
|
else reject(new Error(`${command} exited with ${signal ?? code}`));
|
|
});
|
|
});
|
|
}
|
|
|
|
function databaseUrl(options) {
|
|
const value = String(
|
|
options.databaseUrl ?? process.env.MEMIND_ORCHESTRATOR_DATABASE_URL ?? '',
|
|
).trim();
|
|
if (!value) throw new Error('Provide --database-url or MEMIND_ORCHESTRATOR_DATABASE_URL');
|
|
return value;
|
|
}
|
|
|
|
function requireLocalOrExplicitRemote(url, options) {
|
|
const hostname = new URL(url).hostname.toLowerCase();
|
|
const local = ['localhost', '127.0.0.1', '::1', 'postgres'].includes(hostname);
|
|
if (!local && !options.allowRemoteTarget) {
|
|
throw new Error('Remote database targets require --allow-remote-target');
|
|
}
|
|
}
|
|
|
|
async function backup(options) {
|
|
const output = path.resolve(String(options.output ?? ''));
|
|
if (!options.output || path.extname(output) !== '.dump') {
|
|
throw new Error('Backup requires an explicit --output path ending in .dump');
|
|
}
|
|
await fs.mkdir(path.dirname(output), { recursive: true });
|
|
await fs.access(output).then(
|
|
() => Promise.reject(new Error(`Refusing to overwrite existing backup: ${output}`)),
|
|
() => null,
|
|
);
|
|
await run('pg_dump', [
|
|
'--format=custom',
|
|
'--no-owner',
|
|
'--no-privileges',
|
|
'--file',
|
|
output,
|
|
databaseUrl(options),
|
|
]);
|
|
console.log(JSON.stringify({ ok: true, action: 'backup', output }));
|
|
}
|
|
|
|
async function verify(options) {
|
|
const input = path.resolve(String(options.input ?? ''));
|
|
if (!options.input) throw new Error('Verify requires --input');
|
|
await fs.access(input);
|
|
await run('pg_restore', ['--list', input]);
|
|
console.log(JSON.stringify({ ok: true, action: 'verify', input }));
|
|
}
|
|
|
|
async function restore(options) {
|
|
if (!options.confirmEmptyTarget) {
|
|
throw new Error('Restore requires --confirm-empty-target and never cleans an existing database');
|
|
}
|
|
const input = path.resolve(String(options.input ?? ''));
|
|
if (!options.input) throw new Error('Restore requires --input');
|
|
await fs.access(input);
|
|
const target = databaseUrl(options);
|
|
requireLocalOrExplicitRemote(target, options);
|
|
await run('pg_restore', [
|
|
'--exit-on-error',
|
|
'--no-owner',
|
|
'--no-privileges',
|
|
'--dbname',
|
|
target,
|
|
input,
|
|
]);
|
|
console.log(JSON.stringify({ ok: true, action: 'restore', input }));
|
|
}
|
|
|
|
const options = parseArgs(process.argv.slice(2));
|
|
try {
|
|
if (options.action === 'backup') await backup(options);
|
|
else if (options.action === 'verify') await verify(options);
|
|
else if (options.action === 'restore') await restore(options);
|
|
else {
|
|
throw new Error(
|
|
'Usage: orchestrator-dr.mjs backup --output file.dump | verify --input file.dump | restore --input file.dump --confirm-empty-target',
|
|
);
|
|
}
|
|
} catch (error) {
|
|
console.error(error instanceof Error ? error.message : String(error));
|
|
process.exitCode = 1;
|
|
}
|
|
|
|
export const orchestratorDrInternals = {
|
|
databaseUrl,
|
|
parseArgs,
|
|
requireLocalOrExplicitRemote,
|
|
run,
|
|
};
|