Bulk Download Attachment Files

Contents

Overview

This article introduces how to bulk download attachment files from multiple records in an App, using the JSZip (External link) 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 App

Create the Form

Create an App (External link) with the following field settings.

Field Type Field Name Field Code
Attachment Attachment Attachment

Set the Libraries

This sample uses JSZip (External link) v3.10.1, JSZipUtils (External link) v0.0.2 and FileSaver.js (External link) v 2.0.4. Set the following files and URL into the App's JavaScript and CSS Customization settings (External link) .

The specific versions used in this article are as follows:

Sample Code

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 (External link) .

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
/* 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) {
      return Promise.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}`;
    } else if (totalsizeNum < 999999) { // less than 1MB
      totalsize = `${parseInt(totalsizeNum / 1000, 10)}K`;
    } else if (totalsizeNum < 999999999) { // less than 1GB
      totalsize = `${parseInt(totalsizeNum / 1000000, 10)}M`;
    } else {
      return Promise.reject('File size is too large.');
    }
    const dflag = confirm(`Download ${totalsize} bytes. Is it OK to proceed?`);
    if (!dflag) {
      return Promise.reject('Download canceled.');
    }
    return filekeys;
  };

  // Get file URL (async)
  const addFileURL = (key) => new Promise((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) => {
    return Promise.all(filekeys.map(addFileURL));
  };

  // Asynchronously download a file and add it to the zip
  const downloadFile = (zip, url, filename) => new Promise((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()) => {
    return Promise.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);
  });
})();
caution
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 (External link) .
  • FileSaver.js has restrictions on file sizes, depending on what browser is being used.

Reference