Files
fileupload/fileupload.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

138 lines
4.4 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 通用大文件分片上传(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 分片临时根目录;库在其下建 <uploadId>/ 子目录。
// 建议置于宿主静态根之外,避免分片被静态服务暴露。
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() }