Как я могу закрепить на лету и потоком в Response.Output в режиме реального времени?

Я пытаюсь использовать следующий код: я получаю поврежденный 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();
    }

Ответ 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().


Добавление; некоторые люди прокомментировали мне, что DotNetZip "не годится", потому что он создает весь ZIP в памяти перед потоковой передачей. Это не так. Когда вы вызываете AddFiles, библиотека создает список записей - объектов, которые представляют состояние вещей, подлежащих зашивке. Сжатие или шифрование не выполняются до вызова Save. Если вы укажете поток для вызова Save(), все сжатые байты передаются непосредственно клиенту.

В модели SharpZipLib можно создать запись, затем передать ее, затем создать другую запись и передать ее и т.д. С помощью DotNetZip ваше приложение сначала создает полный список записей, а затем выводит их все. Ни один из подходов не обязательно "быстрее", чем другой, хотя для длинных списков файлов, скажем, 30 000, время от времени до первого байта будет быстрее с SharpZipLib. С другой стороны, я бы не рекомендовал динамически создавать zip файлы с 30 000 записей.


EDIT

Как и в DotNetZip v1.9, DotNetZip также поддерживает ZipOutputStream. Я все еще считаю, что проще сделать то, что я показал здесь.


Некоторые люди имеют случай, когда у них есть zip-контент, который "в основном тот же" для всех пользователей, но есть несколько файлов, которые отличаются друг от друга. DotNetZip хорош в этом тоже. Вы можете читать zip-архив из файла файловой системы, обновлять несколько записей (добавлять несколько, удалять несколько и т.д.), А затем сохранять в Response.OutputStream. В этом случае DotNetZip не будет повторно сжимать или повторно шифровать любую из записей, которые вы не изменили. Намного быстрее.

Конечно, DotNetZip предназначен для любого приложения .NET, а не только для ASP.NET. Таким образом, вы можете сохранить любой поток.

Если вам нужна дополнительная информация, просмотрите сайт или опубликуйте dotnetzip.

Ответ 2

Не совсем уверен, как это сделать в ASP.NET(ранее не пробовал), но в целом, если HTTP-клиент поддерживает HTTP v1.1 (как указано в версии его запроса), сервер может отправить заголовок ответа "Передача-кодирование", который указывает "chunked", а затем отправляет данные ответа с использованием нескольких блоков данных по мере их появления. Это позволяет передавать потоки данных в реальном времени, когда вы заранее не знаете окончательный размер данных (и, следовательно, не можете установить заголовок ответа Content-Length). Посмотрите RFC 2616 Раздел 3.6 для более подробной информации.

Ответ 3

Для тех, кто пропустит ZipOutputStream от SharpZipLib, вот простой код, который позволяет использовать DotNetZip "обычный путь потока .NET".

Однако учтите, что он неэффективен по сравнению с реальным потоковым потоковым решением, подобным SharpZipLib, поскольку он использует внутренний MemoryStream, прежде чем на самом деле вызывает функцию DotNetZip.Save(). Но, к сожалению, 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);
    }
}

Ответ 4

Попробуйте добавить следующий заголовок.

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

Я знаю, что это вызывало у меня проблемы раньше.

Edit:

Эти другие 2 также могут помочь:

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

Ответ 5

Вы пробовали промывать ZipOutputStream до того, как сбросили ответ? Вы можете сохранить zip на клиенте и протестировать его в zip-утилите?

Ответ 6

Некромантия.
Здесь, как это сделано правильно, используя закрытие, начиная с DotNetZip v1.9 +, как рекомендовано Cheeso в комментариях:

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