如何使用Javascript下载,压缩和保存多个文件并取得进展?

时间:2013-06-24 11:43:08

标签: javascript google-chrome-extension zip download save-as

我正在创建一个Chrome扩展程序,需要从网站下载多个文件(图像和/或视频)。这些文件可能有很大的大小,所以我想向用户显示下载进度。经过一些研究,我发现目前可能的解决方案可能是:

  1. 使用XMLHttpRequests下载所有文件。
  2. 下载后,使用JavaScript库将所有文件压缩到一个存档中(例如JSZip.js,zip.js)。
  3. 提示用户使用SaveAs对话框保存zip。
  4. 我被困在第2段),我如何压缩下载的文件?

    要理解,这是一个代码示例:

    var fileURLs = ['http://www.test.com/img.jpg',...];
    var zip = new JSZip();
    
    var count = 0;
    for (var i = 0; i < fileURLs.length; i++){
        var xhr = new XMLHttpRequest();
        xhr.onprogress = calculateAndUpdateProgress;
        xhr.open('GET', fileURLs[i], true);
        xhr.responseType = "blob";
        xhr.onreadystatechange = function () {
            if (xhr.readyState == 4) {
                   var blob_url = URL.createObjectURL(response);
                // add downloaded file to zip:
                var fileName = fileURLs[count].substring(fileURLs[count].lastIndexOf('/')+1);
                zip.file(fileName, blob_url); // <- here's one problem
    
                count++;
                if (count == fileURLs.length){
                    // all download are completed, create the zip
                    var content = zip.generate();
    
                    // then trigger the download link:
                    var zipName = 'download.zip';
                    var a = document.createElement('a'); 
                    a.href = "data:application/zip;base64," + content;
                    a.download = zipName;
                    a.click();
                }
            }
        };
        xhr.send();
    }
    
    function calculateAndUpdateProgress(evt) {
        if (evt.lengthComputable) {
            // get download progress by performing some average 
            // calculations with evt.loaded, evt.total and the number
            // of file to download / already downloaded
            ...
            // then update the GUI elements (eg. page-action icon and popup if showed)
            ...
        }
    }
    

    上面的代码生成一个包含小损坏文件的可下载档案。 文件名同步也存在问题:blob对象不包含文件名,因此如果。 fileURLs[0]下载的时间比fileURLs[1]名称错误(倒置)需要更多时间..

    注意:我知道Chrome有一个下载API,但是它位于开发频道,所以不幸的是它现在不是解决方案,我想避免使用NPAPI来完成这么简单的任务。

3 个答案:

答案 0 :(得分:7)

我被提醒了这个问题..由于它还没有答案,我写了一个可能的解决方案,以防它对其他人有用:

  • 如上所述,第一个问题是将blob url传递给jszip(它不支持blob但它也不会抛出任何错误来通知它并且它成功生成了已损坏文件的存档):要纠正这个问题,只需传递一个base64字符串数据而不是它的blob对象url;
  • 第二个问题是文件名同步:这里最简单的解决方法是一次下载一个文件,而不是使用parallels xhr请求。

因此,修改后的上层代码可以是:

var fileURLs = ['http://www.test.com/img.jpg',...];
var zip = new JSZip();
var count = 0;

downloadFile(fileURLs[count], onDownloadComplete);


function downloadFile(url, onSuccess) {
    var xhr = new XMLHttpRequest();
    xhr.onprogress = calculateAndUpdateProgress;
    xhr.open('GET', url, true);
    xhr.responseType = "blob";
    xhr.onreadystatechange = function () {
        if (xhr.readyState == 4) {
            if (onSuccess) onSuccess(xhr.response);
}

function onDownloadComplete(blobData){
    if (count < fileURLs.length) {
        blobToBase64(blobData, function(binaryData){
                // add downloaded file to zip:
                var fileName = fileURLs[count].substring(fileURLs[count].lastIndexOf('/')+1);
                zip.file(fileName, binaryData, {base64: true});
                if (count < fileURLs.length -1){
                    count++;
                    downloadFile(fileURLs[count], onDownloadCompleted);
                }
                else {
                    // all files have been downloaded, create the zip
                    var content = zip.generate();

                    // then trigger the download link:        
                    var zipName = 'download.zip';
                    var a = document.createElement('a'); 
                    a.href = "data:application/zip;base64," + content;
                    a.download = zipName;
                    a.click();
                }
            });
    }
}

function blobToBase64(blob, callback) {
    var reader = new FileReader();
    reader.onload = function() {
        var dataUrl = reader.result;
        var base64 = dataUrl.split(',')[1];
        callback(base64);
    };
    reader.readAsDataURL(blob);
}

function calculateAndUpdateProgress(evt) {
    if (evt.lengthComputable) {
        ...
    }
}

最后请注意,如果您下载少量文件(小于10个文件的整体大小不到1MB),此解决方案效果很好,在其他情况下,JSZip会在生成存档时崩溃浏览器选项卡,因此使用分离的线程进行压缩是更好的选择(WebWorker,如zip.js)。

如果在此之后生成了存档,浏览器仍会保持与大文件崩溃并且不报告任何错误,尝试触发saveAs窗口而不传递二进制数据,但是通过传递blob引用(a.href = URL.createObjectURL(zippedBlobData);其中zippedBlobData是blob对象,它引用生成的归档数据);

答案 1 :(得分:0)

基于@guari代码,我在本地对其进行了测试,并将其应用于react应用程序,并附加了该代码以供他人参考。

import JSZip from "jszip";
import saveAs from "jszip/vendor/FileSaver.js";

// .......

// download button click event
btnDownloadAudio = record =>{
    let fileURLs = ['https://www.test.com/52f6c50.AMR', 'https://www.test.com/061940.AMR'];
    let count = 0;
    let zip = new JSZip();
    const query = { record, fileURLs, count, zip };
    this.downloadFile(query, this.onDownloadComplete);
}
downloadFile = (query, onSuccess) => {
    const { fileURLs, count, } = query;
    var xhr = new XMLHttpRequest();
    xhr.onprogress = this.calculateAndUpdateProgress;
    xhr.open('GET', fileURLs[count], true);
    xhr.responseType = "blob";
    xhr.onreadystatechange = function (e) {
        if (xhr.readyState == 4) {
            if (onSuccess) onSuccess(query, xhr.response);
        }
    }
    xhr.send();
}
onDownloadComplete = (query, blobData) => {
    let { record, fileURLs, count, zip } = query;
    if (count < fileURLs.length) {
      const _this = this;
      const { audio_list, customer_user_id, } = record;
      this.blobToBase64(blobData, function(binaryData){
        // add downloaded file to zip:
        var sourceFileName = fileURLs[count].substring(fileURLs[count].lastIndexOf('/')+1);
        // convert the source file name to the file name to display
        var displayFileName = audio_list[count].seq + sourceFileName.substring(sourceFileName.lastIndexOf('.'));
        zip.file(displayFileName, binaryData, {base64: true});
        if (count < fileURLs.length -1){
            count++;
            _this.downloadFile({ ...query, count }, _this.onDownloadComplete);
        }
        else {
            // all files have been downloaded, create the zip
            zip.generateAsync({type:"blob"}).then(function(content) {
                // see FileSaver.js
                saveAs(content, `${customer_user_id}.zip`);
            });
        }
      });
    }
}
blobToBase64 = (blob, callback) => {
    var reader = new FileReader();
    reader.onload = function() {
        var dataUrl = reader.result;
        var base64 = dataUrl.split(',')[1];
        callback(base64);
    };
    reader.readAsDataURL(blob);
}
calculateAndUpdateProgress = (evt) => {
    if (evt.lengthComputable) {
        // console.log(evt);
    }
}

答案 2 :(得分:0)

import JSZip from 'jszip'
import JSZipUtils from 'jszip-utils'
import FileSaver from 'file-saver'

const async downloadZip = (urls) => {
      const urlToPromise = (url) => {
        return new Promise((resolve, reject) => {
          JSZipUtils.getBinaryContent(url, (err, data) => {
            if (err) reject(err)
            else resolve(data)
          })
        })
      }

      const getExtension = (binary) => {
        const arr = (new Uint8Array(binary)).subarray(0, 4)
        let hex = ''
        for (var i = 0; i < arr.length; i++) {
          hex += arr[i].toString(16)
        }
        switch (hex) {
          case '89504e47':
            return 'png'
          case '47494638':
            return 'gif'
          case 'ffd8ffe0':
          case 'ffd8ffe1':
          case 'ffd8ffe2':
          case 'ffd8ffe3':
          case 'ffd8ffe8':
            return 'jpg'
          default:
            return ''
        }
      }

      this.progress = true

      const zip = new JSZip()
      for (const index in urls) {
        const url = urls[index]
        const binary = await urlToPromise(url)
        const extension = getExtension(binary) || url.split('.').pop().split(/#|\?/)[0]
        const filename = `${index}.${extension}`
        zip.file(filename, binary, { binary: true })
      }
      await zip.generateAsync({ type: 'blob' })
        .then((blob) => {
          FileSaver.saveAs(blob, 'download.zip')
        })
}

downloadZip(['https://example.net/1.jpg', 'https://example.net/some_picture_generator'])