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