ダウンロードして復号までの流れ
ファイルのダウンロード時にはlocation.hashからパラメータを受け取ります。ダウンロード用リンクのlocation.hashは、暗号化済みのデータのパス、復号のためのパスワード、オリジナルのファイル名をカンマで連結したものです。
location.hashの値は、JavaScriptで明示的に取得し送信を行わない限り、サーバに送られることはありません。暗号化済みデータのパスはダウンロードの際、サーバに送られることになりますが、パスワードとオリジナルのファイル名はサーバに送られずに、ブラウザ内でのみ使用します。つまりサービスの運営者からわかるのは「どのファイルがリクエストされたのか」という情報だけで、そのファイルの内容やファイル名はわからないことになります。
ファイルをダウンロードする
XMLHttpRequestを使って暗号化済みのファイルをダウンロードします。responseTypeを指定することで、ファイルをバイナリデータとして受け取ることが可能です。
XMLHttpRequestで従来よりよく使われるresponseTextは、標準では受信したデータを
UTF-8のテキストとして解釈しようとします(リスト4)。たとえば、
とUTF-8で書かれたファイルを受信した場合、ファイルは15bytesになります(リスト5)。
それに対して、req.responseText.length
はリスト4のように5文字を示します。文字数とバイト数が一致しないと、crypto-jsなどのライブラリにて、1byte単位で取り出すことができません。一般的に、暗号化されると8bytes単位で処理されますが、先の例だと「う」の途中で切れてしまう、という問題が発生してしまうのです。
今までXMLHttpRequestを使ってバイナリファイルを扱うには、overrideMimeTypeを使ってcharset = x-user-defined
という文字コードを指定する方法がよく知られていました[8]。英数字のみを扱うならば文字数=バイト数になりますが、リスト4、リスト5のようにマルチバイト文字を含むデータは、テキストとしてデコードされると、文字数とバイト数が一致しなくなります。
特殊な文字コード指定を使うことで、受信したデータが1byte単位でJavaScriptのstringに格納されるのです[9]。古いブラウザを動作対象とするのであれば覚えておいても損はないですが、最新のブラウザを動作対象にするのであれば、何はともあれArray Bufferを使うのがよいでしょう。
ファイルのダウンロードと復号
単純にXMLHttpRequestで巨大なファイルをダウンロードする場合、やはりメモリに貯めこんでしまいますので、数MBであればともかくとして、GBを超えるファイルはとても現実的には扱えないという状況になります。
この問題はサーバ側でファイルを分割してダウンロードすることで解決できます。問題は、分割して受信したファイルをその都度復号し、復号した結果を結合しなければならないことです。
crypto-jsでは受信したデータを順次復号していくことが可能ですが、その結果をJavaScriptの変数に格納するのであれば、分割して受信したところで最終的なファイルのサイズ分だけメモリを消費することになってしまいます。ローカルで巨大なファイルを生成するには、「サーバからデータを受信して、ファイルに追記し破棄する」という繰り返し処理が必要です。この問題を解決するためには、JavaScriptから「ファイルに追記していく」操作が必要なのですが、原稿執筆時点(2013年1月)でこれが可能なのはGoogle Chromeだけです。MegaがGoogle Chromeを推奨しているのは、こういった事情のためでしょう。
ほかのブラウザでは動かないことになってしまうので、分割ダウンロードと結合部分についてはサンプルでは作りませんでしたが、FileSystem APIがサポートされている場合はFileSystem APIを使うようにしてもよいでしょう(リスト6)。次項ではFileSystem APIを詳しく見ていきます。
FileSystem APIを使って一時ファイルを作る
次に、FileSystem APIを使って一時ファイルを作る方法を説明します。リスト7のようにすれば、一時ファイルを作ることができます。FileSystem APIとはいっても、ユーザのHDDに直接アクセスできるようなものではなく、PC上のファイルシステムとは隔離された、抽象化された独自のストレージを提供するAPIです[10]。開発者は、ファイルの実体がどこに保存されているのかなどを意識する必要はありません。
サイズの小さいデータならば、localStorageを使うことができますし、Indexed DatabaseにもBlobを保存することが可能です。しかしIndexed Databaseを使う場合、結局のところ、JavaScriptによってメモリ内に生成されたBlobを保存することになるため、巨大なBlobを保存するときには、サイズに応じてメモリを消費することになってしまいます。FileSystem APIが必要とされるのは、ファイルやディレクトリといったメタファでデータを扱いたい場合や、あるいは、メモリに収まらないようなサイズの巨大なファイルを扱い、追記したり部分的に書き換えるような処理が必要である場合です。
ファイルをURLに変換する
createObjectURLを使うことで、FileやBlobを参照するURLを生成できます。生成したURLは、画像やビデオやオーディオのsrcとして使うこともできますし、ブラウザで表示することが可能なファイルタイプであればiframeを使ってその場で表示することもできます。
名前を付けてファイルを保存
a
タグのdownload attribute
というものがWHATWGで提案されており、Google Chromeでサポートされています。これは、download="ファイル名"
とすることで、リンク先に遷移することなく指定したファイル名でリンク先を保存できるものです。これが普及すれば、BlobオブジェクトからcreateObjectURLを使って生成したリンクをローカルに保存することが可能になります。
Data URIで代用する
比較的小さなファイルや、ブラウザ上で表示可能な画像などであれば、Data Uriを使って代用することも可能です(リスト8)。FileReaderのreadAsDataURLを使います。
本来使うべきcreateObjectURLと違ってFileReaderを使うため、必然的に非同期インタフェースとなってしまいます。すでにメモリ上に生成されているBlobオブジェクトですので、setTimeoutで0msのwaitを入れることで利用可能です。完全に互換というわけではありませんが、実際にobjectURLを利用するまでにタイムラグがあるのであれば、シームレスに使うことができると思います。
ダウンロード復号の際、特に気を付けたいポイント
次に、ダウンロードを復号する際、特に気を付けたいポイントを解説します。
Blobの組み立て
Blobを生成するためには、以前はBlobBuilderという、Blobを組み立てるための、専用のインタフェースが存在していました[11]。Blobコンストラクタの第1引数には、配列に格納された、ArrayBuffer、Typed Array、Blob、DOMStringを受け取ることができます。DOM Stringが渡された場合は、自動的にUTF-8として解釈されます。
従来の「Stringをバイナリ文字列として使うテクニック」を使っている場合、Blobの挙動はしばしば混乱を引き起こします。Stringが渡されている以上、Blobコンストラクタは文字列として解釈しようとするからです。Stringを使ってバイナリを扱うことは依然として可能ですが、トラブルのもとになるため、なるべく避けたほうがよいでしょう。
Typed ArrayからのBlob生成
Blobコンストラクタに直接渡してBlobオブジェクトを生成できます。しかし、Typed ArrayからのBlob作成をサポートしていない場合があります。
SafariでBlobコンストラクタに空のTyped Arrayを渡した場合、次のようなBlobが生成されます。
このように、new Int8Array( ).toString( ) の実行結果が入っていることがわかります。Blobへの変換に対応していないオブジェクトだったため、文字列化した状態でBlobが生成されてしまったわけです。いずれこの問題は修正されるでしょうから、UserAgentやバージョンで判別するのは避けましょう。
Typed ArrayからBlobの生成に対応しているかどうか見極めるためには、次のように、サイズが0のTyped Arrayを使ってBlobを生成し、生成されたBlobのサイズをチェックすることで対応しているかどうかを判定するのがよいでしょう。