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" /> @: @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.