#!/usr/bin/env bash
# ---------------------------------------------------------------------------
# Restore a backup produced by scripts/pi-backup.sh.
#
# Deliberately conservative:
# - refuses to run unless the manifest verifies
# - moves the current directory aside instead of overwriting it
# - never restarts a service
# - dry run by default
#
# Usage:
# scripts/pi-restore.sh --from
[--only ] [--apply]
#
# Components: runtime, sessions, workspace-, sessions-,
# systemd. Secrets are never restored automatically -- unpack
# secrets.tar.gz by hand so that each file lands where you intend.
# ---------------------------------------------------------------------------
# shellcheck source=lib/common.sh
. "$(dirname "${BASH_SOURCE[0]}")/lib/common.sh"
FROM=""
ONLY=""
APPLY=0
while [ "$#" -gt 0 ]; do
case "$1" in
--from) FROM="${2:?--from needs a path}"; shift 2 ;;
--only) ONLY="${2:?--only needs a component}"; shift 2 ;;
--apply) APPLY=1; shift ;;
-h|--help) sed -n '2,18p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; exit 0 ;;
*) die "unknown argument: $1" ;;
esac
done
[ -n "$FROM" ] || die "--from is required"
[ -d "$FROM" ] || die "no such directory: $FROM"
[ -f "$FROM/MANIFEST.sha256" ] || die "no MANIFEST.sha256 in $FROM"
head1 "verify $FROM"
( cd "$FROM" && sha256sum -c MANIFEST.sha256 ) || die "manifest verification failed; refusing to restore"
ok "manifest verified"
STAMP="$(date -u +%Y%m%dT%H%M%SZ)"
[ "$APPLY" -eq 1 ] || info "\n${C_DIM}(dry run; pass --apply to write)${C_OFF}"
# Move a live path aside rather than overwriting, so a bad restore is reversible.
preserve() {
local path="$1"
[ -e "$path" ] || return 0
local aside="$path.pre-restore-$STAMP"
info " preserve $path -> $aside"
[ "$APPLY" -eq 1 ] && mv "$path" "$aside"
return 0
}
extract() {
local archive="$1" into="$2"
info " extract $(basename "$archive") -> $into"
if [ "$APPLY" -eq 1 ]; then
install -d -m 700 "$into"
tar -xzf "$archive" -C "$into"
fi
}
want() { [ -z "$ONLY" ] || [ "$ONLY" = "$1" ]; }
if want runtime && [ -f "$FROM/pi-agent-runtime.tar.gz" ]; then
head1 "runtime"
preserve "$HOME/.pi/agent"
extract "$FROM/pi-agent-runtime.tar.gz" "$HOME/.pi"
fi
if want sessions && [ -f "$FROM/pi-global-sessions.tar.gz" ]; then
head1 "global sessions"
extract "$FROM/pi-global-sessions.tar.gz" "$HOME/.pi/agent"
fi
while IFS= read -r name; do
profile="$REPO_ROOT/scenarios/$name/profile.toml"
[ -f "$profile" ] || continue
if want "workspace-$name" && [ -f "$FROM/workspace-$name.tar.gz" ]; then
workspace="$(toml_get "$profile" scenario workspace)"
if [ -n "$workspace" ]; then
head1 "workspace $name"
preserve "$workspace"
extract "$FROM/workspace-$name.tar.gz" "$(dirname "$workspace")"
fi
fi
if want "sessions-$name" && [ -f "$FROM/sessions-$name.tar.gz" ]; then
session_dir="$(toml_get "$profile" scenario session_dir)"
if [ -n "$session_dir" ]; then
head1 "sessions $name"
preserve "$session_dir"
extract "$FROM/sessions-$name.tar.gz" "$(dirname "$session_dir")"
fi
fi
done < <(list_scenarios)
if want systemd && [ -f "$FROM/systemd-units.tar.gz" ]; then
head1 "systemd units"
info " ${C_DIM}not extracted automatically: the archive holds every user unit,"
info " and most are unrelated to Pi. Unpack and install selectively:${C_OFF}"
info " mkdir /tmp/units && tar -xzf $FROM/systemd-units.tar.gz -C /tmp/units"
info " install -m 600 /tmp/units/ ~/.config/systemd/user/"
info " systemctl --user daemon-reload"
fi
if [ -f "$FROM/secrets.tar.gz" ]; then
head1 "secrets"
info " ${C_DIM}not restored automatically. Filenames are path-encoded; unpack and"
info " place each file deliberately, then chmod 600:${C_OFF}"
info " mkdir -m 700 /tmp/sec && tar -xzf $FROM/secrets.tar.gz -C /tmp/sec && ls /tmp/sec"
fi
printf '\n'
if [ "$APPLY" -eq 1 ]; then
ok "restore applied; preserved copies carry the suffix .pre-restore-$STAMP"
info ""
info "Restart the affected gateways yourself, then check health:"
info " systemctl --user restart curator.service pi-memo-telegram.service"
info " curl -fsS http://127.0.0.1:8766/api/health"
else
info "${C_DIM}dry run complete; re-run with --apply${C_OFF}"
fi