#!/usr/bin/env bash # --------------------------------------------------------------------------- # Back up the live Pi installation and every scenario workspace. # # Archives are written OUTSIDE the repository by design. The precedent to avoid # is hermes-agent-config, which committed hermes-secrets-*.tar.gz into its own # working tree; had that tree ever been pushed, the credentials would have gone # with it. # # Usage: # scripts/pi-backup.sh # default destination # scripts/pi-backup.sh --dest /path/to/dir # scripts/pi-backup.sh --no-secrets # skip credential archives # --------------------------------------------------------------------------- # shellcheck source=lib/common.sh . "$(dirname "${BASH_SOURCE[0]}")/lib/common.sh" DEST_ROOT="/mnt/truenas/multimedia/curator/backup/pi-agent-config" WITH_SECRETS=1 while [ "$#" -gt 0 ]; do case "$1" in --dest) DEST_ROOT="${2:?--dest needs a path}"; shift 2 ;; --no-secrets) WITH_SECRETS=0; shift ;; -h|--help) sed -n '2,16p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; exit 0 ;; *) die "unknown argument: $1" ;; esac done # Canonicalise before comparing: a relative path such as ./backups would # otherwise slip past a literal prefix test and write credential archives into # the working tree. (.gitignore would still stop them being committed, but the # archives should not be there at all.) DEST_ROOT="$(realpath -m -- "$DEST_ROOT")" REPO_REAL="$(realpath -- "$REPO_ROOT")" case "$DEST_ROOT" in "$REPO_REAL"|"$REPO_REAL"/*) die "refusing to write backups inside the repository requested: $DEST_ROOT repository: $REPO_REAL Backups contain plaintext credentials and must live outside the work tree." ;; esac STAMP="$(date -u +%Y%m%dT%H%M%SZ)" DEST="$DEST_ROOT/$STAMP" install -d -m 700 "$DEST" || die "cannot create $DEST" head1 "backup -> $DEST" # --- 1. Pi agent directory, excluding sessions ----------------------------- if [ -d "$HOME/.pi/agent" ]; then tar --exclude='agent/sessions' -czf "$DEST/pi-agent-runtime.tar.gz" -C "$HOME/.pi" agent ok "pi-agent-runtime.tar.gz ${C_DIM}(contains plaintext apiKey)${C_OFF}" chmod 600 "$DEST/pi-agent-runtime.tar.gz" fi # --- 2. Sessions, separately: bulky and lower value ----------------------- if [ -d "$HOME/.pi/agent/sessions" ]; then tar -czf "$DEST/pi-global-sessions.tar.gz" -C "$HOME/.pi/agent" sessions ok "pi-global-sessions.tar.gz" fi # --- 3. Each scenario workspace and session directory --------------------- while IFS= read -r name; do profile="$REPO_ROOT/scenarios/$name/profile.toml" [ -f "$profile" ] || continue workspace="$(toml_get "$profile" scenario workspace)" session_dir="$(toml_get "$profile" scenario session_dir)" if [ -n "$workspace" ] && [ -d "$workspace" ]; then tar --exclude='.venv' --exclude='node_modules' --exclude='__pycache__' \ --exclude='.ccgram-uploads' \ -czf "$DEST/workspace-$name.tar.gz" \ -C "$(dirname "$workspace")" "$(basename "$workspace")" ok "workspace-$name.tar.gz" fi if [ -n "$session_dir" ] && [ -d "$session_dir" ]; then tar -czf "$DEST/sessions-$name.tar.gz" \ -C "$(dirname "$session_dir")" "$(basename "$session_dir")" ok "sessions-$name.tar.gz" fi done < <(list_scenarios) # --- 4. Credentials ------------------------------------------------------- if [ "$WITH_SECRETS" -eq 1 ]; then STAGE="$(mktemp -d)" found=0 while IFS= read -r f; do [ -f "$f" ] || continue cp -p "$f" "$STAGE/$(printf '%s' "$f" | tr '/' '_')" found=1 done </dev/null || true systemctl --user list-units --all --no-pager --no-legend > "$STAGE/list-units.txt" 2>&1 || true systemctl --user list-unit-files --no-pager --no-legend > "$STAGE/list-unit-files.txt" 2>&1 || true tar -czf "$DEST/systemd-units.tar.gz" -C "$STAGE" . rm -rf "$STAGE" ok "systemd-units.tar.gz" { echo "# Environment freeze -- $(date -u +%FT%TZ)" echo; echo "## pi"; pi --version 2>&1 echo; echo "## npm -g"; npm ls -g --depth=0 2>/dev/null echo; echo "## node"; node --version echo; echo "## python3"; python3 --version echo; echo "## pi-agent-config HEAD" git -C "$REPO_ROOT" log --oneline -1 git -C "$REPO_ROOT" rev-parse HEAD } > "$DEST/environment-freeze.txt" ok "environment-freeze.txt" # --- 6. Manifest ---------------------------------------------------------- ( cd "$DEST" && sha256sum ./*.tar.gz environment-freeze.txt > MANIFEST.sha256 ) ok "MANIFEST.sha256" printf '\n' ( cd "$DEST" && sha256sum -c MANIFEST.sha256 >/dev/null 2>&1 ) \ && ok "checksums verified" \ || die "checksum verification FAILED -- do not rely on this backup" info "" info "Restore with: scripts/pi-restore.sh --from $DEST"