File System Access APIを使ってローカルファイルの読み書きを試してみる

## はじめに

File System Access API（旧名：Native File System API）はファイルシステムをWebブラウザ上で再現したAPIで、特筆すべきはユーザー端末のローカルファイルを直接扱うことができる点です。一応、以前より[HTML5 File API](https://www.html5rocks.com/ja/tutorials/file/dndfiles/)などを使ってのローカルファイル入出力自体は可能でしたが、それらより柔軟に扱えるようになってます。

同APIを用いたサンプルアプリとして[Googleの中の人によるテキストエディタアプリ](https://googlechromelabs.github.io/text-editor/)が[ソースコードとともに](https://github.com/GoogleChromeLabs/text-editor/)公開されてますが、よりAPI自体に焦点を当てた超簡易エディターも作ってみましたので本エントリを眺める上での参考になればと思います。
（最新版Chrome/Chromiumでのみ動作確認、adblockerなどを使ってると上手く動作しないので注意）

- 簡易エディター実行リンク：https://pentamania.github.io/native-fs-sample/
- githubリポジトリ：https://github.com/pentamania/native-fs-sample

:::note warn
執筆時点（2022年8月）で同APIに対応しているブラウザはPC版Chrome、もしくはMS Edge等のChromium系ブラウザ[^brave]のみで、FirefoxやSafari[^safari]、モバイル系ブラウザは対応していません。
参考: [Can I Useリンク](https://caniuse.com/native-filesystem-api)
:::

[^brave]:ただしChromium系であってもBraveブラウザのように[セキュリティ・プライバシー保護への懸念から敢えてサポートしていない場合があります](https://twitter.com/BraveSampson/status/1452025296882970632)

[^safari]:Safariは[一部の機能（Origin Private File System）を実装している](https://twitter.com/tomayac/status/1454018244843606026)ようですが、ローカルファイルにアクセスするためのAPIは実装されておらず、本記事で紹介するような形では利用できません（このOrigin~という機能は、Web StorageやIndexed Databaseのような、アプリ側が独自に生成したデータを管理するためのもののようです）

本記事ではユーザーファイルの読み書き方法について書いていきます。

## 前提・注意

- 筆者使用OSはwindows 10 64bit版
- 関連メソッドのほとんどがPromiseを返すため、非同期処理やasync/awaitが分かってないと辛い
- Secured Contextな環境でしか使えません（httpsやlocalhostなど）

## 使い方

### 基本
起点となるAPIについてですが、読み込みを行うときは`window.showOpenFilePicker`、保存（書き込み）のときは`window.showSaveFilePicker`です。
これらは名前の示す通り、実行するとファイル選択ダイアログを開きます。そしてつつがなくファイル処理を終えると`FileSystemHandle`という型のオブジェクトを返します。
以下、このオブジェクトのことを`handle`と呼びます。

#### FileSystemHandleについて
これはブラウザと対象ファイルの橋渡しを行うオブジェクトのようで、ファイルの内容を読み取ったり、書き込みなどに使います。特に上書き保存の際はこれを保持しておく必要がありますが、詳しくは後述。
参考：https://wicg.github.io/file-system-access/#api-filesystemfilehandle

### ファイル読み込み

1. showOpenFilePickerでダイアログを開き、ユーザーのファイル選択を待ちます
2. ファイルが選択されるとhandleを取得
3. handleからgetFile()でfileオブジェクトを取得
4. fileからさらにgetText()でファイル内容を取得

```js
const options = {
  types: [
    {
      description: "Text Files",
      accept: {
        "text/plain": [".txt", ".text"],
      },
    },
  ],
};

(async ()=> {
  const handle = await window.showOpenFilePicker(options);
  const file = await handle.getFile();
  const text = await file.getText();
  console.log(text); // 内容を出力
})();
```
開きたいファイル種類に合わせてオプション内容は変更してください。
なお、showOpenFilePickerでのファイル選択をキャンセルすると例外が投げられるので、エラーハンドリングする場合はcatchで対応します。


### ファイルの保存（書き込み）
window.showSaveFilePickerで保存ダイアログを開きます。

#### 新規保存（「名前を付けて保存」）

1. showSaveFilePickerでダイアログを開き、ユーザーのファイル選択を待ちます
2. ファイルが選択されるとhandleを取得
3. handleからwritableオブジェクト生成
4. writableを使って内容を書き込みます

ファイル選択をキャンセルすると例外が投げられるのは一緒です。

```js
/**
 * 書き込み処理
 */
async function writeFile(fileHandle, contents) {
  // writable作成
  const writable = await fileHandle.createWritable();

  // コンテンツを書き込む
  await writable.write(contents);

  // ファイル閉じる
  await writable.close();
}

// メイン処理
const saveFileOptions = {
  types: [
    {
      description: "Text Files",
      accept: {
        "text/plain": [".txt"],
      },
    },
  ],
};
(async ()=> {
  const textContent = "書き込みたい内容";
  const handle = await window.showSaveFilePicker(saveFileOptions);
  await writeFile(handle, textContent);
  console.log('書き込み完了');
})();
```

#### 上書き保存
showSaveFilePickerやshowOpenFilePickerで得たhandleを保持しておき、書き込み時に流用することで、新たにダイアログを開くことなく同じファイルにそのまま上書き保存することができます。

handleが存在するかで分岐させると良い感じになるかと思います。

```js
async function saveFile(content, handle) {
  if (!handle) {
    handle = await window.showSaveFilePicker(saveFileOptions);
  }
  await writeFile(handle, contents);
  return handle;
}

(async ()=> {
  let globalHandle;

  /* 上書き保存（初回はglobalHandleがundefinedなので[名前をつけて保存]を実行）*/
  globalHandle = await saveFile("書き込む内容", globalHandle);

  /* 名前を付けて保存 */
  globalHandle = await saveFile("書き込む内容", null);
})();
```

ちなみにローカルファイルを開いて初めて上書き保存する際、以下のようなダイアログでワンクッションが置かれ、ユーザーがファイルを誤って上書きしてしまうということを防いでいます。
![NFS確認ダイアログ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/81823/6426071f-9c7d-f798-8e3c-9ce56b34a81c.png)
（2回目以降は出ません）

## その他
- 自環境では確認できませんでしたが、パーミッションを要求されることがあるかもしれません。
- ファイルのフルパスは（当然ですが）取得できません。
- まだ基本的なAPIしかありませんが、これから拡充されていくらしいです。
- 明らかにセキュリティ・プライバシー関係でトラブることが想定されるAPIのため、MozillaやAppleが追従する見込みは薄そうです。~~というかGoogleが野心的すぎるとも~~
    - ちなみにMicrosoftはMS Edgeがサポートしてるのはもちろん、[Visual Studio Code for the Web](https://vscode.dev/)にて同APIを利用した機能を実装しており、否定的ではない？

前述の通り、ほぼchromium系ブラウザでしか扱えないので本格的な実戦投入は難しそうですが、ちょっとした個人・組織内アプリ等でローカルファイルを取り扱いたいときに便利なのではと思います。


## 参考
- [The File System Access API: simplifying access to local files](https://web.dev/file-system-access/)
- [API仕様ページ](https://wicg.github.io/file-system-access/)
- [オマケ：Origin Trial版との違いがまとめられたドキュメント](https://github.com/WICG/file-system-access/blob/master/changes.md)