Files
clip-sync/ARCHITECTURE.md
T

158 lines
6.3 KiB
Markdown

# 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