This article introduces how to bulk download attachment files from multiple records in an App, using the
JSZip
library. This is especially useful for users who have stored many files in a Kintone App, and wish to download all the files at once.
The Bulk Download Flow
The bulk download process from Kintone is as follows:
To download attachment files from Kintone, fileKeys need to be obtained from the records using the
Get Records API
. The
Download File API
can then be used, using the fileKey as the parameter value.
Note that the steps Get Records, Create fileKey list, and Create URLs need to run asynchronously.
Prepare the following JavaScript code in a text editor and navigate to the Kintone App's settings.
Upload the file into the Upload JavaScript for PC option of the
JavaScript and CSS Customization settings
.
/* global JSZip *//* global JSZipUtils *//* global saveAs */// /
// / License
// / FileSaver.js MIT https://github.com/eligrey/FileSaver.js/blob/master/LICENSE.md
// / jszip MIT or GPLv3 https://github.com/Stuk/jszip/blob/main/LICENSE.markdown
// / jszip-utils MIT or GPLv3 https://github.com/Stuk/jszip-utils/blob/master/LICENSE.markdown
// /
(() => {
'use strict';
const fieldCode = 'Attachment'; // Field code of Attachment field
// Get records function (async) - limit of 100 records
const getAppRecords = () => {
const url = kintone.api.url('/k/v1/records');
const appId = kintone.app.getId();
const condition = kintone.app.getQueryCondition() || '';
const query = `${condition} order by $id asc`;
const body = {
app: appId,
query,
field: fieldCode
};
return kintone.api(url, 'GET', body);
};
// Extract and list fileKeys
const getFileKeys = (json) => {
const keys = [];
json.records.forEach(record => {
const files = record[fieldCode];
files.value.forEach(file => {
keys.push(file);
});
});
return keys;
};
// Check file sizes
const checkFileSize = (filekeys) => {
if (filekeys.length === 0) {
returnPromise.reject('Attachments were not found.');
}
const totalsizeNum = filekeys.reduce((sum, file) => sum + parseInt(file.size, 10), 0);
let totalsize;
if (totalsizeNum < 999) { // less than 1KB
totalsize = `${totalsizeNum}`;
} elseif (totalsizeNum < 999999) { // less than 1MB
totalsize = `${parseInt(totalsizeNum / 1000, 10)}K`;
} elseif (totalsizeNum < 999999999) { // less than 1GB
totalsize = `${parseInt(totalsizeNum / 1000000, 10)}M`;
} else {
returnPromise.reject('File size is too large.');
}
const dflag = confirm(`Download ${totalsize} bytes. Is it OK to proceed?`);
if (!dflag) {
returnPromise.reject('Download canceled.');
}
return filekeys;
};
// Get file URL (async)
const addFileURL = (key) => newPromise((resolve, reject) => {
const xhr = new XMLHttpRequest();
const params = {fileKey: key.fileKey};
const url = kintone.api.urlForGet('/k/v1/file', params);
xhr.open('GET', url, true);
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
xhr.responseType = 'blob';
xhr.onload = () => {
if (xhr.status === 200) {
const blob = new Blob([xhr.response]);
const wurl = window.URL || window.webkitURL;
key.blobUrl = wurl.createObjectURL(blob);
resolve(key);
} else {
reject(JSON.parse(xhr.response));
}
};
xhr.send();
});
// Get multiple file URLs
const addfileURLs = (filekeys) => {
returnPromise.all(filekeys.map(addFileURL));
};
// Asynchronously download a file and add it to the zip
const downloadFile = (zip, url, filename) => newPromise((resolve, reject) => {
JSZipUtils.getBinaryContent(url, (err, data) => {
if (err) {
reject(err);
return;
}
zip.file(filename, data, {binary: true});
resolve(data);
});
});
// Download multiple files
const downloadFiles = (files, zip = new JSZip()) => {
returnPromise.all(
files.map(file => downloadFile(zip, file.blobUrl, file.name))
).then(() => zip);
};
// Zip the files
const doZipFile = (zip) => zip.generateAsync({type: 'blob'});
// Save
const saveZipFile = (content) => saveAs(content, 'example.zip');
// Button click function
const getZipFile = async () => {
try {
const records = await getAppRecords();
const fileKeys = await getFileKeys(records);
const checkedKeys = await checkFileSize(fileKeys);
const filesWithUrls = await addfileURLs(checkedKeys);
const zip = await downloadFiles(filesWithUrls);
const content = await doZipFile(zip);
await saveZipFile(content);
} catch (error) {
alert(error);
}
};
// Create a button in the Record List view
kintone.events.on('app.record.index.show', () => {
if (document.getElementById('menuButton')) return;
const menuButton = document.createElement('button');
menuButton.id = 'menuButton';
menuButton.textContent = 'Download at once';
menuButton.addEventListener('click', getZipFile);
kintone.app.getHeaderMenuSpaceElement().appendChild(menuButton);
});
})();
Attention
Caution:
The order in which JavaScript and CSS are uploaded to an App matters. In this example, ensure that the 3 libraries are uploaded before the sample JavaScript file. The order of the uploads can be changed by clicking and dragging on the arrows for each item on the Upload JavaScript / CSS page.
After saving the settings, update the App.
The source code places the button for the bulk download on the Record List view. When the button is clicked, the getZipFile function is called.
getZipFile is an async function that uses await to handle each step in sequence. This makes the order of operations clear and easy to follow. The result of each step is passed directly to the next, starting with getAppRecords and ending with the file being saved.
Test the Integration
Add a few records into the App, and attach files to the Attachment fields. Navigate to the Record List page of the App. A button should be displayed above the list of records. Click the button to start the download process.
Limitations
In this sample, only data of up to 100 records will be downloaded as a Zip file.
The sample does not support Attachment fields placed in
Table fields
.
FileSaver.js has restrictions on file sizes, depending on what browser is being used.