外に出るねくら

~ 外に出たって結局やることは自宅と同じ ~

ファイルをS3にアップロードするときはサーバー?クライアント?

背景

クライアントサイドとサーバーサイドを分離して、JSONでデータのやり取りをするのが昨今のアプリケーションのトレンド。
クライアントサイドからファイルをアップロードするのがいいのか、サーバーサイドからファイルをアップロードするのがいいのか、どっちがいいのかとふと思いました。
最近はS3に画像データ保存することが多いので、S3にデータを保存するとしたらどうするかを考えます。

サーバーサイドからアップロードする場合

ファイル送信を行う際に取りうる選択肢

ファイル送信を行う際には、multipart/form-dataBase64 Encodeのどちらかが使われるのが一般的

multipart/form-data

HTTPリクエストのヘッダに付与されるContent-Type。
HTTPでファイル送信を行う際の王道。
大体のWebアプリケーションのフレームワークでは、この方法で送られてきたファイルをデコードする仕組みを持っている。
複合データ型であることを意味していて、1回のHTTP通信で複数の種類のデータ(text, file etc...)を扱うことができる。
バウンダリ文字列という境界を定義して、その境界を使ってリクエストボディを複数のパートに区切ってデータを送信する。

  • 良いところ
    バイナリを扱える
    王道

  • 良くないところ
    JSONベースの通信で使えない

Base64 Encode

Base64エンコードして文字列にし、JSONでサーバーへ送る方法。
Base64ってのは、全てのデータを英数字と記号(+/)で表現したもの。
なんでBase64なの?みたいな話は「base64ってなんぞ??理解のために実装してみた」って記事がいい感じでした。
multi-part/form-dataほど王道というわけではないが、GitHubTumblrなどのAPIで利用実績があり、それなりに安心して採用できる手法。

  • 良いところ
    文字列として扱えるのでJSONベースの通信と相性バッチリ
    それなりに安心して使える

  • 良くないところ
    データ容量が大きい
    エンコードとデコードのコストが高い(場合によっては自前で処理を書くかも)

取りうる選択肢

  1. multi-part/form-dataを使うなら、画像をアップロードするエンドポイントを切り出す。
    そもそもJSONで使えないし。
    その場合は、アップロードしたレスポンスでIDを返してもらって、そのIDを保存したいリソースに紐づけて再度サーバーにリクエスト投げる、って感じになるのかな。
  2. Base64 Encodeを使う。
    JSONで使えるけど、処理書くのめんどくさそう。
  3. そのリソースに関する部分だけそもそもJSONの通信やめちゃう?(← 割と一番実装楽そうだったりする)
    そのリソースに関する部分はmulti-part/form-dataでやり取りするということ。

クライアントサイドからアップロードする場合

処理は軽いだろうけど

やっぱりセキュリティのリスクあるよなぁ。
特にS3に保存するってなるとAWS Access Keyとかの取得処理をクライアントに持たないといけなくてかなりリスクあると思う。

と思ったらなんかいけそうなのある

ググったら色々出てきた。
Amazon S3 Browser Upload」とか「Vue.jsからS3に直接ファイルをアップロードする」とか。
Amazonが提供しているSDKの中に、S3への画像アップロードをサポートしてくれるやつがあるんだと。
ここからはちょっとRubyの話。
PresignedPostというのがあるらしい
AWSが提供しているSDKクラスリストを見てみるとS3にアクセスするためのキーを発行できそうな感じ。
Pre signedなので仮なんだろうなという感じはするが・・・
ただ、PresignedPostを直接使うんじゃなくて、Bucket::presigned_postを使った方がいいとも書かれている。
そちらを見てみるとBucket::presigned_postPresignedPostインスタンスを簡単に作ってくれるらしい。
expiresオプションをつけることで有効期限をつけることができるようで、要はワンタイムURLみたいなのを生成してくれるみたい。
これ使えばクライアントサイドからS3に直接アップロードでもさっきの懸念はなさそう。

クライアントサイドからアップロードする場合の処理のイメージ

サーバーでワンタイムURLを作り、クライアントに返す。
クライアントはそれを受け取って、Fileオブジェクトを乗せてS3にリクエスト。
S3からレスポンスとしてアップロード先のkeyなどの情報が入ってるので、保存したいリソースに紐づけて次はサーバーにリクエスト。
・・・
ってこれmulti-part/form-dataの処理と変わらない。
むしろS3との通信は成功したけど、サーバーとの通信失敗しましたってなったときにS3のデータとサーバーのデータで若干不整合が生じている気がする。
それが許されるならそれでいいけど。

結論

クライアントでアップロードするっていう選択肢はなさそう。
サーバーでアップロードする場合は

  1. multi-part/form-dataを使う。画像をアップロードするエンドポイントを切り出す。
    これはクライアントからアップロードする場合と同じ問題を孕んでいる気がする。っていうかそう。
    画像はアップロードできたけど、リソースの保存リクエスト失敗しましたって、S3がサーバーに変わっただけで構造的には同じ。
  2. Base64 Encodeを使う。
    最初はめんどくさいけどエンコード/デコードの処理は一度作れば使いまわせるはず。
    そしたら何も考えずJSONでデータのやり取りができるので幸せになれそう。
  3. そのリソースに関する部分だけそもそもJSONの通信やめちゃう。
    最初楽できそう。だけど同じあとで苦労しそう。

悩むなら2と3。
時間との相談な気がする。

なんか妙な結論になりましたが、本日はここまで。 それぞれのパターンで実装してみようかな。