- 六接口路由:init / chunk / merge / check / task / abort - Repository 接口注入存储,宿主实现 12 方法即可接入 - OnMerged / OnInstantHit 回调衔接宿主业务(如写云端文件表) - Envelope 注入响应封装,与宿主 httpx 解耦 - 与 zogo v0.3.2 的 fileupload 子包同源,后续在此仓库独立演进
120 lines
3.1 KiB
Go
120 lines
3.1 KiB
Go
package fileupload
|
||
|
||
import (
|
||
"errors"
|
||
"net/http"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
)
|
||
|
||
// Mount 挂载分片上传路由到 r(宿主自行决定路由组,如 api.Group("/upload")):
|
||
//
|
||
// POST /init 初始化(JSON:fileName/fileSize/fileMD5/mimeType/chunkSize/totalChunk)
|
||
// POST /chunk 上传分片(multipart:uploadId/chunkIndex/chunk),幂等
|
||
// POST /merge 合并分片(JSON:uploadId),幂等
|
||
// GET /check?fileMD5= 秒传/续传检查
|
||
// GET /task/:uploadId 任务详情与已确认分片
|
||
// DELETE /task/:uploadId 取消任务并清理
|
||
//
|
||
// 身份(operator/tenant)由宿主在 handler 外层取好后经 identity 注入;
|
||
// 响应统一走 Options.Envelope。
|
||
type Identity func(c *gin.Context) (operator string, tenantID int64)
|
||
|
||
type mountConfig struct {
|
||
identity Identity
|
||
}
|
||
|
||
// MountOption 挂载选项。
|
||
type MountOption func(*mountConfig)
|
||
|
||
// WithIdentity 注入身份提取(从 JWT 中间件写入的 context 取 UserId/UserName 等)。
|
||
func WithIdentity(fn Identity) MountOption {
|
||
return func(m *mountConfig) { m.identity = fn }
|
||
}
|
||
|
||
// Mount 挂载路由。r 建议为 api.Group("/upload")。
|
||
func Mount(r *gin.RouterGroup, opts Options, repo Repository, mos ...MountOption) error {
|
||
if err := opts.validate(); err != nil {
|
||
return err
|
||
}
|
||
if repo == nil {
|
||
return errors.New("fileupload: Repository 必填")
|
||
}
|
||
mc := &mountConfig{}
|
||
for _, mo := range mos {
|
||
mo(mc)
|
||
}
|
||
svc := newService(opts, repo)
|
||
|
||
r.POST("/init", func(c *gin.Context) {
|
||
var req InitReq
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "参数错误: " + err.Error()})
|
||
return
|
||
}
|
||
operator, tenantID := mc.identity(c)
|
||
resp, err := svc.init(&req, operator, tenantID)
|
||
opts.Envelope(c, resp, err)
|
||
})
|
||
|
||
r.POST("/chunk", func(c *gin.Context) {
|
||
form, err := c.MultipartForm()
|
||
if err != nil {
|
||
opts.Envelope(c, nil, errors.New("请使用 multipart/form-data 上传"))
|
||
return
|
||
}
|
||
files := form.File["chunk"]
|
||
if len(files) != 1 {
|
||
opts.Envelope(c, nil, errors.New("缺少分片文件字段 chunk"))
|
||
return
|
||
}
|
||
chunkIndex := atoiDefault(c.PostForm("chunkIndex"), -1)
|
||
resp, err := svc.chunk(c.PostForm("uploadId"), chunkIndex, files[0])
|
||
if err != nil {
|
||
opts.Envelope(c, nil, err)
|
||
return
|
||
}
|
||
opts.Envelope(c, resp, nil)
|
||
})
|
||
|
||
r.POST("/merge", func(c *gin.Context) {
|
||
var req MergeReq
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "参数错误: " + err.Error()})
|
||
return
|
||
}
|
||
resp, err := svc.merge(req.UploadId)
|
||
opts.Envelope(c, resp, err)
|
||
})
|
||
|
||
r.GET("/check", func(c *gin.Context) {
|
||
resp, err := svc.check(c.Query("fileMD5"))
|
||
opts.Envelope(c, resp, err)
|
||
})
|
||
|
||
r.GET("/task/:uploadId", func(c *gin.Context) {
|
||
resp, err := svc.taskInfo(c.Param("uploadId"))
|
||
opts.Envelope(c, resp, err)
|
||
})
|
||
|
||
r.DELETE("/task/:uploadId", func(c *gin.Context) {
|
||
resp, err := svc.abort(c.Param("uploadId"))
|
||
opts.Envelope(c, resp, err)
|
||
})
|
||
return nil
|
||
}
|
||
|
||
func atoiDefault(s string, def int) int {
|
||
if s == "" {
|
||
return def
|
||
}
|
||
n := 0
|
||
for _, r := range s {
|
||
if r < '0' || r > '9' {
|
||
return def
|
||
}
|
||
n = n*10 + int(r-'0')
|
||
}
|
||
return n
|
||
}
|