#!/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 + 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 "${containers_dir:?}$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" }