Подтвердить что ты не робот

Запись в 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
4b9b3361

Ответ 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;

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