f21fa5b5e8
Commilitia Drop:自托管的跨设备剪贴板同步与点对点文件传输。 - 后端 Go(chi / SQLite WAL / SSE Hub / WebRTC signaling + 状态机 / Relay ring buffer),编译进单个 distroless 镜像(前端 go:embed)。 - 前端 React + TanStack Router + Zustand,自实现 SSE + WebRTC P2P,NAT 受阻时回退服务端中继;聚珍(Juzhen)CJK 综合排版。 - 桌面端 Wails v2(macOS / Windows),瘦客户端复用 web。 - 鉴权 OIDC PKCE(自建 Casdoor 等),refresh_token 信封加密存系统密钥库;iOS Shortcut 用 HS256 scoped token。 架构文档与变更记录见 docs 分支(PROJECT_BRIEF / FRONTEND_DESIGN / CHANGELOG)。 本次为公开发布初始提交:完整开发历史(含部署细节)留存于私有归档,公开仓库自此干净起步。
152 lines
4.8 KiB
Go
152 lines
4.8 KiB
Go
package platform
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"sync"
|
|
)
|
|
|
|
// ClipboardEvent is one observed change of the local clipboard.
|
|
type ClipboardEvent struct {
|
|
Text string
|
|
// Sensitive is set by the platform Source when the pasteboard carries a
|
|
// privacy marker — on macOS org.nspasteboard.ConcealedType / TransientType /
|
|
// AutoGeneratedType or a known password-manager UTI; on Windows the
|
|
// ExcludeClipboardContentFromMonitorProcessing / CanUploadToCloudClipboard
|
|
// formats. Sensitive events are never uploaded.
|
|
Sensitive bool
|
|
}
|
|
|
|
// Sink receives clipboard text that the policy decided should be uploaded.
|
|
type Sink func(text string)
|
|
|
|
// Source is the platform-specific clipboard backend: it watches for local
|
|
// changes and can write text back. Implementations live in clipboard_darwin.go
|
|
// / clipboard_windows.go. The upstream golang.design/x/clipboard library only
|
|
// surfaces the changed text (no change-count, no pasteboard types), so the
|
|
// Sensitive flag and the upload policy below are this package's responsibility,
|
|
// not the library's.
|
|
type Source interface {
|
|
// Watch emits an event on every observed local clipboard change until ctx
|
|
// is cancelled, then closes the channel. The implementation sets Sensitive
|
|
// from the pasteboard's privacy markers.
|
|
Watch(ctx context.Context) <-chan ClipboardEvent
|
|
// Write replaces the local clipboard text.
|
|
Write(text string) error
|
|
}
|
|
|
|
// Monitor applies cdrop's upload policy on top of a raw clipboard event stream:
|
|
// it drops sensitive content, de-duplicates repeats, and suppresses the echo of
|
|
// our own writes (the loop guard, PLAN.md R4). Everything here is pure and
|
|
// host-independent, so it is unit-tested without a real clipboard; the
|
|
// platform Source produces the raw events and performs the OS writes.
|
|
//
|
|
// Debounce of rapid successive copies is intentionally left to the upload
|
|
// callback (wrap it with github.com/bep/debounce in the app wiring) so the
|
|
// policy stays free of timers and trivially testable.
|
|
type Monitor struct {
|
|
upload Sink
|
|
|
|
mu sync.Mutex
|
|
lastHash string // hash of the last text we accepted (dedup)
|
|
selfHash string // hash of the text we most recently wrote ourselves (loop guard)
|
|
}
|
|
|
|
// NewMonitor builds a Monitor that forwards accepted text to upload.
|
|
func NewMonitor(upload Sink) *Monitor {
|
|
return &Monitor{upload: upload}
|
|
}
|
|
|
|
// Observe runs the policy for one raw event. It returns true when the event was
|
|
// accepted and forwarded to the sink, false when it was filtered out. Safe to
|
|
// call concurrently with ApplyRemote — the watch goroutine and the download
|
|
// path share one Monitor.
|
|
func (m *Monitor) Observe(ev ClipboardEvent) bool {
|
|
if ev.Sensitive || ev.Text == "" {
|
|
return false
|
|
}
|
|
h := hashText(ev.Text)
|
|
|
|
m.mu.Lock()
|
|
if h == m.selfHash {
|
|
// Echo of our own write: consume it once (don't re-upload) and disarm,
|
|
// so a genuine later re-copy of the same text still passes.
|
|
m.selfHash = ""
|
|
m.lastHash = h
|
|
m.mu.Unlock()
|
|
return false
|
|
}
|
|
if h == m.lastHash {
|
|
m.mu.Unlock()
|
|
return false // unchanged content re-observed
|
|
}
|
|
m.lastHash = h
|
|
m.mu.Unlock()
|
|
|
|
m.upload(ev.Text) // outside the lock: the sink may block (network / IPC)
|
|
return true
|
|
}
|
|
|
|
// WillWrite must be called immediately before the Source writes text to the
|
|
// local clipboard, so the resulting change notification (carrying the same
|
|
// text) is recognised as our own echo and not re-uploaded.
|
|
func (m *Monitor) WillWrite(text string) {
|
|
m.mu.Lock()
|
|
m.selfHash = hashText(text)
|
|
m.mu.Unlock()
|
|
}
|
|
|
|
// ApplyRemote writes a remote clipboard change to the local clipboard (the
|
|
// download direction), skipping our own uploads echoed back by the server and
|
|
// content we already hold. It arms the loop guard before writing, so the
|
|
// Source's resulting change event is not re-uploaded. Returns true if it wrote.
|
|
func (m *Monitor) ApplyRemote(src Source, cc ClipboardContent, selfDevice string) bool {
|
|
if cc.Content == "" {
|
|
return false
|
|
}
|
|
if selfDevice != "" && cc.SourceDevice == selfDevice {
|
|
return false // our own upload, echoed back by the server
|
|
}
|
|
h := hashText(cc.Content)
|
|
|
|
m.mu.Lock()
|
|
if h == m.lastHash {
|
|
m.mu.Unlock()
|
|
return false // already the current local content
|
|
}
|
|
m.selfHash = h // arm loop guard inline (avoid re-locking via WillWrite)
|
|
m.mu.Unlock()
|
|
|
|
if err := src.Write(cc.Content); err != nil {
|
|
return false
|
|
}
|
|
|
|
m.mu.Lock()
|
|
m.lastHash = h
|
|
m.mu.Unlock()
|
|
return true
|
|
}
|
|
|
|
// Run drives src's events through m until ctx is cancelled or the source closes.
|
|
// It is the glue the app uses; all policy decisions live in Monitor.
|
|
func Run(ctx context.Context, src Source, m *Monitor) {
|
|
ch := src.Watch(ctx)
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case ev, ok := <-ch:
|
|
if !ok {
|
|
return
|
|
}
|
|
m.Observe(ev)
|
|
}
|
|
}
|
|
}
|
|
|
|
func hashText(s string) string {
|
|
sum := sha256.Sum256([]byte(s))
|
|
return hex.EncodeToString(sum[:])
|
|
}
|