📂

File System Access APIの使い方

2022/11/27に公開

今回はJavaScriptのFile System Access APIの使い方をまとめてみました。

File System Access APIを使えば、ブラウザからローカルファイルもしくはディレクトリにに対してread/writeができます。
ユーザーが許可したファイル、もしくは許可したディレクトリ以下にしかアクセスできませんので安全です。

このAPIはFireFoxとスマホ未対応で、今後仕様変更の可能性もありますが、必要最低限の機能は揃っています。

File System Access APIの使い方

本記事では、よく使うと思われる使い方しか説明しませんので、すべての使い方を知りたい場合はMDNが詳しいので参考にされてください。

https://developer.mozilla.org/en-US/docs/Web/API/File_System_API

ファイルハンドルとディレクトリハンドル

File System Access APIでファイルやディレクトリを操作するには、それぞれ、

  • ファイルハンドル(FileSystemFileHandleオブジェクト)
  • ディレクトリハンドル(FileSystemDirectoryHandleオブジェクト)

を使います。
まずはディレクトリハンドルから説明していきます。

ディレクトリハンドルの取得

ディレクトリハンドルを使うとディレクトリに対して操作ができるようになります。
これを取得するには主に2つの方法があります。

window.showDirectoryPicker()関数

この関数を使うと、ダイアログが立ち上がり、ユーザーにディレクトリを選択してもらうことができます。
関数はPromiseを返します。ユーザーがディレクトリを選択するとPromiseが解決し、選択をキャンセルするとPromiseが失敗になります。
ファイル操作はPromiseが多いためawaitを使ったほうが書きやすいです。

let dirHandle = await window.showDirectoryPicker()

もし、ディレクトリを開く時点で書き込みを許可する場合は、modeオプションを"readwrite"にします。ちなみに指定しなくても初めて書き込むときに、ブラウザがユーザーに許可をとってくれるため、指定しないほうがユーザーは安心すると思います。

書き込みも許可するとき
let dirHandle = await window.showDirectoryPicker({ mode: "readwrite" })

dropイベント

ブラウザで開いているページにディレクトリをドラッグ&ドロップしたときに、dropイベントからディレクトリハンドルを取得することができます(SafariとFirefoxは未対応)。

ディレクトリハンドルだけでなく、後述のファイルハンドルも取得しますので、kindプロパティでディレクトリかファイルかを判別します。

document.addEventListener('dragover', event => event.preventDefault())
document.addEventListener('drop', async event => {
  event.preventDefault()

  for (let item of event.dataTransfer.items) {
    let handle = await item.getAsFileSystemHandle()
    if (handle.kind === 'directory') {
      let dirHandle = handle
      ...
    } else {
      let fileHandle = handle
      ...
    }
  }
})

ディレクトリハンドルの操作

取得したディレクトリハンドルでできることを説明します。

ファイル一覧を取得

for await...of文を使えば、ディレクトリ内のファイル(と子ディレクトリ)の一覧を一つずつ取得できます。

for await (let [name, handle] of dirHandle) {
  if (handle.kind === 'file') { // ファイルのとき
    ...
  } else { // ディレクトリのとき
    ...
  }
}

dirHandledirHandle.entries()メソッドに置き換えても同じイテレータが取得できます。名前だけを一つずつ取得するdirHandle.keys()メソッドや、ハンドルだけを一つずつ取得するdirHandle.values()メソッドもあります。

子ディレクトリを開く(作る)

ディレクトリの名前が分かっている場合はdirHandle.getDirectoryHandle()メソッドを使って子ディレクトリのハンドルを取得できます。

let childDirHandle = await dirHandle.getDirectoryHandle('child')

なお、createオプションをtrueにするとディレクトリが無い時に新規作成します。

let childDirHandle = await dirHandle.getDirectoryHandle('child', { create: true })

ファイルを開く(作る)

ファイルの名前が分かっている場合はdirHandle.getFileHandle()メソッドを使って対象ファイルのハンドルを取得できます。

let fileHandle = await dirHandle.getFileHandle('file.txt')

なお、createオプションをtrueにするとファイルが無い時に新規作成します。

let fileHandle = await dirHandle.getFileHandle('file.txt', { create: true })

ファイルハンドルの取得

次にファイルハンドルを説明していきます。
ファイルハンドルを取得するには次の3つの方法があります。

  • window.showOpenFilePicker()関数
  • ディレクトリハンドルのgetFileHandle()メソッド(前述)
  • dropイベント(前述)

2つはすでに説明していますのでwindow.showOpenFilePicker()関数の使い方を説明します。

window.showOpenFilePicker()関数

この関数を使うと、ダイアログが立ち上がり、ユーザーにファイルを選択してもらうことができます。
結果はファイルハンドルの配列で返ります。

let [fileHandle] = await showOpenFilePicker()

もし、開くファイル形式を指定する場合は、次のようにMINEタイプと拡張子を列挙します。このときimage/*のようにワイルドカードも利用できます。

let [fileHandle] = await showOpenFilePicker({
  types: [
    {
      description: '画像ファイル',
      accept: {
        'image/jpeg': ['.jpeg', '.jpg'],
        'image/webp': ['.webp'],
        'image/png': ['.png'],
        'image/svg': ['.svg'],
        'image/gif': ['.gif'],
      },
    },
  ],
  excludeAcceptAllOption: true,
})

複数ファイル選択を可能にするにはmultipleオプションを使います。

let fileHandles = await showOpenFilePicker({ multiple: true })

ファイルハンドルの操作

取得したファイルハンドルでできることを説明します。

Fileオブジェクトを取得

ファイルの情報を読み取るために、fileHandle.getFile()メソッドでFileオブジェクトを取得できます。
これにより<input type="file">を使ったときと同じようにファイルに対する処理が可能となります。

let file = await fileHandle.getFile()

Fileオブジェクトで何ができるのかは本記事で説明しません。「JS Fileオブジェクト」でググってみてください。

ファイルに書き込む

ファイルに書き込みを行うにはfileHandle.createWritable()メソッドで書き込みストリームを取得します。
ファイルに上書きするには次のようになります。

上書き
let writableStream = await fileHandle.createWritable()
await writableStream.write('Hello\n')
await writableStream.close()

write()で書き込みできるのはArrayBuffer、TypedArray、DataView、Blob、そして文字列です。

上書きせずにファイルの末尾に追記するときはkeepExistingDataオプションをtrueにし、seek()で書き込み開始位置を末尾まで移動する必要があります。

追記
let writableStream = await fileHandle.createWritable({ keepExistingData: true })
let file = await fileHandle.getFile()
await writableStream.seek(file.size)
await writableStream.write('World\n')
await writableStream.close()

File System Access APIの用途

以上。よく使う使い方はこんなところでしょうか。
これにより、次のようなことができるようになります。

  • ローカルファイルへの書き込み
  • ローカルディレクトリ単位でファイルの読み込み
  • ダウンロードボタンを押さずにファイルを保存

ゲームデータやペイントアプリの保存に活用できると思います。このAPIをフル活用しているものと言えばVSCode Onlineです。

https://vscode.dev/

私はコードエディタとしてではなくメモ帳として使っています笑

ElectronやWebView2を使わなくても、色々なことがWebアプリでできるようになってきていますね。

このAPIを使って画像ビュワーを作ってみました。

https://zenn.dev/itte/articles/a6dc3108752a16

気になること

便利なAPIなのですが、気になることがひとつ。

確認アラート

これは、ディレクトリの読み取りを許可するかどうか確認するEdgeのアラートなのですが、小さすぎます(ChromeもOperaも同じ)。
読み取りと書き込みの両方の確認のときは「表示」の表記が「編集」に替わるだけです。

わかりにくい

これだと、よく理解せずに進んでしまう人が増えるので、悪意あるWebサイトが読み取りだけに見せかけて、こっそりウイルスを仕込むことが簡単にできそうです。各ブラウザにはぜひ改善して欲しいです。

Discussion