#!/bin/bash # ============================================================================= # 追光AI 容器卷备份脚本 (在宿主机上跑) # - 把 4 个 docker 卷打包到 /home/ubuntu/zhuiguang-ai-backup/dockers/ # - 默认保留最近 7 天快照 # - 增强:磁盘空间预检 + 完整性校验 + 汇总报告 # - 用法: bash scripts/backup-volumes.sh # ============================================================================= set -e PROJECT="/home/ubuntu/zhuiguang-ai" BACKUP_ROOT="/home/ubuntu/zhuiguang-ai-backup/dockers" STAMP=$(date +%Y%m%d-%H%M%S) BACKUP_DIR="${BACKUP_ROOT}/${STAMP}" KEEP_DAYS=7 REQUIRED_FREE_MB=${REQUIRED_FREE_MB:-500} # 最少需要磁盘空闲空间 (MB) PASS=0 FAIL=0 WARN=0 pass() { echo " ✅ $1"; PASS=$((PASS + 1)); } fail() { echo " ❌ $1"; FAIL=$((FAIL + 1)); } warn() { echo " ⚠️ $1"; WARN=$((WARN + 1)); } echo "========================================" echo " 追光AI 卷备份 ($(date '+%Y-%m-%d %H:%M:%S'))" echo "========================================" echo "" # ---- 1) 磁盘空间预检 ---- echo "--- 1) 磁盘空间检查 ---" BACKUP_DISK=$(df -m "$BACKUP_ROOT" 2>/dev/null | tail -1 | awk '{print $4}') if [ -z "$BACKUP_DISK" ]; then BACKUP_DISK=$(df -m /home 2>/dev/null | tail -1 | awk '{print $4}') fi if [ -z "$BACKUP_DISK" ] || [ "$BACKUP_DISK" -lt "$REQUIRED_FREE_MB" ]; then fail "磁盘空间不足: 可用 ${BACKUP_DISK:-?}MB, 需要 ≥ ${REQUIRED_FREE_MB}MB" exit 1 fi pass "磁盘可用空间: ${BACKUP_DISK}MB" mkdir -p "$BACKUP_DIR" echo "" echo "--- 2) 卷备份 ---" declare -A VOL_SIZES TOTAL_BYTES=0 # 备份 4 个卷 for vol in zhuiguang_ai_bot_data zhuiguang_ai_bot_public zhuiguang_ai_bot_logs zhuiguang_ai_bot_prisma; do if ! docker volume inspect "$vol" >/dev/null 2>&1; then warn "卷 $vol 不存在,跳过" continue fi echo " [备份] $vol → ${vol}.tgz" TAR_OUTPUT=$(docker run --rm \ -v "${vol}:/src:ro" \ -v "${BACKUP_DIR}:/dst" \ alpine:3.19 \ tar czf "/dst/${vol}.tgz" -C /src . 2>&1) || true if [ -f "${BACKUP_DIR}/${vol}.tgz" ]; then SIZE=$(ls -lh "${BACKUP_DIR}/${vol}.tgz" | awk '{print $5}') pass "${vol}.tgz (${SIZE})" VOL_SIZES[$vol]=$SIZE TOTAL_BYTES=$((TOTAL_BYTES + $(stat -c%s "${BACKUP_DIR}/${vol}.tgz" 2>/dev/null || echo 0))) else fail "${vol}.tgz 创建失败" fi done # ---- 3) 完整性校验 ---- echo "" echo "--- 3) 完整性校验 ---" for vol in "${!VOL_SIZES[@]}"; do TGZ_PATH="${BACKUP_DIR}/${vol}.tgz" if tar tzf "$TGZ_PATH" >/dev/null 2>&1; then FILE_COUNT=$(tar tzf "$TGZ_PATH" | wc -l) pass "${vol}.tgz 校验通过 (${FILE_COUNT} 个文件)" else fail "${vol}.tgz 校验失败" fi done # ---- 4) .env 备份 ---- echo "" echo "--- 4) 环境变量备份 ---" if [ -f "$PROJECT/env/.env.production" ]; then cp "$PROJECT/env/.env.production" "${BACKUP_DIR}/.env.production" chmod 600 "${BACKUP_DIR}/.env.production" pass ".env.production 已备份" else warn ".env.production 不存在: $PROJECT/env/.env.production" fi # ---- 5) 备份清单 ---- echo "" echo "--- 5) 备份清单 ---" { echo "# 追光AI 卷备份清单" echo "# 时间: $(date '+%Y-%m-%d %H:%M:%S')" echo "# 标识: $STAMP" echo "" ls -lh "$BACKUP_DIR" | tail -n +2 } > "$BACKUP_DIR/INDEX.txt" ls -lh "$BACKUP_DIR" | tail -n +2 | sed 's/^/ /' # ---- 6) 清理旧备份 ---- echo "" echo "--- 6) 清理 ${KEEP_DAYS} 天前的旧备份 ---" OLD_COUNT=$(find "$BACKUP_ROOT" -maxdepth 1 -type d -mtime +$KEEP_DAYS 2>/dev/null | wc -l) if [ "$OLD_COUNT" -gt 0 ]; then find "$BACKUP_ROOT" -maxdepth 1 -type d -mtime +$KEEP_DAYS -exec rm -rf {} + 2>/dev/null || true pass "清理了 ${OLD_COUNT} 个过期备份" else pass "无需清理" fi # ---- 汇总 ---- echo "" echo "========================================" echo " 备份完成" echo " 结果: ${PASS} 通过 / ${WARN} 警告 / ${FAIL} 失败" if [ "$FAIL" -eq 0 ]; then echo " 🟢 状态: 正常" else echo " 🔴 状态: 有 ${FAIL} 项失败,请检查" fi echo " 路径: $BACKUP_DIR" echo " 总大小: $(numfmt --to=iec ${TOTAL_BYTES} 2>/dev/null || echo "${TOTAL_BYTES} 字节")" echo "========================================" exit $FAIL