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>
89 lines
2.3 KiB
Bash
89 lines
2.3 KiB
Bash
#!/bin/bash
|
|
|
|
resticSnapshotsJson()
|
|
{
|
|
local idx="$1"
|
|
local app_filter="$2"
|
|
local host_filter="$3"
|
|
|
|
resticEnvExport "$idx" || return 1
|
|
|
|
local args=(snapshots --json --no-lock)
|
|
[[ -n "$app_filter" ]] && args+=(--tag "app=$app_filter")
|
|
[[ -n "$host_filter" ]] && args+=(--host "$host_filter")
|
|
|
|
runBackupOp restic "${args[@]}" 2>/dev/null
|
|
local rc=$?
|
|
resticEnvUnset
|
|
return $rc
|
|
}
|
|
|
|
resticSystemSnapshotsJson()
|
|
{
|
|
local idx="$1"
|
|
local host_filter="$2"
|
|
|
|
resticEnvExport "$idx" || return 1
|
|
|
|
local args=(snapshots --json --no-lock --tag "system=config")
|
|
[[ -n "$host_filter" ]] && args+=(--host "$host_filter")
|
|
|
|
runBackupOp restic "${args[@]}" 2>/dev/null
|
|
local rc=$?
|
|
resticEnvUnset
|
|
return $rc
|
|
}
|
|
|
|
resticSnapshotLatestId()
|
|
{
|
|
local idx="$1"
|
|
local app_name="$2"
|
|
local host="${3:-$CFG_INSTALL_NAME}"
|
|
|
|
resticEnvExport "$idx" || return 1
|
|
local id
|
|
id=$(runBackupOp restic snapshots \
|
|
--tag "app=$app_name" --host "$host" \
|
|
--latest 1 --json --no-lock 2>/dev/null | \
|
|
grep -o '"short_id":"[^"]*"' | head -1 | cut -d'"' -f4)
|
|
resticEnvUnset
|
|
echo "$id"
|
|
}
|
|
|
|
resticSnapshotListFiles()
|
|
{
|
|
local idx="$1"
|
|
local snapshot_id="$2"
|
|
|
|
resticEnvExport "$idx" || return 1
|
|
runBackupOp restic ls --json --no-lock "$snapshot_id" 2>/dev/null
|
|
local rc=$?
|
|
resticEnvUnset
|
|
return $rc
|
|
}
|
|
|
|
# The paths recorded ON one snapshot, one per line.
|
|
#
|
|
# Deliberately its own call rather than a filter on resticSnapshotsJson: that
|
|
# function's second parameter is an app TAG filter, so asking it for a snapshot
|
|
# id runs `--tag app=<id>` and matches nothing. storageSnapshotSourcePath did
|
|
# exactly that, so it always returned 1 and every restore silently took the
|
|
# "restore in place" fallback — the very bug storage_restore_path.sh exists to
|
|
# fix. Restore accepts a snapshot id positionally; use that.
|
|
resticSnapshotPaths()
|
|
{
|
|
local idx="$1"
|
|
local snapshot_id="$2"
|
|
|
|
resticEnvExport "$idx" || return 1
|
|
local json
|
|
json=$(runBackupOp restic snapshots "$snapshot_id" --json --no-lock 2>/dev/null)
|
|
local rc=$?
|
|
resticEnvUnset
|
|
[[ $rc -eq 0 && -n "$json" ]] || return 1
|
|
|
|
printf '%s' "$json" \
|
|
| grep -o '"paths":\[[^]]*\]' | head -1 \
|
|
| grep -o '"/[^"]*"' | tr -d '"'
|
|
}
|