RailsとShrineで実現する柔軟なファイルアップロードの完全ガイド
Ruby on Railsには、CarrierWave、Paperclip、Dragonflyなど、数多くのファイルアップロード用gemが存在します。それぞれに特徴があり、少なくとも1つは使ったことがある方も多いのではないでしょうか。
本記事では、Janko Marohnić氏によって開発された比較的新しいながらも非常に優れたソリューション「Shrine」をご紹介します。他の類似gemと異なるのは、そのモジュール方式の設計です。すべての機能が個別のモジュール(Shrineでは「プラグイン」と呼ばれます)として提供されています。バリデーションが必要ならプラグインを追加、画像処理が必要ならプラグインを追加——この柔軟なアプローチにより、モデルごとに必要な機能だけを自由に組み合わせられる点が大きな魅力です。
本記事で学べる内容は以下のとおりです。
- RailsアプリケーションへのShrineの導入方法
- グローバル設定とアップローダー単位の設定
- ファイルアップロード機能の実装
- ファイルの加工(リサイズなど)
- バリデーションルールの追加
- メタデータの保存とAmazon S3を使ったクラウドストレージの活用
なお、本記事のソースコードはGitHubで公開されており、動作するデモも用意されています。
Shrineの導入
まずは、デフォルトのテストスイートなしで新しいRailsアプリケーションを作成しましょう。
rails new FileGuru -T
このデモではRails 5を使用しますが、ここで紹介する概念のほとんどはRails 3や4でも同様に適用できます。
GemfileにShrine gemを追加します。
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つのプラグインを追加します。1つはActiveRecordのサポート、もう1つはロギングの設定です。これらはグローバルに読み込まれます。あわせてファイルシステムストレージも設定しましょう。
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
唯一の注意点は、Strong Parametersでは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タイプの判定
もう1つ重要なポイントがあります。デフォルトでは、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は、ImageMagickのRubyラッパーであるmini_magickに依存しています。つまり、このデモを実行するにはシステムにImageMagickがインストールされている必要があります。
新しいgemをインストールします。
bundle install
次に、依存関係とともにプラグインを読み込みます。
models/image_uploader.rb
require "image_processing/mini_magick"
class ImageUploader < Shrine
include ImageProcessing::MiniMagick
plugin :processing
plugin :versions
# other code...
end
processingプラグインは、画像の縮小・回転・フォーマット変換などの操作を可能にします。そしてversionsプラグインにより、1つの画像を複数のバリアントとして保持できます。このデモでは、「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引数を渡すだけです。
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])。
クラウドストレージへの移行
開発者はアップロードファイルのホスティングにクラウドサービスを選ぶことが多く、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経由でアクセスするためのキーペアを取得する必要があります。Amazon Web Servicesコンソールにサインイン(またはサインアップ)し、Security Credentials > Usersへ移動してください。S3上のファイルを操作する権限を持つユーザーを作成します。以下はS3へのフルアクセスを許可するシンプルなポリシーの例です。
{
"Version": "2016-11-14",
"Statement": [
{
"Effect": "Allow",
"Action": "s3:*",
"Resource": "*"
}
]
}
作成したユーザーのキーペアをダウンロードしてください。rootアクセスキーを使うことも可能ですが、セキュリティ上のリスクが非常に高いため、強く非推奨とします。
次に、ファイルをホストするための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からキーが正しくないというエラーが返される場合は、キーとシークレットを末尾の空白や不可視の特殊文字を含めずに正確にコピーしたか確認してください。
まとめ
本記事は以上です。ここまで読んでいただければ、Shrineを使いこなす自信がつき、自分のプロジェクトでぜひ活用してみたいと思っていただけたのではないでしょうか。今回は多くの機能を取り上げましたが、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のどちらの機能を使用してフォーマットするかを決定できます。 構築するもの 心配する必要はありません。このチュートリアルは、この目的のた