cdrop — 跨 OS 剪贴板与文件传输服务
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)。 本次为公开发布初始提交:完整开发历史(含部署细节)留存于私有归档,公开仓库自此干净起步。
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
package platform
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ClipboardMaxBytes mirrors the backend's CDROP_CLIPBOARD_MAX_BYTES (default
|
||||
// 64 KiB). Uploads larger than this are rejected client-side, matching the web.
|
||||
const ClipboardMaxBytes = 64 * 1024
|
||||
|
||||
// Client is an authenticated client for the cdrop backend API. The access token
|
||||
// is held here in Go (never in the WebView); deviceName identifies this device
|
||||
// as the source of clipboard writes via the X-Device-Name header.
|
||||
type Client struct {
|
||||
baseURL string
|
||||
token string
|
||||
deviceName string
|
||||
http *http.Client
|
||||
}
|
||||
|
||||
// NewClient builds a Client. baseURL is the backend origin, e.g.
|
||||
// https://drop.commilitia.net.
|
||||
func NewClient(baseURL, accessToken, deviceName string) *Client {
|
||||
return &Client{
|
||||
baseURL: strings.TrimRight(baseURL, "/"),
|
||||
token: accessToken,
|
||||
deviceName: deviceName,
|
||||
http: &http.Client{Timeout: 30 * time.Second},
|
||||
}
|
||||
}
|
||||
|
||||
// ClipboardContent is the cloud clipboard state. UpdatedAt is the server's
|
||||
// authoritative timestamp (also the ETag); 0 means never written.
|
||||
type ClipboardContent struct {
|
||||
Content string `json:"content"`
|
||||
ContentType string `json:"content_type"`
|
||||
SourceDevice string `json:"source_device"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (c *Client) authReq(ctx context.Context, method, path string, body io.Reader) (*http.Request, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, method, c.baseURL+path, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+c.token)
|
||||
req.Header.Set("Accept", "application/json")
|
||||
if c.deviceName != "" {
|
||||
req.Header.Set("X-Device-Name", c.deviceName)
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// Clipboard fetches the current cloud clipboard content.
|
||||
func (c *Client) Clipboard(ctx context.Context) (ClipboardContent, error) {
|
||||
req, err := c.authReq(ctx, http.MethodGet, "/api/clipboard", nil)
|
||||
if err != nil {
|
||||
return ClipboardContent{}, fmt.Errorf("api: build clipboard request: %w", err)
|
||||
}
|
||||
resp, err := c.http.Do(req)
|
||||
if err != nil {
|
||||
return ClipboardContent{}, fmt.Errorf("api: clipboard request: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode/100 != 2 {
|
||||
return ClipboardContent{}, fmt.Errorf("api: GET clipboard status %d", resp.StatusCode)
|
||||
}
|
||||
var cc ClipboardContent
|
||||
if err := json.NewDecoder(resp.Body).Decode(&cc); err != nil {
|
||||
return ClipboardContent{}, fmt.Errorf("api: decode clipboard: %w", err)
|
||||
}
|
||||
return cc, nil
|
||||
}
|
||||
|
||||
// PutClipboard uploads plain text to the cloud clipboard.
|
||||
func (c *Client) PutClipboard(ctx context.Context, text string) error {
|
||||
if len(text) > ClipboardMaxBytes {
|
||||
return fmt.Errorf("api: clipboard content %d bytes exceeds limit %d", len(text), ClipboardMaxBytes)
|
||||
}
|
||||
payload, err := json.Marshal(map[string]string{
|
||||
"content": text,
|
||||
"content_type": "text/plain",
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("api: marshal clipboard: %w", err)
|
||||
}
|
||||
req, err := c.authReq(ctx, http.MethodPut, "/api/clipboard", bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
return fmt.Errorf("api: build clipboard request: %w", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, err := c.http.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("api: clipboard upload: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode/100 != 2 {
|
||||
return fmt.Errorf("api: PUT clipboard status %d", resp.StatusCode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user