Files
clip-sync/main.go
T

233 lines
5.8 KiB
Go

// clip-sync — bidirectional LAN clipboard synchronization daemon.
//
// Usage:
//
// clip-sync [flags] # run the daemon
// clip-sync history # print local clipboard history
// clip-sync send-file <path> # send a file to all peers
// clip-sync --one-shot [text] # send the clipboard (or text) once, then exit
//
// Flags:
//
// --config path config file (default ~/.config/clip-sync/peers.toml)
// --version print version and exit
// --generate-key generate a shared key and exit
// --generate-cert generate a self-signed TLS cert/key and exit
package main
import (
"context"
"flag"
"fmt"
"log"
"os"
"os/signal"
"strings"
"syscall"
"time"
"git.dracodev.net/Projets/clip-sync/internal/clipboard"
"git.dracodev.net/Projets/clip-sync/internal/config"
"git.dracodev.net/Projets/clip-sync/internal/daemon"
"git.dracodev.net/Projets/clip-sync/internal/history"
"git.dracodev.net/Projets/clip-sync/internal/peer"
"git.dracodev.net/Projets/clip-sync/internal/security"
"git.dracodev.net/Projets/clip-sync/internal/transfer"
)
// version is overridden at build time via -ldflags "-X main.version=...".
var version = "0.2.0"
func main() {
log.SetFlags(log.LstdFlags | log.Lshortfile)
log.SetPrefix("")
fs := flag.NewFlagSet("clip-sync", flag.ExitOnError)
configPath := fs.String("config", "", "config file path")
showVersion := fs.Bool("version", false, "print version and exit")
generateKey := fs.Bool("generate-key", false, "generate a shared key and exit")
generateCert := fs.Bool("generate-cert", false, "generate a self-signed TLS certificate and exit")
oneShot := fs.Bool("one-shot", false, "send the clipboard once and exit")
fs.Usage = func() {
fmt.Fprintf(os.Stderr, "Usage:\n clip-sync [flags]\n clip-sync history\n\nFlags:\n")
fs.PrintDefaults()
}
_ = fs.Parse(os.Args[1:])
// Subcommand: history
if fs.NArg() > 0 && fs.Arg(0) == "history" {
runHistory()
return
}
if *showVersion {
fmt.Printf("clip-sync %s\n", version)
return
}
if *generateKey {
key, err := security.GenerateKey()
if err != nil {
log.Fatalf("clip-sync: generate key: %v", err)
}
fmt.Println(key)
return
}
if *generateCert {
runGenerateCert()
return
}
cfg, err := loadConfig(*configPath)
if err != nil {
log.Fatalf("clip-sync: config: %v", err)
}
if *oneShot {
runOneShot(cfg, fs.Arg(0))
return
}
// Subcommand: send-file <path>
if fs.NArg() > 0 && fs.Arg(0) == "send-file" {
path := ""
if fs.NArg() > 1 {
path = fs.Arg(1)
}
runSendFile(cfg, path)
return
}
runDaemon(cfg)
}
func loadConfig(path string) (*config.Config, error) {
if path != "" {
return config.LoadFrom(path)
}
return config.Load()
}
func runDaemon(cfg *config.Config) {
d, err := daemon.New(cfg)
if err != nil {
log.Fatalf("clip-sync: init: %v", err)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
go func() {
sig := <-sigCh
log.Printf("clip-sync: received %v, shutting down...", sig)
cancel()
}()
if err := d.Run(ctx); err != nil && err != context.Canceled {
log.Fatalf("clip-sync: %v", err)
}
log.Println("clip-sync: stopped")
}
func runOneShot(cfg *config.Config, textArg string) {
origin, err := os.Hostname()
if err != nil {
log.Fatalf("clip-sync: hostname: %v", err)
}
b := peer.NewBroadcaster(cfg.Peers, origin, peer.Options{
SharedKey: cfg.Security.SharedKey,
TLS: cfg.Security.TLS,
CertFile: cfg.Security.CertFile,
InsecureSkipVerify: cfg.Security.InsecureSkipVerify,
})
text := textArg
if text == "" {
cb, err := clipboard.New()
if err != nil {
log.Fatalf("clip-sync: clipboard: %v", err)
}
text, err = cb.Read()
if err != nil {
log.Fatalf("clip-sync: clipboard read: %v", err)
}
}
if text == "" {
log.Fatal("clip-sync: nothing to send (clipboard is empty)")
}
b.BroadcastSync(text)
fmt.Printf("clip-sync: sent %d bytes to %d peer(s)\n", len(text), b.PeerCount())
}
func runSendFile(cfg *config.Config, path string) {
if path == "" {
log.Fatal("clip-sync: usage: clip-sync send-file <path>")
}
origin, err := os.Hostname()
if err != nil {
log.Fatalf("clip-sync: hostname: %v", err)
}
f, err := transfer.ReadFile(path, origin)
if err != nil {
log.Fatalf("clip-sync: read file: %v", err)
}
b := peer.NewBroadcaster(cfg.Peers, origin, peer.Options{
SharedKey: cfg.Security.SharedKey,
TLS: cfg.Security.TLS,
CertFile: cfg.Security.CertFile,
InsecureSkipVerify: cfg.Security.InsecureSkipVerify,
})
b.SendFileSync(*f)
fmt.Printf("clip-sync: sent %q (%s) to %d peer(s)\n", f.Name, f.Mime, b.PeerCount())
}
func runHistory() {
path, err := history.DefaultPath()
if err != nil {
log.Fatalf("clip-sync: history: %v", err)
}
entries := history.New(path, 0).List()
if len(entries) == 0 {
fmt.Println("clip-sync: no history yet")
return
}
for _, e := range entries {
ts := time.Unix(0, e.Ts).Format("2006-01-02 15:04:05")
text := strings.ReplaceAll(e.Text, "\n", "\\n")
if len(text) > 80 {
text = text[:80] + "…"
}
fmt.Printf("%s [%s] %s\n", ts, e.Origin, text)
}
}
func runGenerateCert() {
certFile, err := config.DefaultCertFile()
if err != nil {
log.Fatalf("clip-sync: cert path: %v", err)
}
keyFile, err := config.DefaultKeyFile()
if err != nil {
log.Fatalf("clip-sync: key path: %v", err)
}
host, _ := os.Hostname()
if err := security.GenerateSelfSignedCert(certFile, keyFile, []string{host, "localhost"}); err != nil {
log.Fatalf("clip-sync: generate cert: %v", err)
}
fmt.Printf("clip-sync: certificate written to %s and %s\n", certFile, keyFile)
fmt.Println("Set [security] tls = true and cert_file/key_file in peers.toml, and share these files with your peers.")
}