Using DotNetZip with a Memory Stream

A colleague of mine recently asked about the possibility of adding a download button that would allow multiple files to be added to a single archive and downloaded. A quick google search lead me to the Nuget Package DotNetZip.

This is a great package and very intuitive to use. Within thirty minutes, I had it all wired into my application and it worked as expected. There were only a couple of minor details I had to work to get everything the way I wanted.

Here is the code:

public ExportDocumentResponse DownloadFilePathsAsZip(IEnumerable<string> filePaths)
{
	if (filePaths.Any())
	{
		using (IStream stream = IStreamFactory.Create(new IStreamParameters()
		{
			StreamType = StreamTypeEnum.MemoryStream
		}))
		{
			using (Ionic.Zip.ZipFile zipFile = new Ionic.Zip.ZipFile())
			{
				foreach (var filePath in filePaths)
				{
					zipFile.AddFile(filePath, "");
				}
				zipFile.Save(stream.GetUnderlyingSource());
			}

			return new ExportDocumentResponse()
			{
				DocumentStream = stream,
				ContentType = "application/zip"
			};
		}
	}
	
	return null;
}

This method takes an IEnumerable of strings that are the full disk file paths of the files I want to add to the zip file. In my application, I have a database that stores references to these filenames, so I have some logic outside of this method that creates the full file paths that I want.

From there, it’s a matter of creating a MemoryStream, instantiating the ZipFile, adding each file to the Zip, and saving out the memory stream to the Zip File.

The ExportDocumentResponse object is a custom object I have that allows me to pass this object back to my view (in this application, I’m using an MVP pattern where object is passed back to the View, which sets up the HttpResponse headers and copies the memory stream to the HttpResponse object (via the CopyTo method). That logic looks something like:

public void HandleResponse(IHttpResponse response, ExportDocumentResponse exportResponse, string fileNameWithExtension)
{
	response.Clear();
	response.ContentType = exportResponse.ContentType;
	response.AddHeader("content-disposition", String.Format("attachment;filename={0}", fileNameWithExtension));
	exportResponse.DocumentStream.Position = 0;
	exportResponse.DocumentStream.CopyTo(exportResponse.OutputStream);
	response.End(); //ThreadAbortException will be handled by presenter
}

Also note that I have some interfaces with wrappers around common objects for the implementations (IHttpResponse, IStream) so that I can unit test these methods. Replacing IStream with a System.IO.MemoryStream instance and IHttpResponse with whatever HttpResponse object is part of your web environment (depends on whether you are using WebForms, MVC, etc) should make this solution usable without my implementations. One final note is that my IStream has a method called GetUnderlyingSource which returns a System.IO.Stream object – if replacing my code with an actual Stream implementation, the call can be simplified to just zipFile.Save(stream)

My favorite part is that the code for creating the Zip file is incredibly minimal – there are a total of four lines here that are dedicated to the library and the rest is all logic to support it and return the result to the user in my application. The one “configurable” part here is the second parameter to the AddFile method. I have input an empty string here, because I want all of the files to be placed at the root of the Zip File. When I used the variant of this method with a single parameter, the Zip File would save a folder structure that looked something like the actual file structure from where the files were located. Here is the definition of the AddFile method that pops up from intellisense:

fileName (string): The name of the file to add. The name of the file may be a relative path or a fully-qualified path.

directoryPathInArchive (string): Specifies a directory path to use to override any path in the fileName. This path may, or may not, correspond to a real directory in the current filesystem. If the files within the zip are later extracted, this is the path used for the extracted file. Passing null (Nothing in VB) will use the path on the fileName, if any. Passing the empty string (“”) will insert the item at the root path within the archive.