kintone の添付ファイルを JSZip で一括ダウンロードしよう

目次

はじめに

kintone にファイルをいっぱい詰め込んだんだけど、ファイルのダウンロードはひとつずつしかできないと嘆いている、そこのアナタに朗報です!
Cybozu CDN に登録されている JSZip (External link) を用いて、添付ファイルを Zip ファイルで一括ダウンロードする方法を紹介します。

添付ファイルの一括ダウンロード/アップロードなどの操作について、kintone コマンドラインツールを使ったやり方の説明記事もあります。
興味のある方は 添付ファイルのダウンロードとアップロード を確認してください。

ライブラリ

今回は一括ダウンロードをより簡単に実装するため、Cybozu CDN 外のライブラリを利用します。
そのような場合は、いったんファイルをダウンロードしてからアプリに適用しましょう。
また、今回紹介する記事で確認したライブラリのバージョンは以下のとおりです。

JSZipUtils と、FileSaver.js は github 上で公開されているものを取得する方法をおすすめします。

デモ環境

デモ環境で実際に動作を確認できます。
https://dev-demo.cybozu.com/k/264/ (External link)

ログイン情報は cybozu developer network デモ環境 で確認してください。

一括ダウンロードのしくみ

今回 kintone からファイルを一括ダウンロードする処理順序は以下のとおりです。

kintone 上の添付ファイルをダウンロードするためには、レコードの取得と URL 生成が必要です。

この点に関しては以下のサンプルで紹介していますので、参考にしましょう。

ファイルダウンロードで必須となる2つの手順

また、「レコードの取得」、「URL の作成」、「ファイルのダウンロード」は非同期通信で行われる点に注意しましょう。

JavaScriptソースコード

ソースコード

  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
174
175
176
177
178
179
180
/* global JSZip */
/* global JSZipUtils */
/* global saveAs */
/*
 * JSZip sample program
 * Copyright (c) 2016 Cybozu
 *
 * Licensed under the MIT License
*/
// /
// / 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
// /

(() => {
  'use strict';
  const fieldCode = '添付ファイル'; // 添付ファイルのフィールドコード
  const isGuestSpace = false;

  // レコード取得関数(非同期)100件まで
  const getAppRecords = () => {
    const url = kintone.api.url('/k/v1/records', isGuestSpace);
    const appId = kintone.app.getId();
    const condition = kintone.app.getQueryCondition() || '';
    const query = condition + 'order by $id asc';
    const body = {
      app: appId,
      query: query,
      field: fieldCode
    };
    return kintone.api(url, 'GET', body);
  };

  // ファイルキーの取得関数
  const getFileKeys = (json) => {
    const keys = [];
    for (let i = 0; i < json.records.length; i++) {
      const filetype = json.records[i][fieldCode];
      // 複数添付ファイルが存在する場合あり
      for (let j = 0; j < filetype.value.length; j++) {
        keys.push(filetype.value[j]);
      }
    }
    return keys;
  };

  // ファイルサイズチェック関数
  const checkFileSize = (filekeys) => {
    if (filekeys.length === 0) {
      return kintone.Promise.reject('添付ファイルが見つかりませんでした。');
    }
    let totalsize = 0;
    for (let i = 0; i < filekeys.length; i++) {
      totalsize += parseInt(filekeys[i].size, 10);
    }
    if (totalsize < 999) { // 1KB未満
      totalsize = String(totalsize);
    } else if (totalsize < 999999) { // 1MB未満
      totalsize = parseInt(totalsize / 1000, 10) + 'K';
    } else if (totalsize < 999999999) { // 1GB未満
      totalsize = parseInt(totalsize / 1000000, 10) + 'M';
    } else {
      // 1GBを上限として設定
      return kintone.Promise.reject('ファイルサイズが大きすぎます。');
    }
    const dflag = confirm(totalsize + 'バイトダウンロードします。よろしいですか?');
    if (!dflag) {
      return kintone.Promise.reject('ダウンロードがキャンセルされました。');
    }
    return filekeys;
  };

  // ファイルURL取得関数(非同期)
  const addFileURL = (key) => {
    return new kintone.Promise((resolve, reject) => {
      const xhr = new XMLHttpRequest();
      const params = {
        fileKey: key.fileKey
      };
      const url = kintone.api.urlForGet('/k/v1/file', params, isGuestSpace);
      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;
          const blobUrl = wurl.createObjectURL(blob);
          key.blobUrl = blobUrl; // URLをkeyレコードに追加
          resolve(key);
        } else {
          reject(JSON.parse(xhr.response));
        }
      };
      xhr.send();
    });
  };

  // 複数ファイルURL取得関数
  const addfileURLs = (filekeys, keynum) => {
    let opt_keynum = keynum || 0;
    return addFileURL(filekeys[opt_keynum]).then((resp) => {
      opt_keynum++;
      if (opt_keynum === filekeys.length) {
        return filekeys;
      }
      return addfileURLs(filekeys, opt_keynum);
    });
  };

  // 非同期で1ファイルダウンロードし、zipへ組み込みする関数
  const downloadFile = (zip, url, filename) => {
    return new kintone.Promise((resolve, reject) => {
      // getbinarycontent は 非同期でURL からファイルを取得するAPI
      // JSZIP util APIを利用
      JSZipUtils.getBinaryContent(url, (err, data) => {
        if (err) {
          reject(err);
        }
        zip.file(filename, data, {binary: true});
        resolve(data);
      });
    });
  };

  // 複数ファイルのダウンロードを実行する関数
  const downloadFiles = (files, zip, filenum) => {
    const opt_zip = zip || new JSZip();
    let opt_filenum = filenum || 0;
    return downloadFile(opt_zip, files[opt_filenum].blobUrl, files[opt_filenum].name).then((data) => {
      opt_filenum++;
      if (opt_filenum === files.length) {
        return opt_zip;
      }
      return downloadFiles(files, opt_zip, opt_filenum);
    });
  };

  // ファイルをzip化する関数
  const doZipFile = (zip) => {
    return zip.generateAsync({type: 'blob'});
  };

  // ファイルを保存する関数
  const saveZipFile = (content) => {
    // FileSaver.jsを利用して保存
    return saveAs(content, 'example.zip');
  };

  // ボタンクリック時に呼び出される関数
  const getZipFile = () => {
    getAppRecords()
      .then(getFileKeys)
      .then(checkFileSize)
      .then(addfileURLs)
      .then(downloadFiles)
      .then(doZipFile)
      .then(saveZipFile)
      .catch((error) => {
        alert(error);
      });
  };

  // レコード一覧画面にボタンを配置
  kintone.events.on('app.record.index.show', (e) => {
    // 増殖バグ対策
    if (document.getElementById('menuButton') !== null) {
      return;
    }
    const menuButton = document.createElement('button');
    menuButton.id = 'menuButton';
    menuButton.textContent = '一括ダウンロード!!';
    menuButton.addEventListener('click', () => {
      getZipFile();
    });
    kintone.app.getHeaderMenuSpaceElement().appendChild(menuButton);
  });
})();

ソースコード解説

一覧画面に一括ダウンロードボタンを配置し、ボタンの押されたタイミングで getZipFile 関数が呼ばれます。
この getZipFiles 関数にてプロミスチェーンを実装し、処理順序を明確にしています。
プロミスを利用すると処理順序が明確になり、非同期処理を簡単に扱えます。

また、then() に関数を渡すと、関数の第一引数に前のプロミスの戻り値の内容が渡されるしくみを利用しています。
これにより、getFileKeys にレコードの一括取得(getAppRecords)の結果のオブジェクトが渡り、filekey の一覧を抽出しています。

その後も同様に処理を重ねて、必要な情報を後処理に渡してつなげていきます。

使い方

ソース上部の fieldCode の値を添付ファイルのフィールドコードに変更する必要があります。

変更した後、JS ファイル(以下例では download.js)をアップロードしましょう。

注意事項

  • ファイル名に日本語を含んでいる場合、文字化けすることがあります。
  • 本サンプルではレコードの取得は 100 件までとなっています。
  • 本サンプルはサブテーブルの添付ファイルには対応していません。
  • FileSaver.js の制限事項として、ブラウザーによってファイルサイズに上限があります。
  • エラー処理が不十分のため、実装の際はご留意ください。

おわりに

今回は一括ダウンロードの手法を紹介しました。

最低限の機能実装でも結構なボリュームがありますが、この後レコードごとにフォルダーを分けたり、ファイル名を可変にしたり、 ファイルのダウンロード中はspin.jsを利用する などいろいろな拡張ができると思います。

いろいろなカスタマイズを行い、ぜひともコミュニティへご投稿ください!

information

この Tips は、2022 年 12 月版 kintone で動作を確認しています。