ブラウザからドライブにファイルの書き込みができるNative File System APIとは?

UI開発者 加藤

Google Chrome 78からNative File System APIのオリジントライアルが始まりました。このAPIはユーザーのデバイス上にあるファイルを読み込んだり、任意のディレクトリにファイルを書き込むことができるAPIです。途中段階のAPIですが、このAPIが提供されることでスマホアプリやデスクトップアプリのような機能をWebアプリでも提供できるようになります。

※ お試しする際はGoogle ChromeでFlags(chrome://flags/#native-file-system-api)を開き、Native File System APIを「enable」にセットする必要があります。

FileSystemFileHandle

読み取りや書き込みは基本的にFileSystemFileHandleオブジェクトを経由して行われます。まずはこのオブジェクトを取得するところから始まりますが、ボタンクリックなどユーザの操作を起点とする必要があります。

let handle;

open.addEventListener('click', async () => {
    try {
        // FileSystemFileHandleを取得
        fileHandle = await window.chooseFileSystemEntries();
    } catch (err) {
        console.error(err.message);
    }
});

chooseFileSystemEntriesはOSのファイルピッカーを表示するメソッドで、選択できるファイルタイプや、複数選択できるか否かなどのオプションを引数に渡すことができます。

また特定のファイルではなく、特定のディレクトリを選択させたい場合はchooseFileSystemEntriesメソッドの引数に{type: 'openDirectory'}を渡します。このとき得られるオブジェクトはFileSystemDirectroyHandleとなります。ファイル選択と違い、ディレクトリの場合はフォルダの場所を選択したあとに読み込みを許可するかどうかのプロンプトが表示されます。このプロンプトが許可されたあとで初めて内容を取得できるようになります。

読み込みを許可するかどうかのプロンプト

読み取り

Handleオブジェクトが取得できたら今度はファイルの中身を取得します。先ほど取得したFileSystemFileHandleオブジェクトを使います。

open.addEventListener('click', async (e) => {
  try {
        fileHandle = await window.chooseFileSystemEntries();

        // ファイルの中身を取得する
        const text = await fileHandle.getFile().then(file => file.text());

        textarea.value = text;
    } catch (err) {
      console.error(err.message);
    }
});

getFileメソッドで取得できるのはFileオブジェクトで、このオブジェクトは<input type="file">を使用した際に取得できるものと同じものです。

FileSystemDirectroyHandleオブジェクトが持つgetEntriesというメソッドでは特定のディレクトリ内に存在するすべてのファイルのHandleオブジェクトを取得できます。

let fileHandles = [];
openDir.addEventListener('click', async (e) => {
    try {
        directoryHandle = await window.chooseFileSystemEntries({type: 'openDirectory'});

        const entries = await directoryHandle.getEntries();
        for await (const entry of entries) {
            fileHandles.push(entry);
        }
    } catch (err) {
        console.error(err.message);
    }
});

ブラウザ上で動作するオンラインエディタなどを作る場合にはgetFilegetEntriesをうまく利用して作ることになりそうですね。

書き込み

これまで動的に生成したファイル自体をユーザーに保存させたい場合はcreateObjectURLを使用して、FileオブジェクトをURLに変換したうえでダウンロードさせるといった工夫をしていたと思います。しかし、Native File System APIが使えるようになれば、ユーザーの許可を得るだけでよりシンプルな保存が実装できるようになります。ファイルの書き込みにはFileSystemWriterオブジェクトを使います。

saveBtn.addEventListener('click', async () => {
    try {
        const value = textarea.value;
        const writer = await fileHandle.createWriter();

        await writer.truncate(0);
        await writer.write(0, value);
        await writer.close();
    } catch (err) {
        console.error(err.message);
    }
});

ここでは「読み込み」の章で選択したファイルをtextarea要素に出力したあと、同じtextarea要素上で編集し、保存ボタンを押下した際に上書き保存する処理を行っています。truncateメソッドでファイルの中身を一度すべて削除し、writeメソッドでファイルの内容を書き込んでいます。しかし、closeメソッドを実行するまではディスクに書き込まれないため、最後にcloseメソッドを実行するのを忘れないようにしましょう。

ページ上で初めて保存をしようとすると、ディレクトリを選択した際と同じようなプロンプトが表示されます。

書き込みを許可するかどうかのプロンプト

一度ユーザーが許可すれば、リロードされたりタブを閉じられない限りプロンプトは表示されなくなります。また、PWAに関しては今後のアップデートで、リロードされてもパーミッションが持ち越されるようになるようです。今回は上書き保存を紹介しましたが、もちろん別名で保存することもできます。

近年の動画や音楽ストリーミングアプリでは、あらかじめデータをダウンロードしておいて、オフラインでも視聴できるような機能が提供されています。NativeFileSystem APIを使えば同じことがWebサイトでも実現できるようになります。ゲームコンテンツの画像など必要なリソースを初期起動時にあらかじめ保存しておいたり、セーブデータをローカルに保存すれば、オフラインでもゲームができるようになります。データ制限に悩まされるユーザーの助けとなるかもしれませんね。