94 行
3.3 KiB
Bash
可执行文件
94 行
3.3 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)}"
|
|
|
|
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"
|