🦓

[Rails]画像アップロード 9/20

2023/06/27に公開

はじめに

carrierwavemini_magickを使って投稿に画像、ユーザにプロフィール画像をアップロードできるようにしていきます。

https://github.com/carrierwaveuploader/carrierwave
https://github.com/minimagick/minimagick

環境:

Rails 6.1.7.3
ruby 3.0.0

gemをインストールする

Gemfile
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
db/migrate/xxx_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ファイル

アップロードに対しての設定はこのファイルに書きます。
後ほど設定を追加していきます。

app/uploaders/image_uploader.rb
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を読み込む

app/models/article.rb
class Article < ApplicationRecord
   mount_uploader :image, ImageUploader
end

Articlesコントローラーにimageパラメーターを追加する

app/controllers/articles_controller.rb
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の向上にも繋がります。

app/views/articles/_form.html.erb
<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>

https://railsguides.jp/form_helpers.html#ファイルのアップロード

image属性の訳文を追加する

config/locales/activerecord/ja.yml
ja:
  activerecord:
    attributes:
      article:
        image: サムネール

投稿一覧に画像を表示させる

画像をアップロードされたらそれを使い、そうでない場合デフォルトの画像を使われます。

デフォルト画像を設定する

uploaderファイルに設定を既に書かれていますが、コメントアウトされているため解除します。

app/uploaders/image_uploader.rb
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"

投稿一覧ページに画像を表示させます。

app/views/articles/index.html.erb
<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
app/uploaders/image_uploader.rb
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"

投稿一覧ページではサムネールバージョンを表示させる

app/views/articles/index.html.erb
<%= image_tag article.image.thumb.url, class:'card-img-top' %>

アプロードされた画像のプレビューを作成する

編集の場合では、既にアプロードされた画像を編集画面で表示させるようにします。
投稿を新規作成の場合、画像のプレビューより、「まだデータベースに保存していない投稿データに対して、選択された画像ファイルを表示する」という処理を行なっています。
データベースから値を取得して表示するのではなく、選択された画像ファイルをJavaScriptを使ってクライアントに表示する流れとなります。
画像をアップロードされたら<img>のソースを書き換えられるように実装します。

app/views/articles/_form.html.erb
<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のファイルを作成します。

app/javascript/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を読み込みます。

app/javascript/packs/application.js
import '../preview'

エラーメッセージを表示させる

画像関係のエラーメッセージを表示させます。

config/locales/views/ja.yml
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ファイルをアップロードしようとするとバリデーションに引っかかリます。

https://github.com/carrierwaveuploader/carrierwave#i18n

.gitignoreに画像アップロードパスを追加する

アップロードされた画像がgithubにプッシュされないように.gitignoreに画像ディレクトリを追加します。

.gitignore
/public/uploads

終わりに

ユーザーのプロフィール画像に関しても同じ手順で実装していきます。
CarrierWaveは非常に便利なgemですね。
アップローダーへの理解をもっと深めていきたいです。

Discussion