Web備忘録

雑多に書いています

RailsAPIでBase64エンコードされた画像ファイルを受け取り、S3(AWS)に保存する

自分が昔に書いた記事の続編です。

fujiten3.hatenablog.com

クライアント層から送られてきたBase64エンコードされた画像ファイルをRailsで扱う方法の紹介するのがこの記事の目的です。

以前書いた記事にコメントを頂いたので、筆を取りました。(コメントありがとうございます!)

実装の時間を短縮したいなら……

Rubyの有名なアップロード用Gem「CarrierWave」と、そのさいのBase64エンコード・デコードをサポートする「Carrierwave-base64」を利用するのが手っ取り早いと思われます。

自分も利用したことがありますが、ドキュメントに詳細に使い方が書いているので、迷うところはないでしょう、たぶん。

それらのGemを利用し、AWSのS3に画像をアップロードする処理自体は、↓の記事が参考になりそうでした。(自分が書いたわけではありません。)

https://qiita.com/junara/items/1899f23c091bcee3b058#carrierwave%E3%81%AE%E8%A8%AD%E5%AE%9A

しかし、この記事では別のライブラリを使っています

ただ、この記事ではShrineという添付ファイルを操るための軽量なGemと、自前で実装したBase64デコード・エンコードモジュールを使った実装を紹介しています。

その理由は、この記事は、過去の記事の続編で、そして過去の自分がこのGemを使って実装していたためです!

このGemを使った理由は、CarrierWaveを使ったものはよく見かけたので、別のものに挑戦してみたい、みたいなモチベーションだった気がします。(半年以上前なので記憶が曖昧)

多少オレオレ実装の気があります。ただ、Carrierwave-base64といったGemが行っていることの一部は、この記事を読めば理解の助けになるのではないかと思います。

まずは受け取ろう

Avatarクラスというのを使って自分は受け取る処理を書きました。

class Avatar < ApplicationRecord
  # Shirne用のメソッド
  include ImageUploader::Attachment.new(:image)

  # 自前実装のモジュール
  include ImageEncodable

  belongs_to :user

end
module Api
  module V1
    class AvatarsController < ApplicationController

      # ...

      def create
        # @avatarという、ユーザーの画像を司るクラスのインスタンスを作る
        @avatar = current_user.avatar

        # Base64で送られてきた画像をデコードする(モジュールの説明はのちにします)
        image_file = ImageEncodable.decode_to_imagefile(avatar_params[:image])

        # イメージを格納して、あとはShrineに任せる
        @avatar.image = image_file
        @avatar.save!
        render json: @avatar
      end

     def show
        # ImageEncodableモジュールのおかげで、Avatarクラスのインスタンスが、自分をBase64でエンコードすることが出来る。
        @avatar = @user.avatar.encode(:icon)
        render json: @avatar
     end


      private

        def avatar_params
          params.require(:avatar).permit(:image)
        end
  
end

Shrineが担保するImageUploader(詳細は公式ドキュメントへGo)

require "image_processing/mini_magick"

class ImageUploader < Shrine
  plugin :processing # allows hooking into promoting
  plugin :versions   # enable Shrine to handle a hash of files
  plugin :delete_raw # delete processed files after uploading
  plugin :validation_helpers

  process(:store) do |io, context|
    versions = { original: io } # retain original


    io.download do |original|
      pipeline = ImageProcessing::MiniMagick.source(original)
      versions[:icon]  = pipeline.resize_to_limit!(200, 200)
      versions[:medium] = pipeline.resize_to_limit!(800, 800)
    end

    Attacher.validate do
      validate_max_size 5 * 1024 * 1024, message: "5MBを超える画像はアップロードできません。"
    end

    versions # return the hash of processed files
  end
end

自前実装のモジュール

module ImageEncodable

  # S3に格納している画像データをエンコードし、クライアント層に返すためのメソッド
  def encode(image_size_symbol)
    encoded_image = Base64.strict_encode64(open(avatar_url(image_size_symbol)).read)
    prefix = "data:image/png;base64,"
    prefix + encoded_image
  end


  # Base64の画像をデコードする
  def self.decode_to_imagefile(prefix_encoded_image)

    # Base64の形式を判断し、画像であればそれに合わせた処理
    meta_data = prefix_encoded_image.match(/data:(image|application)\/(.{3,});base64,(.*)/)
    content_type = meta_data[2]
    encoded_image = meta_data[3]
    if content_type == "jpeg" || content_type == "png"

      # Rubyが担保するデコードを行う。StringIOクラスを利用し、ファイルとして扱えるように。
      decoded_image = Base64.strict_decode64(encoded_image)
      image_file = StringIO.new(decoded_image)
    else
      # raise error
    end
  end

  private

    def avatar_url(image_size_symbol)
      if image.try(:[], image_size_symbol)
        if Rails.env.development?
          url = "public" + url 
        else
          url = image[image_size_symbol].url
        end
        url
      else
        "public/default.png"
      end
    end
    
end

以上です。

昔に書いたコードの、不必要そうな処理を除外してベタ貼りしております。

基本的な処理の流れは、クライアント層から送られてきたBase64データをデコードしてファイルにしてS3に保存、クライアントに返すときはエンコードして返答、という流れです。

しかしですね、今思えば、どう考えても、わざわざ画像をS3に保存するときにデコードする必要がないな、と思います。

そのまま文字列で放り込んどいた方が扱いやすいですよね。

まあ、あくまでこんな実装方法もあるよ、という参考にしていただければ幸いです。