Writing to ZipArchive using the HttpContext OutputStream
Asked Answered
G

6

43

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
Georgeanngeorgeanna answered 16/5, 2013 at 10:51 Comment(11)
are you trying it in development environment or serverNegris
I'm currently running it locally, so development environment. I'm getting the HttpContext from an IHttpHandler.Neuter
ok, MVC or Web forms?Negris
Web forms. I'm limited from using MVC in my project sadly.Neuter
can you try linear coding you maybe missing response.contenttype etc; in meantime i will try to re-create the error and be back at you. Happy codingNegris
I'm not sure what you mean by linear coding. I've already tried setting different ContentTypes to no avail. Not sure if it has anything to do with the outputStream anyway though.Neuter
Try writing to a MemoryStream. Seek to the start and then use CopyTo to copy that to the responseSelfinduced
That will work just like the FileStream, but I'm trying to avoid putting my entire files into memory or disk, because the files I'm handling can be of any huge size. That's why I need to add it directly to the OutputStream.Neuter
Can you get a stack trace from the exception? What actual method is the NotSupportedException coming from? Which object?Diandiana
I updated the post with the stacktrace. It seems the write method calls get_Position() on the stream, which is not supported.. Any workaround?Neuter
You can't access directly the OutputStream. If you press F12 on OutputStream you'll see that OutputStream is ReadOnly (Get).Ina
D
60

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("========================");
    }
}
Divert answered 2/2, 2014 at 16:43 Comment(4)
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.Jae
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! :)Neuter
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!Dyspeptic
@Divert looks good, the only point - it's better to change "pos" variable type to long, because the Position property has long type.Chilt
I
7

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 .

Ina answered 22/5, 2013 at 10:30 Comment(5)
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.Neuter
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.Ina
I'm confused, don't your quotes mean that it should work with ZipArchiveMode.Create?Divert
You're right.The problem is happens when you update the zip archive, to update the archive the stream must support seeking.Ina
The problem occurs with ZipArchiveMode.Create too.Adelaidaadelaide
R
6

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;
    }
}
Recollect answered 18/3, 2016 at 10:19 Comment(2)
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.Chilt
Once I got it working in my implementation, I stopped. But you're probably right.Recollect
I
0

An simplified version of svick's answer for zipping a server-side file and sending it via the OutputStream:

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

(In case this seems obvious, it wasn't to me!)

Isomeric answered 3/10, 2014 at 14:11 Comment(0)
G
0

Presumably this is not an MVC app, where you could easily just use the FileStreamResult class.

I'm using this currently with ZipArchive created using a MemoryStream, so I know it works.

With that in mind, have a look at the FileStreamResult.WriteFile() method:

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)

Here is how I'm generating and returning the ZipArchive.
You should have no issues replacing the FSR with the guts of the WriteFile method from above, where FileStream becomes resultStream from the code below:

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;

Finally, if you will be writing large streams (or a larger number of them at any given time), then you may want to consider using anonymous pipes to write the data to the output stream immediately after you write it to the underlying stream in the zip file. Because you will be holding all the file contents in memory on the server. The end of this answer to a similar question has a nice explanation of how to do that.

Gaud answered 29/10, 2014 at 15:46 Comment(0)
H
0

This question still pops up in Google search so I'll add an answer for .NET Core 5/6/7.

Looks like in .NET Core you can simply provide Reponse.Body to the ZipArchive to save the results to. YOu don't need an intermediary buffer or a MemoryStream that will double your memory usage.

I tested this and it works fine under .NET 6 on IIS server, however, it fails on Kestrel on both Windows and Linux because of this bug: https://github.com/dotnet/runtime/issues/1560

The workaround is to use Response.BodyWriter.AsStream() instead of writing to the stream directly (until the bug is fixed):

//in your controller:
public async Task DownloadZip()
{
    Response.ContentType = "application/zip";
    Response.Headers.Append("Content-Disposition", "attachment;filename=archive.zip");

    using (var z = new ZipArchive(
        Response.BodyWriter.AsStream(), //<-- this
        ZipArchiveMode.Create,
        true))
    {
        //do you thing to add files to archive
    }
}
Hairbrush answered 18/3, 2023 at 20:50 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.