如何实时压缩并实时流式传输到Response.Output?

时间:2009-07-02 14:41:02

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

我正在尝试使用以下代码:我收到了损坏的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();
    }

6 个答案:

答案 0 :(得分:14)

男孩,这是很多代码!使用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。事实并非如此。当您调用 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上发帖。

答案 1 :(得分:2)

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

答案 2 :(得分:1)

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

然而,请注意,与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);
    }
}

答案 3 :(得分:0)

尝试添加以下标题。

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

我知道这之前给我带来了问题。

修改

这些其他2也可能有所帮助:

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

答案 4 :(得分:0)

您是否尝试在刷新响应之前刷新ZipOutputStream? 你可以在客户端保存zip并在zip工具中测试它吗?

答案 5 :(得分:0)

Necromancing。
根据Cheeso在评论中推荐的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