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

非可查找流读取ZIP:.NET 6下规避MemoryStream的技术问询

问题场景

我用C# + .NET 6,基于Docker化的S3存储(ninja)做测试。输入是IFormFile类型的ZIP文件,直接上传整个ZIP到存储没问题,但要解压内部文件分别上传时遇到了问题。

目前只能靠复制MemoryStream实现功能,但处理大文件时会有内存压力。不用MemoryStream的话,一直报“Input stream must be seekable”错误——普通Stream不可查找,只有MemoryStream可以。

两处用到MemoryStream的原因:

  • 上传ZIP后原流已经到末尾,没法再用来创建ZipArchive
  • 解压后的文件上传需要可查找的流

解决方案

解决第一个问题:避免复制整个ZIP到MemoryStream

方案1:调整顺序——先解压再上传原ZIP

把解压逻辑放在上传ZIP之前,此时原流还没被读取到末尾,能直接用来创建ZipArchive。注意给ZipArchive加leaveOpen: true参数,处理完解压后流不会被关闭,还能重置位置用来上传:

private async Task<FileResource> UploadZipFileAsync(Stream source, string containerName, string blobName, string inContentType = null, bool unpack = true)
{
    FileProperties properties;

    if (unpack)
    {
        // 先处理解压,source未被读取到末尾
        using(var zipArchive = new ZipArchive(source, ZipArchiveMode.Read, leaveOpen: true))
        {
            foreach(var zipEntry in zipArchive.Entries)
            {
                if (!zipEntry.FullName.EndsWith("/") && !string.IsNullOrEmpty(zipEntry.Name))
                {
                    var currentDirectoryName = zipEntry.FullName.Contains("/") 
                        ? zipEntry.FullName.TrimEnd(zipEntry.Name.ToCharArray()) 
                        : "";
                    new FileExtensionContentTypeProvider().TryGetContentType(zipEntry.Name, out var contentType);
                    var currentDirectory = $"{blobName}/{currentDirectoryName}";

                    await UploadZipEntryAsync(zipEntry, containerName, $"{currentDirectory}{zipEntry.Name}", contentType);
                }
            }
        }
        // 重置流位置,准备上传原ZIP
        source.Position = 0;
    }

    // 上传原ZIP文件
    await UploadFileAsync(source, containerName, $"{blobName}.zip", inContentType);
    properties = await storageService.GetFilePropertiesAsync(containerName, $"{blobName}.zip");

    var fileResource = new FileResource
    {
        BlobName = blobName,
        BlobContainerName = properties.ContainerName,
        Size = properties.Size,
        MD5 = properties.MD5,
        ContentType = "Folder",
        Uri = properties.Uri
    };

    return fileResource;
}

方案2:用临时文件代替MemoryStream(如果必须先传ZIP)

如果业务要求必须先上传ZIP再解压,就把原流写到临时文件,用临时文件的流做上传和解压,避免占用内存:

private async Task<FileResource> UploadZipFileAsync(Stream source, string containerName, string blobName, string inContentType = null, bool unpack = true)
{
    FileProperties properties;
    var tempFilePath = Path.GetTempFileName();

    try
    {
        using(var tempFileStream = new FileStream(tempFilePath, FileMode.Create, FileAccess.ReadWrite))
        {
            await source.CopyToAsync(tempFileStream);
            tempFileStream.Position = 0;
            // 上传ZIP
            await UploadFileAsync(tempFileStream, containerName, $"{blobName}.zip", inContentType);

            if (unpack)
            {
                tempFileStream.Position = 0;
                using(var zipArchive = new ZipArchive(tempFileStream))
                {
                    foreach(var zipEntry in zipArchive.Entries)
                    {
                        if (!zipEntry.FullName.EndsWith("/") && !string.IsNullOrEmpty(zipEntry.Name))
                        {
                            var currentDirectoryName = zipEntry.FullName.Contains("/") 
                                ? zipEntry.FullName.TrimEnd(zipEntry.Name.ToCharArray()) 
                                : "";
                            new FileExtensionContentTypeProvider().TryGetContentType(zipEntry.Name, out var contentType);
                            var currentDirectory = $"{blobName}/{currentDirectoryName}";

                            await UploadZipEntryAsync(zipEntry, containerName, $"{currentDirectory}{zipEntry.Name}", contentType);
                        }
                    }
                }
            }
        }

        properties = await storageService.GetFilePropertiesAsync(containerName, $"{blobName}.zip");
        var fileResource = new FileResource
        {
            BlobName = blobName,
            BlobContainerName = properties.ContainerName,
            Size = properties.Size,
            MD5 = properties.MD5,
            ContentType = "Folder",
            Uri = properties.Uri
        };

        return fileResource;
    }
    finally
    {
        // 清理临时文件
        if (File.Exists(tempFilePath))
        {
            File.Delete(tempFilePath);
        }
    }
}

解决第二个问题:解压后的文件上传避免MemoryStream

思路1:让存储服务支持不可查找流

检查你的S3兼容存储(ninja)是否支持分块上传或直接上传不可查找流。大部分S3客户端支持传入流和文件大小(用zipEntry.Length获取),可以给CreateAsync加个重载:

private async Task UploadZipEntryAsync(ZipArchiveEntry zipEntry, string containerName, string blobPath, string contentType)
{
    using(var entryStream = zipEntry.Open())
    {
        // 假设CreateAsync支持接受流、大小、内容类型参数
        await storageService.CreateAsync(containerName, blobPath, entryStream, contentType ?? "application/octet-stream", zipEntry.Length);
    }
}

思路2:用临时文件中转(必须要可查找流时)

如果存储服务必须要可查找流,就用临时文件存解压后的内容,避免占用内存:

private async Task UploadZipEntryAsync(ZipArchiveEntry zipEntry, string containerName, string blobPath, string contentType)
{
    var tempEntryPath = Path.GetTempFileName();
    try
    {
        using(var entryStream = zipEntry.Open())
        using(var tempFileStream = new FileStream(tempEntryPath, FileMode.Create, FileAccess.ReadWrite))
        {
            await entryStream.CopyToAsync(tempFileStream);
            tempFileStream.Position = 0;
            await storageService.CreateAsync(containerName, blobPath, tempFileStream, contentType ?? "application/octet-stream");
        }
    }
    finally
    {
        // 清理临时文件
        if (File.Exists(tempEntryPath))
        {
            File.Delete(tempEntryPath);
        }
    }
}

内容的提问来源于stack exchange,提问作者Raso

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.15 16:07:16