backups: seconds in the name, so two in one minute don't overwrite; update.sh prints the api's log on a failed check

This commit is contained in:
iamdoubz committed 2026-10-21 11:34:42 -05:00
1 parent ff59bc7799
commit e2518b03d8
6 files changed
+17 -8

No files matched your search

+8 -4
View File
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Backups (04 §8.1): BACKUPS_DIR/selfphoneng-YYYYMMDD-HHMM.tar.gz with db.sql (pg_dump), the
// Backups (04 §8.1): BACKUPS_DIR/selfphoneng-YYYYMMDD-HHMMSS.tar.gz with db.sql (pg_dump), the
// voicemail, custom sounds and music-on-hold folders, optionally recordings, and manifest.json.
// No generated Asterisk config (plaintext SIP passwords; it's rebuilt from the DB on start).
// Restore is scripts/restore.sh only.
@@ -17,7 +17,9 @@ import { getSetting } from './settings.ts';
const run = promisify(execFile);
/** manifest.json `version`: the backup format, which restore.sh checks. */
export const BACKUP_FORMAT = 1;
export const BACKUP_NAME = /^selfphoneng-\d{8}-\d{4}\.tar\.gz$/;
/** With seconds since 2026-10-21, so two backups in one minute (the update's and the api's own
* before migrating) don't share a name; older names have none. */
export const BACKUP_NAME = /^selfphoneng-\d{8}-\d{4}(\d{2})?\.tar\.gz$/;
/**
* Lets restore.sh tell whether .env has the APP_SECRET the backup's encrypted columns need,
@@ -70,7 +72,9 @@ export async function createBackup(
src: BackupSources,
opts: { keep: number; now?: Date },
): Promise<BackupDto> {
const name = `selfphoneng-${stamp(src.timezone, opts.now)}.tar.gz`;
const now = opts.now ?? new Date();
const seconds = String(now.getSeconds()).padStart(2, '0');
const name = `selfphoneng-${stamp(src.timezone, now)}${seconds}.tar.gz`;
const work = await mkdtemp(join(tmpdir(), 'sp-backup-'));
const tmp = join(src.outDir, `.${name}.tmp`);
try {
@@ -99,7 +103,7 @@ export async function createBackup(
}
const manifest = {
version: BACKUP_FORMAT,
createdAt: (opts.now ?? new Date()).toISOString(),
createdAt: now.toISOString(),
includesRecordings: included.includes('recordings'),
folders: included,
secretCheck: secretCheck(src.appSecret),
+4 -1
View File
@@ -50,14 +50,17 @@ describe.skipIf(!hasTestDb)('backup routes', () => {
it('lists backups newest first, ignoring other files', async () => {
writeFileSync(join(dir(), 'selfphoneng-20261018-0330.tar.gz'), 'old');
writeFileSync(join(dir(), 'selfphoneng-20261019-0330.tar.gz'), 'newer');
// with seconds (from 2026-10-21): a second one in the same minute sorts after it, newer
writeFileSync(join(dir(), 'selfphoneng-20261019-033045.tar.gz'), 'newest');
writeFileSync(join(dir(), '.selfphoneng-20261020-0330.tar.gz.tmp'), 'half-written');
writeFileSync(join(dir(), 'notes.txt'), 'x');
const r = await req('GET', '/api/system/backups');
expect(r.json().map((b: { name: string }) => b.name)).toEqual([
'selfphoneng-20261019-033045.tar.gz',
'selfphoneng-20261019-0330.tar.gz',
'selfphoneng-20261018-0330.tar.gz',
]);
expect(r.json()[0].size).toBe(5);
expect(r.json()[1].size).toBe(5);
});
it('downloads one by name, nothing else', async () => {