Download multiple files with a single action

HttpWeb ApplicationsDownload

Http Problem Overview


I am not sure if this is possible using standard web technologies.

I want the user to be able to download multiple files in a single action. That is click check boxes next to the files, and then get all the files that were checked.

Is it possible - if so what basic strategy do you recommend. I know I can use comets technology to create server side events that trigger an HttpResponse but I am hoping there is a simpler way.

Http Solutions


Solution 1 - Http

var links = [
  'https://s3.amazonaws.com/Minecraft.Download/launcher/Minecraft.exe',
  'https://s3.amazonaws.com/Minecraft.Download/launcher/Minecraft.dmg',
  'https://s3.amazonaws.com/Minecraft.Download/launcher/Minecraft.jar'
];

function downloadAll(urls) {
  var link = document.createElement('a');

  link.setAttribute('download', null);
  link.style.display = 'none';

  document.body.appendChild(link);

  for (var i = 0; i < urls.length; i++) {
    link.setAttribute('href', urls[i]);
    link.click();
  }

  document.body.removeChild(link);
}

<button onclick="downloadAll(window.links)">Test me!</button>

Solution 2 - Http

HTTP does not support more than one file download at once.

There are two solutions:

  • Open x amount of windows to initiate the file downloads (this would be done with JavaScript)
  • preferred solution create a script to zip the files

Solution 3 - Http

You can create a temporary set of hidden iframes, initiate download by GET or POST inside of them, wait for downloads to start and remove iframes:

<!DOCTYPE HTML>
<html>
<body>
  <button id="download">Download</button> 

  <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4/jquery.min.js"></script>
  <script type="text/javascript">

     $('#download').click(function() {
       download('http://nogin.info/cv.doc','http://nogin.info/cv.doc');
     });

     var download = function() {
       for(var i=0; i<arguments.length; i++) {
         var iframe = $('<iframe style="visibility: collapse;"></iframe>');
         $('body').append(iframe);
         var content = iframe[0].contentDocument;
         var form = '<form action="' + arguments[i] + '" method="GET"></form>';
         content.write(form);
         $('form', content).submit();
         setTimeout((function(iframe) {
           return function() { 
             iframe.remove(); 
           }
         })(iframe), 2000);
       }
     }      
     
  </script>
</body>
</html>

Or, without jQuery:

 function download(...urls) {
    urls.forEach(url => {
      let iframe = document.createElement('iframe');
      iframe.style.visibility = 'collapse';
      document.body.append(iframe);

      iframe.contentDocument.write(
        `<form action="${url.replace(/\"/g, '"')}" method="GET"></form>`
      );
      iframe.contentDocument.forms[0].submit();

      setTimeout(() => iframe.remove(), 2000);
    });
  }

Solution 4 - Http

This solution works across browsers, and does not trigger warnings. Rather than creating an iframe, here we creates a link for each file. This prevents warning messages from popping up.

To handle the looping part, we use setTimeout, which is necessary for it to work in IE.

Update 2021: I am aware that the "run code snippet" no longer works, but that's due to cross site cookie issues. The code works fine if deployed on your own site.

/**
 * Download a list of files.
 * @author speedplane
 */
function download_files(files) {
  function download_next(i) {
    if (i >= files.length) {
      return;
    }
    var a = document.createElement('a');
    a.href = files[i].download;
    a.target = '_parent';
    // Use a.download if available, it prevents plugins from opening.
    if ('download' in a) {
      a.download = files[i].filename;
    }
    // Add a to the doc for click to work.
    (document.body || document.documentElement).appendChild(a);
    if (a.click) {
      a.click(); // The click method is supported by most browsers.
    } else {
      $(a).click(); // Backup using jquery
    }
    // Delete the temporary link.
    a.parentNode.removeChild(a);
    // Download the next file with a small timeout. The timeout is necessary
    // for IE, which will otherwise only download the first file.
    setTimeout(function() {
      download_next(i + 1);
    }, 500);
  }
  // Initiate the first download.
  download_next(0);
}

Solution 5 - Http

Easiest way would be to serve the multiple files bundled up into a ZIP file.

I suppose you could initiate multiple file downloads using a bunch of iframes or popups, but from a usability standpoint, a ZIP file is still better. Who wants to click through ten "Save As" dialogs that the browser will bring up?

Solution 6 - Http

A jQuery version of the iframe answers:

function download(files) {
    $.each(files, function(key, value) {
        $('<iframe></iframe>')
            .hide()
            .attr('src', value)
            .appendTo($('body'))
            .load(function() {
                var that = this;
                setTimeout(function() {
                    $(that).remove();
                }, 100);
            });
    });
}

Solution 7 - Http

The following script done this job gracefully.

var urls = [
'https://images.pexels.com/photos/432360/pexels-photo-432360.jpeg',
'https://images.pexels.com/photos/39899/rose-red-tea-rose-regatta-39899.jpeg'
];

function downloadAll(urls) {
  

  for (var i = 0; i < urls.length; i++) {
	forceDownload(urls[i], urls[i].substring(urls[i].lastIndexOf('/')+1,urls[i].length))
  }
}
function forceDownload(url, fileName){
    var xhr = new XMLHttpRequest();
    xhr.open("GET", url, true);
    xhr.responseType = "blob";
    xhr.onload = function(){
        var urlCreator = window.URL || window.webkitURL;
        var imageUrl = urlCreator.createObjectURL(this.response);
        var tag = document.createElement('a');
        tag.href = imageUrl;
        tag.download = fileName;
        document.body.appendChild(tag);
        tag.click();
        document.body.removeChild(tag);
    }
    xhr.send();
}

Solution 8 - Http

I agree that a zip file is a neater solution... But if you have to push multiple file, here's the solution I came up with. It works in IE 9 and up (possibly lower version too - I haven't tested it), Firefox, Safari and Chrome. Chrome will display a message to user to obtain his agreement to download multiple files the first time your site use it.

function deleteIframe (iframe) {
    iframe.remove(); 
}
function createIFrame (fileURL) {
    var iframe = $('<iframe style="display:none"></iframe>');
    iframe[0].src= fileURL;
    $('body').append(iframe);
    timeout(deleteIframe, 60000, iframe);             
 }
 // This function allows to pass parameters to the function in a timeout that are 
 // frozen and that works in IE9
 function timeout(func, time) {
      var args = [];
      if (arguments.length >2) {
           args = Array.prototype.slice.call(arguments, 2);
      }
      return setTimeout(function(){ return func.apply(null, args); }, time);
 }
 // IE will process only the first one if we put no delay
 var wait = (isIE ? 1000 : 0);
 for (var i = 0; i < files.length; i++) {  
      timeout(createIFrame, wait*i, files[i]);
 }

The only side effect of this technique, is that user will see a delay between submit and the download dialog showing. To minimize this effect, I suggest you use the technique describe here and on this question https://stackoverflow.com/questions/1106377/detect-when-browser-receives-file-download that consist of setting a cookie with your file to know it has started download. You will have to check for this cookie on client side and to send it on server side. Don't forget to set the proper path for your cookie or you might not see it. You will also have to adapt the solution for multiple file download.

Solution 9 - Http

Angular solution:

> HTML

    <!doctype html>
    <html ng-app='app'>
        <head>
            <title>
            </title>
            <link rel="stylesheet" href="http://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css">
            <link rel="stylesheet" href="style.css">
        </head>
        <body ng-cloack>        
            <div class="container" ng-controller='FirstCtrl'>           
              <table class="table table-bordered table-downloads">
                <thead>
                  <tr>
                    <th>Select</th>
                    <th>File name</th>
                    <th>Downloads</th>
                  </tr>
                </thead>
                <tbody>
                  <tr ng-repeat = 'tableData in tableDatas'>
                    <td>
                        <div class="checkbox">
                          <input type="checkbox" name="{{tableData.name}}" id="{{tableData.name}}" value="{{tableData.name}}" ng-model= 'tableData.checked' ng-change="selected()">
                        </div>
                    </td>
                    <td>{{tableData.fileName}}</td>
                    <td>
                        <a target="_self" id="download-{{tableData.name}}" ng-href="{{tableData.filePath}}" class="btn btn-success pull-right downloadable" download>download</a>
                    </td>
                  </tr>              
                </tbody>
              </table>
                <a class="btn btn-success pull-right" ng-click='downloadAll()'>download selected</a>
                
                <p>{{selectedone}}</p>
            </div>
            <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.0/jquery.min.js"></script>
            <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular.min.js"></script>
            <script src="script.js"></script>
        </body>
    </html>

> app.js

var app = angular.module('app', []);            
app.controller('FirstCtrl', ['$scope','$http', '$filter', function($scope, $http, $filter){
    
$scope.tableDatas = [
    {name: 'value1', fileName:'file1', filePath: 'data/file1.txt', selected: true},
    {name: 'value2', fileName:'file2', filePath: 'data/file2.txt', selected: true},
    {name: 'value3', fileName:'file3', filePath: 'data/file3.txt', selected: false},
    {name: 'value4', fileName:'file4', filePath: 'data/file4.txt', selected: true},
    {name: 'value5', fileName:'file5', filePath: 'data/file5.txt', selected: true},
    {name: 'value6', fileName:'file6', filePath: 'data/file6.txt', selected: false},
  ];  
$scope.application = [];   

$scope.selected = function() {
    $scope.application = $filter('filter')($scope.tableDatas, {
      checked: true
    });
}

$scope.downloadAll = function(){
    $scope.selectedone = [];     
    angular.forEach($scope.application,function(val){
       $scope.selectedone.push(val.name);
       $scope.id = val.name;        
       angular.element('#'+val.name).closest('tr').find('.downloadable')[0].click();
    });
}         
 
    
}]);

working example: https://plnkr.co/edit/XynXRS7c742JPfCA3IpE?p=preview

Solution 10 - Http

This works in all browsers (IE11, firefox, Edge, Chrome and Chrome Mobile) My documents are in multiple select elements. The browsers seem to have issues when you try to do it too fast... So I used a timeout.

//user clicks a download button to download all selected documents
$('#downloadDocumentsButton').click(function () {
    var interval = 1000;
    //select elements have class name of "document"
    $('.document').each(function (index, element) {
        var doc = $(element).val();
        if (doc) {
            setTimeout(function () {
                window.location = doc;
            }, interval * (index + 1));
        }
    });
});

This is a solution that uses promises:

 function downloadDocs(docs) {
        docs[0].then(function (result) {
            if (result.web) {
                window.open(result.doc);
            }
            else {
                window.location = result.doc;
            }
            if (docs.length > 1) {
                setTimeout(function () { return downloadDocs(docs.slice(1)); }, 2000);
            }
        });
    }

 $('#downloadDocumentsButton').click(function () {
        var files = [];
        $('.document').each(function (index, element) {
            var doc = $(element).val();
            var ext = doc.split('.')[doc.split('.').length - 1];
           
            if (doc && $.inArray(ext, docTypes) > -1) {
                files.unshift(Promise.resolve({ doc: doc, web: false }));
            }
            else if (doc && ($.inArray(ext, webTypes) > -1 || ext.includes('?'))) {
                files.push(Promise.resolve({ doc: doc, web: true }));
            }
        });

        downloadDocs(files);
    });

Solution 11 - Http

By far the easiest solution (at least in ubuntu/linux):

  • make a text file with the urls of the files to download (i.e. file.txt)
  • put the 'file.txt' in the directory where you want to download the files
  • open the terminal in the download directory from the previous lin
  • download the files with the command 'wget -i file.txt'

Works like a charm.

Solution 12 - Http

To solve this, I created a JS library to stream multiple files directly into a zip on the client-side. The main unique feature is that it has no size limits from memory (everything is streamed) nor zip format (it uses zip64 if the contents are more than 4GB).

Since it doesn't do compression, it is also very performant.

Find "downzip" it on npm or github!

Solution 13 - Http

To improve on @Dmitry Nogin's answer: this worked in my case.

However, it's not tested, since I am not sure how the file dialogue works on various OS/browser combinations. (Thus community wiki.)

<script>
$('#download').click(function () {
    download(['http://www.arcelormittal.com/ostrava/doc/cv.doc', 
              'http://www.arcelormittal.com/ostrava/doc/cv.doc']);
});

var download = function (ar) {
    var prevfun=function(){};
    ar.forEach(function(address) {  
        var pp=prevfun;
        var fun=function() {
                var iframe = $('<iframe style="visibility: collapse;"></iframe>');
                $('body').append(iframe);
                var content = iframe[0].contentDocument;
                var form = '<form action="' + address + '" method="POST"></form>';
                content.write(form);
                $(form).submit();
                setTimeout(function() {    
                    $(document).one('mousemove', function() { //<--slightly hacky!
                        iframe.remove();
                        pp();
                    });
                },2000);
        }
        prevfun=fun; 
      });
      prevfun();   
}
</script>

Solution 14 - Http

I am looking for a solution to do this, but unzipping the files in javascript was not as clean as I liked. I decided to encapsulate the files into a single SVG file.

If you have the files stored on the server (I don't), you can simply set the href in the SVG.

In my case, I'll convert the files to base64 and embed them in the SVG.

Edit: The SVG worked very well. If you are only going to download the files, ZIP might be better. If you are going to display the files, then SVG seems superior.

Solution 15 - Http

When using Ajax components it is possible to start multiple downloads. Therefore you have to use https://cwiki.apache.org/confluence/display/WICKET/AJAX+update+and+file+download+in+one+blow

Add an instance of AJAXDownload to your Page or whatever. Create an AjaxButton and override onSubmit. Create an AbstractAjaxTimerBehavior and start downloading.

		button = new AjaxButton("button2") {

		private static final long serialVersionUID = 1L;

		@Override
		protected void onSubmit(AjaxRequestTarget target, Form<?> form)
		{
			MultiSitePage.this.info(this);
			target.add(form);

			form.add(new AbstractAjaxTimerBehavior(Duration.milliseconds(1)) {

				@Override
				protected void onTimer(AjaxRequestTarget target) {
					download.initiate(target);
				}
				
			});		
		}

Happy downloading!

Solution 16 - Http

Below code 100% working.

Step 1: Paste below code in index.html file

<!DOCTYPE html>
<html ng-app="ang">

<head>
    <title>Angular Test</title>
    <meta charset="utf-8" />
</head>

<body>
    <div ng-controller="myController">
        <button ng-click="files()">Download All</button>
    </div>

    <script src="angular.min.js"></script>
    <script src="index.js"></script>
</body>

</html>

Step 2: Paste below code in index.js file

"use strict";

var x = angular.module('ang', []);

    x.controller('myController', function ($scope, $http) {
        var arr = [
            {file:"http://localhost/angularProject/w3logo.jpg", fileName: "imageone"},
            {file:"http://localhost/angularProject/cv.doc", fileName: "imagetwo"},
            {file:"http://localhost/angularProject/91.png", fileName: "imagethree"}
        ];

        $scope.files = function() {
            angular.forEach(arr, function(val, key) {
                $http.get(val.file)
                .then(function onSuccess(response) {
                    console.log('res', response);
                    var link = document.createElement('a');
                    link.setAttribute('download', val.fileName);
                    link.setAttribute('href', val.file);
                    link.style.display = 'none';
                    document.body.appendChild(link);
                    link.click(); 
                    document.body.removeChild(link);
                })
                .catch(function onError(error) {
                    console.log('error', error);
                })
            })
        };
    });

NOTE : Make sure that all three files which are going to download will be placed in same folder along with angularProject/index.html or angularProject/index.js files.

Solution 17 - Http

Getting list of url with ajax call and then use jquery plugin to download multiple files parallel.

  $.ajax({
        type: "POST",
        url: URL,
        contentType: "application/json; charset=utf-8",
        dataType: "json",
        data: data,
        async: true,
        cache: false,
        beforeSend: function () {
            blockUI("body");
        },
        complete: function () { unblockUI("body"); },
        success: function (data) {
           //here data --> contains list of urls with comma seperated
            var listUrls= data.DownloadFilePaths.split(',');
            listUrls.forEach(function (url) {
                $.fileDownload(url);
            });
            return false; 
        },
        error: function (result) {
            $('#mdlNoDataExist').modal('show');
        }

    });

Solution 18 - Http

Here is the way I do that. I open multiple ZIP but also other kind of data (I export projet in PDF and at same time many ZIPs with document).

I just copy past part of my code. The call from a button in a list:

$url_pdf = "pdf.php?id=7";
$url_zip1 = "zip.php?id=8";
$url_zip2 = "zip.php?id=9";
$btn_pdf = "<a href=\"javascript:;\" onClick=\"return open_multiple('','".$url_pdf.",".$url_zip1.",".$url_zip2."');\">\n";
$btn_pdf .= "<img src=\"../../../images/icones/pdf.png\" alt=\"Ver\">\n";
$btn_pdf .= "</a>\n"

So a basic call to a JS routine (Vanilla rules!). here is the JS routine:

 function open_multiple(base,url_publication)
 {
     // URL of pages to open are coma separated
     tab_url = url_publication.split(",");
     var nb = tab_url.length;
     // Loop against URL	
     for (var x = 0; x < nb; x++)
     {
	    window.open(tab_url[x]);
      }

     // Base is the dest of the caller page as
     // sometimes I need it to refresh
     if (base != "")
      {
     	window.location.href  = base;
      }
   }

The trick is to NOT give the direct link of the ZIP file but to send it to the browser. Like this:

  $type_mime = "application/zip, application/x-compressed-zip";
 $the_mime = "Content-type: ".$type_mime;
 $tdoc_size = filesize ($the_zip_path);
 $the_length = "Content-Length: " . $tdoc_size;
 $tdoc_nom = "Pesquisa.zip";
 $the_content_disposition = "Content-Disposition: attachment; filename=\"".$tdoc_nom."\"";

  header("Cache-Control: no-cache, must-revalidate"); // HTTP/1.1
  header("Expires: Sat, 26 Jul 1997 05:00:00 GMT");   // Date in the past
  header($the_mime);
  header($the_length);
  header($the_content_disposition);

  // Clear the cache or some "sh..." will be added
  ob_clean();
  flush();
  readfile($the_zip_path);
  exit();

Solution 19 - Http

           <p class="style1">



<a onclick="downloadAll(window.links)">Balance Sheet Year 2014-2015</a>

</p>

<script>
 var links = [
  'pdfs/IMG.pdf',
  'pdfs/IMG_0001.pdf',
 'pdfs/IMG_0002.pdf',
 'pdfs/IMG_0003.pdf',
'pdfs/IMG_0004.pdf',
'pdfs/IMG_0005.pdf',
 'pdfs/IMG_0006.pdf'
  
];

function downloadAll(urls) {
  var link = document.createElement('a');

  link.setAttribute('download','Balance Sheet Year 2014-2015');
  link.style.display = 'none';

  document.body.appendChild(link);

  for (var i = 0; i < urls.length; i++) {
    link.setAttribute('href', urls[i]);
    link.click();
  }

  document.body.removeChild(link);
}
</script>
	

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionAnkurView Question on Stackoverflow
Solution 1 - HttpMatÄ›j PokornýView Answer on Stackoverflow
Solution 2 - HttpJacob RelkinView Answer on Stackoverflow
Solution 3 - HttpDmitry NoginView Answer on Stackoverflow
Solution 4 - HttpspeedplaneView Answer on Stackoverflow
Solution 5 - HttpThiloView Answer on Stackoverflow
Solution 6 - HttpYirmiyahu FischerView Answer on Stackoverflow
Solution 7 - HttpShyam NarayanView Answer on Stackoverflow
Solution 8 - HttpMelanieView Answer on Stackoverflow
Solution 9 - HttpsureshView Answer on Stackoverflow
Solution 10 - HttpZach PainterView Answer on Stackoverflow
Solution 11 - HttpMartijnView Answer on Stackoverflow
Solution 12 - HttpDebboRView Answer on Stackoverflow
Solution 13 - HttpKarel BílekView Answer on Stackoverflow
Solution 14 - HttpVectorVortecView Answer on Stackoverflow
Solution 15 - HttpkleenxcoderView Answer on Stackoverflow
Solution 16 - Httpsolanki...View Answer on Stackoverflow
Solution 17 - HttpAliView Answer on Stackoverflow
Solution 18 - HttpPeterView Answer on Stackoverflow
Solution 19 - Httpuser5598894View Answer on Stackoverflow