ブラウザから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 モデルのコントローラーにアクションを追加しました。

```ruby
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

です

```html
<!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](https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/PresignedPost.html)
- [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](https://developer.mozilla.org/ja/docs/Web/API/Fetch_API/Using_Fetch)
- [Form​Data オブジェクトの利用](https://developer.mozilla.org/ja/docs/Web/Guide/Using_FormData_Objects)