Запись в ZipArchive с использованием HttpContext OutputStream
Я пытаюсь получить "новый" ZipArchive, включенный в .NET 4.5 (System.IO.Compression.ZipArchive
), для работы на сайте ASP.NET. Но, похоже, ему не нравится писать в поток HttpContext.Response.OutputStream
.
Мой следующий пример кода бросит
System.NotSupportedException: указанный метод не поддерживается
как только будет предпринята попытка записи в потоке.
Свойство CanWrite
в потоке возвращает true.
Если я обменяю OutputStream с файловым потоком, указывая на локальный каталог, он работает. Что дает?
ZipArchive archive = new ZipArchive(HttpContext.Response.OutputStream, ZipArchiveMode.Create, false);
ZipArchiveEntry entry = archive.CreateEntry("filename");
using (StreamWriter writer = new StreamWriter(entry.Open()))
{
writer.WriteLine("Information about this package.");
writer.WriteLine("========================");
}
StackTrace:
[NotSupportedException: Specified method is not supported.]
System.Web.HttpResponseStream.get_Position() +29
System.IO.Compression.ZipArchiveEntry.WriteLocalFileHeader(Boolean isEmptyFile) +389
System.IO.Compression.DirectToArchiveWriterStream.Write(Byte[] buffer, Int32 offset, Int32 count) +94
System.IO.Compression.WrappedStream.Write(Byte[] buffer, Int32 offset, Int32 count) +41
Ответы
Ответ 1
Примечание. Это исправлено в .Net Core 2.0. Я не уверен, каково состояние исправления для .Net Framework.
Ответ Calbertoferreira имеет некоторую полезную информацию, но в большинстве своем вывод неверный. Чтобы создать архив, вам не нужно искать, но вы должны быть в состоянии прочитать Position
.
Согласно документации, чтение Position
должно поддерживаться только для потоков, которые можно ZipArchive
, но ZipArchive
кажется, требует этого даже для потоков, которые нельзя найти, что является ошибкой.
Итак, все, что вам нужно сделать для поддержки записи файлов ZIP непосредственно в OutputStream
- это обернуть его в пользовательский Stream
который поддерживает получение Position
. Что-то вроде:
class PositionWrapperStream : Stream
{
private readonly Stream wrapped;
private int pos = 0;
public PositionWrapperStream(Stream wrapped)
{
this.wrapped = wrapped;
}
public override bool CanSeek { get { return false; } }
public override bool CanWrite { get { return true; } }
public override long Position
{
get { return pos; }
set { throw new NotSupportedException(); }
}
public override void Write(byte[] buffer, int offset, int count)
{
pos += count;
wrapped.Write(buffer, offset, count);
}
public override void Flush()
{
wrapped.Flush();
}
protected override void Dispose(bool disposing)
{
wrapped.Dispose();
base.Dispose(disposing);
}
// all the other required methods can throw NotSupportedException
}
Используя это, следующий код запишет ZIP-архив в OutputStream
:
using (var outputStream = new PositionWrapperStream(Response.OutputStream))
using (var archive = new ZipArchive(outputStream, ZipArchiveMode.Create, false))
{
var entry = archive.CreateEntry("filename");
using (var writer = new StreamWriter(entry.Open()))
{
writer.WriteLine("Information about this package.");
writer.WriteLine("========================");
}
}
Ответ 2
Уточнение ответа svick от 2 февраля 2014 года. Я обнаружил, что необходимо реализовать еще несколько методов и свойств абстрактного класса Stream и объявить pos-член как можно дольше. После этого он работал как шарм. Я не тестировал этот класс, но он работает для возвращения ZipArchive в HttpResponse. Я предполагаю, что я правильно выполнил поиск и чтение, но может потребоваться некоторые настройки.
class PositionWrapperStream : Stream
{
private readonly Stream wrapped;
private long pos = 0;
public PositionWrapperStream(Stream wrapped)
{
this.wrapped = wrapped;
}
public override bool CanSeek
{
get { return false; }
}
public override bool CanWrite
{
get { return true; }
}
public override long Position
{
get { return pos; }
set { throw new NotSupportedException(); }
}
public override bool CanRead
{
get { return wrapped.CanRead; }
}
public override long Length
{
get { return wrapped.Length; }
}
public override void Write(byte[] buffer, int offset, int count)
{
pos += count;
wrapped.Write(buffer, offset, count);
}
public override void Flush()
{
wrapped.Flush();
}
protected override void Dispose(bool disposing)
{
wrapped.Dispose();
base.Dispose(disposing);
}
public override long Seek(long offset, SeekOrigin origin)
{
switch (origin)
{
case SeekOrigin.Begin:
pos = 0;
break;
case SeekOrigin.End:
pos = Length - 1;
break;
}
pos += offset;
return wrapped.Seek(offset, origin);
}
public override void SetLength(long value)
{
wrapped.SetLength(value);
}
public override int Read(byte[] buffer, int offset, int count)
{
pos += offset;
int result = wrapped.Read(buffer, offset, count);
pos += count;
return result;
}
}
Ответ 3
Если вы сравните свою адаптацию кода с версией, представленной на странице MSDN, вы увидите, что ZipArchiveMode.Create никогда не используется, что используется ZipArchiveMode.Update.
Несмотря на это, основная проблема - это OutputStream, который не поддерживает чтение и поиск, которые нужны ZipArchive в режиме обновления:
Когда вы устанавливаете режим "Обновить", базовый файл или поток должны поддерживать чтение, письмо и поиск. Содержание всего архив хранится в памяти, и никакие данные не записываются в файл или поток, пока архив не будет удален.
Источник: MSDN
Вы не получали никаких исключений в режиме создания, потому что ему нужно только написать:
Когда вы устанавливаете режим Create, базовый файл или поток должны поддерживать запись, но не должны поддерживать поиск. Каждая запись в архиве может быть открыта только один раз для записи. Если вы создаете одну запись, данные записываются в базовый поток или файл, как только он будет доступен. Если вы создаете несколько записей, например, вызывая метод CreateFromDirectory, данные записываются в базовый поток или файл после создания всех записей.
Источник: MSDN
Я считаю, что вы не можете создать zip файл непосредственно в OutputStream, так как сетевой поток и поиск не поддерживаются:
Потоки могут поддерживать поиск. Поиск означает запрос и изменение текущей позиции в потоке. Возможность поиска зависит от типа хранилища резервных копий, которое имеет поток. Например, сетевые потоки не имеют единой концепции текущей позиции и поэтому обычно не поддерживают поиск.
Альтернативой может быть запись в поток памяти, а затем использовать метод OutputStream.Write для отправки zip файла.
MemoryStream ZipInMemory = new MemoryStream();
using (ZipArchive UpdateArchive = new ZipArchive(ZipInMemory, ZipArchiveMode.Update))
{
ZipArchiveEntry Zipentry = UpdateArchive.CreateEntry("filename.txt");
foreach (ZipArchiveEntry entry in UpdateArchive.Entries)
{
using (StreamWriter writer = new StreamWriter(entry.Open()))
{
writer.WriteLine("Information about this package.");
writer.WriteLine("========================");
}
}
}
byte[] buffer = ZipInMemory.GetBuffer();
Response.AppendHeader("content-disposition", "attachment; filename=Zip_" + DateTime.Now.ToString() + ".zip");
Response.AppendHeader("content-length", buffer.Length.ToString());
Response.ContentType = "application/x-compressed";
Response.OutputStream.Write(buffer, 0, buffer.Length);
EDIT: С отзывами комментариев и дальнейшим чтением вы можете создавать большие Zip файлы, поэтому поток памяти может вызвать проблемы.
В этом случае я предлагаю вам создать zip файл на веб-сервере, а затем вывести файл с помощью Response.WriteFile.
Ответ 4
Упрощенная версия ответа svick для zip файла на стороне сервера и отправки его через OutputStream:
using (var outputStream = new PositionWrapperStream(Response.OutputStream))
using (var archive = new ZipArchive(outputStream, ZipArchiveMode.Create, false))
{
var entry = archive.CreateEntryFromFile(fullPathOfFileOnDisk, fileNameAppearingInZipArchive);
}
(В случае, если это кажется очевидным, это было не для меня!)
Ответ 5
Предположительно, это не приложение MVC, где вы можете легко использовать класс FileStreamResult
.
Я использую это в настоящее время с ZipArchive
созданным с использованием MemoryStream
, поэтому я знаю, что это работает.
Имея это в виду, взгляните на метод FileStreamResult.WriteFile()
:
protected override void WriteFile(HttpResponseBase response)
{
// grab chunks of data and write to the output stream
Stream outputStream = response.OutputStream;
using (FileStream)
{
byte[] buffer = newbyte[_bufferSize];
while (true)
{
int bytesRead = FileStream.Read(buffer, 0, _bufferSize);
if (bytesRead == 0)
{
// no more data
break;
}
outputStream.Write(buffer, 0, bytesRead);
}
}
}
(Весь FileStreamResult на CodePlex)
Вот как я генерирую и возвращаю ZipArchive
.
У вас не должно возникнуть проблем с заменой FSR на внутренности метода WriteFile
сверху, где FileStream
становится resultStream
из приведенного ниже кода:
var resultStream = new MemoryStream();
using (var zipArchive = new ZipArchive(resultStream, ZipArchiveMode.Create, true))
{
foreach (var doc in req)
{
var fileName = string.Format("Install.Rollback.{0}.v{1}.docx", doc.AppName, doc.Version);
var xmlData = doc.GetXDocument();
var fileStream = WriteWord.BuildFile(templatePath, xmlData);
var docZipEntry = zipArchive.CreateEntry(fileName, CompressionLevel.Optimal);
using (var entryStream = docZipEntry.Open())
{
fileStream.CopyTo(entryStream);
}
}
}
resultStream.Position = 0;
// add the Response Header for downloading the file
var cd = new ContentDisposition
{
FileName = string.Format(
"{0}.{1}.{2}.{3}.Install.Rollback.Documents.zip",
DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day, (long)DateTime.Now.TimeOfDay.TotalSeconds),
// always prompt the user for downloading, set to true if you want
// the browser to try to show the file inline
Inline = false,
};
Response.AppendHeader("Content-Disposition", cd.ToString());
// stuff the zip package into a FileStreamResult
var fsr = new FileStreamResult(resultStream, MediaTypeNames.Application.Zip);
return fsr;
Наконец, если вы будете записывать большие потоки (или большее их количество в любой момент времени), то вы можете рассмотреть возможность использования анонимных каналов для записи данных в выходной поток сразу после записи их в основной поток в почтовый файл Потому что вы будете хранить все содержимое файла в памяти на сервере. В конце этого ответа на аналогичный вопрос есть хорошее объяснение того, как это сделать.