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

参考

元記事はこちら

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