How in JS to download more than 10 files in browser including Firefox
Asked Answered
L

3

6

I try the following code to download multiple files at once:

var urls = [...];

for(var i = 0; i < urls.length; i++) {

    var tempLink = document.createElement('a');
    tempLink.setAttribute('href', urls[i]);
    tempLink.setAttribute('download', urls[i].split('/')[urls[i].split('/').length*1-1*1]);
    tempLink.click();
}

And there are two problems:

1. Chrome and Opera downloads only 10 files at once

2. In Firefox the code doesn't work at all

How to resolve both problems?

ps. I know how to zip all files with server side, but I'm interested in a solution in JS for static websites without server side

Lxx answered 21/5, 2019 at 19:4 Comment(11)
i've downloaded 1000s of files like this in chrome, but i had to stagger the downloads using a setTimeout and semi-generous timings to make sure it didn't back up (there's no callback to make it really async). feed the timeout something like i*333 for the 2nd argument.Stratocumulus
@Stratocumulus thanks! What about Firefox?Lxx
If you have access to the server, it might be a better idea to send the server a list of files to download, have the server zip them together, then it sends you the zipped file. Even if it just sends back the file location for you to download "manually" in your script, that's better than trying to manage time delays between multiple downloads. Ex. in C#: c-sharpcorner.com/article/…, of course, use the language you need.Mathur
@Mathur yes, I know how to zip, but I would like to find a solution for static websites without server sideLxx
you might have to attach the A tag to the document. like document.body.appendChild(a) before you click() it. you might also try a synthetic click event instead of just a.click(). does a simple example (no loop) work in FF?Stratocumulus
Thanks, just found the same solution by the link https://mcmap.net/q/568119/-download-attribute-not-working-in-firefoxLxx
@stckvrw, that would have been good info to put into the question. And now I don't have any good ideas.Mathur
@Stratocumulus yes, simple example for one link works in FF, but in loop doesn't (only the last file in array is downloaded)Lxx
sounds like a timeout would fix that then. again, too bad there's no callback.Stratocumulus
Check #56245540Bibber
Zipping is not necessarily a server side thing. Where are the downloaded files coming from? Browser's memory like in the linked question? Same-origin (Then you could pre-fetch them using AJAX and pack them in memory if server-side really is not an option)? Other domains? (In that case you'll still be unable to even donwload them from Firefox).Anthropography
A
5

Instead of downloading multiple files, the best is probably to pack them all in a single file. You can for instance generate a zip file from all these files.

Since you stated that server-side solution was not an option for you, but that all your files are hosted on your domain, then I can propose you to first fetch through AJAX all your files (since these are images already loaded on the page, they won't even need to be actually downloaded again, but only gathered from cache) and then build your zip file on the front-end.

// all images are from wikimedia
const urls = ['/3/3b/Hulda_Klagers_house_and_lawn.jpg/320px-Hulda_Klagers_house_and_lawn.jpg', '/1/15/P%C3%A8re-Lachaise_-_Division_79_-_Floriot_02.jpg/320px-P%C3%A8re-Lachaise_-_Division_79_-_Floriot_02.jpg', '/a/a6/V37-20180910-055_%2845088120261%29.jpg/320px-V37-20180910-055_%2845088120261%29.jpg', '/2/2b/MormantulLuiAmzaPellea_%284%29.JPG/360px-MormantulLuiAmzaPellea_%284%29.JPG', '/f/f8/Launch_of_LAWRENCE_LCCN2014710971.tif/lossy-page1-174px-Launch_of_LAWRENCE_LCCN2014710971.tif.jpg']
.map((url) => 'https://upload.wikimedia.org/wikipedia/commons/thumb' + url);

fetchBlobs(urls)
  .then(pack)
  .then((zipFile) => dl.href = URL.createObjectURL(zipFile));

function fetchBlobs(urls) {
  return Promise.all(
    urls.map((url) =>
      fetch(url)
      .then((resp) => resp.blob())
      .then((blob) => {
        // store the file name
        blob.name = url.slice(url.lastIndexOf('/') + 1)
        return blob;
      })
    )
  );
}
function pack(blobs) {
  const zip = new JSZip();
  const folder = zip.folder('my_images');
  blobs.forEach((blob) => folder.file(blob.name, blob));
  return zip.generateAsync({type : "blob"});
}
<!-- using JSZip library https://stuk.github.io/jszip/ -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.2.0/jszip.min.js"></script>
<a id="dl" download="images.zip">download</a>
Anthropography answered 22/5, 2019 at 14:19 Comment(2)
Thanks! One more question: how to pack files directly to zip without intermediate subfolder like .folder('my_images') in your answer?Lxx
@Lxx oh just do zip.file(blob.name, blob) directly.Anthropography
L
1

Ok, this is my working solution of downloading files at once with setTimeout()

var fileUrls = [...];    

var tempLink = document.createElement('a');
document.body.appendChild(tempLink);

downloadMultipleFiles(fileUrls);

function downloadMultipleFiles(fileUrls) {
    setTimeout(function() {

        var fileIndex = fileUrls.length*1-1*1;
        var fileUrl = fileUrls[fileIndex];
        tempLink.setAttribute('href', fileUrl);
        tempLink.setAttribute('download', fileUrl.split('/')[fileUrl.split('/').length*1-1*1]);
        tempLink.click();

        if(fileIndex > -1) {
            fileUrls.splice(fileIndex, 1);
        }

        if(fileUrls.length > 0) {
            downloadMultipleFiles(fileUrls);
        } else {
            document.body.removeChild(tempLink);
        }

    }, 200); // if less than 200, not all files are downloaded in Firefox
}

And this my working solution of zipping files without server side using jszip mentioned by @Kaiido:

// prepare blobs with data of files when load a page
var fileUrls = [...];
var blobs = [];
for(i = 0; i < fileUrls.length; i++) {

    var xhttp = new XMLHttpRequest();
    xhttp.onreadystatechange = function() {
        if(this.readyState == 4 && this.status == 200) {

            var filename = this.responseURL.split('/')[this.responseURL.split('/').length*1-1*1];
            var mimeType = this.getResponseHeader('Content-Type');
            blobs.push([filename, new Blob([this.response], {type: mimeType})]);
        }
    };
    xhttp.open('GET', fileUrls[i], true);
    xhttp.responseType = "arraybuffer";
    xhttp.send();
}

document.getElementsByClassName('.download_all_link')[0].addEventListener('click', function(){

    if(this.id != '') {
        var zip = new JSZip();
        var folder = zip.folder('subfolder');

        for(i = 0; i < blobs.length; i++) {
            folder.file(blobs[i][0], blobs[i][1]);
        }

        zip.generateAsync({type : 'blob'})
            .then(zip_blob => {
                download_all.href = URL.createObjectURL(zip_blob);
            });

        // as we don't know when zip is ready, 
        // we check link href every 500 ms by using recursive function with setTimeout()
        checkHref(this);
    }
});
}

function checkHref(thisLink) {
    setTimeout(function() {

        // when zip is ready we click by the link again to download zip
        if(~thisLink.href.indexOf('blob:')) {
            thisLink.download = 'myfiles.zip';
            thisLink.id = ''; // to prevent zipping again
            thisLink.click(); // to download zip
        } else {
            checkHref(thisLink);
        }
    }, 500);
}
Lxx answered 21/5, 2019 at 20:3 Comment(1)
XMLHttpRequest also can fetch Blob directly, set the responseType to "blob" intead of "arraybuffer", this way no need to generate a new Blob yourself and also, it avoids using too much memory for nothing (even though it would get garbage collected later on)Anthropography
Z
0

chrome

You can use setTimeout to download multiple files. (over than 10)

<button onclick="downloadAll()">DownloadAll</button>
<script>
  function downloadBatch(aElems, batchSize, delay) {
    let index = 0
    const nextBatch = () => {
      const batch = aElems.slice(index, index + batchSize)
      batch.forEach(aWithDownload => {
        aWithDownload.click()
      })
      index += batchSize
      if (index < aElems.length) {
        setTimeout(nextBatch, delay)
      }
    }
    nextBatch()
  }

  function downloadAll() {
    const maxDownloadsPerBatch = 8
    const delayBetweenBatches = 1000
    const aArray = [...document.querySelectorAll('a[download]')]
    downloadBatch(aArray, maxDownloadsPerBatch, delayBetweenBatches)
  }
</script>

<!-- 👇 Create Test data-->
<script>
  const svgTemplate = `
<svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<text x="50" y="50">{{.Letter}}</text>
</svg>`;

  // A-Z
  const letters = Array.from({length: 26}, (_, i) => String.fromCharCode(65 + i))

  letters.forEach(letter => {
    const svgContent = svgTemplate.replace('{{.Letter}}', letter)
    const svgBase64 = btoa(svgContent)
    const dataUrl = `data:image/svg+xml;base64,${svgBase64}`
    const a = document.createElement('a')
    a.href = dataUrl
    a.download = `${letter}.svg`
    a.textContent = letter
    document.body.append(a)
  });
</script>

(You cannot run it directly on StackOverflow. Save the file to your local machine and run it again.)

Zebu answered 15/10 at 11:29 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.