概要
RailsでShrineを使ってファイルをS3にダイレクトアップロードするときの注意点あれこれ。
それとそもそもShrineって何っていう人のための軽い紹介とか。
TL;DR
Shrine結構いいぞ。
Shrineそのものについての解説
まずはShrineというgem自体についてです。
概略
http://shrinerb.com
https://github.com/janko-m/shrine
Shrineは
File Attachment toolkit for Ruby applications
です。要はRubyアプリケーションにおいてファイルを何かにアタッチ(追加)するために使えるツールキットです。
執筆時点でのバージョンはv2.5.0
ですが、ほぼ毎月のようにバージョンアップが行われているのでご利用の際は最新版のバージョンを確認するようにしてください。
特徴としてはコアを小さく保って多くの(ほとんど基本的と呼べる)機能をプラグインに分離していることです。
作者
作者のJanko Marohnić氏(janko-m)はもともとRefileというgemに積極的にコントリビュートしてコアチームに招かれてもいたのが、バックグラウンド処理ができないために新しくShrineを作ることにしたようです。
思想
どうやら作者はRodaの大ファンのようで、RodaとRefileからかなり影響を受けているようです。その結果、Shrineは概略でも述べたように、ごく小さいコアと豊富なプラグインというアーキテクチャを持つことになりました。
この記事では既存のファイルアップロード系gemへの不満点(あくまで彼なりの)が詳述されているので、こちらを読むと思想背景がよりよくわかると思います。
機能
いわゆるファイルアップロードに関する機能はだいたい網羅されているのですが、あえて特徴的な機能を挙げるとすれば以下のようになります。
- Rails以外のフレームワークをサポート
- プラグインにより多くのストレージをサポート
- ストレージを
cache
とstore
の2段階に分けることによる最適化 - S3へのダイレクトアップロードをサポート
「ストレージ」というのはどこに実ファイルを置くのかという概念で、デフォルトではファイルシステムとS3が使えますが外部プラグインを導入すると様々な種類のストレージを使うことができます(例えばSQLなど)。
今回はS3へのダイレクトアップロードを実装したので、それの注意点を書きます。
RailsでShrineを使う
多分多くの読者は(あるいは私も)Railsを使うことが多いとおもわれるので(RodaとかHanamiとか、面白そうだけど業務で使う気にはなかなかなれない…)、まずはRailsでShrineを使う方法を軽く説明します。
基本
READMEのquick-startをコピペすればまあ動きます。…と書くだけだと味気ないので、モデル層にかかわらない設定部分だけを下記に抜粋。
gem 'shrine'
require "shrine"
require "shrine/storage/file_system"
Shrine.storages = {
cache: Shrine::Storage::FileSystem.new("public", prefix: "uploads/cache"),
store: Shrine::Storage::FileSystem.new("public", prefix: "uploads/store"),
}
Shrine.plugin :activerecord
注意すべきは最後の.plugin
メソッドで、アプリケーション全体で使用したいプラグインはここでまとめて設定するといいようです。逆に、モデルローカルで使用したいプラグインは該当するUploaderクラス内でplugin
メソッドを呼べばいいと思われますが、ひとまずはプラグインの設定はここに書けばいいでしょう。
DBスキーマ
ここはShrineの中でも数少ない融通が効かない部分で、「利用するUploaderが持つAttachmentに渡した引数の文字列+"_data"」という名前のtext型カラムを作成する必要があります。「???」となったあなた、あなたは正しい。
例えばPhoto
クラスにImageUploader
クラスを利用させたい場合(すでに名前が混乱気味ですが、公式ドキュメントからしてこうなので…)、
class Photo < ApplicationRecord
Include ImageUploader[:image]
end
class ImageUploader < Shrine
end
# 抜粋
create_table :photos do |t|
t.text :image_data
end
のようになるのですが、ややこしいことに、image_data
のimage
はImageUploader[:image]
のシンボル部分から抜き出されています。
S3ダイレクトアップロードについて
必要性
RailsアプリをHerokuで動かしている場合、画像はS3に最終的にアップロードされることになります。この場合、ファイルの大きさやインターネット回線の状況によってはRailsアプリからS3へのアップロードに失敗する可能性があります。あるいはEC2を使っている場合でも、ファイルのアップロード先はどのみちS3になる可能性が高いです。であれば、ブラウザからファイルを直接S3にアップロードすることができるならば、リソースの節約になると同時にユーザー体験の向上も見込めて一石二鳥です。
簡易実装
このドキュメントにあるように実装していきます。
勘所は以下の3つ。
gem 'roda'
plugin :direct_upload
Rails.application.routes.draw do
mount ImageUploader::UploadEndpoint => "/images"
end
最後が重要で、こう書くとGET /images/cache/presign
というルーティングが生成されます。このURLを叩くと、フロント側でS3にファイルをアップロードするのに必要な認証情報その他を含んだJSONが返ってきます。
簡易実装の問題点
あとはフロント側の実装だけ…なのですが、サンプルコードを眺めてみると何やら怪しい感じがします。
# GET /images/cache/presign
{
"url" => "https://my-bucket.s3-eu-west-1.amazonaws.com",
"fields" => {
"key" => "b7d575850ba61b44c8a9ff889dfdb14d88cdc25f8dd121004c8",
"policy" => "eyJleHBpcmF0aW9uIjoiMjAxNS0QwMToxMToyOVoiLCJjb25kaXRpb25zIjpbeyJidWNrZXQiOiJzaHJpbmUtdGVzdGluZyJ9LHsia2V5IjoiYjdkNTc1ODUwYmE2MWI0NGU3Y2M4YTliZmY4OGU5ZGZkYjE2NTQ0ZDk4OGNkYzI1ZjhkZDEyMTAwNGM4In0seyJ4LWFtei1jcmVkZW50aWFsIjoiQUtJQUlKRjU1VE1aWlk0NVVUNlEvMjAxNTEwMjQvZXUtd2VzdC0xL3MzL2F3czRfcmVxdWVzdCJ9LHsieC1hbXotYWxnb3JpdGhtIjoiQVdTNC1ITUFDLVNIQTI1NiJ9LHsieC1hbXotZGF0ZSI6IjIwMTUxMDI0VDAwMTEyOVoifV19",
"x-amz-credential" => "AKIAIJF55TMZYT6Q/20151024/eu-west-1/s3/aws4_request",
"x-amz-algorithm" => "AWS4-HMAC-SHA256",
"x-amz-date" => "20151024T001129Z",
"x-amz-signature" => "c1eb634f83f96b69bd675f535b3ff15ae184b102fcba51e4db5f4959b4ae26f4"
}
}
$.getJSON('/images/cache/presign', options, function(result) {
data.formData = result['fields'];
data.url = result['url'];
data.paramName = 'file';
data.submit();
});
上のサンプルはjQuery-File-Uploadの例なのですが、data
はFormらしい何かであることがわかります(正確にはFormそのものではないようです。出処はjQuery-File-Uploadのコールバック関数の第2引数です)。
S3はファイルのアップロードにPOST
とPUT
を用意しているのですが、上のJSONはPOST
でしか使えません。PUT
でファイルをアップロードしたい場合、別の方法でpresignデータを手に入れる必要があります。
ドキュメントではDropzoneも使えると書いてあるし、事実使えたのですが、そのためには自力でpresign実装を書く必要がありました。実装の場所はサーバー側でなくても大丈夫ではあるようなのですが、サンプルに合わせる意図も含めてサーバー側で実装することにしました。
結局何が問題なのか
フロント側がどうやらjQuery-File-Uploadになっているような感じの情報しかウェブ上にないこと、これに尽きる感じです。
公式ドキュメントのみならず、チュートリアルやサンプルコードでもDropzoneを使ったものは見つかりませんでした。実装も、フォームでPOSTする以外の方法はあまり手厚くサポートされていない印象です。
とはいえ、モダンなフロントエンドだとjQueryに依存しない場合も多くなってきていますし(そういえばRails自体もjQueryへの依存をなくすのでしたね)、Reactのようにフォームを弄るのが簡単ではないようなケースもあることを考えると、もうちょっと多様な方法(というよりPUT
)が簡単にできるようになっているといいなと思います。
まとめ
現時点でShrineは普通に使えるレベルの品質に達しているのではないかと思います(この問題を探るのにソースコードも結構読みましたが、依存関係も少なくコードは比較的シンプルでした)。メンテナンスもきっちりされており、現時点でファイルアップロードを実装するなら使用を検討できるでしょう(類似のコンセプトを持つRefileがあまりメンテナンスされていないのも追い風)。
しかし、モダンなフロントエンドからのS3へのダイレクトアップロードを実装する場合、案外一筋縄ではいかない、ということがありました。