Files
fileupload/mount.go
T
4566704 011eb4348b feat: 独立通用大文件分片上传服务端核心(自 zogo/fileupload v0.3.2 抽出)
- 六接口路由:init / chunk / merge / check / task / abort
- Repository 接口注入存储,宿主实现 12 方法即可接入
- OnMerged / OnInstantHit 回调衔接宿主业务(如写云端文件表)
- Envelope 注入响应封装,与宿主 httpx 解耦
- 与 zogo v0.3.2 的 fileupload 子包同源,后续在此仓库独立演进
2026-09-25 15:21:21 +08:00

120 lines
3.1 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}