-
Notifications
You must be signed in to change notification settings - Fork 0
Contributing
git clone https://github.com/cloud-drive-sync/cloud-drive-sync.git
cd cloud-drive-sync
./dev.sh # Sets up both daemon and UI, starts in demo modeOr manually:
# Daemon
cd daemon
python3 -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"
# UI
cd ui
npm install- Linter: ruff (target: Python 3.12, line length: 100)
- Formatting: ruff format (black-compatible)
- Run:
cd daemon && .venv/bin/ruff check src/ tests/
-
Type checking:
tsc --noEmitin strict mode - Run:
cd ui && npx tsc --noEmit
-
Formatting:
cargo fmt -
Linting:
cargo clippy - Run:
cd ui/src-tauri && cargo fmt && cargo clippy
make lint # Runs ruff + tsccd daemon
source .venv/bin/activate
pytest -v # Run all tests
pytest --cov=cloud_drive_sync # With coverage
pytest tests/test_planner.py # Run a specific fileTests use pytest-asyncio with asyncio_mode = "auto" (async test functions are detected automatically).
The following test files cover specific bugs and features with targeted regression tests:
| Test File | Covers |
|---|---|
test_bug_activity_filters.py |
Activity log filtering by pair and event_type normalization |
test_bug_remote_browser.py |
Remote folder browser query filtering |
test_bug_stale_data.py |
Stale pair cleanup on engine startup |
test_bug_status_counts.py |
Accurate files_synced count from DB |
test_bug_sync_trigger.py |
force_sync/pause_sync/resume_sync with pair_id parameter |
test_feature_ignore_hidden.py |
Hidden file filtering in scanner, watcher, and planner |
make lint # Lint Python + TypeScript
make test # Run pytestUse descriptive branch names with a prefix:
-
feat/add-selective-sync— new feature -
fix/conflict-resolution-race— bug fix -
refactor/split-sync-engine— code refactoring -
docs/update-api-reference— documentation -
test/add-executor-tests— test additions
Write clear, imperative commit messages:
Add selective sync filtering by file extension
Support include/exclude glob patterns in sync pair config.
Patterns are evaluated by the planner before generating actions.
Before submitting a PR:
-
make lintpasses (ruff + tsc) -
make testpasses (pytest) - New features include tests
- Documentation is updated if the public API changes
The app ships with embedded OAuth client credentials for Google Drive (and other providers as they're added). These are intentionally public — OAuth Desktop/public client IDs are not secrets per Google's own documentation. The security comes from the OAuth flow itself (user consent, redirect URI validation), not from the client ID/secret.
This is standard practice for open-source desktop apps (Rclone, Cyberduck, GNOME Online Accounts all do this).
GitHub Push Protection will flag these as secrets. To push changes that modify embedded credentials:
- GitHub will provide unblock URLs in the push rejection message
- Click each URL and select "It's used in tests" or "I'll fix it later" to allow the push
- Re-run
git push
The credentials are in daemon/src/cloud_drive_sync/auth/oauth.py and can be overridden by users via:
- Placing a
client_secret.jsonin~/.config/cloud-drive-sync/ - Setting
CDS_GOOGLE_CLIENT_ID/CDS_GOOGLE_CLIENT_SECRETenv vars
cloud-drive-sync/
├── daemon/ # Python sync daemon
│ ├── src/cloud_drive_sync/
│ │ ├── sync/ # Sync engine, planner, executor, conflicts
│ │ ├── drive/ # Google Drive API client
│ │ ├── local/ # Filesystem watcher, scanner, hasher
│ │ ├── ipc/ # JSON-RPC server + handlers
│ │ ├── db/ # SQLite database + models
│ │ ├── auth/ # OAuth2 credential management
│ │ ├── util/ # Logging, paths, retry
│ │ ├── config.py # TOML config loader
│ │ ├── daemon.py # Main daemon class
│ │ └── cli.py # Click CLI entry point
│ └── tests/
├── ui/ # Tauri + React desktop UI
│ ├── src/
│ │ ├── components/ # React page components
│ │ └── lib/ # IPC client, types, hooks
│ └── src-tauri/src/ # Rust backend (bridge, commands, tray)
├── docs/ # Documentation
└── installer/ # systemd service file
For full architectural details, see docs/ARCHITECTURE.md.
-
Define the method name in
daemon/src/cloud_drive_sync/ipc/protocol.py:METHOD_MY_METHOD = "my_method"
Add it to the
ALL_METHODSlist. -
Add the handler in
daemon/src/cloud_drive_sync/ipc/handlers.py:Register it in the
self._handlersdict inRequestHandler.__init__:self._handlers["my_method"] = self._my_method
Implement the handler:
async def _my_method(self, params: dict) -> dict: value = params.get("value") if value is None: raise TypeError("value is required") result = await self._engine.do_something(value) return {"status": "ok", "data": result}
-
Add the Tauri command in
ui/src-tauri/src/commands.rs:#[tauri::command] pub async fn my_method( state: tauri::State<'_, BridgeState>, value: String, ) -> Result<serde_json::Value, String> { let mut bridge = state.0.lock().await; bridge.call("my_method", json!({"value": value})) .await .map_err(|e| e.to_string()) }
Register it in
main.rs:commands::my_method,
-
Add the TypeScript client in
ui/src/lib/ipc.ts:export async function myMethod(value: string): Promise<MyResult> { return invoke<MyResult>("my_method", { value }); }
-
Add types in
ui/src/lib/types.tsif needed. -
Write tests in
daemon/tests/for the handler. -
Document the method in
docs/API.mdwith params, response, and JSON examples.
-
Create the component in
ui/src/components/MyPage.tsx:export function MyPage() { return ( <div className="my-page"> <h2>My Page</h2> {/* content */} </div> ); }
-
Add the route in
ui/src/App.tsx:import { MyPage } from "./components/MyPage"; // Inside <Routes>: <Route path="/my-page" element={<MyPage />} />
-
Add navigation in the
NavBarcomponent inui/src/App.tsx:<li> <NavLink to="/my-page">My Page</NavLink> </li>
-
Add hooks in
ui/src/lib/hooks.tsif the page needs to fetch daemon data. -
Add styles in the appropriate CSS file.
Cloud Drive Sync
Getting Started
Reference
Project