feat: implémentation complete v0.1.0 — daemon de sync clipboard bidirectionnel
CI / Lint (push) Failing after 14m46s
CI / Test (push) Failing after 4m23s
CI / Build (darwin/amd64) (push) Skipped
CI / Build (linux/amd64) (push) Skipped
CI / Build (windows/amd64) (push) Skipped
CI / Build (darwin/arm64) (push) Skipped
CI / Build (linux/arm64) (push) Skipped
CI / Build (windows/arm64) (push) Skipped
CI / Release (push) Skipped
CI / Lint (push) Failing after 14m46s
CI / Test (push) Failing after 4m23s
CI / Build (darwin/amd64) (push) Skipped
CI / Build (linux/amd64) (push) Skipped
CI / Build (windows/amd64) (push) Skipped
CI / Build (darwin/arm64) (push) Skipped
CI / Build (linux/arm64) (push) Skipped
CI / Build (windows/arm64) (push) Skipped
CI / Release (push) Skipped
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
// Package peer sends clipboard content to remote clip-sync peers via HTTP POST.
|
||||
package peer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"git.dracodev.net/Projets/clip-sync/internal/config"
|
||||
"git.dracodev.net/Projets/clip-sync/internal/dedup"
|
||||
)
|
||||
|
||||
const clipPath = "/clip"
|
||||
|
||||
// Broadcaster sends clip payloads to all configured peers concurrently.
|
||||
type Broadcaster struct {
|
||||
peers []config.PeerConfig
|
||||
origin string
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
// NewBroadcaster creates a Broadcaster for the configured peer list.
|
||||
func NewBroadcaster(peers []config.PeerConfig, origin string) *Broadcaster {
|
||||
return &Broadcaster{
|
||||
peers: peers,
|
||||
origin: origin,
|
||||
client: &http.Client{
|
||||
Timeout: 5 * time.Second,
|
||||
Transport: &http.Transport{
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: 3 * time.Second,
|
||||
}).DialContext,
|
||||
MaxIdleConnsPerHost: 2,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Broadcast sends the given text to all configured peers in parallel.
|
||||
// Errors are logged but not returned — a single unreachable peer should not
|
||||
// block other peers or the daemon loop.
|
||||
func (b *Broadcaster) Broadcast(text string) {
|
||||
payload := dedup.Payload{
|
||||
Text: text,
|
||||
Ts: time.Now().UnixNano(),
|
||||
Origin: b.origin,
|
||||
}
|
||||
|
||||
body, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
log.Printf("peer: marshal payload: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
for i := range b.peers {
|
||||
go b.sendToPeer(b.peers[i], body)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Broadcaster) sendToPeer(p config.PeerConfig, body []byte) {
|
||||
url := fmt.Sprintf("http://%s%s", p.Addr, clipPath)
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
log.Printf("peer [%s]: create request: %v", p.Name, err)
|
||||
return
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := b.client.Do(req)
|
||||
if err != nil {
|
||||
log.Printf("peer [%s] %s: %v", p.Name, p.Addr, err)
|
||||
return
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusNoContent {
|
||||
log.Printf("peer [%s] %s: unexpected status %d", p.Name, p.Addr, resp.StatusCode)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package peer
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.dracodev.net/Projets/clip-sync/internal/config"
|
||||
"git.dracodev.net/Projets/clip-sync/internal/dedup"
|
||||
)
|
||||
|
||||
func TestBroadcastSendsPayload(t *testing.T) {
|
||||
received := make(chan dedup.Payload, 1)
|
||||
|
||||
// Start a fake peer server.
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
t.Errorf("expected POST, got %s", r.Method)
|
||||
}
|
||||
if r.URL.Path != "/clip" {
|
||||
t.Errorf("expected /clip, got %s", r.URL.Path)
|
||||
}
|
||||
|
||||
var p dedup.Payload
|
||||
if err := json.NewDecoder(r.Body).Decode(&p); err != nil {
|
||||
t.Errorf("decode: %v", err)
|
||||
}
|
||||
received <- p
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
peers := []config.PeerConfig{
|
||||
{Name: "test-peer", Addr: ts.Listener.Addr().String()},
|
||||
}
|
||||
|
||||
b := NewBroadcaster(peers, "my-machine")
|
||||
b.Broadcast("hello, peer!")
|
||||
|
||||
select {
|
||||
case p := <-received:
|
||||
if p.Text != "hello, peer!" {
|
||||
t.Errorf("text = %q, want \"hello, peer!\"", p.Text)
|
||||
}
|
||||
if p.Origin != "my-machine" {
|
||||
t.Errorf("origin = %q, want \"my-machine\"", p.Origin)
|
||||
}
|
||||
if p.Ts == 0 {
|
||||
t.Error("timestamp should be non-zero")
|
||||
}
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("timeout waiting for broadcast")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBroadcastEmptyPeers(t *testing.T) {
|
||||
// Should not panic or error with zero peers.
|
||||
b := NewBroadcaster(nil, "my-machine")
|
||||
b.Broadcast("test")
|
||||
b.Broadcast("")
|
||||
}
|
||||
Reference in New Issue
Block a user