Files
clip-sync/ARCHITECTURE.md
T

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
  • 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é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), 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/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