これは何?
Railsアプリケーションを扱うときに、画像周りを扱うのは必須。
必須事項の現時点でのベストプラクティスを記事に残す。
下記の環境で開発を行ってます。
環境
- Mac OS High Sierra バージョン 10.13.6
- Ruby 2.5.1
- Rails 5.2.1
- MySQL 5.7.23
- RSpec
- CarrierWave
- MiniMagick
- Fog
- S3
CarrierWave
gem 'carrierwave'
=> まずはGemを追加
bundle exec rails g uploader Picture
=> uploaderの設定ファイルを生成
=> この設定ファイルについては後述
▼ 単一画像
bundle exec rails g migration add_picture_to_users picture:string
=> 画像をDBで扱うためにカラム追加
class User < ApplicationRecord
mount_uploader :picture, PictureUploader
end
=> ActiveRecordで扱うための処理を記述
<%= form_with(model: User.new, multipart: true, local: true) do |form| %>
<%= form.file_field :picture %>
<% end %>
def show
@user = User.find(params[:id])
end
def create
user = User.new(user_params)
end
private
def user_params
params.require(:user).permit(..., picture)
end
<%= image_tag(@user.picture.url, size: "30x30") unless @user.picture.file.nil? %>
▼ 複数画像
bundle exec rails g migration add_pictures_to_users pictures:json
=> 画像をDBで扱うためにカラム追加
class User < ApplicationRecord
mount_uploaders :pictures, PictureUploader
end
=> mount_uploader
ではなく、mount_uploaders
なので注意
<%= form_with(model: User.new, multipart: true, local: true) do |form| %>
<%= form.file_field :pictures, multiple: true %>
<% end %>
def show
@user = User.find(params[:id])
end
def create
user = User.new(user_params)
end
private
def user_params
params.require(:user).permit(..., {pictures: []})
end
<% @user.pictures.each do |picture| %>
<%= image_tag(picture.url, size: "30x30") unless picture.file.nil? %>
<% end %>
▼ Uploaderの設定
class PictureUploader < CarrierWave::Uploader::Base
# 環境によって保存される場所を変更する
if Rails.env.production?
storage :fog # Fogについては後述
else
storage :file
end
# storage :file の時の画像の保存場所を指定
def store_dir
if Rails.env.test? #テスト画像は一括削除できるようにフォルダを別にする
"uploads_#{Rails.env}/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
else
"uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
end
end
# セキュリティのためにファイル形式を制限
def extension_whitelist
%w(jpg jpeg gif png)
end
end
▼ URLからCarrierWaveに保存
Twitterログインした後に、Twitterのアイコン画像をCarrierWave経由で保存するケースです。
tw_image_url
にアイコン画像がURLで格納されてるとします。
Userモデルには既に
mount_uploader :picture, PictureUploader
が設定されているため、
remote_picture_url
という仮想属性が定義されます。
mount_uploader :picture, PictureUploader
user = User.new
user.remote_picture_url = tw_image_url
user.save
=> これでTwitterから取得した画像をCarrierWave経由で保存可能です。
MiniMagick
画像のリサイズや、別のバージョンのファイル(サムネイルなど)を用意するためのGemとしてCarrierWaveと相性が良いです。
RMagickもありますが、MiniMagickが推奨されてます。
また、MiniMagickはImagemagick上で機能するRuby Gemのため、Imagemagickがインストールされてる必要があります。
Macに直接インストールするなら、
brew install imagemagick
ですし、
Dockerなどにインストールするなら、
それぞれインストールしてから使うようにしてください。
gem 'mini_magick'
=> gemをインストールします。
あとは、CarrierWaveで生成したUploaderを変えていきます。
class PictureUploader < CarrierWave::Uploader::Base
# MiniMagickを有効にします
include CarrierWave::MiniMagick
# このアップローダーを利用した画像の最大数を指定します。
process resize_to_fit: [800, 800]
# 上記とは別にサムネイルを別サイズで用意します。
version :thumb do
process resize_to_fill: [200, 200]
end
end
=> resize_to_fit:
アスペクト比そのまま
=> resize_to_fill:
アスペクト比無視
=> resize_to_limit:
最大を決定
バージョン違いで保存したファイルは下記のように参照可能。
image_tag(user.picture.thumb.url)
Fog
gem 'fog'
=> Fogを使えるように追加。Productionにだけ追加するで問題なし。
if Rails.env.production?
CarrierWave.configure do |config|
config.fog_credentials = {
:provider => 'AWS',
:region => Credentials.env.dig(:S3_REGION),
:aws_access_key_id => Credentials.env.dig(:S3_ACCESS_KEY),
:aws_secret_access_key => Credentials.env.dig(:S3_SECRET_KEY)
}
config.fog_directory = Credentials.env.dig(:S3_BUCKET)
end
end
S3側の設定などは検索をしてください。
RSpec
テストでは下記の2つを確認しようかと思います。
- 適切なファイルフォーマットなら保存可能か、不適切なら保存不可か
- リサイズが適切にされているか。サムネイルも指定サイズで作成されているか
▼ 下準備
ファイルの大きさなどを検証するためのmatchersを追加するために下準備します。
require 'carrierwave/test/matchers'
require 'rails_helper'
RSpec.describe User, type: :model do
include CarrierWave::Test::Matchers
end
fixtures配下にいろんなファイル形式、サイズのものを保存しておいてください。
- sample.jpg <= かなり大きいサイズの画像にしておく(リサイズ検証のため)
- sample.jpeg
- sample.gif
- sample.png
- sample.rb
▼ ファイルフォーマット
it "is valid with jpg, jpeg, gif, png" do
formats = %w(jpg jpeg gif png)
formats.each do |format|
image_path = File.join(Rails.root, "spec/fixtures/sample.#{format}")
user = FactoryBot.build(:user, picture: File.open(image_path))
expect(user).to be_valid
end
end
it "is invalid with rb" do
image_path = File.join(Rails.root, "spec/fixtures/sample.rb")
user = FactoryBot.build(:user, picture: File.open(image_path))
expect(user).not_to be_valid
end
▼ リサイズ
it "resize to small size" do
image_path = File.join(Rails.root, "spec/fixtures/sample.jpg")
user = FactoryBot.create(:user, picture: File.open(image_path))
expect(user.picture).to be_no_larger_than(800, 800)
expect(user.picture.thumb).to have_dimensions(30, 30)
end