syncr
Bidirectional folder sync that keeps local directories in sync with cloud storage (OneDrive, Dropbox, Google Drive). Single binary, JSON config, works anywhere rclone does.
Overview
- Language: Go
- Repo: peteretelej/syncr
- Install:
go install github.com/peteretelej/syncr@latest - Status: active
Architecture
syncr is structured as a thin CLI layer over rclone’s bisync engine, with clean separation between commands, sync logic, config, and state.
Package layout:
main.go- flag parsing and command dispatch (init, sync, daemon, status, add, enable/disable, logs)cmd/- one file per command (init.go, sync.go, daemon.go, add.go, status.go, enable.go, logs.go)internal/config/- JSON config loading, validation (basic + full with warnings), save, project lookupinternal/state/- per-machine sync state in~/.config/syncr/state.json, tracks initialization status, sync count, error streaksinternal/sync/- rclone bisync wrapper (bisync.go), conflict detection (conflicts.go), pre/post snapshot diffing (snapshot.go), shell hooks (hooks.go), trash management (trash.go), error classification (errors.go)internal/logger/- daily rotating log filesinternal/progress/- sync progress reporting
Data flow for a sync cycle:
- Load config (syncr.json) and state (state.json)
- For each enabled, initialized project: take pre-sync snapshots of both directories
- Call
sync.RunBisync()which initializes rclone’s local backend, applies exclude filters, and runsbisync.Bisync()between the local path and the sync folder - Post-sync: take new snapshots, diff for changes, check for conflicts
- Fire hooks (post_sync, on_conflict) if files changed
- Update state (success/error/conflicts) and save
Key types:
config.Configholds sync_root, interval, projects array, conflict strategyconfig.Projectholds local_path, sync_path, excludes, hooks, per-project conflict overridestate.Statemaps project names toProjectState(initialized, last_sync, error_count)sync.BisyncOptionswraps rclone options: resync mode, dry-run, excludes, backup dirs, conflict resolution
Key Design Decisions
rclone as a library, not a subprocess. syncr imports github.com/rclone/rclone directly and calls bisync.Bisync() as a Go function. This avoids shelling out, gives type-safe configuration, and means the binary is self-contained with no runtime dependency on rclone being installed. The tradeoff is a larger binary and coupling to rclone’s internal API.
4-state initialization logic. The init command inspects both directories and picks the right resync strategy: pull from cloud (local empty), push to cloud (cloud empty), merge (both have files), or mark initialized (both empty). This handles the common “I already have files on one side” case without requiring the user to understand rclone’s resync modes.
MaxDelete 50% safety limit. The bisync options hardcode MaxDelete: 50, so rclone aborts if more than half the files would be deleted. This protects against scenarios like a disconnected cloud drive appearing empty.
Per-machine state, shared config. Config (syncr.json) can live in a synced folder so multiple machines share it. State (state.json) lives in ~/.config/syncr/ so each machine tracks its own initialization and error history independently.
Daemon with config hot-reload. The daemon polls the config file’s mtime each tick. If the file changed, it reloads and revalidates before the next sync cycle. Invalid configs are rejected and the previous config is kept.
Consecutive error threshold. After 5 consecutive errors on a project, the daemon skips it and suggests syncr init --force to re-initialize. This prevents a broken project from generating noise every sync cycle.
Development
go build -o syncr .
go test ./...
# Cross-compile
GOOS=windows GOARCH=amd64 go build -o syncr.exe .
GOOS=linux GOARCH=amd64 go build -o syncr-linux .