From 038d1c0729e25ffab6b644eb72dc1f8ab1d14946 Mon Sep 17 00:00:00 2001 From: librelad Date: Tue, 26 May 2026 00:48:18 +0100 Subject: [PATCH] fix(backup): system config in scheduled backups + retention (review findings) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Final-review gaps in the system-config backup: 1. Scheduled (cron) backups skipped it — backupScheduleEnabledApps only queued per-app backups, so the daily schedule never refreshed the system config (and thus the backup-location creds could go stale). Now it queues a `libreportal backup system` task (or runs inline on terminal-only installs), and skips the reproducible libreportal app for consistency with backupAllApps. 2. No retention on system snapshots — they bypass backupAppStart's per-app forget, so they accumulated unbounded. Add resticForgetSystem (tag system=config, respects append-only + the same keep-* policy) + engineForgetSystem dispatcher; backupSystemConfig now applies retention across all locations after snapshotting. Verified with stubs: backupSystemConfig snapshots AND prunes on every location; engineForgetSystem pairs with resticForgetSystem; scheduled createTaskFile call matches the existing 3-arg signature. Co-Authored-By: Claude Opus 4.7 Signed-off-by: librelad --- scripts/backup/app/backup_schedule_all.sh | 17 +++++++++++- scripts/backup/engine/engine_dispatch.sh | 1 + scripts/backup/engine/restic_forget.sh | 33 +++++++++++++++++++++++ scripts/backup/system/backup_system.sh | 8 ++++++ 4 files changed, 58 insertions(+), 1 deletion(-) diff --git a/scripts/backup/app/backup_schedule_all.sh b/scripts/backup/app/backup_schedule_all.sh index 98e051e..4f775b6 100644 --- a/scripts/backup/app/backup_schedule_all.sh +++ b/scripts/backup/app/backup_schedule_all.sh @@ -21,6 +21,9 @@ backupScheduleEnabledApps() local queued=0 for name in "${app_names[@]}"; do + # libreportal (the WebUI) is reproducible — its state rides in the system + # config snapshot, so it's never scheduled as an app backup. + [[ "$name" == "libreportal" ]] && continue local backup_flag="CFG_${name^^}_BACKUP" if [[ "${!backup_flag}" == "true" ]]; then backupAppSchedule "$name" @@ -28,5 +31,17 @@ backupScheduleEnabledApps() fi done - isSuccessful "Backup scheduling complete — $queued app(s) queued" + # Keep the system config fresh on the schedule too (settings + the + # backup-location creds). Queue it via the task processor on WebUI installs, + # else run inline — mirroring backupAppSchedule. backupSystemConfig no-ops + # when no locations are enabled. + if [[ "$CFG_REQUIREMENT_WEBUI" == "true" ]] && declare -f createTaskFile >/dev/null 2>&1; then + createTaskFile "libreportal backup system" "backup" "system" \ + && isSuccessful "System config backup task queued" \ + || backupSystemConfig + else + backupSystemConfig + fi + + isSuccessful "Backup scheduling complete — $queued app(s) queued + system config" } diff --git a/scripts/backup/engine/engine_dispatch.sh b/scripts/backup/engine/engine_dispatch.sh index 81a52d6..bc13839 100644 --- a/scripts/backup/engine/engine_dispatch.sh +++ b/scripts/backup/engine/engine_dispatch.sh @@ -56,6 +56,7 @@ engineSnapshotsJson() { local i="$1"; shift; engineDispatch "$(engineForL engineSystemSnapshotsJson() { local i="$1"; shift; engineDispatch "$(engineForLocation "$i")SystemSnapshotsJson" "$i" "$@"; } engineSnapshotListFiles() { local i="$1"; shift; engineDispatch "$(engineForLocation "$i")SnapshotListFiles" "$i" "$@"; } engineForgetApp() { local i="$1"; shift; engineDispatch "$(engineForLocation "$i")ForgetApp" "$i" "$@"; } +engineForgetSystem() { local i="$1"; shift; engineDispatch "$(engineForLocation "$i")ForgetSystem" "$i" "$@"; } engineCheckLocation() { local i="$1"; shift; engineDispatch "$(engineForLocation "$i")CheckLocation" "$i" "$@"; } engineDumpFile() { local i="$1"; shift; engineDispatch "$(engineForLocation "$i")DumpFile" "$i" "$@"; } diff --git a/scripts/backup/engine/restic_forget.sh b/scripts/backup/engine/restic_forget.sh index 3114007..89c4905 100644 --- a/scripts/backup/engine/restic_forget.sh +++ b/scripts/backup/engine/restic_forget.sh @@ -34,6 +34,39 @@ resticForgetApp() return $rc } +resticForgetSystem() +{ + local idx="$1" + + if resticLocationAppendOnly "$idx"; then + isNotice "$(resticLocationName "$idx") is append-only — skipping forget for system config" + return 0 + fi + + local keep_last keep_daily keep_weekly keep_monthly keep_yearly + keep_last=$(resticRetentionFor "$idx" KEEP_LAST) + keep_daily=$(resticRetentionFor "$idx" KEEP_DAILY) + keep_weekly=$(resticRetentionFor "$idx" KEEP_WEEKLY) + keep_monthly=$(resticRetentionFor "$idx" KEEP_MONTHLY) + keep_yearly=$(resticRetentionFor "$idx" KEEP_YEARLY) + + resticEnvExport "$idx" || return 1 + + local args=(forget --tag "system=config" --group-by tags,host) + [[ -n "$keep_last" ]] && args+=(--keep-last "$keep_last") + [[ -n "$keep_daily" ]] && args+=(--keep-daily "$keep_daily") + [[ -n "$keep_weekly" ]] && args+=(--keep-weekly "$keep_weekly") + [[ -n "$keep_monthly" ]] && args+=(--keep-monthly "$keep_monthly") + [[ -n "$keep_yearly" ]] && args+=(--keep-yearly "$keep_yearly") + [[ "$CFG_BACKUP_PRUNE_AFTER_FORGET" == "true" ]] && args+=(--prune) + + isNotice "Applying retention for system config on $(resticLocationName "$idx")" + runBackupOp restic "${args[@]}" + local rc=$? + resticEnvUnset + return $rc +} + resticRetentionFor() { local idx="$1" diff --git a/scripts/backup/system/backup_system.sh b/scripts/backup/system/backup_system.sh index b60880d..a4dd271 100644 --- a/scripts/backup/system/backup_system.sh +++ b/scripts/backup/system/backup_system.sh @@ -40,6 +40,14 @@ backupSystemConfig() isError "System config backup failed on all locations" return 1 fi + + # Apply retention so system snapshots don't accumulate (respects append-only + # locations; bypasses backupAppStart's per-app forget, so do it here). + while IFS= read -r idx; do + [[ -z "$idx" ]] && continue + engineForgetSystem "$idx" >/dev/null 2>&1 || true + done < <(resticEnabledLocations) + if [[ $fail -gt 0 ]]; then isNotice "System config backed up to $ok location(s), failed on $fail" else