c.FormFile不能用于断点续传,因其会一次性将整个multipart body读入内存导致OOM,且依赖Gin默认解析、无法按需读取分片;必须禁用自动解析,改用ctx.Request.MultipartReader()流式处理,并校验fileMd5、chunkIndex等关键参数。

为什么不能只靠 c.FormFile 做断点续传
它会把整个 multipart body 一次性读进内存,哪怕你只想要一个分片 —— 50MB 文件 + 10 并发 = 直接 OOM。更糟的是,c.FormFile 依赖 Gin 默认的 multipart 解析,一旦触发就无法中途中断或跳过已传部分。
断点续传的核心是「按需读取、不缓存、可寻址」,而 c.FormFile 完全违背这点。真实场景中,你得绕过它,直接操作 ctx.Request.MultipartReader() 或更底层的 ctx.Request.Body。
- 必须禁用 Gin 的自动 multipart 解析:在初始化时加
gin.SetMode(gin.ReleaseMode)并手动调用router.NoRoute(...)避免默认行为干扰 - 不要试图从
c.Request.FormValue里取分片序号 —— 这些字段可能被提前 consume,导致后续读 body 失败 - 若前端用
FormData.append("chunk", blob, "name"),服务端必须用multipart.Reader按 part 解析,不能假设单个请求只含一个文件字段
upload/chunk 接口必须校验哪些参数
断点续传不是“上传一部分就行”,而是要确保同一文件的每个分片都能被唯一识别、顺序还原。光靠 chunkIndex 不够,漏掉任意一项都会导致合并失败或覆盖错位。
-
fileMd5(必传):用于生成临时目录名,避免不同用户上传同名文件时冲突;不能仅用文件名做 key -
chunkIndex(必传):从 0 开始还是 1 开始要前后端统一,建议从 0 起始,便于数组索引映射 -
totalChunks(建议传):用于预创建存储空间、校验完整性;若不传,合并时需扫描目录再排序,IO 开销大 -
chunkSize(可选但推荐):服务端可据此做大小一致性校验,防止客户端伪造小分片绕过限速
注意:fileMd5 必须由前端计算并透传,服务端不做二次哈希 —— 否则断点续传时无法复用已有分片。
分片存储路径和命名怎么设计才安全可靠
路径设计直接影响并发安全、清理成本和磁盘碎片。常见错误是直接拼接 ./uploads/ + fileName,这会导致路径遍历、同名覆盖、中文乱码三重风险。
- 根目录必须是绝对路径且不可写入用户输入,例如
/var/tmp/upload-chunks,而非./uploads - 子目录用
fileMd5(非原始文件名)作为唯一标识,如/var/tmp/upload-chunks/ab12cd34... - 分片文件名固定为
chunk_<code>chunkIndex.bin,不带原始扩展名 —— 扩展名在合并后由主文件元数据决定 - 写入前必须用
os.OpenFile(..., os.O_CREATE|os.O_WRONLY|os.O_EXCL),避免并发写入覆盖
如果用 NFS 或分布式存储,os.O_EXCL 可能失效,此时需引入 Redis 锁或基于 etcd 的分布式文件锁。
合并分片时最容易忽略的三个细节
合并看着简单,但生产环境出问题基本都卡在这几步:顺序错、权限丢、残留多。
- 分片必须按
chunkIndex数值升序读取,不能依赖filepath.Walk的系统返回顺序 —— ext4 和 XFS 排序规则不同 - 合并后文件的
os.FileMode要显式设为0644,否则默认是0600,前端下载时可能因权限拒绝读取 - 合并成功后必须原子性地清理临时目录,推荐先
os.Rename到目标位置,再os.RemoveAll原目录;若 rename 失败,保留分片供重试,别删
特别提醒:iOS Safari 对 Content-Length 异常敏感,合并后的文件若没调用 os.Stat().Size() 精确设置 Content-Length,下载会卡在 99% 或直接失败。











