Downloading multiple files in ASP.NET

4.75 (12 votes)

This short article examines the baffling issue that a lot of people seem to encounter when attempting to send multiple files to the client from an ASP.NET application, and provides the solution.

Every so often a question pops up on the forum asking why their multiple file download code only sends the first file. Typically, the code consists of a loop that iterates a collection of files and attempts to use Response.TransmitFile or a FileResult in MVC to dispatch each file to the client. The reason why this doesn't appear to work is because it is basically not possible. This isn't supported for security reasons. The potential exploit that enabling this scenario would open is known as a Drive-by Download whereby a malicious webmaster could send a whole load of malware to the client in addition to the requested file.

The workaround is to compress the files into one archive file, and download that instead. There are quite a few examples that show how to do this using a variety of third party zip libraries. That's mainly because there was nothing specifically designed for this in the .NET framework until some new stuff was introduced in System.IO.Compression in .NET 4.5, specifically the ZipFile class that enables you to create and work with .zip files.

The simple examples that follow illustrates the use of the ZipFile.CreateFromDirectory method in ASP.NET MVC and Web Forms. In both cases, the user is presented with a list of checkboxes representing a selection of files to choose from. Submitting the form will result in just those files being packaged up into one zip file and downloaded.

ASP.NET MVC

The list of files is passed to the view via ViewBag:

public ActionResult Index()
{
    ViewBag.Files = Directory.EnumerateFiles(Server.MapPath("~/pdfs"));
    return View();
}

The files are listed within a form with a set of checkboxes:

<h2>Select downloads</h2>
@using(Html.BeginForm("Download", "Home"))
{
    foreach(string file in ViewBag.Files)
    {
        <input type="checkbox" name="files" value="@file" /> @:&nbsp;  
             @Path.GetFileNameWithoutExtension(file) <br />
    }
    <div>
        <button class="btn btn-default">Submit</button>
    </div>
}

The form posts to an action called Download which consists of the following code:

[HttpPost]
public FileResult Download(List<string> files)
{
    var archive = Server.MapPath("~/archive.zip");
    var temp = Server.MapPath("~/temp");

    // clear any existing archive
    if (System.IO.File.Exists(archive))
    {
        System.IO.File.Delete(archive);
    }
    // empty the temp folder
    Directory.EnumerateFiles(temp).ToList().ForEach(f => System.IO.File.Delete(f));

    // copy the selected files to the temp folder
    files.ForEach(f => System.IO.File.Copy(f, Path.Combine(temp, Path.GetFileName(f))));

    // create a new archive
    ZipFile.CreateFromDirectory(temp, archive);

    return File(archive, "application/zip", "archive.zip");
}

The code above relies on two additional using directives in the controller class:

using System.IO;
using System.IO.Compression;

The user selection is captured in the files parameter. The code checks to see if a file called archive.zip exists from previous operations and it it does, it is deleted. Then a folder called temp is cleared of any existing files. Next,the selected files are copied from their source directory to the temp folder. The ZipFile.CreateFromDirectory method generates a zip file from the temp directory contents and saves it as archive.zip. Finally, it is written to the Response.

Web Forms

This solution features one page called Download.aspx. The aspx file contains markup for a CheckBoxList and a Button control:

<asp:Content ID="Content1" ContentPlaceHolderID="MainContent" runat="server">
    <asp:CheckBoxList ID="CheckBoxList1" runat="server" OnDataBound="CheckBoxList1_DataBound" />
    <asp:Button ID="Button1" runat="server" Text="Submit" />
</asp:Content>

The Page_Load method in the code behind file is very similar to the Action method in the MVC example. If the page hasn't been posted back, the list of files is obtained and bound to the CheckBoxList.

public partial class Download : Page
{
        
    protected void Page_Load(object sender, EventArgs e)
    {
        if (!IsPostBack)
        {
            CheckBoxList1.DataSource = Directory.GetFiles(Server.MapPath("~/PDFs"));
            CheckBoxList1.DataBind();
        }
        else
        {
            var files = CheckBoxList1.Items.Cast<ListItem>()
                                                    .Where(li => li.Selected).Select(li => li.Value)
                                                    .ToList();
            var archive = Server.MapPath("~/archive.zip");
            var temp = Server.MapPath("~/temp");

            // clear any existing archive
            if (System.IO.File.Exists(archive))
            {
                System.IO.File.Delete(archive);
            }
            // empty the temp folder
            Directory.EnumerateFiles(temp).ToList().ForEach(f => System.IO.File.Delete(f));

            // copy the selected files to the temp folder
            files.ForEach(f => System.IO.File.Copy(f, Path.Combine(temp, Path.GetFileName(f))));

            // create a new archive
            ZipFile.CreateFromDirectory(temp, archive);
            Response.ContentType = "application/zip";
            Response.AddHeader("Content-Disposition", "attachment; filename=archive.zip");
            Response.TransmitFile(archive);
        }
    }

    protected void CheckBoxList1_DataBound(object sender, EventArgs e)
    {
        foreach (ListItem item in CheckBoxList1.Items)
        {
            item.Text = Path.GetFileName(item.Text);
        }
    }
}

I have used the DataBound event of the CheckBoxList to format the Text property of each item to remove the full path to the file. If the form has been posted back, the selected items are stored in a List<string> and then the very same code is used to clear previous files, copy the selected ones to the temp folder and then generate the zip archive. Finally, the content type and disposition of the Response are set appropriately and the archive is sent to the client.

Summary

This short article discussed the reason why multiple file downloads are not enabled, and presented a workaround using only .NET framework code to provide a solution in both ASP.NET MVC and Web Forms scenarios.

You might also like...

Date Posted:
Last Updated:
Posted by:
Total Views to date: 21459

3 Comments

- Gavan McGregor

Hi,

(long time reader, first time poster!)

Although it's a deliberately simple example, It's probably worth highlighting that the implementation has several points of possible concurrency failures / race conditions in that it uses the same temp folder, same zip file, and same download file name for every request.

If multiple users select different sets of files and submit at roughly the same time, you might end up with inconsistent sets of zipped files being created and returned - potentially with the wrong files going to the wrong recipient.

A good way to address this would be to create a new, separate directory underneath ~/temp - using a unique identifier (guid, requestId, etc). This would keep the sets of files separate.

The downside is that a periodic delete of old directories/files would be needed (if the directories used names prefixed with the date in ISO format, this would be relatively straight forward).

Kind Regards,

Gavan.

- Mike

Hi Gavan,

Thanks for your comments which are welcome and save me having to make the points you covered.

It's also worth noting that too much file activity in the root folder is likely to trigger a recompile, which would result in loss of session variables, slow performance etc. For that reason, this kind of copy and delete activity should take place outside of the application root.

- satyabrata

Excellent article!!

Recent Comments

Cyrus 16/05/2017 19:55
In response to Razor Pages - Getting Started With The Preview
There is something wrong related to microsoft.dotnetcore.mvc.taghelpers! if you remove it from page...

Cyrus 16/05/2017 10:18
In response to Razor Pages - Getting Started With The Preview
well done mike, it was very useful, I really appreciate that....

Satyabrata Mohapatra 16/05/2017 07:21
In response to Razor Pages - Getting Started With The Preview
Finally!!!! web pages in asp.net core!!! Super excited !!!! Thank u sir for sharing.....Awaiting on...

Daniele 14/03/2017 10:24
In response to Working With Zip Files In ASP.NET MVC
is it possible give to the user a progress bar of the zipping process? Thanks in advance. ...

Suraj 13/03/2017 22:20
In response to Working With Zip Files In ASP.NET MVC
Very nice article. Thanks....

Satyabrata Mohapatra 19/02/2017 03:01
In response to Free SSL Certificates On IIS With LetsEncrypt
Thanks for sharing. Learned a lot !!...

Gfw 03/02/2017 09:48
In response to Free SSL Certificates On IIS With LetsEncrypt
I have used WinSimple for about the last 9 months - works great. One thing that you want to make of...

Ted Driver 02/02/2017 13:24
In response to Free SSL Certificates On IIS With LetsEncrypt
This looks great is you have command line access to your web server - what about those of us on Is...

Aghil 16/11/2016 18:16
In response to Server.MapPath Equivalent in ASP.NET Core
Hi, Thanks, it was really good. However, how can we access the in the Classes? Is there any...

Carl T. 06/11/2016 05:43
In response to Server.MapPath Equivalent in ASP.NET Core
Very succinct and easy to follow. Worked perfectly the first time for me. Thanks!!...