2

I am currently implementing a file download in a web application. I was struggling a little with the pop up blocker, the download needs to start immediately after the user interaction, so there cannot be a server round trip. Now I noticed, that Goolge Drive for example allows you to download folders. When you do that, their server first creates a compressed zip file, which takes some time.When the server finished, the download starts immediately without another user interaction.

Now I am wondering how this can be done?

Robert P
  • 9,398
  • 10
  • 58
  • 100
  • A trivial solution is that the client simply requests the zip, and the server creates it, then delivers it. This can happen in the background, so 1) fetch() runs 2) client shows "packing..." 3) server packs 4) server delivers 5) download starts. This can cause a timeout though, so you're basically asking how to send a message from the server to the client. This can be done using sockets (WebRTC, socket.io) –  Apr 02 '21 at 09:13
  • As much as I have seen, the only way to download a file is to use a `anchor`. So in that case the client mus request the zip using an `anchor` but then it would immediately appear in the download section of the browser. In the case of google chrome, it first shows a notification about the packing and after that, the download appears in the downlload section. – Robert P Apr 02 '21 at 09:34
  • Like I said, you probably need a sockets based solution. 1) client requests zip via fetch() 2) google drives prepares zip, when finished, send socketmessage with temporary URL 3) client starts download using URL –  Apr 02 '21 at 09:51
  • I often had the problem, that the download has been blocked, if it happened after a server roundtrip. But the solution of @ManhNguyen seems to work, even in Firefox. Not sure why though – Robert P Apr 02 '21 at 10:07
  • 1
    ManH's answer below is basically the five steps I outlined in my first comment. –  Apr 02 '21 at 10:10

2 Answers2

2

I wrote a function to download a file via url. In your case, you must use ajax request to make a zip file on server then give back url and run below function to download:

function download(url, filename){
    fetch(url)
    .then(resp => resp.blob())
    .then(blob => {
      const url = window.URL.createObjectURL(blob);
      const a = document.createElement('a');
      a.style.display = 'none';
      a.href = url;
      a.download = filename;
      document.body.appendChild(a);
      a.click();
      window.URL.revokeObjectURL(url);
      console.log('your file has downloaded!'); 
    })
    .catch(() => console.log('Download failed!'));
}

Test codepen here: download file via url

Manh Nguyen
  • 563
  • 3
  • 7
  • Thank you for the answer. I often had the issue, that if the download starts after a server roundtrip, the browers (especially Firefox) block it, however this solution seems to work. – Robert P Apr 02 '21 at 10:03
1

I noticed, that Google Drive for example allows you to download folders. When you do that, their server first creates a compressed zip file, which takes some time.

Alternatively, you could request the contents of the directory as json, then loop over the json and download each file as a blob and create a zip file, the only blocking then would be the request to the json, then you could show a status of each file downloaded etc.

Libs to do that:

Snippet example, using vue, s3 etc

async download(bucket) {

  this.$snackbar.show({
    type: 'bg-success text-white',
    message: 'Building Zip, please wait...'
  })

  //..eek fetch all items in bucket, plop into a zip, then trigger download
  // - there is some limits on final filesize, will work around it by disabling download :)

  // resolve objects
  const objects = async(bucket) => new Promise(async(resolve, reject) => {
    let objectStream = await this.state.host.client.listObjectsV2(bucket, '', true)
    let objects = []
    //
    objectStream.on('data', (obj) => {
      if (obj && (obj.name || obj.prefix)) {
        this.$snackbar.show({
          type: 'bg-success text-white',
          message: 'Fetching: ' + obj.name
        })
        objects.push(obj)
      }
    })
    objectStream.on('end', () => resolve(objects))
    objectStream.on('error', (e) => reject(e))
  })

  // get an objects data
  const getObject = (bucket, name) => new Promise((resolve, reject) => {
    this.state.host.client.getObject(bucket, name, (err, dataStream) => {
      let chunks = []
      dataStream.on('data', chunk => {
        this.$snackbar.show({
          type: 'bg-success text-white',
          message: 'Downloading: ' + name
        })
        chunks.push(chunk)
      })
      dataStream.on('end', () => resolve(Buffer.concat(chunks || [])))
    })
  })

  // fetch objects info a zip file
  const makeZip = (bucket, objects) => new Promise(async(resolve, reject) => {
    let zip = new JSZip()

    for (let i in objects) {
      this.$snackbar.show({
        type: 'bg-success text-white',
        message: 'Zipping: ' + objects[i].name
      })
      zip.file(objects[i].name, await getObject(bucket, objects[i].name));
    }

    zip.generateAsync({
      type: "blob"
    }).then(content => {
      this.$snackbar.show({
        type: 'bg-success text-white',
        message: 'Zip Created'
      })
      resolve(content)
    })
  })

  // using FileSaver, download file
  const downloadZip = (content) => {
    this.$snackbar.show({
      type: 'bg-success text-white',
      message: `Downloading: ${bucket.name}.zip`
    })
    FileSaver.saveAs(content, `${bucket.name}.zip`)
  }

  try {
    downloadZip(await makeZip(bucket.name, await objects(bucket.name)))
  } catch (e) {
    this.$snackbar.show({
      type: 'bg-danger text-white',
      message: e.message
    })
    console.error(e)
  }
},

If you want an ugly way to download a directory, fetch the json list then place a bunch of document.createElement('a') on the dom with a.setAttribute("target", "_blank"), but you will get a bunch of "Save As" dialogues open.

How to download multiple images?

Lawrence Cherone
  • 46,049
  • 7
  • 62
  • 106