クラウドストレージ「Box」にAPIを使ってファイルをアップロードする


# 概要

クラウドストレージのBoxにAPI経由でファイルのアップロードを行う必要があったのですが、
調べてもあまり情報がなかったので記載しておきます。

# 環境

開発環境はMacでNodebrewを使っています。

```
$ nodebrew 
nodebrew 1.0.1
$ node -v
v12.14.1
```

# カスタムアプリの作成と承認

## カスタムアプリの作成

BoxAPIを使うためにまずカスタムアプリを作成します。

デベロッパーコンソールにアクセスしてBoxアカウントにログインします。
https://app.box.com/developers/console

アプリの新規作成を押下します。

アプリの種類でカスタムアプリを選択します。

<img width="1544" alt="スクリーンショット 2020-01-13 1.13.16.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227811/a0ffbef2-7099-c6fb-bfff-559b1199b837.png">

認証方法でJWTを使用したOAuth 2.0 (サーバー認証)を選択します。

<img width="1004" alt="スクリーンショット 2020-01-13 1.13.30.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227811/d1a76fc4-775a-2bf1-5247-322851a90f36.png">

アプリの名前を入力してアプリの作成を押下します。

<img width="1242" alt="スクリーンショット 2020-01-13 1.13.55.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227811/27759f9a-06bc-75d7-47c8-4c0519646857.png">

アプリを作成するとデベロッパーコンソールのマイアプリに追加されるので、
作成したアプリを選択して左のメニューから構成画面を表示します。

<img width="1271" alt="スクリーンショット 2020-01-13 1.15.31.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227811/b8635319-4f45-eed7-8b82-0f27e417f4f7.png">

ここに表示されているDeveloperトークンを使ってAPIを実行することもできますが、
今回は認証キーを使用します。

構成画面内に公開キーの追加と管理という項目があるので、そちらから公開/秘密キーペアを生成します。
※アカウントに二段階認証が設定されている必要があります。

<img width="1009" alt="スクリーンショット 2020-01-13 1.16.50.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227811/6d85ebf5-3775-78ee-796c-2eee03728e14.png">

キーペアを生成すると認証情報が記載されたJSONファイルが保存されるので、こちらを使って認証を行います。

## カスタムアプリの承認

次にBoxアカウントにカスタムアプリの承認を行います。

Boxアカウントの管理コンソールからEnterprise設定を開いて
アプリ→カスタムアプリケーション→新しいアプリケーションを承認を押下します。

<img width="1497" alt="スクリーンショット 2020-01-13 1.19.50.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227811/c69041ae-ce0b-9d31-1edf-ea6c3604f790.png">

APIキーの入力画面が表示されるので、
先ほど保存したJSONファイルの中のclientIDを入力して承認します。

<img width="1406" alt="スクリーンショット 2020-01-13 1.22.28.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227811/2d41342c-a84a-3165-9d10-6455c3ce9705.png">

これでカスタムアプリからAPIを実行する準備ができました。

# BoxSDKからAPIの実行

## フォルダ内の項目一覧の取得

BoxAPIにはPythonやJAVAのSDKも用意されていますが、今回はbox-node-sdkを使って実行してみます。
https://github.com/box/box-node-sdk

APIリファレンス
https://developer.box.com/jp/reference/

まず適当なフォルダを作成してbox-node-sdkをインストールします。

```
$ mkdir box-app
$ cd box-app/
$ npm init -y
$ npm install box-node-sdk
```

作成したフォルダ内に認証情報のJSONファイルを保存しておき、
以下のようなスクリプトを作成します。

こちらの処理では指定したフォルダID内の項目の一覧を取得するAPIを実行しています。
フォルダID'0'はルートディレクトリのIDです。
https://developer.box.com/jp/reference/get-folders-id-items/

``` script.js
const BoxSDK = require('box-node-sdk');

// 認証キーの読み込み
const sdkConfig = require('./config.json');
const sdk = BoxSDK.getPreconfiguredInstance(sdkConfig);

const client = sdk.getAppAuthClient('enterprise');

// フォルダ内の項目の取得
client.folders.getItems('0')
    .then(folder => {
        console.log(folder);
    })
    .catch(err => {
        console.log(err);
    })
```

APIを実行してみます。

```
$ node script.js 
{
  total_count: 0,
  entries: [],
  offset: 0,
  limit: 100,
  order: [
    { by: 'type', direction: 'ASC' },
    { by: 'name', direction: 'ASC' }
  ]
```

実行するとAPIで取得した項目一覧が出力されますが、
もし自分のBoxアカウントのルートディレクトリにファイルやフォルダを保存していたとしても
ここではなにも表示されないと思います。

ここで取得しているルートディレクトリというのはカスタムアプリ（というアカウント）のルートディレクトリとなるため、
自分のBoxアカウントのルートディレクトリとは別の場所になります。

これだとBoxSDKを使っても結果がわかりづらいのでBoxアカウントとの共有フォルダを作成します。

## 共有フォルダの作成

まずブラウザで自分のBoxアカウントにアクセスして適当なフォルダを作成します。

管理コンソールからユーザーとグループを開いて
グループ→作成を押下します。

<img width="1409" alt="スクリーンショット 2020-01-13 1.06.02.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227811/166934c3-c346-1ee3-79e1-0ba57d6ed66a.png">

グループ名を入力して権限設定でグループメンバーを選択します。

<img width="1606" alt="スクリーンショット 2020-01-13 1.28.00.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227811/ca641c56-aff7-1aa2-084f-65d291449f3e.png">

メンバーを追加を押下してユーザーの入力欄にカスタムアプリ名を入力すると
サジェストが表示されるのでそちらを選択して追加します。

<img width="1783" alt="スクリーンショット 2020-01-13 1.28.52.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227811/ce85d8b4-ebdf-7c6f-6b23-2af1c72239cd.png">

最後にフォルダを共有を押下して先ほど作成したフォルダを選択して完了します。

<img width="1683" alt="スクリーンショット 2020-01-13 1.30.07.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227811/e022697d-12e4-fa9a-eb87-d0aa9bbb0ea6.png">

これでカスタムアプリとフォルダの共有を行なっているグループが作成されました。

<img width="1705" alt="スクリーンショット 2020-01-13 1.31.23.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227811/6210c94c-0dca-8fe1-1a75-8933e5a21c51.png">

もう一度APIを実行してみると今度は共有フォルダが取得されていることが確認できます。

```
$ node script.js 
{
  total_count: 1,
  entries: [
    {
      type: 'folder',
      id: '99896711320',
      sequence_id: '0',
      etag: '0',
      name: 'shared'
    }
  ],
  offset: 0,
  limit: 100,
  order: [
    { by: 'type', direction: 'ASC' },
    { by: 'name', direction: 'ASC' }
  ]
}
```

## ファイルアップロード

共有フォルダが作成できたのでファイルアップロードAPIを実行します。
https://developer.box.com/jp/reference/post-files-content/

ソースを以下のように修正します。

``` script.js
const BoxSDK = require('box-node-sdk');
// 追加
const fs = require('fs');

// 認証キーの読み込み
const sdkConfig = require('./config.json');
const sdk = BoxSDK.getPreconfiguredInstance(sdkConfig);

const client = sdk.getAppAuthClient('enterprise');

// フォルダ内の項目の取得
client.folders.getItems('0')
    .then(folder => {
        // 追加
        fs.readFile('./upload_file.txt', (err, data) => {
            if (err) {
                console.log(err);
                return;
            }
            // ファイルアップロード
            client.files.uploadFile(folder.entries[0].id, 'upload_file.txt', data)
                .then(file => {
                    console.log(file);
                })
                .catch(err => {
                    console.log(err);
                })
        })
    })
    .catch(err => {
        console.log(err);
    })
```

スクリプトを実行するとアップロードしたファイルの情報が出力されます。

```
$ node script.js 
{
  total_count: 1,
  entries: [
    {
      type: 'file',
      id: '595943878038',
      file_version: [Object],
      sequence_id: '0',
      etag: '0',
      sha1: '',
      name: 'upload_file.txt',
      description: '',
      size: 247379,
      path_collection: [Object],
      created_at: '2020-01-12T08:54:09-08:00',
      modified_at: '2020-01-12T08:54:09-08:00',
      trashed_at: null,
      purged_at: null,
      content_created_at: '2020-01-12T08:54:09-08:00',
      content_modified_at: '2020-01-12T08:54:09-08:00',
      created_by: [Object],
      modified_by: [Object],
      owned_by: [Object],
      shared_link: null,
      parent: [Object],
      item_status: 'active'
    }
  ]
}
```

Boxアカウントでフォルダを確認するとファイルが保存されています。

<img width="1783" alt="スクリーンショット 2020-01-13 1.56.28.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227811/f38c6e53-4f34-6d67-81fc-8f5107bb5e2e.png">

※フォルダ内に同名ファイルがある状態でアップロードを行うと409エラーが返ってくるので
その場合はファイル名を変更するかファイルのバージョンアップAPIを実行してください。
https://developer.box.com/jp/reference/post-files-id-content/

## 大容量ファイルのアップロード

サイズの小さいファイルは上の方法でアップロードを行えば良いのですが、
50MBを超えるファイルは分割アップロードを使用することが推奨されているとリファレンスに記載があります。

分割アップロードAPI
https://developer.box.com/jp/reference/get-files-upload-sessions-id/

ただ、APIリファレンスのやり方だと分割アップロードのセッションの作成、
パーツごとに分割アップロード、アップロードセッションのコミットなど複数のAPIを実行する必要があり少し面倒なのですが、
box-node-sdkのドキュメントを見てみるとこれらをまとめて行なってくれる分割アップロード用のAPIが用意されているようです。
https://github.com/box/box-node-sdk/blob/master/docs/files.md#chunked-upload

こちらを使って分割アップロードを試してみます。

サイズが大きいファイルを用意してソースを修正します。
※20MB以下のファイルを分割アップロードするとエラーになります。

``` script.js
const BoxSDK = require('box-node-sdk');
const fs = require('fs');

// 認証キーの読み込み
const sdkConfig = require('./config.json');
const sdk = BoxSDK.getPreconfiguredInstance(sdkConfig);

const client = sdk.getAppAuthClient('enterprise');

// フォルダ内の項目の取得
client.folders.getItems('0')
    .then(folder => {
        // 修正
        fs.readFile('./large_file.csv', (err, data) => {
            if (err) {
                console.log(err);
                return;
            }
            // 分割ファイルアップロード
            client.files.getChunkedUploader(folder.entries[0].id, data.length, 'large_file.csv', data)
                .then(uploader => uploader.start())
                .then(file => {
                    console.log(file);
                })
                .catch(err => {
                    console.log(err);
                })
        })
    })
    .catch(err => {
        console.log(err);
    })
```

スクリプトを実行するとアップロードしたファイルの情報が出力されます。

```
$ node script.js 
{
  total_count: 1,
  entries: [
    {
      type: 'file',
      id: '595967596582',
      file_version: [Object],
      sequence_id: '0',
      etag: '0',
      sha1: '',
      name: 'large_file.csv',
      description: '',
      size: 20243278,
      path_collection: [Object],
      created_at: '2020-01-12T09:31:27-08:00',
      modified_at: '2020-01-12T09:31:27-08:00',
      trashed_at: null,
      purged_at: null,
      content_created_at: '2020-01-12T09:31:27-08:00',
      content_modified_at: '2020-01-12T09:31:27-08:00',
      created_by: [Object],
      modified_by: [Object],
      owned_by: [Object],
      shared_link: null,
      parent: [Object],
      item_status: 'active'
    }
  ]
}
```

Boxアカウントで確認すると大容量ファイルも保存されていることが確認できます。

# おわりに

~~今回BoxAPIをいくつか試していたのですが、検索APIでファイルやフォルダの検索を行うと~~
~~作成してから少し時間が経ったものでないと検索に引っかかりませんでした。~~

~~もし原因を知っている方がいれば教えていただけると助かります。~~

→インデックスの作成に時間がかかるためでした。
https://community.box.com/t5/%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%81%A8%E3%83%95%E3%82%A9%E3%83%AB%E3%83%80%E3%81%AE%E7%AE%A1%E7%90%86/%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB-%E3%83%95%E3%82%A9%E3%83%AB%E3%83%80-%E3%82%B3%E3%83%B3%E3%83%86%E3%83%B3%E3%83%84%E3%81%AE%E6%A4%9C%E7%B4%A2/ta-p/36911#search_hdsiw