back up on publish, which is when this site actually changes
A nightly backup of a site that only changes when somebody publishes spends most of its life copying a database identical to yesterday's, and misses the one moment worth capturing. So "On publish" joins the schedules, and goes first. Stored in the same column as a cron expression because it answers the same question — WHEN — and a second column would mean every read had to decide which of the two was authoritative. It creates no cron job and no catch-up tick. A missed run is a run whose TIME passed while the machine was down; an event has no time, so there is nothing to have missed, and a catch-up here would take a backup every fifteen minutes of a site nobody had touched. `backupAfterPublish()` does nothing unless this is the configured trigger, so the publish path can call it unconditionally and never has to know what the backup settings say. It swallows failures for the same reason the timer does: a backup that could not be taken must not present as a publish that failed, when the thing the person asked for worked. Nothing calls it yet — there is no publish in this plugin, because the sections that would do the publishing are not built. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+39
-2
@@ -35,6 +35,18 @@ import { readDatabases } from './databases';
|
||||
* also why no credential is stored here — there is none to store, and none to leak.
|
||||
*/
|
||||
|
||||
/**
|
||||
* "Back up when something is published", rather than at a time.
|
||||
*
|
||||
* Stored in the same column as a cron expression because it answers the same question — WHEN — and a
|
||||
* second column would mean every read had to decide which of the two was authoritative.
|
||||
*
|
||||
* It is the right answer for this site and probably for most: nothing changes until somebody publishes,
|
||||
* so a nightly backup spends most of its life copying a database that is identical to yesterday's, and
|
||||
* the one moment worth capturing is the one it does not cover.
|
||||
*/
|
||||
export const ON_PUBLISH = 'on-publish';
|
||||
|
||||
/** How often to look for a run that was missed while this machine was down. */
|
||||
const CATCHUP_TICK_MS = 15 * 60 * 1000;
|
||||
|
||||
@@ -85,6 +97,7 @@ function periodMs(expression: string): number | null {
|
||||
export function checkCron(expression: string): string {
|
||||
const trimmed = expression.trim();
|
||||
if (!trimmed) throw new Error('A schedule is required.');
|
||||
if (trimmed === ON_PUBLISH) return trimmed;
|
||||
if (!validateCronExpression(trimmed).valid) throw new Error(`"${trimmed}" is not a schedule cron understands.`);
|
||||
return trimmed;
|
||||
}
|
||||
@@ -153,7 +166,8 @@ export async function readBackups(): Promise<BackupSettings> {
|
||||
const dir = row?.backupDir ?? defaultBackupDir();
|
||||
|
||||
let nextRunAt: string | null = null;
|
||||
if (cron) {
|
||||
// Nothing to predict when the trigger is an event: the next run is whenever somebody publishes.
|
||||
if (cron && cron !== ON_PUBLISH) {
|
||||
try {
|
||||
nextRunAt = new CronTime(cron).getNextDateFrom(new Date()).toISO();
|
||||
} catch {
|
||||
@@ -426,7 +440,7 @@ let catchup: ReturnType<typeof setInterval> | null = null;
|
||||
/** Is a scheduled run already overdue, because the machine was off when it should have happened? */
|
||||
async function runIfMissed(): Promise<void> {
|
||||
const row = await readRow();
|
||||
if (!row?.backupCron) return;
|
||||
if (!row?.backupCron || row.backupCron === ON_PUBLISH) return;
|
||||
|
||||
const period = periodMs(row.backupCron);
|
||||
if (!period) return;
|
||||
@@ -454,6 +468,15 @@ export async function schedule(): Promise<void> {
|
||||
const row = await readRow();
|
||||
if (!row?.backupCron) return;
|
||||
|
||||
/*
|
||||
Nothing to schedule, and nothing to catch up on.
|
||||
|
||||
A missed run is a run whose TIME passed while the machine was down. An event has no time, so there
|
||||
is nothing to have missed — and a catch-up tick here would take a backup every fifteen minutes of a
|
||||
site nobody had touched.
|
||||
*/
|
||||
if (row.backupCron === ON_PUBLISH) return;
|
||||
|
||||
try {
|
||||
job = new CronJob(row.backupCron, () => void runBackup().catch(() => {}), null, true);
|
||||
} catch {
|
||||
@@ -466,3 +489,17 @@ export async function schedule(): Promise<void> {
|
||||
// And once now, because the restart after a missed run is the most likely moment to discover it.
|
||||
void runIfMissed();
|
||||
}
|
||||
|
||||
/**
|
||||
* Take a backup because something was just published.
|
||||
*
|
||||
* Does nothing unless that is the configured trigger, so the publish path can call it unconditionally
|
||||
* and never has to know what the backup settings say. Failures are swallowed for the same reason the
|
||||
* timer swallows them: a backup that could not be taken must not turn into a publish that appears to
|
||||
* have failed, when the thing the person asked for actually worked.
|
||||
*/
|
||||
export async function backupAfterPublish(): Promise<void> {
|
||||
const row = await readRow();
|
||||
if (row?.backupCron !== ON_PUBLISH) return;
|
||||
await runBackup().catch(() => {});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user