c.FormFile不能用于分块上传,因其强制解析整个multipart请求体并载入内存,易触发OOM;须禁用默认解析,改用c.Request.MultipartReader()流式处理或直接读取raw body,配合元数据手动存取分片。

为什么不能用 c.FormFile 做分块上传
因为 c.FormFile 会强制解析整个 multipart 请求体,把所有分片(哪怕只传一个)连同 boundary 一起读进内存,再返回 *multipart.FileHeader。一旦客户端发错、重试或并发上传多个分片,服务端内存会线性暴涨——不是慢,是直接触发 runtime: out of memory。
真正分块上传必须绕过 Gin 默认的 multipart 解析逻辑,否则 c.Request.MultipartReader() 也拿不到干净流(已被提前消费)。
- 禁用默认解析:在初始化时设
r.MaxMultipartMemory = 0,或对分块上传路由单独关闭(通过自定义中间件清空c.Request.MultipartForm) - 手动读取原始 body:
io.Copy或分段io.ReadFull配合边界识别,但极易出错 - 更稳妥的做法:用
c.Request.MultipartReader()+ 自定义multipart.Reader消费逻辑,按 part 迭代,跳过非文件字段,只提取含filename=的 part
c.SaveUploadedFile 不能直接用于分片存储
它底层调用 file.Open() → io.Copy(),看似安全,但前提是 file 来自 c.FormFile。而分块上传里你根本没有 *multipart.FileHeader,只有原始字节流和分片元数据(如 chunkNumber、totalChunks、identifier)。
正确做法是:接收 raw body 后,用 os.Create 写入临时目录,文件名格式建议为 <identifier>_<chunknumber></chunknumber></identifier>,并确保写完后 sync.File.Sync() 落盘(防止断电丢片)。
- 不要用
c.SaveUploadedFile(file, dst)—— 它依赖 Gin 已解析的 header,此时file为空或 panic - 临时文件权限设为
0600,避免未合并前被恶意读取 - 写入前校验
chunkNumber是否越界(>totalChunks),防止客户端伪造
合并分片时 os.OpenFile 和 io.Copy 的坑
合并不是简单地按序 cat 所有分片文件,顺序错、缺片、重复写都会导致最终文件损坏。关键点在于打开目标文件时必须用 os.O_CREATE | os.O_WRONLY | os.O_APPEND,且每次 io.Copy 前不 Seek 到末尾——O_APPEND 保证原子追加。
如果用 os.O_TRUNC 或反复 os.Create,前面已写的分片会被清空;如果手动 Seek(0, io.SeekEnd),在某些 NFS 或容器文件系统上可能不准。
- 合并前先确认所有分片文件存在且大小匹配(比如每片应为 5MB,最后一片可小于)
- 用
filepath.Walk按chunkNumber数字排序,别依赖文件系统 ls 顺序 - 合并完成后立即
os.Remove所有临时分片,失败则记录日志并告警,避免磁盘占满
断点续传依赖的两个硬性前提
客户端能“续”上去,前提是服务端能准确回答“哪些片已收”。这要求两件事:一是文件唯一标识(如 MD5/SHA256)必须由客户端计算并随首片提交;二是服务端必须持久化记录每个 identifier 对应的已收 chunkNumber 集合,不能放内存里。
用 Redis 存最轻量:SMEMBERS upload:<identifier></identifier> 返回已传分片号;用本地 SQLite 也行,但要注意 WAL 模式防写阻塞。千万别用 map + sync.RWMutex —— 进程重启就丢了,用户重传全部分片。
- 首片请求必须带
identifier和totalChunks,服务端据此初始化记录 - 每个分片上传成功后,立刻
SADD upload:<identifier><chunknumber></chunknumber></identifier>,失败不重试就不管 - 合并成功后,
DEL upload:<identifier></identifier>清理状态,避免 key 泄漏


















