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

# はじめに
Rails5.2系から[Active Storage](https://railsguides.jp/active_storage_overview.html)というファイルアップロード機能が追加されました。Rails5.1以前のバージョンでは[Carrierwave](https://github.com/carrierwaveuploader/carrierwave)などのgemを使ってファイルアップロード機能を実装していたのではないでしょうか。本記事はActive Storageを用いて画像ファイルをbase64形式でやりとりする方法を記します。

本記事は[こちら](https://medium.com/@sulmanweb/active-storage-as-attachment-in-rails-api-with-base64-decoding-f8362376d77c)の記事を参考にしています。

# 今回のゴール
- base64形式でエンコードされた画像をデコードし.png形式などで保存する

# 環境
- os: macOS X High Sierra
- ruby: 2.5.0
- rails: 5.2.0

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

## 準備

###1. rails new
今回はDBにmysqlを使用しapiモードで作成しようと思います。（DBのパスワード等は各々で）

```.shell
$ 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のインストールを行います。

```.shell
$ rails active_storage:install
```

`config/environments/development.rb`と`config/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しましょう。

```.shell
$ rails g resource book title:string
$ rails db:migrate
```

###5. viewの作成

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

```.shell
$ 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_params`に`image`を追加します。

```.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](http://localhost:3000/books) にアクセスした際`index.json.jbuilder`を参照しません。また、画像は保存されていますが画像データが多くなるとN+1問題が発生します。そのため以下のようにindexを書き直しましょう。`with_attached_book_image`で何をしているかは[こちら](https://qiita.com/ozin/items/f4aea5b244a6aa03caee)を参照してください。

```.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](http://localhost:3000/books) にアクセスして確認して見ましょう。
![スクリーンショット 2018-10-10 0.54.21.png](https://qiita-image-store.s3.amazonaws.com/0/81341/c67d1892-d541-233c-dff3-b4368b42ea12.png)

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

 
# 参考にしたもの
- https://medium.com/@sulmanweb/active-storage-as-attachment-in-rails-api-with-base64-decoding-f8362376d77c
- https://railsguides.jp/active_storage_overview.html