xiaowei-system/scripts/dual-backup.sh

277 lines
12 KiB
Bash
Executable File
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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 静默模式:只写日志文件,不输出 stdoutno_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 automountfstab 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 拖进不可中断 IOtimeout 杀不掉18:01 卡 50min、20:15 残留 78min
# 2026-09-04 修复二:同一天只打一次 bundlecron 每 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 垃圾直写 SMBtimeout 600 必失败 → 卡死 cron18: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