【问题标题】:How do I zip on the fly and stream to Response.Output in real time?如何即时压缩并实时流式传输到 Response.Output?
【发布时间】:2010-11-07 16:04:48
【问题描述】:

我正在尝试使用以下代码:我得到一个损坏的 zip 文件。为什么? 文件名似乎没问题。也许它们不是相对名称,这就是问题所在?

      private void trySharpZipLib(ArrayList filesToInclude)
    {
        // Response header
        Response.Clear();
        Response.ClearHeaders();
        Response.Cache.SetCacheability(HttpCacheability.NoCache);
        Response.StatusCode = 200; // http://community.icsharpcode.net/forums/p/6946/20138.aspx
        long zipSize = calculateZipSize(filesToInclude);
        string contentValue = 
            string.Format("attachment; filename=moshe.zip;"
                          ); // + " size={0}", zipSize);
        Response.ContentType = "application/octet-stream"; //"application/zip"; 
        Response.AddHeader("Content-Disposition", contentValue);
        Response.Flush();

        using (ZipOutputStream zipOutputStream = new ZipOutputStream(Response.OutputStream) ) 
        {
            zipOutputStream.SetLevel(0);

            foreach (string f in filesToInclude)
            {
                string filename = Path.Combine(Server.MapPath("."), f);
                using (FileStream fs = File.OpenRead(filename))
                {
                    ZipEntry entry =
                        new ZipEntry(ZipEntry.CleanName(filename))
                            {
                                DateTime = File.GetCreationTime(filename),
                                CompressionMethod = CompressionMethod.Stored,
                                Size = fs.Length
                            };
                    zipOutputStream.PutNextEntry(entry);

                    byte[] buffer = new byte[fs.Length];
                    // write to zipoutStream via buffer. 
                    // The zipoutStream is directly connected to Response.Output (in the constructor)
                    ICSharpCode.SharpZipLib.Core.StreamUtils.Copy(fs, zipOutputStream, buffer); 
                    Response.Flush(); // for immediate response to user
                } // .. using file stream
            }// .. each file
        }
        Response.Flush();
        Response.End();
    }

【问题讨论】:

    标签: asp.net zip real-time outputstream sharpziplib


    【解决方案1】:

    男孩,代码太多了!使用DotNetZip,您的工作会更简单。假设一个 HTTP 1.1 客户端,这是可行的:

    Response.Clear();
    Response.BufferOutput = false;
    string archiveName= String.Format("archive-{0}.zip", DateTime.Now.ToString("yyyy-MMM-dd-HHmmss"));
    Response.ContentType = "application/zip";
    // see http://support.microsoft.com/kb/260519
    Response.AddHeader("content-disposition", "attachment; filename=" + archiveName);  
    using (ZipFile zip = new ZipFile())
    {
        // filesToInclude is a IEnumerable<String> (String[] or List<String> etc)
        zip.AddFiles(filesToInclude, "files");
        zip.Save(Response.OutputStream);
    }
    // Response.End(); // will throw an exception internally.
    // Response.Close(); // Results in 'Failed - Network error' in Chrome.
    Response.Flush(); // See https://stackoverflow.com/a/736462/481207
    // ...more code here...
    

    如果你想对 zip 进行密码加密,那么在 AddFiles() 之前,插入这些行:

        zip.Password = tbPassword.Text; // optional
        zip.Encryption = EncryptionAlgorithm.WinZipAes256; // optional
    

    如果您想要自解压存档,请将 zip.Save() 替换为 zip.SaveSelfExtractor()。


    附录; some people have commented to me DotNetZip “不好”,因为它会在将整个 ZIP 流式传输出来之前在内存中创建整个 ZIP。事实并非如此。当您调用 AddFiles 时,该库会创建一个条目列表 - 表示要压缩的事物状态的对象。在调用 Save 之前不会进行压缩或加密。如果您为 Save() 调用指定流,则所有压缩字节都会直接流式传输到客户端。

    在 SharpZipLib 模型中,可以创建一个条目,然后将其流式传输,然后创建另一个条目,然后将其流式传输,等等。使用 DotNetZip,您的应用首先会创建完整的条目列表,然后将它们全部输出。两种方法都不一定比另一种“更快”,尽管对于长文件列表,比如 30,000 个,使用 SharpZipLib 的时间到第一个字节会更快。另一方面,我不建议动态创建包含 30,000 个条目的 zip 文件。


    编辑

    从 DotNetZip v1.9 开始,DotNetZip 也支持 ZipOutputStream。不过,我仍然认为按照我在这里展示的方式做事更简单。


    有些人的情况是,他们的 zip 内容对所有用户来说“基本相同”,但每个用户都有几个不同的文件。 DotNetZip 在这方面也很擅长。您可以从文件系统文件中读取 zip 存档,更新一些条目(添加一些,删除一些等),然后保存到 Response.OutputStream。在这种情况下,DotNetZip 不会重新压缩或重新加密您未更改的任何条目。快多了。

    当然,DotNetZip 适用于任何 .NET 应用程序,而不仅仅是 ASP.NET。因此,您可以保存到任何流。

    如果您想了解更多信息,请查看site 或在dotnetzip forums 上发帖。

    【讨论】:

    • 啊,插入你自己的库。 :) 诚然,它看起来确实稍微简单一些。
    • 我们使用的是 ICSharpCode SharpZipLib,刚刚将其替换为 DotNetZip。 DotNetZip 界面比 SharpZipLib 简单得多。我们最终得到的代码量不到一半。我现在是一个超级粉丝。如果您需要 Zip 库,请不要使用 SharpZip,请使用 DotNetZip。
    • DotNetZip 解决了我的问题。它取代了 ICSharpCode.. 我的赢家是 zip.Save(Response.OutputStream);我为其他 API 的使用而苦苦挣扎。
    【解决方案2】:

    不太清楚如何在 ASP.NET 中执行此操作(之前没有尝试过),但通常如果 HTTP 客户端支持 HTTP v1.1(由其请求的版本指示),服务器可以发送指定“分块”的“Transfer-Encoding”响应标头,然后在可用时使用多个数据块发送响应数据。这允许在您提前不知道最终数据大小的情况下实时传输数据(因此无法设置“Content-Length”响应标头)。请查看RFC 2616 第 3.6 节了解更多详情。

    【讨论】:

    • 在 ASP.NET 中,您可以通过设置 Response.BufferOutput = false 来做到这一点。
    【解决方案3】:

    对于那些会错过 SharpZipLib 的 ZipOutputStream 的人,这里有一个简单的代码,可以使用“常规 .NET 流方式”来使用 DotNetZip。

    但是,请注意,与 SharpZipLib 等真正的即时流媒体解决方案相比,它效率低下,因为它在实际调用 DotNetZip.Save() 函数之前使用内部 MemoryStream。但不幸的是,SharpZibLib 还没有运动 EAS 加密(当然从来没有)。让我们希望 Cheeso 很快就会在 dotNetZip 中添加这个功能吗? ;-)

    /// <summary>
    /// DotNetZip does not support streaming out-of-the-box up to version v1.8.
    /// This wrapper class helps doing so but unfortunately it has to use
    /// a temporary memory buffer internally which is quite inefficient
    /// (for instance, compared with the ZipOutputStream of SharpZibLib which has other drawbacks besides).
    /// </summary>
    public class DotNetZipOutputStream : Stream
    {
        public ZipFile ZipFile { get; private set; }
    
        private MemoryStream memStream = new MemoryStream();
        private String nextEntry = null;
        private Stream outputStream = null;
        private bool closed = false;
    
        public DotNetZipOutputStream(Stream baseOutputStream)
        {
            ZipFile = new ZipFile();
            outputStream = baseOutputStream;
        }
    
        public void PutNextEntry(String fileName)
        {
            memStream = new MemoryStream();
            nextEntry = fileName;
        }
    
        public override bool CanRead { get { return false; } }
        public override bool CanSeek { get { return false; } }
        public override bool CanWrite { get { return true; } }
        public override long Length { get { return memStream.Length; } }
        public override long Position
        {
            get { return memStream.Position; }
            set { memStream.Position = value; }
        }
    
        public override void Close()
        {
            if (closed) return;
    
            memStream.Position = 0;
            ZipFile.AddEntry(nextEntry, Path.GetDirectoryName(nextEntry), memStream);
            ZipFile.Save(outputStream);
            memStream.Close();
            closed = true;
        }
    
        public override void Flush()
        {
            memStream.Flush();
        }
    
        public override int Read(byte[] buffer, int offset, int count)
        {
            throw new NotSupportedException("Read");
        }
    
        public override long Seek(long offset, SeekOrigin origin)
        {
            throw new NotSupportedException("Seek");
        }
    
        public override void SetLength(long value)
        {
            memStream.SetLength(value);
        }
    
        public override void Write(byte[] buffer, int offset, int count)
        {
            memStream.Write(buffer, offset, count);
        }
    }
    

    【讨论】:

    • DotNetZip v1.9 允许您直接写入输出流。使用 AddEntry(string, WriteDelegate)。使用匿名方法将 XML 从 DataSet 嵌入到 zip 文件的示例: zip.AddEntry(zipEntryName, (name,stream) => dataset1.WriteXml(stream) );
    【解决方案4】:

    尝试添加以下标题。

    Response.AddHeader("Content-Length", zipSize);
    

    我知道这曾经给我带来了问题。

    编辑:

    其他 2 个也可能有帮助:

    Response.AddHeader("Content-Description", "File Transfer");
    Response.AddHeader("Content-Transfer-Encoding", "binary");
    

    【讨论】:

      【解决方案5】:

      您是否尝试在刷新响应之前刷新 ZipOutputStream? 您可以将 zip 保存在客户端并在 zip 实用程序中进行测试吗?

      【讨论】:

        【解决方案6】:

        死灵术。
        以下是 Cheeso 在 cmets 中推荐的使用闭包的正确方法,从 DotNetZip v1.9+ 开始:

        public static void Run()
        {
            using (Ionic.Zip.ZipFile zip = new Ionic.Zip.ZipFile())
            {
        
                for (int i = 1; i < 11; ++i)
                {
                    zip.AddEntry("LeaseContractForm_" + i.ToString() + ".xlsx", delegate(string filename, System.IO.Stream output)
                    {
                        // ByteArray from ExecuteReport - only ONE ByteArray at a time, because i might be > 100, and ba.size might be > 20 MB
                        byte[] ba = Portal_Reports.LeaseContractFormPostProcessing.ProcessWorkbook();
                        output.Write(ba, 0, ba.Length);
                    });
                } // Next i 
        
                using (System.IO.Stream someStream = new System.IO.FileStream(@"D:\test.zip", System.IO.FileMode.Create, System.IO.FileAccess.Write, System.IO.FileShare.None))
                {
                    zip.Save(someStream);
                }
            } // End Using zip 
        
        } // End Sub Run 
        

        还有 VB.NET 变体,以防万一有人需要它(请注意,这只是一个测试;实际上,循环中的每个步骤都会使用不同的 in_contract_uid 和 in_premise_uid 调用它):

        Imports System.Web
        Imports System.Web.Services
        
        
        Public Class test
            Implements System.Web.IHttpHandler
        
        
            Sub ProcessRequest(ByVal context As HttpContext) Implements IHttpHandler.ProcessRequest
                Dim in_contract_uid As String = context.Request.Params("in_contract_uid")
                Dim in_premise_uid As String = context.Request.Params("in_premise_uid")
        
                If String.IsNullOrWhiteSpace(in_contract_uid) Then
                    in_contract_uid = "D57A62D7-0FEB-4FAF-BB09-84106E3E15E9"
                End If
        
                If String.IsNullOrWhiteSpace(in_premise_uid) Then
                    in_premise_uid = "165ECACA-04E6-4DF4-B7A9-5906F16653E0"
                End If
        
                Dim in_multiple As String = context.Request.Params("in_multiple")
                Dim bMultiple As Boolean = False
        
                Boolean.TryParse(in_multiple, bMultiple)
        
        
                If bMultiple Then
                    Using zipFile As New Ionic.Zip.ZipFile
        
                        For i As Integer = 1 To 10 Step 1
                            ' Dim ba As Byte() = Portal_Reports.LeaseContractFormReport.GetLeaseContract(in_contract_uid, in_premise_uid) '
                            ' zipFile.AddEntry("LeaseContractForm_" + i.ToString() + ".xlsx", ba) '
        
                            zipFile.AddEntry("LeaseContractForm_" + i.ToString() + ".xlsx", Sub(filename As String, output As System.IO.Stream)
                                                                                                Dim ba As Byte() = Portal_Reports.LeaseContractFormReport _
                                                                                                .GetLeaseContract(in_contract_uid, in_premise_uid)
                                                                                                output.Write(ba, 0, ba.Length)
                                                                                            End Sub)
                        Next i
        
                        context.Response.ClearContent()
                        context.Response.ClearHeaders()
                        context.Response.ContentType = "application/zip"
                        context.Response.AppendHeader("content-disposition", "attachment; filename=LeaseContractForm.zip")
                        zipFile.Save(context.Response.OutputStream)
                        context.Response.Flush()
                    End Using ' zipFile '
                Else
                    Dim ba As Byte() = Portal_Reports.LeaseContractFormReport.GetLeaseContract(in_contract_uid, in_premise_uid)
                    Portal.ASP.NET.DownloadFile("LeaseContractForm.xlsx", "attachment", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", ba)
                End If
        
            End Sub
        
        
            ReadOnly Property IsReusable() As Boolean Implements IHttpHandler.IsReusable
                Get
                    Return False
                End Get
            End Property
        
        
        End Class
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2013-11-11
          • 2022-11-10
          • 2019-08-16
          相关资源
          最近更新 更多