Add flit watch: one-way folder sync to a trusted peer

Watches local directories and auto-pushes new/changed files to a
known daemon peer via fsnotify, with startup reconciliation, a
stable_after debounce, and per-file JSON state so failed sends retry
on the next connection. Multiple watch entries targeting the same
peer share a single connection/room (watchPeerGroup), resolving the
room-name-collision question flagged in PROPOSAL-watch.md. Push-only,
never propagates deletes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
explewd
2026-07-12 21:50:18 +02:00
parent 0ffe9fc29d
commit 932a4bd7bb
6 changed files with 602 additions and 2 deletions

View File

@@ -20,6 +20,7 @@ It is deliberately not a replacement for [Zipline](https://github.com/diced/zipl
- Auto-downloads on the receiver side — no "click to save" step.
- In-app QR scanner (Chrome/Android) — scan the sender's QR without leaving flit.
- Android share sheet integration — "Share → flit" works from Photos, Files, any app.
- One-way folder sync (`flit watch`) — drop a file into a watched folder, it gets pushed to a trusted peer automatically, no manual send.
---
@@ -101,6 +102,7 @@ For headless machines (homelab boxes, servers) that can't run a browser.
flit send path/to/file.tar.gz # print QR + invite, wait for peer, send
flit recv "flit:eyJhbmNob3..." # join from invite string, accept file
flit daemon # persistent receiver for trusted peers
flit watch [--dry-run] # watch folders, auto-push new files to trusted peers
```
**Built with:** Go, pion/webrtc, libsodium (via nhooyr.io/websocket + internal crypto).
@@ -132,6 +134,38 @@ Copy `cli/daemon.toml.example` to `~/.flit/daemon.toml` to get started. The daem
**Bootstrapping:** to get a peer's `id`, pair once via QR (`flit send` / PWA "Invite new") and copy the hex ID from the "peer connected" log line, or read it from the PWA's Known devices list after tapping "Remember this device."
### Watch mode
`flit watch` is the send-side sibling of `flit daemon`: it watches one or more local folders and automatically pushes new or changed files to a specific trusted peer — no manual `flit send`, no QR, no per-file interaction. It's **one-way sync only** (source → destination, push only) — deliberately not a general bidirectional sync tool, and it never propagates deletes.
The receiving side needs no changes: an already-running `flit daemon`, configured with the watcher's device as a trusted peer, already does the right thing on receipt.
Config lives at `~/.flit/watch.toml`:
```toml
signal_url = "wss://your-anchor.example.com/ws"
turn_url = "" # optional
turn_secret = "" # optional
[[watch]]
dir = "/home/user/backups/documents"
peer_id = "<hex Ed25519 pubkey of the destination device>"
peer_label = "home-server"
pattern = "*.pdf,*.docx" # optional glob include-list; default "*" if omitted
stable_after = "10s" # optional quiet period before considering a file ready
```
Multiple `[[watch]]` blocks are allowed — different folders can go to different peers, or the same peer (entries sharing a `peer_id` share a single connection).
Copy `cli/watch.toml.example` to `~/.flit/watch.toml` to get started. Notes on behavior:
- **Startup reconciliation** — on start, before subscribing to filesystem events, `flit watch` walks each `dir` and diffs it against local state, so anything that changed while the watcher wasn't running gets picked up (not just live events going forward).
- **Debounce (`stable_after`)** — a file is only considered ready to send after this quiet period has passed since its last write, so a large file being actively written isn't offered mid-write.
- **State tracking** — one JSON state file per `[[watch]]` entry lives in `~/.flit/watch-state/`, recording what's been sent. If a send fails partway (peer disconnects mid-transfer), the file is retried on the next successful connection — no separate retry logic needed. Losing this state is harmless (worst case: the whole directory gets re-sent) but not something to engineer around.
- **`flit watch --dry-run`** walks the configured directories and prints what *would* be sent, without connecting to anything — use it to sanity-check a new `watch.toml` before it goes live.
- **`pattern` should be as narrow as your use case allows.** `*` is the default if omitted, but an unconstrained watch on a broad directory turns "sync my documents" into "sync everything that ever lands in this tree." There's no per-file confirmation in watch mode — the directory and pattern in `watch.toml` are the only safety knob.
- Reconnects with the same exponential backoff (2s → 30s cap) as daemon mode, and reuses the same trusted-peer identity model — a `peer_id` here is the same kind of already-known device as anything in the PWA's Known devices list or `daemon.toml`.
---
## Self-hosting