Skip to content

About

Bidirectional file sync for Cubbit DS3 and other rclone remotes

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

CubbitSync

Bidirectional file sync between a local directory and a Cubbit DS3 bucket — or any rclone remote — for Linux desktops.

Install it on two machines pointing at the same bucket and they converge: an edit on one appears on the other, a deletion removes the file everywhere, and a rename stays a rename instead of turning into a second copy under a new name.

Why not just rclone copy both ways

Copying in both directions cannot work, because a directory listing alone is ambiguous. A file present locally but not remotely is either created here or deleted there, and the two demand opposite actions. Tools that guess pick "created" every time, which is why deletions never stick, renames leave the old object behind, and the bucket slowly fills with duplicates.

CubbitSync keeps a baseline — a record of how both sides looked when they last agreed — in a local SQLite database. Comparing three pictures instead of two makes every case unambiguous:

Since the baseline Remote unchanged Remote changed Remote deleted
Local unchanged nothing to do download delete locally
Local changed upload conflict → policy upload (resurrect)
Local deleted delete remotely download (resurrect) nothing to do

A deletion paired with a creation of identical content is recognised as a rename and issued as a server-side move, so the bytes never travel.

Features

  • True bidirectional sync — creations, edits, deletions and renames each propagate exactly once, in either direction
  • Multi-machine — any number of machines on one bucket; each keeps its own baseline and they converge
  • No duplicate copies — renames become server-side moves; nothing is ever re-uploaded under a second name
  • Recoverable deletions — deleted objects move to a timestamped .cubbit-trash/ prefix with configurable retention, instead of being erased
  • Conflict handling — a file edited on two machines is resolved by policy, and the losing version is always kept, never discarded
  • Ignore filters — gitignore-style patterns, with a default set covering node_modules/, .git/, build output, editor scratch files and OS cruft
  • Preview before it runs — every cycle can be inspected as a plan and approved before anything moves
  • Safety limits — mass deletions are refused; a missing or empty local directory never wipes the remote
  • Change history — a persistent, colour-coded record of every file the app has moved, kept across restarts
  • Instant + scheduled — inotify picks up local edits immediately; the remote is polled on a timer
  • Headless mode — --cli runs a cycle from a terminal or cron, no desktop session required

Prerequisites

Dependency Purpose Required
Python ≥ 3.9 Runtime yes
PyQt5 Desktop interface yes (not needed for --cli)
rclone ≥ 1.50 Listing and transferring objects yes
inotify-tools Instant reaction to local edits recommended
sudo apt install python3 python3-pyqt5 rclone inotify-tools

You also need a configured rclone remote. Cubbit's own settings are documented at docs.cubbit.io/integrations/rclone; run rclone config once and create an s3 remote:

[<your-remote>]
type = s3
provider = Cubbit
access_key_id = <your-access-key>
secret_access_key = <your-secret-key>
region = eu-west-1
endpoint = s3.cubbit.eu
acl = private

CubbitSync reads this file directly and lists the remotes it finds in the Settings tab. It never stores your credentials — they stay in rclone.conf.

Installing

bash build-deb.sh

The script runs the test suite first and refuses to package if anything fails. Then:

sudo apt install ./build/cubbit-sync_2.0.0_all.deb

Launch from your application menu, or:

cubbit-sync &

To remove it (per-user settings and sync state under $HOME are left alone):

sudo apt remove cubbit-sync

First run

Because the app has no baseline yet, automatic sync stays on hold until you approve the initial reconciliation. Nothing moves before you do.

  1. Open Settings, set the local directory and pick your remote and bucket, then Test connection.
  2. Open Filters and check the default ignore list. Test against local files reports how many files would sync — worth doing before the first run if your directory contains build trees or dependency folders.
  3. Click Preview. You get an itemised plan: what would upload, download, be renamed, or be deleted, with byte counts.
  4. Adjust filters and preview again until the plan looks right, then Apply.

From then on the app syncs on its own.

If both sides already hold the same files and you would rather not transfer anything, use Adopt current state on the Status tab. That records the current situation as the baseline; only changes made afterwards will sync.

Configuration

Settings live in ~/.config/cubbit-sync/config.json, ignore patterns in ~/.config/cubbit-sync/filters.txt, and sync state in ~/.local/share/cubbit-sync/state.db. Everything is editable from the Settings tab.

What to sync

Setting Default Description
local_dir ~/Cubbit Directory to keep in sync
remote_path — rclone remote and bucket, e.g. <your-remote>:<bucket>

Behaviour

Setting Default Description
delete_policy trash trash moves deleted objects to .cubbit-trash/; hard deletes them; never keeps the remote additive
trash_retention_days 30 Trash batches older than this are purged; 0 keeps them forever
conflict_policy newer_wins Also keep_both, remote_wins, local_wins
detect_renames true Turn delete + create of identical content into a server-side move
use_hashes true Compare MD5 checksums when the remote supplies them
use_server_modtime true See Timestamps below
follow_symlinks true Upload the target of a symlink rather than skipping it
remove_empty_dirs true Clean up local directories a sync leaves empty
modify_window 2.0 Seconds of timestamp difference treated as "unchanged"

Automation

Setting Default Description
auto_sync true Master switch for automatic syncing
watch_local true React to local edits via inotify
poll_remote true Poll the remote on a timer
check_interval 300 Seconds between remote polls (minimum 30)
debounce_seconds 10 Quiet period after the last local change before syncing

Safety

Setting Default Description
max_delete_files 500 Ask before propagating more deletions than this
max_delete_percent 25 …or more than this share of tracked files
abort_if_local_missing true Refuse to sync when the local directory is empty but files are tracked
require_approval_first_run true Hold automatic sync until the first plan is approved

Transfers

Setting Default Description
transfers 16 Parallel transfers
checkers 8 Parallel checkers
bwlimit — e.g. 50M; empty means unlimited
extra_flags — Additional rclone flags, space separated
rclone_binary rclone Path to the binary
rclone_config — Alternative rclone.conf; empty uses rclone's default

Filters

~/.config/cubbit-sync/filters.txt uses gitignore syntax:

node_modules/        a directory and everything under it
*.tmp                any file with that extension, at any depth
build/*.o            path-relative (contains a slash)
/secret.txt          anchored at the sync root
!keep/important.log  re-include something an earlier pattern excluded

* does not cross a directory separator; ** does. Filtered paths are invisible to the engine in both directions: they are never uploaded, never downloaded, and never deleted. Objects already on the remote that match a filter are simply left alone.

Adding a filter for files that are already synced does not delete them — it stops tracking them. Removing a filter brings the matching files back into scope on the next cycle.

Conflicts

Two machines editing the same file between syncs produce a conflict. Under the default newer_wins, the more recently modified version keeps the original name and the other is preserved next to it:

report.pdf                                    the newer version
report.conflict-<machine>-20260812-143002.pdf the older version, kept

The conflict copy syncs like any other file, so both versions reach every machine. Nothing is discarded. When the two timestamps are identical but the contents differ, there is no defensible winner and both versions are renamed.

Trash and recovery

With the default trash policy, a deleted object moves to a timestamped batch:

<bucket>/.cubbit-trash/2026-08-12T14-03-55/path/to/file.pdf

To restore something, copy it back with rclone:

rclone copy "<your-remote>:<bucket>/.cubbit-trash/<batch>/path/to/file.pdf" ~/Cubbit/path/to/

Batches older than trash_retention_days are purged automatically at the end of a sync. The trash prefix is in the default filters, so it is never downloaded.

Timestamps, and why this setting matters

S3 does not return rclone's stored modification time in a bucket listing — it lives in object metadata, and reading it costs one extra request per object. On a 30,000-object bucket that turns a five-second listing into one that takes over ten minutes.

With use_server_modtime enabled (the default), CubbitSync uses the server's own LastModified, which comes free with the listing and changes on every upload — all that change detection requires. Content comparison then relies on MD5 checksums, which S3 also returns free of charge in the ETag.

The one gap: objects uploaded in multiple parts have no usable MD5. For those, and only those, the app falls back to a single metadata lookup for that object. Turn the setting off if your remote supplies no checksums at all; expect listings to get much slower.

Headless use

cubbit-sync --cli --dry-run     # print the plan, change nothing
cubbit-sync --cli               # run one cycle
cubbit-sync --cli --verbose     # …and print every file action
cubbit-sync --cli --force       # run even if a safety limit is exceeded

Exit codes: 0 success, 1 finished with errors, 3 refused by a safety check, 4 another instance holds the lock. Suitable for cron; the GUI and the CLI share the same lock file, so they will not collide.

Interface

Tab Contents
Status Current state, policies, tracked file count, last sync, watcher and poller health, baseline controls
Activity Live log of the current and recent cycles
Changes Persistent, colour-coded history of every file moved, filterable by action
Filters Edit ignore patterns, test them against your directory
Settings Everything above, plus a connection test

The tray icon reflects state: blue idle, orange scanning, green uploading, blue downloading, purple resolving conflicts, grey paused, red error. Closing the window minimises to the tray; Quit in the tray menu exits.

Upgrading from 1.x

v1 delegated transfers to two shell scripts running rclone copy --ignore-existing, which skipped every file that already existed and never deleted anything — so edits did not propagate, deletions did not stick, and renames left the old object in place.

v2 replaces that entirely. On first launch it reads your v1 config.json, carries over the local directory, remote path, debounce and startup preferences, and fills in the v2 defaults. The old up_script and down_script entries are kept in the file but no longer used; ~/bin/cubbitsync_up and ~/bin/cubbitsync_down can be deleted once you are happy with v2.

Because there is no baseline yet, the first run is a reconciliation and asks for approval. Files already identical on both sides are adopted without transferring anything.

Project layout

cubbit-sync-app/
├── cubbit_sync.py          # launcher
├── cubbitsync/
│   ├── config.py           # settings, defaults, v1 migration
│   ├── engine.py           # three-way reconciliation and execution
│   ├── filters.py          # gitignore-style pattern matching
│   ├── state.py            # SQLite baseline, history, process lock
│   ├── rclone.py           # batched rclone wrapper
│   ├── watcher.py          # inotify file watcher
│   ├── worker.py           # Qt threading bridge
│   ├── gui.py              # PyQt5 interface
│   └── icons.py            # generated tray icons
├── tests/                  # unit tests, no third-party runner needed
├── build-deb.sh            # package builder
└── build/                  # generated .deb packages

Run the tests with:

python3 -m unittest discover -s tests -t . -v

Known limitations

  • Empty directories are not synced. S3 has no concept of a directory, and creating placeholder objects would litter the bucket.
  • Multipart-uploaded objects expose no MD5. Change detection for those relies on size and the server timestamp, with a metadata lookup as a fallback.
  • File permissions and ownership are not preserved; S3 stores none of that.
  • A sync cycle lists the whole bucket. That is a few seconds for tens of thousands of objects, but the poll interval should be raised for very large buckets.

License

Copyleft 🄯 2026 acme@dyne.org, Rome

This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see http://www.gnu.org/licenses/.

About

Bidirectional file sync for Cubbit DS3 and other rclone remotes

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages