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