【Rails】ブラウザからS3へ画像を直接アップロード

[こちら](http://qiita.com/yuku_t/items/40b7daf018d3dab48974)を参考に, 画像をブラウザからS3へ直接アップロードできるようにしました.

# 1. AWSの準備 (時間:5分)

IAMでS3アクセス用のユーザを作成します. まずは以下のようにコンソール画面からIAMへアクセス.

<img width="500" alt="ss 2017-09-18 12.44.57.png" src="https://qiita-image-store.s3.amazonaws.com/0/80357/7ffd7963-918d-d7ee-e328-7fd87975ca27.png">

次に, 以下のUsersをクリック
<img width="500" alt="ss 2017-09-18 12.48.08.png" src="https://qiita-image-store.s3.amazonaws.com/0/80357/7d7bbddb-7fea-651a-ab6a-a60c39b0e40e.png">

Add userをクリック
<img width="500" alt="ss 2017-09-18 12.48.16.png" src="https://qiita-image-store.s3.amazonaws.com/0/80357/c7520ae6-0315-5333-b687-77fa65d2e3fb.png">

任意の名前を入力. プログラマティックアクセスにチェックして次へ.
<img width="500" alt="ss 2017-09-18 12.48.37.png" src="https://qiita-image-store.s3.amazonaws.com/0/80357/9ba6d82b-4065-9a85-5b63-18944c2d5548.png">

既存ポリシーの中から AmazonS3FullAccessを見つけてチェック.
<img width="500" alt="eee" src="https://qiita-image-store.s3.amazonaws.com/0/80357/86d326f3-9174-621b-3c34-7535e3dd7a5b.png">

以上でユーザ作成は完了. 詳細画面でCreate Access keyをクリックして必要なキーを取得する.
<img width="500" alt="aaa" src="https://qiita-image-store.s3.amazonaws.com/0/80357/aca0cdc0-0c55-794c-0812-afe4b63bb43c.png">

このようにkeyIDとシークレットkeyが表示されるので, メモっておく.
<img width="500" alt="ss 2017-09-18 12.50.27.png" src="https://qiita-image-store.s3.amazonaws.com/0/80357/030f4b36-ef35-7119-06f9-a83d673c3622.png">

次にS3の設定を行う.

適当なバケットを作成する. 各種設定はデフォルトのままで良い. 作成し終えたら, そのバケットのプロパティを開き, Permissions -> CORS configurationに以下のように記述する. これで終わり.

<img width="500" alt="ssd" src="https://qiita-image-store.s3.amazonaws.com/0/80357/6c4bd597-4055-1418-0828-d94b00dc9b49.png">

```
<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<CORSRule>
    <AllowedOrigin>*</AllowedOrigin>
    <AllowedMethod>GET</AllowedMethod>
    <AllowedMethod>POST</AllowedMethod>
    <AllowedMethod>PUT</AllowedMethod>
    <MaxAgeSeconds>3000</MaxAgeSeconds>
    <AllowedHeader>*</AllowedHeader>
</CORSRule>
</CORSConfiguration>
```

# 2. バックエンドでS3ポリシーを作成

自身のRailsアプリに, POSTメソッドで叩ける適当なエンドポイントを作成する. 

`routes.rb`に例えば以下のように記述.

```ruby
post 'image_upload', to: 'images#upload'
```

そして, `images#upload`を次のように定義する.

```ruby
class ImagesController < ApplicationController
  # Deviceを使って認証処理をしている場合, 次の1行によって認証を要求できる.
  before_action :authenticate_user!, only: [:upload]

  # 先ほど作ったバケットの名前とアクセスKeyIdとシークレットkey
  S3_BUCKET = 'your-bucket-name'
  AWS_ACCESS_KEY_ID = 'XXX'
  AWS_SECRET_KEY = 'XXX'

  def upload
    # アップロード後のファイル名
    key = 'hogehoge'

    acl = 'public-read'
    ctype = params[:content_type]

    # ポリシー作成
    policy_document = {
      # 1分間のみ有効
      expiration: (Time.now + 1.minute).utc,
      conditions: [
        # アップロード先のS3バケット
        { bucket: S3_BUCKET },
        # ファイルの権限
        { acl: acl },
        # ファイル名
        { key: key },
        # ファイルの形式
        { 'Content-Type' => ctype },
        # アップロード可能なファイルのサイズ
        ['content-length-range', params[:size], params[:size]]
      ]
    }.to_json
    policy = Base64.encode64(policy_document).gsub("\n", '')

    # signatureの作成
    signature = Base64.encode64(
        OpenSSL::HMAC.digest(
            OpenSSL::Digest::Digest.new('sha1'),
            AWS_SECRET_KEY, policy)).gsub('\n', '')

    # アップロードに必要な情報をJSON形式でクライアントに返す
    render json: {
      url: "https://#{S3_BUCKET}.s3.amazonaws.com/",
      form: {
        AWSAccessKeyId: AWS_ACCESS_KEY_ID,
        signature: signature,
        policy: policy,
        key: key,
        acl: acl,
        'Content-Type' => ctype
      }
    }
  end
end
```

# 3. フロントエンドでポリシー取得しアップロード実行

画像をアップロードしたいViewに, 以下のようなjavascriptを仕込みます.

```html

画像を投稿します

<div style="width: 500px">
  <form enctype="multipart/form-data" method="post">
    <input type="file" name="userfile" accept="image/*">
  </form>
</div>

<div id="thumbnail" style="max-width: 100px;">
  <img src="/plus.png" id="image_to_upload">
</div>

<button class="btn btn-primary" id="upload">投稿</button>

<script type="text/javascript">
$(function() {
  var file = null;

  // アップロードするファイルを選択
  $('input[type=file]').change(function() {
    file = $(this).prop('files')[0];
    // 画像以外は処理を停止させるためファイル形式をチェック
    if (file.type != 'image/jpeg' && file.type != 'image/png') {
      // 画像でない場合は消す
      var img_src = $('<img>').attr('src', '/plus.png')
      $('#thumbnail').html(img_src);
      file = null
      return;
    }

    // サムネ表示
    var reader = new FileReader();
    reader.onload = function() {
      var img_src = $('<img>').attr('src', reader.result)
      img_src.css('width', '500px');
      $('#thumbnail').html(img_src);
    }
    reader.readAsDataURL(file);
  });

  // アップロードボタンクリック
  $('#upload').click(function(){
    // ファイルが指定されていなければ何も起こらない
    if(!file) {
      return;
    }

    // ポリシーを発行する
    $.ajax({
      url: 'http://localhost:3000/image_upload',
      type: 'POST',
      data: {
        content_type: file.type,
        size: file.size
      }
    })
    .done(function( data, textStatus, jqXHR ) {
      // 取得したポリシーをフォームデータの形に整形する
      var name, fd = new FormData();
      for (name in data.form) if (data.form.hasOwnProperty(name)) {
        fd.append(name, data.form[name]);
      }
      fd.append('file', file); // ファイルを添付

      $.ajax({
        url: data.url,
        type: 'POST',
        dataType: 'json',
        data: fd,
        processData: false,
        contentType: false
      })
      .done(function( data, textStatus, jqXHR ) {
        console.log('success!')
      })
      .fail(function( jqXHR, textStatus, errorThrown ) {
        // アップロード時のエラー
        console.log('error: 2'); 
      });  

    })
    .fail(function( jqXHR, textStatus, errorThrown ) {
      // ポリシー取得時のエラー
      console.log('error: 1');
    });

  });

});
</script>
```

次のようなUIが出来上がります. 「ファイルを選択」をクリックしてファイルを適当に選択すると, サムネイルが表示されます. この状態で投稿ボタンを押すと画像がS3へ直接アップロードされます.
<img width="500" alt="ss 2017-09-18 13.16.07.png" src="https://qiita-image-store.s3.amazonaws.com/0/80357/d6e23d56-8573-41df-1737-ea62e778f606.png">


以上です.