前阵子整理照片目录,我把两层文件夹合成一层。同步任务很顺利,另一块盘上的旧目录也随之消失。文件没有丢,但这件事提醒我:一份始终跟着源目录变化的副本,留不住昨天的错误。现在我把镜像同步、历史副本和恢复测试分别记账,再决定 rsync 该带哪些选项。
先确认路径与挂载
以下命令假定源目录是 /srv/notes/,独立备份盘已经挂载在 /mnt/archive,目标目录为 /mnt/archive/notes/。先用 findmnt --mountpoint /mnt/archive 确认这里确实是挂载点,并核对输出的设备和文件系统;如果只是一个普通空目录,就先停止,不要往系统盘里继续写。
findmnt --mountpoint /mnt/archive
df -h /srv/notes /mnt/archive
ls -ld /srv/notes /mnt/archive/notes
源路径末尾的斜杠值得单独检查。/srv/notes/ 表示同步这个目录里面的内容;/srv/notes 通常会在目标下再建立一层 notes 目录。第一次执行时,我会先放一个容易辨认的小文件,确认目标层级,再扩大范围。命令只列出本地盘之间的复制,权限应由实际维护账户明确持有。
把变化摊开看
rsync -a --itemize-changes --dry-run /srv/notes/ /mnt/archive/notes/
rsync -a --itemize-changes /srv/notes/ /mnt/archive/notes/
-a 会递归处理目录,尽量保留常用属性和符号链接,但并不包含硬链接关系、ACL 和扩展属性。需要这些元数据时,要分别评估 -H、-A、-X,同时确认两端文件系统和执行权限是否支持。对于我这份普通文本目录,先保持选项简单更容易检查结果。
rsync 通常用文件大小与修改时间快速判断是否需要传输;这和逐字节核对内容是两件事。--checksum 可以改变比较依据,但会增加两端读取开销。它也不会自动生成历史版本。默认整文件替换通常会先写临时文件再改名,目标盘仍需预留空间,不能只看最后净增加了多少数据。
给误删留出余地
初次建立备份时,我不加删除选项,先观察几轮。确实需要目标和源一致时,再把删除预览单独读一遍。下面演示把被替换和删除的旧文件收进一个有时间戳的目录。时间目录必须与当前镜像分开,不能塞进被同步的目标树里;每次运行也应使用新的名字。
stamp=$(date +%Y%m%d-%H%M%S)
history="/mnt/archive/history/$stamp"
mkdir -p "$history"
rsync -a --delete --backup --backup-dir="$history" \
--itemize-changes --dry-run /srv/notes/ /mnt/archive/notes/
# 读完预览,确认路径、挂载和删除范围后,再去掉试运行选项
rsync -a --delete --backup --backup-dir="$history" \
--itemize-changes /srv/notes/ /mnt/archive/notes/
这个 history 目录只收本轮被覆盖或删除的旧内容,不是某个时间点的完整快照;新建的文件也不会在那里再存一份。要还原一个历史目录树,还需要当前镜像与后续多轮变化记录,实际操作并不轻松。若经常需要按日期恢复整棵树,我会另做文件系统快照或完整的分代副本,而不把这组参数当成版本管理器。
备份任务也不能同时写同一个目标。挂载检查必须成为脚本的前置条件,失败就退出;源目录意外变空时,同样不应继续删除目标。源数据在复制期间仍可能变化,数据库和持续写入的大文件还需要应用自己的导出方式,或者一个状态一致的快照。
从恢复验证开始
restore_dir=$(mktemp -d /tmp/notes-restore.XXXXXX)
rsync -a /mnt/archive/notes/reading.txt "$restore_dir/"
sha256sum /srv/notes/reading.txt "$restore_dir/reading.txt"
这里的 reading.txt 是我预先放进源目录的检查文件;自己的目录应换成真实存在、此时没有被修改的文件。两个校验值一致后,我仍会打开恢复件,看看中文文件名和内容能否正常读取,再抽查一个较大的文件。恢复位置保持独立,避免一次演练反过来覆盖原始数据。
最后记录执行时间、退出码和恢复结果。rsync 的非零退出码需要逐项理解,例如文件在复制过程中消失和目标盘写满,后续处理不同。定期备份只有在能恢复时才有意义,另一块始终连着机器的硬盘,也不能替代隔离保存的副本。