加固备份恢复本地基线(07-26-internal-ops-recovery-baseline)
这个提交包含在:
可执行文件
+93
@@ -0,0 +1,93 @@
|
||||
#!/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)"
|
||||
|
||||
node - "$TEMP_DIR/$MANIFEST_NAME" "$BACKUP_RUN_ID" "$CREATED_AT" "$BUILD_SHA" \
|
||||
"$DB_NAME" "$DB_BYTES" "$DB_SHA256" "$STORAGE_NAME" "$STORAGE_BYTES" "$STORAGE_SHA256" <<'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
|
||||
|
||||
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"
|
||||
在新工单中引用
屏蔽一个用户