macOS 原生数据同步工具,使用 SwiftUI 构建(SwiftPM 手工打包,无 Xcode 工程文件依赖)。
面向的是「把一批目录稳妥地同步到另一个位置」这类需求:本地磁盘之间,或同步到局域网 NAS(SMB)与 WebDAV 服务器。核心引擎用 SHA-256 逐文件做内容指纹,用内容哈希而非修改时间 判断变更 —— 外接盘常是 FAT/exFAT(时间戳精度只有 2 秒),时钟也会漂移,靠时间戳判变更 必然出错。
当前版本是 v1.0.0,功能尚未完整。 已完成的是引擎、目标端连接与读写原语; 三方比对、冲突裁决、增量同步与断点续传尚未实现。详见下方 当前进度与限制 —— 请先读完那一节再决定是否使用。
- 目录扫描与内容指纹 —— 递归遍历目录树,逐文件计算 SHA-256;显式栈实现,不跟随软链接 (跟随软链接会导致目录环无限遍历、同一份内容被重复计入,统计数字直接失真)。
- 并发哈希 —— 多核并行,滑动窗口控制内存占用。实测 6.3 GB 目录树上 10 路并发比串行快 4.2 倍(283 → 1179 MB/s)。
- 三个阶段分开计时 —— 遍历 / 哈希 / 合计分别统计。遍历受文件数主导、哈希受字节数主导, 混在一起看不出瓶颈在哪。
- 本地目录目标端 —— 路径逃逸双重校验(词法 + 软链接解析,且校验目标自身)。
- SMB 目标端 —— 走系统 NetFS 框架挂载后复用本地驱动,因此读写语义与本地目录天然一致。 会自动复用已挂载的卷,不重复挂载。
- WebDAV 目标端 —— URLSession 直发
PROPFIND/PUT/MKCOL/DELETE,自写 multistatus 解析。 Basic / Digest 认证、可选中放行自签名证书。 - 凭据存系统钥匙串 —— 任务结构里只保存用户名,密码不落配置文件、不出现在日志里。
- 界面 —— 浅色 / 深色 / 跟随系统;任务可编辑;菜单栏常驻;关闭主窗口行为可选 (退出软件 / 最小化到菜单栏)。
| 机型 | 安装包 |
|---|---|
| Apple Silicon(M 系列) | sync-engine-v1.0.0-arm64.dmg |
| Intel(x86_64) | sync-engine-v1.0.0-x86_64.dmg |
均为临时签名、未公证的版本。首次打开会被 Gatekeeper 拦下,这是预期行为而非缺陷:
把 sync-engine.app 拖进「应用程序」后右键 → 打开,或到
「系统设置 → 隐私与安全性」点「仍要打开」。放行一次即可,之后正常双击启动。
全部版本见 Releases。
浅色主题:
深色主题:
- macOS 14+(Observation 框架与
NavigationSplitView稳定可用的下限) - Swift 6(Command Line Tools 或完整 Xcode)—— 仅构建时需要,运行不需要
无第三方依赖:界面与引擎只用系统框架(SwiftUI / Foundation / CryptoKit / AppKit / Security / NetFS),构建脚本只用系统自带工具。
cd SyncApp
swift build # 构建
swift test # 单元测试(76 项)
bash Scripts/build_app.sh # 打包成 dist/sync-engine.app无界面验证(跑的是打包产物本身,即真实交付路径):
dist/sync-engine.app/Contents/MacOS/sync-engine --selfcheck [目录]
dist/sync-engine.app/Contents/MacOS/sync-engine --bench <目录> [并发数]打发行版安装包:
bash Scripts/make_dmg.sh 1.0.0 arm64 # → dist/sync-engine-v1.0.0-arm64.dmg
bash Scripts/make_dmg.sh 1.0.0 x86_64 # → dist/sync-engine-v1.0.0-x86_64.dmg这套项目的验证原则是不依赖肉眼,分四层:
| 层 | 手段 | 覆盖 |
|---|---|---|
| 引擎逻辑 | swift test(76 项) |
哈希向量、扫描统计、软链策略、路径逃逸、计时不变量、错误路径 |
| 存储驱动 | swift test(含真实 IO) |
对进程内真实 WebDAV 服务端做完整往返;对真实 SMB 挂载做只读验证 |
| 交付产物 | --selfcheck(16 项) |
真实 .app 内那份二进制能否跑通引擎与驱动接线 |
| 界面渲染 | 单窗口截图 + 位图像素分析 | 主题一致性、强调色是否统一、内容是否真的画出来了 |
两条值得一提的做法:
- 断言验结构性不变量,不验机器相关的具体数值。「分项耗时之和 ≤ 总耗时」这类断言 不随机器快慢变化,而「耗时必须小于 3 秒」必然变成 flaky 测试。
- 只在某条操作路径上出现的缺陷,必须把那条路径本身做成可复现的。
静态启动验证再密也验不到「运行中切换主题」这类路径,所以应用里保留了一个
走同一入口的诊断钩子(
--appearance-transition-test,带参数才生效), 配一个六场景的像素判据矩阵脚本Scripts/verify_appearance.sh。
已完成:引擎(扫描 + 并发哈希 + 三阶段计时)、三种目标端驱动(本地 / SMB / WebDAV) 的连接与读写原语、钥匙串凭据、界面外壳与设置。
尚未实现,且界面上已如实标注:
- 没有传输能力。 目前只能扫描源目录并列出文件,不会真正写入目标端。 编辑器里的「预览扫描」跑的是真实扫描,但它不等于同步。
- 没有增量同步。 每次扫描都全量重算哈希,没有哈希缓存。十万文件级目录需要数十秒。
- 没有三方比对与冲突裁决。 目标端已被改动过的文件不会被识别,也不会产生冲突副本。 这是本项目最关键的一块:它决定「同步删除」是否安全,因此「同步删除」开关当前不存在。
- 没有断点续传。 传输未实现,也就谈不上续传。
- 没有文件系统事件监听。 未接入 FSEvents,只能手动触发扫描。
- 扫描期间不能取消。
- 任务与日志无持久化,退出即丢(只有外观与关闭行为写入了 UserDefaults)。
- SMB 挂载后不会自动卸载,网络卷上也不会自动降低哈希并发度。
SyncApp/ SwiftPM 包
Sources/
SyncEngine/ 引擎与存储层(**不 import SwiftUI**,由构建系统强制)
Storage/ 存储驱动抽象 + 本地 / SMB / WebDAV 三个实现 + 钥匙串
SyncApp/ 界面与入口(State / Support / Views)
CNetFS/ NetFS 框架的 module map(SwiftPM 无法直接 import C 框架)
Tests/SyncEngineTests/ 单元测试 + 进程内 WebDAV 服务端
Scripts/ 构建、打包、外观矩阵与窗口探针
website/ 官网静态页(发布内容同步至 gh-pages 分支)
技术方案设计.md 完整设计文档(含全部取舍理由与踩坑记录)
引擎与界面分成两个 target,且由构建系统强制分离 —— 引擎层若误 import SwiftUI
会直接编译失败。这不是洁癖,而是可验证性的前提:它保证引擎能被 swift test
与无界面自检直接驱动,不需要先起一个 GUI。
仓库有三个对外产物:代码(main)、安装包(Releases)、官网(gh-pages)。
版本号只有一处来源 —— Scripts/build_app.sh 的 APP_VERSION,make_dmg.sh 从那里读。
这样不会出现「应用内显示 1.0.0、文件名写着 1.0.1」这类不一致。
cd SyncApp
bash Scripts/make_dmg.sh 1.0.0 arm64
bash Scripts/make_dmg.sh 1.0.0 x86_64
git tag -a v1.0.0 -m "sync-engine v1.0.0"
git push origin v1.0.0
# 然后在 GitHub 上创建 Release 并上传 dist/ 下的两个 dmg发版前的核对清单(这几项都做过,写下来免得下次漏):
swift test全绿 —— 单元测试dist/sync-engine.app/Contents/MacOS/sync-engine --selfcheck全绿 —— 跑的是打包产物本身bash Scripts/verify_appearance.sh全绿 —— 三种外观模式 × 静态启动与运行中切换- 两个 DMG 挂载后实跑自检,并核对架构与 Info.plist 名称
- Release 里附上 SHA-256,用户可自行校验
gh-pages 分支的根就是站点内容,源文件在 main 的 website/。两者需要手动同步:
git worktree add -b gh-pages /tmp/gh-pages-site
cd /tmp/gh-pages-site
git rm -rq . && rsync -a --exclude .DS_Store /path/to/repo/website/ ./
touch .nojekyll # 让 Pages 跳过 Jekyll 处理
git add -A && git commit -m "更新官网" && git push origin gh-pages
cd - && git worktree remove /tmp/gh-pages-site用独立 worktree 而不是在主工作区分支间来回切 —— 在主工作区做
git rm -r .会真的删掉工作目录里的文件,风险不成比例。
MIT © 2026 SyncEngine-macOS Contributors

