[RAILS] Upload multiple images to Cloudinary on Active Storage and publish to Heroku

at first

The Ruby on Rails 5 Quick Practice Guide that you can use in the field introduces Active Storage as a way to upload files and attach them to your model.

It was written about how to attach a single image in a local environment. Because it's a big deal

I will write about. By the way, I'll be able to send emails using SendGrid.

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

What is Active Storage?

Starting with Rails 5.2, ActiveStorage (https://railsguides.jp/active_storage_overview.html) is included. You can now easily upload images / videos to cloud storage services (Amazon S3, Cloudinary, etc.) and link them to the database (ActiveRecord).

What is Cloudinary?

Cloudinary is a cloud service that allows you to distribute and edit images and videos. Even if it's free, you can use up to 25 credits (≈25GB) per month, so I think it's enough for small-scale development.

Preparation

Active Storage was introduced when you created a new Rails app. And also introduce the following Gem.

# Gemfile
gem 'image_processing'                 # (Resize etc.)For image processing
gem 'cloudinary', require: true        # Cloudinary
gem 'activestorage-cloudinary-service' #Link Cloudinary and Active Storage
gem 'active_storage_validations'       #For image file validation
% bundle install

Install Active Storage.

% rails active_storage:install

A migration file will be generated, so execute the migrate command to reflect it in the database.

% rails db:migrate

Set where to save the actual attachment file. Set attachments to be saved locally in the development environment and cloudinary in the production environment.

# config/development.rb
Rails.application.configure do
  # Store uploaded files on the local file system (see config/storage.yml for options).
  #Save the uploaded file to your local file system
  # (For options config/storage.See yml)。
  config.active_storage.service = :local
end
# config/production.rb
Rails.application.configure do
  config.active_storage.service = :cloudinary
end

: local,: cloudinary describes the details in 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) %>

Since it is convenient to display the image in the view later, I will also describe the following. enhance_image_tag: By writing true When you write = image_tag in the view, you can use the convenient extension by 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

Here, Rails.application.credentials is a function called credentials that appeared in Rails 5.2 to manage keys.

Your important key information is encrypted and stored in config/credentials_yml.enc. You can see the encrypted information with the following command:

% rails credentials:show

To edit, run the following command:

% rails credentials:edit

The editor will start up, so edit it accordingly. Save with Command + S and close the tab with Command + W.

cloudinary:
  cloud_name: (Any name given to cloudinary)
  api_key: (15 digit number specified by cloudinary)
  api_secret: (27-digit alphanumeric symbol specified by cloudinary)

# Used as the base secret for all MessageVerifiers in Rails, 
including the one protecting cookies.
secret_key_base:
(128 hexadecimal number)

Allow images to be attached to the task model.

Allows you to attach image files to the Task model. In addition, we will also add validation of the attached image by activestorage-validator.

# app/models/task.rb
class Task < ApplicationRecord
  # has_one_attached :image #One attached image
  has_many_attached :images #Multiple attachments

  # activestorage-Verification of attached image by validator
  validates :images,
    content_type: %i(gif png jpg jpeg),                        #Image type
    size: { less_than_or_equal_to: 5.megabytes },              #file size
    dimension: { width: { max: 2000 }, height: { max: 2000 } } #Image size
end

Also create a view.

# app/views/task/new.html.slim
New registration of h1 task
= link_to 'List', 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;Delete image
    = f.file_field :images, 
        accept: 'image/jpg, image/jpeg, image/png, image/gif', 
        multiple: true
  = f.submit nil, class: 'ui primary button'

The following parts are responsible for displaying the image. When an image is attached, all the images are displayed by the each method.

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

In addition, a check box is provided so that unnecessary images can be deleted.

= f.check_box :image_ids, { multiple: true }, image.id, false
= f.label "image_ids_#{image.id}"
  |Delete image

Allows you to select multiple attachments and Only image files can be selected.

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

Create a controller.

# app/controllers/tasks_controller.rb
class TasksController < ApplicationController
  def update
    #Image deletion process
    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"#{@task.name}"The has been updated."
    else
      render :edit
    end
  end

  private

  def task_params
    #When attaching one image
    # params.require(:task).permit(:name, :description, :image)

    #When attaching multiple images
    params.require(:task).permit(:name, :description, images:[])
  end
end

The update action allows you to delete the checked image. We also added an image to task_params so that we can receive attachments from the form.

This completes the attachment of multiple images using Active Storage.

The view is a bit dull, so I decided to use the Fomantic-UI card to display the image. Using css/javascript to clean the file form, it looks like this:

# 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;Delete image

    = 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="There is no image file" type="text"
    label.ui.small.teal.left.floated.button for="embed_file_input"
      = semantic_icon('upload')
      |Image selection

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

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

javascript:
  // "embed_file_input"Get the element of ID attribute.(Image selection button)
  const input_files = document.getElementById("embed_file_input");
  //Selected file name display area
  const selected_filenames_display_area = 
    document.getElementById("selected_filenames_display_area");

  //When the value changes(When selecting a file)Event to be executed in
  input_files.onchange = function() {
    //Get a FileList object
    let file_lists = input_files.files;
    //Array for storing image file names
    let file_names = []
    for (let i = 0; i < file_lists.length; i++) {
      //Get the image file name from the File object
      file_names.push(file_lists[i].name)
    }
    //Export the image file name to the selected file name display area
    selected_filenames_display_area.value = file_names.join(', ')
  }

Use SendGrid in production

Add to production.rb so that you can send an email using 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
}

Publishing to Heroku

Create an account

Heroku is a service that allows you to easily publish (deploy) web applications created with Ruby on Rails. Go to https://heroku.com and go to Sign up to create your own account.

CLI (Command Line Interface) installation

You can also create web applications from a browser using the GUI (Graphical User Interface). And since it's a big deal, I also installed the Heroku Command Line Interface (CLI). You can do various things with just one command from the terminal, so it's easy once you get used to it.

% brew tap heroku/brew && brew install heroku

Create a web app and publish it using Git (deploy)

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

Log in to Heroku and create a web app. Name it taskleaf. (If it's already in use, give it a different name.)

% heroku login
% heroku create taskleaf

Heroku allows you to use a variety of add-ons. Postgresql for the database, For storage services, use Cloudinary, I want to use SendGrid as an email sending service, so Use the following command to add a function. Each add-on has various price plans depending on the capacity used, etc. Here, we have a free plan.

% 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

Publish (deploy).

% git push heroku master

Now that we have published it, we will update the database.

% heroku run rails db:migrate

Open it in your browser and check.

% heroku open

You should be able to upload the file as you would in your local environment. Also, when you log in to the Cloudinary site (https://cloudinary.com/), You should see that you have the image file you uploaded to the Media Library.

Afterword

I wrote it roughly, but I hope it helps someone.

reference

現場で使える Ruby on Rails 5速習実践ガイド [Rails 5.2] How to use Active Storage --Qiita [Rails on Docker on Heroku] Memo for managing images with Active Storage + Cloudinary

Recommended Posts

Upload multiple images to Cloudinary on Active Storage and publish to Heroku
Zip and upload multiple files to Firebase Storage on Android.
[Rails API + Vue] Upload and display images using Active Storage
upload images to refile heroku S3
How to publish an application on Heroku
Post / delete multiple images with Active Storage
Java upload and download notes to Azure storage
How to save images on Heroku to S3 on AWS
Upload multiple images easily with rails rails + carrierwave + cloudinary
[Rails] How to upload multiple images using Carrierwave
How to link images using FactoryBot Active Storage
[Ruby on Rails] Upload multiple images with refile
How to post images on Heroku + CarrierWave + S3
CarrierWave Upload multiple images
[Rails 6] Add images to seed files (using Active Storage)
Can't upload to heroku
How to deploy on heroku
[Java] Upload images and base64
[Rails] How to upload images to AWS S3 using Carrierwave and fog-aws
[Rails] How to upload images to AWS S3 using refile and refile-s3
[rails6.0.0] How to save images using Active Storage in wizard format
How to narrow down the image format from the gallery on Android and then select and import multiple images
Move Active Storage on S3 locally
How to connect Heroku and Sequel
[Rails] How to use Active Storage
Deploy Rails on Docker to heroku