#!/bin/bash restoreFirstRunDiscover() { local idx="$1" if ! resticLocationEnabled "$idx"; then isError "Location $idx is not enabled" return 1 fi resticEnvExport "$idx" || return 1 # Via runBackupOp rather than its own sudo: this was the one backup-engine # call bypassing that funnel, so it silently missed the -E fix for sudo-rs # (and the -H that puts restic's cache under the backup user's HOME). runBackupOp restic snapshots --tag engine=libreportal --json --no-lock 2>/dev/null local rc=$? resticEnvUnset return $rc } # Restore a host's apps onto this machine. # # With no app list this is a WHOLE-HOST restore: the apps are discovered from # the repository and filtered through the preflight. Both halves matter. # # Discovery, because an explicit list has to survive the CLI wrapper's fixed # positional slots to get here — a 13-app restore arrived as four, restored # those, and reported success. The wrapper now forwards the real argv, but a # whole-host restore that never builds a list cannot be truncated at all. # # The preflight, because the installer prints its report in a separate process, # so the decision it made there is gone by the time this runs. Without # re-applying it, an app the user was told would be skipped — one this version # no longer ships, or one too big for the disk — gets restored anyway. restoreFirstRunBulk() { local idx="$1" local source_host="$2" shift 2 local apps_to_restore=("$@") local -i preflighted=0 if [[ ${#apps_to_restore[@]} -eq 0 ]]; then restorePreflightReport "$idx" "$source_host" >/dev/null 2>&1 apps_to_restore=("${RESTORE_PREFLIGHT_OK[@]}") preflighted=1 fi if [[ ${#apps_to_restore[@]} -eq 0 ]]; then isError "No apps to restore for '$source_host' in this repository" return 1 fi isHeader "First-run bulk restore from $(resticLocationName "$idx") (host=$source_host)" (( preflighted )) && isNotice "Restoring ${#apps_to_restore[@]} apps the preflight approved." # Count what actually landed. A per-app failure must not be reported as a # complete restore — that is how "4 apps restored" read as success when # nine had gone missing. # # An app can also come back only half-running: continue-on-error (the # default) lets a failed compose-up log and carry on, so restoreAppStart # still returns 0. That is how a restore reported thirteen successes while # stoat's livekit had lost a port race and four containers that depended on # it exited 101. checkSuccess appends every such failure to error_report.log, # so watch that file grow across each app and name the noisy ones. local _errlog="${logs_dir%/}/error_report.log" _restoreErrLines() { wc -l < "$_errlog" 2>/dev/null || echo 0; } local app local -i ok=0 bad=0 before=0 after=0 local -a failed=() noisy=() for app in "${apps_to_restore[@]}"; do before=$(_restoreErrLines) if restoreAppStart "$app" "latest" "$idx" "$source_host"; then ok=$(( ok + 1 )) after=$(_restoreErrLines) (( after > before )) && noisy+=("$app") else bad=$(( bad + 1 )); failed+=("$app") fi done unset -f _restoreErrLines if (( bad > 0 )); then isError "First-run restore finished with failures — $ok of ${#apps_to_restore[@]} restored" isNotice "Failed: ${failed[*]}" (( ${#noisy[@]} )) && isNotice "Restored but reported errors: ${noisy[*]}" return 1 fi if (( ${#noisy[@]} )); then isSuccessful "First-run restore complete — $ok apps restored" isNotice "${#noisy[@]} reported errors while starting: ${noisy[*]}" isNotice "They are restored, but check them: $_errlog" return 0 fi isSuccessful "First-run restore complete — $ok apps restored" return 0 } # The WebUI's rebuild: adopt the settings, reconcile the domains, restore the # apps. Same order the installer uses and for the same reason — the system # config carries every other backup location's credentials, so one password the # user remembers unlocks the rest, and only then are apps worth restoring. # # restoreWebuiRebuild [drop-domains] # # Called from a task, so its output is the progress the user watches. restoreWebuiRebuild() { local idx="${1:-}" host="${2:-}" drop="${3:-no}" if [[ -z "$idx" ]]; then isError "restoreWebuiRebuild requires a backup location" return 1 fi isHeader "Rebuilding from backup" # --- settings first ------------------------------------------------------ isNotice "Restoring settings and credentials…" if backupRestoreSystemConfig "$idx" >/dev/null 2>&1; then # --force: the WebUI is only reachable at all because this machine has # a working install on it, so restoreAdoptIsFirstRun will say no. The # user asked for this explicitly on the Rebuild step, which is the # confirmation the guard exists to require. if restoreSystemAdopt "" --force; then isSuccessful "Settings and backup repositories restored" else isNotice "Settings were staged but could not be adopted — apps will still be restored." fi else isNotice "No system config in this backup — apps will still be restored." fi # --- domains ------------------------------------------------------------- restoreDomainReport || true if [[ "$drop" == "yes" || "$drop" == "true" ]]; then restoreDomainsDropElsewhere || true fi # --- apps ---------------------------------------------------------------- # No app list, deliberately: bulk discovers the host's apps and re-applies # the preflight itself. Passing a list here is what let a 13-app restore # arrive as four and still report success. isNotice "Restoring apps — this takes a while." restoreFirstRunBulk "$idx" "$host" isSuccessful "Rebuild complete" return 0 }