File System Access API 試す
The File System Access API (formerly known as Native File System API and prior to that it was called Writeable Files API)
Native File System API は古い呼び名か
The File System Access API—despite the similar name—is distinct from the FileSystem interface exposed by the File and Directory Entries API, which documents the types and operations made available by browsers to script when a hierarchy of files and directories are dragged and dropped onto a page or selected using form elements or equivalent user actions. It is likewise distinct from the deprecated File API: Directories and System specification, which defines an API to navigate file system hierarchies and a means by which browsers may expose sandboxed sections of a user's local filesystem to web applications.
Fle System Access API と FileSystem は別物なのか
実際に試してみる
とりあえず vite + React + TS で試してみる
ありがたいことに型も見つかったので、@types/wicg-file-system-access を使っていく
File 読み込み
雑ではあるが....
これで選択した file の中身を表示できた
function App() {
const [content, setContent] = useState<string>('');
return (
<div>
<h1>File System Access API</h1>
<div>
<button
onClick={async () => {
const [fileHandle] = await window.showOpenFilePicker();
const file = await fileHandle.getFile();
const content = await file.text();
setContent(content);
}}
>
Click Me to select file
</button>
</div>
<div>
<h2>File Content</h2>
<div>{content}</div>
</div>
</div>
);
}
File 書き込み (File の新規作成)
const CreateNewFile = () => {
const [content, setContent] = useState<string>('');
return (
<div>
<h1>File System Access API - Create a new file</h1>
<div>
<textarea
value={content}
onChange={(e) => setContent(e.currentTarget.value)}
/>
<button
onClick={async () => {
const handle = await window.showSaveFilePicker({
types: [
{
description: 'Text Files',
accept: {
'text/plain': ['.txt'],
},
},
],
});
const writable = await handle.createWritable();
await writable.write(content);
await writable.close();
}}
>
Create a new file
</button>
</div>
</div>
);
};
Remove のデータを local の File に保存
Writing data to disk uses a FileSystemWritableFileStream object, a subclass of WritableStream.
The write() method takes a string, which is what's needed for a text editor. But it can also take a BufferSource, or a Blob. For example, you can pipe a stream directly to it:
てなわけで、fetch した response を pipe で流し込めるみたい、便利そう
const WriteFileFromRemoteUrl = () => {
return (
<div>
<h1>File System Access API - Write a file from remote url</h1>
<div>
<button
onClick={async () => {
const handle = await window.showSaveFilePicker({
types: [
{
description: 'Text Files',
accept: {
'text/plain': ['.txt'],
},
},
],
});
const writable = await handle.createWritable();
const response = await fetch(
'https://jsonplaceholder.typicode.com/posts'
);
await response.body?.pipeTo(writable);
}}
>
Create a new file
</button>
</div>
</div>
);
};
デフォルトの File 名を指定
const handle = await window.showSaveFilePicker({
suggestedName: 'Untitled Text.txt',
types: [
{
description: 'Text Files',
accept: {
'text/plain': ['.txt'],
},
},
],
});

Specifying the purpose of different file pickers
id を振って、複数の file handle を扱うことも可能
const fileHandle1 = await self.showSaveFilePicker({
id: 'openText',
});
const fileHandle2 = await self.showSaveFilePicker({
id: 'importImage',
});
Storing file handles or directory handles in IndexedDB
File handles and directory handles are serializable, which means that you can save a file or directory handle to IndexedDB, or call postMessage() to send them between the same top-level origin.
FIle handle は serialize できるので、indexedDB 等に保存することもできる
Stored file or directory handles and permissions
queryPermission(): すでに権限を許可済みか確認
requestPermission(): 権限をリクエスト
async function verifyPermission(fileHandle, readWrite) {
const options = {};
if (readWrite) {
options.mode = 'readwrite';
}
// Check if permission was already granted. If so, return true.
if ((await fileHandle.queryPermission(options)) === 'granted') {
return true;
}
// Request permission. If the user grants permission, return true.
if ((await fileHandle.requestPermission(options)) === 'granted') {
return true;
}
// The user didn't grant permission, so return false.
return false;
}
While FileSystemHandle objects can be serialized and stored in IndexedDB, the permissions currently need to be re-granted each time, which is suboptimal. Star crbug.com/1011533 to be notified of work on persisting granted permissions.
FileSystemHandle を serialize して保存した場合でも、権限に関しては保持されないので、毎回許可してもらう必要がある
Opening a directory and enumerating its contents
To enumerate all files in a directory, call showDirectoryPicker(). The user selects a directory in a picker, after which a FileSystemDirectoryHandle is returned, which lets you enumerate and access the directory's files. By default, you will have read access to the files in the directory, but if you need write access, you can pass { mode: 'readwrite' } to the method.
showDirectoryPicker() で FileSystemDirectoryHandle を取得して、 directory 内の file にアクセス
const dirHandle = await window.showDirectoryPicker();
for await (const entry of dirHandle.values()) {
console.log(entry.kind, entry.name);
}
You can suggest a default start directory by passing a startIn property to the showSaveFilePicker, showDirectoryPicker(), or showOpenFilePicker methods like so.
showDirectoryPicker() に startIn でデフォルトの選択 directory を設定できる
const dirHandle = await window.showDirectoryPicker({
startIn: 'documents',
});
Creating or accessing files and folders in a directory
FileSystemDirectoryHandle を利用して、
- 新規 directory の作成
- 新規 file の作成
が可能。
// In an existing directory, create a new directory named "My Documents".
const newDirectoryHandle = await existingDirectoryHandle.getDirectoryHandle('My Documents', {
create: true,
});
// In this new directory, create a file named "My Notes.txt".
const newFileHandle = await newDirectoryHandle.getFileHandle('My Notes.txt', { create: true });
Resolving the path of an item in a directory
path の取得も可能
// Resolve the path of the previously created file called "My Notes.txt".
const path = await newDirectoryHandle.resolve(newFileHandle);
// `path` is now ["My Documents", "My Notes.txt"]
Deleting files and folders in a directory
If you have obtained access to a directory, you can delete the contained files and folders with the removeEntry() method. For folders, deletion can optionally be recursive and include all subfolders and the files contained therein.
removeEntry() で directory 内の file おそび folder の削除が可能
// Delete a file.
await directoryHandle.removeEntry('Abandoned Projects.txt');
// Recursively delete a folder.
await directoryHandle.removeEntry('Old Stuff', { recursive: true });
Deleting a file or folder directly
FileSystemFileHandle, FileSystemDirectoryHandle に対して、remove() を直接呼び出せば自身を削除可能
// Delete a file.
await fileHandle.remove();
// Delete a directory.
await directoryHandle.remove();
Renaming and moving files and folders
file, folder の名前の変更・移動
// Rename the file.
await file.move('new_name');
// Move the file to a new directory.
await file.move(directory);
// Move the file to a new directory and rename it.
await file.move(directory, 'newer_name');
基本的にはこんなもんで、他にもこの辺についても記述あり↓
- Drag and drop integration
- Accessing the origin private file system
The origin private file system is a storage endpoint that, as the name suggests, is private to the origin of the page.
origin private file system があるの知らなかった
Security and permissions
Restricted folders
To help protect users and their data, the browser may limit the user's ability to save to certain folders, for example, core operating system folders like Windows, the macOS Library folders, etc. When this happens, the browser shows a modal prompt and ask the user to choose a different folder.
そもそもアクセスが制限されている folder も存在する
e.g.) Windows, the macOS Library folders
Permission persistence
The web app can continue to save changes to the file without prompting until all tabs for its origin are closed. Once a tab is closed, the site loses all access. The next time the user uses the web app, they will be re-prompted for access to the files.
origin ごとに、全ての tab が閉じるまでアクセス権限は保持される