LibrePortal/scripts/restore/restore_app_start.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

155 lines
5.0 KiB
Bash

#!/bin/bash
restoreAppStart()
{
local app_name="$1"
local snapshot_arg="$2"
local location_idx="$3"
local host_filter="$4"
local stored_app_name="$app_name"
if [[ -z "$app_name" ]]; then
isError "restoreAppStart called with empty app_name"
return 1
fi
if [[ -z "$(resticEnabledLocations)" ]]; then
isError "No backup locations enabled — cannot restore"
return 1
fi
isHeader "Restoring $stored_app_name"
local restore_started_at
restore_started_at=$(date -Iseconds)
isNotice "Task started: restore $stored_app_name at $restore_started_at"
((menu_number++))
echo ""
echo "---- $menu_number. Picking backup"
echo ""
local pick
pick=$(restorePickSnapshot "$stored_app_name" "$location_idx" "$snapshot_arg" "$host_filter")
if [[ -z "$pick" ]]; then
isError "No backup to restore from"
return 1
fi
local chosen_idx="${pick%%:*}"
local chosen_id="${pick##*:}"
isSuccessful "Using backup ${chosen_id:0:8} from $(resticLocationName "$chosen_idx")"
((menu_number++))
echo ""
echo "---- $menu_number. Setting up install folder and config for $stored_app_name"
echo ""
dockerConfigSetupToContainer "loud" "$stored_app_name" "install"
initializeAppVariables "$stored_app_name"
((menu_number++))
echo ""
echo "---- $menu_number. Shutting down container(s) for restoration"
echo ""
dockerComposeDown "$stored_app_name"
((menu_number++))
echo ""
echo "---- $menu_number. Wiping existing app folder"
echo ""
if [[ -d "$(appDir "$stored_app_name")" ]]; then
# Root-owned helper, not runFileOp — restoring over an app that left
# sub-UID data behind (postgres, www-data, …) needs to actually wipe
# those dirs before laying the snapshot down.
runOwnership app-data-remove "$stored_app_name"
fi
((menu_number++))
echo ""
echo "---- $menu_number. Running pre-restore hook (if present)"
echo ""
restoreAppRunHook "$stored_app_name" pre
((menu_number++))
echo ""
echo "---- $menu_number. Restoring snapshot ${chosen_id:0:8}"
echo ""
# Take the source path from the SNAPSHOT, not from local config. Building the
# include filter from this host's containers root only works when both sides
# agree byte-for-byte, so a snapshot taken on a host with a different
# --containers-dir (or on a different storage location) matched nothing and
# restored silently. storageRestoreAppTo restores in place when the paths
# agree and stages-then-moves when they do not.
if ! storageRestoreAppTo "$chosen_idx" "$chosen_id" "$stored_app_name"; then
isError "Restore failed — leaving app in stopped state"
return 1
fi
# NO blanket chown here. The snapshot's ownership is the thing being restored:
# container-owned data (postgres, mongo, prometheus' store) must come back
# under the container's uid or the app will not boot. resticRestoreSnapshot
# now recreates those uids faithfully, and this line could only ever undo
# that — it used to run as the docker install user, so against a correctly
# restored tree it just fails file by file, and where it did "work" it was
# cementing the ownership that broke the app.
((menu_number++))
echo ""
echo "---- $menu_number. Rehydrating databases + files (pre-start)"
echo ""
restoreDbRehydratePreStart "$stored_app_name"
restoreFilesRehydratePreStart "$stored_app_name"
((menu_number++))
echo ""
echo "---- $menu_number. Updating docker compose file(s)"
echo ""
dockerComposeUpdateAndStartApp "$stored_app_name" install
((menu_number++))
echo ""
echo "---- $menu_number. Fixing permissions before starting"
echo ""
fixPermissionsBeforeStart "$stored_app_name"
((menu_number++))
echo ""
echo "---- $menu_number. Starting up the $stored_app_name docker service(s)"
echo ""
dockerComposeUp "$stored_app_name"
((menu_number++))
echo ""
echo "---- $menu_number. Running post-restore hook (if present)"
echo ""
restoreAppRunHook "$stored_app_name" post
((menu_number++))
echo ""
echo "---- $menu_number. Loading database dumps (post-start)"
echo ""
restoreDbReplayPostStart "$stored_app_name"
((menu_number++))
echo ""
echo "---- $menu_number. Logging restore into database"
echo ""
databaseRestoreInsert "$stored_app_name"
databaseInstallApp "$stored_app_name"
((menu_number++))
echo ""
echo "---- $menu_number. Running Headscale setup (if required)"
echo ""
setupHeadscale "$stored_app_name"
((menu_number++))
echo ""
echo "---- $menu_number. Running app-specific updates (if required)"
echo ""
appUpdateSpecifics "$stored_app_name"
local restore_finished_at
restore_finished_at=$(date -Iseconds)
isSuccessful "Task finished: restore $stored_app_name at $restore_finished_at (started $restore_started_at)"
menu_number=0
}