Dotr: 一个简单的配置备份工具

这周我的 Mac 突然无法开机了,睡午觉之前我没插电让它继续跑程序,回来后就发现已经黑屏,无论如何充电都无法开机。用了 14 年 Mac 第一次出现这种情况,麻烦的是我暑假在湖南老家待着,发现整个地级市都没办法修复 Mac,无奈寄去长沙的苹果售后修理。

但这两天我还有些工作需要处理,所以就在家人的 PC 机上开始重建我的工作流程和工具。这时候才想起,我应该是有份比较新的配置备份。然后直接从 Github 的私有库上拉取下来,交给 AI 帮我恢复配置,一切都方便多了。

完成这个备份的小工具是 dotr,几个月之前我 vibe coding 的。

那次的契机也是自己一个命令行失误,导致 .zshrc 完全覆盖了。我最早的备份就是一个 Git 仓库,把 .zshrc.gitconfig、编辑器配置这些文件手工复制进去。后来用过各种 dotfiles 管理工具,也折腾过 symlink、bootstrap 脚本、chezmoi/yadm 这类更完整的方案。

这些工具都还行,但有的地方又不符合我的使用习惯,我的需求其实更偏“备份”而不是“配置部署系统”。我不太想把 home 目录变成一套复杂的模板工程,也不想每加一个配置就思考它应该如何 symlink、如何跨机器自动恢复。很多时候我只是想:这个文件对我有用,把它安全地放进一个 Git 仓库里,以后坏了能找回来。

于是写了一个小工具,发布到 crates.io 的 crate 叫 dotr-cli,安装后命令是 dotr

cargo install dotr-cli

备份

dotr 的核心思路很简单:把你指定的文件和目录复制到一个 Git 仓库里,同时保存一份 metadata,方便以后 restore。

初始化一个仓库:

dotr init ~/dotbackup --with-defaults --set-default

仓库结构大概是这样:

dotr.toml
files/
  home/
metadata/
  index.json

~/.zshrc 会被存到:

files/home/.zshrc

如果是 $HOME 之外的绝对路径,则会放到:

files/root/Library/example/hello/world

这个设计看起来朴素,但我挺喜欢。它没有要求真实文件必须变成 symlink,也不要求原来的目录结构适配工具。源文件仍然在它原来的位置,dotr 只是按配置把它们复制到备份仓库。

配置要短

dotfiles 备份最麻烦的地方,是每个应用目录都不一样。有的目录里只有配置,有的目录混着缓存、session、数据库、token、日志和临时文件。粗暴地备份整个 .config 基本等于把垃圾也一起提交了。

所以 dotr 的配置主要围绕 [[path]][[path_set]]

[[path_set]]
base = "~"
items = [
  ".gitconfig",
  { src = ".config/nvim", include = ["init.lua", "lua/**", "after/**"] },
  { src = ".config/some-app", include = ["config.toml", "assets/**"], include_binary_file = true },
]

path_set 是为了让常见的 home-relative 配置写起来更短。字符串等价于 { src = "..." },table 则可以继续加 includeexcludeencryptforce 等字段。

默认策略会跳过缓存、日志、本地数据库、session、build output、临时文件、环境变量文件、私钥等内容。如果一个目录里只有几个文件值得备份,我更推荐显式写 include,而不是整个目录打包。

status 和 backup

常用命令很少:

dotr status
dotr backup
dotr restore --diff ~/.ssh/config
dotr restore --apply ~/.zshrc

status 本质上是一次 dry-run backup:扫描配置的 source,比较当前文件和仓库里的备份,然后告诉你会新增、修改、删除什么。backup 则真正把变化写进去。

如果开启 Git 自动提交:

[git]
auto_commit = true
auto_push = false
commit_message = "chore(dotr): automated backup"

dotr 只会 stage 自己管理的路径:dotr.tomlfiles/metadata/recipients.gitignore。这个细节很重要,因为 backup repo 里可能还有你手工放的 README 或其他文件,工具不应该顺手把用户未准备好的东西一起提交。

dotr add 也会顺手跑一次 scoped backup:

dotr add ~/.config/yazi
dotr add --encrypt ~/.npmrc
dotr add --force /Library/Logs/MCXTools.log

如果这次 add 实际不会备份任何东西,比如被默认规则全部跳过,它会失败,并给出 dotr add --force PATH 的提示,而不是悄悄把一条无效配置写进 dotr.toml

restore 默认保守

备份工具还有一个危险操作:恢复。

dotr 的 restore 默认是 dry-run,必须加 --apply 才会写回原位置:

dotr restore ~/.zshrc          # 只预览
dotr restore --apply ~/.zshrc  # 真正恢复

如果目标文件已经存在,而且内容不同,默认不会覆盖,除非显式加 --force。如果只是想把某个加密文件临时解出来看看,也可以用 -o 输出到另一个路径:

dotr restore -o /tmp/ssh-config ~/.ssh/config

对于 $HOME 之外的绝对路径,恢复更严格,必须同时给 --apply--allow-absolute

dotr restore --apply --allow-absolute /Library/example/hello/world

我自己写这类工具时很在意“危险动作”。backup 可以勤快一点,restore 应该慢一点。尤其是配置文件,一次错误覆盖可能让一个环境很难排查。

age 加密文件

很多配置文件里会混进 token、内部 URL、账号名、私有 endpoint。dotfiles 仓库如果要推到远程,这些东西就很尴尬。

dotr 支持对单个 path 显式开启 age 加密:

dotr keygen
dotr add --encrypt ~/.npmrc

对应配置大概是:

[encryption]
backend = "age"
recipients_file = "recipients"
identity = "~/.config/dotr/identity"

[[path]]
src = "~/.npmrc"
encrypt = true

这里没有依赖系统安装的 age 命令,而是直接用 Rust 的 age 实现完成 keygen、backup 和 restore。recipients 是公钥,可以进仓库;~/.config/dotr/identity 是私钥,必须像密码一样保存,不能提交。

我没有让 dotr 自动猜哪些文件应该加密。秘密的边界很难可靠推断,自动猜错反而危险。它只提供机制:你明确说这个 path 要加密,它才加密。

提醒:如果一个 secret 已经以明文进过 Git 历史,后来把 encrypt = true 打开,只能保护未来的备份文件,不能让历史记录自动消失。该 rotate 的 secret 还是要 rotate。

normalize 去掉琐碎的备份

现实里的配置文件经常有一些“每次运行都会变”的字段:时间戳、计数器、last refresh、last seen。它们变化了,但并不代表这个配置有备份价值。如果每次 backup 都因为这些字段产生 diff,最后 Git 历史会很吵。

dotr 支持对 matched 文件做 compare-only normalization:

[[path]]
src = "~/.config/some-app"
include = ["config.toml"]
normalize = { match = "config.toml", drop_paths = ["runtime.last_updated"] }

它的关键点是 compare-only:不会改写源文件,也不会把备份仓库里的文件改成删字段后的版本。它只是在比较 hash 时,先把指定字段 drop 掉。如果只有这些字段变化,backup 就认为没有值得写入的新版本;如果后来真正的配置字段变化了,dotr 仍然会复制当前原始文件。

目前支持 TOML、JSON、txt、conf 的识别,TOML/JSON 可以按 dot path 删除字段,也支持 * 匹配一层对象,比如:

drop_paths = ["marketplaces.*.last_updated"]

watch 和 daemon

如果只靠手动运行 dotr backup,最后大概率会忘。

所以 dotr 有 watch 模式:

dotr watch

它会监听配置里的 source path,做 debounce,再按 backup_interval_secs 控制备份频率。也可以用 daemon 命令在后台跑:

dotr daemon start
dotr daemon status
dotr daemon restart
dotr daemon stop

这里我没有做 systemd unit 或 launchd plist。dotr daemon start 会解析 repo,写一个用户级 daemon config,记录当前 dotr executable 和 repo,然后启动 dotr --repo watch 到后台。日志默认写到:

~/.local/state/dotr/dotr-watch.log

自定义备份命令

有些东西不是直接复制现有文件就能备份的。比如 Homebrew 包列表、VS Code 扩展列表、某些工具的导出配置。

dotr 用 [[custom_backup]] 处理这类场景:

[[custom_backup]]
name = "homebrew"
backup = "brew bundle dump --file ~/.config/homebrew/Brewfile --force"
restore = "brew bundle --file ~/.config/homebrew/Brewfile"
paths = ["~/.config/homebrew/Brewfile"]

backup 前先跑 backup 命令,生成文件后再扫描;restore 时文件恢复后再跑 restore 命令。

这让我不用把 Homebrew、VS Code、各种工具的“导出命令”写进单独脚本里,dotr 本身就能描述这个备份关系。

和已有 dotfiles 工具的区别

如果你需要跨机器模板、条件渲染、复杂 bootstrap、secret manager 集成,那些成熟工具可能更适合。dotr 更像一个保守的 copy-based backup tool:我列出想备份的东西,它定期复制到仓库。它也提供恢复功能,但能力比较基础。就我这次的实际体验而言,直接把备份交给 AI 恢复更方便,AI 还能顺手处理一些跨平台问题。

添加评论
点赞收藏
点踩分享查看原文
评论
?
参与讨论