Files
xinmi/internal/resumable/handler.go
T
yiandCursor 9fb2c1321a
App / Format, vet, test, and build (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
fix: store digital-human upload text in files, slim jsonb for list
Avoid pulling multi-MB result payloads on uploads?view=summary over remote DB, which caused 30s client timeouts and HTTP 500.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-23 19:20:13 +08:00

109 lines
3.3 KiB
Go

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) {
summary := c.Query("view") == "summary"
rows, e := s.List(c.Request.Context(), c.Query("kind"), c.Query("target_id"), summary)
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)
}