Files
zogo/fileupload/mount.go
T
4566704 33574d4ac2 feat(fileupload): 并入通用大文件分片上传服务端核心
- 六接口(init/chunk/merge/check/task/abort)+ 分片幂等 + 断点续传 +
  .part 原子改名 + 合并整文件 MD5 校验 + 秒传
- 零宿主耦合:Repository 接口注入存储、Envelope 注入响应封装、
  OnMerged/OnInstantHit 回调衔接业务(仅依赖既有 gin/uuid)
- 自 FileUpload 项目 restful/upload 抽出;maintain-service 云端文件为其首个宿主
2026-09-25 15:11:30 +08:00

120 lines
3.1 KiB
Go
Raw 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
}