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:
1 parent
ff59bc7799
commit
e2518b03d8
6 files changed
+17
-8
No files matched your search
@@ -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),
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -100,7 +100,7 @@ A later rebuild without `--build-arg` goes back to the version the code names.
|
||||
|
||||

|
||||
|
||||
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;
|
||||
|
||||
@@ -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
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user