Как скачать, архивировать и сохранить несколько файлов с помощью Javascript и получить прогресс?
Я создаю расширение Chrome, которое должно загружать несколько файлов (изображений и / или видео) с веб-сайта. Эти файлы могут иметь огромный размер, поэтому я хочу показать пользователю процесс загрузки. После некоторых исследований я обнаружил, что в настоящее время возможное решение может быть:
- Загрузите все файлы с XMLHttpRequests.
- После загрузки заархивируйте все файлы в один архив с библиотекой JavaScript (например, JSZip.js, zip.js).
- Предложите пользователю сохранить zip с помощью диалога SaveAs.
Я застрял в отрывке 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 для такой простой задачи.
4 ответа
Мне напомнили об этом вопросе.. поскольку на него еще нет ответов, я напишу возможное решение на тот случай, если оно будет полезно кому-то еще:
- Как уже было сказано, первая проблема заключается в передаче URL-адреса BLOB-объекта в jszip (он не поддерживает BLOB-объектов, но также не выдает никакой ошибки, чтобы уведомить об этом, и успешно генерирует архив поврежденных файлов): чтобы исправить это, просто передайте строку base64 данных вместо URL-адреса объекта BLOB-объекта;
- Вторая проблема связана с синхронизацией имен файлов: самый простой способ обойти это - загружать по одному файлу за раз вместо использования запросов 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) {
...
}
}
Последнее замечание: это решение работает довольно хорошо, если вы загружаете несколько маленьких файлов (размером менее 1 МБ в целом для менее 10 файлов), в других случаях JSZip вызывает сбой на вкладке браузера при создании архива, поэтому будет лучшим выбором использовать отдельный поток для сжатия (WebWorker, как это делает zip.js).
Если после этого архив был сгенерирован, браузер продолжает сбой в работе с большими файлами и без сообщения об ошибках, попробуйте вызвать окно saveAs, не передавая двоичные данные, но передавая ссылку большого двоичного объекта (a.href = URL.createObjectURL(zippedBlobData);
где zippedBlobData
объект блоба, который ссылается на сгенерированные архивные данные);
Мое решение с использованиемAxios
,FileSaver.js
иJSZip
import JSZip from "jszip";
import axios from "axios";
import { saveAs } from "file-saver";
const zip = new JSZip();
const fileArr = [
{
name: "file1.jpg",
url: "https://url.com/file1.jpg",
},
{
name: "file2.docx",
url: "https://url.com/file2.docx",
},
{
name: "file3.pdf",
url: "https://url.com/file3.pdf",
},
];
const download = (item) => {
//download single file as blob and add it to zip archive
return axios.get(item.url, { responseType: "blob" }).then((resp) => {
zip.file(item.name, resp.data);
});
};
//call this function to download all files as ZIP archive
const downloadAll = () => {
const arrOfFiles = fileArr.map((item) => download(item)); //create array of promises
Promise.all(arrOfFiles)
.then(() => {
//when all promises resolved - save zip file
zip.generateAsync({ type: "blob" }).then(function (blob) {
saveAs(blob, "hello.zip");
});
})
.catch((err) => {
console.log(err);
});
};
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'])
На основе кода @guari я протестировал его локально и применил к реагирующему приложению, прикрепив код для справки других.
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);
}
}