Writing to ZipArchive using the HttpContext OutputStream

13,209

Solution 1

Note: This has been fixed in .Net Core 2.0. I'm not sure what is the status of the fix for .Net Framework.


Calbertoferreira's answer has some useful information, but the conclusion is mostly wrong. To create an archive, you don't need seek, but you do need to be able to read the Position.

According to the documentation, reading Position should be supported only for seekable streams, but ZipArchive seems to require this even from non-seekable streams, which is a bug.

So, all you need to do to support writing ZIP files directly to OutputStream is to wrap it in a custom Stream that supports getting Position. Something like:

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 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
}

Using this, the following code will write a ZIP archive into 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("========================");
    }
}

Solution 2

A refinement to svick's answer of 2nd February 2014. I found that it was necessary to implement some more methods and properties of the Stream abstract class and to declare the pos member as long. After that it worked like a charm. I haven't extensively tested this class, but it works for the purposes of returning a ZipArchive in the HttpResponse. I assume I've implemented Seek and Read correctly, but they may need some tweaking.

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;
    }
}

Solution 3

If you compare your code adaptation with the version presented in MSDN page you'll see that the ZipArchiveMode.Create is never used, what is used is ZipArchiveMode.Update.

Despite that, the main problem is the OutputStream that doesn't support Read and Seek which is need by the ZipArchive in Update Mode:

When you set the mode to Update, the underlying file or stream must support reading, writing, and seeking. The content of the entire archive is held in memory, and no data is written to the underlying file or stream until the archive is disposed.

Source: MSDN

You weren't getting any exceptions with the create mode because it only needs to write:

When you set the mode to Create, the underlying file or stream must support writing, but does not have to support seeking. Each entry in the archive can be opened only once for writing. If you create a single entry, the data is written to the underlying stream or file as soon as it is available. If you create multiple entries, such as by calling the CreateFromDirectory method, the data is written to the underlying stream or file after all the entries are created.

Source: MSDN

I believe you can't create a zip file directly in the OutputStream since it's a network stream and seek is not supported:

Streams can support seeking. Seeking refers to querying and modifying the current position within a stream. Seek capability depends on the kind of backing store a stream has. For example, network streams have no unified concept of a current position, and therefore typically do not support seeking.

An alternative could be writing to a memory stream, then use the OutputStream.Write method to send the zip file.

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: With feedback from comments and further reading, you could be creating large Zip files, so the memory stream could cause you problems.

In this case i suggest you create the zip file on the web server then output the file using Response.WriteFile .

Share:
13,209
Daniel Sørensen
Author by

Daniel Sørensen

Updated on June 05, 2022

Comments

  • Daniel Sørensen
    Daniel Sørensen almost 2 years

    I've been trying to get the "new" ZipArchive included in .NET 4.5 (System.IO.Compression.ZipArchive) to work in a ASP.NET site. But it seems like it doesn't like writing to the stream of HttpContext.Response.OutputStream.

    My following code example will throw

    System.NotSupportedException: Specified method is not supported

    as soon as a write is attempted on the stream.

    The CanWrite property on the stream returns true.

    If I exchange the OutputStream with a filestream, pointing to a local directory, it works. What gives?

    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
    
  • Daniel Sørensen
    Daniel Sørensen almost 11 years
    So in other words, it's impossible to write it directly to the outputStream? Like I said before, a memoryStream is not an option as the file sizes vary and can become huge.
  • Carlos Ferreira
    Carlos Ferreira almost 11 years
    I'm trying with response.filter to see if i can find a workaround, but according to MSDN you can't: Streams can support seeking. Seeking refers to querying and modifying the current position within a stream. Seek capability depends on the kind of backing store a stream has. For example, network streams have no unified concept of a current position, and therefore typically do not support seeking.
  • svick
    svick over 10 years
    I'm confused, don't your quotes mean that it should work with ZipArchiveMode.Create?
  • Carlos Ferreira
    Carlos Ferreira over 10 years
    You're right.The problem is happens when you update the zip archive, to update the archive the stream must support seeking.
  • AndyD
    AndyD over 10 years
    Nice, I did wonder whether it was just getting the position that was the issue and assumed there would be other issues. I didn't try it, you did so thanks for finding out! I agree, the ZipArchive code could easily keep track of how many bytes it has already written and not require a wrapper like this.
  • Daniel Sørensen
    Daniel Sørensen about 10 years
    Nice indeed, I used a 3rd party zip library instead, cause I couldn't find a solution at the time. But I'll go back and try to use your idea when I have time. I'll return and mark it answered, if it works! :)
  • Doug Domeny
    Doug Domeny over 7 years
    The problem occurs with ZipArchiveMode.Create too.
  • Brandon Tull
    Brandon Tull almost 7 years
    After spending more than a day on this issue, you helped me solve a problem using ZipArchive with a custom non-seekable stream I implemented. Thank you!
  • Andrey Burykin
    Andrey Burykin almost 4 years
    @svick looks good, the only point - it's better to change "pos" variable type to long, because the Position property has long type.
  • Andrey Burykin
    Andrey Burykin almost 4 years
    Looks strange - CanSeek returns false, but at the same time Seek(...) method is implemented. Also I guess there is no needing for reading in the Response.OutputStream. So I prefer to return false for the CanRead property and do not implement Read(...) method.
  • flayman
    flayman almost 4 years
    Once I got it working in my implementation, I stopped. But you're probably right.