#!/bin/bash # 备份完整性校验脚本 # 用法: ./backup-verify.sh [backup_file] [--min-size BYTES] # 不传参数时校验最新备份 set -u show_help() { cat <&2 exit 1 fi MIN_SIZE="$2" shift 2 ;; --no-sql-check) NO_SQL_CHECK=1 shift ;; *) if [ -z "$BACKUP_FILE" ]; then BACKUP_FILE="$1" else echo "ERROR: Unknown argument: $1" >&2 exit 1 fi shift ;; esac done BACKUP_DIR="${BACKUP_DIR:-./backups}" # 如果未指定文件,查找最新备份 if [ -z "$BACKUP_FILE" ]; then BACKUP_FILE=$(ls -t "$BACKUP_DIR"/db_backup_*.sql.gz 2>/dev/null | head -1) if [ -z "$BACKUP_FILE" ]; then echo "ERROR: No backup file found in $BACKUP_DIR" >&2 echo "Hint: Run scripts/backup-db.sh first or specify a file path" >&2 exit 1 fi fi echo "=== Backup Verification Report ===" echo "File: $BACKUP_FILE" echo "Time: $(date -u +"%Y-%m-%dT%H:%M:%SZ")" echo "" ERRORS=0 WARNINGS=0 # 步骤 1: 检查文件存在 echo "[1/4] Checking file existence..." if [ ! -f "$BACKUP_FILE" ]; then echo " FAIL: File does not exist: $BACKUP_FILE" exit 1 fi echo " PASS: File exists" # 步骤 2: 检查文件大小 echo "[2/4] Checking file size..." FILE_SIZE=$(stat -c%s "$BACKUP_FILE" 2>/dev/null || stat -f%z "$BACKUP_FILE" 2>/dev/null) if [ -z "$FILE_SIZE" ]; then echo " WARN: Could not determine file size" WARNINGS=$((WARNINGS + 1)) elif [ "$FILE_SIZE" -lt "$MIN_SIZE" ]; then echo " FAIL: File size ${FILE_SIZE} bytes is below threshold ${MIN_SIZE} bytes" echo " This may indicate a corrupted or empty backup" exit 1 else echo " PASS: File size ${FILE_SIZE} bytes (threshold: ${MIN_SIZE} bytes)" fi # 步骤 3: 校验 gzip 完整性 echo "[3/4] Verifying gzip integrity..." if ! gunzip -t "$BACKUP_FILE" 2>/dev/null; then echo " FAIL: gzip integrity check failed - file may be corrupted" exit 1 fi echo " PASS: gzip integrity verified" # 步骤 4: 校验 SQL 内容 echo "[4/4] Verifying SQL content..." TEMP_SQL=$(mktemp 2>/dev/null || echo "/tmp/backup_verify_$$.sql") trap "rm -f \"$TEMP_SQL\" /tmp/backup_verify_errors_$$.txt 2>/dev/null" EXIT if ! gunzip -c "$BACKUP_FILE" > "$TEMP_SQL" 2>/dev/null; then echo " FAIL: Could not decompress backup file" exit 1 fi # 检查文件非空 SQL_SIZE=$(stat -c%s "$TEMP_SQL" 2>/dev/null || stat -f%z "$TEMP_SQL" 2>/dev/null) if [ -z "$SQL_SIZE" ] || [ "$SQL_SIZE" -eq 0 ]; then echo " FAIL: Decompressed SQL file is empty" exit 1 fi echo " PASS: Decompressed size: ${SQL_SIZE} bytes" # 检查 mysqldump 头部 if grep -q "MySQL dump" "$TEMP_SQL" 2>/dev/null || grep -q "mysqldump" "$TEMP_SQL" 2>/dev/null; then echo " PASS: mysqldump header found" else echo " WARN: mysqldump header not found (may not be a standard mysqldump file)" WARNINGS=$((WARNINGS + 1)) fi # 检查 SQL 语句数量 STMT_COUNT=$(grep -c ";" "$TEMP_SQL" 2>/dev/null || echo 0) if [ "$STMT_COUNT" -lt 10 ]; then echo " WARN: Low statement count (${STMT_COUNT} semicolons)" WARNINGS=$((WARNINGS + 1)) else echo " PASS: Found ${STMT_COUNT} SQL statements" fi # 检查 CREATE TABLE 数量 CREATE_COUNT=$(grep -ci "CREATE TABLE" "$TEMP_SQL" 2>/dev/null || echo 0) echo " INFO: Found ${CREATE_COUNT} CREATE TABLE statements" # 检查明显的语法错误标记 if grep -qi "ERROR at line" "$TEMP_SQL" 2>/dev/null; then echo " FAIL: Found error markers in SQL file" exit 1 fi # SQL 语法校验(可选,需要 DATABASE_URL) if [ "$NO_SQL_CHECK" -eq 1 ]; then echo " SKIP: SQL syntax check skipped (--no-sql-check)" elif [ -z "${DATABASE_URL:-}" ]; then echo " SKIP: DATABASE_URL not set, skipping SQL syntax check" else echo " Performing SQL syntax check via mysql..." # 解析 DATABASE_URL DB_USER=$(echo "$DATABASE_URL" | sed -n 's/.*:\/\/\([^:]*\):.*/\1/p') DB_PASS=$(echo "$DATABASE_URL" | sed -n 's/.*:\/\/[^:]*:\([^@]*\)@.*/\1/p') DB_HOST=$(echo "$DATABASE_URL" | sed -n 's/.*@\([^:]*\):.*/\1/p') DB_PORT=$(echo "$DATABASE_URL" | sed -n 's/.*:\([0-9]*\)\/.*/\1/p') if [ -z "$DB_HOST" ] || [ -z "$DB_USER" ]; then echo " WARN: Could not parse DATABASE_URL, skipping SQL syntax check" WARNINGS=$((WARNINGS + 1)) else # 创建临时数据库进行语法校验(不影响生产数据) TEMP_DB="verify_$(date +%s)_$$" ERROR_FILE="/tmp/backup_verify_errors_$$.txt" if mysql -h "$DB_HOST" -P "${DB_PORT:-3306}" -u "$DB_USER" -p"$DB_PASS" \ -e "CREATE DATABASE \`$TEMP_DB\`;" 2>"$ERROR_FILE"; then # 使用 --force 继续执行,捕获所有语法错误 mysql -h "$DB_HOST" -P "${DB_PORT:-3306}" -u "$DB_USER" -p"$DB_PASS" \ --force "$TEMP_DB" < "$TEMP_SQL" > /dev/null 2>"$ERROR_FILE" || true # 检查是否有语法错误(区分语法错误和执行错误) SYNTAX_ERRORS=$(grep -i "You have an error in your SQL syntax" "$ERROR_FILE" 2>/dev/null | wc -l || echo 0) if [ "$SYNTAX_ERRORS" -gt 0 ]; then echo " FAIL: Found $SYNTAX_ERRORS SQL syntax errors" grep -i "You have an error in your SQL syntax" "$ERROR_FILE" | head -3 # 清理临时数据库 mysql -h "$DB_HOST" -P "${DB_PORT:-3306}" -u "$DB_USER" -p"$DB_PASS" \ -e "DROP DATABASE IF EXISTS \`$TEMP_DB\`;" 2>/dev/null || true exit 1 else echo " PASS: SQL syntax check passed (no syntax errors)" fi # 清理临时数据库 mysql -h "$DB_HOST" -P "${DB_PORT:-3306}" -u "$DB_USER" -p"$DB_PASS" \ -e "DROP DATABASE IF EXISTS \`$TEMP_DB\`;" 2>/dev/null || true else echo " WARN: Could not create temp database for syntax check, skipping" WARNINGS=$((WARNINGS + 1)) fi fi fi echo "" echo "=== Verification Summary ===" echo "Errors: $ERRORS" echo "Warnings: $WARNINGS" if [ "$ERRORS" -gt 0 ]; then echo "Result: FAILED" exit 1 fi echo "Result: PASSED" exit 0