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)
サンプル
参照
- Direct to S3 Image Uploads in Rails | Heroku Dev Center
- CORS(Cross-Origin Resource Sharing)について整理してみた | Developers.IO
覚え書き
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
してください。
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 オブジェクトを用意します。
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 を生成する行を追加します。
# 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
.
.
//= require jquery.ui.widget.js
//= require z.jquery.fileupload
//= require_tree .
確認
rails s
open http://localhost:3000/users/new
> 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 に変更
<%= 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 での手動処理になります。
・・・省略・・・
プログレスバーのスタイルを指定します。
.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>
を追加します。
<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