LibrePortal/scripts/backup/app/backup_app_start.sh
librelad d9f2feef05 feat(backup): consistent live database backups with auto strategy
Adds a logical-dump path so apps with a database can be backed up with zero
downtime and full consistency, instead of stopping the container.

- backup_db.sh: dump each declared DB live (mysqldump --single-transaction /
  pg_dump / sqlite3 .backup), exclude the raw data dir from the snapshot, and
  replay the dump on restore (pre-start rehydrate for sqlite, post-start load
  for server engines).
- Databases are declared via a 'libreportal.backup.db' compose label so the
  metadata travels with the app in the snapshot.
- New 'auto' strategy (now the default): live where a DB is dumpable or the app
  is marked live-safe, stop-snapshot-start otherwise. Explicit stop/pause/live
  remain as overrides.
- restic/borg/kopia adapters honour an exclude list on the live path.
- Manifest records the resolved per-app strategy and dumped databases.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-23 15:09:14 +01:00

142 lines
4.3 KiB
Bash
Executable File

#!/bin/bash
backupAppStart()
{
local app_name="$1"
local stored_app_name="$app_name"
if [[ -z "$app_name" ]]; then
isError "backupAppStart called with empty app_name"
return 1
fi
if [[ ! -d "$containers_dir$app_name" ]]; then
isError "Cannot back up '$app_name' — not installed at $containers_dir$app_name"
return 1
fi
if [[ -z "$(resticEnabledLocations)" ]]; then
isError "No backup locations enabled — configure at least one on the Locations page before running backups."
return 1
fi
engineEnsureAllLocationsReady
isHeader "Backing up $stored_app_name"
((menu_number++))
echo ""
echo "---- $menu_number. Running pre-backup hook (if present)"
echo ""
backupAppRunHook "$stored_app_name" pre
local strategy
strategy=$(backupResolveStrategy "$stored_app_name")
((menu_number++))
echo ""
echo "---- $menu_number. Quiescing container(s) for $stored_app_name (strategy: $strategy)"
echo ""
if [[ "$strategy" == "pause-snapshot-unpause" ]]; then
dockerComposePause "$stored_app_name" 2>/dev/null || dockerComposeDown "$stored_app_name"
elif [[ "$strategy" == "live" ]]; then
isNotice "Live strategy — containers stay running; databases dumped consistently"
backupDbDump "$stored_app_name"
else
dockerComposeDown "$stored_app_name"
fi
((menu_number++))
echo ""
echo "---- $menu_number. Writing backup manifest"
echo ""
local manifest_sha
manifest_sha=$(manifestWrite "$stored_app_name")
checkSuccess "Manifest written (sha: ${manifest_sha:0:8})"
((menu_number++))
echo ""
echo "---- $menu_number. Snapshotting to all enabled locations"
echo ""
# On the live path the raw DB data dirs are torn and superseded by the
# dumps written above — exclude them so the snapshot carries only the
# consistent copy. Other strategies quiesced the DB, so keep everything.
backup_exclude_paths=""
if [[ "$strategy" == "live" ]]; then
backup_exclude_paths=$(backupDbExcludePaths "$stored_app_name")
fi
local primary_snapshot_id=""
local primary_idx=""
local first_loc=true
local idx
while IFS= read -r idx; do
[[ -z "$idx" ]] && continue
local snap_id
snap_id=$(engineBackupApp "$idx" "$stored_app_name" "$manifest_sha")
if [[ "$first_loc" == true && -n "$snap_id" ]]; then
primary_snapshot_id="$snap_id"
primary_idx="$idx"
first_loc=false
fi
done < <(resticEnabledLocations)
((menu_number++))
echo ""
echo "---- $menu_number. Restarting container(s) for $stored_app_name"
echo ""
if [[ "$strategy" == "pause-snapshot-unpause" ]]; then
dockerComposeUnpause "$stored_app_name" 2>/dev/null || dockerComposeUp "$stored_app_name"
elif [[ "$strategy" != "live" ]]; then
dockerComposeUp "$stored_app_name"
fi
((menu_number++))
echo ""
echo "---- $menu_number. Running post-backup hook (if present)"
echo ""
backupAppRunHook "$stored_app_name" post
if [[ "$CFG_BACKUP_VERIFY_AFTER" == "true" && -n "$primary_snapshot_id" ]]; then
((menu_number++))
echo ""
echo "---- $menu_number. Verifying snapshot integrity"
echo ""
backupVerifySnapshot "$primary_idx" "$primary_snapshot_id" "$stored_app_name"
fi
((menu_number++))
echo ""
echo "---- $menu_number. Applying retention policy"
echo ""
engineForgetAppAllLocations "$stored_app_name"
((menu_number++))
echo ""
echo "---- $menu_number. Logging backup into database"
echo ""
databaseBackupInsert "$stored_app_name"
if [[ "$CFG_REQUIREMENT_WEBUI" == "true" ]]; then
((menu_number++))
echo ""
echo "---- $menu_number. Updating WebUI backup data"
echo ""
webuiGenerateBackupLocations
webuiGenerateBackupDashboard
webuiGenerateBackupSnapshots all
webuiGenerateBackupAppStatus "$stored_app_name"
fi
echo ""
echo "A backup of $stored_app_name has been taken on $current_date at $current_time" >> "$logs_dir$backup_log_file"
echo ""
menu_number=0
}
backupSchedule()
{
local app_name="$1"
backupAppStart "$app_name"
}