- 六接口路由:init / chunk / merge / check / task / abort - Repository 接口注入存储,宿主实现 12 方法即可接入 - OnMerged / OnInstantHit 回调衔接宿主业务(如写云端文件表) - Envelope 注入响应封装,与宿主 httpx 解耦 - 与 zogo v0.3.2 的 fileupload 子包同源,后续在此仓库独立演进
138 lines
4.4 KiB
Go
138 lines
4.4 KiB
Go
// 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() }
|