MindSphere IoT File APIの使い方

## はじめに

今回、とある案件においてMindSphereを利用した開発していたのですが、よくあるDBのようなテーブル操作・データ永続化が必要になりました。
それらしい仕組みがなかったため、今回はDBの代わりとして**IoT File API**経由でAssetにストレージされたファイルを読み書きすることにしました。
その方法についてまとめたので記事として紹介したいと思います。

## 環境

- Nuxt 2.12.2
- Composition-API
- axios
- TypeScript 3.8.3

根本的にはAPIなのでどんな言語でも構わないのですが、今回はNuxt/Vue/TypeScriptアプリからのアクセス例を紹介します。
使用するHTTPクライアントはaxiosですが、fetchでも基本的に同じです。

また契約プランによって使用できるファイルサイズは異なります。
自分の使用していたMindAccess Developer Planではファイルストレージは10GBまででした。ユーザー用テーブル代わりだったので、まず10GBを超えることはないでしょう。

エンドポイントの詳細は[公式のドキュメント](https://developer.mindsphere.io/apis/iot-iotfile/api-iotfile-api.html)を参照してください。

## ファイルストレージを使用してみる

今回は仮にファイル名が`user.json`という、何かのユーザーリストとしてのファイルを作ります

```json
[{"name": "John","age": 33}]
```

axiosを使う手前json形式にしましたが、他のHTTPクライアントを使用する場合は適切なファイル形式にしてください。

## アセットを作る

まずファイルストレージ用のAssetを作ります。このとき作るAssetは基本的に何でも構いません。すでに時系列データが入っている既存のアセットでも問題ありません。
時系列データやエージェントの違いにおいてAssetTypeやAspectは重要ですが、ファイルストレージを利用するだけならタイプはあまり関係ないようです。
今回はデフォルトで利用できるBasicAreaをAssetTypeに選んで、**FileAsset**と名付けます。

作った後は、アセットのIDを忘れずにメモしておいてください。
アセットのIDはそのアセットを特定する識別子で、アセットマネージャーからアセットを開いたときのURLから取得することができます。

例えば

```
https://{テナント名}-assetmanager.eu1.mindsphere.io/entity/5e7recbb082e41af9c1410b9d212r45b/details?selected=037cbwdct7eb4ecra4e04dea5a4dw6de
```

アセットの設定ページを開いたとき以上のようなURLの場合は、selected以下のパラメータである`037cbwdct7eb4ecra4e04dea5a4dw6de`がアセットのIDとなります。

## ファイルをアップロードする
![コメント 2020-07-22 175956.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/365619/b7cbdd77-c4c8-73e3-053a-d207840f37a5.png)

MindSphereランチパッドを開き、Fleet Managerを開きます。
左ペインからFileAssetアセットを選択してください。右にファイル一覧が表示します(最初は空ですが)。
右下の「↑」ボタンからファイルをアップロードします。正常にアップされれば

```
File has been uploaded successfully. The uploaded file may take sometime to appear in the table.
```

というトーストが画面中央下に開きます。ただしファイル一覧へは即座には反映されません。メッセージが表示されればとりあえずOKです。

## MindSphereにホストされたアプリから開く

MindSphereにホストされたアプリ(CF hosted apps)から先程アップロードした`user.json`開きます。

一例として以下のようにします。

```vue
<template>
  <div>
    {{ userList }}
  </div>
</template>
<script lang="ts">
import { ref, onMounted } from '@vue/composition-api'
import axios from 'axios'
export default {
  name: '',
  setup() {
    const userList = ref('')
    const assetId = '037cbwdct7eb4ecra4e04dea5a4dw6de'
    onMounted(async () => {
      const { data } = await axios.get(
        `/api/iotfile/v3/files/${assetId}/user.json`
      )
      userList.value = data
    })
    return {}
  }
}
</script>
<style></style>

```

変数`assetId`だけ書き換えればOKです。
これで先程のuser.jsonの内容が展開されていれば問題ありません。





## 書き込みについて

どちらかといえば書き込みのほうがやや面倒です。
IoTFileAPIでは、書き込みごとにHTTP ETagを利用します。これはリソースの特定バージョンの識別子となります。そのサーバーによって識別子は変わるようですが、MindSphereの場合はそのファイルのバージョン(更新回数)が利用されます。
そうすることで上書きをされるような「空中衝突」を未然に防ぐ目的があります。

[ETag](https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/ETag)

最初の書き込み = ファイルがない状態から新しいファイルを作る場合は不要です(むしろあるとエラーが返されます)。
既存のファイルを変更する場合はHTTPリクエストヘッダーに`if-Match`ヘッダーを入れる必要があります。

そのため書き込み前にまずEtagを取得してから、書き込みになります。一例として以下のようにします。

```typescript
  /**
   * Etagを取得する
   * @param fileName {string} ファイル名
   * @return {Promise<Number | boolean>} Etagがあれば数字、なければnullのプロミスオブジェクト
   */
  async function fetchEtag(fileName: string): Promise<Number | null> {
    const res = await BaseApi.get(
      `/api/iotfile/v3/files/${assetId}/?filter=name eq '${fileName}'`
    )
    // もし返ってきた内容の配列が空 = ファイルが存在しないならnullを、そうでなければetagを返す
    return res.data.length === 0 ? null : res.data[0].etag
  }

```

データの書き込みは以下のようにします。要求時の`Content-type`は `application/octet-stream` なのでご注意ください。

```TypeScript
  /**
   * APIから設定データ書き込み
   * @param assetId {string} アセットID
   * @param fileName {string} ファイル名
   * @param params {object} 保存する内容
   */
  async function writeFileApi(assetId:string, fileName: string, params: object) {
    const etag = await fetchEtag(fileName)

    // if-Matchヘッダーをセットする(ファイル新規作成なら空、更新ならif-Matchとetagをセット)
    const ifMatchHeader = etag === null ? {} : { 'if-Match': etag }

    const headers = {
      'Content-Type': 'application/octet-stream',
      ...ifMatchHeader
    }
    await axios.put(`/api/iotfile/v3/files/${assetId}/${fileName}`, params, {
      headers
    })
  }
```

実行する方法は一例として以下のようにします。

```typescript
const saveParams = async () => {
    const newUserList = [
        { name: 'Bob', age: 12 },
        { name: 'Alice', age: 20 }
    ]
    await writeFileApi(assetId, 'user.json', newUserList)
}
```

書き込みはIoT File APIの１つの壁だったので、何度か試して成功しました。



## 削除

これは特に難しいことはなく、ファイル名を指定してdeleteメソッドを飛ばすという方法で大丈夫です。



## 終わりに

ドキュメントによればIoT File APIの予想されるシナリオ例として、アプリケーションに付随する非本質的なもの、たとえば画像、技術説明、マニュアルなどを入れるのに使うことを想定しているようです。
それを考えるとDB的に使うというのは想定外の使用例ではないかと思いますが、ただ付随的な情報を保存するためにどうしてもこのような仕組みが必要となることもあると思うので、本記事にお役に立てれば幸甚の至りです。