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:

Flowchart: The bulk download process flow diagram.

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.1.4, JSZipUtils (External link) v0.0.2 and FileSaver.js (External link) v 1.0.0. Set the following files and URL into the App’s JavaScript and CSS Customization settings (External link).

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
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
/* 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/master/LICENSE.markdown
// / jszip-utils MIT or GPLv3 https://github.com/Stuk/jszip-utils/blob/master/LICENSE.markdown
// /

(function() {
  'use strict';
  var fieldCode = 'Attachment'; // Field code of Attachment field
  var isGuestSpace = false;

  // Get records function(async) - limit of 100 records
  function getAppRecords() {
    var url = kintone.api.url('/k/v1/records', isGuestSpace);
    var appId = kintone.app.getId();
    var condition = kintone.app.getQueryCondition() || '';
    var query = condition + 'order by $id asc';
    var body = {
      'app': appId,
      'query': query,
      'field': fieldCode
    };
    return kintone.api(url, 'GET', body);
  }

  // Extract and list fileKeys
  function getFileKeys(json) {
    var keys = [];
    for (var i = 0; i < json.records.length; i++) {
      var filetype = json.records[i][fieldCode];
      // When multiple files are attached
      for (var j = 0; j < filetype.value.length; j++) {
        keys.push(filetype.value[j]);
      }
    }
    return keys;
  }

  // Check file sizes
  function checkFileSize(filekeys) {
    if (filekeys.length === 0) {
      return kintone.Promise.reject('Attachements were not found.');
    }
    var totalsize = 0;
    for (var i = 0; i < filekeys.length; i++) {
      totalsize += parseInt(filekeys[i].size, 10);
    }
    if (totalsize < 999) { // less than 1KB
      totalsize = String(totalsize);
    } else if (totalsize < 999999) { // less than 1MB
      totalsize = parseInt(totalsize / 1000, 10) + 'K';
    } else if (totalsize < 999999999) { // less than 1GB
      totalsize = parseInt(totalsize / 1000000, 10) + 'M';
    } else {
      // Limit to 1GB
      return kintone.Promise.reject('File size is too large');
    }
    var dflag = confirm('Download ' + totalsize + ' bytes. Is it OK to proceed?');
    if (!dflag) {
      return kintone.Promise.reject('Download canceled');
    }
    return filekeys;
  }

  // Get file URL (async)
  function addFileURL(key) {
    return new kintone.Promise(function(resolve, reject) {
      var xhr = new XMLHttpRequest();
      var params = {
        'fileKey': key.fileKey
      };
      var url = kintone.api.urlForGet('/k/v1/file', params, isGuestSpace);
      xhr.open('GET', url, true); // async
      xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
      xhr.responseType = 'blob';
      xhr.onload = function() {
        if (xhr.status === 200) {
          var blob = new Blob([xhr.response]);
          var wurl = window.URL || window.webkitURL;
          var blobUrl = wurl.createObjectURL(blob);
          key.blobUrl = blobUrl; // Add URL to key record
          resolve(key);
        } else {
          reject(JSON.parse(xhr.response));
        }
      };
      xhr.send();
    });
  }

  // Get multiple file URLs
  function addfileURLs(filekeys, keynum) {
    var opt_keynum = keynum || 0;
    return addFileURL(filekeys[opt_keynum]).then(function(resp) {
      opt_keynum++;
      if (opt_keynum === filekeys.length) {
        return filekeys;
      }
      return addfileURLs(filekeys, opt_keynum);
    });
  }

  // Asynchronously download a file and add it to the zip file
  function downloadFile(zip, url, filename) {
    return new kintone.Promise(function(resolve, reject) {
      // getBinaryContent is an API that retrieves files from URLs asynchronously
      JSZipUtils.getBinaryContent(url, function(err, data) {
        if (err) {
          reject(err);
        }
        zip.file(filename, data, {binary: true});
        resolve(data);
      });
    });
  }

  // Download multiple files
  function downloadFiles(files, zip, filenum) {
    var opt_zip = zip || new JSZip();
    var opt_filenum = filenum || 0;
    return downloadFile(opt_zip, files[opt_filenum].blobUrl, files[opt_filenum].name).then(function(data) {
      opt_filenum++;
      if (opt_filenum === files.length) {
        return opt_zip;
      }
      return downloadFiles(files, opt_zip, opt_filenum);
    });
  }

  // Zip the file
  function doZipFile(zip) {
    return zip.generateAsync({type: 'blob'});
  }

  // Save the file
  function saveZipFile(content) {
    // Use FileSaver.js
    return saveAs(content, 'example.zip');
  }

  // Button click function
  function getZipFile() {
    getAppRecords()
      .then(getFileKeys)
      .then(checkFileSize)
      .then(addfileURLs)
      .then(downloadFiles)
      .then(doZipFile)
      .then(saveZipFile)
      .catch(function(error) {
        alert(error);
      });
  }

  // Create a button in the Record List view
  kintone.events.on('app.record.index.show', function(e) {
    // For debug
    if (document.getElementById('menuButton') !== null) {
      return;
    }
    var menuButton = document.createElement('button');
    menuButton.id = 'menuButton';
    menuButton.innerHTML = 'Download at once';
    menuButton.onclick = function() {
      getZipFile();
    };
    kintone.app.getHeaderMenuSpaceElement().appendChild(menuButton);
  });
})();

The JavaScript and CSS Customization settings should look like the following:

Screenshot: The necessary files have been uploaded on to the JavaScript and CSS Customization settings.

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 starts a promise chain. This makes the order of the later processes clearer and handling of asynchronous processes easier. then() is used to pass on the object obtained from getAppRecords from promise to promise, and a list of fileKeys is created from this object.

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