Help us understand the problem. What is going on with this article?

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

More than 5 years have passed since last update.

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 で設定/取得したものを書いてください。

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
usutani
好物は Ruby on Rails や iOS アプリの開発です。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした