34
26

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Ruby on RailsAdvent Calendar 2016

Day 16

RailsでShrineを使ってファイルをS3にダイレクトアップロードするときの注意点

Posted at

概要

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以外のフレームワークをサポート
  • プラグインにより多くのストレージをサポート
  • ストレージをcachestoreの2段階に分けることによる最適化
  • S3へのダイレクトアップロードをサポート

「ストレージ」というのはどこに実ファイルを置くのかという概念で、デフォルトではファイルシステムとS3が使えますが外部プラグインを導入すると様々な種類のストレージを使うことができます(例えばSQLなど)。
今回はS3へのダイレクトアップロードを実装したので、それの注意点を書きます。

RailsでShrineを使う

多分多くの読者は(あるいは私も)Railsを使うことが多いとおもわれるので(RodaとかHanamiとか、面白そうだけど業務で使う気にはなかなかなれない…)、まずはRailsでShrineを使う方法を軽く説明します。

基本

READMEのquick-startをコピペすればまあ動きます。…と書くだけだと味気ないので、モデル層にかかわらない設定部分だけを下記に抜粋。

Gemfile
gem 'shrine'
config/initializers/shrine.rb
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クラスを利用させたい場合(すでに名前が混乱気味ですが、公式ドキュメントからしてこうなので…)、

app/models/photo.rb
class Photo < ApplicationRecord
  Include ImageUploader[:image]
end
app/uploaders/image_uploader.rb
class ImageUploader < Shrine
end
db/schema.rb
# 抜粋
create_table :photos do |t|
  t.text :image_data
end

のようになるのですが、ややこしいことに、image_dataimageImageUploader[:image]のシンボル部分から抜き出されています。

S3ダイレクトアップロードについて

必要性

RailsアプリをHerokuで動かしている場合、画像はS3に最終的にアップロードされることになります。この場合、ファイルの大きさやインターネット回線の状況によってはRailsアプリからS3へのアップロードに失敗する可能性があります。あるいはEC2を使っている場合でも、ファイルのアップロード先はどのみちS3になる可能性が高いです。であれば、ブラウザからファイルを直接S3にアップロードすることができるならば、リソースの節約になると同時にユーザー体験の向上も見込めて一石二鳥です。

簡易実装

このドキュメントにあるように実装していきます。
勘所は以下の3つ。

Gemfile
gem 'roda'
config/initializers/shrine.rb
plugin :direct_upload
config/routes.rb
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はファイルのアップロードにPOSTPUTを用意しているのですが、上のJSONはPOSTでしか使えません。PUTでファイルをアップロードしたい場合、別の方法でpresignデータを手に入れる必要があります。
ドキュメントではDropzoneも使えると書いてあるし、事実使えたのですが、そのためには自力でpresign実装を書く必要がありました。実装の場所はサーバー側でなくても大丈夫ではあるようなのですが、サンプルに合わせる意図も含めてサーバー側で実装することにしました。

結局何が問題なのか

フロント側がどうやらjQuery-File-Uploadになっているような感じの情報しかウェブ上にないこと、これに尽きる感じです。
公式ドキュメントのみならず、チュートリアルやサンプルコードでもDropzoneを使ったものは見つかりませんでした。実装も、フォームでPOSTする以外の方法はあまり手厚くサポートされていない印象です。
とはいえ、モダンなフロントエンドだとjQueryに依存しない場合も多くなってきていますし(そういえばRails自体もjQueryへの依存をなくすのでしたね)、Reactのようにフォームを弄るのが簡単ではないようなケースもあることを考えると、もうちょっと多様な方法(というよりPUT)が簡単にできるようになっているといいなと思います。

まとめ

現時点でShrineは普通に使えるレベルの品質に達しているのではないかと思います(この問題を探るのにソースコードも結構読みましたが、依存関係も少なくコードは比較的シンプルでした)。メンテナンスもきっちりされており、現時点でファイルアップロードを実装するなら使用を検討できるでしょう(類似のコンセプトを持つRefileがあまりメンテナンスされていないのも追い風)。
しかし、モダンなフロントエンドからのS3へのダイレクトアップロードを実装する場合、案外一筋縄ではいかない、ということがありました。

34
26
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
34
26

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?