Advice on how to Zip

Apr 27, 2010 at 2:52 PM

Hi,

I have a fairly specific and straightforward requirement and would like some advice on the best way to approach the zipping part of it.

I have a webservice which has to return a Zip file, as a Stream (this bit is important as the file is going to be streamed across the wire!).

The client will call the webservice with a list of object IDs which will be used to identify the files to be added to the zip.  The files come from a database and are read as byte arrays.

The files will need to be added to directories in the zip file based on metadata which is retrieved as part of the database read to get the file byte array.

The entire zip file must NOT be held in memory on the server as it could potentially be very large and many users could be calling the service to get zip files, so could run out of memory.

How would you suggest I approach it?  I see the DotNetZip library is very extensive and I don't know which mechanism to use!

I don't know if I need to use ZipOutputStream, etc etc...

Thanks in advance!

Coordinator
Apr 28, 2010 at 4:46 AM
Edited Apr 28, 2010 at 4:55 AM

Yes, it's possible to do, but maybe not as simple as you'd like.

The problem is, I don't know if WCF Streaming allows the use of a writable stream.  I think it does not.  Every WCF Streaming example I have seen employs a readable stream, where the client gets a System.IO.Stream from the service, and then performs a Read on it.  The server-side stream must provide the implementation of Read().

DotNetZip (currently) does not provide a way to get a readable stream out of a ZipFile object.  The ZipFile can WRITE into a stream to save the zipped data, but the ZipFile itself cannot act as or generate a readable Stream that contains the zipped data, which is what the WCF runtime wants.  There is a mismatch between the stream genders, if you see what I mean.  The same gender problem occurs if you consider using the ZipOutputStream class - it is a WRITEable stream - it does not support Read().

The simple way to work around this mismatch is to save the dynamically-generated zip to a filesystem file with a temp name on the server side, then return a FileStream for that file. 

Stream GetZippedData(ObjectIds objectIds)
{
  string t = GetTemporaryFileName();
  using (var zip = new ZipFile())
  {
    foreach (var id in objectIds)     
      zip.AddEntry(...);
    zip.Save(t);
  }
  return new FileStream(t, FileMode.Open);
}

When the transfer is complete, you'll need to delete the temporary file from the server filesystem. I don't know the easiest way to do that. At no point would you ever have "all the zipfile data" in memory at any time, for any request. But, you would need to use the filesystem for a "staging area".

Longer term, it would be nice if the ZipFile class implemented a  GetReadableSaveStream() method, or something like that.  This would provide a stream of the correct gender, so that you could just return it directly to the client. When the client invoked Read() on that stream, it would read the next bytes from the saved form of the ZipFile instance.  There would never be a Write required to the filesystem on the server side. 

I can envision how to implement this in DotNetZip, but ... it's just an idea at this point.  The server-side code would look like this:

Stream GetZippedData(ObjectIds objectIds)
{
  var zip = new ZipFile();
  foreach (var o in objectIds)
    zip.AddEntry(...);
  return zip.GetReadableZipStream();
}

 

All of this discussion is based on the assumption that WCF Streaming does not support WRITEable streams.  I don't know that to be true, for certain.  You might first want to confirm that before proceeding.  If WCF Streaming supports writable streams, then it's very simple indeed.  The WCF Service just needs to call ZipFile.Save(writeableStream) and you're done.

 

 

Apr 28, 2010 at 6:56 AM

Hi Cheeso,

That's great!!

I suspected I may have to dump the file out to the file system.  I've looked into WCF streaming and you're exactly right you need to return a readable  System.IO.Stream from the service.  That's fine.

So, I assume I will just use an instance of ZipFile, add my files to it and then save to the file system on the server with a temp name like a guid?  Reading the documentation the ZipFile instance is not an in-memory instance of the whole zip file with all of it's compressed content? it uses buffers?  All good.

The next bit I am struggling with is how to add the files with the correct directory path in the archive.  The files I want to add are not on the file system, but read from a database as a byte array, along with a "directory name" which needs to be the directory in the archive. So I was thinking I will execute a data reader against the db to get my byte array and directory, and then  loop through the reader adding each byte array file to the zip in the relevant archive directory. So I may end up with:

ZipFile
    DirectoryA
        Bytes 1
        Bytes 2
        Bytes 3
    DirectoryB
        Bytes 4
        Etc...

However I couldn't see a method in ZipFile to add a file as a byte array to a specific directoryPathInArchive...can you help?

Many thanks,

Paul

On 28 Apr 2010 04:46, "Cheeso" <notifications@codeplex.com> wrote:

From: Cheeso

Yes, it's possible to do, but maybe not as simple as you'd like.

The problem is, I don't know if WCF Streaming allows the use of a writable stream.  I think it does not.  Every WCF Streaming example I have seen employs a readable stream, where the client gets a System.IO.Stream from the service, and then performs a Read on it.  The server-side stream must provide the implementation of Read().

DotNetZip (currently) does not provide a way to get a readable stream out of a ZipFile object.  The ZipFile can WRITE into a stream, but the ZipFile itself cannot act as or generate a readable Stream, which is what the WCF runtime wants.  There is a mismatch between the stream genders, if you see what I mean.  The same gender problem occurs if you consider using the ZipOutputStream class - it is a WRITEable stream - it does not support Read().

The simple way to work around this mismatch is to save the dynamically-generated zip to a filesystem file with a temp name on the server side, then return a FileStream for that file.  When the transfer is complete, you'll need to delete the temporary file from the server filesystem.  At no point would you ever have "all the zipfile data" in memory at any time, for any request.  But, you would need to use the filesystem for a "staging area".

Longer term, it would be nice if the ZipFile class implemented a  GetReadableSaveStream() method, or something like that.  This would provide a stream of the correct gender, so that you could just return it directly to the client. When the client invoked Read() on that stream, it would read the next bytes from the saved form of the ZipFile instance.  There would nevevr be a Write required to the filesystem on the server side. 

I can envision how to implement this in DotNetZip, but ... it's just an idea at this point.

 

 

Read the full discussion online.

To add a post to this discussion, reply to this email (DotNetZip@discussions.codeplex.com)

To start a new discussion for this project, email DotNetZip@discussions.codeplex.com

You are receiving this email because you subscribed to this discussion on CodePlex. You can unsubscribe on CodePlex.com.

Please note: Images and attachments will be removed from emails. Any posts to this discussion will also be available online at CodePlex.com

Coordinator
Apr 28, 2010 at 7:22 AM

Yes, I can help.

Use ZipFile.AddEntry() to add an entry using a byte array as the source for the data.  Specify the "full path" (including the "directoryPathInArchive") in the entry name.  You can use forward or backward slashes in the name.

It's possible to create a "directory entry" in a zip archive.  They are specially-marked zero-length entries.  But, they are not container objects. In other words, there's no hierarchical organization within a zip archive.  The names may suggest a filesystem hierarchy, but the storage in the zip does not reflect that suggested hierarchy.  In fact it isn't necessary to have directory entries at all.  A file entry named "DirectoryA\File1.bin" will be extracted into "DirectoryA" on the target filesystem, even if there is no "DirectoryA" dir entry in the zip archive itself, if you see what I mean.  Some unzip tools like to have the directory entry  - Java Archives for example, require the dir entries in the zip.  Most don't need it.

So that answers your questions.  There's more, though - an answer to a question you didn't ask.

You want to minimize memory usage.  But, adding entries with byte[] as the source, will require that these byte arrays remain in memory at least until the call to ZipFile.Save completes. If your zip consists entirely of data from byte arrays, that means all of the data will be in memory before you save the zip file (before ZipFile.Save()).   Seems like you don't want that.

To avoid that, you should look into the other AddEntry() overloads.  One alows you to specify an opener and a closer delegate.  These will be called within the context of the ZipFile.Save(), one at a time.  So you could, in the opener, read the db data into a byte array, then return a MemoryStream() to DotNetZip.  After DotNetZip finishes compressing that particular entry, it will invoke the closer, at which point you could destroy the byte array.

A better alternative might be the AddEntry() overload that accepts a WriteDelegate.  This one lets you write the data for the entry yourself - so you can chunkwise read bytes from the db, and write them to the zip stream.  DotNetZip calls your WriteDelegate code, for each entry, at the time of Save(). 

Either of these approaches will keep memory usage to a minimum as you create large zip files.

Also consider the ZipOutputStream class - it's an alternative to ZipFile for creating zip files.  ZipOutputStream is a forward-only writable stream, which may be suitable for your purposes.

Apr 28, 2010 at 8:40 AM

Hi Cheeso,

Thanks for the quick response! You're exactly right, I must not keep all the byte arrays in memory at one time.

The delegate techniques sound quite complex, I'm not sure how I actually hook that in to my database reader.

It sounds like the ZipOutputStream may be easier as I can just loop through the data reader adding each byte array to the stream. I presume I can still specify a path in the archive when using a ZipOutputStream?

Also I had another look at WCF streaming and it looks like it may be possible to return a writeable stream, as long as it derives from System.IO.Stream.  could I just return an instance of ZipOutputStream in this case?

Thank you for your continued support!

On 28 Apr 2010 07:22, "Cheeso" <notifications@codeplex.com> wrote:

From: Cheeso

Yes, I can help.

Use ZipFile.AddEntry() to add an entry using a byte array as the source for the data.  Specify the "full path" (including the "directoryPathInArchive") in the entry name.  You can use forward or backward slashes in the name.

It's possible to create a "directory entry" in a zip archive.  They are specially-marked zero-length entries.  But, they are not container objects. In other words, there's no hierarchical organization within a zip archive.  The names may suggest a filesystem hierarchy, but the storage in the zip does not reflect that suggested hierarchy.  In fact it isn't necessary to have directory entries at all.  A file entry named "DirectoryA\File1.bin" will be extracted into "DirectoryA" on the target filesystem, even if there is no "DirectoryA" dir entry in the zip archive itself, if you see what I mean.  Some unzip tools like to have the directory entry  - Java Archives for example, require the dir entries in the zip.  Most don't need it.

So that answers your questions.  There's more, though - an answer to a question you didn't ask.

You want to minimize memory usage.  But, adding entries with byte[] as the source, will require that these byte arrays remain in memory at least until the call to ZipFile.Save completes. If your zip consists entirely of data from byte arrays, that means all of the data will be in memory before you save the zip file (before ZipFile.Save()).   Seems like you don't want that.

To avoid that, you should look into the other AddEntry() overloads.  One alows you to specify an opener and a closer delegate.  These will be called within the context of the ZipFile.Save(), one at a time.  So you could, in the opener, read the db data into a byte array, then return a MemoryStream() to DotNetZip.  After DotNetZip finishes compressing that particular entry, it will invoke the closer, at which point you could destroy the byte array.

A better alternative might be the AddEntry() overload that accepts a WriteDelegate.  This one lets you write the data for the entry yourself - so you can chunkwise read bytes from the db, and write them to the zip stream.  DotNetZip calls your WriteDelegate code, for each entry, at the time of Save(). 

Either of these approaches will keep memory usage to a minimum as you create large zip files.

Also consider the ZipOutputStream class - it's an alternative to ZipFile for creating zip files.  ZipOutputStream is a forward-only writable stream, which may be suitable for your purposes.



Read the full discussion online.

To add a post to this discussion, reply to this email (DotNetZip...

Coordinator
Apr 28, 2010 at 4:53 PM

No, I don't think you can return a ZipOutputStream.  

The delegate methods may sound intimidating but they're pretty simple, check the docs.

On the other hand using ZipOutputStream to create a zip file will probably be very straightforward. Yes, you can still specify a path for the zip entry, using ZipOutputStream.

 

Coordinator
Apr 28, 2010 at 6:43 PM
Edited Apr 30, 2010 at 8:37 PM

Ah, Robinson, an update.

You can do what you want more simply, without resorting to writing the file out to the disk, and without keeping data in memory, using a pair of classes in .NET called  AnonymousPipeClientStream  and AnonymousPipeServerStream . These were added to .NET in v3.5.

What they allow you to do is fixup the mismatch I described above - the one where DotNetZip wants a writable stream, but WCF requires a readable stream.

I just learned about this, it seems like a very elegant and simple solution for your scenario.

You would need this utility method:

static Stream GetPipedStream(Action<Stream> writeAction) 
{ 
    AnonymousPipeServerStream pipeServer = new AnonymousPipeServerStream(); 
    ThreadPool.QueueUserWorkItem(s => 
    { 
        using (pipeServer) 
        { 
            writeAction(pipeServer); 
            pipeServer.WaitForPipeDrain(); 
        } 
    }); 
    return new AnonymousPipeClientStream(pipeServer.GetClientHandleAsString()); 
} 

Don't worry about that code - it's boilerplate and you don't need to understand it too much, in order to use it.

Then, your WCF Service method would be something like this:

Stream GetZippedData(ObjectIds objectIds)
{
    return GetPipedStream(s => 
    { 
        var zip = new ZipFile();
        foreach (var o in objectIds)
            zip.AddEntry(...);
        zip.Save(s);
    }); 
}

Edit: Don't try this, it likely won't work. See lower in the thread for correct code.

Apr 28, 2010 at 7:31 PM

Thanks, you're a legend!

I haven't tried it yet as I'm through for the day but it certainly looks very elegant!  I sure don't understand the anonymous pipe stuff but I will take it as read.

Thanks again!!

On 28 Apr 2010 18:43, "Cheeso" <notifications@codeplex.com> wrote:

From: Cheeso

Ah, Robinson, an update.

You can do what you want more simply, without resorting to writing the file out to the disk, and without keeping data in memory, using a pair of classes in .NET called  AnonymousPipeClientStream  and AnonymousPipeServerStream . These were added to .NET in v3.5.

What they allow you to do is fixup the mismatch I described above - the one where DotNetZip wants a writable stream, but WCF requires a readable stream.

I just learned about this, it seems like a very elegant and simple solution for your scenario.

You would need this utility method:

static Stream GetPipedStream(Action<Stream> writeAction) 
{ 
    AnonymousPipeServerStream pipeServer = new AnonymousPipeServerStream(); 
    ThreadPool.QueueUserWorkItem(s => 
    { 
        using (pipeServer) 
        { 
            writeAction(pipeServer); 
            pipeServer.WaitForPipeDrain(); 
        } 
    }); 
    return new AnonymousPipeClientStream(pipeServer.GetClientHandleAsString()); 
} 

Don't worry about that code - it's boilerplate and you don't need to understand it too much, in order to use it.

Then, your WCF Service method would be something like this:

Stream GetZippedData(ObjectIds objectIds)
{
    return GetPipedStream(s => 
    { 
        var zip = new ZipFile();
        foreach (var o in objectIds)
            zip.AddEntry(...);
        zip.Save(s);
    }); 
}



Read the full discussion online.

To add a post to this discussion, reply to this email (DotNetZip...

Apr 28, 2010 at 8:33 PM

Just one thing that's baffling me about this....if you don't write the file out to the disk and it's not in memory, where exactly is it!?!?

And can I still add byte arrays using this technique and only have one in memory at a time!?

Just a bit puzzled but I don't have a deep understanding of what's going on!

On 28 Apr 2010 18:43, "Cheeso" <notifications@codeplex.com> wrote:

From: Cheeso

Ah, Robinson, an update.

You can do what you want more simply, without resorting to writing the file out to the disk, and without keeping data in memory, using a pair of classes in .NET called  AnonymousPipeClientStream  and AnonymousPipeServerStream . These were added to .NET in v3.5.

What they allow you to do is fixup the mismatch I described above - the one where DotNetZip wants a writable stream, but WCF requires a readable stream.

I just learned about this, it seems like a very elegant and simple solution for your scenario.

You would need this utility method:

static Stream GetPipedStream(Action<Stream> writeAction) 
{ 
    AnonymousPipeServerStream pipeServer = new AnonymousPipeServerStream(); 
    ThreadPool.QueueUserWorkItem(s => 
    { 
        using (pipeServer) 
        { 
            writeAction(pipeServer); 
            pipeServer.WaitForPipeDrain(); 
        } 
    }); 
    return new AnonymousPipeClientStream(pipeServer.GetClientHandleAsString()); 
} 

Don't worry about that code - it's boilerplate and you don't need to understand it too much, in order to use it.

Then, your WCF Service method would be something like this:

Stream GetZippedData(ObjectIds objectIds)
{
    return GetPipedStream(s => 
    { 
        var zip = new ZipFile();
        foreach (var o in objectIds)
            zip.AddEntry(...);
        zip.Save(s);
    }); 
}



Read the full discussion online.

To add a post to this discussion, reply to this email (DotNetZip...

Coordinator
Apr 28, 2010 at 11:29 PM
Edited Apr 29, 2010 at 7:46 PM

Ha!

Well the data is gated in the anonymous pipe, which is an OS-level structure for intercommunication.  By using the GetPipedStream method, your server creates a pipe, and also starts a background thread to write into the pipe.  (FYI Background: A pipe is just an OS structure for intercommunication.  There's a buffer, and some synchronization logic, which allows a reader and writer to access the pipe.)  So the WCF server has a backround thread writing to the pipe.  In the main thread, the WCF server then returns the client side of that pipe - the readable side - from the WCF service method.  In the meantime, the server background thread continues to run.  But the background thread, the writer, cannot actually make any progress, because a writer can write into a pipe only if something else is reading from the pipe, you see.  For each pipe, the OS maintains a buffer, but once that buffer fills up, the writer will have to wait until something reads from the pipe, in order to continue writing.

The WCF client receives the pipe stream, just as it would any Stream from a WCF service method.  As the client calls Stream.Read(), data is read from the buffer on the server side, and the WCF server-side background writer thread is unblocked and is able to write more data into the pipe.  When the client reads enough, the server will complete its writing.  At this point the background thread on the server will just.. stop.  It will be returned to the pool of ready threads.  No cleanup necessary. Eventually the client will drain all the data from the stream - in other words, it will read everything that has been buffered.

So you can see that the answer to the question, where is the data?  is... on the wire.  As the client reads, the server is unblocked from writing.  It is a just-in-time, streaming model.  Each chunk of generated data stays a short while in a small IO buffer on the server, and then is transmitted to the client.  Using this approach, the entire contents of the generated zipfile is never present on the server together at any one time - not in the filesystem and not in a memory buffer.  Only chunks are retained, and each chunk only for a short time.

As for whether you could use ZipFile and byte arrays with this approach?  No - the pitfalls of putting all your entry data into byte arrays remains.

So I guess your server-side code, rather than using the ZipFile class, would use the ZipOutputStream class, and would look more like this:

Stream GetZippedData(ObjectIds objectIds)
{
    return GetPipedStream(s => 
    { 
        var zos = new ZipOutputStream(s);
        foreach (var o in objectIds) 
        {
            zos.PutNextEntry(GetName(o));
            zos.Write(GetData(o));
        }
    }); 
}
Apr 29, 2010 at 1:32 PM

Hi Cheeso,

Thanks for the explanation, makes perfect sense at a high level!

OK so I've tried the anonymous pipe stuff, and things are looking promising.

I tried your first suggestion using the ZipFile class.  I removed the added complication of database access from the equation and wrote a simple test method to zip the same file into an archive a number of times (I put a count in so I could request a large number of files and observe the memory usage).  So my webmethod looked like this:

public Stream GetFileZip(string file, int copies)
{
	return GetPipedStream(s =>
		{
			ZipFile zip = new ZipFile();
			for (int i = 1; i <= copies; i++)
			{
				byte[] bytes = File.ReadAllBytes(file);
				zip.AddEntry("Copy" + i, bytes);
			}
			zip.Save(s);
		});
}

This did work, but as you already stated, the memory usage shot up on the server when I requested 50 copies of a 50mb file.   Presumably because of holding on to the byte array instances...

So next I tried the same thing but this time with a ZipOutputStream:

public Stream GetFileZip(string file, int copies)
{
	return GetPipedStream(s =>
	{
		ZipOutputStream zos = new ZipOutputStream(s);
		for (int i = 1; i <= copies; i++)
		{
			byte[] bytes = File.ReadAllBytes(file);
			zos.PutNextEntry("Copy" + i);
			zos.Write(bytes, 0, bytes.Length);
		}
	});
}

This looked good initially, and I was about to crack open the bubbly.  The zip file streamed back to my client and the memory usage stayed low on the server.  However, when I tried to open the zip file it was "invalid or corrupt".

So I tried getting a small file, and only one copy of it and had the same problem - invalid zip file.

I back up and implemented a simple example to zip directly to the file system using a ZipOutputStream just to make sure I wasn't doing something wrong with the way I was writing the entry.  That simple example was fine so it seems to be a problem when using the ZipOutputStream in conjunction with the anonymous pipes. 

Hmmm, now I'm well and truly stuck!

 
 
Coordinator
Apr 29, 2010 at 7:35 PM
Edited Apr 29, 2010 at 7:39 PM

Hmm, yes.  ok, try this slight modification. 

public Stream GetFileZip(string file, int copies)
{
    return GetPipedStream(s =>
        {
            using (ZipOutputStream zos = new ZipOutputStream(s, true))
            {
                for (int i = 1; i <= copies; i++)
                {
                    byte[] bytes = File.ReadAllBytes(file);
                    zos.PutNextEntry("Copy" + i);
                    zos.Write(bytes, 0, bytes.Length);
                }
            }

        });
}

The ZipOutputStream writes the terminating part of the zip file only when ZipFile.Dispose() is called. (You may know that Dispose() is called implicitly at exit of a "using" scope in C#). In the original version of your code, you were not calling Dispose. It's required by the PipeStream thing to keep the underlying stream open, which is why I omitted the call to Dispose, or the equivalent using() clause, in my initial suggestion.  I had forgotten about the need to call Dispose on the ZipOutputStream to get a valid zip.   I hadn't realized the conflict.

But the ZipOutputStream supports an option to NOT close the stream it wraps - in this case the AnonymousPipeClientStream - when it itself Closes or Disposes. To get that option, use the constructor of ZipOutputStream that accepts a boolean, and specify true for that boolean.  The code here will close (Dispose) the ZipOutputStream, thereby writing the zip terminating data, but leave the underlying stream (the pipe) open.  This is what you want, I think.

 

Coordinator
Apr 29, 2010 at 7:58 PM

Ahhh, just one more thing. When you obtain the bytes from the filesystem, or, in the final application, the database, depending on the size of the data for each entry, you should consider doing it in chunks.
Like this:

public Stream GetFileZip(string file, int copies)
{
    return GetPipedStream(s =>
        {
            int n;
            byte[] bytes = new byte[1024]; // any size will do
            using (ZipOutputStream zos = new ZipOutputStream(s, true))
            {
                for (int i = 1; i <= copies; i++)
                {
                    zos.PutNextEntry("Copy" + i);
                    using (FileStream fs = File.OpenRead(file))
                    {
                        while((n = fs.Read(bytes,0,bytes.Length))>0)
                        {
                            zos.Write(bytes, 0, n);
                        }
                    }
                }
            }
        });
}

In this case, if the filesystem file is 16k, it won't much matter. But if the filesystem file is 100mb, reading that data into a byte array as your code does, will exhibit some poor scalability. The same is true with the byte arrays coming from the database. I don't know how large they are, but if they are larger than, say, 64k each, you should consider streaming them, as above, instead of putting them into one large byte array, before adding them to the zip file. The actual threshold depends on a ton of things, like how much memory your server has, how much concurrency you expect, the relative cost of memory access, and so on. But for sure, doing it in large monolithic chunks will exhibit poor behavior as the chunk size and concurrency rise.

Apr 30, 2010 at 2:08 PM

Hi Cheeso,

You are a true legend!  Works like a dream, minimal memory use on the server, and I just successfully streamed back 3GB of zipped data without a hitch.

And also thanks for the advice on not reading the entire files from the file system into memory!

You've been a fantastic help here!!!!

Regards,

Paul

Coordinator
Apr 30, 2010 at 5:38 PM
Edited Apr 30, 2010 at 5:41 PM

Glad I could help.

Can you tell me, what's the application?  Can you share any details?

Also, I wouldn't mind very much if you left some feedback on this site.  At the top if this page: http://dotnetzip.codeplex.com/releases/view/27890 
...there is an "add your own rating" link. 

Apr 30, 2010 at 5:45 PM

Sure, it's a document server solution, where clients can add documents to their basket on a website and then request them for download.  At which point my bit (well, more yours!) kicks in, zips and streams the docs back to the client.

On 30 Apr 2010 17:39, "Cheeso" <notifications@codeplex.com> wrote:

From: Cheeso

Glad I could help.

Can you tell me, what's the application?  Can you share any details?



Read the full discussion online.

To add a post to this discussion, reply to this email (DotNetZip...

May 7, 2010 at 10:24 AM

Can this approach be used in ASP.Net application with context.Response.OutputStream. 

Coordinator
May 7, 2010 at 2:54 PM
Edited May 7, 2010 at 3:17 PM

Guja, no.

you don't need this approach with ASPNET's Response.OutputStream, because that stream is writable.

You can just save directly to that stream. In an ASPX page it would look like this:

Response.Clear();
Response.ClearHeaders();
Response.BufferOutput= false;
var c= System.Web.HttpContext.Current;
string archiveName= String.Format("archive-{0}.zip", DateTime.Now.ToString("yyyy-MMM-dd-HHmmss"));
Response.ContentType = "application/zip";
Response.AddHeader("content-disposition", "inline; filename=\"" + archiveName + "\"");

using (ZipFile zip = new ZipFile())
{
    zip.Password = "whatever";
    // filesToInclude is a string[] or List<String>
    zip.AddFiles(filesToInclude, "files");
    zip.Save(Response.OutputStream);
}
c.ApplicationInstance.CompleteRequest();

The complete source for an .ASHX handler looks like this:

<%@ WebHandler Language="C#" Class="Handler" %>

using System;
using System.IO;
using System.Web;
using Ionic.Zip;

public class Handler : IHttpHandler
{
    void IHttpHandler.ProcessRequest(HttpContext ctx)
    {
        string fileToInclude = ctx.Request.QueryString["file"];
        if (fileToInclude!=null)
        {
            string filename = ctx.Server.MapPath(Path.Combine("fodder", fileToInclude));
            if ((new FileInfo(filename)).Exists)
            {
                ctx.Response.BufferOutput = false;
                var archiveName = String.Format("archive-{0}.zip", DateTime.Now.ToString("yyyy-MMM-dd-HHmmss"));
                ctx.Response.ContentType = "application/zip";
                ctx.Response.AddHeader("content-disposition", "inline; filename=\"" + archiveName + "\"");
                using (var zip = new ZipFile())
                {
                    zip.AddFile(filename, "file");
                    zip.Save(ctx.Response.OutputStream);
                }
            }
        }
        ctx.ApplicationInstance.CompleteRequest();
    }


    Boolean IHttpHandler.IsReusable
    {
        get { return false; }
    }
}

 

May 7, 2010 at 3:14 PM
Edited May 7, 2010 at 3:26 PM

Ok I tried something like this:

 

SessionUser user = (SessionUser)context.Session[SessionNames.SessionUser];
                    context.Response.Clear();
                    context.Response.AddHeader("Content-Disposition", String.Format("attachment; filename={0}.zip", ldocs.Count == 1 ? ldocs[0].Label : "Dokumenti"));
                    context.Response.AddHeader("Content-Length", Convert.ToString(ldocs.Sum(s=>
                                                                            s.GetPDocs(false).ToList<PDoc>().Sum(p=>p.DataLength))));
                    context.Response.ContentType = "application/octet-stream";

                    context.Response.Flush();
                    GetFileZip(ldocs, context.Response.OutputStream, user, database);


/// <summary>
        /// Creates a zip file into the given stream in our case Response
        /// </summary>
        /// <param name="ldocs">List of LDocs</param>
        /// <param name="response">Stream response</param>
        /// <param name="user">Session user</param>
        /// <param name="database">database</param>
        public void GetFileZip(List<LDoc> ldocs, Stream response,SessionUser user, Database database)
        {
            using (ZipFile zos = new ZipFile())
            {
                List<string> folderNames = new List<string>();
                foreach (LDoc ldoc in ldocs)
                {
                        //Check if the folder already exists and create a folder number if it does => Test, Test(1), Test(2)
                        string folderName;
                        int count = folderNames.Count(s => s.Equals(ldoc.Label));
                        folderName = count > 0 ? String.Format("{0}({1})", ldoc.Label, count) : ldoc.Label;
                        folderNames.Add(ldoc.Label);
                        
                        //Put each phisical document into a folder for it's logical document
                        foreach (PDoc pdoc in ldoc.GetPDocs(true))
                        {
                            byte[] data;
                            UTF8Encoding encoding;
                            if (pdoc.Binary)
                            {
                                data = pdoc.DataBinary;
                            }
                            else
                            {
                                //get bytes from string
                                encoding = new System.Text.UTF8Encoding();
                                data = encoding.GetBytes(pdoc.DataText);
                            }
                            zos.AddEntry(String.Format("{2}/{0}.{1}", pdoc.Filename, pdoc.Extension, ldocs.Count == 1 ? "" : folderName), data);
                            zos.Save(response);
                        }
                    }
            }
        }

 

I also tried using the ZipOutputStream and both of them work, the only problem I'm having is that the file is first streamed to my computer and only then the dialog to download or save the file appears. It's a problem when I try to download large files, because I have to wait without knowing that the file is downloading. It eventualy downloads. But I need to resolve thi issue. Please help.

Coordinator
May 7, 2010 at 3:20 PM

Did you use

Response.BufferOutput= false;

... in your code? I didn't see it. That's the thing that allows the response to be transferred in chunks. I think that may help. Try and let me know.

Coordinator
May 7, 2010 at 3:21 PM
Edited May 7, 2010 at 3:26 PM

Ah, also ---

you don't want to save inside the loop where you are adding entries.  You want to call zip.Save() ONCE.  After all entries have been added.

Like this:

using (ZipFile zos = new ZipFile())
{
    List<string> folderNames = new List<string>();
    foreach (LDoc ldoc in ldocs)
    {
        ...
        foreach (PDoc pdoc in ldoc.GetPDocs(true))
        {
            ...
            zos.AddEntry(String.Format("{2}/{0}.{1}", pdoc.Filename, pdoc.Extension, ldocs.Count == 1 ? "" : folderName), data);
            // NO.  zos.Save(response);
        }
    }
    zos.Save(response);  // YES. 
}
May 7, 2010 at 3:34 PM

It's fully functional now. Thanks for the quick and efficient answer :D