Done
- Rewrote the app as a package (
cubbitsync/) around a three-way reconciliation engine: local scan + remote listing + a SQLite baseline of the last agreement. Replaces v1's tworclone copy --ignore-existingshell scripts entirely. - Added rename detection (delete + create of identical content → server-side
moveto), trash-on-delete with retention, conflict resolution with the losing version preserved, gitignore-style filters, safety caps, a plan-preview approval gate, persistent change history, and a headless--climode. - 66 unit tests, no third-party runner.
build-deb.shruns them before packaging. - Verified end-to-end with the real rclone binary: single-machine create / modify / rename / delete, two-machine convergence, conflict resolution, mass-deletion refusal. Read-only planning verified against a live bucket.
Decisions
- Own engine rather than
rclone bisync. Debian trixie ships rclone 1.60, whose bisync is the early beta: no--conflict-resolve,--resilient,--recoveror--max-lock, and any interruption forces a full--resync. Not acceptable for a multi-gigabyte tree. The engine uses rclone purely as transport, so it works on the rclone that is actually installed. - Deletions default to a remote trash prefix, conflicts to newer-wins with the loser kept, first run to a dry-run preview behind an approval gate. All three are configurable.
--use-server-modtimeon every listing. S3 keeps rclone's mtime in object metadata, which a LIST does not return — reading it costs one HEAD per object. Measured against a live bucket: >10 minutes for ~30k objects versus 5 seconds with server timestamps. Cross-side content comparison therefore uses the MD5 that S3 returns free in the ETag, never a local-vs-remote timestamp difference.
Findings worth remembering
- Dependency and build directories dominate a typical sync root: on the tree this was developed against, the default filters removed roughly seven eighths of the local file count and a third of the remote objects. Filters are not cosmetic — preview before the first apply.
- v1's scripts ran with
--copy-links, so symlinked content already exists on the remote as regular objects.follow_symlinkstherefore defaults to true; turning it off would make that content look locally deleted.
Next
- Run Preview on the real pair and tune
filters.txtbefore the first apply. - Consider a
--resync-style "remote wins entirely" recovery action if a baseline is ever lost mid-transfer.