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"` // ReceiverOnline is false when the receiver had no live SSE at init time (a wake push was // sent). The sender uses it to wait for the receiver to come online before offering, rather // than immediately failing/relaying to an absent peer. ReceiverOnline bool `json:"receiver_online"` } 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, receiverOnline, 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, ReceiverOnline: receiverOnline}) } // 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()}) } } }