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)。 本次为公开发布初始提交:完整开发历史(含部署细节)留存于私有归档,公开仓库自此干净起步。
109 lines
3.3 KiB
Go
109 lines
3.3 KiB
Go
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
|
|
}
|