package resumable import ( "encoding/json" "errors" "net/http" "strconv" "github.com/Tencent/WeKnora/internal/logger" "github.com/Tencent/WeKnora/internal/types" "github.com/gin-gonic/gin" "gorm.io/gorm" ) func respond(c *gin.Context, status int, value any, err error) { if err == nil { c.JSON(status, gin.H{"success": true, "data": value}) return } message := publicError(err) switch { case errors.Is(err, gorm.ErrRecordNotFound): status = 404 message = "上传不存在或不属于当前用户" case errors.Is(err, ErrForbidden): status = 403 case errors.Is(err, ErrInvalid): status = 400 case errors.Is(err, ErrConflict): status = 409 case errors.Is(err, ErrQuota): status = 413 default: var input UserError if errors.As(err, &input) { status = 400 } else { status = 500 logger.Errorf(c.Request.Context(), "Upload request failed: %v", err) } } c.AbortWithStatusJSON(status, gin.H{"success": false, "message": message}) } // Register requires the existing authenticated API group. Every operation also // resolves tenant and real-user identity and enforces upload ownership itself. func (s *Service) Register(group *gin.RouterGroup) { r := group.Group("/uploads") r.GET("", func(c *gin.Context) { rows, e := s.List(c.Request.Context(), c.Query("kind"), c.Query("target_id")) if e == nil && c.Query("view") == "summary" { for i := range rows { rows[i].Result = uploadResultSummary(rows[i].Result) } } respond(c, 200, rows, e) }) r.POST("", func(c *gin.Context) { c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, 40<<10) var in CreateInput if c.ShouldBindJSON(&in) != nil { respond(c, 400, nil, ErrInvalid) return } row, e := s.Create(c.Request.Context(), in) respond(c, 201, row, e) }) r.GET("/:upload_id", func(c *gin.Context) { row, e := s.Get(c.Request.Context(), c.Param("upload_id")) respond(c, 200, row, e) }) r.GET("/:upload_id/chunks", func(c *gin.Context) { offset, e := strconv.ParseInt(c.DefaultQuery("offset", "0"), 10, 64) if e != nil { respond(c, 400, nil, ErrInvalid) return } chunks, e := s.Chunks(c.Request.Context(), c.Param("upload_id"), offset) respond(c, 200, chunks, e) }) r.PUT("/:upload_id/chunks", func(c *gin.Context) { offset, e := strconv.ParseInt(c.GetHeader("Upload-Offset"), 10, 64) if e != nil { respond(c, 400, nil, ErrInvalid) return } c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, ChunkSize) row, e := s.Put(c.Request.Context(), c.Param("upload_id"), offset, c.Request.ContentLength, c.GetHeader("Upload-Checksum"), c.Request.Body) respond(c, 200, row, e) }) r.POST("/:upload_id/finalize", func(c *gin.Context) { row, e := s.Finalize(c.Request.Context(), c.Param("upload_id")) respond(c, 202, row, e) }) r.DELETE("/:upload_id", func(c *gin.Context) { e := s.Cancel(c.Request.Context(), c.Param("upload_id")) respond(c, 200, gin.H{"cancelled": e == nil}, e) }) } // Shell polling needs progress and the extraction mode, not extracted source // text. Full results remain available on the owned upload's detail endpoint. func uploadResultSummary(result types.JSON) types.JSON { var summary struct { Selection string `json:"selection,omitempty"` } if json.Unmarshal(result, &summary) != nil || (summary.Selection != "full" && summary.Selection != "sampled") { return nil } encoded, _ := json.Marshal(summary) return types.JSON(encoded) }