Вопрос по file, asp.net-mvc-3, c# – MVC3 возвращает несколько PDF-файлов в виде ZIP-файла

3

У меня есть представление, которое возвращает pdf (используя iTextSharp) с несколькими страницами, но теперь мне нужно изменить его, чтобы каждая страница представляла собой отдельный pdf (с собственным уникальным заголовком) и возвращала zip-файл.

Мой оригинальный код выглядит так:

public FileStreamResult DownloadPDF()
{
    MemoryStream workStream = new MemoryStream();
    Document document = new Document();
    PdfWriter.GetInstance(document, workStream).CloseStream = false;
    document.Open();

    // Populate pdf items

    document.Close();

    byte[] byteInfo = workStream.ToArray();
    workStream.Write(byteInfo, 0, byteInfo.Length);
    workStream.Position = 0;

    FileStreamResult fileResult = new FileStreamResult(workStream, "application/pdf");
    fileResult.FileDownloadName = "fileName";

    return fileResult;
}

Сжать файл с помощью gzip выглядит довольно просто, но я не знаю, как сжать несколько файлов и вернуть их как один zip-файл. Или я должен использовать что-то, кроме gzip, например dotnetzip или sharpzip?

Заранее спасибо!

Ваш Ответ

4   ответа
11

С другой стороны, у меня есть некоторые комментарии по поводу использования вами библиотеки DoTNetZip.

Во-первых, твой код ошибочен. В этой секции:

byte[] byteInfo = workStream.ToArray();                        

zip.Save(workStream);                        

workStream.Write(byteInfo, 0, byteInfo.Length);                        
workStream.Position = 0;                        

... вы читаете workStream в массив. Но на тот момент вы ничего не записали в workStream, поэтому массив пустой, нулевой длины. Затем вы сохраняете почтовый индекс в рабочем потоке. Затем вы записываете массив (нулевой длины) в тот же рабочий поток. Это НЕТ-ОП. Наконец вы сбросили позицию.

Вы можете заменить все это:

zip.Save(workStream);                        
workStream.Position = 0;                        

Это не проблема с DotNetZip как таковая, это просто неправильное понимание с вашей стороны относительно работы потоков.

ОК, затем, вы без необходимости выделяете временные буферы (потоки памяти). Представьте MemoryStream просто как массив байтов с оберткой Stream для поддержки Write (), Read (), Seek () и т. Д. По сути, ваш код записывает данные во этот временный буфер, а затем говорит DotNetZip считать данные из временного буфера в его собственный буфер для сжатия. Вам не нужен этот промежуточный буфер. Это работает так, как вы это сделали, но может быть более эффективным.

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

В этом делегате писателя ваш код записывается непосредственно в поток DotNetZip - поток, который передается в делегат DotNetZip. Нет промежуточного буфера. Приятно за эффективность.

Помните о правилах закрытия. Если вы вызываете этот делегат писателя в цикле for, у вас должен быть способ получить & quot; bla & quot; соответствующий zipentry внутри делегата. Делегат не исполняется доzip.Save() называется! Таким образом, вы не можете полагаться на значение «bla». из цикла.

public FileStreamResult DownloadPDF() 
{ 
    MemoryStream workStream = new MemoryStream(); 
    using(var zip = new ZipFile()) 
    {
        foreach(Bla bla in Blas) 
        { 
            zip.AddEntry(bla.filename + ".pdf", (name,stream) => {
                    var thisBla = GetBlaFromName(name);
                    Document document = new Document(); 
                    PdfWriter.GetInstance(document, stream).CloseStream = false; 

                    document.Open(); 

                    // write PDF Content for thisBla into stream/PdfWriter 

                    document.Close(); 
                });
        } 

        zip.Save(workStream); 
    }
    workStream.Position = 0; 

    FileStreamResult fileResult = new FileStreamResult(workStream, System.Net.Mime.MediaTypeNames.Application.Zip); 
    fileResult.FileDownloadName = "MultiplePDFs.zip"; 

    return fileResult; 
}

Наконец, мне не особенно нравится твое созданиеFileStreamResult изMemoryStream, Проблема в том, что весь ваш zip-файл хранится в памяти, что может быть очень сложно для использования памяти. Если ваши zip-файлы большие, ваш код сохранит все содержимое в памяти.

Я недостаточно знаю модель MVC3, чтобы понять, есть ли в ней что-то, что помогает в этом. Если нет, вы можетеиспользовать анонимный канал, чтобы инвертировать направление потокови устранить необходимость хранить все сжатые данные в памяти.

Вот что я имею в виду: созданиеFileStreamResult требует, чтобы вы предоставили читаемый поток. Если вы используете MemoryStream, чтобы сделать его читаемым, вам нужно сначала записать в него, а затем вернуться к позиции 0, прежде чем передать его вFileStreamResult конструктор. Это означает, что в определенный момент все содержимое этого zip-файла должно храниться в памяти непрерывно.

Предположим, вы можете предоставить читаемый потокFileStreamResult конструктор, который позволит читателю читать именно в тот момент, когда вы написали ему. Это то, что делает поток анонимного канала. Это позволяет вашему коду использовать поток с возможностью записи, в то время как код MVC получает поток с возможностью чтения.

Вот как это будет выглядеть в коде.

static Stream GetPipedStream(Action<Stream> writeAction) 
{ 
    AnonymousPipeServerStream pipeServer = new AnonymousPipeServerStream(); 
    ThreadPool.QueueUserWorkItem(s => 
    { 
        using (pipeServer) 
        { 
            writeAction(pipeServer); 
            pipeServer.WaitForPipeDrain(); 
        } 
    }); 
    return new AnonymousPipeClientStream(pipeServer.GetClientHandleAsString()); 
} 


public FileStreamResult DownloadPDF() 
{
    var readable = 
        GetPipedStream(output => { 

            using(var zip = new ZipFile()) 
            {
                foreach(Bla bla in Blas) 
                { 
                    zip.AddEntry(bla.filename + ".pdf", (name,stream) => {
                        var thisBla = GetBlaFromName(name);
                        Document document = new Document(); 
                        PdfWriter.GetInstance(document, stream).CloseStream = false; 

                        document.Open(); 

                        // write PDF Content for thisBla to PdfWriter

                        document.Close(); 
                    });
                } 

                zip.Save(output); 
            }
        }); 

    var fileResult = new FileStreamResult(readable, System.Net.Mime.MediaTypeNames.Application.Zip); 
    fileResult.FileDownloadName = "MultiplePDFs.zip"; 

    return fileResult; 
}

Я не пробовал это, но это должно работать. Это имеет преимущество перед тем, что вы написали, более эффективное использование памяти. Недостатком является то, что это немного сложнее, используя именованные каналы и несколько анонимных функций.

Это имеет смысл, только если содержимое почтового индекса находится в диапазоне & gt; 1 МБ. Если ваши почтовые индексы меньше этого, то вы можете просто сделать это первым способом, который я показал выше.

Addendum

Why can you not rely on the value of bla within the anonymous method?

Есть два ключевых момента. Во-первых, цикл foreach определяет переменная с именемbla, который принимает другое значение, каждый раз через петлю. Кажется очевидным, но это стоит заявить в явном виде.

Во-вторых, анонимный метод передается какargument в ZipFile.AddEntry() метод, и он не будет запущен во время цикл foreach запускается На самом деле анонимный метод вызывается неоднократно, один раз для каждой добавленной записи, во время ZipFile.Save(), Если вы ссылаетесь наbla в анонимном метод, он получаетthe last value назначен наbla, потому что это значениеbla держит в то времяZipFile.Save() пробеги.

Это отложенное выполнение, которое вызывает сложность.

То, что вы хотите, это каждая отдельная ценностьbla из цикла foreach быть доступный в то время, когда анонимная функция вызывается - позже, вне цикла foreach. Вы может сделать это с помощью служебного метода (GetBlaForName()), как я показал выше. Вы можете также сделайте это с дополнительным закрытием, например так:

Action<String,Stream> GetEntryWriter(Bla bla)
{
   return new Action<String,Stream>((name,stream) => {
     Document document = new Document();  
     PdfWriter.GetInstance(document, stream).CloseStream = false;  

     document.Open();  

     // write PDF Content for bla to PdfWriter 

     document.Close();  
  };
}

foreach(var bla in Blas)
{
  zip.AddEntry(bla.filename + ".pdf", GetEntryWriter(bla));
}

GetEntryWriter возвращает метод - фактически Action, который является просто типизированным методом. Каждый раз в цикле создается новый экземпляр этого действия, и он ссылается на другое значение для bla. Это действие не называется до времениZipFile.Save().

+1 - Приятно видеть такое подробное объяснение от одного из разработчиков DotNetZip. Потрясающая библиотека. :)
+1 Спасибо, вы дали отличную разбивку и эффективный код! Не могли бы вы рассказать подробнее о том, почему я не могу полагаться на значение слова "bla"? из цикла. Garrett Fogerlie
да, я положил объяснение в конце ответа выше. Если вы хотите узнать больше об этом, вы должны прочитать о закрытиях.stackoverflow.com/a/428624/48082
Благодаря тонну! Я очень ценю подробный уровень вашего ответа! Garrett Fogerlie
Нет проблем, рад помочь. удачи.
2

Turnkey сказал - SharpZipLib довольно хорош с несколькими файлами и потоком памяти. Просто создайте файлы, которые нужно сжать и добавить в архив. Вот пример:

        // Save it to memory
        MemoryStream ms = new MemoryStream();
        ZipOutputStream zipStream = new ZipOutputStream(ms);

        // USE THIS TO CHECK ZIP :)
        //FileStream fileOut = File.OpenWrite(@"c:\\test1.zip");
        //ZipOutputStream zipStream = new ZipOutputStream(fileOut);

        zipStream.SetLevel(0);

        // Loop your pages (files)
        foreach(string filename in files)
        {
            // Create and name entry in archive
            FileInfo fi = new FileInfo(filename);
            ZipEntry zipEntry = new ZipEntry(fi.Name);
            zipStream.PutNextEntry(zipEntry);

            // Put entry to archive (from file or DB)
            ReadFileToZip(zipStream, filename);

            zipStream.CloseEntry();

        }

        // Copy from memory to file or to send output to browser, as you did
        zipStream.Close();

Я не знаю, как вы получаете информацию для архивирования, поэтому я предполагаю, что файл в порядке :)

    /// <summary>
    /// Reads file and puts it to ZIP stream
    /// </summary>
    private void ReadFileToZip(ZipOutputStream zipStream, ,string filename)
    {
        // Simple file reading :)
        using(FileStream fs = File.OpenRead(filename))
        {
            StreamUtils.Copy(fs, zipStream, new byte[4096]);
        }
    }
3

DotNetZip вместо SharpZipLib, потому что решение проще. Вот то, что я в итоге сделал, все работает отлично, однако, если у кого-то есть какие-либо советы / изменения, я буду рад здесь их изложить.

public FileStreamResult DownloadPDF()
{
    MemoryStream workStream = new MemoryStream();
    ZipFile zip = new ZipFile();

    foreach(Bla bla in Blas)
    {
        MemoryStream pdfStream = new MemoryStream();
        Document document = new Document();
        PdfWriter.GetInstance(document, pdfStream).CloseStream = false;

        document.Open();

        // PDF Content

        document.Close();
        byte[] pdfByteInfo = pdfStream.ToArray();
        zip.AddEntry(bla.filename + ".pdf", pdfByteInfo);
        pdfStream.Close();
    }

    zip.Save(workStream);
    workStream.Position = 0;

    FileStreamResult fileResult = new FileStreamResult(workStream, System.Net.Mime.MediaTypeNames.Application.Zip);
    fileResult.FileDownloadName = "MultiplePDFs.zip";

    return fileResult;
}
мои комментарии были слишком большими для комментария, поэтому я поместил их в ответ.stackoverflow.com/a/10891136/48082
Бла и Блас только что восполнили этот пост. Это содержание, которое вы хотите в вашем PDF. В моем случае это была модель в базе данных, но это могут быть строки или что-то еще. Garrett Fogerlie
как ты получаешь блас? что это
1

артный zip-файл. Поместите файлы во временную папку и используйте класс FastZip для создания zip.

Я вижу, SharpZipLib поддерживает потоковую передачу, но я не использовал ее для входных данных. Должно быть в состоянии сделать это, хотя с использованием их основных классов.
Из-за ограничений этого проекта я не могу создавать файлы и сохранять их, даже временно, в папке. Мне нужно динамически создавать файлы в памяти и вернуть его / их в виде файлового потока. Garrett Fogerlie

Похожие вопросы