LibrePortal/scripts/backup/app/backup_app_start.sh
librelad 8b5e02c760 refactor(storage): resolve every app directory through appDir
The main sweep — ~260 call sites across ~100 files move from string
concatenation on a single root to appDir/storageAppDirs/storageAppConfigs.
On a single-root install the resolved paths are identical, so this is a
no-op until a location is registered.

Enumerators were the interesting half. `for d in "$containers_dir"/*/`
appears in the menus, the registry/artifact scanners and the DNS setup —
and a shell glob cannot list a rootless 751 tree at all, which is the
same bug config_find_file.sh already documents in a comment. Routing them
through storageAppDirs (which enumerates as the owning user) fixes that
alongside the multi-root work.

Three places needed judgement rather than substitution:

db_app_scan.sh deletes database rows and port allocations for apps whose
folder is missing, and reaps "empty" app dirs. With a storage location
unmounted, every app on it looks exactly like that. Each of those
branches now gates on appStorageAvailable first — an app on an unplugged
drive is skipped with a notice, never deleted.

instance_create.sh rewrites cloned hooks so an instance touches its own
directory instead of the base app's. Its sed matched ${containers_dir}<type>,
which this sweep just replaced with $(appDir <type>) — so it would have
silently stopped redirecting, and an instance would have written to the
original's files (the adguard auth adapter case its own comment warns
about). Now matches both appDir forms, verified against bare, quoted,
unrelated-app, legacy and prose cases.

peer_shell/peer_pull streamed and extracted relative to the primary root.
Both now use the app's own root, and peer_shell keeps a single-root
fallback since it runs as a restricted SSH shell with no LibrePortal env.

Also fixes a pre-existing bug found on the way: webui_app_config.sh
tested "$containers_dir/frontend/data/last_update", one level short of the
real tree under the libreportal app dir, so the WebUI refresh trigger
after a config update has never once fired.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 04:09:51 +01:00

174 lines
6.0 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 "$(appDir "$app_name")" ]]; then
isError "Cannot back up '$app_name' — not installed at $(appDir "$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 + private files captured via their containers"
if ! backupDbDump "$stored_app_name" || ! backupFilesCapture "$stored_app_name"; then
isError "Live capture failed — falling back to stop-snapshot-start for safety"
runFileOp rm -rf "$(appDir "$stored_app_name")/.lp-backup"
strategy="stop-snapshot-start"
dockerComposeDown "$stored_app_name"
fi
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 (superseded by the dumps) and the
# private file trees (superseded by the container-side captures) are excluded
# so the snapshot carries only the consistent copies. Other strategies
# quiesced the app, so keep everything.
backup_exclude_paths=""
if [[ "$strategy" == "live" ]]; then
backup_exclude_paths=$(printf '%s\n%s\n' \
"$(backupDbExcludePaths "$stored_app_name")" \
"$(backupFilesExcludePaths "$stored_app_name")")
fi
local primary_snapshot_id=""
local primary_idx=""
local first_loc=true
local snap_ok=0 snap_failed=0
local idx
while IFS= read -r idx; do
[[ -z "$idx" ]] && continue
local snap_id
snap_id=$(engineBackupApp "$idx" "$stored_app_name" "$manifest_sha")
if [[ -n "$snap_id" ]]; then
snap_ok=$((snap_ok + 1))
if [[ "$first_loc" == true ]]; then
primary_snapshot_id="$snap_id"
primary_idx="$idx"
first_loc=false
fi
else
snap_failed=$((snap_failed + 1))
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
# Containers are back up; now be honest about the snapshot result. With no
# good snapshot on any location there is nothing to verify, retain, or
# record — returning non-zero here is what marks the task failed instead of
# logging a backup that doesn't exist (or is missing files) as a success.
if [[ $snap_ok -eq 0 ]]; then
isError "Backup of $stored_app_name FAILED — no complete snapshot was created on any location (see errors above)"
menu_number=0
return 1
fi
if [[ $snap_failed -gt 0 ]]; then
isNotice "Backup of $stored_app_name succeeded on $snap_ok location(s) but failed on $snap_failed — check the errors above"
fi
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"
# Signal the throttled routine refresh (webuiLibrePortalUpdate) that a
# new snapshot exists, so its next pass does a full remote pull to
# reconcile the bits this targeted regen skips (migrate + other apps'
# status) even while the refresh window is otherwise closed.
touch "/tmp/libreportal_webui_backup_dirty" 2>/dev/null || true
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"
}