WebAPI でファイルをアップロードする方法アレコレ

WebAPI を開発していると、ファイルを扱いたい場面に出くわすこともあると思います。
ただ、いざ WebAPI にファイルアップロードの仕組みを入れようとすると、いまいちしっくり来る方法がわからず、悩んだりするのではないでしょうか。
今回は実サービスの例を踏まえつつ、どのような方法が使われていて、何がベストプラクティスなのかを検討しようと思います。

# ファイルの送信方法

モダンな RESTful WebAPI では、だいたい JSON でデータがやりとりされていると思いますが、この中にファイルの概念を入れようとすると、特に Input（ファイル送信）のやり方に悩むのではないかと思います。
世の中の WebAPI をざっと見てみると、ファイル送信については主に「multipart/form-data」と「Base64 エンコード」のどちらかが使われていることがわかります。
では、それぞれの特徴を見ていきましょう。

## multipart/form-data

これは HTTP でのファイル送信の王道といえます。[RFC2388](https://www.ietf.org/rfc/rfc2388.txt) でも定義されていて、ほとんどの Web アプリケーションフレームワークでは、この手法で送られてきたファイルをデコードする仕組みを持っています。
そもそもは HTML フォームからファイルを送信する際のエンコード手法として定義されていて、ファイルデータ以外にも様々なデータを付随させることができます。
「バウンダリ文字列」という境界線を定義して、それでリクエストボディをいくつかのパートに区切り、データを送信します。
バイナリを扱えるというメリットがあるのですが、JSON ベースの API との親和性が取りづらい、というデメリットもあります。

### 送信例

以下のようなリクエストボディを構築します。

```
------------XnJLe9ZIbbGUYtzPQJ16u1
Content-Disposition: form-data; name="title"

title1
------------XnJLe9ZIbbGUYtzPQJ16u1
Content-Disposition: form-data; name="description"

description1
------------XnJLe9ZIbbGUYtzPQJ16u1
Content-Disposition: form-data; name="stream_id"

1019900359
------------XnJLe9ZIbbGUYtzPQJ16u1
Content-Disposition: form-data; name="item_images[]"; filename="sample.png"
Content-Type: image/png
Content-Length: 4323

{ Put binary contents that you want to upload }
```

（[tab API](http://tonchidot.github.io/tab-api-docs/api/item/create_new_item.html) の送信サンプルより引用）

## Base64

ファイルのデータを Base64 エンコードして文字列化したものを、JSON につめて送信する手法です。
特に RFC で定義されているわけではなく、これといった正統なやり方があるわけではないのですが、大手サービスの WebAPI での採用実績もあり、安心して使える手法ではあります。
文字列化して扱えるため、JSON ベースの API と親和性が取れるメリットがありますが、データ容量がかさむのと、エンコード／デコードのコストがかかるというデメリットがあります。

### 送信例

以下のようなリクエストボディを構築します。

```json
{
 "asset": {
    "key": "example.gif",
    "attachment": "TWFuIGlzIGRpc3Rpbmd1aXNoZWQsIG5vdCBvbmx5IGJ5IGhpcyByZWFzb24sIGJ1dCBieSB0aGlz
IHNpbmd1bGFyIHB
dWVkIGFuZCBpbmRlZmF0aWdhYmxlIGdlbmVyYXRpb24gb2Yga25vd2xlZGdlLCBleGNlZWRzIHRo
ZSBzaG9ydCB2ZWhlbWVuY2Ugb2YgYW55IGNhcm5hbCBwbGVhc3VyZS4="
  }
}
```

# ファイルの保存方法

送られてきたファイルの保存方法に関してはどうでしょう。
こちらも、世の中の WebAPI を見てみると、「リソースに直接ひも付ける（= リソースの一部として保存する）」パターンと、「汎用的なストレージに入れる（= 1つのリソースとして保存する）」パターンにわかれることがわかります。
では、それぞれの特徴を見てみましょう。

## リソース紐付けパターン

特定リソースの特定アトリビュートに対してファイルデータを直接送る方式です。
RESTful API ではほとんどの場合、送信するファイルデータは何かしらのリソースに紐づくはずなので、直感的といえば直感的です。
逆に、ファイルをいくつかのリソースで使いまわしたい場合には向いていない手法といえます。

## 汎用ファイルアップロードパターン

汎用的なファイルアップロードのエンドポイントを作る方式です。
リソース紐付けパターンとは違って、アップロードしたファイルを1つのリソースとして汎用的に扱いたい場合に有効な手法です。
このパターンでアップロードしたファイルデータをあるリソースと紐付ける場合は、ファイルのID や URL、 ストレージ上のパスなどを使ってリソースの特定アトリビュートを更新する必要があります。

# 実サービスの例

## GitHub API

- Contents API という汎用ファイルアップロードのエンドポイントがあります
- これを使うことで、「コメントエリアにファイルをドラッグアンドドロップしてそのままマークダウンに落とし込む」みたいな挙動を実現していると思われます
- Base64 でデータを送信します

## Cybozu API

- 汎用ファイルアップロードのエンドポイントがあります
- アップロード完了すると「ファイルキー」が返ってきて、これを使って該当するアトリビュートを更新します
- アトリビュートとのひも付きがないファイルは3日間で消去されます
- multipart/form-data でデータを送信します

## Tumblr API

- リソース紐付き形式のファイルアップロードです
- Base64 のほか、URL-encoded binary、URL 形式でデータを送信できます

## tab API

- リソース紐付き形式のファイルアップロードです
- multipart/form-data でデータを送信します

# まとめ

ファイルの送信方法と保存方法には以下のパターンがあり、API の性質やファイルの用途に応じて、これらを適切に組み合わせましょう。

- ファイル送信方法
    - multipart/form-data
    - Base64
- ファイル保存方法
    - リソース紐付け
    - 汎用ファイルアップロード

# 参考リンク

- [GitHub Contents API の仕様](https://developer.github.com/v3/repos/contents/)
- [Cybozu API ファイルアップロード仕様](https://cybozudev.zendesk.com/hc/ja/articles/201941824)
- [Tumblr API の仕様](https://www.tumblr.com/docs/en/api/v2)
- [Tab API の仕様](http://tonchidot.github.io/tab-api-docs/api/item/create_new_item.html)
- 似たような議論がされている StackOverflow
    - [rest \- Posting a File and Data to RESTful WebService as JSON \- Stack Overflow](http://stackoverflow.com/questions/4083702/posting-a-file-and-data-to-restful-webservice-as-json)