非可查找流读取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
相关产品推荐
相关产品推荐

