はじめまして、streampackチームのminsuです。
やりたいこと
S3へファイルをアップロードする際に、Railsサーバを通すことなくブラウザからS3へのダイレクトアップロードを実装してみます。
ブラウザからS3にファイルを直接アップロードすることにより、余分な負荷を削減できるメリットがあります。
また、Railsのgem aws-sdkを利用して生成したpresigned POSTを利用することでブラウザにaws credentialsを持たせる事なくアップロードを行えます。
AWSリソースの準備
まず、AWSアクセスキーを作成してACCESS_KEY_ID, SECRET_ACCESS_KEYを取得してください。
次にS3のバケットの作成します。
作成したバケットのCORSの設定を行い、外部からのPOSTを許可します。
<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<CORSRule>
<AllowedOrigin>*</AllowedOrigin>
<AllowedMethod>GET</AllowedMethod>
<AllowedMethod>PUT</AllowedMethod>
<AllowedMethod>POST</AllowedMethod>
<MaxAgeSeconds>3000</MaxAgeSeconds>
<AllowedHeader>*</AllowedHeader>
</CORSRule>
</CORSConfiguration>
AllowedOrigin、AllowedHeaderはワイルドカードを設定しましたが、環境に合わせて変更してください。
Railsでpresigned POSTを返すアクションを設定
まずは環境変数に必要な値を持たせておきます。
AWS_ACCESS_KEY_ID=your-key-id
AWS_SECRET_ACCESS_KEY=your-secret-key
BUCKET=your-bucket-name
次にGemfileに
gem `aws-sdk', '~3'
を追加して
$ bundle install
そして環境変数に保存した値を使ってS3のインスタンスを作成します。
Aws.config.update({
region: 'ap-northeast-1',
credentials: Aws::Credentials.new(ENV['AWS_ACCESS_KEY_ID'], ENV['AWS_SECRET_ACCESS_KEY']),
})
S3_BUCKET = Aws::S3::Resource.new.bucket(ENV['S3_BUCKET'])
GET 要求に対して、 ブラウザから S3 へ POST するために必要な情報を返すアクションを実装します。
今回は video モデルのコントローラーにアクションを追加しました。
VideosController < ApplicationController
def upload
filename = params[:filename]
filetype = params[:filetype]
post = S3_BUCKET.presigned_post(
key: "upload_video/#{filename}",
acl: 'public-read',
content_type: filetype,
metadata: {
'original-filename' => filename
}
})
render json: {url: post.url,fields: post.fields}
end
end
バケット内の保存先はkey:
で指定するので、この値をDBに保存してモデルと紐づけることが可能です。
GET リクエストで filename,filetype パラメータ受け取ったuploadアクションは以下のpresigned POSTとして次のjsonを返します。
{
"url": "https://your-bucket-name.s3.ap-northeast-1.amazonaws.com",
"fields": {
"key": "upload_video/test.mp4",
"acl": "public-read",
"Content-Type": "video/mp4",
"x-amz-meta-original-filename": "test.mp4",
"policy": "eyJleHBpc...",
"x-amz-credential": "oiMjAxO...",
"x-amz-algorithm": "AWS4-HMAC-SHA256",
"x-amz-date": "20190607T004657Z",
"x-amz-signature": "mF0aW9uIj..."
}
}
ブラウザページの作成
動作としては
- RailsにGETリクエストを送ってpresigned POSTを受け取る
- presigned POSTを使ってS3へPOST
- 実装はfetch api
です
<!DOCTYPE html>
<html>
<head>
<title>S3 POST Form</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<input type="file" id="up_file">
<br><input type="button" id="send" onclick="upload();" value="アップロード">
<!-- fetch api -->
<script>
function upload(){
const up_files = document.getElementById('up_file');
const up_file = up_files.files[0];
if (up_files.value === "") {
return false;
}
const url= 'http://localhost:3000/api/v1/video_upload/get_post_fields?filename=' + up_file.name + "&filetype=" + up_file.type;
// Rails に GET
console.log("GET 開始");
fetch(
url,
{method: 'GET'}
).then(response => {
if(response.ok){
console.log("GET 成功");
return response.json();
}
}).then((data)=>{
formdata = new FormData()
for (key in data.fields) {
formdata.append(key,data.fields[key]);
}
formdata.append("file",up_file);
const headers = {
"accept": "multipart/form-data"
}
// S3 に POST
console.log("POST 開始");
fetch(
data.url,
{
method: 'POST',
headers,
body: formdata
}
).then((response) => {
if(response.ok){
console.log("POST 成功");
return response.text();
}
})
});
}
</script>
</body>
</html>
これでブラウザからのS3へのダイレクトアップロードを実装することができました。
参考
- Class: Aws::S3::PresignedPost
- [Creating an HTML Form (Using AWS Signature Version 4)]
(https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/API/sigv4-HTTPPOSTForms.html) - Fetch を使う - Web API - MDN - Mozilla
- FormData オブジェクトの利用