フロントエンドWeb戦略室

最終回 クライアントサイドでの暗号化とバイナリデータの扱い(2)

この記事を読むのに必要な時間:およそ 5.5 分

FileSystem APIを使って一時ファイルを作る

次に,FileSystem APIを使って一時ファイルを作る方法を説明します。リスト7のようにすれば,一時ファイルを作ることができます。FileSystem APIとはいっても,ユーザのHDDに直接アクセスできるようなものではなく,PC上のファイルシステムとは隔離された,抽象化された独自のストレージを提供するAPIです注10⁠。開発者は,ファイルの実体がどこに保存されているのかなどを意識する必要はありません。

リスト7 FileSystem APIを使って一時ファイルを作る(Google Chrome限定)

window.requestFileSystem = window.requestFileSystem ||
window.webkitRequestFileSystem;
// for chrome
if (window.requestFileSystem) {
// var file = new TempFile; file.write(); file.
downloadLink;
  var TempFile = function(){
    var self = {
      ready: false
    };
    var errorHandler = function(){ console.log(arguments)
};
    var onInitFs = function(fs) {
      var fileHandler = function(fileEntry) {
        self.fileEntry = fileEntry;
        // Create a FileWriter object for our FileEntry
        fileEntry.createWriter(function(fileWriter) {
        fileWriter.onwriteend = function(e) {
          console.log('Write completed.') };
        fileWriter.onerror = function(e) {
          console.log('Write failed: ' + e.toString())
};
        // Create a new Blob and write it to log.txt.
        self.ready = true;
        self.writer = fileWriter;
          self.seek = function(pos) { fileWriter.seek(pos) };
          self.write = function(blob) {
            fileWriter.write(blob) };
          self.append = function(blob) {
            self.seek(fileWriter.length);
            self.write(blob);
          };
          self.toURL = function(){
            return self.fileEntry.toURL() };
          if (self.onready) {
            self.onready();
          }
        }, errorHandler);
      }
      fs.root.getFile(
       'data', // ファイル名
       {create: true}, fileHandler , errorHandler);
    };
    window.requestFileSystem(
      window.TEMPORARY, // 一時領域
      1024*1024, onInitFs, errorHandler);
    return self;
  }
}

サイズの小さいデータならば,localStorageを使うことができますし,Indexed DatabaseにもBlobを保存することが可能です。しかしIndexed Databaseを使う場合,結局のところ,JavaScriptによってメモリ内に生成されたBlobを保存することになるため,巨大なBlobを保存するときには,サイズに応じてメモリを消費することになってしまいます。FileSystem APIが必要とされるのは,ファイルやディレクトリといったメタファでデータを扱いたい場合や,あるいは,メモリに収まらないようなサイズの巨大なファイルを扱い,追記したり部分的に書き換えるような処理が必要である場合です。

注10)
FileSystem APIはしばらくの間,Google Chrome以外のブラウザでは積極的にサポートされないことが予想されますので,使用の際にはその点を注意してください。
https://dev.mozilla.jp/2012/07/why-no-filesystem-api-in-firefox/

ファイルをURLに変換する

createObjectURLを使うことで,FileやBlobを参照するURLを生成できます。生成したURLは,画像やビデオやオーディオのsrcとして使うこともできますし,ブラウザで表示することが可能なファイルタイプであればiframeを使ってその場で表示することもできます。

名前を付けてファイルを保存

aタグのdownload attributeというものがWHATWGで提案されており,Google Chromeでサポートされています。これは,download="ファイル名"とすることで,リンク先に遷移することなく指定したファイル名でリンク先を保存できるものです。これが普及すれば,BlobオブジェクトからcreateObjectURLを使って生成したリンクをローカルに保存することが可能になります。

Data URIで代用する

比較的小さなファイルや,ブラウザ上で表示可能な画像などであれば,Data Uriを使って代用することも可能ですリスト8⁠。FileReaderのreadAsDataURLを使います。

リスト8 createObjectURLとData URIを使って取得する

var dataURLsim = {
  createObjectURL: function(blob){
    var fr = new FileReader;
    fr.readAsDataURL(blob);
    return {
      toString: function(){ return fr.result }
    };
  }
};
var objectURL = (
  window.URL || window.webkitURL || dataURLsim
).createObjectURL(blob);
setTimeout(function(){ img.src = objectURL }, 0);

本来使うべきcreateObjectURLと違ってFileReaderを使うため,必然的に非同期インタフェースとなってしまいます。すでにメモリ上に生成されているBlobオブジェクトですので,setTimeoutで0msのwaitを入れることで利用可能です。完全に互換というわけではありませんが,実際にobjectURLを利用するまでにタイムラグがあるのであれば,シームレスに使うことができると思います。

ダウンロード復号の際,特に気を付けたいポイント

次に,ダウンロードを復号する際,特に気を付けたいポイントを解説します。

Blobの組み立て

Blobを生成するためには,以前はBlobBuilderという,Blobを組み立てるための,専用のインタフェースが存在していました注11⁠。Blobコンストラクタの第1引数には,配列に格納された,ArrayBuffer,Typed Array,Blob,DOMStringを受け取ることができます。DOM Stringが渡された場合は,自動的にUTF-8として解釈されます。

// 文字列からBlob を作る
// 長さ1 の内部表現文字列がUTF-8 にエンコードされて
// 3bytes のBlob になった
new Blob([" あ"]).size // 3bytes

// 長さ1 の内部表現文字列をUTF-8 のバイト列に変換する
inta = new Uint8Array( encodeURI(" あ").split("%").
slice(1).map(function(v){ return parseInt(v,16)}) )
// 長さ3 のUint8Array がそのままBlob に変換された
new Blob([inta]).size // 3bytes
// " あ" を3 文字に分解する
bytes = encodeURI(" あ").split("%").slice(1).
map(function(v){ return String.fromCharCode(
parseInt(v,16) ) }).join("");
bytes.length // 3 文字
// 長さ3 の内部表現文字列をUTF-8 にエンコードした結果
// 2×3 で6bytes になった
new Blob([bytes]).size // 6bytes

従来の「Stringをバイナリ文字列として使うテクニック」を使っている場合,Blobの挙動はしばしば混乱を引き起こします。Stringが渡されている以上,Blobコンストラクタは文字列として解釈しようとするからです。Stringを使ってバイナリを扱うことは依然として可能ですが,トラブルのもとになるため,なるべく避けたほうがよいでしょう。

Typed ArrayからのBlob生成

Blobコンストラクタに直接渡してBlobオブジェクトを生成できます。しかし,Typed ArrayからのBlob作成をサポートしていない場合があります。

// "\x01\x02\x03\x04\x05" 相当のバイナリデータを作る
var array = new Uint8Array([1,2,3,4,5]);
new Blob([array]) // 現在推奨されている方法注12
new Blob([array.buffer]) // 古い方法注13

SafariでBlobコンストラクタに空のTyped Arrayを渡した場合,次のようなBlobが生成されます。

b = new Blob([new Int8Array()]);
f = new FileReader;f.readAsText(b);
f.result; // "[object Int8Array]"

このように,new Int8Array( ).toString( ) の実行結果が入っていることがわかります。Blobへの変換に対応していないオブジェクトだったため,文字列化した状態でBlobが生成されてしまったわけです。いずれこの問題は修正されるでしょうから,UserAgentやバージョンで判別するのは避けましょう。

Typed ArrayからBlobの生成に対応しているかどうか見極めるためには,次のように,サイズが0のTyped Arrayを使ってBlobを生成し,生成されたBlobのサイズをチェックすることで対応しているかどうかを判定するのがよいでしょう。

var isOldBlobConstructor = (
  new Blob([new Int8Array()]).size > 0 ) ? true : false;
注11)
Blob自身がコンストラクタとして機能するようになりましたので,BlobBuilderは現在では廃止されています。
注12)
Safariでは正常に動作しません。
注13)
Safari/Chromeともに動作はするものの,Chromeだと警告が出ます。推奨されていないこともあり,Chromeでいつ動かなくなるかわかりません。
<続きの(3)はこちら。>

著者プロフィール

mala(マラ)

NHN Japan所属。livedoor Readerの開発で知られる。JavaScriptを使ったUI,非同期処理,Webアプリケーションセキュリティなどに携わる。

Twitter:@bulkneets