backups go to another machine, and old ones are pruned
Three things. The local directory defaults to <officerdev root>/dockers/postgres/dumps, derived from OFFICER_ROOT rather than written down — the same reasoning as rootPath: a path fixed at install freezes the layout of the machine it was installed on, and a restore elsewhere then points somewhere that is not there while looking deliberately chosen. The destination is SSH, and it is now what "set up" MEANS: a dump sitting on the machine it came from is a second copy on one disk, lost to the same failure that would make anybody want it. So local is staging and the copy off the box is the backup, and a schedule with nowhere to send it does not count as configured. Testable, in two steps reported separately, because they fail for different reasons and the fixes differ: the connection is keys and networking, the directory is permissions on the far side. It tests what is in the FORM rather than what is stored — testing a saved destination while somebody looks at a different one is worse than not testing. Everything goes out with BatchMode=yes. Nobody is at a terminal when this runs, so a key that has lost access must FAIL rather than block forever on a prompt nothing will answer. It is also why no credential is stored: there is none to store and none to leak. The remote path is the one value here a shell interprets — `ssh host mkdir -p <path>` runs through it — so shell characters are refused rather than escaped. Refusing is checkable by reading it; escaping is checkable by being right about every shell. And rotation keeps the newest N per database at both ends. Per database, because two sharing one budget means a busy one evicts the other's history. Sorted by NAME rather than mtime: the timestamp is in the filename and is sortable, while mtime stops meaning "when this was taken" the moment a file is copied. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+33
-3
@@ -9,7 +9,7 @@ import { readSync, startSync } from './sync';
|
||||
import { readRuns } from './ledger';
|
||||
import { reconcile } from './derive';
|
||||
import { createDatabases, pushSchema, readDatabases } from './databases';
|
||||
import { readBackups, runBackup, saveBackups, schedule } from './backups';
|
||||
import { readBackups, runBackup, saveBackups, schedule, testDestination } from './backups';
|
||||
import { join } from 'node:path';
|
||||
import { fofosRequest, FofosError, probe, type FofosResponse } from './fofos-client';
|
||||
|
||||
@@ -330,9 +330,39 @@ router.post('/_config/databases/create', async (ctx) => {
|
||||
router.get('/_config/backups', async (ctx) => ctx.json(await readBackups()));
|
||||
|
||||
router.put('/_config/backups', async (ctx) => {
|
||||
const { cron, dir } = (ctx.get('body') ?? {}) as { cron?: string; dir?: string };
|
||||
const body = (ctx.get('body') ?? {}) as {
|
||||
cron?: string;
|
||||
dir?: string;
|
||||
sshHost?: string;
|
||||
sshPath?: string;
|
||||
keep?: number | null;
|
||||
};
|
||||
try {
|
||||
return ctx.json(await saveBackups(cron ?? '', dir ?? ''));
|
||||
return ctx.json(
|
||||
await saveBackups({
|
||||
cron: body.cron ?? '',
|
||||
dir: body.dir ?? '',
|
||||
sshHost: body.sshHost ?? '',
|
||||
sshPath: body.sshPath ?? '',
|
||||
keep: body.keep ?? null,
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
return ctx.json({ error: (error as Error).message }, 400);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Prove the destination before anything is trusted to it.
|
||||
*
|
||||
* Takes the values from the FORM rather than from storage, so it tests what is on screen — testing a
|
||||
* saved destination while somebody looks at a different one is worse than not testing at all.
|
||||
*/
|
||||
router.post('/_config/backups/test', async (ctx) => {
|
||||
const { sshHost, sshPath } = (ctx.get('body') ?? {}) as { sshHost?: string; sshPath?: string };
|
||||
try {
|
||||
const result = await testDestination(sshHost ?? '', sshPath ?? '');
|
||||
return ctx.json(result, result.ok ? 200 : 400);
|
||||
} catch (error) {
|
||||
return ctx.json({ error: (error as Error).message }, 400);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user