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,94 @@
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"commilitia.net/cdrop/internal/jwtauth"
|
||||
"commilitia.net/cdrop/internal/transfer"
|
||||
)
|
||||
|
||||
type initReq struct {
|
||||
ReceiverName string `json:"receiver_name"`
|
||||
FileName string `json:"file_name"`
|
||||
FileSize int64 `json:"file_size"`
|
||||
FileSHA256 string `json:"file_sha256,omitempty"`
|
||||
}
|
||||
|
||||
type initResp struct {
|
||||
SessionID string `json:"session_id"`
|
||||
}
|
||||
|
||||
func (s *Server) handleTransferInit(w http.ResponseWriter, r *http.Request) {
|
||||
claims, _ := jwtauth.ClaimsFromContext(r.Context())
|
||||
sender, _ := jwtauth.DeviceNameFromContext(r.Context())
|
||||
|
||||
var req initReq
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
|
||||
return
|
||||
}
|
||||
|
||||
id, err := s.transfers.Init(r.Context(), transfer.InitParams{
|
||||
UserID: claims.UserID,
|
||||
SenderName: sender,
|
||||
ReceiverName: req.ReceiverName,
|
||||
FileName: req.FileName,
|
||||
FileSize: req.FileSize,
|
||||
FileSHA256: req.FileSHA256,
|
||||
})
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, initResp{SessionID: id})
|
||||
}
|
||||
|
||||
// handleTransferTransition is the shared body for accept / cancel / fallback /
|
||||
// done / fail. The target state and any side-fields are bound by the route.
|
||||
type transitionReq struct {
|
||||
Reason string `json:"reason,omitempty"`
|
||||
BytesTransferred int64 `json:"bytes_transferred,omitempty"`
|
||||
}
|
||||
|
||||
func (s *Server) transitionHandler(target, mode string) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
claims, _ := jwtauth.ClaimsFromContext(r.Context())
|
||||
id := chi.URLParam(r, "id")
|
||||
if id == "" {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "missing id"})
|
||||
return
|
||||
}
|
||||
|
||||
var req transitionReq
|
||||
if r.ContentLength > 0 {
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
err := s.transfers.Transition(
|
||||
r.Context(), claims.UserID, id, target,
|
||||
mode, req.Reason, req.BytesTransferred,
|
||||
)
|
||||
switch {
|
||||
case err == nil:
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
case errors.Is(err, transfer.ErrNotFound):
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": "not found"})
|
||||
case errors.Is(err, transfer.ErrForbidden):
|
||||
writeJSON(w, http.StatusForbidden, map[string]string{"error": "forbidden"})
|
||||
case errors.Is(err, transfer.ErrInvalidTransition):
|
||||
writeJSON(w, http.StatusConflict, map[string]string{"error": err.Error()})
|
||||
case errors.Is(err, transfer.ErrRelayUnavailable):
|
||||
writeJSON(w, http.StatusServiceUnavailable,
|
||||
map[string]string{"error": "relay unavailable, retry later"})
|
||||
default:
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user