2026-09-22 16:19:06 +08:00
|
|
|
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) {
|
2026-09-23 19:20:13 +08:00
|
|
|
summary := c.Query("view") == "summary"
|
|
|
|
|
rows, e := s.List(c.Request.Context(), c.Query("kind"), c.Query("target_id"), summary)
|
2026-09-22 16:19:06 +08:00
|
|
|
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)
|
|
|
|
|
}
|