A pg_dump carries CREATE TABLE, so it cannot be layered onto a database that
already has the schema — it fails on every object. The only honest restore
empties the target first, and that is not a detail to bury behind a button. So
choosing a destination and confirming are two separate clicks, and the second
names what it is about to destroy.
`confirm` is required by the function, not merely by the panel, so no code path
can reach a restore by accident.
The file is matched against what listDumps actually found in the backup
directory rather than taken as given. There is no path to traverse out of and no
way to point psql at a file this module did not write: an unknown name is
refused before anything is opened, rather than sanitised and hoped about.
Emptied with DROP SCHEMA public CASCADE through psql, not by dropping the
DATABASE — that would need a connection to another one, fail while anything is
attached, and lose whatever was granted on it. Replacing the schema does the
same job to the part that matters.
psql rather than pg_restore, because these are plain-SQL dumps, which is what
pg_dump writes without -F c.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Requiring an absolute one was simply wrong. `scp file host:db-backups/fofos/`
lands under the remote account's home — that is what every ssh tool does, and
what anybody typing a destination expects, since the home directory is the one
place they are certain to be able to write.
What is still refused is unchanged and is the part that matters: the characters
a shell would act on. This value IS interpreted remotely, since `ssh host
mkdir -p <path>` runs through a shell, so they are rejected rather than escaped
— refusing is checkable by reading the function, escaping is checkable only by
being right about every shell.
The summary shows a relative destination as ~/… because that is where it lands.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
Anchored to the clock — midnight, six, noon, six — rather than to whenever the
schedule was saved, which is the whole reason these are cron expressions.
Verified the catch-up check reads its period as six hours, since that is what
decides whether a run was missed while the machine was down.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Restore goes. What it would read from was never decided, and a disabled button
with a tooltip explaining it is not built is worse than no button.
In its place, backups. `cron` was already a dependency and used nowhere, so the
schedule is a real cron expression — a backup wants to happen at an HOUR, and an
interval counted from whenever somebody pressed save drifts across the clock
until it runs mid-afternoon.
Cron alone is not enough, and this is the part worth writing down: it does not
run a job it was down for. A daily backup at three on a machine that restarts
overnight is skipped silently — the schedule is still there, the next fire is
still in the future, and a month later there is one file. So `lastBackupAt` is
in the database, and both the startup and a fifteen-minute tick compare it
against the schedule's own period, computed from two successive fires. A backup
that should already have happened happens now.
The job handle is module-level and stopped before a new one is made. Two saves
leaving two jobs is a bug that surfaces as backups arriving in pairs, weeks
later, with nothing connecting it to a settings form.
Back up now exists because a scheduled job nobody has ever run by hand is a
guess — it is how you learn the location is writable and pg_dump can reach the
databases without waiting until three to find out it cannot. The list underneath
is a readdir of the location rather than a record of what should be there: a
schedule that has been failing quietly looks exactly like a working one until
you count the files.
pg_dump per database, not pg_dumpall — a cluster dump would also carry Officer's
own, which is not this plugin's to copy around. And a failed run still records
the timestamp, or one undumpable database would have the catch-up tick retrying
every fifteen minutes for the rest of the week.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>