Ruby
Rails
ActiveStorage

base64でエンコードされた画像をActive Storageで保存する

はじめに

Rails5.2系からActive Storageというファイルアップロード機能が追加されました。Rails5.1以前のバージョンではCarrierwaveなどのgemを使ってファイルアップロード機能を実装していたのではないでしょうか。本記事はActive Storageを用いて画像ファイルをbase64形式でやりとりする方法を記します。

本記事はこちらの記事を参考にしています。

今回のゴール

  • base64形式でエンコードされた画像をデコードし.png形式などで保存する

環境

  • os: macOS X High Sierra
  • ruby: 2.5.0
  • rails: 5.2.0

実装

本記事ではサンプルアプリを開発しながら行います。

準備

1. rails new

今回はDBにmysqlを使用しapiモードで作成しようと思います。(DBのパスワード等は各々で)

$ rails new test-app -d mysql --api

2. jbuilderのインストール

apiの作成ということでjsonを返す際にjbuilderというテンプレートエンジンを使います。
Gemfileファイル内にコメントアウトされているためそれを外しjbuilderのインストールを行います。

# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
gem 'jbuilder', '~> 2.5'

コメントアウトを外したらbundle installを実行します。

$ bundle install --path vendor/bundle

3. Active Storageのインストール

Active Storageのインストールを行います。

$ rails active_storage:install

config/environments/development.rbconfig/storage.ymlが以下のようになっているか確認してください。

config/environments/development.rb
# Store uploaded files on the local file system (see config/storage.yml for options)
config.active_storage.service = :local
config/storage.yml
local:
  service: Disk
  root: <%= Rails.root.join("storage") %>

4. book modelの作成

book modelの作成を行います。作成を行ったら一度migrateしましょう。

$ rails g resource book title:string
$ rails db:migrate

5. viewの作成

viewの作成を行います。test_app/app/views/配下にbooksディレクトリを作成します。作成したbooksディレクトリ配下にindex.json.jbuilderファイルを作成します。

$ mkdir test_app/app/views/books
$ touch test_app/app/views/index.json.jbuilder

6. ファイルをbookモデルに紐付ける

画像をbookと紐付けるためにhas_one_attachedを使いファイルをbookモデルに添付します。

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

model/book.rb
class Book < ApplicationRecord
  has_one_attached :book_image
end

これで準備完了です。

base64でエンコードされた画像をデコードしActive Storageで保存

model/book.rbにpase_base64メソッドを作成します。base64形式で画像データが送られてきた際にcreate_extensionで正規表現を用いて拡張子が何であるかを調べます。次に正規表現でdata:image/png;base64の後の部分を抜き出しデコードを行います。test_app/tmp配下に一時的に保存し、保存した画像データをattach_imageでActive Storageの方で保存を行います。保存を行うとtest_app/tmp配下に一時的に保存してあった画像ファイルを削除します。

app/model/book.rb
class Book < ApplicationRecord
  has_one_attached :book_image
  attr_accessor :image

  def parse_base64(image)
    if image.present? || rex_image(image) == ''
      content_type = create_extension(image)
      contents = image.sub %r/data:((image|application)\/.{3,}),/, ''
      decoded_data = Base64.decode64(contents)
      filename = Time.zone.now.to_s + '.' + content_type
      File.open("#{Rails.root}/tmp/#{filename}", 'wb') do |f|
        f.write(decoded_data)
      end
    end
    attach_image(filename)
  end

  private

  def create_extension(image)
    content_type = rex_image(image)
    content_type[%r/\b(?!.*\/).*/]
  end

  def rex_image(image)
    image[%r/(image\/[a-z]{3,4})|(application\/[a-z]{3,4})/]
  end

  def attach_image(filename)
    book_image.attach(io: File.open("#{Rails.root}/tmp/#{filename}"), filename: filename)
    FileUtils.rm("#{Rails.root}/tmp/#{filename}")
  end
end

作成したpase_base64メソッドを画像データ保存時に使うためにCreate部分を以下のようにします。また、book_paramsimageを追加します。

app/controllers/books_controller.rb
class BooksController < ApplicationController
  # POST /books
  def create
    @book = Book.new(book_params)

    if @book.save
      @book.parse_base64(params[:image])
      render json: @book, status: :created, location: @book
    else
      render json: @book.errors, status: :unprocessable_entity
    end
  end

  private

  def book_params
    params.require(:book).permit(:title, :image)
  end
end

保存した画像をblob_urlで取得

jbuilderでjsonをいい感じに表示するために以下のようにします。

views/books/index.json.jbuilder
if @books.present?
  json.books do
    json.array!(@books) do |book|
      json.extract! book, :id, :title
      json.image rails_blob_url(book.book_image) if book.book_image.attached?
    end
  end
end

現状のままだとlocalhost:3000/books にアクセスした際index.json.jbuilderを参照しません。また、画像は保存されていますが画像データが多くなるとN+1問題が発生します。そのため以下のようにindexを書き直しましょう。with_attached_book_imageで何をしているかはこちらを参照してください。

app/controllers/books_controller.rb
class BooksController < ApplicationController
  # GET /books
  def index
    @books = Book.with_attached_book_image
    render 'index', formats: 'json', handlers: 'jbuilder'
  end
end

localhost:3000/books にアクセスして確認して見ましょう。
スクリーンショット 2018-10-10 0.54.21.png

まとめ

APIなどを作成する際にbase64形式で画像データをやり取りすることはあると思います。少しでも役に立ったら幸いです。

参考にしたもの