ブログ

ryuzeeによるブログ記事。不定期更新

直近開催のScrum Alliance認定スクラムマスター研修のご案内

AzureのBlobサービスにブラウザから直接ファイルをアップロードする

こんにちは。@ryuzeeです。

ずっと趣味で作っているスライド共有アプリケーションはAWS専用なのですが、Azureにも対応させようとして色々Azureを触っています。

そこで今回は、AzureのBlobサービス(AWSのS3相当)にブラウザから直接ファイルをアップロードする方法について調査したので共有します。

下準備

Azure上のBlobの画面などで、適当なコンテナを作成してください(S3のバケット相当のものです)。アクセスポリシーはプライベートに設定します(でないとアップロードしたファイルが外界から認証なしでアクセスされてしまいます)。

必要なgemのインストール

AzureのAPIを叩くために必要なgemをインストールします。適当なディレクトリで以下のようにコマンドを実行してください。

bundle init

Gemfileが作成されるので以下の内容にします。

source "https://rubygems.org"

gem 'azure', '~> 0.7.1'
gem 'azure-contrib', git: 'https://github.com/dmichael/azure-contrib.git'
gem 'sinatra'
gem 'sinatra-contrib'

gemのうち上2つが、直接ファイルをアップロードする上で必要なgemです。azureは公式のSDKで、azure-contribは第三者が作っている拡張用のgemになります。バージョン指定しておかないと色々面倒なことになるので明示的にバージョンを指定しています。

下2つは、このあと作成するWebアプリに使うsinatraをインストールするものです。

環境変数の設定

AzureのBlobストレージをいじくる上で必要な環境変数を設定しましょう。 設定が必要なのは、Azureのストレージにアクセスするためのアクセスキー、ストレージアカウントの名前、そして実際にファイルを保存するコンテナ名です。自分の環境にあわせて変更して設定します。

export AZURE_STORAGE_ACCESS_KEY=xxxxxxxxxxxxx
export AZURE_STORAGE_ACCOUNT_NAME=openslideshare
export CONTAINER_NAME=files

CORS(Cross-Origin Resource Sharing)を設定する

Ajax経由でファイルをアップロードするので、CORSを設定します。 Azureの場合はGUIでは設定できないので、SDKを使ってコードで実施します。ファイル名は適当なもので構いませんが、今回は、set_cors.rbとしておきましょう。

require 'azure'

Azure.config.storage_account_name = ENV['AZURE_STORAGE_ACCOUNT_NAME']
Azure.config.storage_access_key = ENV['AZURE_STORAGE_ACCESS_KEY']

blob_service = Azure::Blob::BlobService.new
props = Azure::Service::StorageServiceProperties.new

props.logging = nil
props.hour_metrics = nil
props.minute_metrics = nil

rule = Azure::Service::CorsRule.new
rule.allowed_headers = ["*"]
rule.allowed_methods = ["PUT", "GET", "HEAD", "POST"]
rule.allowed_origins = ["*"]
rule.exposed_headers = ["*"]
rule.max_age_in_seconds = 1800

props.cors.cors_rules = [rule]
blob_service.set_service_properties(props)

puts blob_service.get_service_properties.inspect

ここまでできたら

bundle exec ruby set_cors.rb

としてください。

なお、CORSはストレージアカウントに対しての設定になっているようです(Container単位にできないのかは要調査)

SAS(Shared Access Signature)を取得する

SASは有効期限付きのワンタイムURLみたいなものです。発行するには以下の内容で適当なrubyスクリプトを用意して実行します。ここでは、アップロードファイル名は、hogehogeに固定されています。

require 'azure'
require 'azure-contrib'

Azure.config.storage_account_name = ENV['AZURE_STORAGE_ACCOUNT_NAME']
Azure.config.storage_access_key = ENV['AZURE_ACCESS_KEY']

def generate_url(container_name, blob_name, permissions)
  start_time = Time.now - 10
  expiration_time = Time.now + 1800
  bs = Azure::Blob::BlobService.new
  uri = bs.generate_uri Addressable::URI.escape("#{container_name}/#{blob_name}")

  signer = Azure::Contrib::Auth::SharedAccessSignature.new(uri, {
    resource:    "b",
    permissions: permissions,
    start:       start_time.utc.iso8601,
    expiry:      expiration_time.utc.iso8601
  }, Azure.config.storage_account_name)

  url = signer.sign
  url
end

puts generate_url('files', 'hogehoge', 'w')

これを実行すると以下のようなURLができ上がります。

http://openslideshare.blob.core.windows.net/files/hogegoge?se=2016-02-06T03%3A28%3A42Z&sig=%2FFr80o0c7W1%2BITXqg7DCcfnx2sL4zhq1op6nJOY%2Flsc%3D&sp=rw&sr=b&st=2016-02-06T02%3A58%3A32Z

このURLに対してファイルをPUTすればOKです。以下のようにして試してみましょう。

curl -X PUT "http://openslideshare.blob.core.windows.net/files/hogehoge?se=2016-02-06T04%3A43%3A09Z&sig=HewO2DhvwKASDg6LCF0GNhvATQA0Lfl3J0nPsKNCJ9k%3D&sp=w&sr=b&st=2016-02-06T04%3A12%3A59Z" -F "file=~/Desktop/hogehoge;type=application-octetstream"

これでAzureのBlobの指定したコンテナ内にファイルができていればOKです!

注意点

注意点としてはSASのURLを取得する時点でBlobに保存する際のファイル名が必要になる点で、ユーザーがアップロードしたファイルの名前を元にしたい場合は、JavaScriptなどとの組み合わせで先にファイル名取得→SASのURLを発行という流れを経てからファイルのアップロードに進まないといけないことになります。

アップロードフォームを作ってみる

ここまで来たら後はWebアプリでアップロードできるようにしましょう。前述の通り、Sinatraを使ってWebアプリを作ってみます。これからカレントディレクトリにファイルを作っていきましょう。

app.rb (メイン部分)

# -*- coding: utf-8 -*-

require 'sinatra'
require 'sinatra/reloader'
require 'azure'
require 'azure-contrib'
require 'uri'
require 'json'
Azure.config.storage_account_name = ENV['AZURE_STORAGE_ACCOUNT_NAME']
Azure.config.storage_access_key = ENV['AZURE_STORAGE_ACCESS_KEY']

get '/' do
  erb :form
end

post '/sas' do
  filename = params[:filename]
  url = generate_url(ENV['CONTAINER_NAME'], filename, 'w')
  puts url
  content_type :json
  data = { url: url }
  JSON.dump(data)
end

def generate_url(container_name, blob_name, permissions)
  start_time = Time.now - 10
  expiration_time = Time.now + 1800
  bs = Azure::Blob::BlobService.new
  uri = bs.generate_uri Addressable::URI.escape("#{container_name}/#{blob_name}")

  signer = Azure::Contrib::Auth::SharedAccessSignature.new(uri, {
    resource:    "b",
    permissions: permissions,
    start:       start_time.utc.iso8601,
    expiry:      expiration_time.utc.iso8601
  }, Azure.config.storage_account_name)

  url = signer.sign
  url
end

views/form.erb (View部分)

<html>
<head>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
</head>
<body>

<div>
  <form id="upload-form" method="post" enctype="multipart/form-data">
    <div>
      <label class="control-label col-sm-2"><span>File</span></label>
      <div>
        <input type="file" name="file" id="file" />
    </div>
  </div>
</div>

<span id="progress">0%</span>

<script type="text/javascript">
$(document).ready(function() {
  $('#file').on("change", function(event) {
    var file = this.files[0];
    if(file != null) {
      console.log(file.name);
    } else {
      return;
    }
    event.preventDefault();

    url = '';
    var formDataSAS = new FormData();
    formDataSAS.append('filename', file.name);

    $.ajax({
      type: 'POST',
      url: '/sas',
      dataType: 'json',
      data: formDataSAS,
      async: false,
      cache: false,
      contentType: false,
      processData: false
    }).done(function( data, textStatus, jqXHR ) {
      console.debug(data.url);
      url = data.url;
    }).fail(function( jqXHR, textStatus, errorThrown ) {
      console.log('Fail...');
    });

    if (url == '') {
      alert('Could not get SAS url...')
      return false;
    }

    var formData = new FormData();
    var form = $('#upload-form');
    $(form.serializeArray()).each(function(i, v) {
      if(v.name != "file") {
        formData.append(v.name, v.value);
      }
    });
    formData.append("file", $("#file").prop("files")[0]);

    $.ajax({
      url: url,
      type: 'PUT',
      contentType: 'application/octet-stream',
      data: formData,
      async: true,
      crossDomain: true,
      xhr: function() {
        xhr = $.ajaxSettings.xhr();
        xhr.upload.addEventListener("progress", function(evt) {
          if (evt.lengthComputable) {
            var percentComplete = evt.loaded / evt.total;
            var p = Math.round(percentComplete * 100);
            $("#progress").html(p + "%");
          }
        }, false);
        return xhr;
      },
      statusCode: {
        201: function(){
          console.log("201:OK");
        },
        403: function(){
          console.log("403:Forbidden");
        },
        404: function(){
          console.log("404:NOT Found");
        },
        405: function(){
          console.log("405:Authentication Error");
        }
      },
      cache: false,
      contentType: false,
      processData: false
    }).done(function( data, textStatus, jqXHR ) {
      alert('Success');
    }) .fail(function( jqXHR, textStatus, errorThrown ) {
      alert('Fail...');
    });
    return false;
  });
});
</script>
</body>
</html>

アプリケーションの起動

ここまでできたらSinatraのアプリケーションを起動しましょう。以下のようにします。

bundle exec ruby app.rb

以下のような感じでログが出力されます。4567番ポートでListenしているのが分かります。

I, [2016-02-06T20:13:12.143616 #19922]  INFO -- : Celluloid 0.17.3 is running in BACKPORTED mode. [ http://git.io/vJf3J ]
[2016-02-06 20:13:12] INFO  WEBrick 1.3.1
[2016-02-06 20:13:12] INFO  ruby 2.2.3 (2015-08-18) [x86_64-darwin14]
== Sinatra (v1.4.7) has taken the stage on 4567 for development with backup from WEBrick
[2016-02-06 20:13:12] INFO  WEBrick::HTTPServer#start: pid=19922 port=4567

ブラウザでのアクセス

http://localhost:4567 にアクセスすると以下のような(無味乾燥な)画面が表示されますので適当なファイルを指定してみてください。

アップロードが終わったらAzure側の画面を確認して、BLOBが生成されているかを確認しましょう。

まとめ

AWSでもAzureでも、大きなファイルをごにょごにょする際に、一端仮想マシン側でPOSTで受け付けるようなことをしてしまうと、仮想マシンが止まった場合に問題が起こりやすく、仮想マシンの負荷が増えたりまた転送料金が高くなったりします。したがってこのような仕掛けを使ってファイルを直接ストレージに配置することはベスト・プラクティスの1つになります。 ぜひ試してみてください。

課題

ファイルサイズが64MBを超えると、以下のエラーが出力されます。

413 (The request body is too large and exceeds the maximum permissible limit.)

これについては、こちらのドキュメントに記載があります。

The maximum size for a block blob created via Put Blob is 64 MB. If your blob is larger than 64 MB, you must upload it as a set of blocks. For more information, see the Put Block and Put Block List operations. It's not necessary to also call Put Blob if you upload the blob as a set of blocks.

ということで、単一ブロックではなく、複数ブロックに分割してアップロードが必要なようです。

アジャイル開発チーム向けのコーチングや、技術顧問、Scrum Alliance認定スクラムマスター研修などのトレーニングを提供しています。お気軽にご相談ください(初回相談無料)
前の記事 こんなスクラムには気をつけろ!?
次の記事 【続編:64MB超え】AzureのBlobサービスにブラウザから直接ファイルをアップロードする

プロダクト開発で、こんな課題を感じていませんか?

  • 何を作るべきか、順位の決め方が定まらない
  • プロダクトの方向性をチームで共有できていない
  • 開発組織の体制や役割がうまく機能していない
  • 開発プロセスが形骸化し、目的を見失っている
  • アジャイルを導入したが、組織に定着しない

プロダクトマネジメント、組織構造、開発プロセスの課題について、組織全体の視点から支援します。

お問い合わせ(初回相談無料)

契約を前提にした相談でなくて構いません。相談に際して事前の整理や準備は不要です。

Aligned ―プロダクト開発におけるステークホルダーとの関係性の築き方
ダイナミックリチーミング 第2版
Tidy First?
脳に収まるコードの書き方
プロダクトマネージャーのしごと 第2版
エンジニアリングマネージャーのしごと
チームトポロジー
スクラム実践者が知るべき97のこと
プロダクトマネジメント
SCRUM BOOT CAMP THE BOOK
みんなでアジャイル
レガシーコードからの脱却
Effective DevOps
変革の軌跡
ジョイ・インク
アジャイルコーチの道具箱
カンバン仕事術
Software in 30 Days
How to Change the World