投稿日
Data URI形式の画像をS3へ保存してURLを取得する
もくじ
こんにちは。西日本テクノロジー&イノベーション室の藤田です。
日々(技術的に)強くなりたいと言っている新卒3年目です。趣味はプロレス観戦、座右の銘は「己こそ己の寄る辺」です。
先日まで関わっていたサービス開発案件で使用した、RubyでData URI形式で送られてきた画像をURLとして扱う方法を書いていきます。
背景
React + Ant Design Mobile + Railsでサービス開発をしています。
開発中のサービスでは、利用を希望するユーザーにFacebookログイン(OAuth 2.0)をしてもらった後にユーザー登録をしてもらいます。ユーザー登録時に本サービスで使うアバターを設定してもらうのですが、アバターの初期値にはFacebookから取得したアバターのURLを設定しています。ユーザーは必要があればアバターを変更できます。
ユーザー登録画面でアバターを変更するのに、Ant Design MobileのImage Pickerを使っています。ユーザーがFacebookに設定された初期アバターを変更しなかった場合、その画像はURLとして送られますが、ユーザーがImage Pickerを使用してアバターを変更した場合、その画像はData URIとして送られます。
Data URI化された画像データをそのままDBに登録することは避けたいですし、ユーザー登録以降は通常のURLかData URIかを意識したくありません。そのためData URIをデコードしてファイルとして保存して、そのファイルのURLをDBに登録することにしました。
Data URIとは
Data URIとは、data:
スキームが先頭についたURIのことで、ファイルをインラインで文書に埋め込むことができるものです。
スキーム(data:
)、MIMEタイプ、BASE64トークン、データ自体の4つの部品で構成されます。
data:[<mediatype>][;base64],<data>
扱うデータがURIで使える文字のみで構成されていればエンコードすることなく埋め込むことができますが、URIで使えない文字列に対してはエスケープが必要です。また、文字以外のデータはBASE64エンコードする必要があります。
例えば、PNGの画像をBASE64エンコードしたData URIは以下のようになります。
data:image/png;base64,<data>
<data>
の部分は、実際にBASE64エンコードされたデータそのものが入ります。
Data URIをDBに登録することの問題点
- サイズが大きい
画像を表すData URIはDBに登録するにはサイズが大きく、ストレージを圧迫してパフォーマンス低下を引き起こしかねません。
- ページサイズが大きくなる
Data URIはページに埋め込まれます。ページそのものにData URIのサイズが乗るので、読み込むページのサイズが画像の分だけ増えます。
Data URI形式の画像をURLで扱うためにやったこと
- 送られてきた画像がData URIかどうかを判断する
- Data URIであればBASE64でデコードする
- S3に画像を保存する
- S3に保存した画像のURLを取得する
- URLをDBに登録する
実装
以下で、コードを見ていきます。
送られてきた画像がData URIかどうかを判断する
URIクラスを使ってパースします。スキームがdata
であればData URIであると判断します。
def create
avatar = params[:avatar]
uri = URI.parse(avatar)
if uri.scheme == "data" then
# Data URIと判断した場合の処理
end
Data URIであればBASE64でデコードする
Data URIであればデコードして拡張子を取得します。
opaque
でスキーム以外の文字列を取得できるので、区切り文字を利用してmime_type
とdata
を取得します。
data
はBASE64でデコードし、mime_type
は拡張子の判断に使用します。拡張子はS3に画像を保存する時に使用します。
data = decode(uri)
extension = extension(uri)
def decode(uri)
opaque = uri.opaque
data = opaque[opaque.index(",") + 1, opaque.size]
Base64.decode64(data)
end
def extension(uri)
opaque = uri.opaque
mime_type = opaque[0, opaque.index(";")]
case mime_type
when "image/png" then
".png"
when "image/jpeg" then
".jpg"
else
raise "Unsupport Content-Type"
end
end
S3に画像を保存する
AWS SDK for Rubyで画像をS3に保存します。ここで画像を公開し、URLを返します。
def put_s3(data, extension)
file_name = Digest::SHA1.hexdigest(data) + extension
s3 = Aws::S3::Resource.new
bucket = s3.bucket("example-bucket")
obj = bucket.object("avatars/#{file_name}")
obj.put(acl: "public-read", body: data)
obj.public_url
end
S3に保存した画像のURLを取得する
URLを取得して、avatar
に設定します。
user = User.create(email: params[:email], avatar: avatar)
URLをDBに登録する
ユーザー情報をDBに登録します。
user = User.create(email: params[:email], avatar: avatar)
コード全体
Controllerのソースコードは次のようになります。
class UsersController < ApplicationController
def create
avatar = params[:avatar]
uri = URI.parse(avatar)
if uri.scheme == "data" then
data = decode(uri)
extension = extension(uri)
avatar = put_s3 data, extension
end
user = User.create(email: params[:email], avatar: avatar)
render json: user
end
private
def decode(uri)
opaque = uri.opaque
data = opaque[opaque.index(",") + 1, opaque.size]
Base64.decode64(data)
end
def extension(uri)
opaque = uri.opaque
mime_type = opaque[0, opaque.index(";")]
case mime_type
when "image/png" then
".png"
when "image/jpeg" then
".jpg"
else
raise "Unsupport Content-Type"
end
end
def put_s3(data, extension)
file_name = Digest::SHA1.hexdigest(data) + extension
s3 = Aws::S3::Resource.new
bucket = s3.bucket("example-bucket")
obj = bucket.object("avatars/#{file_name}")
obj.put(acl: "public-read", body: data)
obj.public_url
end
end
avatar
がData URIでなければ処理はせず、Data URIであった場合にデコードしてS3に保存し、URLをData URIの代わりにavatar
に格納しています。
avatar
をDBに登録すれば、以降同じようにURLで画像が扱えます。
参考
RFC 2397 – The “data” URL scheme
本コンテンツはクリエイティブコモンズ(Creative Commons) 4.0 の「表示—継承」に準拠しています。