ファイルダウンロード時に「Downloadディレクトリではなくユーザーが選択したディレクトリにダウンロードする」という処理を実装する機会がありました。というわけで今回はこちらの実装方法を解説します。
JavaScriptのFileSystemAccessAPIというものを使います。
FileSystemAccessAPIとは?
まずはあまり馴染みのないFileSystemAccessAPIについて簡単に説明します。
FileSystemAccessAPIを使うとローカルのファイルやディレクトリに対して読み書き(read/write)の操作できます。例えば、
- ローカルのファイルを取得する
- ローカルに新しいファイルを作成して内容を書き込む
- 特定のディレクトリ配下のファイルを全て取得する
- 任意のディレクトリを新規作成し、そこにファイルを保存する
みたいなことができます。今回行うのは「ユーザーが選択したディレクトリにファイルを保存する」ですね。
なお実行する際はユーザーの許可が必要(ブラウザがポップアップを表示する)のでセキュリティ的にも問題ないと思います。
そしてFileSystemAccessAPIには、
- FileSystemFileHandle(ファイル操作)
- FileSystemDirectoryHandle(ディレクトリ操作)
の2種類のインターフェイスがあり、これらを使ってローカルのファイルやディレクトリを読み書きできるわけですね。
様々な処理ができるのでより詳しく知りたい人はMDNを参照してください。
>>ファイルシステム API – Web API | MDN
選択した任意のディレクトリにファイルを保存する方法
ファイルダウンロードというよりは、ファイルオブジェクトを用意して任意のディレクトリにファイルを作成して内容を「書き込む」というイメージが近いです。
試しにボタンクリックでテキストファイルを任意のディレクトリに保存する処理を実装してみます。FileSystemAccessAPIを使って以下のように書けばokです。
実際のデモを用意しました。ボタンをクリックするとディレクトリ選択ウィンドウが表示されるので任意のディレクトリを選択するとそこにファイルが保存されます。
このデモ上では実際にダウンロードはできないため試してみたい人は以下のURLからどうぞ。
>>https://vitejsvitersdr2v-zxlu–5173–134daa3c.local-credentialless.webcontainer.io
コードは以下のようになっています。
<button type="button" id="button">ダウンロード</button>
async function saveFileToDirectory(file) {
try {
// 1.ディレクトリ選択ダイアログを表示
const dirHandle = await window.showDirectoryPicker();
// 2.ファイル名を指定して保存(元のファイル名はfile.nameで取得可能)
const newFileHandle = await dirHandle.getFileHandle(file.name, {
create: true,
});
// 3.書き込み用のファイルストリームを取得
const writable = await newFileHandle.createWritable();
// 4.ファイル内容を書き込む
await writable.write(file);
// 5.書き込みを完了してファイルを閉じる
await writable.close();
} catch (error) {
// ディレクトリ選択をキャンセルした場合など
}
}
// Fileオブジェクト
const file = new File(["Hello, world!"], "hello.txt", { type: "text/plain" });
document.getElementById("button").addEventListener("click", async () => {
// 6.ファイルオブジェクトをファイルとして保存
await saveFileToDirectory(file);
});
1のwindow.showDirectoryPicker()はディレクトリ選択ダイアログを表示させるメソッドです。これが発火するとユーザーが任意のディレクトリを選択できるダイアログが表示されます。
また、window.showDirectoryPicker()
の戻り値はFileSystemDirectoryHandleです。ディレクトリ選択後にFileSystemDirectoryHandleを使ってディレクトリやファイルを操作する感じです。
2で早速FileSystemDirectoryHandleのgetFileHandle()メソッドを使います。オプションで{ create: true }
を渡すとファイルが存在しない場合に新規作成します。
また、getFileHandle(
)の戻り値は今度はFileSystemFileHandleです。これを使って今度はファイル操作を行います。
3でFileSystemFileHandleのcreateWritable()でファイル書き込みのストリームを作成します。ファイル書き込みのための準備みたいなものだと思えばokです。
戻り値はFileSystemWritableFileStreamというインターフェイスで、次はこれを使って実際に書き込みを行います。
4でFileSystemWritableFileStreamのwrite()メソッドを使ってファイルに書き込みを行います。write()
渡すデータはArrayBufferやBlobなどです。今回はFileオブジェクトを渡しています。FileオブジェクトはBlobオブジェクトを継承しているのでwrite()
にそのまま渡せばokです(Blobオブジェクトを拡張したものがFileオブジェクト)。
5のclose()
でストリームを閉じ、書き込みを終了します。
また、catch
側はユーザーがディレクトリ選択をキャンセルしたときなどに通ります。この場合の処理も書いておかないとコンソールにエラーが出るので注意しましょう。
ボタンクリックでこれらの一連の処理を行うことで、ユーザーが選択した任意のディレクトリにファイルをダウンロード(実際はファイルを新規作成して書き込んで保存)できるという感じです。難しいですね。
見て分かる通り様々なインターフェイスが連なって一連の処理を行っています。ちょっと大変ですが全体像をなんとなくでも掴んでおくと理解が深まると思います。
FileSystemAccessAPIを使う際の注意点
MDNを見ると分かる通り、一部のメソッドやメソッドに渡せるオプションがFirefoxとSafariとスマホで非対応のものが結構あります(2024年10月現在)。
特にSafariはPCでのシェアも多いので実際の現場で使っても大丈夫かはよく確認したほうがいいでしょう。
まとめ
FileSystemAccessAPIは普段なかなか使わないと思いますが、いざ使うとなると専用のインターフェイスやメソッドがたくさんあって結構理解が大変だと思います。
階層構造になっている部分も多いので、処理を伝っていってなんとなく全体像をつかめると良いと思います。
使いこなせると更にできることが増えて楽しそうですね…。