LibrePortal/scripts/backup/manifest/manifest_collect.sh
librelad 8fad6c6a4d fix(restore): take the source path from the snapshot, not local config
Restore built its restic include filter from THIS host's containers root:

    engineRestoreSnapshot "$idx" "$id" "/" "$containers_dir$app"

restic reproduces a snapshot's absolute paths, so that only works when
both sides agree byte-for-byte. LibrePortal has shipped configurable
roots for a while, so restoring a snapshot taken on a host installed with
--containers-dir=/mnt/ssd/apps onto a default host matched no include
path and restored NOTHING — with no error, because an include filter that
matches nothing is not a failure. Storage locations turn that from a rare
cross-host case into an ordinary one.

storageSnapshotSourcePath asks the repository where the app actually
lived. storageRestoreAppTo restores in place when that agrees with where
the app belongs here, and stages-then-moves when it does not — which is
also what makes "restore this app onto a different disk" possible at all.
Both restore_app_start.sh and resticRestoreAppLatest go through it, and
both fall back to the old behaviour when a snapshot does not report its
paths, so older snapshots restore exactly as before.

The move into place runs as root (app-adopt) for the same reason app-move
does: a restored tree carries container sub-UIDs the manager cannot
recreate. Staging is constrained to the restore/migrate area and the
destination is validated against the root-owned registry, so neither end
is taken on trust from the caller.

The manifest now records where an app lived — location name, path and fs
uuid. The name is what travels, since a path means nothing on the other
host; the rest is for diagnostics and for answering "is this the same
disk?" during a migrate.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 20:39:35 +01:00

95 lines
3.5 KiB
Bash

#!/bin/bash
manifestCollect()
{
local app_name="$1"
local app_dir="$(appDir "$app_name")"
local libreportal_commit
libreportal_commit=$(git -C "${install_scripts_dir%/scripts/}" rev-parse --short HEAD 2>/dev/null || echo "unknown")
local compose_file=""
for candidate in "$app_dir/docker-compose.yml" "$app_dir/compose.yml"; do
[[ -f "$candidate" ]] && compose_file="$candidate" && break
done
local compose_hash="unknown"
[[ -n "$compose_file" ]] && compose_hash=$(sha256sum "$compose_file" 2>/dev/null | cut -d' ' -f1)
local images_json="[]"
if [[ -n "$compose_file" ]]; then
local images=()
while IFS= read -r line; do
local img
img=$(echo "$line" | sed -E 's/^[[:space:]]*image:[[:space:]]*["'\'']?([^"'\'' ]+)["'\'']?.*/\1/')
[[ -n "$img" ]] && images+=("\"$img\"")
done < <(grep -E '^[[:space:]]+image:' "$compose_file" 2>/dev/null)
if [[ ${#images[@]} -gt 0 ]]; then
images_json="[$(IFS=,; echo "${images[*]}")]"
fi
fi
local volumes_json="[]"
if [[ -d "$app_dir" ]]; then
local vols=()
while IFS= read -r d; do
[[ -n "$d" ]] && vols+=("\"$(basename "$d")\"")
done < <(find "$app_dir" -mindepth 1 -maxdepth 1 -type d 2>/dev/null)
if [[ ${#vols[@]} -gt 0 ]]; then
volumes_json="[$(IFS=,; echo "${vols[*]}")]"
fi
fi
local size_bytes
size_bytes=$(runFileOp du -sb "$app_dir" 2>/dev/null | awk '{print $1}')
[[ -z "$size_bytes" ]] && size_bytes=0
local file_count
file_count=$(runFileOp find "$app_dir" -type f 2>/dev/null | wc -l | tr -d ' ')
# Where this app lived, so a restore or migrate onto a host with a
# different disk layout can resolve it instead of guessing. The NAME is what
# travels (paths mean nothing on the other host); the path and fs uuid are
# recorded for diagnostics and for the "is this the same disk?" question.
local storage_json='{}'
if declare -f storageLocationName >/dev/null 2>&1; then
local _root="${app_dir%/*}" _locname _fsuuid
_locname=$(storageLocationName "$_root" 2>/dev/null) || _locname="default"
_fsuuid=$(findmnt -no UUID --target "$_root" 2>/dev/null | tail -1)
storage_json="{\"location\":\"${_locname}\",\"path\":\"${app_dir}\",\"fs_uuid\":\"${_fsuuid}\"}"
fi
local strategy="${CFG_BACKUP_STRATEGY:-auto}"
declare -f backupResolveStrategy >/dev/null 2>&1 && strategy=$(backupResolveStrategy "$app_name")
local databases_json="[]"
if declare -f backupDbDescriptors >/dev/null 2>&1; then
local dbs=() desc kind container datadir path
while IFS= read -r desc; do
[[ -z "$desc" ]] && continue
IFS=':' read -r kind container datadir path <<< "$desc"
dbs+=("{\"kind\":\"$kind\",\"container\":\"$container\",\"path\":\"$path\"}")
done < <(backupDbDescriptors "$app_name")
[[ ${#dbs[@]} -gt 0 ]] && databases_json="[$(IFS=,; echo "${dbs[*]}")]"
fi
cat <<EOF
{
"version": 2,
"app": "$app_name",
"host": "${CFG_INSTALL_NAME:-libreportal}",
"created_at": "$(date -Iseconds)",
"libreportal_commit": "$libreportal_commit",
"engine": "restic",
"compose_hash": "$compose_hash",
"images": $images_json,
"volumes": $volumes_json,
"size_bytes": $size_bytes,
"file_count": $file_count,
"strategy": "$strategy",
"storage": $storage_json,
"databases": $databases_json
}
EOF
}