# ARCHITECTURE.md — clip-sync ## Vue d'ensemble clip-sync est un daemon de synchronisation bidirectionnelle de presse-papiers en mesh pair-à-pair sur réseau local. Chaque nœud est à la fois client et serveur. ## Flux de données ``` ┌────────────────────────────────────┐ │ clip-sync (nœud) │ │ │ Presse-papiers ──►│ Poller (500ms) │ (Ctrl+C) │ │ lecture (OS clipboard) │ │ ▼ │ │ Comparaison (vs dernier texte) │ │ │ si nouveau texte │ │ ▼ │ │ Filtre anti-boucle │ │ │ MarkWritten(hash, ts) │ │ ▼ │ │ Broadcaster │ │ │ POST /clip → chaque pair │ │ │ goroutine par pair │ └────┼───────────────────────────────┘ │ ▼ ┌───────────────────────┐ │ Pair distant :9137 │ │ POST /clip {json} │ │ ↓ │ │ Filtre anti-boucle │ │ ↓ (si accepté) │ │ Write clipboard │ │ ↓ (Ctrl+V possible)│ └───────────────────────┘ ``` ## Composants ### 1. Clipboard (`internal/clipboard/`) Interface unifiée `Read() / Write()` avec implémentations par plateforme. L'interface optionnelle `ImageClipboard` (`ReadImage` / `WriteImage`) ajoute la sync d'images PNG (xclip/wl-clipboard sur Linux, CF_DIB sur Windows, AppleScript sur macOS) : - **Linux** : auto-détection X11/Wayland via `$XDG_SESSION_TYPE` - X11 → `xclip -selection clipboard -o` / `xclip -selection clipboard` - Wayland → `wl-paste` / `wl-copy` - **Windows** : Win32 API via `syscall` (user32.dll, kernel32.dll), zéro CGo - **macOS** : `pbpaste` / `pbcopy` ### 2. Configuration (`internal/config/`) - Fichier TOML : `~/.config/clip-sync/peers.toml` - Parsing : `github.com/BurntSushi/toml` (seule dépendance externe — TOML absent de stdlib) - Override par variables d'environnement : `CLIP_SYNC_PORT`, `CLIP_SYNC_POLL_MS` ### 3. Anti-boucle (`internal/dedup/`) Deux garde-fous empêchent les boucles de synchronisation : | Garde-fou | Mécanisme | Scénario bloqué | |-----------|-----------|-----------------| | **Hash** | Cache LRU (20 derniers) des SHA-256 locaux | Notre texte nous revient via un pair | | **Origine** | Comparaison `origin == hostname` | Rebond via 3e pair | | **Timestamp** | `ts <= lastWriteTs` | Contournement multi-pair | Payload JSON : ```json { "text": "contenu du presse-papiers", "ts": 1690000000000000000, "origin": "nom-de-machine" } ``` ### 4. Communication pair-à-pair (`internal/peer/`, `internal/server/`) - Protocole : HTTP POST `/clip` (JSON), réponse `204 No Content` - Timeout connexion : 3s (dial), 5s (request) - Chaque broadcast aux pairs est lancé dans sa propre goroutine (non-bloquant) - Échec d'un pair = log + continue (pas de blocage des autres) ### 5. Daemon (`internal/daemon/`) Boucle principale : 1. Démarre le serveur HTTP dans une goroutine 2. Ticker 500ms : lit le presse-papiers, compare, broadcast si changé 3. Signal SIGINT/SIGTERM → arrêt gracieux ### 6. Sécurité (`internal/security/`) - `--generate-key` : clé partagée hex (256 bits) - `--generate-cert` : certificat ECDSA P-256 auto-signé - Auth Bearer token (comparaison à temps constant), validation d'origine ### 7. Historique (`internal/history/`) - Anneau mémoire borné (`history_size`), persisté en JSON - Commandes `clip-sync history` et endpoint `/history` ### 8. Transfert de fichiers (`internal/transfer/`) - Payload `{name, mime, data(base64), ts, origin}` via `POST /file` - Réception opt-in (`receive_files`), écrit dans `receive_dir` - Nom de fichier assaini (anti path-traversal) ### 9. Découverte (`internal/discovery/`) et notifications (`internal/notify/`) - mDNS `_clip-sync._tcp` (hashicorp/mdns), activation via `daemon.discovery` - Notifications desktop : notify-send (Linux), osascript (macOS), Shell_NotifyIcon (Windows) ## Dépendances | Dépendance | Version | Justification | |-----------|---------|---------------| | `github.com/BurntSushi/toml` | v1.3.2 | Parsing TOML (stdlib Go n'inclut pas TOML) | | `github.com/hashicorp/mdns` | v1.0.7 | Découverte mDNS (optionnelle, `daemon.discovery`) | **Total : 2 dépendances externes directes.** Tout le reste est stdlib Go. mdns est optionnel (utilisé uniquement si `daemon.discovery = true`). ## Décisions architecturales ### Pourquoi HTTP et pas TCP raw / gRPC ? - Débogable avec `curl` - `net/http` est dans la stdlib, zéro dépendance - Le surcoût HTTP est négligeable pour du texte de presse-papiers - Facile à étendre (TLS, auth headers) dans les versions futures ### Pourquoi TOML et pas YAML/JSON ? - TOML est le format standard de configuration en Go - Plus lisible que JSON pour des humains - Pas de problème de whitespace-significant comme YAML ### Pourquoi pas mDNS en v1.0 ? - Philosophie du projet : configuration explicite, zéro magie - mDNS ajoute une complexité significative (multicast, découverte, timeouts) - Prévu pour la v1.0 dans la roadmap ### Pourquoi du polling (500ms) et pas des events OS ? - Les APIs de notification de presse-papiers sont non-portables (Win32 `AddClipboardFormatListener`, X11 `XFixes`, etc.) - Le polling 500ms est un compromis acceptable entre réactivité et CPU - Un seul `Read()` par tick, coût négligeable ### Pourquoi un cache LRU de 20 hash et pas plus ? - 20 entrées couvrent ~10 secondes de copies rapides à 500ms/poll - Au-delà, le risque de collision est négligeable (SHA-256) - Garde la mémoire constante et prévisible