大文件分片上传后文件损坏问题排查求助
大文件分片上传合并后损坏的排查与修复
核心排查方向
- 分片偏移量校验:确认客户端传递的
Offset是否准确对应分片的起始位置。如果偏移量计算错误(比如重复或跳跃),会导致写入的字节重叠或缺失,直接损坏文件。可以在日志中记录每个分片的Offset和Data.Length,合并后验证总大小是否等于所有分片长度之和。 - MemoryStream线程安全问题:
ConcurrentDictionary仅保证字典本身的线程安全,但MemoryStream不是线程安全的。当多个分片请求同时操作同一个MemoryStream时,Seek和WriteAsync的并发执行会导致数据写入混乱。 - 资源泄漏与重复使用:当前代码在处理完最后一个分片后,仅
Dispose了MemoryStream但未从ConcurrentDictionary中移除。如果后续上传同名文件,会获取到已释放的MemoryStream,导致写入失败或数据损坏。 - 客户端数据完整性:检查
FileChunkDto.Data是否正确传递了分片的原始字节。如果客户端通过Base64编码传输,需确认解码过程无错误;如果是表单上传,需确保没有遗漏或篡改字节数据。
具体修复建议
1. 替换MemoryStream为临时文件存储(推荐)
大文件用MemoryStream会占用大量内存,且线程安全难以保证。改为每个分片写入临时文件,最后合并临时文件更稳定:
private readonly ConcurrentDictionary<string, string> chunkTempDirs = new ConcurrentDictionary<string, string>(); [HttpPost("UploadFileChunk")] public async Task<bool> UploadFileChunk([FromBody] FileChunkDto fileChunkDto) { try { string basePath = Path.Combine(Environment.CurrentDirectory, "StaticFiles"); string fileName = Path.Combine(basePath, fileChunkDto.FileName); string tempChunkDir = chunkTempDirs.GetOrAdd(fileName, _ => Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString())); // 处理第一个分片:清空临时目录 if (fileChunkDto.FirstChunk) { if (Directory.Exists(tempChunkDir)) Directory.Delete(tempChunkDir, true); Directory.CreateDirectory(tempChunkDir); } // 写入分片到临时文件,文件名用偏移量保证顺序 string chunkFilePath = Path.Combine(tempChunkDir, $"{fileChunkDto.Offset}.chunk"); await File.WriteAllBytesAsync(chunkFilePath, fileChunkDto.Data!); if (fileChunkDto.LastChunk) { // 合并所有分片 using var outputStream = new FileStream(fileName, FileMode.Create, FileAccess.Write); var chunkFiles = Directory.GetFiles(tempChunkDir) .OrderBy(f => long.Parse(Path.GetFileNameWithoutExtension(f))); foreach (var chunkFile in chunkFiles) { using var chunkStream = new FileStream(chunkFile, FileMode.Open, FileAccess.Read); await chunkStream.CopyToAsync(outputStream); File.Delete(chunkFile); } Directory.Delete(tempChunkDir); chunkTempDirs.TryRemove(fileName, out _); // 后续S3上传等逻辑... return true; } return false; } catch (Exception ex) { // 日志记录异常 return false; } }
2. 修复MemoryStream的线程安全与资源管理
如果坚持使用MemoryStream,需添加锁保证单个流的操作原子性,并正确清理资源:
private readonly ConcurrentDictionary<string, (MemoryStream Stream, object LockObj)> fileChunks = new ConcurrentDictionary<string, (MemoryStream, object)>(); [HttpPost("UploadFileChunk")] public async Task<bool> UploadFileChunk([FromBody] FileChunkDto fileChunkDto) { try { string filePath = Path.Combine(Environment.CurrentDirectory, "StaticFiles"); string fileName = Path.Combine(filePath, fileChunkDto.FileName); var entry = fileChunks.GetOrAdd(fileName, _ => (new MemoryStream(), new object())); lock (entry.LockObj) { entry.Stream.Seek(fileChunkDto.Offset, SeekOrigin.Begin); await entry.Stream.WriteAsync(fileChunkDto.Data!, 0, fileChunkDto.Data!.Length); } if (fileChunkDto.LastChunk) { lock (entry.LockObj) { using (var fileStream = new FileStream(fileName, FileMode.Create, FileAccess.Write)) { entry.Stream.Seek(0, SeekOrigin.Begin); await entry.Stream.CopyToAsync(fileStream); } entry.Stream.Dispose(); } fileChunks.TryRemove(fileName, out _); // 后续逻辑... return true; } return false; } catch (Exception ex) { // 日志记录 return false; } }
3. 添加完整性校验
- 客户端计算每个分片的MD5/SHA哈希,上传时一并传递;服务端接收后校验哈希,确保分片未损坏。
- 合并完成后,计算整个文件的哈希,与客户端传递的文件总哈希对比,确认合并后的文件完整。
4. 恢复FirstChunk的清理逻辑
取消注释FirstChunk时删除文件/清空流的代码,避免旧数据干扰新上传:
if (fileChunkDto.FirstChunk) { if (fileChunks.TryRemove(fileName, out var oldEntry)) { oldEntry.Stream.Dispose(); } // 如果是文件存储则删除旧文件 if (File.Exists(fileName)) File.Delete(fileName); }
内容的提问来源于stack exchange,提问作者Saqif Haque
相关产品推荐
相关产品推荐

