AWS S3へのアップロード方法
静的なコンテンツをAWS S3へアップロードするのはよくある手法です。
例えば画像や動画をS3へアップロードすることがあると思います。
その中でも、ダイレクトアップロードという方法があります。ダイレクトアップロード機能を使うとサーバーに依存せずにアップロードが出来ます。
従来の方法
S3へのアップロードはサーバー側で処理を行い、DBに関連付けを記録することが一般的でした。この方法ではシンプルにアップロードするファイルをPOSTデータとしてWebサーバーへ送信し、WebサーバーでS3へのアップロードを行い、アップロードされたファイルの関連付けをDBへ記録しています。
ダイレクトアップロード
最近ではフロントエンドとバックエンドの分離が進んで来ています。このためダイレクトアップロードという方法でアップロードという手法を用いてS3へアップロードする方法が主流になってきています。
この方法では事前にアップロードに必要な情報を取得しておき、アップロード自体はフロントエンドのJavaScriptのAjax処理に任せて、アップロードした後に関連付け情報のみをサーバー側で行うという構成になります。
S3へのアップロードをサーバー側に依存させずに、フロントで直接アップロードできるのがダイレクトアップロードという機能になります。
実装
今回はバックエンドにRailsAPIサーバーを建てます。
Railsで標準で使えるActiveStorageを使います。
以下のページを参考に導入してください。
Userモデルに1:Nで紐づく画像がアップロードできることを想定します。
class User < ActiveRecord::Base
# Active Storage
has_many_attached :images
end
ActiveStorageを使うとUserモデルと紐づく下記のテーブルが作成されます。
S3へアップロードした画像への関連付けはこのテーブルに保存されます。
- active_storage_variant_records
- active_storage_blobs
- active_storage_attachments
1. アップロードに必要な情報を返すAPI
フロントエンドから直接アップロードするためには、事前にS3への署名付きURLの発行が必要になります。
署名付きURLの発行には以下のパラメータが必要になります。これらはフロントエンド側の処理で計算を行いパラメータとして含める必要があります。
APIサーバー側はレスポンスとしてS3へアプリケーションするのに必要な署名付きURLを返却します。さらにblob_signed_idというアップロード後にDBと関連付けするための情報も必要になります。
もう少し詳しくレスポンスの内容を見てみましょう。
blob_signed_idは先述の通り、このあとアップロードが成功した後にDBと紐づけるために必要なIDになります。
direct_upload_urlはS3への署名付きURLになります。フロントエンドでこのURLに向けてAjax通信でファイルを送信する必要があります。
また、アップロードにはdirect_upload_headersで指定したパラメータをHeader情報に含める必要があります。
このAPIから取得できた情報で実際にはフロントエンド側でアップロードの処理を行いますが、crulコマンドを使ってアップロードのテストを行う事ができます。
curl -X PUT -T "/Users/XX/Desktop/test.png" \
-H Content-Type:"image/png" \
-H Content-MD5:"n4UCchK/eHAXIsO5QVWVCA==" \
-H Content-Disposition:"inline; filename=\"test.png\"; filename*=UTF-8''test.png" \
"http://XXX/activestorage/blobs/uploaded/users/1/user_images/u6dssto3bvwq72bz3bqcztom0f36?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=minio%2F20221009%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20221009T122108Z&X-Amz-Expires=300&X-Amz-SignedHeaders=content-length%3Bcontent-md5%3Bcontent-type%3Bhost&X-Amz-Signature=11caec4677467f3c3410c8e5dceb924330ea967eb97dafff040d3f4e2acb3a0d"
(実装例)
class Api::V1::UsersController < ApplicationController
before_action :authenticate_api_v1_user!
def direct_uploads
# Userでスペースを区切る
prefix = "activestorage/blobs/uploaded/users/#{current_user.id}/user_images/"
key = File.join(prefix, ActiveStorage::Blob.generate_unique_secure_token)
blob = ActiveStorage::Blob.create_before_direct_upload!(
filename: params[:filename],
byte_size: params[:byte_size],
checksum: params[:checksum],
content_type: params[:content_type],
key:,
)
# アップロードとアタッチに必要な情報
@blob_signed_id = blob.signed_id # アップロード後の紐付け(アタッチ)に使うid
@direct_upload_url = blob.service_url_for_direct_upload # アップロードURL
@direct_upload_headers = blob.service_headers_for_direct_upload # アップロードヘッダー
end
end
json.status :ok
json.data do
json.blob_signed_id @blob_signed_id
json.direct_upload_url @direct_upload_url
json.direct_upload_headers @direct_upload_headers
end
2. アップロードしたファイル情報とDBを紐付ける(アタッチする)API
アタッチAPIと名称していますが、フロントエンドでアップロードしたS3のファイルとDBの関連付けを行うAPIになります。
さきほどのアップロードに必要な情報を返すAPIでレスポンスとして取得できたblob_signed_idをパラメータとして送信します。これによりユーザーとアップロードした画像との関連付けをDBへ記録することが出来ます。
(実装例)
class Api::V1::UsersController < ApplicationController
before_action :authenticate_api_v1_user!
def attach_image
blob = ActiveStorage::Blob.find_signed!(params[:blob_signed_id])
current_user.images.attach([blob])
@message = 'Attached the user image'
end
end
json.status :ok
json.message @message
まとめ
フロントエンドはモダンなシステムであればReactやVue.jsなどの構成があると思います。
またWebシステムだけでなく、モバイルアプリケーションのような場合も考えらます。
バックエンドだけでアップロードをすることも可能ですが、APIサーバーのタイムアウトなどを考えると、このようにフロントエンド側でアップロードという時間がかかる処理を行うことはとても合理的です。