49
53

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Railsで(大きな)画像をS3に直接アップロード

Posted at

Heroku Dev Center「Direct to S3 Image Uploads in Rails」の覚え書きです。

環境

  • Rails 4.1.8
  • heroku-toolbelt 3.16.0
  • aws-sdk 1.59.0
  • JQuery UI (1.11.2)
  • jQuery File Upload Plugin (9.8.0)

サンプル

参照

覚え書き

Philosophy

この記事では jQuery-File-Uploadプラグインと AWS gem を用います。
画像をS3に直接アップロードできる CarrierWaveDirect のような他のライブラリもありますが、クライアント側に関する低レベルの知識なしでのそれらの実装は困難です。

jQuery-File-Upload プラグインを用いると、JavaScript のコードは比較的読みやすく短いものになり、そのコードを任意の画像アップロード入力フォームとして再利用することもできます。
また、ユーザインタフェースは非常にカスタマイズ可能です。
Rails側では、AWSで事前に署名したPOSTを生成し、画像のURLをデータベースに保存します。

Example app

このアプリでは User モデルを持ち、アバター画像をユーザー単位で S3 に保存したいと思います。

rails new direct-s3-example
cd direct-s3-example
rails generate scaffold user name avatar_url
rake db:migrate

S3

ファイルを S3 に送る前に、S3 のアカウントと、適切に構成されたバケットが必要になります。

S3 SDK

次に Ruby で S3 とやりとりするためのライブラリが必要になります。
aws-sdk を Gemfile に追加して bundle install してください。

Gemfile
gem 'aws-sdk'

ローカルの開発では、.env ファイルと Foreman を使います。
次のように環境変数を追加してください。
尚、下記の値は例ですので、S3 で設定/取得したものを書いてください。

.env
S3_BUCKET=my-s3-development
AWS_ACCESS_KEY_ID=EXAMPLEKVFOOOWWPYA
AWS_SECRET_ACCESS_KEY=exampleBARZHS3sRew8xw5hiGLfroD/b21p2l

確認

$ foreman run rails runner "puts ENV['S3_BUCKET']"
my-s3-development

環境変数を設定できたら、コントローラーから使えるように S3 オブジェクトを用意します。

config/initializers/aws.rb
AWS.config(access_key_id:     ENV['AWS_ACCESS_KEY_ID'],
           secret_access_key: ENV['AWS_SECRET_ACCESS_KEY'] )

S3_BUCKET = AWS::S3.new.buckets[ENV['S3_BUCKET']]

Cross origin support

(参考)CORS(Cross-Origin Resource Sharing)について整理してみた

環境に合わせてバケットの CORS 設定を変更します。
適切なオリジンを AllowedOrigin に設定しましょう。

<?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>
        <AllowedHeader>*</AllowedHeader>
    </CORSRule>
</CORSConfiguration>

Pre-signed post

AWS ruby gem で事前に署名した(Pre-signed)POSTを生成します。

あなたのユーザーや顧客が特定のオブジェクトをあなたのバケットにアップロードしたい場合に、事前に署名したURLは役立ちます。詳しくは Class: AWS::S3::PresignedPost をご覧ください。

users_controller.rb に pre-signed post を生成する行を追加します。

app/controllers/users_controller.rb
  # GET /users/new
  def new
    @s3_direct_post = S3_BUCKET.presigned_post(
      key: "uploads/#{SecureRandom.uuid}/${filename}",
      success_action_status: 201,
      acl: :public_read)
    @user = User.new
  end

Client side code

一時的なキャッシュとしてサーバーに頼ることはできないので、S3 にファイルを配信するために、クライアント側のコード(JavaScript)を使用しなければなりません。

HTML 5はファイルAPIを導入していますが、IE 10までサポートされませんでした。これを回避するために、jQuery File Upload を使います。これを使うには先に JQuery UI が必要になります。

  • JQuery UI
  • jQuery File Upload

Rails プロジェクトに JavaScript ファイルを取り込みます。

$ curl \
https://raw.githubusercontent.com/jquery/jquery-ui/master/ui/widget.js \
>> app/assets/javascripts/jquery.ui.widget.js

$ curl \
https://raw.githubusercontent.com/blueimp/jQuery-File-Upload/master/js/jquery.fileupload.js \
>> app/assets/javascripts/z.jquery.fileupload.js
app/assets/javascripts/application.js
.
.
//= require jquery.ui.widget.js
//= require z.jquery.fileupload
//= require_tree .

確認

rails s
open http://localhost:3000/users/new
JavaScriptコンソール
> console.log($().fileupload)
function ( options ) {
          var isMethodCall = typeof options === "string",
               args = slice.call( arguments, 1 ),
               returnValue = this;
//...

Prepare the view

  • form_for に directUpload クラスを追加
  • avatar_url を f.file_field に変更
app/views/users/_form.html.erb
<%= form_for(@user, html: { class: "directUpload" }) do |f| %>
  <% if @user.errors.any? %>
.
.
<div class="field">
  <%= f.label :avatar_url %><br>
  <%= f.file_field :avatar_url %>
</div>

Detecting file field on the client side

以下のものが用意できました。

  • S3 バケット
  • 有効な pre-signed post オブジェクト
  • User モデル(avatar_url、ファイル入力)

ユーザーの画像を S3 から取得したり、avatar_url に URL を保存する必要がありますが、これは主に JavaScript での手動処理になります。

・・・省略・・・

プログレスバーのスタイルを指定します。

app/assets/stylesheets/screen.css
.progress {
  max-width: 600px;
  margin:    0.2em 0 0.2em 0;
}

.progress .bar {
  height:  1.2em;
  padding: 0.2em;
  color:   white;
  display: none;
}

Finished jquery-file-upload code

・・・省略・・・

jQuery-File-Upload callbacks

タグ <script></script> を追加します。

app/views/users/new.html.erb
<h1>New user</h1>

<%= render 'form' %>

<%= link_to 'Back', users_path %>

<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:             '<%= @s3_direct_post.url %>',
      type:            'POST',
      autoUpload:       true,
      formData:         <%= @s3_direct_post.fields.to_json.html_safe %>,
      paramName:        'file',
      dataType:         'XML',
      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   = '//<%= @s3_direct_post.url.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>

備考

# .envの環境変数を参照するためforemanで起動
foreman run rails server
# 確認
aws --profile foo s3 ls s3://aaa-bbb-ccc-1/uploads/
# 全て削除(注意!)
aws --profile bar s3 rm s3://aaa-bbb-ccc-1/uploads/ --recursive
49
53
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
49
53

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?