kintoneでPDFなどをレコードへ添付するとき、「ファイルのアップロードが成功した」だけでは、まだレコードへの保存は終わっていません。また、添付先へ新しいfileKeyだけを書き込むと、既存ファイルを残す意図になりません。
この記事では、弊社kintacoの帳票出力プラグインで使っている添付保存処理を抜粋し、既存ファイルの保持と同時更新の検知を読み解きます。帳票生成やボタン設置までを含む完成サンプルではなく、生成済みのバイト列を保存する部分の解説です。
対象と前提
- 対象は保存済みの1レコードと、テーブル外の添付ファイルフィールドです。
- kintone内でログイン済みのブラウザーから実行する処理です。APIトークンをブラウザーへ埋め込みません。
- 対象レコードとフィールドを閲覧・編集できる権限が必要です。
-
source.revisionには、帳票の元データを取得した時点のリビジョンを渡します。保存直前の番号に無条件で差し替えません。 - 以下はExcelReport 1.0.6のローカルソースにある処理を確認したものです。既存添付の保持・競合時の停止をローカルのモックで検証し、この記事のための実環境への書き込みは行っていません。
処理はGET、ファイルPOST、レコードPUTの順
- 最新レコードを取得し、帳票の元データとリビジョンが一致するか確認する。
- 保存先がFILE型で、既存のfileKeyを取得できるか確認する。
- 生成済みファイルをアップロードし、新しいfileKeyを受け取る。
- 既存のfileKeyと新しいfileKeyをまとめ、同じリビジョンを指定してレコードを更新する。
アップロードとレコード更新は別のAPIです。ファイルをアップロードするAPIで得たキーを、1件のレコードを更新するAPIへ渡します。後者の公式ドキュメントでも、既存ファイルを残して追加するときは既存のfileKeyを含めるよう案内されています。
添付保存部分の実コード
async function saveAttachment(data, filename, mime, fieldCode, source) {
if (!source.app || !source.id || !/^\d+$/.test(String(source.revision))) throw new Error('レコード情報を確認できません。詳細画面を再読み込みしてください。');
if (!fieldCode) throw new Error('保存先の添付ファイルフィールドを設定してください。');
const url = kintone.api.url('/k/v1/record.json', true);
const latest = await kintone.api(url, 'GET', {app:source.app,id:source.id});
if (String(latest.record?.$revision?.value) !== String(source.revision)) throw new Error('レコードが更新されています。詳細画面を再読み込みしてから帳票を出力してください。');
const field = latest.record[fieldCode];
if (!field || field.type !== 'FILE' || !Array.isArray(field.value)) throw new Error('保存先の添付ファイルフィールドを取得できません。フィールド設定・閲覧権限を確認してください。');
const existing = field.value.map(file => {
if (!file.fileKey) throw new Error('既存の添付情報を確認できません。保存を中止しました。');
return {fileKey:file.fileKey};
});
const form = new FormData();
form.append('__REQUEST_TOKEN__', kintone.getRequestToken());
form.append('file', new Blob([data], {type:mime}), filename);
const response = await window.fetch(kintone.api.url('/k/v1/file.json', true), {method:'POST',credentials:'same-origin',headers:{'X-Requested-With':'XMLHttpRequest'},body:form});
const uploaded = await response.json();
if (!response.ok || typeof uploaded.fileKey !== 'string' || !uploaded.fileKey) throw new Error(uploaded.message || '帳票ファイルのアップロードに失敗しました。');
// Preserve all existing files and reject concurrent changes; never retry a write automatically.
return kintone.api(url, 'PUT', {app:source.app,id:source.id,revision:source.revision,record:{[fieldCode]:{value:[...existing,{fileKey:uploaded.fileKey}]}}});
}
引数のdataは生成済みのバイト列、filenameは保存名、mimeはMIMEタイプ、fieldCodeは保存先の実際のフィールドコードです。sourceにはapp、id、revisionを渡します。帳票生成処理やイベントハンドラー、二重クリック防止は、この関数を呼ぶ側で扱う必要があります。
既存のfileKeyを残す
更新値のvalueには、既存添付を{fileKey: ...}へ整形した配列と、アップロードで得たキーを入れています。現在のファイルがA・B、新しいファイルがCなら、送るキーはA・B・Cです。
この関数は同名ファイルを探して置き換える処理を持ちません。再実行すると追加になるため、同名判定や版管理が必要なら別途設計します。ファイル名が同じという理由だけで既存添付を自動削除しない点も、意識しておきたいところです。
GETで確認してもPUTにrevisionを付ける理由
GET直後に番号が一致していても、その後のアップロード中に他の利用者がレコードを更新する可能性があります。そこで、最終PUTにも元のsource.revisionを渡します。チェック後から保存までの間に起きた更新も、競合として止めるためです。
競合エラーの後で最新リビジョンを取り直し、同じ帳票を自動でPUTし直す処理は入れていません。帳票が古い値から生成されていないか、既存添付がどう変わったかを人が確認する必要があるからです。
FormDataにContent-Typeを手で付けない
ファイルのPOSTではFormDataを使い、リクエストトークンを含めています。Content-Type: multipart/form-dataを自分で固定せず、境界文字列をブラウザーに設定させます。kintone.api.url(..., true)はゲストスペースを考慮したURL生成のために使用していますが、権限や配置する画面の動作確認は別途必要です。
自動リトライしない境界を決める
この関数はエラーを呼び出し元へ返します。権限不足、アップロード失敗、PUT失敗を無視して成功扱いにはしません。アップロード後にPUTが失敗した場合、そのファイルはレコードへ紐付いていない可能性があります。
通信が切れたときは「保存されなかった」と断定できない場合もあります。画面側でエラーを表示し、レコードの値・添付を確認してから再実行する運用にします。レコードを更新すると更新者・更新日時や通知・Webhook等に影響する場合もあるため、帳票以外の処理を含めて検証してください。
ローカルで確認したことと、実環境で確認すること
モックでは、既存添付2件への追加、空の添付先への追加、事前のリビジョン不一致、FILE型以外、既存キー欠落、アップロード失敗、PUT時の競合を確認します。失敗時に後続処理や自動再試行へ進まないことも確認します。
実環境では、実際の利用者の権限、生成ファイルが開けること、保存後の表示更新、通知・Webhook、モバイル、二重クリックや再発行の扱いを別途確認してください。ローカルの成功は、これらの実機動作を保証するものではありません。
帳票生成と保存先設定をまとめて使う場合
この記事の処理を題材にした自社製品は、Excel・PDF帳票出力プラグインです。テンプレートから生成し、帳票ごとにダウンロード・添付保存・両方を選べます。機能と制約はリンク先で確認できます。
無料利用では、非契約ドメインのPC・モバイル一覧画面に有償プラン案内が30%の確率で表示されます。契約済みドメインには表示されません。一度閉じると、同じブラウザー・同じkintoneドメインの全Kintacoプラグインで24時間非表示です。
設定画面を使った運用手順は、作成した帳票をレコードへ添付保存するコラムに分けています。