`restore preflight <loc_idx> <host>` reads every app's manifest out of its
own snapshot (engineDumpFile pulls a single file without restoring) and
checks it against this machine before anything is written:
* an app this version no longer ships is skipped — restoring one
produces a directory that can never start, and looks like success
until someone opens it
* an app that will not fit is skipped individually, because filling the
disk part-way through takes the apps that already landed with it
* a manifest naming a storage location this machine lacks falls back to
the default, and says which app moved where
The installer's restore path runs it and asks once before continuing.
Two bugs found by running it against the live repository rather than
reading it:
The CLI dispatcher calls handlers with NO arguments, so `shift 4; "$@"`
inside one operates on an empty list. `restore first-run bulk` has always
had this — a bulk restore silently received zero apps. Fixed at the entry
point: start.sh now captures LP_CLI_ARGS from "$@", and both call sites
use it.
And the wrapper fills unset argv slots with the literal string "empty"
(${5:-empty} … ${9:-empty}), so a trailing slot arrives as a five-
character app name rather than a blank. Filtering on -n alone let five
phantom apps through and reported each as "no longer shipped". Both call
sites now drop the sentinel. That also caps any explicit list at five, so
preflight discovers the host's apps itself when given none.
Verified against the live repository: 13 apps discovered and checked.
Sizes read "?" there because those snapshots predate manifests carrying
size_bytes — the intended graceful fallback, not a refusal.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
173 lines
6.9 KiB
Bash
173 lines
6.9 KiB
Bash
#!/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:
|
|
#
|
|
# <verdict>\t<app>\t<size_human>\t<detail>
|
|
#
|
|
# 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
|
|
|
|
manifest=$(restorePreflightManifest "$idx" "$app" "$host" 2>/dev/null)
|
|
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
|
|
}
|