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>
97 lines
3.4 KiB
Bash
97 lines
3.4 KiB
Bash
#!/bin/bash
|
|
|
|
# Resolving where a snapshot's data actually lives, and where it should land.
|
|
#
|
|
# The bug this exists to fix predates storage locations. Restore did:
|
|
#
|
|
# engineRestoreSnapshot "$idx" "$id" "/" "$containers_dir$app"
|
|
#
|
|
# — restore to / with an include filter built from the LOCAL containers root.
|
|
# restic reproduces a snapshot's absolute paths, so that only works when source
|
|
# and destination paths are byte-identical. LibrePortal has shipped three
|
|
# configurable roots for a while, so migrating from a host installed with
|
|
# --containers-dir=/mnt/ssd/apps onto a default host matched no include path and
|
|
# restored NOTHING, silently. Storage locations make that ordinary rather than
|
|
# rare.
|
|
#
|
|
# The fix is to take the source path from the SNAPSHOT rather than from local
|
|
# config, and to stage-and-move whenever it differs from where the app belongs
|
|
# here.
|
|
|
|
# The path an app occupied in a snapshot. restic records the backed-up paths on
|
|
# the snapshot itself, so ask the repository rather than guessing.
|
|
storageSnapshotSourcePath()
|
|
{
|
|
local idx="$1" snapshot_id="$2" app="$3"
|
|
local json path
|
|
|
|
json=$(engineSnapshotsJson "$idx" "$snapshot_id" 2>/dev/null) || return 1
|
|
# "paths":["/libreportal-containers/bookstack"]
|
|
path=$(printf '%s' "$json" \
|
|
| grep -o '"paths":\[[^]]*\]' \
|
|
| head -1 \
|
|
| grep -o '"/[^"]*"' \
|
|
| tr -d '"' \
|
|
| grep -E "/${app}(/|$)" \
|
|
| head -1)
|
|
[[ -n "$path" ]] || return 1
|
|
printf '%s' "${path%/}"
|
|
}
|
|
|
|
# Restore an app from a snapshot to wherever it belongs on THIS host.
|
|
#
|
|
# Same path on both sides -> restore in place, exactly as before.
|
|
# Different -> restore into staging, then move the tree into position. The move
|
|
# goes through the root helper because the restored tree carries container
|
|
# sub-UIDs the manager cannot handle.
|
|
storageRestoreAppTo()
|
|
{
|
|
local idx="$1" snapshot_id="$2" app="$3"
|
|
|
|
local dest
|
|
if ! dest=$(appDir "$app"); then
|
|
isError "Cannot restore $app — its storage location is not mounted."
|
|
return 1
|
|
fi
|
|
|
|
local src
|
|
if ! src=$(storageSnapshotSourcePath "$idx" "$snapshot_id" "$app"); then
|
|
# Older snapshot, or an engine that does not report paths: fall back to
|
|
# the historical behaviour rather than refusing.
|
|
isNotice "Snapshot does not report its source path — restoring in place."
|
|
engineRestoreSnapshot "$idx" "$snapshot_id" "/" "$dest"
|
|
return $?
|
|
fi
|
|
|
|
if [[ "${src%/}" == "${dest%/}" ]]; then
|
|
engineRestoreSnapshot "$idx" "$snapshot_id" "/" "$src"
|
|
return $?
|
|
fi
|
|
|
|
isNotice "This snapshot was taken at '$src'; restoring to '$dest'."
|
|
|
|
local stage="${restore_dir%/}/relocate-$app.$$"
|
|
runInstallOp mkdir -p "$stage"
|
|
if ! engineRestoreSnapshot "$idx" "$snapshot_id" "$stage" "$src"; then
|
|
isError "Restore into staging failed."
|
|
runInstallOp rm -rf "$stage"
|
|
return 1
|
|
fi
|
|
|
|
# restic reproduces the full absolute path beneath --target.
|
|
local staged="$stage/${src#/}"
|
|
if [[ ! -d "$staged" ]]; then
|
|
isError "Restored tree not found at '$staged' — leaving staging in place for inspection."
|
|
return 1
|
|
fi
|
|
|
|
if ! runOwnership app-adopt "$app" "$staged" "${dest%/*}"; then
|
|
isError "Could not move the restored tree into '$dest' — it is still at '$staged'."
|
|
return 1
|
|
fi
|
|
|
|
runInstallOp rm -rf "$stage"
|
|
isSuccessful "Restored $app to $dest"
|
|
return 0
|
|
}
|