// Package fileupload 通用大文件分片上传(Go/Gin 服务端核心)。 // // 职责:init/chunk/merge/check/task/abort 六接口的路由挂载与业务逻辑—— // 分片幂等、断点续传、.part 原子改名、合并时整文件 MD5 校验、秒传。 // 宿主差异全部通过 Options 与 Repository 注入,本包不依赖任何宿主的 // 响应封装、ORM 生成代码或业务表: // // fileupload.Mount(api.Group("/upload"), opts, repo) // // 与业务层的衔接: // - merge 成功 → OnMerged(TaskInfo)(宿主写自己的业务表/云盘记录); // 回调失败则 merge 整体失败、任务置 error // - 秒传命中 → OnInstantHit(TaskInfo)(宿主确保业务侧可见行,可空回调) // - 响应封装 → Options.Envelope(必填,如 httpx.HandleResult / gin.H{code:200,...}) package fileupload import ( "errors" "math" "path" "strings" "github.com/gin-gonic/gin" "github.com/google/uuid" ) // TaskInfo 上传任务的库级视图(与宿主的表结构解耦)。 type TaskInfo struct { Id int64 UploadId string FileName string FileSize int64 FileMd5 string MimeType string ChunkSize int64 TotalChunk int32 Status string // uploading / merging / done / error StoragePath string // 相对路径(如 cloud/xxx),由宿主的目录约定决定 Operator string TenantId int64 } // ChunkInfo 分片记录的库级视图。 type ChunkInfo struct { UploadId string ChunkIndex int32 ChunkSize int64 ChunkMd5 string } // Status 常量。 const ( StatusUploading = "uploading" StatusMerging = "merging" StatusDone = "done" StatusError = "error" ) // DefaultMaxChunkBytes 单分片硬上限(防滥用,与常见 multipart 内存缓冲同量级)。 const DefaultMaxChunkBytes int64 = 64 << 20 // Envelope 宿主响应封装:resp 非 nil 时写成功响应,err 非 nil 时写失败响应。 type Envelope func(c *gin.Context, resp any, err error) // Options 宿主注入配置。 type Options struct { // ChunkRoot 分片临时根目录;库在其下建 / 子目录。 // 建议置于宿主静态根之外,避免分片被静态服务暴露。 ChunkRoot string // CloudDir 合并产物目录(宿主的云盘/文件目录约定)。 CloudDir string // WebPathPrefix storagePath 的 Web 相对前缀(如 "cloud"),空则无前缀。 // 最终 storagePath = WebPathPrefix + "/" + 产物名(存库、回调、BS 拼接均用该值), // 需与 CloudDir 相对宿主静态根的子目录一致。 WebPathPrefix string // URLPrefix 拼接 fileUrl 的静态前缀,默认 "/uploads"; // 最终 fileUrl = URLPrefix + "/" + storagePath。 URLPrefix string // MaxTotalSizeMB 文件总大小上限(MB),0=不限制。 MaxTotalSizeMB int64 // MaxChunkBytes 单分片字节上限,0=DefaultMaxChunkBytes(64MB)。 MaxChunkBytes int64 // OnMerged 合并成功回调(宿主写业务表等)。返回 err 则 merge 失败、任务置 error。 OnMerged func(t TaskInfo) error // OnInstantHit 秒传命中回调(宿主确保业务侧可见行)。可空。 OnInstantHit func(t TaskInfo) error // Envelope 响应封装(必填)。 Envelope Envelope } func (o *Options) validate() error { if o.ChunkRoot == "" || o.CloudDir == "" { return errors.New("fileupload: Options.ChunkRoot / CloudDir 必填") } if o.Envelope == nil { return errors.New("fileupload: Options.Envelope 必填") } if o.MaxChunkBytes <= 0 { o.MaxChunkBytes = DefaultMaxChunkBytes } if o.URLPrefix == "" { o.URLPrefix = "/uploads" } return nil } func (o *Options) maxChunkBytes() int64 { if o.MaxChunkBytes > 0 { return o.MaxChunkBytes } return DefaultMaxChunkBytes } // expectChunks 期望分片数:ceil(fileSize/chunkSize),空文件视为 1 个空分片。 func expectChunks(fileSize, chunkSize int64) int { if chunkSize <= 0 { return 0 } n := int(math.Ceil(float64(fileSize) / float64(chunkSize))) if n == 0 { n = 1 } return n } // sanitizeFileName 取路径基名并剥离路径穿越/非法字符。 func sanitizeFileName(name string) string { name = path.Base(strings.TrimSpace(name)) name = strings.ReplaceAll(name, "\\", "") if name == "." || name == ".." || name == "/" || name == "" { return "" } return name } // newUploadID 生成上传任务 ID(uuid v4,URL 安全)。 func newUploadID() string { return uuid.NewString() }