[Rails]画像アップロード 9/20
はじめに
carrierwave
とmini_magick
を使って投稿に画像、ユーザにプロフィール画像をアップロードできるようにしていきます。
環境:
Rails 6.1.7.3
ruby 3.0.0
gemをインストールする
gem 'carrierwave', '>= 3.0.0.rc', '< 4.0'
gem "mini_magick"
bundle install
Articlesにarticle_imageカラムを追加する
画像そのままではなく、ファイル名をDBに保存されます。
bin/rails generate migration AddImageToArticles image:string
invoke active_record
create db/migrate/20230626045650_add_image_to_articles.rb
class AddImageToArticles < ActiveRecord::Migration[6.1]
def change
add_column :articles, :image, :string
end
end
bin/rails db:migrate
Running via Spring preloader in process 95920
== 20230626045650 AddImageToArticles: migrating ===============================
-- add_column(:articles, :image, :string)
-> 0.0021s
== 20230626045650 AddImageToArticles: migrated (0.0022s) ======================
uploaderを作成する
bin/rails generate uploader Image
create app/uploaders/image_uploader.rb
uploaderファイル
アップロードに対しての設定はこのファイルに書きます。
後ほど設定を追加していきます。
class ImageUploader < CarrierWave::Uploader::Base
# Choose what kind of storage to use for this uploader:
storage :file
# storage :fog
# Override the directory where uploaded files will be stored.
# This is a sensible default for uploaders that are meant to be mounted:
def store_dir
"uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
end
# コメント解除
# For images you might use something like this:
def extension_allowlist
%w(jpg jpeg gif png)
end
end
Articlesモデルにuploaderを読み込む
class Article < ApplicationRecord
mount_uploader :image, ImageUploader
end
Articlesコントローラーにimageパラメーターを追加する
class ArticleController < ApplicationController
...
private
def article_params
params.require(:article).permit(:title, :body, :image, :image_cache)
end
end
app/views/articles/_form.html.erb
にファイルアップロード用フィールドを追加する
アップロードに失敗した際もファイルが消えないようにするためにcache
も入れます。
UXの向上にも繋がります。
<div class="form-group mb-3"">
<%= form.label :image %>
<%= form.file_field :image, class:'form-control', accept: 'image/*'%>
<%= form.hidden_field :article_image_cache %>
</div>
image
属性の訳文を追加する
ja:
activerecord:
attributes:
article:
image: サムネール
投稿一覧に画像を表示させる
画像をアップロードされたらそれを使い、そうでない場合デフォルトの画像を使われます。
デフォルト画像を設定する
uploaderファイルに設定を既に書かれていますが、コメントアウトされているため解除します。
class ImageUploader < CarrierWave::Uploader::Base
# Provide a default URL as a default if there hasn't been a file uploaded:
def default_url(*args)
"image_placeholder.png"
end
end
default_url
を設定しているため、if文の処理を記載する必要はありません。
## BAD
<% if article&.image? %>
<%= image_tag article.image.url, size: '300x230', class:'card-img-top object-fit-cover' %>
<% else %>
<%= image_tag 'image_placeholder.png', size: '300x230', class:'card-img-top object-fit-cover' %>
<% end %>
## GOOD
<%= image_tag article.image.url, size: '300x230', class:'card-img-top object-fit-cover' %>
保存される画像のパスを確認します。
irb(main):001:0> article = Article.last
(0.9ms) SELECT sqlite_version(*)
Article Load (0.5ms) SELECT "articles".* FROM "articles" ORDER BY "articles"."id" DESC LIMIT ? [["LIMIT", 1]]
=> #<Article id: 7, title: "サムネールあり", body: "サムネール画像使われる"...
irb(main):002:0> article.image
=> #<ImageUploader:0x00007fe42e50bb98 @model=#<Article id: 7, title: "サムネールあり", body: "サムネール画像使われる", created_at: "2023-06-26 14:56:07.602216000 +0900", updated_at: "2023-06-26 14:56:07.602216000 +0900", user_id: 4, image: "image-15.png">, @mounted_as=:image, @staged=false, @file=#<CarrierWave::SanitizedFile:0x00007fe42e50a950 @file="/Users/chloe/workspace/schedule_tweet/public/uploads/article/image/7/image-15.png", @original_filename=nil, @declared_content_type=nil, @content_type=nil, @content=nil>, @filename=nil, @cache_id=nil, @identifier="image-15.png", @deduplication_index=nil, @versions={}, @storage=#<CarrierWave::Storage::File:0x00007fe42e50b800 @uploader=#<ImageUploader:0x00007fe42e50bb98 ...>, @cache_called=nil>>
irb(main):003:0> article.image.url
=> "/uploads/article/image/7/image-15.png"
投稿一覧ページに画像を表示させます。
<div class="row row-cols-1 row-cols-md-3 g-4">
<% @articles.each do |article| %>
<div class="col">
<div class="card h-100">
<%= image_tag article.image.url, size: '300x230', class:'card-img-top object-fit-cover' %>
<div class="card-body">
<small class="muted-text"><%= l(article.created_at, format: :long) %></small>
<h5 class="card-title mt-3"><%= link_to article.title, article %></h5>
<p class="card-text"><%= article.body %></p>
<p class="text-end"><small><%= article.user.user_name %></small></p>
<div class="card-footer text-end">
<%= link_to "Show", article_path(article), class: 'btn btn-secondary' %>
<% if Current.user && Current.user.id == article.user_id %>
<%= link_to "Edit", edit_article_path(article), class: 'btn btn-primary' %>
<%= link_to "Delete", article_path(article),
method: :delete, data: { confirm: t('defaults.message.delete_confirm'), item: Article.model_name.human }, class: 'btn btn-danger' %>
<% end %>
</div>
</div>
</div>
</div>
<% end %>
</div>
デフォルトの画像を使われていることを確認します。
サムネールバージョンを作成する
投稿一覧ではアップロードされた画像のサムネールバージョンを作って表示させていきます。
詳細ページなどでは違うサイズで画像を表示させたい時にバージョンで使い分けできるので便利です。
uploaderにサンプルコードがあるのでそれを使います。
imagemagick
をインストールする
mini_magick
を効かせるためにimagemagick
というCLIツールが必要です。
imagemagick
はgemではないのでbrewを使ってインストールします。
brew list
brew install imagemagick
class ImageUploader < CarrierWave::Uploader::Base
include CarrierWave::MiniMagick
...
# Create different versions of your uploaded files:
version :thumb do
process resize_to_fit: [300, 250]
end
end
サムネール画像のパスを確認します。
irb(main):009:0> article.image.thumb.url
=> "/uploads/article/image/7/thumb_image-15.png"
投稿一覧ページではサムネールバージョンを表示させる
<%= image_tag article.image.thumb.url, class:'card-img-top' %>
アプロードされた画像のプレビューを作成する
編集の場合では、既にアプロードされた画像を編集画面で表示させるようにします。
投稿を新規作成の場合、画像のプレビューより、「まだデータベースに保存していない投稿データに対して、選択された画像ファイルを表示する」という処理を行なっています。
データベースから値を取得して表示するのではなく、選択された画像ファイルをJavaScriptを使ってクライアントに表示する流れとなります。
画像をアップロードされたら<img>
のソースを書き換えられるように実装します。
<div class="form-group mb-3"">
<%= form.label :image %>
<%= form.file_field :image, class:'form-control js-file-select-preview', accept: 'image/*', data: { target: '#preview-target' } %>
<%= form.hidden_field :article_image_cache %>
</div>
<div class="form-group mt-3 mb-3">
<% if article.image.present? %>
<%= image_tag article.image.url %>
<% else %>
<%= image_tag 'no-imag.jpg', id: 'preview-target'%>
<% end %>
</div>
preview.js
のファイルを作成します。
document.addEventListener('DOMContentLoaded', () => {
const elements = document.querySelectorAll('.js-file-select-preview')
if (!elements) return
elements.forEach((element) => {
const previewElement = document.querySelector(element.dataset.target)
element.addEventListener('change', (e) => {
const reader = new FileReader();
reader.onloadend = () => {
previewElement.src = reader.result;
}
const file = e.target.files[0]
if (file) {
reader.readAsDataURL(file);
}
})
})
})
preview.js
を読み込みます。
import '../preview'
エラーメッセージを表示させる
画像関係のエラーメッセージを表示させます。
ja:
errors:
messages:
carrierwave_processing_error: '処理できませんでした'
carrierwave_integrity_error: 'は許可されていないファイルタイプです'
carrierwave_download_error: 'はダウンロードできません'
extension_whitelist_error: "は %{allowed_types}の形式でアップロードしてください"
extension_blacklist_error: "%{extension}ファイルのアップロードは許可されていません。アップロードできないファイルタイプ: %{prohibited_types}"
content_type_whitelist_error: "%{content_type}ファイルのアップロードは許可されていません。アップロードできるファイルタイプ: %{allowed_types}"
content_type_blacklist_error: "%{content_type}ファイルのアップロードは許可されていません"
rmagick_processing_error: "rmagickがファイルを処理できませんでした。画像を確認してください。エラーメッセージ: %{e}"
mini_magick_processing_error: "MiniMagickがファイルを処理できませんでした。画像を確認してください。エラーメッセージ: %{e}"
min_size_error: "を%{min_size}以上のサイズにしてください"
max_size_error: "を%{max_size}以下のサイズにしてください"
pdfファイルをアップロードしようとするとバリデーションに引っかかリます。
.gitignore
に画像アップロードパスを追加する
アップロードされた画像がgithub
にプッシュされないように.gitignore
に画像ディレクトリを追加します。
/public/uploads
終わりに
ユーザーのプロフィール画像に関しても同じ手順で実装していきます。
CarrierWave
は非常に便利なgemですね。
アップローダーへの理解をもっと深めていきたいです。
Discussion