Files
Commilitia-Drop/internal/httpapi/transfer.go
T
admin f21fa5b5e8 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)。

本次为公开发布初始提交:完整开发历史(含部署细节)留存于私有归档,公开仓库自此干净起步。
2026-06-15 21:38:28 +08:00

95 lines
2.8 KiB
Go

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()})
}
}
}