Seafile 云盘备份到本地磁盘:从 SeaDrive+rsync 踩坑到年度快照方案
背景
一直以来我用 SeaDrive + rsync 的组合来备份 Seafile 云盘数据到本地磁盘。但用久了发现这个方案有一个根本性问题,值得写出来给同样在用 Seafile 的朋友避坑。
问题:SeaDrive 是虚拟盘,rsync 备份的可能只是"影子"
SeaDrive 的工作模式是 on-demand(按需下载):本地默认只保存"影子文件"(占位符),真正的文件内容要在被访问时才会从服务器拉取下来。
这意味着直接对 SeaDrive 挂载目录跑 rsync,会遇到两难:
- 要么触发大量按需下载:rsync 遍历目录时把所有影子文件的实际内容都拉取一遍,速度慢、流量大,尤其是移动网络或跨境访问服务器时体验很差;
- 要么根本没备份到完整内容:如果不触发下载,rsync 复制到的可能只是占位符,不是真实文件,备份形同虚设。
这也是我决定换方案的直接原因。
候选方案对比
| 方案 | 说明 | 适用场景 |
|---|---|---|
| seaf-cli | 官方命令行同步客户端,本地是真实文件,非虚拟盘 | 命令行环境、树莓派等 headless 设备 |
| seafile-client(同步模式) | 官方桌面客户端,选"同步"而非"虚拟盘" | 有图形界面的常驻电脑 |
| rclone + WebDAV | 利用 Seafile 的 WebDAV 接口做同步/备份 | 想要多目标备份(同时备份到 S3、OneDrive 等)的场景 |
| 强制落盘后 rsync | 先遍历触发 SeaDrive 全量下载,再 rsync | 不推荐,效率低、逻辑 tricky |
最终选择基于官方 seaf-cli,因为它本身就是同步工具,不需要再叠加 rsync 这一层,依赖轻,适合脚本化和定时任务。
我的场景:另一台电脑 + 移动硬盘,每年备份一次
具体需求是:
- 不影响 Seafile 服务器端的数据;
- 用一台额外的电脑,接上移动硬盘做本地留档;
- 备份频率很低——一年一次。
关键坑:官方客户端默认是双向同步
无论是 seafile-client 桌面版还是 seaf-cli,默认都是双向同步。这意味着如果备份电脑上误删了文件,或者移动硬盘故障导致文件丢失/损坏,这个"删除"或"损坏"状态会被同步推回服务器,造成反向污染——这是这套方案里最容易被忽视、也最危险的一点。
解决思路:单向"快照式"备份
不做长期挂载的双向同步,而是把每次备份当作一次性的"拉取快照":
# 每年备份时执行
seaf-cli sync -l <repo-id> -s https://your-server -u user -p pass -d /path/to/backup
# 同步完成后立刻断开
seaf-cli desync -d /path/to/backup
这样处理后,移动硬盘在平时(也就是一年中的绝大部分时间)是完全离线、与服务器无任何连接的状态,即使备份电脑本身出问题(误操作、勒索软件等),也不会波及服务器数据。风险被压缩在"一年一次、几十分钟到几小时"的备份窗口内。
完整脚本:轮询等待 + 超时保护 + 重试机制
考虑到大库同步可能要跑很久,简单的 sleep N 猜测等待时间并不可靠,所以脚本里加了几个健壮性设计:
- 轮询检测同步状态,而不是固定等待时间
- 超时保护(默认 6 小时),避免异常情况下无限挂起
- 失败自动重试(默认 3 次)
- 信号捕获:即使中途 Ctrl+C 或脚本崩溃,也会自动断开同步、清理现场,不会留下"半连接"状态
- 密码运行时输入,不写死在脚本里
- 按年份建目录,历史快照互不覆盖,方便追溯
#!/bin/bash
#
# Seafile 年度备份脚本(带轮询等待 + 超时保护 + 重试机制)
# 用法: ./seafile_yearly_backup.sh
#
# 设计原则:
# 1. 每次运行只做"拉取快照",同步完成后立即 desync + stop
# 2. 平时(脚本运行窗口之外)本地目录与服务器完全无连接
# 3. 按年份建目录,历史快照互不覆盖
#
set -euo pipefail
# ============ 配置区(根据实际情况修改) ============
SERVER="https://your-seafile-server"
USER="[email protected]"
BACKUP_BASE_DIR="/Volumes/BackupDisk" # 移动硬盘挂载路径
CONF_DIR="$HOME/.seafile-backup-conf" # seaf-cli 配置/状态目录
# 要备份的资料库 repo-id 列表(用 seaf-cli list-remote 获取)
REPOS=(
"repo-id-1"
"repo-id-2"
)
# 轮询设置
POLL_INTERVAL=30 # 每次检查间隔(秒)
MAX_WAIT_SECONDS=21600 # 最长等待时间,默认6小时,超时则报警退出
MAX_RETRIES=3 # 单个库同步失败后的最大重试次数
# ============ 配置区结束 ============
YEAR=$(date +%Y)
BACKUP_ROOT="${BACKUP_BASE_DIR}/seafile-backup-${YEAR}"
LOG_FILE="${BACKUP_ROOT}/backup_log_$(date +%Y%m%d_%H%M%S).txt"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"
}
fail() {
log "❌ 错误: $*"
cleanup_and_exit 1
}
cleanup_and_exit() {
local exit_code=$1
log "开始清理:断开所有已同步的库并停止 seaf-cli..."
for REPO in "${REPOS[@]}"; do
local target_dir="${BACKUP_ROOT}/${REPO}"
if [ -d "$target_dir" ]; then
seaf-cli desync -d "$target_dir" 2>/dev/null || true
fi
done
seaf-cli stop 2>/dev/null || true
log "清理完成,退出码: $exit_code"
exit "$exit_code"
}
# 捕获中断信号(Ctrl+C、异常退出等),确保不会留下挂着的同步连接
trap 'log "收到中断信号"; cleanup_and_exit 1' INT TERM
# ------------ 预检查 ------------
if ! command -v seaf-cli &> /dev/null; then
echo "错误: 未找到 seaf-cli,请先安装 (apt install seafile-cli 或 brew install seaf-cli)"
exit 1
fi
if [ ! -d "$BACKUP_BASE_DIR" ]; then
echo "错误: 移动硬盘路径不存在: $BACKUP_BASE_DIR"
echo "请确认硬盘已挂载"
exit 1
fi
mkdir -p "$BACKUP_ROOT"
touch "$LOG_FILE"
log "========================================="
log "Seafile 年度备份开始"
log "备份目录: $BACKUP_ROOT"
log "========================================="
# 密码运行时输入,不写死在脚本中
if [ -z "${SEAFILE_PASSWORD:-}" ]; then
read -rsp "请输入 Seafile 密码: " SEAFILE_PASSWORD
echo
fi
mkdir -p "$CONF_DIR"
seaf-cli init -d "$CONF_DIR" 2>/dev/null || true
seaf-cli start
sleep 2
# ------------ 轮询等待单个库同步完成 ------------
# 返回值: 0=成功, 1=超时, 2=同步出错
wait_for_sync() {
local repo_id=$1
local target_dir=$2
local elapsed=0
while [ "$elapsed" -lt "$MAX_WAIT_SECONDS" ]; do
local status_output
status_output=$(seaf-cli status 2>/dev/null || echo "")
# 检查该库是否还在同步中
if echo "$status_output" | grep -q "$repo_id"; then
local repo_line
repo_line=$(echo "$status_output" | grep "$repo_id" || true)
if echo "$repo_line" | grep -qiE "synchronized|up to date|up-to-date"; then
return 0
elif echo "$repo_line" | grep -qiE "error|failed"; then
log " ⚠ 检测到同步错误状态: $repo_line"
return 2
fi
# 否则认为仍在同步中(如 "syncing", "downloading" 等),继续等待
else
# seaf-cli status 中已找不到该库条目,通常表示同步完成或已完成初始化
# 做一次额外确认:目录是否存在且非空
if [ -d "$target_dir" ] && [ "$(ls -A "$target_dir" 2>/dev/null)" ]; then
return 0
fi
fi
sleep "$POLL_INTERVAL"
elapsed=$((elapsed + POLL_INTERVAL))
log " ...同步中,已等待 ${elapsed}s / ${MAX_WAIT_SECONDS}s"
done
return 1 # 超时
}
# ------------ 主同步流程(带重试) ------------
FAILED_REPOS=()
SUCCESS_REPOS=()
for REPO in "${REPOS[@]}"; do
TARGET_DIR="${BACKUP_ROOT}/${REPO}"
log "-----------------------------------------"
log ">>> 开始同步库: $REPO"
log " 目标目录: $TARGET_DIR"
ATTEMPT=1
REPO_OK=false
while [ "$ATTEMPT" -le "$MAX_RETRIES" ]; do
log " 第 $ATTEMPT 次尝试..."
if seaf-cli sync -l "$REPO" -s "$SERVER" -u "$USER" -p "$SEAFILE_PASSWORD" -d "$TARGET_DIR" 2>>"$LOG_FILE"; then
wait_for_sync "$REPO" "$TARGET_DIR"
RESULT=$?
if [ "$RESULT" -eq 0 ]; then
log " ✅ 库 $REPO 同步成功"
REPO_OK=true
break
elif [ "$RESULT" -eq 1 ]; then
log " ⏱ 库 $REPO 同步超时(超过 ${MAX_WAIT_SECONDS}s)"
else
log " ⚠ 库 $REPO 同步过程中报错"
fi
else
log " ⚠ seaf-cli sync 命令执行失败"
fi
# 失败后先 desync 清理,再决定是否重试
seaf-cli desync -d "$TARGET_DIR" 2>/dev/null || true
ATTEMPT=$((ATTEMPT + 1))
[ "$ATTEMPT" -le "$MAX_RETRIES" ] && sleep 10
done
if [ "$REPO_OK" = true ]; then
SUCCESS_REPOS+=("$REPO")
# 成功后立即断开,缩短联网窗口
seaf-cli desync -d "$TARGET_DIR" 2>/dev/null || true
log " 已断开 $REPO 的同步连接"
else
FAILED_REPOS+=("$REPO")
log " ❌ 库 $REPO 在 $MAX_RETRIES 次尝试后仍然失败"
fi
done
# ------------ 收尾 ------------
seaf-cli stop 2>/dev/null || true
unset SEAFILE_PASSWORD
log "========================================="
log "备份任务结束"
log "成功: ${#SUCCESS_REPOS[@]} 个库 (${SUCCESS_REPOS[*]:-无})"
log "失败: ${#FAILED_REPOS[@]} 个库 (${FAILED_REPOS[*]:-无})"
log "备份位置: $BACKUP_ROOT"
log "日志文件: $LOG_FILE"
log "========================================="
if [ "${#FAILED_REPOS[@]}" -gt 0 ]; then
echo ""
echo "⚠ 部分库备份失败,请检查日志: $LOG_FILE"
exit 1
fi
echo ""
echo "✅ 全部备份成功完成"
exit 0
使用方法
1. 安装 seaf-cli
# Debian/Ubuntu
sudo apt install seafile-cli
# macOS
brew install seafile-client seaf-cli
2. 获取要备份的资料库 repo-id
seaf-cli list-remote -s https://your-seafile-server -u [email protected]
3. 修改脚本配置区
SERVER、USER:Seafile 服务器地址和账号BACKUP_BASE_DIR:移动硬盘挂载路径(Linux 类似/media/username/BackupDisk,macOS 是/Volumes/BackupDisk)REPOS:填入上一步获取到的 repo-id 列表
4. 赋予执行权限并运行
chmod +x seafile_yearly_backup.sh
./seafile_yearly_backup.sh
密码会在运行时以隐式方式输入,不落盘。如果想跳过交互(比如接入 cron),可以提前 export SEAFILE_PASSWORD=xxx。
小结
| 要点 | 说明 |
|---|---|
| 别用虚拟盘做 rsync 备份源 | SeaDrive 的"影子文件"机制会让 rsync 备份不完整或效率低下 |
| 优先用官方同步工具 | seaf-cli / seafile-client 的同步模式本地就是真实文件 |
| 警惕默认的双向同步 | 备份电脑上的误删/损坏可能被同步回服务器 |
| 低频备份用"快照式"而非长期挂载 | 同步完立即 desync,缩短暴露窗口,离线时零风险 |
| 脚本要考虑异常情况 | 超时保护、重试、信号捕获,避免"半成品"备份或悬挂状态 |
这套方案目前用在我自己的场景里:一台闲置电脑 + 移动硬盘,每年跑一次,跑完硬盘直接拔下离线保存。如果你的备份频率更高(比如每周/每天),更适合用**方案 A:只读同步(download-only)**长期挂载,而不是这种一次性快照方式,具体可以根据自己的风险偏好选择。