跳转到主内容
趣航编程网 - 趣学编程,启航技术之路!

Go大文件断点续传流式秒传与Hash校验

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 解析中间态的内容——任何预处理都会导致客户端与服务端哈希不一致。

相关文章