filesystemwatcher仅通知变更事件,无法回滚;必须在事件触发时立即读取并持久化文件内容、元数据及路径结构,生成带哈希和时间戳索引的快照,再通过反向diff实现原子覆盖式回滚。

用 FileSystemWatcher 捕获变更不等于快照
很多人一上来就加 FileSystemWatcher,以为监听到创建/删除/修改就能“回滚”。错。它只告诉你“发生了什么”,不保存“变成什么样”。没内容快照,回滚就是空谈。
真正要回滚到某个时间点,你得在变更发生前或发生后,把文件的二进制内容、元数据(时间戳、权限)、路径结构都存下来——而且得带时间戳索引。否则你连“那个时间点的 config.json 长啥样”都说不清。
- 别只监听事件,要在事件触发时立刻读取并持久化目标文件(
File.ReadAllBytes(path)),不是只记个日志 - 避免在
Changed事件里直接读大文件,容易丢事件;建议用Created/Changed触发异步快照任务,加队列限流 - Windows 上注意
FileSystemWatcher对重命名和移动的报告不一致,Renamed事件里OldFullPath和NewFullPath才是关键,漏掉就断链
快照存储必须带版本哈希和路径快照树
单纯按时间建文件夹(如 2024-05-20_14-30-00/)存一堆副本?空间爆炸,且查不到“这个目录当时有哪些子项”。你需要两层结构:一是每个快照点有唯一 ID(推荐用 UTC 时间戳 + 随机短哈希,如 20240520143000_abc123),二是每个快照内用轻量级 JSON 记录该时刻的完整路径状态树。
示例快照元数据 snapshot_20240520143000_abc123.manifest.json:
{
"timestamp": "2024-05-20T14:30:00Z",
"root": "C:\project",
"entries": [
{
"path": "config.json",
"hash": "a1b2c3...",
"size": 2048,
"lastWriteTimeUtc": "2024-05-20T14:29:55Z",
"isDirectory": false
},
{
"path": "src/",
"hash": null,
"size": 0,
"lastWriteTimeUtc": "2024-05-20T14:28:10Z",
"isDirectory": true
}
]
}- 文件内容用 SHA256 哈希去重,相同内容只存一份(
data/a1b2c3...bin),多个快照引用同一 hash - 目录不存内容,但必须记录其存在性、时间戳、子项列表(哪怕为空)——否则回滚时会漏删多余文件
- 别用
DateTime.Now记时间,统一用DateTime.UtcNow,避免本地时区切换导致时间乱序
回滚操作本质是「反向 diff + 原子覆盖」
回滚不是“把整个快照目录拷回去”。你要对比当前状态和目标快照状态,算出三类操作:该删的(当前有、快照无)、该覆的(当前有、快照有但 hash 不同)、该加的(当前无、快照有)。顺序错了会出问题——比如先删父目录再加子文件,直接失败。
核心逻辑在 RollbackToSnapshot(string snapshotId) 里:
- 先递归收集当前磁盘上所有路径(用
Directory.GetFiles(root, "*", SearchOption.AllDirectories)),生成当前状态快照树 - 加载目标
snapshotId.manifest.json,做路径集合差分:用HashSet<string>.ExceptWith()</string>找出待删路径 - 对共有的文件,比较 manifest 中的 hash 和当前文件实际 hash(
ComputeFileHash(path)),不一致才触发覆盖 - 所有写操作前调用
File.SetAttributes(path, FileAttributes.Normal),否则只读/隐藏文件会拒绝覆盖 - 最后一步:删多余目录必须从深到浅(
OrderByDescending(p => p.Length)),否则父目录先删,子项删不了
硬链接快照(Windows)或 reflink(Linux/macOS)不是 C# 原生能干的事
有人搜“C# 文件系统快照”,其实是想抄 ZFS/Btrfs 那套。但 .NET 运行时根本不暴露底层 copy-on-write 接口。Windows 的 fsutil hardlink 或 mklink /j 是命令行工具,Linux 的 cp --reflink 是 shell 调用——C# 只能通过 Process.Start() 调,且跨平台极难对齐语义。
这意味着:你写的“快照”本质上是带索引的副本管理器,不是内核级快照。性能瓶颈不在 C#,而在磁盘 I/O 和哈希计算。别幻想零拷贝。
- 大文件(>100MB)建议跳过实时哈希,改用
FileInfo.Length+LastWriteTimeUtc粗略比对,再按需全量校验 - SSD 上频繁写小快照(每秒多次)会导致 IO 毛刺,加内存缓存最近 3 个快照的 manifest,减少磁盘读
- 别让回滚操作阻塞主线程——UI 冻结或服务超时都是因为你在同步执行
File.Copy()几 GB 的文件
最常被忽略的一点:没有事务日志。如果快照写到一半程序崩溃,manifest 写了但部分文件没存完,这个快照就不可用。要么加写前预占位(File.Create("xxx.tmp").Close()),要么用原子 rename 提交(先写 tmp_manifest.json,再 File.Move() 成正式名)。










