6.2 KiB
6.2 KiB
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 :
- Linux : auto-détection X11/Wayland via
$XDG_SESSION_TYPE- X11 →
xclip -selection clipboard -o/xclip -selection clipboard - Wayland →
wl-paste/wl-copy
- X11 →
- 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 :
{
"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éponse204 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 :
- Démarre le serveur HTTP dans une goroutine
- Ticker 500ms : lit le presse-papiers, compare, broadcast si changé
- 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 historyet endpoint/history
8. Transfert de fichiers (internal/transfer/)
- Payload
{name, mime, data(base64), ts, origin}viaPOST /file - Réception opt-in (
receive_files), écrit dansreceive_dir - Nom de fichier assaini (anti path-traversal)
9. Découverte (internal/discovery/) et notifications (internal/notify/)
- mDNS
_clip-sync._tcp(hashicorp/mdns), activation viadaemon.discovery - Notifications desktop : notify-send (Linux), osascript (macOS), no-op (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/httpest 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, X11XFixes, 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