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

Posted

技术标签:

【中文标题】如何使用 Javascript 下载、压缩和保存多个文件并取得进展?【英文标题】:How to download, zip and save multiple files with Javascript and get progress? 【发布时间】:2013-06-24 11:43:08 【问题描述】:

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

    使用 XMLHttpRequests 下载所有文件。 下载后,使用 javascript 库(例如 JSZip.js、zip.js)将所有文件压缩到一个存档中。 使用“另存为”对话框提示用户保存 zip。

我卡在第 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 来完成这样一个简单的任务。

【问题讨论】:

当我阅读问题时,您想压缩所有图像并提供一个 zip 作为下载,对吗?那么其他问题的答案是 100% 适用的。将 URL 替换为您使用所选 zip 库生成的数据 URL 或 blob-URL。如果您想要一个示例,请随时查看我的 Chrome extension source viewer 扩展的源代码。 popup.js 显示了启动下载生成的 zip 文件的代码。 我看到你试图从我的扩展中复制代码而不理解它,对吗?如果是这样,请阅读 zip.js 的文档并在遇到困难或不清楚时返回:gildas-lormeau.github.io/zip.js/core-api.html 来自您的扩展(有用)我只使用了代码来触发下载,问题不存在,它是将下载的文件二进制数据传递给 zip 库,我不明白它是怎么做到的因为我没有找到任何相关的文档,所以要完成。 在我的扩展程序中,我正在下载一个与 zip 兼容的文件,并立即将其显示为下载。在您的情况下这不行,因为您正在下载图像,并且想要压缩它。我刚刚发现另一个问题似乎完全与您的原始问题和新问题一致:***.com/questions/14180375/… 我知道,我已经阅读了那个答案,但在这种情况下,如果文件是由 zip 库内部下载的(而不是在我的代码中使用 xhr 请求),我如何获取下载信息在下载文件需要几分钟的情况下通知用户的进度? 【参考方案1】:

我想起了这个问题..因为它还没有答案,我写了一个可能的解决方案,以防它对其他人有用:

如前所述,第一个问题是将 blob url 传递给 jszip(它不支持 blob,但它也不会抛出任何错误来通知它,它会成功生成损坏文件的存档):要更正此问题,只需传递数据的 base64 字符串,而不是其 blob 对象 url; 第二个问题是文件名同步:这里最简单的解决方法是一次下载一个文件,而不是使用并行的 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); where @987654323 @ 是引用生成的归档数据的 blob 对象);

【讨论】:

但是“data:application/zip”类型的 a.href 不会触发下载,对吗?也许 data:attachment 可以在这里使用? 如果我们必须处理一个大文件会使用什么?【参考方案2】:
import JSZip from 'jszip'
import JSZipUtils from 'jszip-utils'
import FileSaver from 'file-saver'

const downloadZip = async (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'])

【讨论】:

欢迎来到 Stack Overflow。此处不鼓励仅使用代码的答案,因为它们没有解释代码如何回答问题。请编辑您的答案以解释代码的作用以及它如何解决问题,以便它对其他用户以及 OP 也有用。【参考方案3】:

基于@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);
    

【讨论】:

以上是关于如何使用 Javascript 下载、压缩和保存多个文件并取得进展?的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 JavaScript 压缩文件?

JavaScript文件上传和下载

如何使用Javascript实现不下载的保存和另存为?

如何实现,读取远程文件,用GZIP压缩后保存成文件

如何使用 C# 下载和解压缩 gzip 文件?

如何即时从 hyper::Response 解压缩 XZ 数据?