🦓

[Rails]Active Storageによる画像アップロード

2023/07/30に公開

はじめに

Active Storageを使って画像のアップロードを実装していきます。

https://railsguides.jp/active_storage_overview.html

環境

Rails 7.0.4.3
ruby 3.2.1

Active Storageとは

Active Storageは、Railsの一部として提供されるファイルアップロードとストレージのためのライブラリです。Rails 5.2から導入され、ファイルのアップロードと管理を簡単に実装することができるようになりました。

Active Storageを使うと、以下のような機能を簡単に実現できます:

  1. ファイルアップロード: ユーザーがWebアプリケーションにファイルをアップロードできます。一般的な用途として、画像のアップロードやPDFファイルのアップロードなどがあります。

  2. ストレージの管理: Active Storageは、アップロードされたファイルをローカルファイルシステムやAmazon S3、Google Cloud Storageなどのクラウドストレージに簡単に保存できます。これにより、大容量のファイルをアプリケーションサーバーから切り離し、パフォーマンスの向上やスケーラビリティの向上が期待できます。

  3. アクティブストレージの機能: Active Storageはファイルのアップロードとストレージ管理だけでなく、画像のリサイズや回転などの画像処理もサポートしています。これにより、アプリケーション内で簡単に画像の縮小や切り抜きを行うことができます。

Active Storageは、Railsの他のコンポーネントとシームレスに連携し、アプリケーション内でのファイル操作を簡単にすることができます。たとえば、Active Recordモデルと関連付けてファイルを保存したり、ビューでファイルを表示したりすることができます。アプリケーションでファイルアップロードや画像処理が必要な場合は、Active Storageを使うことでこれらの機能を効率的に実装することができます。

Active Storageをインストールする

bin/rails active_storage:install
Copied migration 20230729155113_create_active_storage_tables.active_storage.rb from active_storage
bin/rails db:migrate
== 20230729155113 CreateActiveStorageTables: migrating ========================
-- create_table(:active_storage_blobs, {:id=>:primary_key})
   -> 0.0401s
-- create_table(:active_storage_attachments, {:id=>:primary_key})
   -> 0.0148s
-- create_table(:active_storage_variant_records, {:id=>:primary_key})
   -> 0.0398s
== 20230729155113 CreateActiveStorageTables: migrated (0.0949s) ===============

image_processingをインストールする

Gemfileにコメントアウトされたgemをアンコメントしインストールします。
image_processingは、Railsにおいて画像処理を行うためのGemです。これを使用することで、画像のリサイズ、トリミング、回転、フィルター処理などを簡単に実装できます。

Gemfile
# Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images]
gem "image_processing", "~> 1.2"
bundle install

image_processingmini_magickvipsを使ってます。

Gemfile.lock
image_processing (1.12.2)
    mini_magick (>= 4.9.5, < 5)
    ruby-vips (>= 2.0.17, < 3)
  1. mini_magick: Rubyで画像処理を行うためのGemです。画像のリサイズ、トリミング、フィルター処理など、様々な画像操作を行うことができます。ImageMagickという画像処理のライブラリをラップしており、RubyからImageMagickの機能を利用できるようにしています。

  2. vips: VIPSは、高速でメモリ効率が高い画像処理ライブラリです。mini_magickと同様に、画像のリサイズ、回転、フィルター処理などを行うことができます。mini_magickよりも高速であり、大量の画像を効率的に処理する際に特に有用です。

config/environments/development.rb
Rails.application.configure do
# デフォルト
config.active_storage.variant_processsor = :vips

config.active_storage.variant_processsor = :mini_magick

Active Storageでは、バリアントプロセッサとしてVipsまたはMiniMagickを利用できます。デフォルトで使われるバリアントプロセッサはconfig.load_defaultsのターゲットバージョンに依存し、config.active_storage.variant_processorで変更できます。

<!-- MiniMagick -->
<%= image_tag user.avatar.variant(resize_to_limit: [100, 100], format: :jpeg, sampling_factor: "4:2:0", strip: true, interlace: "JPEG", colorspace: "sRGB", quality: 80) %>

<!-- Vips -->
<%= image_tag user.avatar.variant(resize_to_limit: [100, 100], format: :jpeg, saver: { subsample_mode: "on", strip: true, interlace: true, quality: 80 }) %>

mini_magickをインストールする

brew install imagemagick

vipsをインストールする

brew install libvips

blogモデルにアソシエーションを追加する

app/models/blog.rb
class Blog < ApplicationRecord
...
  has_one_attached :image
  # 複数のイメージを持たせる
  has_many_attached :images
end

has_one_attachedマクロは、レコードとファイルの間に1対1のマッピングを設定します。レコード1件ごとに1個のファイルを添付できます。

https://railsguides.jp/active_storage_overview.html#has-one-attached

imageにバリデーションを追加する

アップロードできるファイル形式を指定し、それ以外の場合エラーメッセージを表示させるようにします。
ファイルサイズにも上限を追加します。

app/models/blog.rb
class Blog < ApplicationRecord
...
  has_one_attached :image
  validate :image_content_type
  validate :image_size
  
    def image_content_type
    if image.attached? && !image.content_type.in?(%w[image/jpeg image/png image/gif])
      errors.add(:image, ':ファイル形式が、JPEG, PNG, GIF以外になってます。ファイル形式をご確認ください。')
    end
  end

  def image_size
    if image.attached? && image.blob.byte_size > 1.megabytes
      errors.add(:image, ':1MB以下のファイルをアップロードしてください。')
    end
  end
end

ストロングパラメーターを追加する

app/controllers/blog_controller.rb
class BlogsController < ApplicationController
...
    private
    
    def blog_params
        params.require(:blog).permit(:title, :content, :image, images: [])
    end
end

フォームにアップローダーフィルドを追加する

app/views/blogs/_form.html.erb
<div class="my-3">
    <%= form.label :image%><br>
    <%= form.file_field :image%>
</div>

# 複数のイメージを持たせる
<div class="my-3">
    <%= form.label :images%><br>
    <%= form.file_field :images, multiple: true %>
</div>

ビューに画像を表示させる

app/views/blogs/_blog.html.erb
<% if blog.image.attached? %>
  <div><%= image_tag(blog.image) %></div>
<% end %>

# 複数のイメージを持たせる
<% if blog.images.attached? %>
  <% blog.images.each do |image| %>
     <div><%= image_tag(image) %></div>
  <% end %>
<% end %>

image_tagが必要です。ないとこうなります:

イメージをリサイズする

フルサイズのイメージをそのまま使うとロードに時間かかりますのでサムネバージョンを作ります。

リサイズメソッドを作成する

imageをそのまま使うよりimage_as_thumbnailメソッドを作成し画像のリサイズ処理を設定します。
複数のイメージを持たせる場合each文が必要です。

app/models/blog.rb
class Blog < ApplicationRecord
...
  def image_as_thumbnail
    return unless image.content_type.in?(%w[image/jpeg image/png])
    image.variant(resize_to_limit: [200, 100]).processed
  end
end

可能であれば、content_type:オプションも指定しておきましょう。Active Storageは、渡されたデータからファイルのContent-Typeの判定を試みますが、判定できない場合は指定のContent-Typeにフォールバックします。

resize_to_limitは、image_processing Gemにおける画像処理のオプションの一つです。このオプションを使用すると、画像のサイズを指定した範囲内に収めるようにリサイズできます。

ビューにサムネを使う

app/views/blogs/_blog.html.erb
<% if blog.image.attached? && blog.image.content_type.in?(%w[image/jpeg image/png]) %>
  <div><%= image_tag(blog.image_as_thumbnail) %></div>
<% end %>

プレビューを追加する

ファイル名しか表示されないので分かりずらいですね。
画像のプレビューも追加していきます。

プレビューコントローラーを作成する

bin/rails generate stimulus previews
      create  app/javascript/controllers/previews_controller.js

プレビュー用パーシャルを作成する

新規ブログを作成する場合アップロードされた画像を表示させ、ブログを編集する場合既存の画像を表示させるようにif文を追加します。

app/views/blogs/_image_preview.html.erb
<div class="my-3" data-controller="previews">
    <%= form.label :image%><br>
    <%= form.file_field :image,
    direct_uploade: true,
    data: { previews_target: "input", action: "change->previews#preview" } %>

    <% if blog.image.attached? %>
      <%= image_tag blog.image, data: { previews_target: "preview" } %>
    <% else %>
      <%= image_tag "", data: { previews_target: "preview" } %>
    <% end %>

</div>

パーシャルを読み込む

app/views/blogs/_form.html.erb
<%= render 'blogs/image_preview', form: form, blog: @blog %>

jsを作成する

アップロードされた画像をpreviews_targetに表示させます。

app/javascript/controllers/previews_controller.js
import { Controller } from "@hotwired/stimulus"

// Connects to data-controller="previews"
export default class extends Controller {
  static targets = ["input", "preview"]
  connect() {
  }
  preview() {
    let input = this.inputTarget;
    let preview = this.previewTarget;
    let file = input.files[0];
    let reader = new FileReader();

    reader.onloadend = function(){
      preview.src = reader.result;
    };

    if(file) {
      reader.readAsDataURL(file);
    } else {
      preview.src = "";
    }
  }
}

プレビューを表示されましたね。

画像を削除する

アップロードした画像を削除したいこともあるので削除機能を追加していきます。

delete_imageアクションを作成する

app/controllers/blogs_controller.rb
class BlogsController < ApplicationController
...

  def delete_image
    @blog = Blog.find(params[:id])
    @blog.image.purge
    respond_to do |format|
        render turbo_stream: turbo_stream.remove(@blog.image)
    end
  end
end

添付ファイルをモデルから削除するには、添付ファイルに対してpurgeを呼び出します。 Active Jobを使うようにアプリケーションが設定されている場合は、バックグラウンドで削除を実行できます。purgeすると、blobとファイルがストレージサービスから削除されます。

# avatarと実際のリソースファイルを同期的に破棄します。
user.avatar.purge

# Active Jobを介して、関連付けられているモデルと実際のリソースファイルを非同期で破棄します。
user.avatar.purge_later

アクションに対応するURLを追加する

config/routes.rb
Rails.application.routes.draw do
...
  resources :blogs do
    member do
      delete 'delete_image'
    end
  end

end

ビューに削除ボタンーを追加する

app/views/blogs/_image_preview.html.erb
<% if blog.image.attached? %>
        <%= link_to '画像を削除する', delete_image_blog_path(blog),
        data: { turbo_method: :delete },
        class: 'bg-white hover:bg-gray-100 text-gray-800 font-semibold py-2 px-4 border border-gray-400 rounded shadow' %>
        <%= turbo_frame_tag dom_id(blog.image) do %>
          <%= image_tag blog.image, data: { previews_target: "preview" } %>
        <% end %>
<% end %>

削除されたことを確認します。

16:56:32 web.1  | [ActiveJob] [ActiveStorage::PurgeJob] [6c1bd72e-dd32-4814-98e2-30673761327d]   ActiveStorage::Blob Destroy (0.7ms)  DELETE FROM "active_storage_blobs" WHERE "active_storage_blobs"."id" = $1  [["id", 8]]
16:56:32 web.1  | [ActiveJob] [ActiveStorage::PurgeJob] [6c1bd72e-dd32-4814-98e2-30673761327d]   TRANSACTION (0.4ms)  COMMIT
16:56:32 web.1  | [ActiveJob] [ActiveStorage::PurgeJob] [6c1bd72e-dd32-4814-98e2-30673761327d]   Disk Storage (0.2ms) Deleted file from key: vrvs1x21uuntwpdu3pl1lfrf579r
16:56:32 web.1  | [ActiveJob] [ActiveStorage::PurgeJob] [6c1bd72e-dd32-4814-98e2-30673761327d]   Disk Storage (0.1ms) Deleted files by key prefix: variants/vrvs1x21uuntwpdu3pl1lfrf579r/
16:56:32 web.1  | [ActiveJob] [ActiveStorage::PurgeJob] [6c1bd72e-dd32-4814-98e2-30673761327d] Performed ActiveStorage::PurgeJob (Job ID: 6c1bd72e-dd32-4814-98e2-30673761327d) from Async(default) in 27.62ms

終わり

Active Storageによる画像アップロードでした。
過去にCarrierWavegemを使った画像アップロード機能についても実装しましたのでご参考いただけますと幸いです。

https://zenn.dev/redheadchloe/articles/24e0fb357df71b

Discussion