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 lookup
  • internal/state/ - per-machine sync state in ~/.config/syncr/state.json, tracks initialization status, sync count, error streaks
  • internal/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 files
  • internal/progress/ - sync progress reporting

Data flow for a sync cycle:

  1. Load config (syncr.json) and state (state.json)
  2. For each enabled, initialized project: take pre-sync snapshots of both directories
  3. Call sync.RunBisync() which initializes rclone’s local backend, applies exclude filters, and runs bisync.Bisync() between the local path and the sync folder
  4. Post-sync: take new snapshots, diff for changes, check for conflicts
  5. Fire hooks (post_sync, on_conflict) if files changed
  6. Update state (success/error/conflicts) and save

Key types:

  • config.Config holds sync_root, interval, projects array, conflict strategy
  • config.Project holds local_path, sync_path, excludes, hooks, per-project conflict override
  • state.State maps project names to ProjectState (initialized, last_sync, error_count)
  • sync.BisyncOptions wraps 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 .