15

How can I create a Web API controller that generates and returns a compressed zip file streamed from a collection of in-memory JPEG files (MemoryStream objects). I'm attempting to use DotNetZip Library. I found this example: https://web.archive.org/web/20211020131216/https://www.4guysfromrolla.com/articles/092910-1.aspx#postadlink. But the Response.OutputStream is not available in Web API and so that technique doesn't quite work. Therefore I tried saving the zip file to a new MemoryStream; but it threw. Lastly, I tried using PushStreamContent. Here's my code:

    public HttpResponseMessage Get(string imageIDsList) {
        var imageIDs = imageIDsList.Split(',').Select(_ => int.Parse(_));
        var any = _dataContext.DeepZoomImages.Select(_ => _.ImageID).Where(_ => imageIDs.Contains(_)).Any();
        if (!any) {
            throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.NotFound));
        }
        var dzImages = _dataContext.DeepZoomImages.Where(_ => imageIDs.Contains(_.ImageID));
        using (var zipFile = new ZipFile()) {
            foreach (var dzImage in dzImages) {
                var bitmap = GetFullSizeBitmap(dzImage);
                var memoryStream = new MemoryStream();
                bitmap.Save(memoryStream, ImageFormat.Jpeg);
                var fileName = string.Format("{0}.jpg", dzImage.ImageName);
                zipFile.AddEntry(fileName, memoryStream);
            }
            var response = new HttpResponseMessage(HttpStatusCode.OK);
            var memStream = new MemoryStream();
            zipFile.Save(memStream); //Null Reference Exception
            response.Content = new ByteArrayContent(memStream.ToArray());
            response.Content.Headers.ContentType = new MediaTypeHeaderValue("application/zip");
            response.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment") { FileName = string.Format("{0}_images.zip", dzImages.Count()) };
            return response;
        }
    }

zipFile.Save(memStream) throws null reference. But neither zipFile nor memStream are null and there is no internal exception. So I'm not sure what's causing the null reference. I've very little experience with Web API, memory streams, and I've never used DotNetZipLibrary before. This is a follow up to this question: Want an efficient ASP.NET Web API controller that can reliably return 30 to 50 ~3MB JPEGs

Any ideas? thanks!

Community
  • 1
  • 1
CalvinDale
  • 9,005
  • 5
  • 29
  • 38
  • I don't see anything wrong with your WebApi code. However I would use StreamContent instead of ByteArrayContent. I realize you said you get the null reference at the zipFile.Save, but I would try removing the using() around the new ZipFile(). – Darrel Miller Jan 29 '13 at 17:14
  • Thanks. Unfortunately removing the using didn't help. It still throws NullReferenceException at zipFile.Save(...). Here's the trace: at Ionic.Zlib.ParallelDeflateOutputStream._Flush(Boolean lastInput) at Ionic.Zlib.ParallelDeflateOutputStream.Close() at Ionic.Zip.ZipEntry.FinishOutputStream(Stream s, CountingStream entryCounter, Stream encryptor, Stream compressor, CrcCalculatorStream output) at Ionic.Zip.ZipEntry._WriteEntryData(Stream s) at Ionic.Zip.ZipEntry.Write(Stream s) at Ionic.Zip.ZipFile.Save() at Ionic.Zip.ZipFile.Save(Stream outputStream) – CalvinDale Jan 29 '13 at 18:12
  • After doing bitmap.Save, what is the position of memoryStream? You might need to set memoryStream.Position = 0 otherwise you could be storing zero-bytes into the zip file. – Darrel Miller Jan 29 '13 at 18:33
  • So it doesn't get past the bitmap.Save(). That's where it throws. The MemoryStream's position is zero when Save is called. As I mentioned above, I don't have much experience using MemoryStream objects. And I haven't had time to read up on them yet. I wonder if I need to pre-allocate space in the stream. – CalvinDale Jan 29 '13 at 18:50
  • 1
    I thought you had said it was the zipFile.Save(...) where it was throwing. You don't need to pre-allocate anything for memory streams. The way you are using them is fine. I just think you are missing `memoryStream.Position = 0;` after `bitmap.Save(memoryStream, ImageFormat.Jpeg);` – Darrel Miller Jan 29 '13 at 19:02
  • Yes indeed -- I was in a bit of a rush -- zipFile.Save(...) is where it throws. Setting `memoryStream.Position = 0; after bitmap.Save(memoryStream, ImageFormat.Jpeg);` solved it. THANKS for your help. – CalvinDale Jan 29 '13 at 19:38

4 Answers4

16

A more generic approach would work like this:

using Ionic.Zip; // from NUGET-Package "DotNetZip"

public HttpResponseMessage Zipped()
{
    using (var zipFile = new ZipFile())
    {
        // add all files you need from disk, database or memory
        // zipFile.AddEntry(...);

        return ZipContentResult(zipFile);
    }
}

protected HttpResponseMessage ZipContentResult(ZipFile zipFile)
{
    // inspired from http://stackoverflow.com/a/16171977/92756
    var pushStreamContent = new PushStreamContent((stream, content, context) =>
    {
        zipFile.Save(stream);
        stream.Close(); // After save we close the stream to signal that we are done writing.
    }, "application/zip");

    return new HttpResponseMessage(HttpStatusCode.OK) {Content = pushStreamContent};
}

The ZipContentResult method could also live in a base class and be used from any other action in any api controller.

Felix Alcala
  • 2,837
  • 3
  • 27
  • 31
  • 2
    that works great. Don't know why this went unnoticed – Jonesopolis Dec 16 '14 at 21:45
  • @Felix Alcala this code works great but I'm getting a corrupted zip, any idea why? – user3378165 May 11 '16 at 08:42
  • @user3378165 Unfortunately not. It has been working on our server without any problem since I've posted it. Yet, C#-zip-handling is always some sort of a pain. Some starting points could be: is the download truncated? Maybe the filenames contain non-US-characters and might need special consideration? Can you find out more about the corruption (e.g. work in windows explorer, but not in 7zip)? When opening zips in mono/Xamarin, special build settings need to be set, etc. – Felix Alcala May 11 '16 at 08:48
7

The PushStreamContent class can be used in this case to eliminate the need for the MemoryStream, at least for the whole zip file. It can be implemented like this:

    public HttpResponseMessage Get(string imageIDsList)
    {
        var imageIDs = imageIDsList.Split(',').Select(_ => int.Parse(_));
        var any = _dataContext.DeepZoomImages.Select(_ => _.ImageID).Where(_ => imageIDs.Contains(_)).Any();
        if (!any)
        {
            throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.NotFound));
        }
        var dzImages = _dataContext.DeepZoomImages.Where(_ => imageIDs.Contains(_.ImageID));
        var streamContent = new PushStreamContent((outputStream, httpContext, transportContent) =>
            {
                try
                {
                    using (var zipFile = new ZipFile())
                    {
                        foreach (var dzImage in dzImages)
                        {
                            var bitmap = GetFullSizeBitmap(dzImage);
                            var memoryStream = new MemoryStream();
                            bitmap.Save(memoryStream, ImageFormat.Jpeg);
                            memoryStream.Position = 0;
                            var fileName = string.Format("{0}.jpg", dzImage.ImageName);
                            zipFile.AddEntry(fileName, memoryStream);
                        }
                        zipFile.Save(outputStream); //Null Reference Exception
                    }
                }
                finally
                {
                    outputStream.Close();
                }
            });
        streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/zip");
        streamContent.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment")
        {
            FileName = string.Format("{0}_images.zip", dzImages.Count()),
        };

        var response = new HttpResponseMessage(HttpStatusCode.OK)
            {
                Content = streamContent
            };

        return response;
    }

Ideally it would be possible to make this even more dynamically created using the ZipOutputStream class to dynamically create the zip instead of using ZipFile. In that case the MemoryStream for each bitmap would not be needed.

Steve Wranovsky
  • 5,503
  • 4
  • 34
  • 52
1
public HttpResponseMessage GetItemsInZip(int id)
    {           
            var itemsToWrite = // return array of objects based on id;

            // create zip file stream
            MemoryStream archiveStream = new MemoryStream();
            using (ZipArchive archiveFile = new ZipArchive(archiveStream, ZipArchiveMode.Create, true))
            {
                foreach (var item in itemsToWrite)
                {
                    // create file streams
                    // add the stream to zip file

                    var entry = archiveFile.CreateEntry(item.FileName);
                    using (StreamWriter sw = new StreamWriter(entry.Open()))
                    {
                        sw.Write(item.Content);
                    }
                }
            }

            // return the zip file stream to http response content                
            HttpResponseMessage responseMsg = new HttpResponseMessage(HttpStatusCode.OK);                
            responseMsg.Content = new ByteArrayContent(archiveStream.ToArray());
            archiveStream.Dispose();
            responseMsg.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment") { FileName = "test.zip" };
            responseMsg.Content.Headers.ContentType = new MediaTypeHeaderValue("application/zip");

            return responseMsg;          
    }

Used Framework .NET 4.6.1 with MVC 5

Vytheese
  • 101
  • 1
  • 11
0

I just had the same problem as you.

zipFile.Save(outputStream); //I got Null Reference Exception here.

The problem was that I was adding files from a memory stream like so:

zip.AddEntry(fileName, ms);

All you have to do is change it to this:

zip.AddEntry(fileName, ms.ToArray());

It seems that when the writer decides to actually write the file and tries to read the stream, the stream is garbage collected or sth...

Cheers!

user2173353
  • 4,316
  • 4
  • 47
  • 79