#!/usr/bin/env bash set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" BACKUP_DIR="${BACKUP_DIR:-$ROOT_DIR/deploy/trialrun/backups}" BACKUP_RUN_ID="${BACKUP_RUN_ID:-$(date -u +%Y%m%dT%H%M%SZ)}" if [[ ! "$BACKUP_RUN_ID" =~ ^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$ ]]; then echo "[ERR] BACKUP_RUN_ID must use 1-80 safe characters" >&2 exit 1 fi mkdir -p "$BACKUP_DIR" FINAL_DIR="$BACKUP_DIR/hyb-oa-backup-$BACKUP_RUN_ID" if [[ -e "$FINAL_DIR" ]]; then echo "[ERR] backup batch already exists: hyb-oa-backup-$BACKUP_RUN_ID" >&2 exit 1 fi TEMP_DIR="$(mktemp -d "$BACKUP_DIR/.hyb-oa-backup-$BACKUP_RUN_ID.tmp.XXXXXX")" cleanup() { if [[ -n "${TEMP_DIR:-}" && -d "$TEMP_DIR" && "$TEMP_DIR" == "$BACKUP_DIR"/.hyb-oa-backup-*.tmp.* ]]; then rm -rf -- "$TEMP_DIR" fi } trap cleanup EXIT DB_NAME="database.dump" STORAGE_NAME="storage.tar.gz" MANIFEST_NAME="manifest.json" DB_FILE="$TEMP_DIR/$DB_NAME" STORAGE_FILE="$TEMP_DIR/$STORAGE_NAME" BACKUP_DIR="$TEMP_DIR" OUT_FILE="$DB_FILE" "$ROOT_DIR/scripts/backup-db.sh" BACKUP_DIR="$TEMP_DIR" OUT_FILE="$STORAGE_FILE" "$ROOT_DIR/scripts/backup-storage.sh" DB_VALIDATED="false" if [[ "${DB_TOOL_MODE:-auto}" != "docker" ]] && command -v pg_restore >/dev/null 2>&1; then if pg_restore --list "$DB_FILE" >/dev/null; then DB_VALIDATED="true" elif [[ "${DB_TOOL_MODE:-auto}" == "host" ]]; then echo "[ERR] host pg_restore could not validate the DB dump" >&2 exit 1 fi fi if [[ "$DB_VALIDATED" != "true" ]] && command -v docker >/dev/null 2>&1 && [[ -n "${DB_CONTAINER:-}" ]]; then docker exec -i "$DB_CONTAINER" pg_restore --list <"$DB_FILE" >/dev/null DB_VALIDATED="true" fi if [[ "$DB_VALIDATED" != "true" ]]; then echo "[ERR] pg_restore or DB_CONTAINER is required to validate the DB dump" >&2 exit 1 fi tar -tzf "$STORAGE_FILE" >/dev/null while IFS= read -r listing_line; do if [[ "${listing_line:0:1}" == "l" || "${listing_line:0:1}" == "h" ]]; then echo "[ERR] storage backup contains links that the safe restore path will reject" >&2 exit 1 fi done < <(tar -tvzf "$STORAGE_FILE") DB_SHA256="$(sha256sum "$DB_FILE" | awk '{print $1}')" STORAGE_SHA256="$(sha256sum "$STORAGE_FILE" | awk '{print $1}')" DB_BYTES="$(wc -c <"$DB_FILE" | tr -d ' ')" STORAGE_BYTES="$(wc -c <"$STORAGE_FILE" | tr -d ' ')" BUILD_SHA="$(git -C "$ROOT_DIR" rev-parse --short=12 HEAD 2>/dev/null || true)" CREATED_AT="$(date -u +%Y-%m-%dT%H:%M:%SZ)" MANIFEST_ARGS=( "$TEMP_DIR/$MANIFEST_NAME" "$BACKUP_RUN_ID" "$CREATED_AT" "$BUILD_SHA" "$DB_NAME" "$DB_BYTES" "$DB_SHA256" "$STORAGE_NAME" "$STORAGE_BYTES" "$STORAGE_SHA256" ) if command -v node >/dev/null 2>&1; then node - "${MANIFEST_ARGS[@]}" <<'NODE' const fs = require("node:fs"); const [manifestPath, runId, createdAt, buildSha, dbFile, dbBytes, dbSha256, storageFile, storageBytes, storageSha256] = process.argv.slice(2); const manifest = { schemaVersion: 1, runId, createdAt, buildSha: buildSha || null, status: "complete", artifacts: { database: { file: dbFile, bytes: Number(dbBytes), sha256: dbSha256 }, storage: { file: storageFile, bytes: Number(storageBytes), sha256: storageSha256 }, }, }; fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, { encoding: "utf8", flag: "wx" }); NODE elif command -v python3 >/dev/null 2>&1; then python3 - "${MANIFEST_ARGS[@]}" <<'PYTHON' import json import sys ( manifest_path, run_id, created_at, build_sha, db_file, db_bytes, db_sha256, storage_file, storage_bytes, storage_sha256, ) = sys.argv[1:] manifest = { "schemaVersion": 1, "runId": run_id, "createdAt": created_at, "buildSha": build_sha or None, "status": "complete", "artifacts": { "database": {"file": db_file, "bytes": int(db_bytes), "sha256": db_sha256}, "storage": {"file": storage_file, "bytes": int(storage_bytes), "sha256": storage_sha256}, }, } with open(manifest_path, "x", encoding="utf-8") as manifest_file: json.dump(manifest, manifest_file, ensure_ascii=True, indent=2) manifest_file.write("\n") PYTHON else echo "[ERR] node or python3 is required to write the backup manifest" >&2 exit 1 fi mv "$TEMP_DIR" "$FINAL_DIR" TEMP_DIR="" trap - EXIT echo "[OK] Paired backup complete: hyb-oa-backup-$BACKUP_RUN_ID" echo "[INFO] Copy the complete batch directory to approved off-host/off-disk storage"