MVC3 возвращает несколько PDF-файлов в виде ZIP-файла
У меня есть представление, которое возвращает 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 ответа
Если ваше решение работает, то проще всего оставить его как есть.
С другой стороны, у меня есть некоторые комментарии по поводу использования вами библиотеки 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, у вас должен быть способ получения "bla", соответствующего zipentry внутри делегата. Делегат не исполняется до zip.Save()
называется! Таким образом, вы не можете полагаться на значение "бла" из цикла.
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;
}
Я не пробовал это, но это должно работать. Это имеет преимущество перед тем, что вы написали, более эффективное использование памяти. Недостатком является то, что это немного сложнее, используя именованные каналы и несколько анонимных функций.
Это имеет смысл, только если содержимое архива находится в диапазоне>1 МБ. Если ваши почтовые индексы меньше, чем вы, вы можете просто сделать это первым способом, который я показал выше.
добавление
Почему вы не можете положиться на ценность bla
в рамках анонимного метода?
Есть два ключевых момента. Во-первых, цикл foreach определяет переменную с именем bla
, который принимает другое значение, каждый раз через цикл. Кажется очевидным, но это стоит заявить прямо.
Во-вторых, анонимный метод передается в качестве аргумента ZipFile.AddEntry()
метод, и он не будет работать во время выполнения цикла foreach. Фактически анонимный метод вызывается повторно, один раз для каждой добавленной записи, во время ZipFile.Save()
, Если вы ссылаетесь на bla
внутри анонимного метода он получает последнее значение, присвоенное 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()
,
В итоге я использовал 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;
}
Как сказал 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]);
}
}
Я бы порекомендовал использовать SharpZipLib для архивирования в стандартный zip-файл. Поместите файлы во временную папку и используйте класс FastZip для создания zip.