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: 23601

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

Pam 30/08/2017 11:30
In response to Sending Email in Razor Pages
Mike, RazorPages sound like a nice choice for somebody still working in ASP classic who wants to to...

Robby Robson 15/08/2017 00:43
In response to Routing in Razor Pages
Mike: great stuff. Now that .Core Standard 2.0 is formally out, how soon will you rewrite your book...

Satyabrata Mohapatra 28/07/2017 08:59
In response to Sending Email in Razor Pages
Bit off topic, but congratulation sir for your MVP award. You deserve it !!!...

Satyabrata Mohapatra 23/07/2017 16:43
In response to Razor Pages - The Elevator Pitch
@Dale Severin You can continue to build apps using asp.net web pages....

Satyabrata Mohapatra 23/07/2017 16:40
In response to Sending Email in Razor Pages
Thanks for sharing...learned a lot...

Gfw 22/07/2017 11:53
In response to Sending Email in Razor Pages
Question... Does System.Net.Mail support SSL?...

Dale Severin 20/07/2017 03:38
In response to Razor Pages - The Elevator Pitch
I work with razor web pages extensively. I appreciate the rapid development it permits me to I am as...

Obinna Okafor 14/07/2017 01:19
In response to Routing in Razor Pages
Thank you, Mike. Good post....

Satyabrata Mohapatra 11/07/2017 16:02
In response to Routing in Razor Pages
Very powerful routing system!!...

Cyrus 05/07/2017 03:41
In response to Razor Pages - Getting Started With The Preview
How can I trim packages and services as much as possible to use just razor pages? I don’t want to to...