はじめに
ActiveStorageを使って画像を複数枚アップロード&削除機能を実装していきます。
複数枚のアップロードに対応した画像を設定
Site
モデルがあるとします。site
に複数枚の画像アップロードに対応したmain_images
を持たせたいので、has_many_attached
で設定します。
class Site < ApplicationRecord
has_many_attached :main_images
end
has_many_attached
: レコードとファイルの間に1対多の関係を設定します。各レコードには、多数の添付ファイルをアタッチできます。
ストロングパラメータに追記
トップ画像用のmain_images
をストロングパラメータに追記します。
def site_params
params.require(:site).permit(:name, :subtitle, :description, :favicon, :og_image, main_images: [])
end
main_images: []
: 複数画像が入ることを想定するので配列を渡しています。末尾に追加していきます。
ビューファイル
= f.input :main_images, as: :file, hint: 'JPEG/PNG (1200x400)', input_html: { multiple: true } # 入力フォーム部分(simple_form使用)
- if @site.main_images.attached? # 画像がある場合、
.main_images_box
- @site.main_images.each do |main_image| # 配列の中の画像をeach文で回す
.main_image
= image_tag main_image.variant(resize: '300x100').processed
multiple: true
: simple_formには無い機能なので、input_html
と記述することで、有効になります。variant
: 画像のサイズを変更するにはresizeを引数として渡します。高さを100pxとして指定していますが、横幅を優先した状態でアスペクト比を保つため、高さは指定したとおりにはなりません。 processed
: すでにそのサイズで保存されて画像があれば、変換処理は行われず、即時にURLが返されます。
ActiveStorageのカスタムバリデーション
実はActiveStorageは非常に便利な機能なのですが、バリデーション機能がありません。現状、手動で作成しなければいけません。(2021.11月現在) 複数画像に対応するようなカスタムバリデーターを作成しましょう。
モデルにバリデーションを定義
まずはSite
モデルにバリデーションを定義します。
validates :main_images, attachment: { purge: true, content_type: %r{\\Aimage/(png|jpeg)\\Z}, maximum: 524_288_000 }
content_type
: 画像のファイルタイプmaximum
: 画像のファイルサイズ
カスタムバリデーター
個別のバリデーションをattachment_validator.rb
に記入していきます。 モデルで定義した2つのメソッドを定義していきます。 クラス名は<検証名>Validator
の形式で命名し、ActiveModel::EachValidator
クラスを継承させます。 また、必ずvalidate_each
メソッドを実装する必要があることに注意しましょう。
class AttachmentValidator < ActiveModel::EachValidator # モデルで呼び出す際はattachmentで呼び出す
include ActiveSupport::NumberHelper
def validate_each(record, attribute, value) # recordにはモデルのインスタンス、attributeには属性名、valueには属性値 が入る
return if value.blank? || !value.attached? # 属性値が空もしくは、属性値がアタッチされてない場合、
has_error = false # 「エラーは無し」ということでreturnし終了
if options[:maximum] # オプションにmaximumがある場合、
if value.is_a?(ActiveStorage::Attached::Many) # 継承関係を遡ってどのクラスに属しているか調べている。複数の画像が保存された時にtrueとなる。一枚の画像を受け取るレコードの時にもこのメソッドを使えるように、条件分岐する
value.each do |one_value| # 属性値をeach文で回す
unless validate_maximum(record, attribute, one_value) # それぞれの属性値にvalidate_maximumを実行し、falseなら
has_error = true # has_errorにtrueを代入
break # 繰り返し終了
end
end
else # 一枚の画像を受け取るレコードの時、
has_error = true unless validate_maximum(record, attribute, value) # validate_maximumを実行し、falseならhas_errorにtrueを代入
end
end
if options[:content_type] # オプションにcontent_typeがあれば、
if value.is_a?(ActiveStorage::Attached::Many) # 継承関係を遡ってどのクラスに属しているか調べている。複数の画像が保存された時にtrueとなる。一枚の画像を受け取るレコードの時にもこのメソッドを使えるように、条件分岐する
value.each do |one_value| # 属性値をeach文で回す
unless validate_content_type(record, attribute, one_value) # それぞれの属性値にvalidate_content_typeを実行し、falseなら
has_error = true # has_errorにtrueを代入
break # 繰り返し終了
end
end
else # 一枚の画像を受け取るレコードの時、
has_error = true unless validate_content_type(record, attribute, value) # validate_content_typeを実行し、falseならhas_errorにtrueを代入
end
end
record.send(attribute).purge if options[:purge] && has_error # オプションのpurgeがtrueかつhas_errorがtrueなら属性値を削除。
end
private
def validate_maximum(record, attribute, value) # valueのbyte_size(ファイルサイズ)がSiteモデルで定義したバイト数を超えている場合はエラーメッセージが出る
if value.byte_size > options[:maximum]
record.errors[attribute] << (options[:message] || "は#{number_to_human_size(options[:maximum])}以下にしてください")
false
else
true
end
end
def validate_content_type(record, attribute, value) # valueのファイルタイプがcontent_typeで指定しているものでなければエラーメッセージが出る
if value.content_type.match?(options[:content_type])
true
else
record.errors[attribute] << (options[:message] || 'は対応できないファイル形式です')
false
end
end
end
value.is_a?(ActiveStorage::Attached::Many)
: 継承関係を遡ってどのクラスに属しているか調べています。一枚の画像を受け取るレコードの時にもこのメソッドを使えるように、条件分岐を使って定義しています。
ActiveStorageの削除機能
ActiveStorageを使って画像のアップロードの仕方を学んできましたが、削除はどうやるのでしょうか。今回のような場合、新たに削除機能を持つコントローラを作成したほうが良いでしょう。なぜなら、sites_controller
はSiteモデルに関するリソースを担うコントローラなので、destroy
アクションを実装するとしたら、site
を削除するためのアクションになるでしょう。 また、sites_controller
に画像削除用の独自アクションを命名し、作成することもできますが、RESTに基づく7つのアクションで設計したほうが管理しやすいです。
削除機能専用のコントローラを作成
admin/site
ディレクトリ配下にattachments
コントローラを作成します。
$ bundle exec rails g controller admin::site::attachments destroy
ルーティング設定
routingではadminsiteのresourceにnestさせて定義すると既存のアクションとの関連性が分かりやすいです。 admin配下にあるのでadminの内側、さらにsiteの配下にあるのでsiteの内側に追記しましょう。
resource :site, only: %i[edit update] do
resources :attachments, controller: 'site/attachments', only: %i[destroy]
end
controller: 'site/attachments'
: これを付けないと生成されるルーティングが、admin/attachments#destroy
となり、エラーになります。 付けることでadmin/site/attachments#destroy
となります。
削除用コントローラを追記
画像削除の処理はparamsで条件分岐をせず、ActiveStorage::Attachment
から取得するようにしましょう。
[BAD] 条件分岐をした場合の書き方
# BAD
class Admin::Site::AttachmentsController < Admin::SitesController
def destroy
@site.favicon.purge if params[:status] == 'favicon'
@site.og_image.purge if params[:status] == 'og_image'
@site.main_images.find(params[:id]).purge if params[:status] == 'main_images'
redirect_to edit_admin_site_path
end
end
[GOOD] ActiveStorage::Attachmentを利用した書き方
# GOOD
class Admin::Site::AttachmentsController < ApplicationController
def destroy
authorize(current_site) # current_site は Site.first で対応できるが、今後の拡張を考慮して権限管理する
image = ActiveStorage::Attachment.find(params[:id])
image.purge
redirect_to edit_admin_site_path
end
end
ActiveStorage::Attachment.find(params[:id])
をすることによって、favicon
og_image
main_images
全ての中からidを取得するのでどの画像からの削除リクエストがきても対応できます。current_site
: application_controller.rb
の中で定義されています。image.purge
: 探し出した画像を削除します。Active Storageで使う削除メソッドです。
削除ボタンを追加
= f.input :main_images, as: :file, hint: 'JPEG/PNG (1200x400)', input_html: { multiple: true }
- if @site.main_images.attached?
.main_images_box
- @site.main_images.each do |main_image|
.main_image
= image_tag main_image.variant(resize: '300x100').processed
= link_to '削除', admin_site_attachment_path(main_image.id), method: :delete, class: 'btn btn-danger'
Siteの削除権限
Siteの画像はadminユーザー以外は削除できないようにしておくことが望ましいでしょう。
def destroy?
user.admin?
end
参考記事


コメント