FileSystem API について知る

HTML5 Rocks

はじめに

ウェブ アプリケーションでファイルとディレクトリの読み取りと書き込みができれば便利だと常に思っていました。オフラインからオンラインに移行すると、アプリケーションは複雑さを増し、ファイル システム API がないことがウェブの進化にとって障害になってきました。バイナリ データの保存ややり取りはデスクトップに限定すべきではありません。ありがたいことに、FileSystem API のおかけでこの問題は解消されます。FileSystem API を使用すると、ユーザーのローカル ファイル システムのサンドボックス化されたセクションの作成、読み取り、ナビゲート、書き込みをウェブ アプリケーションから行うことができます。

FileSystem API は、次のような多様なテーマに分けることができます。

  • ファイルの読み取りと操作: File/BlobFileListFileReader
  • 作成と書き込み: BlobBuilderFileWriter
  • ディレクトリとファイル システムのアクセス: DirectoryReaderFileEntry/DirectoryEntryLocalFileSystem

ブラウザのサポートと保存の制限

この記事の執筆時点では、実際に機能する FileSystem API の実装を備えているのは Google Chrome だけです。ファイル/クオータ管理専用のブラウザ UI はまだ存在しません。ユーザーのシステムにデータを保存するには、必要に応じてアプリケーションでクオータを要求します。ただし、テストの際には --unlimited-quota-for-files フラグを使用して Chrome を実行できます。さらに、Chrome ウェブストア向けのアプリケーションまたは拡張機能を構築する場合は、クオータを要求する代わりにマニフェスト ファイルunlimitedStorage 許可を使用できます。最終的に、アプリケーションのストレージを付与、拒否、または増設する許可を求めるダイアログがユーザーに対して表示されることになります。

file:// からアプリケーションをデバッグする場合は、--allow-file-access-from-files フラグが必要になることがあります。これらのフラグを使用しないと、SECURITY_ERR または QUOTA_EXCEEDED_ERR FileError が発生します。

ファイル システムを要求する

ウェブ アプリケーションからサンドボックス化したファイル システムにアクセスを要求するには、window.requestFileSystem() を呼び出します。

// Note: The file system has been prefixed as of Google Chrome 12:
window.requestFileSystem  = window.requestFileSystem || window.webkitRequestFileSystem;

window.requestFileSystem(type, size, successCallback, opt_errorCallback)
type
持続的なファイル ストレージにするかどうか。使用できる値は window.TEMPORARY または window.PERSISTENT です。TEMPORARY を使用して保存したデータは、ブラウザの裁量で(空き容量が必要な場合など)削除できます。PERSISTENT ストレージは、ユーザーまたはアプリケーションが明示的に承認しない限り消去できません。また、使用するためにはユーザーがアプリケーションに対してクオータを許可する必要があります。クオータを要求するを参照してください。
size
アプリケーションがストレージとして必要とするサイズ(バイト単位)。
successCallback
ファイル システムの要求が成功したときに呼び出されるコールバック。引数は FileSystem オブジェクトです。
opt_errorCallback
エラー処理のため、またはファイル システムを取得する要求が拒否されたときに備えたオプションのコールバック。引数は FileError オブジェクトです。

初めて requestFileSystem() を呼び出したときに、そのアプリケーション用の新しいストレージが作成されます。このファイル システムはサンドボックス化されていることに注意してください。つまり、ウェブ アプリケーションは他のアプリケーションのファイルにアクセスできません。また、ユーザーのハード ドライブ上にある任意のフォルダ(マイ ピクチャ、マイ ドキュメントなど)との間で読み取りや書き込みを行うこともできません。

使用例:

function onInitFs(fs) {
  console.log('Opened file system: ' + fs.name);
}

window.requestFileSystem(window.TEMPORARY, 5*1024*1024 /*5MB*/, onInitFs, errorHandler);

FileSystem 仕様には、Web Workers で使用するための同期 API の LocalFileSystemSync インターフェースも定義されています。ただし、このチュートリアルは同期 API については説明しません。

この文書ではこの後、非同期呼び出しによるエラーを処理するために同じハンドラを使用します。

function errorHandler(e) {
  var msg = '';

  switch (e.code) {
    case FileError.QUOTA_EXCEEDED_ERR:
      msg = 'QUOTA_EXCEEDED_ERR';
      break;
    case FileError.NOT_FOUND_ERR:
      msg = 'NOT_FOUND_ERR';
      break;
    case FileError.SECURITY_ERR:
      msg = 'SECURITY_ERR';
      break;
    case FileError.INVALID_MODIFICATION_ERR:
      msg = 'INVALID_MODIFICATION_ERR';
      break;
    case FileError.INVALID_STATE_ERR:
      msg = 'INVALID_STATE_ERR';
      break;
    default:
      msg = 'Unknown Error';
      break;
  };

  console.log('Error: ' + msg);
}

このエラー コールバックはきわめて汎用的なものですが、考え方の理解には役立ちます。代わりに、人間が読み取れるメッセージをユーザーに表示することもできます。

ストレージ クオータを要求する

PERSISTENT ストレージを使用するには、持続的なデータを保存する許可をユーザーから取得します。TEMPORARY ストレージには、同じ制限は適用されません。ブラウザの裁量で一時的に保存されているデータを削除することもできるためです。

FileSystem API で PERSISTENT ストレージを使用するために、Chrome はストレージを要求する新しい API を window.webkitStorageInfo 以下に公開しています。

window.webkitStorageInfo.requestQuota(PERSISTENT, 1024*1024, function(grantedBytes) {
  window.requestFileSystem(PERSISTENT, grantedBytes, onInitFs, errorHandler);
}, function(e) {
  console.log('Error', e);
});

ユーザーが許可を与えると、その後は requestQuota() を呼び出す必要はありません。以降の呼び出しは不要です。

また、呼び出し元の現在のクオータの使用と割り当てをクエリする APIwindow.webkitStorageInfo.queryUsageAndQuota() もあります。

ファイルを操作する

サンドボックス化された環境内のファイルは、FileEntry インターフェースによって表されます。FileEntry には、標準のファイル システムでは備えられているのが当然と考えられる種類のプロパティ(nameisFile など)やメソッド(removemoveTocopyTo など)が含まれます。

FileEntry のプロパティとメソッド:

fileEntry.isFile === true
fileEntry.isDirectory === false
fileEntry.name
fileEntry.fullPath
...

fileEntry.getMetadata(successCallback, opt_errorCallback);
fileEntry.remove(successCallback, opt_errorCallback);
fileEntry.moveTo(dirEntry, opt_newName, opt_successCallback, opt_errorCallback);
fileEntry.copyTo(dirEntry, opt_newName, opt_successCallback, opt_errorCallback);
fileEntry.getParent(successCallback, opt_errorCallback);
fileEntry.toURL(opt_mimeType);

fileEntry.file(successCallback, opt_errorCallback);
fileEntry.createWriter(successCallback, opt_errorCallback);
...

FileEntry をよりよく理解できるように、このセクションでは、一般的なタスクを実行するための使用例を多数紹介しています。

ファイルを作成する

DirectoryEntry インターフェースのメソッドの 1 つである、ファイル システムの getFile() を使用すると、ファイルを検索したり、作成することができます。ファイル システムの要求後に返される成功のコールバックには、アプリケーションのファイル システムのルートを指す DirectoryEntryfs.root)を含んだ FileSystem オブジェクトが渡されます。

次のコードは、アプリケーションのファイル システムのルートに「log.txt」という空のファイルを作成します。

function onInitFs(fs) {

  fs.root.getFile('log.txt', {create: true, exclusive: true}, function(fileEntry) {

    // fileEntry.isFile === true
    // fileEntry.name == 'log.txt'
    // fileEntry.fullPath == '/log.txt'

  }, errorHandler);

}

window.requestFileSystem(window.TEMPORARY, 1024*1024, onInitFs, errorHandler);

ファイル システムが要求されると、成功のハンドラに FileSystem オブジェクトが渡されます。コールバック内では、作成するファイル名を使用して fs.root.getFile() を呼び出すことができます。絶対パスと相対パスのどちらも渡すことができますが、有効なパスである必要があります。たとえば、作成しようとするファイルのすぐ上の親ディレクトリが存在しないとエラーになります。getFile() の 2 つ目の引数は、ファイルが存在しない場合の関数の動作を記述するオブジェクト リテラルです。この例では、ファイルが存在しない場合は create: true でファイルを作成し、ファイルが存在する場合は(exclusive: true)エラーをスローします。それ以外の場合(create: false の場合)、ファイルを単にフェッチし、返します。いずれの場合でも、該当のファイルに対する参照エントリを取得するだけなので、ファイルのコンテンツは上書きされません。

名前でファイルを読み取る

次のコードでは、「log.txt」というファイルを取得し、FileReader API を使用してコンテンツを読み取り、ページの新しい <textarea> に付加します。log.txt が存在しない場合は、エラーがスローされます。

function onInitFs(fs) {

  fs.root.getFile('log.txt', {}, function(fileEntry) {

    // Get a File object representing the file,
    // then use FileReader to read its contents.
    fileEntry.file(function(file) {
       var reader = new FileReader();

       reader.onloadend = function(e) {
         var txtArea = document.createElement('textarea');
         txtArea.value = this.result;
         document.body.appendChild(txtArea);
       };

       reader.readAsText(file);
    }, errorHandler);

  }, errorHandler);

}

window.requestFileSystem(window.TEMPORARY, 1024*1024, onInitFs, errorHandler);

ファイルに書き込む

次のコードでは、「log.txt」という空のファイルを作成し(存在しない場合)、「Lorem Ipsum」というテキストを入力します。

function onInitFs(fs) {

  fs.root.getFile('log.txt', {create: true}, function(fileEntry) {

    // Create a FileWriter object for our FileEntry (log.txt).
    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.
      var bb = new BlobBuilder(); // Note: window.WebKitBlobBuilder in Chrome 12.
      bb.append('Lorem Ipsum');
      fileWriter.write(bb.getBlob('text/plain'));

    }, errorHandler);

  }, errorHandler);

}

window.requestFileSystem(window.TEMPORARY, 1024*1024, onInitFs, errorHandler);

今回は、FileEntry の createWriter() メソッドを呼び出して FileWriter オブジェクトを取得します。成功のコールバック内では、error イベントと writeend イベント用のイベント ハンドラを設定します。テキスト データをファイルに書き込むには、blob を作成し、そこにテキストを付加し、blob を FileWriter.write() に渡します。

データをファイルに付加する

次のコードでは、「Hello World」というテキストをログ ファイルの末尾に付加します。ファイルが存在しない場合はエラーがスローされます。

function onInitFs(fs) {

  fs.root.getFile('log.txt', {create: false}, function(fileEntry) {

    // Create a FileWriter object for our FileEntry (log.txt).
    fileEntry.createWriter(function(fileWriter) {

      fileWriter.seek(fileWriter.length); // Start write position at EOF.

      // Create a new Blob and write it to log.txt.
      var bb = new BlobBuilder(); // Note: window.WebKitBlobBuilder in Chrome 12.
      bb.append('Hello World');
      fileWriter.write(bb.getBlob('text/plain'));

    }, errorHandler);

  }, errorHandler);

}

window.requestFileSystem(window.TEMPORARY, 1024*1024, onInitFs, errorHandler);

ユーザーが選択したファイルのコピーを作成する

次のコードを使用すると、ユーザーは <input type="file" multiple /> を使用して複数のファイルを選択し、アプリケーションのサンドボックス化したファイル システムにそのファイルのコピーを作成できるようになります。

<input type="file" id="myfile" multiple />
document.querySelector('#myfile').onchange = function(e) {
  var files = this.files;

  window.requestFileSystem(window.TEMPORARY, 1024*1024, function(fs) {
    // Duplicate each file the user selected to the app's fs.
    for (var i = 0, file; file = files[i]; ++i) {

      // Capture current iteration's file in local scope for the getFile() callback.
      (function(f) {
        fs.root.getFile(file.name, {create: true, exclusive: true}, function(fileEntry) {
          fileEntry.createWriter(function(fileWriter) {
            fileWriter.write(f); // Note: write() can take a File or Blob object.
          }, errorHandler);
        }, errorHandler);
      })(file);

    }
  }, errorHandler);

};

ここではファイル インポートの入力を使用しましたが、HTML5 ドラッグ&ドロップを利用して簡単に同じ目的を達成できます。

コメントで示しているように、FileWriter.write()Blob または File を受け入れることができます。これは、FileBlob から継承されており、そのためファイル オブジェクトが blob であるためです。

ファイルを削除する

次のコードでは、「log.txt」というファイルを削除します。

window.requestFileSystem(window.TEMPORARY, 1024*1024, function(fs) {
  fs.root.getFile('log.txt', {create: false}, function(fileEntry) {

    fileEntry.remove(function() {
      console.log('File removed.');
    }, errorHandler);

  }, errorHandler);
}, errorHandler);

ディレクトリを操作する

サンドボックス内のディレクトリは DirectoryEntry インターフェースで表し、FileEntry のプロパティのほとんどを共有します(これらのプロパティは共通の Entry インターフェースから継承されます)。ただし、DirectoryEntry には、ディレクトリを操作するための追加のメソッドがあります。

DirectoryEntry のプロパティとメソッド:

dirEntry.isDirectory === true
// See the section on FileEntry for other inherited properties/methods.
...

var dirReader = dirEntry.createReader();
dirEntry.getFile(path, opt_flags, opt_successCallback, opt_errorCallback);
dirEntry.getDirectory(path, opt_flags, opt_successCallback, opt_errorCallback);
dirEntry.removeRecursively(successCallback, opt_errorCallback);
...

ディレクトリを作成する

ディレクトリの読み取りまたは作成には、DirectoryEntrygetDirectory() メソッドを使用します。検索または作成するディレクトリとして名前またはパスを渡すことができます。

たとえば、次のコードでは、ルート ディレクトリに「MyPictures」というディレクトリを作成します。

window.requestFileSystem(window.TEMPORARY, 1024*1024, function(fs) {
  fs.root.getDirectory('MyPictures', {create: true}, function(dirEntry) {
    ...
  }, errorHandler);
}, errorHandler);
  

サブディレクトリ

サブディレクトリの作成は、他のディレクトリを作成する場合とまったく同じです。ただし、作成しようとするディレクトリのすぐ上の親ディレクトリが存在しないと、API からエラーがスローされます。これを解決する方法は各ディレクトリを順番に作成することですが、非同期 API の処理に使用するにはやや面倒です。

次のコードでは、アプリケーションの FileSystem のルートに新しい階層を作成するために、親フォルダを作成してから各サブディレクトリを再帰的に追加しています。

var path = 'music/genres/jazz/';

function createDir(rootDirEntry, folders) {
  // Throw out './' or '/' and move on to prevent something like '/foo/.//bar'.
  if (folders[0] == '.' || folders[0] == '') {
    folders = folders.slice(1);
  }
  rootDirEntry.getDirectory(folders[0], {create: true}, function(dirEntry) {
    // Recursively add the new subfolder (if we still have another to create).
    if (folders.length) {
      createDir(dirEntry, folders.slice(1));
    }
  }, errorHandler);
};

function onInitFs(fs) {
  createDir(fs.root, path.split('/')); // fs.root is a DirectoryEntry.
}

window.requestFileSystem(window.TEMPORARY, 1024*1024, onInitFs, errorHandler);

これによって「music/genres/jazz」ディレクトリが作成されるので、フルパスを getDirectory() に渡し、その下に新しいサブフォルダやファイルを作成できるようになります。例:

window.requestFileSystem(window.TEMPORARY, 1024*1024, function(fs) {
  fs.root.getFile('/music/genres/jazz/song.mp3', {create: true}, function(fileEntry) {
    ...
  }, errorHandler);
}, errorHandler);

ディレクトリ コンテンツの読み込み

ディレクトリ コンテンツを読み込むには、DirectoryReader を作成し、readEntries() メソッドを呼び出します。ディレクトリのエントリのすべてが readEntries() の 1 回の呼び出しで返されるという保証はありません。つまり、返される結果がなくなるまで DirectoryReader.readEntries() を呼び出し続ける必要があります。次に、この処理のコード例を示します。

<ul id="filelist"></ul>
function toArray(list) {
  return Array.prototype.slice.call(list || [], 0);
}

function listResults(entries) {
  // Document fragments can improve performance since they're only appended
  // to the DOM once. Only one browser reflow occurs.
  var fragment = document.createDocumentFragment();

  entries.forEach(function(entry, i) {
    var img = entry.isDirectory ? '<img src="folder-icon.gif">' :
                                  '<img src="file-icon.gif">';
    var li = document.createElement('li');
    li.innerHTML = [img, '<span>', entry.name, '</span>'].join('');
    fragment.appendChild(li);
  });

  document.querySelector('#filelist').appendChild(fragment);
}

function onInitFs(fs) {

  var dirReader = fs.root.createReader();
  var entries = [];

  // Call the reader.readEntries() until no more results are returned.
  var readEntries = function() {
     dirReader.readEntries (function(results) {
      if (!results.length) {
        listResults(entries.sort());
      } else {
        entries = entries.concat(toArray(results));
        readEntries();
      }
    }, errorHandler);
  };

  readEntries(); // Start reading dirs.

}

window.requestFileSystem(window.TEMPORARY, 1024*1024, onInitFs, errorHandler);

ディレクトリの削除

DirectoryEntry.remove() メソッドは FileEntry の場合と同じように動作します。違いは、空でないディレクトリを削除しようとするとエラーになるという点です。

次のコードでは、「/music/genres/」から空のディレクトリ「jazz」を削除します。

window.requestFileSystem(window.TEMPORARY, 1024*1024, function(fs) {
  fs.root.getDirectory('music/genres/jazz', {}, function(dirEntry) {

    dirEntry.remove(function() {
      console.log('Directory removed.');
    }, errorHandler);

  }, errorHandler);
}, errorHandler);

ディレクトリの再帰的削除

エントリを含んでいる不要なディレクトリを削除する場合は、removeRecursively() が便利です。ディレクトリとそのコンテンツが再帰的に削除されます。

次のコードでは、ディレクトリ「music」とそのディレクトリに含まれるすべてのファイルおよびディレクトリを再帰的に削除します。

window.requestFileSystem(window.TEMPORARY, 1024*1024, function(fs) {
  fs.root.getDirectory('/misc/../music', {}, function(dirEntry) {

    dirEntry.removeRecursively(function() {
      console.log('Directory removed.');
    }, errorHandler);

  }, errorHandler);
}, errorHandler);

コピー、名前の変更、移動

FileEntryDirectoryEntry は同じ操作を共有しています。

エントリのコピー

FileEntryDirectoryEntry には、いずれも既存のエントリをコピーするための copyTo() があります。このメソッドを使用すると、フォルダの再帰的コピーが自動的に実行されます。

次のコード例では、あるディレクトリ内のファイル「me.png」を別のディレクトリにコピーします。

function copy(cwd, src, dest) {
  cwd.getFile(src, {}, function(fileEntry) {

    cwd.getDirectory(dest, {}, function(dirEntry) {
      fileEntry.copyTo(dirEntry);
    }, errorHandler);

  }, errorHandler);
}

window.requestFileSystem(window.TEMPORARY, 1024*1024, function(fs) {
  copy(fs.root, '/folder1/me.png', 'folder2/mypics/');
}, errorHandler);

エントリの移動または名前の変更

FileEntryDirectoryEntry moveTo() を使用すると、ファイルまたはディレクトリの移動または名前の変更を行うことができます。最初の引数は、ファイルの移動先の親ディレクトリであり、2 番目の引数は、ファイルの新しい名前(オプション)です。新しい名前を指定しないと、ファイルの元の名前が使用されます。

次の例では、「me.png」の名前を「you.png」に変更しますが、ファイルは移動しません。

function rename(cwd, src, newName) {
  cwd.getFile(src, {}, function(fileEntry) {
    fileEntry.moveTo(cwd, newName);
  }, errorHandler);
}

window.requestFileSystem(window.TEMPORARY, 1024*1024, function(fs) {
  rename(fs.root, 'me.png', 'you.png');
}, errorHandler);

次の例では、「me.png」(ルート ディレクトリ)を「newfolder」というフォルダに移動します。

function move(src, dirName) {
  fs.root.getFile(src, {}, function(fileEntry) {

    fs.root.getDirectory(dirName, {}, function(dirEntry) {
      fileEntry.moveTo(dirEntry);
    }, errorHandler);

  }, errorHandler);
}

window.requestFileSystem(window.TEMPORARY, 1024*1024, function(fs) {
  move('/me.png', 'newfolder/');
}, errorHandler);

filesystem: URL

FileSystem API には、filesystem: という新しい URL スキームがあります。これは、src 属性または href 属性の入力に使用できます。たとえば、画像とその fileEntry を表示する場合、toURL() を呼び出すと、ファイルの filesystem: URL を取得できます。

var img = document.createElement('img');
img.src = fileEntry.toURL(); // filesystem:http://example.com/temporary/myfile.png
document.body.appendChild(img);

または、filesystem: URL がすでにある場合、resolveLocalFileSystemURL() を呼び出すと、fileEntry を取得できます。

window.resolveLocalFileSystemURL = window.resolveLocalFileSystemURL ||
                                   window.webkitResolveLocalFileSystemURL;

var url = 'filesystem:http://example.com/temporary/myfile.png';
window.resolveLocalFileSystemURL(url, function(fileEntry) {
  ...
});

すべてを合わせる

基本的な例

このデモでは、ファイルシステム内のファイルとフォルダをリスト表示します。

HTML5 端末

このシェルでは、FileSystem API を抽象化することで、UNIX ファイルシステムの一般的な操作(cdmkdirrmopencat など)の一部をレプリケートします。ファイルを追加するには、デスクトップから下のターミナル ウィンドウにドラッグ&ドロップします。

使用例

HTML5 にも使用できるストレージ オプションはいくつかありますが、FileSystem が違うのは、データベースでは適切に対処できない、クライアント側ストレージを使用するケースに対応することを意図している点です。これは一般的に、アプリケーションがサイズの大きなバイナリ blob を処理したり、ブラウザの枠組みの外にあるアプリケーションとデータを共有する場合です。

このような使用例をいくつか具体的に紹介しましょう。

  1. 持続的なアップローダ
    • アップロードするファイルまたはディレクトリをユーザーが選択すると、ファイルをローカルのサンドボックスにコピーし、一度にまとめてアップロードします。
    • ブラウザのクラッシュ、ネットワークの中断などがあっても、アップロードは再開されます。
  2. ビデオ ゲーム、音楽など多数のメディア アセットを含むアプリケーション
    • 1 つまたは複数の大きな tar 書庫をダウンロードし、ローカルのディレクトリ構造に展開します。
    • 同じダウンロード処理がすべてのオペレーティング システムで機能します。
    • 次に必要なアセットのみをバックグラウンドで事前に取得するように管理できるので、ゲームで次のレベルに進むときや新しい機能を有効にするときに、ダウンロードを待つ時間がなくなります。
    • また、ファイルを直接読み込んだり、image タグ、video タグ、WebGL アセット ローダーなどにローカル URI を渡すことによって、アセットをローカル キャッシュから直接使用します。
    • ファイルには任意のバイナリ形式を使用できます。
    • 多くの場合、サーバー側では、圧縮された tar 書庫は個々に圧縮されたファイルの集合よりもはるかに小さくなります。また、1,000 個の小さなファイルではなく 1 つの tar 書庫にすると、他の処理はすべて同じまま、シーク回数が減ります。
  3. 速度向上のためにオフライン アクセス機能とローカル キャッシュ機能を備えたオーディオ/フォト エディタ
    • データ blob は非常に大きくなる可能性があり、読み取りと書き込みが行われます。
    • ファイルへの部分書き込みが望ましい場合があります(たとえば、ID3/EXIF タグのみを上書きするなど)。
    • ディレクトリを作成してプロジェクト ファイルを整理する機能があると便利です。
    • 編集後のファイルは、クライアント側アプリケーション(iTunes や Picasa など)からアクセス可能である必要があります。
  4. オフラインの動画ビューア
    • 後で視聴するために大きなファイル(1GB 超)をダウンロードします。
    • 効率的なシークとストリーミングが必要です。
    • URI を video タグに渡す機能が必要です。
    • 部分的にダウンロードされたファイルへのアクセスを有効にする必要があります。たとえば、飛行機に乗る前にダウンロードが完了しなかった場合でも、DVD の最初のエピソードを見ることができるようにします。
    • ダウンロードが途中のファイルから 1 つのエピソードを抜き出し、video タグにそのエピソードのみを渡すことができるようにします。
  5. オフラインのウェブ メール クライアント
    • 添付ファイルをダウンロードし、ローカルで保存します。
    • ユーザーが選択した添付ファイルをキャッシュに入れ、後でアップロードできるようにします。
    • キャッシュ内の添付ファイルと画像のサムネイルを参照して、表示およびアップロードできるようにする必要があります。
    • サーバーと通信しているときと同じように UA のダウンロード マネージャをトリガーできるようにします。
    • 添付ファイル付きの電子メールの場合、XHR で一度に 1 つずつファイルを送信するのではなく、マルチパート ポストとしてアップロードできるようにします。

参照仕様

Comments

0