Edited at

ブラウザからS3へのダイレクトアップロード

はじめまして、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を返すアクションを設定

まずは環境変数に必要な値を持たせておきます。


.env

AWS_ACCESS_KEY_ID=your-key-id

AWS_SECRET_ACCESS_KEY=your-secret-key
BUCKET=your-bucket-name

次にGemfileに


Gemfile

gem `aws-sdk', '~3'


を追加して

$ bundle install

そして環境変数に保存した値を使ってS3のインスタンスを作成します。


config/initializers/aws.rb

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へのダイレクトアップロードを実装することができました。


参考