🐥

Active Storageで複数画像をCloudinary に上げ Herokuに公開する

2021/06/20に公開

初めに

現場で使える Ruby on Rails 5速習実践ガイド では、ファイルをアップロードしてモデルに添付する方法として、Active Storage が紹介されています。

ローカル環境で、一枚の画像を添付する方法について書かれておりました。
せっかくですので、

  • 複数の画像の添付方法
  • 画像や動画の管理が得意なクラウドサービス Cloudinary の利用方法
  • Heroku への 公開方法

について、記していきます。
ついでに、SendGridを使ってメール送信できるようにします。

現場で使える Ruby on Rails 5速習実践ガイド

Active Storage とは

Rails 5.2 から、ActiveStorage が同梱されました。
クラウドストレージサービス(Amazon S3 や Cloudinaryなど)への画像・動画をアップロードして、データベース(ActiveRecord)に紐付けることが簡単にできるようになりました。

Cloudinary とは

Cloudinaryとは、画像や動画の配信や編集ができるクラウドサービスです。
無料でも、一ヶ月当たり25クレジット(≒25GB)まで使うことができますので、小規模な開発には充分かと思います。

準備

Active Storage は、Rails アプリを新規作成した際に導入されています。
そして、以下のGemも導入しておきます。

# Gemfile
gem 'image_processing'                 # (サイズ変更など)画像処理用
gem 'cloudinary', require: true        # Cloudinary
gem 'activestorage-cloudinary-service' # Cloudinary と Active Storage の連携をする
gem 'active_storage_validations'       # 画像ファイルのバリデーション用
% bundle install

Active Storage をインストールします。

% rails active_storage:install

マイグレーションファイルが生成されますので、データベースに反映させるべく、migrate コマンドを実行します。

% rails db:migrate

添付ファイルの実体を、どこに保存するのか、設定を行います。
開発環境ではローカルに、本番環境では cloudinary に、添付ファイルが保存されるように設定します。

# config/development.rb
Rails.application.configure do
  # Store uploaded files on the local file system (see config/storage.yml for options).
  # アップロードされたファイルをローカルファイルシステムに保存します
  # (オプションについては config/storage.yml を参照してください)。
  config.active_storage.service = :local
end
# config/production.rb
Rails.application.configure do
  config.active_storage.service = :cloudinary
end

:local, :cloudinary は、config/storage.yml に詳細を記述します。

# config/storage.yml
local:
  service: Disk
  root: <%= Rails.root.join("storage") %>

cloudinary:
  service: Cloudinary
  cloud_name: <%= Rails.application.credentials.dig(:cloudinary, :cloud_name) %>
  api_key:    <%= Rails.application.credentials.dig(:cloudinary, :api_key) %>
  api_secret: <%= Rails.application.credentials.dig(:cloudinary, :api_secret) %>

後々、ビューで画像を表示するのに便利なので、以下も記述しておきます。
enhance_image_tag: true と書くことで、
ビュー内で、= image_tag と書いた際に、cloudinary による便利な機能拡張が使えるようになります。

# config/cloudinary.yml
development:
  cloud_name: <%= Rails.application.credentials.dig(:cloudinary, :cloud_name) %>
  api_key:    <%= Rails.application.credentials.dig(:cloudinary, :api_key) %>
  api_secret: <%= Rails.application.credentials.dig(:cloudinary, :api_secret) %>
  enhance_image_tag: true
  static_file_support: false

production:
  cloud_name: <%= Rails.application.credentials.dig(:cloudinary, :cloud_name) %>
  api_key:    <%= Rails.application.credentials.dig(:cloudinary, :api_key) %>
  api_secret: <%= Rails.application.credentials.dig(:cloudinary, :api_secret) %>
  enhance_image_tag: true
  static_file_support: false

test:
  cloud_name: <%= Rails.application.credentials.dig(:cloudinary, :cloud_name) %>
  api_key:    <%= Rails.application.credentials.dig(:cloudinary, :api_key) %>
  api_secret: <%= Rails.application.credentials.dig(:cloudinary, :api_secret) %>
  enhance_image_tag: true
  static_file_support: false

ここで、書かれている Rails.application.credentials は、鍵の管理を行うために、Rails 5.2 から登場したcredentials(信任状)という機能です。

大切な鍵の情報は、config/credentials_yml.enc に暗号化されて保存されています。
次のコマンドで、暗号化された情報を見ることができます。

% rails credentials:show

編輯するためには、次のコマンドを実行します。

% rails credentials:edit

エディタが立ち上がりますので、適宜編輯します。
Command + S で保存して、Command + W でタブを閉じます。

cloudinary:
  cloud_name: (cloudinaryに付けた任意の名前)
  api_key: (cloudinaryより指定された15桁の数字)
  api_secret: (cloudinaryより指定された27桁の英数記号)

# Used as the base secret for all MessageVerifiers in Rails, 
including the one protecting cookies.
secret_key_base:
(128桁の16進数)

タスクモデルに画像を添付できるようにする。

Taskモデルに画像ファイルを添付できるようにします。
ついでに、activestorage-validator による添付画像の検証も追加します。

# app/models/task.rb
class Task < ApplicationRecord
  # has_one_attached :image # 添付画像は一つ
  has_many_attached :images # 複数の添付画像

  # activestorage-validator による添付画像の検証
  validates :images,
    content_type: %i(gif png jpg jpeg),                        # 画像の種類
    size: { less_than_or_equal_to: 5.megabytes },              # ファイルサイズ
    dimension: { width: { max: 2000 }, height: { max: 2000 } } # 画像の大きさ
end

ビューも作成します。

# app/views/task/new.html.slim
h1 タスクの新規登録
= link_to '一覧', tasks_path, class: 'ui right floated primary tertiary button'
= render partial: 'form', locals: { task: @task }
# app/views/task/_form.html.slim
= form_with model: task, class: 'ui form', local: true do |f|
  .field
    = f.label :name
    = f.text_field :name, required: true
  .field
    = f.label :description
    = f.text_area :description
  .field
    = f.label :images
    - if task.images.attached?
        - task.images.each do |image|
              = image_tag image
              / = image_tag task.image.variant(resize_to_limit: [300, 300])
              = f.check_box :image_ids, { multiple: true }, image.id, false
              = f.label "image_ids_#{image.id}"
                | &nbsp; 画像を削除する
    = f.file_field :images, 
        accept: 'image/jpg, image/jpeg, image/png, image/gif', 
        multiple: true
  = f.submit nil, class: 'ui primary button'

画像の表示を担当しているのは、次の部分です。
画像の添付があった場合、eachメソッドで、全ての画像を表示させています。

- if task.images.attached?
    - task.images.each do |image|
          = image_tag image

また、不要な画像を削除できるよう、チェックボックスを設けています。

= f.check_box :image_ids, { multiple: true }, image.id, false
= f.label "image_ids_#{image.id}"
  | 画像を削除する

添付ファイルを複数選択できるようにするとともに、
画像ファイルのみを選べるようにしています。

= f.file_field :images, 
    accept: 'image/jpg, image/jpeg, image/png, image/gif', 
    multiple: true

コントローラを作成します。

# app/controllers/tasks_controller.rb
class TasksController < ApplicationController
  def update
    # 画像の削除処理
    params[:task][:image_ids]&.each do |image_id|
      @task.images.find(image_id).purge
    end

    if @task.update(task_params)
      redirect_to tasks_url, notice: "タスク「#{@task.name}」を更新しました。"
    else
      render :edit
    end
  end

  private

  def task_params
    # 一つの画像を添付する場合
    # params.require(:task).permit(:name, :description, :image)

    # 複数の画像を添付する場合
    params.require(:task).permit(:name, :description, images:[])
  end
end

update アクションで、チェックが入っている画像を削除できるようにしています。
また、フォームから添付ファイルを受け取れるよう、task_paramsに、imageを追加しています。

以上で、Active Storage を使った複数画像の添付は完成です。

少し、ビューが味気ないので、
Fomantic-UI の card を使って画像を表示することとし、
css/javascript を使って、ファイルフォームを綺麗にすると、次のようになります。

# app/views/task/_form.html.slim
- if task.errors.present?
    ul#error_explanation
      - task.errors.full_messages.each do |message|
        li = message

= form_with model: task, class: 'ui form', local: true do |f|
  .field
    = f.label :name
    = f.text_field :name, required: true
  .field
    = f.label :description
    = f.text_area :description

  .field
    = f.label :images
    - if task.images.attached?
      .ui.cards
        - task.images.each do |image|
          .card
            .image
              = image_tag image
              /
              / = image_tag task.image.variant(resize_to_limit: [300, 300])
            .extra.content
              .ui.checkbox
                = f.check_box :image_ids, { multiple: true }, image.id, false
                = f.label "image_ids_#{image.id}"
                  | &nbsp; 画像を削除する

    = f.file_field :images, 
        accept: 'image/jpg, image/jpeg, image/png, image/gif', 
        multiple: true, 
        id: 'embed_file_input'

  .ui.fluid.action.input.mb-3
    input#selected_filenames_display_area disabled="disabled"
      placeholder="画像ファイルはありません" type="text"
    label.ui.small.teal.left.floated.button for="embed_file_input"
      = semantic_icon('upload')
      | 画像選択

  = f.submit nil, class: 'ui primary button'

css:
  input[type="file"] {
    display: none;
  }
  #selected_filenames_display_area {
    opacity: 1;
  }

javascript:
  // "embed_file_input" という ID属性の要素を取得する。(画像選択ボタン)
  const input_files = document.getElementById("embed_file_input");
  // 選択ファイル名表示領域
  const selected_filenames_display_area = 
    document.getElementById("selected_filenames_display_area");

  // 値が変化した時(ファイル選択時)に実行されるイベント
  input_files.onchange = function() {
    // FileList オブジェクトを取得する
    let file_lists = input_files.files;
    // 画像ファイル名格納用の配列
    let file_names = []
    for (let i = 0; i < file_lists.length; i++) {
      // File オブジェクトより、画像ファイル名を取得する
      file_names.push(file_lists[i].name)
    }
    // 選択ファイル名表示領域に、画像ファイル名を書き出す
    selected_filenames_display_area.value = file_names.join(', ')
  }

本番環境でSendGridを使う

production.rb に SendGrid を使ってメール送信できるよう、追記します。

# config/environments/production.rb

# Ignore bad email addresses and do not raise email delivery errors.
# Set this to true and configure the email server for 
# immediate delivery to raise delivery errors.
config.action_mailer.raise_delivery_errors = false
config.action_mailer.delivery_method       = :smtp
host                                       = 'taskleaf.herokuapp.com'
config.action_mailer.default_url_options   = { host: host }
ActionMailer::Base.smtp_settings           = {
  address:        'smtp.sendgrid.net',
  port:           '587',
  authentication: :plain,
  user_name:      ENV['SENDGRID_USERNAME'],
  password:       ENV['SENDGRID_PASSWORD'],
  domain:         'heroku.com',
  enable_starttls_auto: true
}

Heroku への公開

アカウントの作成

Heroku(へろく)は、Ruby on Rails で作成したウェブアプリを簡単に公開(デプロイ)できるサービスです。
https://heroku.com にアクセスし、Sign up から、自分のアカウントを作成します。

CLI(Command Line Interface)のインストール

GUI(Graphical User Interface)を使って、ブラウザからウェブアプリの作成もできます。
そしてせっかくですから、Heroku Command Line Interface (CLI) もインストールしておきます。
ターミナルからコマンド一つで、いろいろできるようになるので、慣れると簡単です。

% brew tap heroku/brew && brew install heroku

ウェブアプリの作成とGitを使っての公開(デプロイ)

% cd taskleaf
% git init
Initialized empty Git repository in .git/
% git add .
% git commit -m "My first commit"

Herokuにログインしてウェブアプリを作成します。
名前は、taskleaf にします。(すでに使われていたら別の名前にします。)

% heroku login
% heroku create taskleaf

Heroku では、いろいろなadd-on(追加機能)を使えるようになっています。
データベースには、Postgresql を、
ストレージサービスには、Cloudinary を、
メール送信サービスとして、SendGrid を使いたいので、
以下のコマンドで、機能追加します。
各アドオンとも、利用する容量等によって、さまざまな料金プランが用意されていますが、
ここでは、無料プランにしています。

% heroku addons:create heroku-postgresql:hobby-dev
% heroku addons:create cloudinary:starter
% heroku addons:create sendgrid:starter

% heroku config:get SENDGRID_USERNAME
% heroku config:get SENDGRID_PASSWORD

<font color="red">
従前は、
% heroku addons:create sendgrid:starter
% heroku config:get SENDGRID_USERNAME
% heroku config:get SENDGRID_PASSWORD
と書くことで、動作していましたが、
SendGridの仕様が変更となり、USERNAMEとPASSWORDによる認証ではなく、
APIキーによる認証を求められるようになりました。
手順を以下の記事に記しましたので、ご覧ください。
Heroku にデプロイした Rails アプリから SENDGRID を使ってメール送信する
</font>

公開(デプロイ)します。

% git push heroku master

公開できましたので、データベースを更新します。

% heroku run rails db:migrate

ブラウザで開いて確認します。

% heroku open

ローカル環境と同じようにファイルをアップロードできるはずです。
また、Cloudinaryのサイト(https://cloudinary.com/)にログインすると、
「Media Library」にアップロードした画像ファイルがあることが確認できるはずです。

あとがき

ざっくりと書きましたが、どなたかのお役に立てば幸いです。

参考

現場で使える Ruby on Rails 5速習実践ガイド
【Rails 5.2】 Active Storageの使い方 - Qiita
【Rails on Docker on Heroku】ActiveStorage + Cloudinaryで画像を管理するメモ

Discussion