Всякий раз, когда я пытаюсь создать zip-файл с помощью JSZip, я либо получаю сообщение об ошибке, указывающее, что изображения не могут быть экземпляром Blob (так оно и есть), либо результат поврежден/не может быть извлечен.
У меня есть внутренний API, который возвращает изображение .png с идентификатором, соответствующим изображению из аутентифицированной конечной точки.
В серверном компоненте я получаю набор идентификаторов изображений из отдельной конечной точки, а затем сопоставляю его с массивом изображений .png (как объекты Blob, используя await res.blob()
). Затем я создаю zip-файл из этих изображений с помощью JSZip, который возвращаю как объект Buffer.
В клиентском компоненте я получаю этот объект Buffer, создаю из него объект Blob, а затем URL-адрес, по которому щелкаю.
Серверная составляющая:
const download = async (file, format: string) => {
"use server"
const imageArray = await makeImageArray(file.id, format);
const zip = new JSZip();
await Promise.all(imageArray.map(async (rendered) => {
console.info("blob", rendered.data)
zip.file(`${rendered.name.replaceAll(" ", "_")}.${rendered.type}`, Buffer.from(await rendered.data.arrayBuffer()).toString("base64"), {base64: true});
return rendered.data;
}));
const zipped = await zip.generateAsync({type: "blob"})
const arrayBuffer = await zipped.arrayBuffer();
const buffer = Buffer.from(arrayBuffer)
return buffer
}
Клиентский компонент:
const clickAZip = async (file, format) => {
const a = document.createElement("a");
const zip = await onDownload(file, format)
a.href = URL.createObjectURL(new Blob(zip,{type: 'application/zip'}))
a.download=`${file.name}.zip`
a.click()
}
Обратите внимание: код здесь загружает поврежденный zip-архив. Если я заменю rendered.data.arrayBuffer()).toString("base64")
на rendered.data
, я получу следующую ошибку в серверном компоненте:
Internal error: Error: Can't read the data of 'Generic_image.png'. Is it in a supported JavaScript type (String, Blob, ArrayBuffer, etc) ?
Редактировать:
Код функции makeImageArray
, которая может быть источником поврежденного файла:
const makeImageArray = async (id: string, format: string) => {
"use server"
return await authenticatedFetch(process.env.NEXT_PUBLIC_API_URL + `/project/sheets/all/${id}`, "GET", null)
.then(async (response) => {
return await Promise.all(response
.map(async (sheet) => {
return {
data: await blobFetch(process.env.NEXT_PUBLIC_API_URL + '/private/render', "POST", JSON.stringify({
text: sheet.text,
format: format
})), name: sheet.name, type: format
}
}))
}).then(res => res.filter(r => !r.data?.error))
}
Функция получает массив идентификаторов (строков) из первой выборки, а затем сопоставляет его с массивом изображений .png посредством другой выборки. Маловероятно, что сами изображения повреждены, поскольку поврежден zip-архив в целом, а не отдельные содержащиеся в нем файлы.
Итак, я нашел проблему:
По сути, клиентский компонент получал буфер, а затем пытался обработать его как массив, создавая поврежденный файл.
Решением было создание экземпляра объекта Uint8Array из буфера перед обработкой этого массива:
Серверный компонент:
const download = async (file, format) => {
"use server";
const imageArray = await makeImageArray(file.id, format);
const zip = new JSZip();
await Promise.all(imageArray.map(async (rendered) => {
const arrayBuffer = await rendered.data.arrayBuffer();
zip.file(`${rendered.name.replaceAll(" ", "_")}.${rendered.type}`, arrayBuffer);
}));
const zipped = await zip.generateAsync({type: "arraybuffer"});
return Buffer.from(zipped)
}
Клиентский компонент:
const clickAZip = async (file, format) => {
const a = document.createElement("a");
const zipBuffer = await onDownload(file, format);
const zipArr = new Uint8Array(zipBuffer);
const blob = new Blob([zipArr.buffer], {type: 'application/zip'});
a.href = URL.createObjectURL(blob);
a.download = `${file.name}.zip`;
a.click();
}