kintone プラグインアップロード処理

kintone REST API に、プラグインアップロードが追加されたので、試してみました。

# 概要

kintone にプラグイン関連の REST API が追加されました。
plugin-uploader は、ブラウザー経由でプラグインをアップロードするため、よく失敗する。
またそれなりに処理時間もかかるため、REST API を試してみました。
REST API 版アップロードは、処理時間が早く、アップロードの失敗も無くよさそうです。

> cybozu developer network>kintone>kintone APIドキュメント>kintone REST API>プラグイン
- [プラグインを読み込む](https://cybozu.dev/ja/kintone/docs/rest-api/plugins/add-plugin/)
- [プラグインを更新する](https://cybozu.dev/ja/kintone/docs/rest-api/plugins/update-plugin/)


開発は ChatGpt に、コードを生成してもらい、動作しない部分を手直ししました。
REST API の呼び出し部分は、間違いが多いのですが、パラメータや環境変数処理などは、そのまま使えます。
ゼロから作るより、かなり効率よく開発できました。

# プラグインアップロード処理

REST API で、アップロードする場合、プラグインID が必要になります。
kintone 環境にプラグインを追加時に、プラグインID が返されますので、それをテキストファイルに保存しました。
プラグイン更新時は、そのテキストファイルを読み込んで使用します。

※~~既存のプラグインについては、とりあえずマニュアルでテキストファイルを作成~~
-> [kintone プラグインアップロード処理用プラグインIDセット](https://qiita.com/rex0220/items/65932c3216d1c3d94d8a)で、セットした。

```実行.log
>npm run upload-api

> narrow-down@0.1.0 upload-api
> node scripts/uploadPlugin-api.js --file dist/narrow-down-plugin24.zip

Uploading file...
File uploaded successfully. fileKey: 2efc7529-3adb-4a69-b0f0-852f7a30fe5e
Plugin updated successfully. { id: 'gbjnadbngbniopdeibcjihdkhkcinljf', version: '24' }
```

プラグインID 無しで、追加処理を行った場合のエラー
```エラー.log
> npm run upload-api

> narrow-down@0.1.0 upload-api
> node scripts/uploadPlugin-api.js --file dist/narrow-down-plugin24.zip

Uploading file...
File uploaded successfully. fileKey: f05ec05f-b68f-43f1-ac1a-7ba8337b659c
An error has occurred.
{
  code: 'GAIA_PL18',
  id: 'Gt2WOVwDLnZNZogNh67q',
  message: 'インストールに失敗しました。プラグインのIDが重複しています。'
}
```

必要なライブラリをインストール
```.cmd
npm install axios form-data yargs
```

```uploadPlugin-api.js
const axios = require('axios');
const fs = require('fs');
const FormData = require('form-data');
const path = require('path');
const yargs = require('yargs');
const { pid } = require('process');

// プラグインIDファイルパス
const pluginIdPath = 'pluginId.txt';

// 環境変数を取得します
const envDomain = process.env.KINTONE_DOMAIN;
const envUsername = process.env.KINTONE_USERNAME;
const envPassword = process.env.KINTONE_PASSWORD;

// 実行時パラメータを取得します
const argv = yargs
  .option('domain', {
    alias: 'd',
    description: 'Kintone domain',
    type: 'string',
    default: envDomain
  })
  .option('username', {
    alias: 'u',
    description: 'Kintone username',
    type: 'string',
    default: envUsername
  })
  .option('password', {
    alias: 'p',
    description: 'Kintone password',
    type: 'string',
    default: envPassword
  })
  .option('file', {
    alias: 'f',
    description: 'The path of the plugin file.',
    type: 'string',
    demandOption: true
  })
  .help()
  .alias('help', 'h')
  .argv;

// パラメータを変数に格納します
const subdomain = argv.domain;
const username = argv.username;
const password = argv.password;
const pluginFilePath = argv.file;

// 環境変数またはコマンドライン引数が不足している場合のチェック
if (!subdomain || !username || !password) {
  console.error('Domain, username and password must all be specified.');
  process.exit(1);
}

// プラグインIDの書き込み
function writePluginID(content) {
  try {
    const rawData = fs.writeFileSync(pluginIdPath, content, 'utf8');
  } catch (error) {
    console.error('Could not write pluginId file:', error.message);
    return '';
  }
}

// プラグインIDをロード
function loadPluginID() {
  try {
    const rawData = fs.readFileSync(pluginIdPath, 'utf8');
    return rawData;
  } catch (error) {
    console.error('Could not load pluginId file:', error.message);
    return '';
  }
}

// プラグインをアップロードする非同期関数
async function uploadPlugin() {
  try {
    // プラグインファイルを読み込み、FormDataオブジェクトを作成します
    const form = new FormData();
    form.append('file', fs.createReadStream(pluginFilePath));


    // KintoneのファイルアップロードAPIエンドポイント
    const fileUploadUrl = `https://${subdomain}/k/v1/file.json`;

    // ヘッダーに基本認証とFormDataのヘッダーを設定します
    const fileUploadHeaders = {
      'X-Cybozu-Authorization': Buffer.from(`${username}:${password}`).toString('base64'),
      ...form.getHeaders(),
    };

    const pluginId = loadPluginID();

    // ファイルをアップロードしてfileKeyを取得します
    console.log('Uploading file...');
    const fileUploadResponse = await axios.post(fileUploadUrl, form, { headers: fileUploadHeaders });
    const fileKey = fileUploadResponse.data.fileKey;
    console.log('File uploaded successfully. fileKey:', fileKey);
    
    // Kintoneのプラグイン読み込みAPIエンドポイント
    const pluginUploadUrl = `https://${subdomain}/k/v1/plugin.json`;

    // ヘッダーに基本認証を設定します
    const pluginUploadHeaders = {
      'X-Cybozu-Authorization': Buffer.from(`${username}:${password}`).toString('base64'),
      'Content-Type': 'application/json'
    };

    // プラグイン読み込みリクエストのペイロード
    const payload = {
      fileKey: fileKey
    };

    if (pluginId) {
      // プラグインの更新
      payload.id = pluginId;
      const respUpload = await axios.put(pluginUploadUrl, payload, { headers: pluginUploadHeaders });
      console.log('Plugin updated successfully.', respUpload.data);
    }
    else {
      // プラグインのインストール
      const respUpload = await axios.post(pluginUploadUrl, payload, { headers: pluginUploadHeaders });
      console.log('Plugin added successfully.', respUpload.data);
      writePluginID(respUpload.data.id);
    }
  } catch (error) {
    console.error('An error has occurred.');
    console.error(error.response ? error.response.data : error.message);
  }
}

// プラグインをアップロードする関数を実行します
uploadPlugin();
```