#!/usr/bin/env bash set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" CONTAINER="geointel" BACKUP_DIR="" usage() { cat <<'EOF' Usage: bash scripts/verify_release_backup.sh --backup-dir PATH [--container NAME] Verifies checksums, manifest shape and PostgreSQL archive readability. This command is read-only and never creates, restores or drops a database. EOF } while [ "$#" -gt 0 ]; do case "$1" in --backup-dir) BACKUP_DIR="$2"; shift 2 ;; --container) CONTAINER="$2"; shift 2 ;; --help|-h) usage; exit 0 ;; *) echo "Unknown argument: $1" >&2; usage >&2; exit 2 ;; esac done if [ -z "$BACKUP_DIR" ]; then usage >&2 exit 2 fi for required in docker python3 sha256sum; do command -v "$required" >/dev/null 2>&1 || { echo "Missing required command: $required" >&2 exit 2 } done BACKUP_DIR="$(python3 -c 'import pathlib,sys; print(pathlib.Path(sys.argv[1]).expanduser().resolve())' "$BACKUP_DIR")" for required_file in manifest.json database.dump database.list database-metadata.tsv table-counts.tsv CHECKSUMS.sha256; do test -s "$BACKUP_DIR/$required_file" || { echo "Missing or empty backup artifact: $required_file" >&2 exit 3 } done ( cd "$BACKUP_DIR" sha256sum -c CHECKSUMS.sha256 ) python3 - "$BACKUP_DIR/manifest.json" <<'PY' import json import pathlib import sys payload = json.loads(pathlib.Path(sys.argv[1]).read_text(encoding="utf-8")) required = { "schema_version", "release_id", "created_at", "read_only_source", "database_name", "database_user", "image_id", } missing = sorted(required - payload.keys()) if missing: raise SystemExit(f"Backup manifest misses: {', '.join(missing)}") if payload["schema_version"] != 1 or payload["read_only_source"] is not True: raise SystemExit("Unsupported or unsafe backup manifest") tool_revision = payload.get("backup_tool_revision", payload.get("git_commit")) if not isinstance(tool_revision, str) or len(tool_revision) < 7: raise SystemExit("Backup manifest lacks its backup-tool revision") running_revision = payload.get("running_image_revision") if running_revision is not None and not isinstance(running_revision, str): raise SystemExit("Backup manifest has an invalid running-image revision") PY python3 "$ROOT/scripts/release_backup_snapshot.py" verify-backup --backup-dir "$BACKUP_DIR" if [ "$(docker inspect -f '{{.State.Running}}' "$CONTAINER" 2>/dev/null || true)" != "true" ]; then echo "Container '$CONTAINER' is required to run pg_restore --list." >&2 exit 3 fi TMP_LIST="$(mktemp)" trap 'rm -f -- "$TMP_LIST"' EXIT docker exec -i "$CONTAINER" pg_restore --list \ < "$BACKUP_DIR/database.dump" \ > "$TMP_LIST" cmp -s "$TMP_LIST" "$BACKUP_DIR/database.list" || { echo "The current pg_restore listing differs from the retained listing." >&2 exit 3 } grep -q "TABLE DATA" "$TMP_LIST" || { echo "Archive contains no table data entries." >&2 exit 3 } echo "Release backup verified read-only: $BACKUP_DIR"