文件
wangsli 64ed844bfe
ci-e2e / e2e (push) Waiting to run
ci-minimal / check-minimal (push) Waiting to run
补强生产数据路径与备份来源护栏(07-26-internal-ops-recovery-baseline)
2026-07-27 06:37:47 +08:00

159 行
5.2 KiB
Bash
可执行文件

#!/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)}"
BUILD_SHA="${BUILD_SHA:-}"
SOURCE_IMAGE_DIGEST="${SOURCE_IMAGE_DIGEST:-}"
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
if [[ -z "$BUILD_SHA" ]]; then
BUILD_SHA="$(git -C "$ROOT_DIR" rev-parse --short=12 HEAD 2>/dev/null || true)"
fi
if [[ -n "$BUILD_SHA" && ! "$BUILD_SHA" =~ ^[0-9a-fA-F]{7,64}$ ]]; then
echo "[ERR] BUILD_SHA must be a 7-64 character hexadecimal commit id" >&2
exit 1
fi
if [[ -n "$SOURCE_IMAGE_DIGEST" && ! "$SOURCE_IMAGE_DIGEST" =~ ^sha256:[0-9a-fA-F]{64}$ ]]; then
echo "[ERR] SOURCE_IMAGE_DIGEST must use sha256:<64 hexadecimal characters>" >&2
exit 1
fi
if [[ -z "$BUILD_SHA" && -z "$SOURCE_IMAGE_DIGEST" ]]; then
echo "[ERR] BUILD_SHA or SOURCE_IMAGE_DIGEST is required to identify the backup source" >&2
exit 1
fi
BUILD_SHA="${BUILD_SHA,,}"
SOURCE_IMAGE_DIGEST="${SOURCE_IMAGE_DIGEST,,}"
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 ' ')"
CREATED_AT="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
MANIFEST_ARGS=(
"$TEMP_DIR/$MANIFEST_NAME" "$BACKUP_RUN_ID" "$CREATED_AT" "$BUILD_SHA" "$SOURCE_IMAGE_DIGEST"
"$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, sourceImageDigest, dbFile, dbBytes, dbSha256, storageFile, storageBytes, storageSha256] = process.argv.slice(2);
const manifest = {
schemaVersion: 1,
runId,
createdAt,
buildSha: buildSha || null,
sourceImageDigest: sourceImageDigest || 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,
source_image_digest,
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,
"sourceImageDigest": source_image_digest 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"