#!/usr/bin/env bash set -euo pipefail CREDS_FILE="${CREDS_FILE:-$HOME/.openclaw/credentials/minio-zap.env}" MC_BIN="${MC_BIN:-$HOME/.openclaw/workspace/bin/mc}" PREFIX_ROOT="${PREFIX_ROOT:-workspace-backups}" MAX_AGE_HOURS="${MAX_AGE_HOURS:-12}" if [[ ! -x "$MC_BIN" ]]; then MC_BIN="$(command -v mc || true)" fi fail() { echo "STATE=FAIL" echo "ERROR=$1" exit 1 } [[ -f "$CREDS_FILE" ]] || fail "Missing creds file: $CREDS_FILE" [[ -x "$MC_BIN" ]] || fail "MinIO client not found (set MC_BIN)" # shellcheck disable=SC1090 source "$CREDS_FILE" [[ -n "${MINIO_ENDPOINT:-}" ]] || fail "MINIO_ENDPOINT missing" [[ -n "${MINIO_ACCESS_KEY:-}" ]] || fail "MINIO_ACCESS_KEY missing" [[ -n "${MINIO_SECRET_KEY:-}" ]] || fail "MINIO_SECRET_KEY missing" [[ -n "${MINIO_BUCKET:-}" ]] || fail "MINIO_BUCKET missing" "$MC_BIN" alias set minio "$MINIO_ENDPOINT" "$MINIO_ACCESS_KEY" "$MINIO_SECRET_KEY" >/dev/null latest_prefix="$($MC_BIN ls "minio/$MINIO_BUCKET/$PREFIX_ROOT" 2>/dev/null | awk '{print $NF}' | tr -d '/' | grep -E '^[0-9]{8}T[0-9]{6}Z$' | sort | tail -n1 || true)" [[ -n "$latest_prefix" ]] || fail "No backup prefixes found under $PREFIX_ROOT" # freshness check backup_iso="${latest_prefix:0:4}-${latest_prefix:4:2}-${latest_prefix:6:2} ${latest_prefix:9:2}:${latest_prefix:11:2}:${latest_prefix:13:2} UTC" backup_epoch="$(date -u -d "$backup_iso" +%s 2>/dev/null || echo 0)" now_epoch="$(date -u +%s)" [[ "$backup_epoch" -gt 0 ]] || fail "Could not parse timestamp from prefix: $latest_prefix" age_h=$(( (now_epoch - backup_epoch) / 3600 )) if (( age_h > MAX_AGE_HOURS )); then fail "Latest backup too old (${age_h}h > ${MAX_AGE_HOURS}h): $latest_prefix" fi prefix_path="minio/$MINIO_BUCKET/$PREFIX_ROOT/$latest_prefix" archive="openclaw-${latest_prefix}.tar.gz" sha_file="${archive}.sha256" manifest="manifest.txt" # Ensure required objects exist "$MC_BIN" stat "$prefix_path/$archive" >/dev/null 2>&1 || fail "Missing archive: $archive" "$MC_BIN" stat "$prefix_path/$sha_file" >/dev/null 2>&1 || fail "Missing checksum: $sha_file" "$MC_BIN" stat "$prefix_path/$manifest" >/dev/null 2>&1 || fail "Missing manifest: $manifest" TMPDIR="$(mktemp -d)" trap 'rm -rf "$TMPDIR"' EXIT "$MC_BIN" cp "$prefix_path/$archive" "$TMPDIR/" >/dev/null "$MC_BIN" cp "$prefix_path/$sha_file" "$TMPDIR/" >/dev/null "$MC_BIN" cp "$prefix_path/$manifest" "$TMPDIR/" >/dev/null cd "$TMPDIR" awk '{print $1" '$archive'"}' "$sha_file" > check.sha256 sha256sum -c check.sha256 >/dev/null || fail "SHA256 verification failed" mkdir -p restore # Extract without touching live ~/.openclaw tar -xzf "$archive" -C restore || fail "Archive extraction failed" # Basic structure checks for req in \ "restore/.openclaw/openclaw.json" \ "restore/.openclaw/agents" \ "restore/.openclaw/credentials" \ "restore/.openclaw/workspace"; do [[ -e "$req" ]] || fail "Missing required path in restore: $req" done size_bytes="$(stat -c '%s' "$archive")" echo "STATE=PASS" echo "LATEST_PREFIX=$latest_prefix" echo "AGE_HOURS=$age_h" echo "ARCHIVE_BYTES=$size_bytes" echo "CHECKSUM=OK" echo "EXTRACT=OK" echo "STRUCTURE=OK"