#!/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 }