RailsとShrineで作る柔軟なファイルアップロード機能の実装ガイド
Ruby on Railsには、CarrierWave、Paperclip、Dragonflyなど、数多くのファイルアップロード用gemが存在します。それぞれに特徴があり、少なくともひとつは使ったことがある方も多いのではないでしょうか。
しかし今回は、Janko Marohnić氏によって開発された比較的新しいながらも非常に優れたソリューション「Shrine」をご紹介します。他の類似gemと大きく異なるのは、そのモジュール方式の設計です。Shrineではすべての機能が個別のモジュール(Shrineの用語では「プラグイン」)として提供されており、「バリデーションが必要ならプラグインを追加」「画像処理をしたいならプラグインを追加」というように、必要な機能だけを自由に組み合わせられます。モデルごとに利用可能な機能を細かく制御できるこのアプローチは、まさにエレガントと言えるでしょう。
本記事では、以下の内容を順番に解説していきます。
- RailsアプリケーションへのShrineの組み込み
- グローバル設定とアップローダー単位での設定方法
- ファイルアップロード機能の追加
- 画像の加工処理(リサイズなど)
- バリデーションルールの設定
- メタデータの保存とAmazon S3を使ったクラウドストレージの活用
本記事のソースコードはGitHubで公開されており、動作するデモも用意されています。
Shrineの導入
まずは、テストスイートなしで新しいRailsアプリケーションを作成しましょう。
rails new FileGuru -T
本記事のデモではRails 5を使用しますが、ここで紹介する概念のほとんどはRails 3や4でも同様に適用できます。
GemfileにShrineを追加します。
gem "shrine"
続いて、以下のコマンドを実行してください。
bundle install
次に、Photoという名前のモデルを作成します。Shrineはファイルに関するすべての情報を、_dataという接尾辞が付いた特殊なテキストカラムに保存します。対応するマイグレーションを作成して適用しましょう。
rails g model Photo title:string image_data:text rails db:migrate
古いバージョンのRailsでは、マイグレーションコマンドは次のようになります。
rake db:migrate
初期設定とプラグイン
Shrineの設定オプションは、グローバルとモデル単位の両方で指定できます。グローバル設定はもちろんイニシャライザーファイル内で行います。ここでは必要なファイルとプラグインを読み込みます。
プラグインとは、Shrineの機能を独立したモジュールとして切り出したものです。バリデーション、画像処理、添付ファイルのキャッシュなど、さまざまなプラグインが用意されており、必要なものだけを選んで有効化できます。
まずは2つのプラグインを追加します。ひとつはActiveRecordサポート、もうひとつはロギングです。どちらもグローバルに読み込みます。あわせてファイルシステムストレージも設定しておきましょう。
config/initializers/shrine.rb
require "shrine"
require "shrine/storage/file_system"
Shrine.plugin :activerecord
Shrine.plugin :logging, logger: Rails.logger
Shrine.storages = {
cache: Shrine::Storage::FileSystem.new("public", prefix: "uploads/cache"),
store: Shrine::Storage::FileSystem.new("public", prefix: "uploads/store"),
}
ロガーを有効にすると、コンソールにファイル処理にかかった時間などのデバッグ情報が出力されるようになり、非常に便利です。
2015-10-09T20:06:06.676Z #25602: STORE[cache] ImageUploader[:avatar] User[29543] 1 file (0.1s) 2015-10-09T20:06:06.854Z #25602: PROCESS[store]: ImageUploader[:avatar] User[29543] 1-3 files (0.22s) 2015-10-09T20:06:07.133Z #25602: DELETE[destroyed]: ImageUploader[:avatar] User[29543] 3 files (0.07s)
アップロードされたファイルはすべてpublic/uploadsディレクトリ内に保存されます。これらのファイルをGitで管理したくないので、除外設定をしておきましょう。
.gitignore
public/uploads
続いて、モデル固有の設定をホストするための特別な「アップローダー」クラスを作成します。現時点では空のクラスで構いません。
models/image_uploader.rb
class ImageUploader < Shrine end
最後に、このクラスをPhotoモデルに組み込みます。
models/photo.rb
include ImageUploader[:image]
[:image]は、フォーム構築時に使用される仮想属性を追加します。上記のコードは、次のように書き換えることもできます。
include ImageUploader.attachment(:image) # または include ImageUploader::Attachment.new(:image)
これでモデルにShrineの機能が搭載されました。次のステップに進みましょう。
コントローラー・ビュー・ルーティングの準備
このデモでは、写真を管理するためのコントローラーを1つだけ用意します。indexページをルートとして設定します。
photos_controller.rb
class PhotosController < ApplicationController
def index
@photos = Photo.all
end
end
ビューは次のとおりです。
views/photos/index.html.erb
<h1>Photos</h1> <%= link_to 'Add Photo', new_photo_path %> <%= render @photos %>
@photos配列をレンダリングするには、パーシャルが必要です。
views/photos/_photo.html.erb
<div>
<% if photo.image_data? %>
<%= image_tag photo.image_url %>
<% end %>
<p><%= photo.title %> | <%= link_to 'Edit', edit_photo_path(photo) %></p>
</div>
image_data?はActiveRecordによって提供されるメソッドで、レコードが画像を持っているかどうかを判定します。
image_urlはShrineのメソッドで、元画像へのパスを返します。実際には小さなサムネイルを表示する方が望ましいですが、それは後ほど対応します。
必要なルーティングをすべて追加します。
config/routes.rb
resources :photos, only: [:new, :create, :index, :edit, :update] root 'photos#index'
これで下準備は完了です。いよいよ本題に入りましょう!
ファイルアップロードの実装
このセクションでは、実際にファイルをアップロードする機能を追加します。コントローラーのアクションはとてもシンプルです。
photos_controller.rb
def new
@photo = Photo.new
end
def create
@photo = Photo.new(photo_params)
if @photo.save
flash[:success] = 'Photo added!'
redirect_to photos_path
else
render 'new'
end
end
注意点はひとつだけです。ストロングパラメーターでは、image_dataではなくimageという仮想属性を許可する必要があります。
photos_controller.rb
private
def photo_params
params.require(:photo).permit(:title, :image)
end
newビューを作成します。
views/photos/new.html.erb
<h1>Add photo</h1> <%= render 'form' %>
フォームのパーシャルもごくシンプルです。
views/photos/_form.html.erb
<%= form_for @photo do |f| %> <%= render "shared/errors", object: @photo %> <%= f.label :title %> <%= f.text_field :title %> <%= f.label :image %> <%= f.file_field :image %> <%= f.submit %> <% end %>
ここでも繰り返しになりますが、image_dataではなくimage属性を使用している点に注目してください。
最後に、エラー表示用のパーシャルをもうひとつ追加します。
views/shared/_errors.html.erb
<% if object.errors.any? %>
<h3>The following errors were found:</h3>
<ul>
<% object.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
<% end %>
以上で基本の実装は完了です。すでに画像のアップロードを試せる状態になっています。
バリデーションの追加
ただし、デモアプリを完成させるにはまだやるべきことがあります。最大の問題は、ユーザーが任意の種類・任意のサイズのファイルをアップロードできてしまう点です。これは好ましくありませんよね。そこで、バリデーションをサポートするプラグインを追加しましょう。
config/initializers/shrine.rb
Shrine.plugin :validation_helpers
ImageUploaderにバリデーションロジックを設定します。
models/image_uploader.rb
Attacher.validate do
validate_max_size 1.megabyte, message: "is too large (max is 1 MB)"
validate_mime_type_inclusion ['image/jpg', 'image/jpeg', 'image/png']
end
ここでは、1MB未満のJPGおよびPNG画像のみアップロードを許可しています。ルールはプロジェクトに合わせて自由に調整してください。
MIMEタイプの正確な判定
もうひとつ重要なポイントがあります。デフォルトでは、ShrineはHTTPヘッダーのContent-Typeに基づいてファイルのMIMEタイプを判定します。しかし、このヘッダーはブラウザーが送信するもので、ファイル拡張子から推測された値にすぎません。そのため、必ずしも信頼できるとは限りません。
ファイルの中身に基づいてMIMEタイプを判定したい場合は、determine_mime_typeプラグインを使用します。他のモデルではこの機能が不要かもしれないため、今回はアップローダークラス側に組み込みます。
models/image_uploader.rb
plugin :determine_mime_type
このプラグインは、デフォルトでLinuxのfileユーティリティを使用します。
入力中の画像のキャッシュ保持
現在の実装では、ユーザーが誤ったデータでフォームを送信すると、フォームはエラーメッセージ付きで再表示されます。しかし問題なのは、選択済みの画像が失われてしまうことです。ユーザーは再度ファイルを選び直さなければなりません。幸い、cached_attachment_dataプラグインを使えば簡単に解決できます。
models/image_uploader.rb
plugin :cached_attachment_data
あとはフォームに隠しフィールドを追加するだけです。
views/photos/_form.html.erb
<%= f.hidden_field :image, value: @photo.cached_image_data %> <%= f.label :image %> <%= f.file_field :image %>
写真の編集機能
これで画像のアップロードはできるようになりましたが、編集機能がまだありません。すぐに実装してしまいましょう。対応するコントローラーのアクションはごく標準的なものです。
photos_controller.rb
def edit
@photo = Photo.find(params[:id])
end
def update
@photo = Photo.find(params[:id])
if @photo.update_attributes(photo_params)
flash[:success] = 'Photo edited!'
redirect_to photos_path
else
render 'edit'
end
end
先ほど作成した同じ_formパーシャルを再利用します。
views/photos/edit.html.erb
<h1>Edit Photo</h1> <%= render 'form' %>
しかしこれだけでは不十分です。ユーザーはまだアップロード済みの画像を削除できません。そこで、またしてもプラグインの出番です。
models/image_uploader.rb
plugin :remove_attachment
このプラグインは:remove_imageという仮想属性を使用するため、コントローラーで許可しておきます。
photos_controller.rb
def photo_params
params.require(:photo).permit(:title, :image, :remove_image)
end
そして、レコードに添付ファイルが存在する場合にのみ、削除用チェックボックスを表示します。
views/photos/_form.html.erb
<% if @photo.image_data? %>
Remove attachment: <%= f.check_box :remove_image %>
<% end %>
サムネイル画像の生成
現状では元画像をそのまま表示していますが、プレビュー用途としては最善とは言えません。写真によってはサイズが大きく、画面スペースを占有しすぎる可能性があります。CSSのwidthやheightで縮小表示することも考えられますが、それも良策ではありません。スタイルで見た目を小さくしても、ユーザーは結局元の大きなファイルをダウンロードすることになるからです。
そこで、初回アップロード時にサーバー側で小さなプレビュー画像を生成するのがベストです。これには2つのプラグインと2つの追加gemが必要になります。まずはgemを導入しましょう。
gem "image_processing" gem "mini_magick", ">= 4.3.5"
image_processingは、Shrineの作者自身が開発したgemで、画像操作のための高レベルなヘルパーメソッドを提供します。このgemは内部でmini_magick(ImageMagickのRubyラッパー)に依存しています。つまり、このデモを動かすにはシステムにImageMagickがインストールされている必要があります。
新しいgemをインストールします。
bundle install
続いて、依存関係とともにプラグインを読み込みます。
models/image_uploader.rb
require "image_processing/mini_magick"
class ImageUploader < Shrine
include ImageProcessing::MiniMagick
plugin :processing
plugin :versions
# 他のコード...
end
processingプラグインは、画像の縮小・回転・フォーマット変換などの操作を可能にします。またversionsプラグインにより、ひとつの画像を複数のバリアントとして持てるようになります。本デモでは「original」と「thumb」(300x300にリサイズ)の2つのバージョンを保存します。
画像を処理して2つのバージョンを保存するコードは次のとおりです。
models/image_uploader.rb
class ImageUploader < Shrine
process(:store) do |io, context|
{ original: io, thumb: resize_to_limit!(io.download, 300, 300) }
end
end
resize_to_limit!はimage_processing gemが提供するメソッドです。画像が300x300より大きければ縮小し、小さければ何もしません。さらに、元のアスペクト比は維持されます。
画像を表示する際は、image_urlメソッドに:originalまたは:thumb引数を渡すだけでOKです。
views/photos/_photo.html.erb
<div>
<% if photo.image_data? %>
<%= image_tag photo.image_url(:thumb) %>
<% end %>
<p><%= photo.title %> | <%= link_to 'Edit', edit_photo_path(photo) %></p>
</div>
フォーム内でも同様に行えます。
views/photos/_form.html.erb
<% if @photo.image_data? %>
<%= image_tag @photo.image_url(:thumb) %>
Remove attachment: <%= f.check_box :remove_image %>
<% end %>
なお、アップロード完了後に処理用の一時ファイルを自動的に削除したい場合は、delete_rawプラグインを追加します。
models/image_uploader.rb
plugin :delete_raw
画像のメタデータ取得
画像の表示だけでなく、そのメタデータを取得することもできます。例として、元画像のファイルサイズとMIMEタイプを表示してみましょう。
views/photos/_photo.html.erb
<div>
<% if photo.image_data? %>
<%= image_tag photo.image_url(:thumb) %>
<p>
Size <%= photo.image[:original].size %> bytes<br>
MIME type <%= photo.image[:original].mime_type %><br>
</p>
<% end %>
<p><%= photo.title %> | <%= link_to 'Edit', edit_photo_path(photo) %></p>
</div>
では、画像の寸法(幅と高さ)はどうでしょうか?残念ながらデフォルトでは保存されませんが、store_dimensionsプラグインを使えば取得可能です。
画像の寸法情報
store_dimensionsプラグインはfastimage gemに依存しているため、まずはそれを導入します。
gem 'fastimage'
忘れずに実行してください。
bundle install
あとはプラグインを読み込むだけです。
models/image_uploader.rb
plugin :store_dimensions
そして、widthメソッドとheightメソッドを使って寸法を表示します。
views/photos/_photo.html.erb
<div>
<% if photo.image_data? %>
<%= image_tag photo.image_url(:thumb) %>
<p>
Size <%= photo.image[:original].size %> bytes<br>
MIME type <%= photo.image[:original].mime_type %><br>
Dimensions <%= "#{photo.image[:original].width}x#{photo.image[:original].height}" %>
</p>
<% end %>
<p><%= photo.title %> | <%= link_to 'Edit', edit_photo_path(photo) %></p>
</div>
また、幅と高さを配列で返すdimensionsメソッドもあります(例:[500, 750])。
クラウドストレージへの移行(Amazon S3)
アップロードファイルのホスティング先としてクラウドサービスを選ぶ開発者は多く、Shrineも当然その可能性を提供しています。このセクションでは、ファイルをAmazon S3へアップロードする方法を紹介します。
最初のステップとして、Gemfileに2つのgemを追加します。
gem "aws-sdk", "~> 2.1"
group :development do
gem 'dotenv-rails'
end
aws-sdkはS3 SDKとの連携に必要であり、dotenv-railsは開発環境における環境変数の管理に使用します。
bundle install
作業を進める前に、S3へAPI経由でアクセスするためのキーペアを取得しておきましょう。AWSコンソールにサインイン(または新規登録)し、Security Credentials > Usersへ移動します。S3上のファイルを操作できる権限を持つユーザーを作成してください。以下はS3へのフルアクセスを許可するシンプルなポリシーの例です。
{
"Version": "2016-11-14",
"Statement": [
{
"Effect": "Allow",
"Action": "s3:*",
"Resource": "*"
}
]
}
作成したユーザーのキーペアをダウンロードしてください。ルートアカウントのアクセスキーを使うことも技術的には可能ですが、セキュリティ上のリスクが非常に高いため強く非推奨です。
次に、ファイルを格納するS3バケットを作成し、プロジェクトのルートに設定ファイルを配置します。
.env
S3_KEY=YOUR_KEY S3_SECRET=YOUR_SECRET S3_BUCKET=YOUR_BUCKET S3_REGION=YOUR_REGION
このファイルは絶対に公開してはいけません。Gitの除外設定にも必ず追加しておきましょう。
.gitignore
.env
続いて、Shrineのグローバル設定を修正し、新しいストレージを定義します。
config/initializers/shrine.rb
require "shrine"
require "shrine/storage/s3"
s3_options = {
access_key_id: ENV['S3_KEY'],
secret_access_key: ENV['S3_SECRET'],
region: ENV['S3_REGION'],
bucket: ENV['S3_BUCKET'],
}
Shrine.storages = {
cache: Shrine::Storage::FileSystem.new("public", prefix: "uploads/cache"),
store: Shrine::Storage::S3.new(prefix: "store", **s3_options),
}
これで完了です!アプリケーションの他の部分には一切変更を加える必要がなく、すぐに新しいストレージをテストできます。もしS3からキー不正のエラーが返される場合は、キーとシークレットを正確にコピーできているか、末尾に余分な空白や不可視の特殊文字が混入していないかを確認してください。
まとめ
本記事は以上となります。ここまで読んでいただければ、Shineを使う自信がつき、自分のプロジェクトでもぜひ活用してみたいと思っていただけたのではないでしょうか。本記事では多くの機能を取り上げましたが、Shrineにはまだまだ魅力的な機能があります。たとえば、ファイルと一緒に追加のコンテキスト情報を保存する機能や、ダイレクトアップロードの仕組みなどです。
ぜひShrineの公式ドキュメントや公式サイトも参照してみてください。利用可能なすべてのプラグインが詳しく解説されています。このgemについて他に疑問がある場合は、遠慮なく質問を投稿してください。最後までお読みいただきありがとうございました。またお会いしましょう!
-
RailsでTailwind CSSを使う方法|導入から実践的なスタイリングまで徹底解説
CSSは魔法のような存在ですが、同時に時間のかかる作業でもあります。美しく、機能的で、アクセシブルなサイトは使っていて心地よいものですが、自分でCSSを一から書くのは骨の折れる仕事です。近年はBootstrapをはじめとする多くのCSSフレームワークが登場し、その中でもTailwind CSSは特に注目を集めています。 RailsにはTailwindが標準搭載されていませんが、この記事では新しいRuby on RailsプロジェクトにTailwind CSSを追加する方法を解説します。これにより、デザイン実装にかかる時間を大幅に節約できるでしょう。さらに、Tailwindのユーティリティクラス
-
Rails5でのAngularの使用
あなたは前にその話を聞いたことがあります。分散型で完全に機能するバックエンドAPIと、通常のツールセットで作成されたフロントエンドで実行されているアプリケーションがすでにあります。 次に、Angularに移動します。または、AngularをRailsプロジェクトと統合する方法を探しているだけかもしれません。これは、この方法を好むためです。私たちはあなたを責めません。 このようなアプローチを使用すると、両方の世界を活用して、たとえばRailsとAngularのどちらの機能を使用してフォーマットするかを決定できます。 構築するもの 心配する必要はありません。このチュートリアルは、この目的のた