storageSnapshotSourcePath resolved a snapshot's source path with
engineSnapshotsJson "$idx" "$snapshot_id"
but that function's second parameter is an app TAG filter. So it ran
`restic snapshots --tag app=<snapshot-id>`, matched nothing, and returned 1 —
every time, for every snapshot, since the file was written.
Nothing broke loudly, because both callers have a fallback:
* storageRestoreAppTo fell through to "restoring in place", reinstating the
exact cross-root bug the file exists to fix — restoring onto a host whose
containers root differs from the source's matched no include path and
restored nothing, silently
* the first-run preflight never read a manifest, so every app reported size
"?" and its fit and location checks passed unconditionally. Thirteen green
ticks that had checked nothing.
Add engineSnapshotPaths: restic answers it with a positional snapshot id, kopia
by filtering its list. borg has no adapter on purpose — it rebuilds its listing
from archive metadata that carries no paths — so a missing adapter is a quiet
"no" and those callers keep their in-place fallback.
Add scripts/dev/lp-preflight-test, which pins the cases that must say NO: an
app too big for the disk, one this version no longer ships, one whose storage
location is gone, and a resolver that reaches for the app-tag filter again.
Verified against both historical bugs — reintroducing either fails the test.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
98 lines
3.7 KiB
Bash
98 lines
3.7 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 path
|
|
|
|
# Ask about this ONE snapshot. The first version passed the id to
|
|
# engineSnapshotsJson, whose second parameter is an app TAG filter — so it
|
|
# searched for `--tag app=<snapshot-id>`, matched nothing, and returned 1
|
|
# every single time. Nothing broke loudly, because both callers have a
|
|
# fallback: storageRestoreAppTo restored in place (reinstating the
|
|
# cross-root bug this file was written to fix) and the restore preflight
|
|
# reported every app's size as "?" while its fit and location checks passed
|
|
# unconditionally.
|
|
path=$(engineSnapshotPaths "$idx" "$snapshot_id" 2>/dev/null \
|
|
| 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
|
|
}
|