S3に直接画像をアップロードする（Ruby on Rails）

#やりたいこと
ブラウザからS3に画像を直接アップロードする

#経緯
carrierwaveを使ってアップロード処理を実装していたが、複数枚あげる場合非常に時間がかかる。
ブラウザ　→ Railsアプリ　→ S3
という流れなので当然といえば当然。

「時間がかかるが、待てば無事処理完了」、となればいいが、
今回Herokuを使っており、Herokuには30秒制限があるため30秒を越えると落ちる。

なのでブラウザからS3に直接アップロードすることにした。

[Herokuのドキュメント](https://devcenter.heroku.com/articles/s3#direct-upload)でも4MBを越えるものはダイレクトアップロードを推奨している。

#環境
Ruby 2.5.0
Rails 5.2.0
#手順
Herokuにダイレクトアップロードの手順についてのドキュメントがあったので、それを参考に実装する
[Direct to S3 Image Uploads in Rails](https://devcenter.heroku.com/articles/direct-to-s3-image-uploads-in-rails)

なお、他の言語のドキュメントもある。
[Direct to S3 File Uploads in Node.js](https://devcenter.heroku.com/articles/s3-upload-node)
[Direct to S3 File Uploads in Python](https://devcenter.heroku.com/articles/s3-upload-python)
[Uploading Files to S3 in Java with Play 2](Uploading Files to S3 in Java with Play 2)
[Uploading Files to S3 in PHP](https://devcenter.heroku.com/articles/s3-upload-php)

##アプリケーションの作成

```
$ rails new direct-s3-example
$ cd direct-s3-example

```

##ユーザモデルの作成
scaffoldで作る

```
$ rails generate scaffold user name avatar_url
      invoke  active_record
      create    db/migrate/20140519195131_create_users.rb
      create    app/models/user.rb
      invoke    test_unit
      create      test/models/user_test.rb
      create      test/fixtures/users.yml
      # ...
```

マイグレーション

```
$ rake db:migrate
```
##S3のバケット作成およびアクセスキーの取得
ググれば出てくるので割愛します。

##aws-sdkのインストール

```
gem 'aws-sdk', '~> 2'
```

`$ bundle install`

#バケット名等々の設定
ドキュメントには`.env`を使っている前提で書かれているが今回はもっと簡略化します。

###awsの初期設定ファイルを生成する
```
$ touch config/initializers/aws.rb
```

###バケット名、リージョン、アクセスキー、シークレートキーの設定
**※簡略化のためハードコーディングしてますが、実際はNGです**

```ruby:config/initializers/aws.rb
Aws.config.update({
  region: 'ap-northeast-1',
  credentials: Aws::Credentials.new('xxxxxxxxxxxxxxxx', 'xxxxxxxxxxxxxxxx'),
})

S3_BUCKET = Aws::S3::Resource.new.bucket('xxxxxxxxxxxxxxxx')
```

##Cross Originの設定
ブラウザからS3へのダイレクトアップロードを可能にするためにはCrossOriginの設定が必要。

S3 → バケット名選択 → アクセス権限　→ CORSの設定
![スクリーンショット 2018-04-18 13.22.45.png](https://qiita-image-store.s3.amazonaws.com/0/112614/01a8689c-491c-a59a-514f-b1df48f266f0.png)


以下をコピペ

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


##UsersController.rbの編集
```
class UsersController < ApplicationController
  before_action :set_user, only: [:show, :edit, :update, :destroy]
  before_action :set_s3_direct_post, only: [:new, :edit, :create, :update] # 追加

...

private
  # 追加
  def set_s3_direct_post
    @s3_direct_post = S3_BUCKET.presigned_post(key: "uploads/#{SecureRandom.uuid}/${filename}", success_action_status: '201', acl: 'public-read')
  end
```

##ファイルアップロード用ライブラリの設置
以下二つのライブラリをダウンロードし、`app/assets/javascripts`配下に設置
https://raw.githubusercontent.com/jquery/jquery-ui/master/ui/widget.js

→`app/assets/javascripts/jquery.ui.widget.js`
https://raw.githubusercontent.com/blueimp/jQuery-File-Upload/master/js/jquery.fileupload.js
→`app/assets/javascripts/z.jquery.fileupload.js`
先頭にzをつけることで読み込み順を調整しているみたい。

##viewの編集
javascriptで色々制御するためにclass等々を設定しておく。

```
<%= form_for(@user, html: { class: 'directUpload', data: { 'form-data' => (@s3_direct_post.fields), 'url' => @s3_direct_post.url, 'host' => URI.parse(@s3_direct_post.url).host } }) do |f| %>
```

text_fieldをfile_fieldに変更する。

```
<div class="field">
  <%= f.label :avatar_url %><br>
  <%= f.file_field :avatar_url %> # もともとはf.text_field
</div>
```

##javascript、styleの設定
今回は簡略化のために`_form.html.erb`に直接書いた。

script。ファイルを選択したタイミングでS3にアップロードしてくれる処理。アップロードに成功したらその画像のパスがhidden項目に入ったinput要素を生成してくれる。

```
<script>
$(function() {
  $('.directUpload').find("input:file").each(function(i, elem) {
    var fileInput    = $(elem);
    var form         = $(fileInput.parents('form:first'));
    var submitButton = form.find('input[type="submit"]');
    var progressBar  = $("<div class='bar'></div>");
    var barContainer = $("<div class='progress'></div>").append(progressBar);
    fileInput.after(barContainer);
    fileInput.fileupload({
      fileInput:       fileInput,
      url:             form.data('url'),
      type:            'POST',
      autoUpload:       true,
      formData:         form.data('form-data'),
      paramName:        'file', // S3 does not like nested name fields i.e. name="user[avatar_url]"
      dataType:         'XML',  // S3 returns XML if success_action_status is set to 201
      replaceFileInput: false,
      progressall: function (e, data) {
        var progress = parseInt(data.loaded / data.total * 100, 10);
        progressBar.css('width', progress + '%')
      },
      start: function (e) {
        submitButton.prop('disabled', true);

        progressBar.
          css('background', 'green').
          css('display', 'block').
          css('width', '0%').
          text("Loading...");
      },
      done: function(e, data) {
        submitButton.prop('disabled', false);
        progressBar.text("Uploading done");

        // extract key and generate URL from response
        var key   = $(data.jqXHR.responseXML).find("Key").text();
        var url   = '//' + form.data('host') + '/' + key;

        // create hidden field
        var input = $("<input />", { type:'hidden', name: fileInput.attr('name'), value: url })
        form.append(input);
      },
      fail: function(e, data) {
        submitButton.prop('disabled', false);

        progressBar.
          css("background", "red").
          text("Failed");
      }
    });
  });
});
</script>
```

スタイル。あってもなくてもいい。

```
<style>
.progress {
  max-width: 600px;
  margin:    0.2em 0 0.2em 0;
}

.progress .bar {
  height:  1.2em;
  padding-left: 0.2em;
  color:   white;
  display: none;
}
</style>
```


これでひとまず完成。
ファイルを選択すると自動でS3にアップしてくれて、かつプログレスバーが表示される。

![スクリーンショット 2018-04-18 12.17.14.png](https://qiita-image-store.s3.amazonaws.com/0/112614/8f5c51fb-028b-8305-babe-953c57c5d8f5.png)


hidden項目に画像のパスが入っているので、この状態でサブミットすればユーザモデルのavatar_urlカラムに画像のパスが入る。
表示するときはこれで表示できる。
`<%= image_tag @user.avatar_url %>`

#画像を複数枚アップしたいとき
Herokuのドキュメントは1枚の画像をあげる想定でつくられている。

複数枚アップロードするときは単純に`multiple: true`を追記してあげればOK。

```
<%= form.file_field :avatar_url, multiple: true %>
```

ただ、これだとS3へのアップはできるが、モデルとの紐付けができないので何か方法を考えないといけない。
また、carrierwaveを使用しなくなったのでサムネイルの作成処理も別途考えないといけない。