Files
zhuiguang-ai/scripts/backup-volumes.sh
T

138 lines
4.1 KiB
Bash

#!/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