ブログ

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

【続編:64MB超え】AzureのBlobサービスにブラウザから直接ファイルをアップロードする

こんにちは。@ryuzeeです。

前回、AzureのBlobサービスにブラウザから直接ファイルをアップロードするという話を書きましたが、残念ながら前回の実装は、AzureのBlobサービスのPUTの制限で64MBまでしか登録することができませんでした。

そこで今回は、AzureのBlobサービスにブラウザ経由で直接64MB超えのファイルを登録する方法を紹介します。なお、Rubyスクリプトの部分(CORSの設定やSASの取得など)は前回と変更ありませんので、フォームのみの修正です。

細かいところはコードを見てもらうとして、分割アップロードの実装において注意すべき点を列挙します。

  • 64MBを超えるファイルをアップロードしたい場合は、最大4MBのファイルのブロックに分割してPUTします。
  • 分割したブロックをPUTする際には、生成したSASのURLの末尾に、comp=block&blockid=ブロックIDを追加しないといけません。
  • ブロックIDは、base64エンコードされた文字列である必要があります。さらに、複数のブロックがある場合、全てのブロックのブロックIDの文字列長は同じでなければいけません。
  • 全てのブロックを送信し終わったら、ブロックリストをPUTする必要があります。URLは、SASの末尾にcomp=blocklistを追加します。
  • ブロックリストの送信の本文はXMLで、それまでに送信したブロックIDを列挙します。
  • ブロックリストの送信の際は、HTTPヘッダーに、x-ms-versionの設定が必要です。

その他、前回の話も含めて検討した方がよさそうな箇所についても列挙します。

  • SASを使って認証URLを取得しますが、このURLを知っていれば有効期間中は、当該ファイル名での書き込みができます。すなわち認証URLの有効期限はあまり長くしてはいけません。
  • いくらJavaScriptで制御したとしても、ユーザーがSASのURLを知るのは簡単なので、巨大ファイルをPUTされたりしそう。
  • アップロード画面で任意のファイル名をユーザーの指定でアップロードできるようにするのは上書きなどの問題がありそう。SASを発行する際のファイル名はシステム側で作成することを推奨。

ちなみに実行するとこんな感じですね。

以上、誰得かまったく分からない話でした…

views/form.erb

<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">
function segmentation(arrayBuffer, segmentSize)
{
    var segments = [];
    var fi = 0;
    while(fi * segmentSize < arrayBuffer.byteLength){
        segments.push(arrayBuffer.slice(fi * segmentSize, (fi + 1) * segmentSize));
        ++fi;
    }
    return segments;
}

$(document).ready(function() {
  var send_block_list = function(sas_base_url, block_prefix, block_num) {
    var defer = $.Deferred();
    var put_to = sas_base_url + '&comp=blocklist';
    var body = '<?xml version="1.0" encoding="utf-8"?><BlockList>';
    for (var i = 0; i < block_num; i++) {
      body += '<Latest>' + base64encode(block_id(block_prefix, i)) + '</Latest>';
    }
    body += '</BlockList>';
    $.ajax({
      type:   'PUT',
      url:    put_to,
      headers: {
        'x-ms-version': '2015-04-05',
        'x-ms-blob-content-type': 'application/octet-stream'
      },
      data:   body,
      async: true,
      cache: false,
      contentType: false,
      processData: false
    }).done(function(data, textStatus, jqXHR) {
      defer.notify().resolve(data);
    }).fail(function( jqXHR, textStatus, errorThrown) {
      var msg = 'ブロックリストの送信に失敗しました...';
      console.log(msg);
      defer.reject(msg)
    });
    return defer.promise();
  };

  var get_sas_base_url = function(filename) {
    var form = new FormData();
    form.append('filename', filename);
    var defer = $.Deferred();
    $.ajax({
      url: '/sas',
      type: 'POST',
      contentType: 'application/octet-stream',
      data: form,
      async: true,
      crossDomain: true,
      cache: false,
      contentType: false,
      processData: false
    }).done(function(data) {
      data = data.url;
      defer.resolve(data);
    }).fail(function(jqXHR, textStatus, errorThrown) {
      var msg = 'SASのURL取得に失敗しました...';
      console.log(msg);
      defer.reject(msg)
    });
    return defer.promise();
  };

  var send_block = function(sas_base_url, block_prefix, block_index, block) {
    var defer = $.Deferred();
    var url = sas_base_url + '&comp=block&blockid=' + base64encode(block_id(block_prefix, block_index));
    $.ajax({
      url: url,
      type: 'PUT',
      contentType: 'application/octet-stream',
      data: block,
      async: true,
      crossDomain: true,
      cache: false,
      contentType: false,
      processData: false
    }).done(function(data, textStatus, jqXHR) {
      var msg = 'Azureへのデータ送信に成功しました...';
      console.log(msg);
      defer.notify().resolve(data);
    }).fail(function(jqXHR, textStatus, errorThrown) {
      var msg = 'Azureへのデータ送信に失敗しました...';
      console.log(msg);
      defer.reject(msg)
    });
    return defer.promise();
  };

  function block_id(block_prefix, index) {
    return block_prefix + ("0000000" + index).substr(-7,7);
  }

  function base64encode(str) {
    return window.btoa(str);
  }

  $('#file').on("change", function(event) {
    var file = this.files[0];
    if(file != null) {
      console.log("アップロードファイル名は" + file.name + "です...");
    } else {
      return;
    }
    event.preventDefault();

    var block_prefix = 'BlockId';
    var reader = new FileReader();

    reader.onload = (function(file) {
      return function(evt) {
        buffer = reader.result;
        var segments = segmentation(buffer, 4 * 1024 * 1024);

        $.when(get_sas_base_url(file.name))
        .fail(function(msg) {
          $("#progress").html(msg);
        })
        .done(function(url){
          var functions = [];
          $.each(segments,function(i, val) {
            functions.push(send_block(url, block_prefix, i, val));
          });

          // See http://stackoverflow.com/questions/26066198/jquery-when-progress-for-array-of-deferred-and-or-promise
          $.whenWithProgress = function(arrayOfPromises) {
            var cntr = 0, defer = $.Deferred();
            for (var i = 0; i < arrayOfPromises.length; i++) {
              arrayOfPromises[i].done(function() {
                defer.notify(++cntr, arrayOfPromises.length);
              });
            }
            jQuery.when.apply(jQuery, arrayOfPromises).done(function() {
              defer.resolveWith(null, arguments);
            });
            return defer.promise();
          };

          $.whenWithProgress(functions)
          .progress(function(cnt, total) {
            $("#progress").html(Math.round(cnt * 100 / total) + "%");
          })
          .fail(function(msg) {
            $("#progress").html(msg);
          })
          .done(function() {
            console.log('全てのブロックのアップロード完了...');
            $.when(send_block_list(url, block_prefix, segments.length))
            .fail(function(msg) {
              $("#progress").html(msg);
            })
            .done(function(){
              console.log('ブロックリストの登録完了...');
              $("#progress").html('100%');
            });
          });
        });
      };
    })(file);

    reader.onloadstart = function(e) {
      $("#progress").html('0%');
    };
    reader.readAsArrayBuffer(file);
  });
});
</script>
</body>
</html>

アジャイルコーチングやトレーニングを提供しています

株式会社アトラクタでは、アジャイル開発に取り組むチーム向けのコーチングや、認定スクラムマスター研修などの各種トレーニングを提供しています。ぜひお気軽にご相談ください。

詳細はこちら
  • スクラム実践者が知るべき97のこと
  • 著者/訳者:Gunther Verheyen / 吉羽龍太郎 原田騎郎 永瀬美穂
  • 出版社:オライリージャパン(2021-03-23)
  • 定価:¥ 2,640
  • スクラムはアジャイル開発のフレームワークですが、その実装は組織やチームのレベルに応じてさまざまです。本書はスクラムの実践において、さまざまな課題に対処してきた実践者が自らの経験や考え方を語るエッセイ集です。日本語書き下ろしコラムを追加で10本収録
  • プロダクトマネジメント ―ビルドトラップを避け顧客に価値を届ける
  • 著者/訳者:Melissa Perri / 吉羽龍太郎
  • 出版社:オライリージャパン(2020-10-26)
  • 定価:¥ 2,640
  • プロダクト開発を作った機能の数やベロシティなどのアウトプットで計測すると、ビルドトラップと呼ばれる失敗に繋がります。本書ではいかにしてビルドトラップを避けて顧客に価値を届けるかを解説しています。
  • SCRUM BOOT CAMP THE BOOK 【増補改訂版】
  • 著者/訳者:西村直人 永瀬美穂 吉羽龍太郎
  • 出版社:翔泳社(2020-05-20)
  • 定価:¥ 2,640
  • スクラム初心者に向けて基本的な考え方の解説から始まり、プロジェクトでの実際の進め方やよく起こる問題への対応法まで幅広く解説。マンガと文章のセットでスクラムを短期間で理解できます。スクラムの概要を正しく理解したい人、もう一度おさらいしたい人にオススメ。
  • みんなでアジャイル ―変化に対応できる顧客中心組織のつくりかた
  • 著者/訳者:Matt LeMay / 吉羽龍太郎、永瀬美穂、原田騎郎、有野雅士
  • 出版社:オライリージャパン(2020-3-19)
  • 定価:¥ 2,640
  • アジャイルで本当の意味での成果を出すには、開発チームだけでアジャイルに取り組むのではなく、組織全体がアジャイルになる必要があります。本書にはどうやってそれを実現するかのヒントが満載です
  • レガシーコードからの脱却 ―ソフトウェアの寿命を延ばし価値を高める9つのプラクティス
  • 著者/訳者:David Scott Bernstein / 吉羽龍太郎、永瀬美穂、原田騎郎、有野雅士
  • 出版社:オライリージャパン( 2019-9-18 )
  • 定価:¥ 3,132
  • レガシーコードになってから慌てるのではなく、日々レガシーコードを作らないようにするにはどうするか。その観点で、主にエクストリームプログラミングに由来する9つのプラクティスとその背後にある原則をわかりやすく説明しています。
  • Effective DevOps ―4本柱による持続可能な組織文化の育て方
  • 著者/訳者:Jennifer Davis、Ryn Daniels / 吉羽 龍太郎、長尾高弘
  • 出版社:オライリージャパン( 2018-3-24 )
  • 定価:¥ 3,888
  • 主にDevOpsの文化的な事柄に着目し、異なるゴールを持つチームが親和性を高め、矛盾する目標のバランスを取りながら最大限の力を発揮する方法を解説します
  • ジョイ・インク 役職も部署もない全員主役のマネジメント
  • 著者/訳者:リチャード・シェリダン / 原田騎郎, 安井力, 吉羽龍太郎, 永瀬美穂, 川口恭伸
  • 出版社:翔泳社( 2016-12-20 )
  • 定価:¥ 1,944
  • 米国で何度も働きやすい職場として表彰を受けているメンローの創業者かつCEOであるリチャード・シェリダン氏が、職場に喜びをもたらす知恵や経営手法、より良い製品の作り方などを惜しみなく紹介しています
  • アジャイルコーチの道具箱 – 見える化の実例集
  • 著者/訳者:Jimmy Janlén / 原田騎郎, 吉羽龍太郎, 川口恭伸, 高江洲睦, 佐藤竜也
  • 出版社:Leanpub( 2016-04-12 )
  • 定価:$14.99
  • この本は、チームの協調とコミュニケーションを改善したり、行動を変えるための見える化の実例を集めたものです。96個(+2)の見える化の方法をそれぞれ1ページでイラストとともに解説しています。アジャイル開発かどうかに関係なくすぐに使えるカタログ集です
  • カンバン仕事術 ―チームではじめる見える化と改善
  • 著者/訳者:原田騎郎 安井力 吉羽龍太郎 角征典 高木正弘
  • 出版社:オライリージャパン( 2016-03-25 )
  • 定価:¥ 2,138
  • チームの仕事や課題を見える化する手法「カンバン」について、その導入から実践までを図とともにわかりやすく解説した書籍。カンバンの原則などの入門的な事柄から、サービスクラス、プロセスの改善など、一歩進んだ応用的な話題までを網羅的に解説します。
  • Software in 30 Days スクラムによるアジャイルな組織変革“成功"ガイド
  • 著者/訳者:Ken Schwaber、Jeff Sutherland著、角征典、吉羽龍太郎、原田騎郎、川口恭伸訳
  • 出版社:アスキー・メディアワークス( 2013-03-08 )
  • 定価:¥ 1,680
  • スクラムの父であるジェフ・サザーランドとケン・シュエイバーによる著者の日本語版。ビジネス層、マネジメント層向けにソフトウェア開発プロセス変革の必要性やアジャイル型開発プロセスの優位性について説明
  • How to Change the World 〜チェンジ・マネジメント3.0〜
  • 著者/訳者:Jurgen Appelo, 前川哲次(翻訳), 川口恭伸(翻訳), 吉羽龍太郎(翻訳)
  • 出版社:達人出版会
  • 定価:500円
  • どうすれば自分たちの組織を変えられるだろう?それには、組織に変革を起こすチェンジ・マネジメントを学習することだ。アジャイルな組織でのマネージャーの役割を説いた『Management 3.0』の著者がコンパクトにまとめた変化のためのガイドブック