2013-05-16 3 views
32

Я пытаюсь получить «новый» ZipArchive, включенный в .NET 4.5 (System.IO.Compression.ZipArchive), для работы на сайте ASP.NET. Но, похоже, ему не нравится писать в поток HttpContext.Response.OutputStream.Запись в ZipArchive с использованием HttpContext 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 
+0

вы пытаетесь использовать его в среде разработки или сервере – Alok

+0

В настоящее время я запускаю его локально, поэтому среда разработки. Я получаю HttpContext от IHttpHandler. –

+0

ОК, MVC или веб-формы? – Alok

ответ

4

Если сравнить адаптации кода с версией, представленной в MSDN page вы увидите, что ZipArchiveMode.Create никогда не используется, то, что используется ZipArchiveMode.Update.

Несмотря на это, основной проблемой является OutputStream, который не поддерживает чтение и Seek, которая необходима в ZipArchive в режиме обновления:

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

Source: MSDN

Вы не получали каких-либо исключений с режимом создания, потому что нужно только написать:

Когда вы установите режим Create, основной файл или поток должен поддерживать но не нуждается в поддержке поиска. Каждая запись в архиве может быть открыта только один раз для записи. Если вы создаете одну запись, данные записываются в базовый поток или файл, как только он будет доступен. Если вы создаете несколько записей, например, вызывая метод CreateFromDirectory, данные записываются в базовый поток или файл после создания всех записей.

Source: MSDN

Я считаю, что вы не можете создать почтовый файл непосредственно в OutputStream, так как это сетевой поток и искать не поддерживается:

Streams может поддерживать поиск. Поиск означает запрос и изменение текущей позиции в потоке. Возможность поиска зависит от типа хранилища резервных копий, которое имеет поток. Например, сетевые потоки не имеют единой концепции текущей позиции и, как правило, не поддерживают поиск.

Альтернатива может быть записана в поток памяти, а затем использовать метод 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.

+0

Таким образом, невозможно записать его непосредственно в outputStream? Как я уже говорил, memoryStream не является вариантом, так как размеры файлов варьируются и могут стать огромными. –

+0

Я пытаюсь с response.filter посмотреть, могу ли я найти обходное решение, но, согласно MSDN, вы не можете: Потоки могут поддерживать поиск. Поиск означает запрос и изменение текущей позиции в потоке. Возможность поиска зависит от типа хранилища резервных копий, которое имеет поток. Например, сетевые потоки не имеют единой концепции текущей позиции и, как правило, не поддерживают поиск. –

+0

Я смущен, разве ваши кавычки не означают, что он должен работать с 'ZipArchiveMode.Create'? – svick

34

Ответ Calbertoferreira содержит полезную информацию, но вывод в основном неправильный. Чтобы создать архив, вам не нужен поиск, но вам нужно уметь читать Position.

Согласно the documentation, чтение Position должен поддерживаться только для потоков доступных для поиска, но ZipArchive, кажется, требует это даже без потоков, доступных для поиска, которые я думаю, это a bug.

Итак, все, что вам нужно сделать для поддержки записи 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("========================"); 
    } 
} 
+0

Приятно, я действительно задавался вопросом, было ли это просто занятие, которое было проблемой, и предположил, что будут другие проблемы. Я не пробовал, вы сделали это, спасибо, что узнали! Я согласен, что код ZipArchive может легко отслеживать, сколько байтов он уже написал, и не требует такой оболочки. – AndyD

+0

Действительно, я использовал стороннюю zip-библиотеку вместо этого, потому что я не мог найти решение в то время. Но я вернусь и попытаюсь использовать вашу идею, когда у меня будет время. Я вернусь и отметю его, если это сработает! :) –

+0

Сохранение моего бекона, когда мы говорим @svick спасибо тонну. – TheNorthWes

0

упрощенная версия svick «s ответ для архивирования на стороне сервера файл и отправка это через OutputStream:

using (var outputStream = new PositionWrapperStream(Response.OutputStream)) 
using (var archive = new ZipArchive(outputStream, ZipArchiveMode.Create, false)) 
{ 
    var entry = archive.CreateEntryFromFile(fullPathOfFileOnDisk, fileNameAppearingInZipArchive); 
} 

(в случае, если это кажется очевидным, это не для меня!)

0

Предположительно это не приложение 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); 
     } 
    } 
} 

(Entire FileStreamResult on 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; 

Наконец, если вы будете писать большие потоки (или большее количество из них в любой момент времени), тогда вы можете захотеть использовать анонимные каналы для записи данных в выходной поток сразу же после того, как вы напишете его в базовый поток в zip-файле. Потому что вы будете хранить все содержимое файла в памяти на сервере.The end of this answer к подобному вопросу имеет хорошее объяснение как сделать то.

3

Уточнение ответа 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; 
    } 
} 
Смежные вопросы