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
/* global JSZip */
/* global JSZipUtils */
/* global saveAs */
//
// JSZip sample program
// Copyright (c) 2025 Cybozu
//
// Licensed under the MIT 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,
      field: fieldCode
    };
    return kintone.api(url, 'GET', body);
  };

  // ファイルキーの取得関数
  const getFileKeys = (json) => {
    const keys = [];
    json.records.forEach(record => {
      const files = record[fieldCode];
      files.value.forEach(file => {
        keys.push(file);
      });
    });
    return keys;
  };

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

  // ファイルURL取得関数(非同期)
  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, 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;
        key.blobUrl = wurl.createObjectURL(blob);
        resolve(key);
      } else {
        reject(JSON.parse(xhr.response));
      }
    };
    xhr.send();
  });

  // 複数ファイルURL取得関数(Promise.allで並列化)
  const addfileURLs = (filekeys) => {
    return Promise.all(filekeys.map(addFileURL));
  };

  // 非同期で1ファイルダウンロードし、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);
    });
  });

  // 複数ファイルのダウンロードを実行する関数(Promise.allで並列化)
  const downloadFiles = (files, zip = new JSZip()) => {
    return Promise.all(
      files.map(file => downloadFile(zip, file.blobUrl, file.name))
    ).then(() => zip);
  };

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

  // ファイルを保存する関数
  const saveZipFile = (content) => saveAs(content, 'example.zip');

  // ボタンクリック時に呼び出される関数
  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);
    }
  };

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

ソースコード解説

一覧画面に一括ダウンロードボタンを配置し、ボタンの押されたタイミングでgetZipFile関数が呼ばれます。
このgetZipFiles関数にて、async/awaitで非同期処理を実装しています。
これにより、getFileKeysにレコードの一括取得(getAppRecords)の結果のオブジェクトが渡り、filekeyの一覧を抽出しています。
その後も同様に処理を重ねて、必要な情報を後処理に渡してつなげていきます。

使い方

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

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

注意事項

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

おわりに

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

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

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

information

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