os.OpenFile不能用os.O_APPEND断点续传,因其强制Write跳转至文件末尾,违背从指定offset写入需求;正确方式是用os.O_CREATE|os.O_WRONLY打开,配合Seek校验与Truncate防稀疏。

os.OpenFile 为什么不能用 os.O_APPEND
断点续传不是追加,是“从指定 offset 写入”,而 os.O_APPEND 会强制把每次 Write() 跳转到文件末尾——哪怕你刚调过 file.Seek(1024, io.SeekStart),它也无效。这是 POSIX 行为 + Go 运行时共同决定的,不是 bug,是设计如此。
正确打开方式必须是:os.OpenFile(filename, os.O_CREATE|os.O_WRONLY, 0644),不加 os.O_TRUNC(否则清空已有内容),也不加 os.O_APPEND。
- 先
os.Stat()拿当前长度作为起始offset - 立即
file.Seek(offset, io.SeekStart),并严格比对返回值是否等于offset;不等说明文件被篡改或 NFS 不支持随机写,应中止 - 写入前可选
file.Truncate(expectedSize)防稀疏文件,尤其在合并阶段
HTTP Range 请求与 206 响应校验
客户端不能只看响应头有 Accept-Ranges: bytes 就认为服务端支持断点;有些 CDN 或 Nginx 会伪造该 header,实际对 Range 请求返回 200 或 500。
必须发试探请求验证:
- 构造
HEAD或小范围GET:Range: bytes=0-1023 - 检查状态码:只接受
206 Partial Content - 检查响应头
Content-Range是否匹配,例如bytes 0-1023/123456789 - 若返回
200,说明不支持,后续请求应弃用Range头并重置本地文件
上传侧同理:客户端需带 Content-Range: bytes 1024-2047/5000,服务端据此定位、校验、写入。
流式 SHA256 校验不能等下载完再算
大文件(如 2GB 视频)若等全部写入磁盘后再读一遍算 sha256.Sum256(),既 OOM 又延迟高,完全违背“秒传”和“流式”本意。
hash.Hash 实现了 io.Writer,可直接作为 io.Copy 的目标,实现零拷贝同步哈希:
h := sha256.New()
f, _ := os.OpenFile("part.tmp", os.O_CREATE|os.O_WRONLY, 0644)
defer f.Close()
io.Copy(io.MultiWriter(f, h), resp.Body) // 一次完成写入 + 更新哈希
fileHash := hex.EncodeToString(h.Sum(nil))
- 别手动
buf := make([]byte, 32*1024); n, _ := r.Read(buf); h.Write(buf[:n])—— 多余且易错 - 分片场景下,每个 chunk 必须携带独立
X-Chunk-Hash,服务端收到后立即用io.TeeReader(chunkBody, h)边读边校验 -
h.Sum(nil)返回的是追加后的切片,转字符串必须用hex.EncodeToString(),不能string(h.Sum(nil))
秒传依赖内容哈希,不是文件名或大小
仅比对 os.Stat().Size 或文件路径,碰撞率极高。两个不同视频文件完全可能大小相同;用户改个后缀就失效。秒传成立的前提,是服务端能用 O(1) 时间判断“这个内容我存过了”。
推荐方案:
- 客户端上传前流式计算整个文件的
sha256(同样用io.Copy(h, f)) - 服务端用 Redis SET 存所有已存在哈希:
SET file_hash:abc123... ONNX true EX 3600 - 查重走
EXISTS file_hash:abc123...,毫秒级响应,多实例共享,不压 DB - 不要用 MySQL 或本地 map 做查重,高并发下立刻成为瓶颈
真正容易被忽略的点是:哈希必须基于原始字节流,而非解压后、转码后、或经 multipart 解析中间态的内容——任何预处理都会导致客户端与服务端哈希不一致。


















