158 lines
6.3 KiB
Markdown
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
|