You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

大文件分片上传后文件损坏问题排查求助

大文件分片上传合并后损坏的排查与修复

核心排查方向

  • 分片偏移量校验:确认客户端传递的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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.07 13:05:59