学習記録

アウトプット用に作りました

Active Storageで複数枚の画像を添付する

Active storageについては以前まとめたのでこちらを参照してください。
今回は画像を複数枚同時にアップロードできるようにしていきます、またアップロードされている画像を個別に削除できるような実装もしていきます。

複数枚の画像を添付する

has_many_attachedを使用して、画像とモデルの間に1対多の関係を設定します。多数の画像をアタッチできるようになります。
たとえばアプリケーションにMessageモデルがあるとします。メッセージごとに多数の画像を持たせるようにMessageモデルを定義します。

message.rb

has_many_attached :images


ビューとコントローラを書き換えて、imagesを受け取れるように実装していきます。※ビューではsimple_formとslim記法を使っています。

messages/edit.html.slim

= f.input :images, as: :file, hint: 'JPEG/PNG (1200x400)', input_html: { multiple: true }

  - if @message.images.attached?     
     - @message.images.each do |image|
       = image_tag image.variant(resize: "100x100").processed

{ multiple: true }は複数枚画像投稿の場合に追記することによって、複数枚画像を選択することができます。
input_html: { multiple: true }と記載しているのはsimple_formを使用しているのでinput_html:は必要な記述のようです。
参考 : 【Rails5】carrierwaveとsimple_formで複数の画像をアップロードする | ニートエンジニア
if文でimagesが添付されたらプレビューが表示されるように記述しました。
variantで画像を使用したいサイズに加工しています。
processedは保存されたサイズと加工しようとしているサイズが同じなら自身のインスタンスを返すという意味です。

messages_controller.rb

class MessagesController < ApplicationController
  def create
    message = Message.create!(message_params)
    redirect_to edit_message_path
  end

  private
  
  def message_params
    params.require(:message).permit(:title, :content, images: [])
  end
end

images: []と書く理由は、複数枚の画像がアップロードされた時に送られてくるパラメーターは配列に格納されているため、以下のようにストロングパラメーターも配列形式にする必要があるからです。
参考 : 【Rails6.0】Active Storageを用いた単数枚、複数枚画像投稿の実装手順をそれぞれ解説|TechTechMedia

これで複数枚の画像の添付ができるようになりましたが、バリデーションをつけていないので想定外の画像のタイプやサイズが送られてくる可能性があります。次で画像のバリデーションについて説明していきます。


Active Storageのバリデーション

Active Storageにはデフォルトのバリデーションがないため、自分でバリデーション設定をしなくてはいけません。 下記のようにcontent_typeで画像のファイルタイプ、maximumで画像のファイルサイズを設定できるようにしておきます。

message.rb

validates :images, attachment: { content_type: %r{\Aimage/(png|jpeg)\Z}, maximum: 524_288_000 }

上記で使用した2つのバリデーションで使用するメソッドを定義していきます。ここで使うのが個別のカスタムバリデーターです。バリデーションのメソッドをattachment_validator.rbに記入していきます。※ここではmaximumメソッドを用いて説明していきます。

validators/attachment_validator.rb

class AttachmentValidator < ActiveModel::EachValidator
  include ActiveSupport::NumberHelper

  def validate_each(record, attribute, value)
    return if value.blank? || !value.attached?

    has_error = false

    if options[:maximum]
      if value.is_a?(ActiveStorage::Attached::Many)
        value.each do |value|
          unless validate_maximum(record, attribute, value)
            has_error = true
            break   # breakは繰り返し終了
          end
        end
      else
        has_error = true unless validate_maximum(record, attribute, value)
      end
    end
  end

  private

  def validate_maximum(record, attribute, value)
    if value.byte_size > options[:maximum]
      record.errors[attribute] << (options[:message] || "#{number_to_human_size(options[:maximum])}以下にしてください")
      false
    else
      true
    end
  end
end

value.is_a?(ActiveStorage::Attached::Many)は継承関係を遡ってどのクラスに属しているか調べています。一枚の画像を受け取るレコードの時にもこのメソッドを使えるように、条件分岐を使って定義しています。
validate_maximumメソッドではvaluebyte_sizeがmessageモデルで定義したバイト数を超えている場合はエラーメッセージが出るように定義しています。
参考 : ActiveStorageのバリデーション - Qiita


Active Storageの削除機能

最後にアップロードした画像を削除する方法について説明していきます。私はmessagesコントローラのdestroyアクションに画像削除の記述をしていたのですが、これだとmessageを削除する定義を実装することができなくなってしまいます。またmessagesコントローラに画像削除用に独自のアクション名を増やすこともできますが、RESTに基づく7つのactionのみのほうがわかりやすく管理しやすいです。なので新たにControllerを作成してdestroyアクションを追加する方法が一番良いです。

まず最初に画像を削除するためのコントローラを作成します。admin/commentsディレクトリ配下にattachmentsコントローラを作成します。

$ rails g controller admin::comment::attachments destroy

ここで作成されたビューファイル、ルーティングの記述などの不要なものは削除しておきます。

そしてルーティングはadminとcommentsのresourceにネストさせて定義すると関連が分かりやすくなります。

routes.rb

namespace :admin do
  resource :comments do
    resources :attachments, only: %i[destroy]
  end
end

しかしこのままだとルーティングエラーが出てしまします。どのようなルーティングになっているか確認すると、下記のようになっていました。
f:id:kimura34:20210505212956p:plain

attachmentsコントローラはadmin/commentsディレクトリの配下にあるので、admin/attachments#destroyではおかしいですよね。

namespace :admin do
  resource :comments do
    resources :attachments, only: %i[destroy], controller: 'comments/attachments'
  end
end

このようにコントローラを指定してあげるとadmin/commentsディレクトリを指定することができるのでエラーは解決しました。 f:id:kimura34:20210505213811p:plain 他にもresourcesにmodule: :commentsと記載する方法もあります。

次に作成したattachmentsコントローラのdestroyアクションに画像の削除をするための定義をしていきます。

class Admin::Comments::AttachmentsController < ApplicationController
  def destroy
    image = ActiveStorage::Attachment.find(params[:id])
    image.purge
    redirect_to root_path
  end
end

ActiveStorage::Attachment.find(params[:id])は全てのActive Storageで管理している画像ファイルのIDとパラメータで送られてきたIDの一致するものを探しています。
image.purgeで探し出した画像を削除しています。purgeはActive Storageで使う削除メソッドです。

最後に削除ボタンを実装していきます。

messages/edit.html.slim

= f.input :images, as: :file, hint: 'JPEG/PNG (1200x400)', input_html: { multiple: true }

  - if @message.images.attached?     
     - @message.images.each do |image|
       = image_tag image.variant(resize: "100x100").processed
          = link_to '削除', admin_comments_attachment_path(image.id), method: :delete, class: %w[btn btn-danger]

admin_comments_attachment_path(image.id)でパスを送るときに画像IDを指定しています。