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 // + 可选 targetDir:目标目录,相对 CloudDir,空=根目录/固定路径模式) // POST /chunk 上传分片(multipart:uploadId/chunkIndex/chunk),幂等 // POST /merge 合并分片(JSON:uploadId),幂等 // GET /check?fileMD5=&targetDir= 秒传/续传检查(targetDir 可选) // 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"), c.Query("targetDir")) 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 }