- package.json (ESM, bin entry) - bin/delta-backup.js — entrypoint - lib/args.js — CLI arg parsing via Node parseArgs - lib/config.js — config file merging + required path guards - lib/spawn.js — safe process spawning (no shell strings) - lib/state.js — sequence number + phase state management - lib/backends/zstd.js — zstd delta backend - lib/backends/index.js — backend registry - lib/commands/run.js — full run skeleton (phases 1-3 wired, 4-6 stubbed) - lib/commands/status.js — status command
77 lines
3.3 KiB
JavaScript
77 lines
3.3 KiB
JavaScript
/**
|
|
* run command — full backup run.
|
|
*/
|
|
import { rm, mkdir } from 'fs/promises';
|
|
import { join } from 'path';
|
|
import { run as spawn } from '../spawn.js';
|
|
import { getBackend } from '../backends/index.js';
|
|
import { readState, writeState, PHASES } from '../state.js';
|
|
|
|
export async function runCommand(config) {
|
|
const { source, prev, pend, deltas, backend: backendName, dryRun } = config;
|
|
const backend = getBackend(backendName);
|
|
const dry = dryRun;
|
|
|
|
if (dry) console.log('[dry-run] No changes will be made.\n');
|
|
|
|
// ── Load state ──────────────────────────────────────────────
|
|
const state = await readState(deltas);
|
|
const seq = state.next_seq;
|
|
|
|
console.log(`Starting run — seq ${seq} (last complete: ${state.last_complete})`);
|
|
|
|
// TODO: detect and handle partially-committed previous run
|
|
|
|
// ── Phase 1: Clear PEND ─────────────────────────────────────
|
|
await setPhase(deltas, state, PHASES.CLEARING_PEND, dry);
|
|
console.log('\n── Clear PEND ──');
|
|
if (!dry) {
|
|
await rm(pend, { recursive: true, force: true });
|
|
await mkdir(pend, { recursive: true });
|
|
} else {
|
|
console.log(`[dry-run] rm -rf ${pend} && mkdir -p ${pend}`);
|
|
}
|
|
|
|
// ── Phase 2: rsync PREV → PEND (local seed) ─────────────────
|
|
await setPhase(deltas, state, PHASES.RSYNC_LOCAL, dry);
|
|
console.log('\n── rsync PREV → PEND (local seed) ──');
|
|
await spawn('rsync', ['-aP', trailingSlash(prev), pend], { dryRun: dry });
|
|
|
|
// ── Phase 3: rsync SOURCE → PEND (remote changes) ───────────
|
|
await setPhase(deltas, state, PHASES.RSYNC_REMOTE, dry);
|
|
console.log('\n── rsync SOURCE → PEND ──');
|
|
await spawn('rsync', ['-aP', trailingSlash(source), pend], { dryRun: dry });
|
|
|
|
// ── Phase 4: Generate delta ──────────────────────────────────
|
|
await setPhase(deltas, state, PHASES.GENERATING, dry);
|
|
console.log('\n── Generate delta ──');
|
|
// TODO: walk PREV and PEND, diff per file, build manifest
|
|
|
|
// ── Phase 5: Commit delta ────────────────────────────────────
|
|
await setPhase(deltas, state, PHASES.COMMITTING, dry);
|
|
console.log('\n── Commit delta ──');
|
|
// TODO: atomic rename DELTAS/tmp/N → DELTAS/N
|
|
|
|
// ── Phase 6: Promote PEND → PREV ────────────────────────────
|
|
await setPhase(deltas, state, PHASES.PROMOTING, dry);
|
|
console.log('\n── Promote PEND → PREV ──');
|
|
// TODO: mv PEND PREV (swap)
|
|
|
|
// ── Done ─────────────────────────────────────────────────────
|
|
state.last_complete = seq;
|
|
state.next_seq = seq + 1;
|
|
state.phase = PHASES.IDLE;
|
|
if (!dry) await writeState(deltas, state);
|
|
|
|
console.log(`\nRun complete — seq ${seq} committed.`);
|
|
}
|
|
|
|
async function setPhase(deltas, state, phase, dry) {
|
|
state.phase = phase;
|
|
if (!dry) await writeState(deltas, state);
|
|
}
|
|
|
|
function trailingSlash(p) {
|
|
return p.endsWith('/') ? p : p + '/';
|
|
}
|