Zip whole folder on the fly in ASP.NET MVC application

Generate folder archive and send zip file in a response in ASP.NET MVC

  • Share

Generating Zip archives has always been a great thing if you need to generate different content for the request depending on some parameters. In case you need to return multiple files this is one approach to go with.

Note

Generating Zip archive on the fly takes certain amount of processor power and resources so be careful when generating Zip files on the fly. If your web page is exposed publicly and not behind any type of authentication, consider adding CAPTCHA or Honeypot protection to your website to avoid possible crashes.

Basically generating Zip file is pretty easy, but there are several tricks that you need to know especially when you are doing it on the fly and sending content in http response. The easier way is to use available NuGet package SharpZipLib by adding it to your web project from NuGet console or package manager.

If you prefer to see how things work you can fork the project source from GitHub.

Now to start. After you added nuget reference to your project, you will need a separate action in a controller to return a zip stream in a response.

using ICSharpCode.SharpZipLib.Zip;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Web;
using System.Web.Mvc;

namespace ZipWebDemo.Controllers
{
    public class HomeController : Controller
    {
        // GET: Home
        public ActionResult Index()
        {
            return View();
        }

        public FileResult DownloadFiles(string id)
        {
            var context = System.Web.HttpContext.Current;
            var folderPath = context.Server.MapPath(string.Format("~/{0}", id));
            var baseOutputStream = new MemoryStream();
            ZipOutputStream zipOutput = new ZipOutputStream(baseOutputStream);
            zipOutput.IsStreamOwner = false;

            /* 
            * Higher compression level will cause higher usage of reources
            * If not necessary do not use highest level 9
            */

            zipOutput.SetLevel(3);
            byte[] buffer = new byte[4096];
            foreach (var file in Directory.GetFiles(folderPath))
            {
                ZipEntry entry = new ZipEntry(Path.GetFileName(file));
                entry.DateTime = DateTime.Now;
                zipOutput.PutNextEntry(entry);

                using (FileStream fs = System.IO.File.OpenRead(file))
                {
                    int sourceBytes = 0;
                    do
                    {
                        sourceBytes = fs.Read(buffer, 0, buffer.Length);
                        zipOutput.Write(buffer, 0, sourceBytes);
                    } while (sourceBytes > 0);
                }
            }

            zipOutput.Finish();
            zipOutput.Close();

            /* Set position to 0 so that cient start reading of the stream from the begining */
            baseOutputStream.Position = 0;

            /* Set custom headers to force browser to download the file instad of trying to open it */
            return new FileStreamResult(baseOutputStream, "application/x-zip-compressed")
            {
                FileDownloadName = "Archive.zip"
            };

        }
    }
}
    

Be careful that you set position for the owner stream. If you do not do that, reading from your stream will start from the end and you will basically return zero length stream.

After writing the content to output stream we need to tell the browser to show download dialog instead of trying to open the file this is done by adding headers to response.

 return new FileStreamResult(baseOutputStream, "application/x-zip-compressed")
            {
                FileDownloadName = "Archive.zip"
            };
    

However, this controller will not add subfolders and files in them, since you need to explicitly add the entries in output zip archive. So fr SharpZipLib does not support archiving the whole folder with subfolder structure.

For this case we will need a helper method that will iterate through folder structure and replicate it in output zip stream. To make things more clear I'll create another controller which does the same as the previous one pus it iterates through folder structure and replicates it to output stream.

To make it more organized I moved these methods to separate class

using ICSharpCode.SharpZipLib.Zip;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ZipWebDemo
{
    internal static class SharpZipLibHelper
    {
        public static void ZipFolder(string folderPath, ZipOutputStream zipStream)
        {
            string path = !folderPath.EndsWith("\\") ? string.Concat(folderPath, "\\") : folderPath;
            ZipFolder(path, path, zipStream);
        }

        private static void ZipFolder(string RootFolder, string CurrentFolder,
            ZipOutputStream zipStream)
        {
            string[] SubFolders = Directory.GetDirectories(CurrentFolder);
            foreach (string Folder in SubFolders)
            {
                ZipFolder(RootFolder, Folder, zipStream);
            }
            string relativePath = string.Concat(CurrentFolder.Substring(RootFolder.Length), "/");
            if (relativePath.Length > 1)
            {
                ZipEntry dirEntry;
                dirEntry = new ZipEntry(relativePath);
                dirEntry.DateTime = DateTime.Now;

            }
            foreach (string file in Directory.GetFiles(CurrentFolder))
            {
                AddFileToZip(zipStream, relativePath, file);
            }
        }

        private static void AddFileToZip(ZipOutputStream zStream, string relativePath, string file)
        {
            byte[] buffer = new byte[4096];
            string fileRelativePath = string.Concat((relativePath.Length > 1 ? relativePath : string.Empty), Path.GetFileName(file));

            ZipEntry entry = new ZipEntry(fileRelativePath);
            entry.DateTime = DateTime.Now;
            zStream.PutNextEntry(entry);

            using (FileStream fs = File.OpenRead(file))
            {
                int sourceBytes;
                do
                {
                    sourceBytes = fs.Read(buffer, 0, buffer.Length);
                    zStream.Write(buffer, 0, sourceBytes);
                } while (sourceBytes > 0);
            }

        }
    }
}

    

As you can see, most of the code was copy/paste from the first method, which is not the good case, but I used it only for demonstration purposes to describe the whole flow. Ideally you should use an optional parameter to reduce the code and make it more centralized and easier to maintain.

 

Now a new action in controller to zip whole folder

        public FileResult DownloadAllFiles(string id)
        {
            var context = System.Web.HttpContext.Current;
            var folderPath = context.Server.MapPath(string.Format("~/{0}", id));
            var baseOutputStream = new MemoryStream();
            ZipOutputStream zipOutput = new ZipOutputStream(baseOutputStream);
            zipOutput.IsStreamOwner = false;

            /* 
            * Higher compression level will cause higher usage of reources
            * If not necessary do not use highest level 9
            */

            zipOutput.SetLevel(3);
            byte[] buffer = new byte[4096];
            SharpZipLibHelper.ZipFolder(folderPath, zipOutput);

            zipOutput.Finish();
            zipOutput.Close();

            /* Set position to 0 so that cient start reading of the stream from the begining */
            baseOutputStream.Position = 0;

            /* Set custom headers to force browser to download the file instad of trying to open it */
            return new FileStreamResult(baseOutputStream, "application/x-zip-compressed")
            {
                FileDownloadName = "Archive.zip"
            };

        }
    

All the code snippets described here can be downloaded with the whole ASP.NET MVC project which is attached to this article.

References

  • Share

Disclaimer

Purpose of the code contained in snippets or available for download in this article is solely for learning and demo purposes. Author will not be held responsible for any failure or damages caused due to any other usage.

Comments for this article

comments powered by Disqus