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 () => {
+1 -1
View File
@@ -100,7 +100,7 @@ A later rebuild without `--build-arg` goes back to the version the code names.
![Settings → Backups](img/settings-backups.png)
A backup (`data/backups/selfphoneng-YYYYMMDD-HHMM.tar.gz`) holds:
A backup (`data/backups/selfphoneng-YYYYMMDD-HHMMSS.tar.gz`) holds:
- the database: users, extensions, numbers, settings, call history;
- voicemail;
+1 -1
View File
@@ -477,7 +477,7 @@ whole minutes). Each job has a running flag; an error is logged, not fatal.
backup emails them too (08 §3.2). Email not set up: logged, nothing else.
### 8.1 Backups
Folder `/var/lib/selfphoneng/backups/selfphoneng-YYYYMMDD-HHMM.tar.gz` containing:
Folder `/var/lib/selfphoneng/backups/selfphoneng-YYYYMMDD-HHMMSS.tar.gz` containing:
`db.sql` (`pg_dump --no-owner`), `voicemail/`, `sounds-custom/`, `moh/`,
`manifest.json`. Do **not** include `asterisk-generated/`: it holds plaintext SIP passwords and is rebuilt from the DB on start.
`manifest.json` is `{version, createdAt, includesRecordings}`. The backup file mode is 0600. Recordings are optional
+1 -1
View File
@@ -6,7 +6,7 @@
# .env must hold the APP_SECRET the backup was made with (SIP and trunk passwords are encrypted
# with it): the script checks, and stops if it doesn't match.
#
# Usage: scripts/restore.sh <selfphoneng-YYYYMMDD-HHMM.tar.gz> (run from anywhere)
# Usage: scripts/restore.sh <selfphoneng-YYYYMMDD-HHMMSS.tar.gz> (run from anywhere)
set -eu
[ $# -eq 1 ] || { sed -n '9s/^# \{0,1\}//p' "$0" >&2; exit 2; }
file=$(cd "$(dirname "$1")" && pwd)/$(basename "$1")
+2
View File
@@ -153,6 +153,8 @@ say "The check failed: going back to v$current"
# The new api's own backup, made just before it migrated, beats the one above.
own=$(dc logs --no-log-prefix --since "$started" api 2>&1 \
| sed -n 's/.*"backup":"\(selfphoneng-[0-9-]*\.tar\.gz\)".*backup before migrating made.*/\1/p' | tail -1)
echo "The api's last log lines:"
dc logs --no-log-prefix --tail 15 api 2>&1 || true
git -c advice.detachedHead=false checkout -q "$prev"
dc build
if [ "$(mig)" != "$mig_before" ]; then