在这里插入图片描述

前面几期把目录折叠、过滤、差异块操作和编码保真陆续做完后,这个文件对比工具已经可以用于日常检查文件了。

但真正把它拿来同步项目目录时,还有一个风险一直没有处理:点击“同步到右侧”或“同步到左侧”以后,程序只弹一句确认,然后直接覆盖所有不同文件。

旧版核心代码基本就是这样:

Files.copy(source, target,
        StandardCopyOption.REPLACE_EXISTING,
        StandardCopyOption.COPY_ATTRIBUTES);

一两个文件问题不大。目录里有几十个差异时,使用者看不到将新增哪些文件、覆盖哪些文件,也不能临时取消某一项。更麻烦的是,如果第 12 个文件复制失败,前 11 个已经写进目标目录,后面的没有执行,程序只告诉你“同步失败”。

哪些成功了、目标文件原来是什么、能不能恢复,都不知道。

所以这期继续做开发计划里的 P1:给目录同步加上预览、默认备份、安全写入、失败结果和事务回滚。

这里的目标不是把一个本地文件复制功能包装成复杂系统,而是把所有会覆盖磁盘内容的步骤说清楚、记下来,并且尽量留出恢复手段。

旧同步流程为什么不够用

原来的同步规则其实很明确:

  • 左右相同的文件跳过。
  • 来源侧有、目标侧没有时新增。
  • 两侧都有但 Hash 不同时覆盖目标。
  • 来源侧没有时跳过,不删除目标侧文件。

问题不在规则,而在执行方式。

1. 对比结果不等于执行时状态

目录对比完成后,使用者可能先打开文件看内容,也可能让构建程序继续运行。等几分钟后再点击同步,来源文件和目标文件都可能已经变化。

如果程序仍按几分钟前的对比结果覆盖,就会把别人刚写入的内容抹掉。

2. 直接写目标文件容易留下半套结果

多文件同步没有文件系统级事务。不能像数据库一样,把 20 个文件放在一个事务里,要么全部提交,要么全部撤销。

即使单个 Files.copy 成功,下一项也可能因为文件占用、权限、磁盘空间或路径问题失败。程序必须承认这种现实,记录哪些已经完成,而不是把整批任务简单归为“成功”或“失败”。

3. 覆盖之后没有恢复依据

目标文件一旦被 REPLACE_EXISTING 替换,旧内容就没有了。没有备份文件,也没有一份事务清单记录原路径和新文件 Hash,所谓“撤销同步”就只能停留在按钮文案上。

先把同步变成一份计划

这次没有直接改原来的复制循环,而是先增加 SyncPlan

主界面仍然使用当前 CompareResult 展示左右差异。用户点击某个同步方向后,程序把结果转换成一份不可变计划:

SyncPlan
├── transactionId
├── direction
├── sourceRoot
├── targetRoot
├── entries
└── excludedCount

每个 SyncPlanEntry 记录:

  • 相对路径。
  • 动作:新增、覆盖、创建目录或跳过。
  • 来源路径和目标路径。
  • 来源与目标的文件指纹。
  • 文件大小和跳过原因。

方向规则继续保持对称。以左侧同步到右侧为例:

当前状态同步动作
内容相同跳过
内容不同用左侧覆盖右侧
仅左侧在右侧新增
仅右侧跳过,不删除右侧

这一期仍然不做镜像删除。目标侧独有文件只显示“来源侧不存在,不删除目标文件”,不会因为用户点了同步就被清理。

点击同步后,先看清楚再写盘

旧版的简单确认框被同步预览窗口替换。

在这里插入图片描述

预览页显示:

  • 同步方向。
  • 来源根目录和目标根目录。
  • 新增、覆盖、跳过数量。
  • 相对路径、文件大小和修改时间。
  • 已选择文件数量、复制大小和预计备份大小。

新增和覆盖默认勾选,相同文件与来源侧缺失项只能查看,不能执行。使用者可以取消某个文件,也可以按“全部、将执行、覆盖、新增、跳过”筛选。

下面是最终运行截图:

在这里插入图片描述

备份选项在存在覆盖项时默认开启。用户可以关闭,但必须再确认一次,因为关闭后被覆盖的目标文件不具备事务回滚条件。

这不是为了多弹一个窗口。同步真正写盘前,使用者至少应该能回答三个问题:方向对不对、哪些文件会被覆盖、哪些文件不应该执行。

预览之后还要再复核一次

预览页展示的是生成 SyncPlan 时的状态。用户在窗口里看了几十秒,文件仍可能被外部程序修改。

因此点击“开始同步”后,第一步不是复制,而是 preflight 预检。

每个文件指纹至少包含:

exists
directory
size
modifiedTime
sha256

执行器重新读取所有已选文件的状态,对比来源和目标是否仍与预览一致。只要有一项变化,整批任务都不开始写入:

  • 变化项标记为失败。
  • 其他已选项标记为未执行。
  • 提示重新对比。

这里专门采用“整批预检通过后再写”的方式,而不是边检查边复制。否则第一个文件已经覆盖,到第十个文件才发现目标被修改,还是会形成半套结果。

内容是否相同继续只看大小和 SHA-256,不会因为文件修改时间不同就误判为覆盖;但预览后的状态复核会同时检查修改时间和 Hash,用来发现外部变化。

单个文件怎样安全写入

预检通过后,每个文件按固定步骤执行:

  1. 如果是覆盖且备份开启,先备份原目标文件。
  2. 在目标文件同级目录创建 .fctmp 临时文件。
  3. 把来源内容复制到临时文件。
  4. 计算临时文件 SHA-256,与预检来源 Hash 比较。
  5. 校验通过后,再把临时文件替换成正式目标文件。
  6. 清理未提交临时文件。

临时文件必须放在目标文件同一个目录。这样调用:

Files.move(temp, target,
        StandardCopyOption.ATOMIC_MOVE,
        StandardCopyOption.REPLACE_EXISTING);

时,更有机会落在同一文件系统中完成原子移动。

如果底层文件系统不支持 ATOMIC_MOVE,程序会退回普通 REPLACE_EXISTING,并在条目结果里记录警告。这里不能承诺所有磁盘和网络目录都支持原子替换。

原子移动解决的是“单个文件最终状态”问题,不代表多文件整体原子。第 3 个文件成功、第 4 个失败仍然可能发生,所以后面还需要事务清单和回滚。

备份不放进项目目录

覆盖文件默认备份到:

%LOCALAPPDATA%\FileCompareTool\backups\<transactionId>

备份目录保持目标侧相对路径,并保存 manifest.properties 事务清单。

不把备份放进项目目录有两个原因:

  1. 避免 .bak 或隐藏目录在下一次对比中又被扫描出来。
  2. 避免把临时恢复文件混进 Git、构建产物或发布包。

每次事务记录方向、来源和目标根目录、备份状态、条目路径、动作、执行阶段、最终状态、错误原因、备份位置和写入后的 Hash。

事务清单自己也不直接覆盖。先写临时文件,再替换正式 manifest.properties。程序执行到哪个阶段,清单就更新到哪个阶段。

已完成或已回滚事务默认保留最近 10 次。失败、回滚冲突和回滚失败的事务不会自动清理,避免真正需要恢复时备份被回收。

同步进度不是一个转圈图标

执行窗口把过程拆成五个阶段:

复核 → 备份 → 复制 → 校验 → 提交

在这里插入图片描述

窗口显示当前文件、当前阶段、已完成数量和剩余数量。取消操作只在两个文件之间生效。

如果一个文件正在备份或提交,点击取消不会在线程中间强行打断,而是显示“正在完成当前文件”,等它进入稳定状态后再停止后续条目。直接终止文件写入线程,反而更容易留下临时文件或不完整事务记录。

默认策略是遇到第一个错误立即停止。继续执行虽然可能多复制几个文件,但也会扩大需要判断和恢复的范围。当前工具不是无人值守批处理,先停下来把失败原因看清楚更合适。

失败后必须知道发生了什么

旧版遇到异常只显示错误字符串。新版结果页把所有已选项目分成:

  • 已完成。
  • 失败。
  • 未执行。
  • 已取消。
  • 已回滚。
  • 回滚冲突。
  • 回滚失败。

在这里插入图片描述

每条记录都有失败阶段和原因。例如“备份阶段,拒绝访问,目标文件正在被占用”,而不是笼统地写“同步失败”。

如果前面已有文件写入成功,结果页会提供“回滚本次同步”。关闭结果窗口、保留已完成内容或完成回滚后,主界面都会重新执行目录 Hash 对比,不继续显示旧结果。

正常完成时的实际结果如下:

在这里插入图片描述

回滚不是直接把备份盖回去

假设同步完成后,另一个程序又修改了目标文件。这时用户点击回滚,如果程序不做判断,旧备份会把刚才的新修改再次覆盖。

因此每个成功写入条目都会记录本次写入后的 SHA-256。回滚前重新计算当前目标文件 Hash:

覆盖文件

  • 当前 Hash 仍等于本次写入 Hash:从备份恢复。
  • 当前 Hash 已变化:标记回滚冲突,不强制覆盖。

新增文件

  • 当前 Hash 仍等于本次写入 Hash:删除本次新增文件。
  • 当前文件已变化:保留文件,标记回滚冲突。

新建目录

  • 目录为空:删除。
  • 目录里已有其他内容:保留。

这样做以后,回滚只撤销“仍然可以确认属于本次同步”的修改,不把回滚本身变成第二次误覆盖。

关闭覆盖备份后,覆盖文件不会显示虚假的回滚能力。纯新增项仍然可以在未被外部修改时删除,但没有备份的覆盖内容无法恢复,这一点会在开始前明确提示。

代码没有继续塞回主窗口

原来的同步逻辑都在 FileCompareTool.sync() 中。如果把预览、备份、临时文件、事务和回滚继续写在这个方法里,后面几乎无法测试失败场景。

这次拆成几组职责:

  • SyncPlanBuilder:从当前比较结果和方向生成同步计划。
  • SyncPlanSyncPlanEntry:保存不可变计划和文件指纹。
  • SyncService:预检、备份、安全写入、事务清单、失败停止和回滚。
  • SyncPreviewDialog:勾选、筛选、方向和备份设置。
  • SyncProgressDialog:进度、阶段和安全取消。
  • SyncResultDialog:结果列表、备份入口和回滚操作。
  • FileCompareTool:只负责从当前结果创建计划,并在流程结束后重新对比。

同步核心服务不依赖 Swing。测试可以直接在临时目录里创建左右文件,执行同步,再检查目标文件和事务状态。

这次测试了哪些情况

新增加了两组测试:同步计划规则测试和同步服务集成测试。

集成测试真实创建临时目录,覆盖这些场景:

  • 覆盖文件并备份。
  • 新增文件。
  • 成功同步后完整回滚。
  • 预览后目标文件变化,整批不写入。
  • 只选择部分文件。
  • 回滚前目标被外部修改,报告冲突并保留新内容。
  • 第一个文件预检失败,其他项目标记未执行。
  • 执行前取消,不写入任何文件。
  • 关闭备份后覆盖成功,但不提供虚假恢复入口。
  • 相同内容、不同修改时间仍然判定为跳过。

另外继续运行原有 7 组回归测试:编码检测、编码保存、换行保真、差异算法、差异块应用、行对齐和过滤规则,全部通过。

最后启动真实 Swing 窗口,用“新增 + 覆盖 + 中文路径”目录跑完整流程并截图,检查表格列宽、筛选、复选框、备份路径和结果页没有重叠。

这次实际修改了哪些内容

1. 同步计划

  • 增加同步方向和新增、覆盖、创建目录、跳过动作。
  • CompareResult 转换为不可变 SyncPlan
  • 保存来源与目标文件指纹。
  • 支持部分勾选和操作筛选。

2. 写入安全

  • 执行前整批复核。
  • 覆盖前默认备份。
  • 目标同目录临时文件。
  • 临时文件 SHA-256 校验。
  • 优先原子替换,不支持时回退并记录。
  • 失败后清理未提交临时文件。

3. 事务与恢复

  • 每次同步生成事务编号和清单。
  • 记录成功、失败、未执行和取消状态。
  • 默认遇到首个错误停止。
  • 支持覆盖恢复、新增删除和空目录清理。
  • 回滚前检查外部修改。
  • 最近保留 10 次已完成事务。

4. Swing 界面

  • 用同步预览替换旧确认框。
  • 增加全部、将执行、覆盖、新增和跳过筛选。
  • 显示来源、目标、文件大小、修改时间和备份估算。
  • 增加执行阶段和安全取消。
  • 增加事务结果、备份位置和回滚入口。

5. 测试和文档

  • 增加同步计划单元测试。
  • 增加临时目录同步与回滚集成测试。
  • 增加真实 Swing 窗口测试和最终截图。
  • 更新 README、开发计划和 P1 实施方案。

最终版提示词

如果需要给一个现有 Java Swing 文件工具增加同类同步安全能力,可以使用下面这份提示词:

请继续开发一个现有的 Java 8 Swing 文件对比工具。工具已经支持文件和目录
SHA-256 对比、折叠目录树、过滤规则、F5 刷新、双向同步、差异块编辑、
编码识别和换行保真。

本次实现 P1“同步预览、备份和失败恢复”。当前同步按钮只弹一次确认,随后使用
Files.copy(REPLACE_EXISTING) 直接覆盖所有不同文件。需要改造成可预览、可选择、
可追踪、可安全取消并具备冲突保护回滚能力的同步流程。

功能要求:

1. 点击同步到左侧或右侧后先生成不可变 SyncPlan,不得立即写文件。
2. 每个计划项记录相对路径、ADD/OVERWRITE/CREATE_DIRECTORY/SKIP 动作、
   来源和目标路径、存在状态、大小、修改时间和 SHA-256。
3. 保持当前不删除目标侧多余文件的规则。来源侧缺失项只能显示为跳过。
4. 同步预览明确显示方向、来源根目录、目标根目录、新增、覆盖和跳过数量。
5. 新增和覆盖默认选中,跳过项不可执行;支持取消部分文件。
6. 提供全部、将执行、覆盖、新增和跳过筛选,显示复制大小和预计备份大小。
7. 有覆盖项时默认启用备份。关闭备份且仍选择覆盖项时必须再次确认,
   并明确覆盖内容无法通过本次事务恢复。
8. 备份默认保存在 %LOCALAPPDATA%\FileCompareTool\backups\<transactionId>,
   保持目标侧相对路径,不把备份放进被对比目录。
9. 已完成或已回滚事务默认保留最近 10 次;失败、冲突和回滚失败事务不自动清理。
10. 用户确认开始后先复核全部选中项。来源或目标的存在状态、大小、修改时间或
    SHA-256 与预览不一致时,整批不写入,并提示重新对比。
11. 相同内容判断仍以大小和 SHA-256 为准,不能因为修改时间不同误判为覆盖。
12. 单文件在目标同目录创建临时文件,复制完成后校验 SHA-256,再使用
    ATOMIC_MOVE + REPLACE_EXISTING 提交。
13. 文件系统不支持原子移动时回退到 REPLACE_EXISTING,并在结果中记录警告。
14. 无论成功或失败都清理未提交临时文件。属性复制失败可以记录警告,
    不能把完整内容写入成功误判成失败。
15. 同步过程显示复核、备份、复制、校验和提交阶段,以及当前文件和整体进度。
16. 支持安全取消。取消只在两个文件操作之间生效,不强行终止正在提交的文件。
17. 默认遇到首个错误立即停止,后续选中项标记未执行。
18. 结果页区分成功、失败、未执行、取消、已回滚、回滚冲突和回滚失败,
    每项显示失败阶段和根因。
19. 每次同步保存事务清单,记录方向、根目录、备份位置、条目动作、状态、
    错误、目标路径和本次写入后的 SHA-256。清单自身也使用临时文件替换。
20. 回滚覆盖文件前检查当前目标 Hash 是否仍等于本次写入 Hash,匹配时才恢复备份。
21. 回滚新增文件前执行同样检查,匹配时才删除;外部修改后必须保留并报告冲突。
22. 本次创建目录仅在为空时删除。回滚冲突或失败时保留事务备份。
23. 同步完成、失败结果关闭或回滚结束后自动重新执行目录 Hash 对比。
24. 将同步计划、服务和 Swing 对话框拆开。SyncService 不得依赖 Swing 组件,
    以便在临时目录中测试。
25. 增加测试,至少覆盖正反方向、ADD/OVERWRITE/SKIP、部分选择、预检变化、
    取消、无备份覆盖、成功回滚、外部修改冲突、空目录和临时文件清理。
26. 完成后编译全部源码与测试,运行真实 Swing 窗口,生成同步预览、进度、
    成功和失败恢复截图,检查中文、长路径、最小窗口和按钮文字。

修改前先阅读现有同步实现和开发计划,给出实施计划、数据模型、安全边界、
失败语义、回滚规则和 UI 效果图。我确认后再修改正式代码。实现完成后更新
README、开发计划、实施方案和测试说明。

下一步

按照开发计划,下一项进入 P2:扫描进度、取消和受控并行 Hash。

目前目录扫描仍然是串行执行。目录小的时候感觉不明显,文件数量上万后,界面只能显示“正在对比”,看不到发现了多少文件、Hash 处理到哪里,也不能取消。

下一步需要把扫描拆成发现文件、计算 Hash 和构建结果几个阶段,提供准确进度和取消能力,再根据磁盘类型与文件数量控制 Hash 并发数。取消后不能清空上一份有效结果,也不能让旧任务在后台完成后覆盖新结果。

后面再按计划做对比历史缓存、过滤规则预设和发布打包。

最后

这期最大的变化不是多了一个备份复选框,而是同步终于有了明确的执行边界。

以前点击同步,程序只知道“开始复制”;现在它先生成计划,让人确认,然后复核磁盘状态,逐项备份和安全提交,最后留下可以检查的事务结果。

它仍然不是数据库事务。多个文件无法保证一起原子提交,网络目录也可能不支持原子移动;关闭备份后,覆盖内容同样无法恢复。但这些限制现在都被明确处理和展示,不再用一句“同步失败”掩盖现场。

当前使用体验也还有不足。大目录扫描没有精确进度,取消机制只覆盖同步阶段,事务备份还没有独立管理界面,异常退出后的未完成事务目前主要依靠清单保留,还没有启动时恢复向导。

后面继续按开发计划推进。下一期先处理扫描性能,把“程序是不是还在工作、还要多久、能不能停”这三个问题解决掉。

更多推荐