#!/bin/bash # Preflight for a first-run restore: check every app in a backup against THIS # machine, before anything is written. # # The whole thing hinges on one fact — each app's snapshot carries its own # .libreportal-manifest.json, and engineDumpFile pulls a single file out of a # snapshot without restoring it. So we can read what an app needs (its size, # where it used to live, which images it runs) and compare that to the machine # in front of us, while it is still free to say no. # # What it catches, and why each matters more than a failed restore would: # # template missing restoring an app this version no longer ships produces a # directory that can never start, and looks like a # successful restore until the user tries to open it # won't fit filling the disk mid-restore takes the OTHER apps with # it, so this must be decided per app, up front # location gone the manifest names a storage location this machine does # not have; falling back is right, but silently is not # # Emits one record per app on stdout: # # \t\t\t # # verdict is ok | move | skip. Returns non-zero only if it could not read the # repository at all — a skipped app is a finding, not a failure. # Human-readable bytes, for a report a person reads rather than parses. _restorePfSize() { local b="${1:-0}" [[ "$b" =~ ^[0-9]+$ ]] || { printf '?'; return; } if (( b >= 1099511627776 )); then printf '%sT' "$(( b / 1099511627776 ))" elif (( b >= 1073741824 )); then printf '%sG' "$(( b / 1073741824 ))" elif (( b >= 1048576 )); then printf '%sM' "$(( b / 1048576 ))" else printf '%sK' "$(( b / 1024 ))"; fi } # Pull one app's manifest out of its newest snapshot. Empty on failure — an # older backup may predate manifests, and that is not a reason to refuse. restorePreflightManifest() { local idx="$1" app="$2" host="$3" local snap; snap=$(engineSnapshotLatestId "$idx" "$app" "$host" 2>/dev/null) [[ -n "$snap" ]] || return 1 # The manifest sits at the app dir's root, but that path is the SOURCE # machine's — which is exactly what we cannot assume. Ask the snapshot where # its own files are rather than guessing. local base base=$(storageSnapshotSourcePath "$idx" "$snap" "$app" 2>/dev/null) [[ -n "$base" ]] || return 1 engineDumpFile "$idx" "$snap" "$base/.libreportal-manifest.json" 2>/dev/null } # Check one app. Echoes a single verdict record. restorePreflightApp() { local idx="$1" app="$2" host="$3" local manifest size_bytes size_h loc want_dir avail_kb need_kb # Squeezed: the manifest is pretty-printed, so `"size_bytes": 123` carries a # space that a `"key":[0-9]*` pattern misses — which silently emptied both # the size and the location, turning the fit and location checks into # no-ops that always passed. manifest=$(restorePreflightManifest "$idx" "$app" "$host" 2>/dev/null | tr -d ' \n\t') size_bytes=$(printf '%s' "$manifest" | grep -o '"size_bytes":[0-9]*' | head -1 | cut -d: -f2) loc=$(printf '%s' "$manifest" | grep -o '"location":"[^"]*"' | head -1 | cut -d'"' -f4) size_h=$(_restorePfSize "${size_bytes:-0}") [[ -z "$size_bytes" ]] && size_h="?" # 1. Does this version still ship the app? if [[ ! -f "${install_containers_dir%/}/$app/$app.config" ]]; then printf 'skip\t%s\t%s\t%s\n' "$app" "$size_h" "this version no longer ships it" return 0 fi # 2. Where would it go, and does that location still exist? local detail="" verdict="ok" if [[ -n "$loc" && "$loc" != "default" && "$loc" != "primary" ]]; then if ! storageLocationPath "$loc" >/dev/null 2>&1; then verdict="move" detail="its old location \\"$loc\\" is not on this machine" fi fi want_dir=$(appDir "$app" 2>/dev/null) || { printf 'skip\t%s\t%s\t%s\n' "$app" "$size_h" "its storage location is not mounted" return 0 } # 3. Will it fit? Checked per app: filling the disk part-way through a # restore damages the apps that already landed. if [[ -n "$size_bytes" && "$size_bytes" =~ ^[0-9]+$ ]]; then need_kb=$(( size_bytes / 1024 )) avail_kb=$(df -Pk "${want_dir%/*}" 2>/dev/null | awk 'NR==2 {print $4}') if [[ -n "$avail_kb" ]] && (( avail_kb < need_kb )); then printf 'skip\t%s\t%s\t%s\n' "$app" "$size_h" \ "needs $size_h, $(( avail_kb / 1048576 ))G free where it would go" return 0 fi fi if [[ "$verdict" == "move" ]]; then printf 'move\t%s\t%s\t-> %s (%s)\n' "$app" "$size_h" "$(storageLocationName "${want_dir%/*}")" "$detail" else printf 'ok\t%s\t%s\t%s\n' "$app" "$size_h" "restores as-is" fi return 0 } # Check a whole list. Prints the report and leaves the apps worth restoring in # RESTORE_PREFLIGHT_OK. restorePreflightReport() { local idx="$1" host="$2"; shift 2 # Callers may pass an explicit list, but the CLI wrapper pads its argv to # nine slots and fills the unset ones with the literal string "empty" # (${5:-empty} … ${9:-empty} in /usr/local/lib/libreportal/libreportal). # So a trailing slot arrives as a five-character app name, not a blank — # which is why filtering on -n alone let five phantom apps through and # reported them as "no longer shipped". # # That also caps any explicit list at five apps, so when nothing real is # left we discover the host's apps from the repository instead: a whole-host # restore then has no list to truncate. local -a apps=() local _a for _a in "$@"; do [[ -z "$_a" || "$_a" == "empty" ]] && continue apps+=("$_a") done if (( ${#apps[@]} == 0 )); then while IFS= read -r _a; do [[ -n "$_a" ]] && apps+=("$_a"); done \ < <(migrateDiscoverApps "$host" "$idx" 2>/dev/null) fi RESTORE_PREFLIGHT_OK=() RESTORE_PREFLIGHT_SKIPPED=0 if (( ${#apps[@]} == 0 )); then isNotice "No apps found for '$host' in this repository." return 0 fi isNotice "Checking ${#apps[@]} apps against this machine…" echo "" local rec verdict app size detail for app in "${apps[@]}"; do rec=$(restorePreflightApp "$idx" "$app" "$host") IFS=$'\t' read -r verdict app size detail <<< "$rec" case "$verdict" in skip) printf ' \033[0;31m✗\033[0m %-16s %-6s %s\n' "$app" "$size" "skipped — $detail" RESTORE_PREFLIGHT_SKIPPED=$(( RESTORE_PREFLIGHT_SKIPPED + 1 )) ;; move) printf ' \033[0;33m~\033[0m %-16s %-6s %s\n' "$app" "$size" "$detail" RESTORE_PREFLIGHT_OK+=("$app") ;; *) printf ' \033[0;32m✓\033[0m %-16s %-6s %s\n' "$app" "$size" "$detail" RESTORE_PREFLIGHT_OK+=("$app") ;; esac done echo "" if (( RESTORE_PREFLIGHT_SKIPPED > 0 )); then isNotice "${#RESTORE_PREFLIGHT_OK[@]} will restore, $RESTORE_PREFLIGHT_SKIPPED skipped." else isNotice "All ${#RESTORE_PREFLIGHT_OK[@]} will restore." fi return 0 }