277 lines
12 KiB
Bash
Executable File
277 lines
12 KiB
Bash
Executable File
#!/bin/bash
|
||
# ============================================================
|
||
# dual-backup.sh — 双备份同步脚本
|
||
# 本机 git 版本控制 + 服务器 SMB 远程备份
|
||
# 用法: dual-backup.sh [push|pull|status]
|
||
# push : 本机 → 服务器(默认,增量同步)
|
||
# pull : 服务器 → 本机(恢复场景)
|
||
# status: 查看备份状态
|
||
# ============================================================
|
||
set -e
|
||
|
||
HOME_DIR="$HOME"
|
||
HERMES="$HOME_DIR/.hermes"
|
||
BACKUP_ROOT="/mnt/server-backup/xiaowei"
|
||
BACKUP_CONFIG="$BACKUP_ROOT/config"
|
||
BACKUP_SCRIPTS="$BACKUP_ROOT/scripts"
|
||
BACKUP_WORKFLOWS="$BACKUP_ROOT/workflows"
|
||
BACKUP_DAEMON="$BACKUP_ROOT/daemon"
|
||
BACKUP_SYSTEMD="$BACKUP_ROOT/systemd"
|
||
BACKUP_SNAPSHOTS="$BACKUP_ROOT/snapshots"
|
||
LOG="$HERMES/watchdog/dual-backup.log"
|
||
|
||
mkdir -p "$(dirname "$LOG")"
|
||
log() {
|
||
if [ "${QUIET:-0}" = "1" ]; then
|
||
# cron 静默模式:只写日志文件,不输出 stdout(no_agent 空输出=不推飞书)
|
||
echo "[$(date '+%H:%M:%S')] $*" >> "$LOG"
|
||
else
|
||
echo "[$(date '+%H:%M:%S')] $*" | tee -a "$LOG"
|
||
fi
|
||
}
|
||
|
||
# 检查服务器挂载
|
||
# ⚠️ 2026-08-12 修复:家庭服务器不在同一局域网是常态(走 frp 域名),
|
||
# 离线静默跳过(exit 0),不当作错误上报。只有"挂载点在但同步失败"才报错。
|
||
# ⚠️ 2026-09-04 牧尘指示(铁律):SMB 备份只在家庭局域网做(192.168.123.11)。
|
||
# StarVPN(192.168.188.11) 只用于 Gitea 推送,绝不用来做 SMB 备份——
|
||
# 外网走 StarVPN 传 1.9GB bundle 网络抖动会僵死(9/4 卡 78 分钟教训)。
|
||
# 所以:外网状态一律跳过备份;只有在局域网才挂载 + 同步。
|
||
check_mount() {
|
||
# 判断真实 cifs 挂载(/proc/mounts 为准;mountpoint -q 对 autofs 恒真不可用)
|
||
if grep -q '//192\.168\.123\.11/.* /mnt/server-backup ' /proc/mounts; then
|
||
return 0 # 局域网 cifs 已挂,直接可用
|
||
fi
|
||
# 挂载为外网残留(StarVPN) → 外网不做备份
|
||
if grep -q '//192\.168\.188\.11/.* /mnt/server-backup ' /proc/mounts; then
|
||
log "ℹ️ 挂载为外网残留(StarVPN),外网不做备份(牧尘 09-04 指示),跳过本次备份"
|
||
return 2
|
||
fi
|
||
# 未挂载:只探测局域网 IP。StarVPN 通也不备份。
|
||
log "⚠️ 服务器未挂载,尝试连接(仅局域网)..."
|
||
if ! timeout 3 bash -c 'echo > /dev/tcp/192.168.123.11/445' 2>/dev/null; then
|
||
log "ℹ️ 不在家庭局域网(外网常态),跳过本次备份"
|
||
return 2
|
||
fi
|
||
# 局域网可达:访问挂载点触发 systemd automount(fstab x-systemd.automount)
|
||
ls /mnt/server-backup >/dev/null 2>&1 || true
|
||
sleep 3
|
||
if grep -q '//192\.168\.123\.11/.* /mnt/server-backup ' /proc/mounts; then
|
||
return 0
|
||
fi
|
||
# automount 未触发/未配置 → 尝试 sudo mount(有 NOPASSWD 才生效,失败走跳过)
|
||
sudo -n mount /mnt/server-backup 2>/dev/null || true
|
||
sleep 2
|
||
if grep -q '//192\.168\.123\.11/.* /mnt/server-backup ' /proc/mounts; then
|
||
return 0
|
||
fi
|
||
log "ℹ️ 局域网可达但挂载失败,跳过本次备份"
|
||
return 2
|
||
}
|
||
|
||
# === 推送到服务器 ===
|
||
push_backup() {
|
||
# 2026-09-04 并发锁:cron 每 6h 与手动/重试轮可能重叠,两轮同时写 SMB 互相卡死
|
||
exec 9>"$HERMES/watchdog/dual-backup.lock"
|
||
if ! flock -n 9; then
|
||
log "⚠️ 已有备份在运行,跳过本轮(防并发)"
|
||
return 0
|
||
fi
|
||
log "📤 开始双备份推送..."
|
||
|
||
# 1. 本机 git 快照(无论服务器是否可达都做——本机备份是底线)
|
||
log " 本机快照..."
|
||
bash "$HERMES/scripts/config-protector.sh snapshot" 2>/dev/null || true
|
||
|
||
# 2. 检查服务器(if 条件中的命令不受 set -e 影响,且能拿到真实返回码)
|
||
if check_mount; then
|
||
: # 挂载成功,继续
|
||
else
|
||
local rc=$?
|
||
if [ "$rc" -eq 2 ]; then
|
||
# 离线常态:静默跳过(QUIET 下 stdout 为空 → cron 不推飞书)
|
||
log "ℹ️ 服务器不在局域网,跳过服务器同步(等电脑拿回家自动恢复)"
|
||
return 0
|
||
fi
|
||
log "❌ 跳过备份(服务器离线)"
|
||
return 1
|
||
fi
|
||
|
||
# 3. 同步核心文档
|
||
log " 同步配置..."
|
||
rsync -av --delete "$HERMES/SOUL.md" "$BACKUP_CONFIG/" 2>&1 | tail -1
|
||
rsync -av --delete "$HERMES/AGENTS.md" "$BACKUP_CONFIG/" 2>&1 | tail -1
|
||
rsync -av --delete "$HERMES/MEMORY.md" "$BACKUP_CONFIG/" 2>&1 | tail -1
|
||
rsync -av --delete "$HERMES/USER.md" "$BACKUP_CONFIG/" 2>&1 | tail -1
|
||
rsync -av --delete "$HERMES/CREATIVE.md" "$BACKUP_CONFIG/" 2>&1 | tail -1
|
||
|
||
# 4. 同步核心脚本
|
||
log " 同步脚本..."
|
||
rsync -av --delete "$HERMES/scripts/"*.py "$BACKUP_SCRIPTS/" 2>&1 | tail -1
|
||
rsync -av --delete "$HERMES/scripts/"*.sh "$BACKUP_SCRIPTS/" 2>&1 | tail -1
|
||
|
||
# 5. 同步工作流模板
|
||
log " 同步工作流..."
|
||
rsync -av --delete "$HERMES/workflows/"*.yaml "$BACKUP_WORKFLOWS/" 2>&1 | tail -1
|
||
|
||
# 6. 同步 daemon 状态
|
||
log " 同步 daemon..."
|
||
rsync -av --delete "$HERMES/daemon/solutions.json" "$BACKUP_DAEMON/" 2>&1 | tail -1
|
||
rsync -av --delete "$HERMES/daemon/context.json" "$BACKUP_DAEMON/" 2>&1 | tail -1
|
||
|
||
# 7. 同步 systemd 服务文件
|
||
log " 同步 systemd..."
|
||
rsync -av --delete "$HOME_DIR/.config/systemd/user/xiaowei-daemon.service" "$BACKUP_SYSTEMD/" 2>&1 | tail -1
|
||
rsync -av --delete "$HOME_DIR/.config/systemd/user/xiaowei-startup.service" "$BACKUP_SYSTEMD/" 2>&1 | tail -1
|
||
|
||
# 8. 同步 skills 目录(通过 git bundle,包含全部变更历史)
|
||
log " 同步 skills..."
|
||
if [ -d "$HERMES/.git" ]; then
|
||
cd "$HERMES"
|
||
# 2026-09-04 根治:bundle 先在本地打(快、可靠),再 rsync 单文件到服务器
|
||
# ——之前直写 SMB 会把 git 拖进不可中断 IO,timeout 杀不掉(18:01 卡 50min、20:15 残留 78min)
|
||
# 2026-09-04 修复二:同一天只打一次 bundle(cron 每 6h 一次,文件按天命名会被反复覆盖重传 1.88GB)
|
||
BUNDLE_DAY="$(date '+%Y%m%d')"
|
||
BUNDLE_LOCAL="$HERMES/backups/dual-backup/hermes-git-$BUNDLE_DAY.bundle"
|
||
mkdir -p "$(dirname "$BUNDLE_LOCAL")"
|
||
if [ ! -f "$BUNDLE_LOCAL" ] || [ "$(stat -c %Y "$BUNDLE_LOCAL")" -lt "$(date -d 'today 00:00' +%s)" ]; then
|
||
# 本地打包(写本地盘,不受 SMB 影响;git 读本地 .git 1.9G,通常 1-3 分钟)
|
||
log " git bundle 本地打包中($BUNDLE_LOCAL)..."
|
||
if timeout 300 git bundle create "$BUNDLE_LOCAL" --all 2>/dev/null; then
|
||
log " git bundle 本地完成"
|
||
else
|
||
log " ⚠️ git bundle 本地创建失败(跳过,下次再试)"
|
||
fi
|
||
else
|
||
log " 今日 bundle 已存在,跳过本地打包"
|
||
fi
|
||
# rsync 单文件到服务器(网络差时 rsync 可被 timeout 正常杀掉,不拖死脚本;--partial 支持续传)
|
||
if [ -f "$BUNDLE_LOCAL" ]; then
|
||
timeout 300 rsync -a --partial "$BUNDLE_LOCAL" "$BACKUP_SNAPSHOTS/" 2>/dev/null \
|
||
&& log " 📦 git bundle 已同步" \
|
||
|| log " ⚠️ git bundle rsync 超时/失败(本地保留 $BUNDLE_LOCAL,下次续传)"
|
||
fi
|
||
fi
|
||
|
||
# 9. 同步 cron 配置
|
||
log " 同步 cron..."
|
||
if [ -f "$HERMES/cron/jobs.json" ]; then
|
||
cp "$HERMES/cron/jobs.json" "$BACKUP_ROOT/cron-jobs.json"
|
||
log " cron配置已备份"
|
||
else
|
||
log " ⚠️ 无cron配置可备份"
|
||
fi
|
||
|
||
# 10. 同步织忆数据(SQLite 图谱真数据 53M)
|
||
# 2026-09-04 根治:原 10a 的 LanceDB tar 步骤移除——
|
||
# ① exclude='*.lance' 把真库(memories/episodes.lance 35G)排除了,反而打进 98G broken/bak 垃圾
|
||
# (不以 .lance 结尾不匹配)→ 每次 tar ~95G 垃圾直写 SMB,timeout 600 必失败 → 卡死 cron(18:52)
|
||
# ② zhiyid(213252) 在线运行中,直接 tar 运行中 LanceDB 目录会产出损坏快照(2026-08-21 教训)
|
||
# ③ 35G 真库 tar+rsync 过 StarVPN 远超 cron 60min 预算 → LanceDB 全量备份需独立低频任务
|
||
# TODO: 等 zhiyid 提供 snapshot/backup API 或独立 cron rsync LanceDB 目录(排除 broken/bak)
|
||
log " 同步织忆数据..."
|
||
# SQLite 图谱真数据(53M,~/.hermes/graph.db;绕过软链接直接从 ~/.hermes/ 取)
|
||
if [ -f "$HERMES/graph.db" ] || [ -f "$HERMES/memoryweave.db" ]; then
|
||
mkdir -p "$HERMES/backups/dual-backup"
|
||
SQL_LOCAL="$HERMES/backups/dual-backup/zhiyi-sqlite-$(date '+%Y%m%d').tar.gz"
|
||
timeout 120 tar czf "$SQL_LOCAL" -C "$HERMES" graph.db memoryweave.db 2>/dev/null \
|
||
&& log " 📦 织忆SQLite已打包" \
|
||
|| log " ⚠️ 织忆SQLite打包失败"
|
||
if [ -f "$SQL_LOCAL" ]; then
|
||
timeout 120 rsync -a --partial "$SQL_LOCAL" "$BACKUP_SNAPSHOTS/" 2>/dev/null \
|
||
&& log " 📦 织忆SQLite已同步" \
|
||
|| log " ⚠️ 织忆SQLite rsync 失败"
|
||
fi
|
||
fi
|
||
|
||
# 11. 定期快照(每天保留一份关键配置快照)
|
||
SNAPSHOT_NAME="snapshot-$(date '+%Y%m%d')"
|
||
if [ ! -d "$BACKUP_SNAPSHOTS/$SNAPSHOT_NAME" ]; then
|
||
mkdir -p "$BACKUP_SNAPSHOTS/$SNAPSHOT_NAME"
|
||
cp "$HERMES/SOUL.md" "$BACKUP_SNAPSHOTS/$SNAPSHOT_NAME/"
|
||
cp "$HERMES/daemon/solutions.json" "$BACKUP_SNAPSHOTS/$SNAPSHOT_NAME/" 2>/dev/null || true
|
||
log " 📸 每日快照: $SNAPSHOT_NAME"
|
||
fi
|
||
|
||
log "✅ 双备份推送完成"
|
||
# 服务器同步成功才输出(cron 收到确认,牧尘拿回家后能看到)
|
||
if [ "${QUIET:-0}" = "1" ]; then
|
||
echo "✅ 双备份推送完成(服务器已连接)"
|
||
fi
|
||
}
|
||
|
||
# === 从服务器恢复 ===
|
||
pull_backup() {
|
||
log "📥 从服务器恢复..."
|
||
|
||
if ! check_mount; then
|
||
log "❌ 跳过恢复(服务器离线)"
|
||
return 1
|
||
fi
|
||
|
||
# 检查备份是否存在
|
||
if [ ! -f "$BACKUP_CONFIG/SOUL.md" ]; then
|
||
log "❌ 服务器上无备份数据"
|
||
return 1
|
||
fi
|
||
|
||
# 反向同步(服务器 → 本机)
|
||
rsync -av --delete "$BACKUP_CONFIG/" "$HERMES/" 2>&1 | tail -1
|
||
rsync -av --delete "$BACKUP_SCRIPTS/" "$HERMES/scripts/" 2>&1 | tail -1
|
||
rsync -av --delete "$BACKUP_WORKFLOWS/" "$HERMES/workflows/" 2>&1 | tail -1
|
||
rsync -av "$BACKUP_DAEMON/solutions.json" "$HERMES/daemon/" 2>&1 | tail -1
|
||
|
||
log "✅ 恢复完成"
|
||
}
|
||
|
||
# === 查看状态 ===
|
||
show_status() {
|
||
echo "=== 双备份状态 ==="
|
||
echo ""
|
||
|
||
if mountpoint -q /mnt/server-backup; then
|
||
echo "🟢 服务器: 已连接"
|
||
echo " 路径: /mnt/server-backup/xiaowei"
|
||
echo " 空间: $(df -h /mnt/server-backup | awk 'NR==2 {print $3 "已用 / " $2 " (" $5 ")"}')"
|
||
echo ""
|
||
echo " 备份内容:"
|
||
for dir in config scripts workflows daemon systemd snapshots; do
|
||
count=$(find "/mnt/server-backup/xiaowei/$dir" -maxdepth 1 -type f 2>/dev/null | wc -l)
|
||
last=$(ls -lt "/mnt/server-backup/xiaowei/$dir" 2>/dev/null | head -2 | tail -1 | awk '{print $6, $7, $8}')
|
||
echo " $dir: $count 个文件 (最新: ${last:-无})"
|
||
done
|
||
else
|
||
echo "🔴 服务器: 未连接"
|
||
fi
|
||
|
||
echo ""
|
||
echo "=== 本机 git 状态 ==="
|
||
cd "$HERMES"
|
||
echo " 最近提交: $(git log --oneline -1 2>/dev/null || echo '无')"
|
||
echo " 未跟踪: $(git status --short 2>/dev/null | wc -l) 个"
|
||
echo " 稳定标记: $(git tag -l 'stable-*' 2>/dev/null | wc -l) 个"
|
||
|
||
echo ""
|
||
echo "=== Gitea ==="
|
||
curl -sf --max-time 3 http://192.168.123.11:3000/api/v1/repos/xiaoxue_admin/xiaowei-system 2>/dev/null \
|
||
| python3 -c "import json,sys; d=json.load(sys.stdin); print(f' 仓库: {d.get(\"name\",\"?\")} | 更新: {d.get(\"updated_at\",\"?\")[:10]}')" 2>/dev/null \
|
||
|| echo " 仓库: xiaowei-system (离线)"
|
||
}
|
||
|
||
|
||
# === 主入口 ===
|
||
case "${1:-push}" in
|
||
push)
|
||
push_backup
|
||
;;
|
||
pull)
|
||
pull_backup
|
||
;;
|
||
status)
|
||
show_status
|
||
;;
|
||
*)
|
||
echo "用法: dual-backup.sh [push|pull|status]"
|
||
;;
|
||
esac
|