The File System Access API with Origin Private File System

It is very common for an application to interact with local files. For example, a general workflow is opening a file, making some changes, and saving the file. For web apps, this might be hard to implement. It is possible to simulate the file operations using IndexedDB API, an HTML input element with the file type, an HTML anchor element with the download attribute, etc, but that would require a good understanding of these standards and careful design for a good user experience. Also, the performance may not be satisfactory for frequent operations and large files.

The File System Access API makes it possible for web apps to have easy and efficient file access. It provides a way to create, open, read, and write files directly. It also allows apps to create directories and enumerate their contents.

Origin Private File System

WebKit has added support for the File System Access API with the origin private file system — a private storage endpoint to some origin. Conceptually, every origin owns an independent directory, and a page can only access files or directories in its origin’s directory. For example, https://webkit.org cannot read files created by https://apple.com.

Based on the implementation of different browsers, one entry in the origin private file system does not necessarily map to an entry in the user’s local filesystem — it can be an object stored in some database. That means a file or directory created via the File System Access API may not be easily retrieved from outside of the browser.

Persistence

The API is currently unavailable for Safari windows in Private Browsing mode. For where is it available, its storage lifetime is the same as other persistent storage types like IndexedDB and LocalStorage. The storage policy will conform to the Storage Standard. Safari users can view and delete file system storage for a site via Preferences on macOS or Settings on iOS.

Browser Support

The File System Access API with origin private file system is enabled in WebKit from r284131. It is available in Safari on:

  • macOS 12.2 and above
  • iOS 15.2 and above

In Safari on macOS 12.4 and iOS 15.4, we introduced the getFile() method of FileSystemFileHandle.

The API

WebKit currently supports four interfaces of the File System Access API:

  • FileSystemHandle, which represents an entry in the file system. It is available in Worker and
  • FileSystemFileHandle, which inherits from FileSystemHandle and represents a file entry.
  • FileSystemDirectoryHandle, which inherits from FileSystemHandle and represents a directory entry.
  • FileSystemSyncAccessHandle, which provides an exclusive duplex stream for synchronous read and write on an entry. Unlike the interfaces above, which exist in both Window and Worker contexts, FileSystemSyncAccessHandle is only available in Worker.

With these basic interfaces in mind, let’s look at how to use them by diving into some examples.

Examples

Accessing the Origin Private File System

In the origin private file system, a FileSystemHandle represents either the root directory of the origin’s space, or a descendant of the root directory. Therefore, the first step is to get the root FileSystemDirectoryHandle. It is done via StorageManager interface.

const root = await navigator.storage.getDirectory();

Creating a directory or a file

With a FileSystemDirectoryHandle object like root, you can get access to its child with some specific name using getDirectoryHandle() and getFileHandle() methods.

// Create a file named Untiled.txt under root directory.
const untitledFile = await root.getFileHandle("Untitled.txt", { "create" : true });
// Get access to existing Untitled.txt file.
// untitledFile and existingUntitledFile point to the same entry.
const existingUntitledFile = await root.getFileHandle("Untitled.txt");
// Create a directory named Diary Folder.
const diaryDirectory = await root.getDirectoryHandle("Diary Folder", { "create" : true });

Moving or Renaming a Directory or a File

To move around the file or directory a FileSystemHandle represents, you can use the move() method. The first parameter is a FileSystemDirectoryHandle representing the target parent directory, and the second parameter is a USVString representing the target file name. The string must be a valid file name.

// Move Untitled.txt from /root/ to /root/Diary Folder/.
await untitledFile.move(diaryDirectory, untitledFile.name);
// Rename Untitled.txt to Feb_01.txt
await untitledFile.move(diaryDirectory, "Feb_01.txt");
// The two steps above can be combined as:
// await untitledFile.move(diaryDirectory, "Feb_01.txt");

Resolving the Path from a Directory Entry to its Descendant

To find out if a FileSystemHandle is a descendant of an existing FileSystemDirectoryHandle, and to get their relative path, you can use the resolve() method. The result is an array of component names that forms the path.

// Get access to Feb_01.txt in Diary Folder.
const diaryFile = await diaryDirectory.getFileHandle("Feb_01.txt");
// Resolve path between Feb_01.txt and root.
const relativePath = await root.resolve(diaryFile);
// relativePath is ["Diary Folder", "Feb_01.txt"].

Enumerating Contents in a Directory

The methods introduced above require you to know the name of target, but if you don’t know the name, you can still get it by enumerating the contents of an existing directory with async iterators returned by the keys(), values(), and entries() methods.

// Create a directory named Trash under the root directory.
const trashDirectory = await root.getDirectoryHandle("Trash", { "create" : true });
// Find directories under root/ and print their names.
const directoryNames = [];
for await (const handle of root.values()) {
    if (handle.kind == "directory") {
        directoryNames.push(handle.name);
    }
}
// directoryNames is ["Trash", "Diary Folder"].

Deleting a Directory or a File

With a FileSystemDirectoryHandle object, you can delete its child entries by name with the removeEntry() method.

// Delete Feb_01.txt in Diary Folder.
await diaryDirectory.removeEntry(diaryFile.name);
// Delete Trash and all its descendants.
await root.removeEntry(trashDirectory.name, { "recursive" : true });

Reading a File

Once you have the FileSystemFileHandle representing the target file, you can read its properties and content by converting it to a File object using the getFile() method. You can get file information and content using interfaces of File.

const fileHandle = await root.getFileHandle("Draft.txt", { "create" : true });
const file = await fileHandle.getFile();

Reading and Writing a File in a Worker Thread

Another way to read a file is to use the read() method of the FileSystemSyncAccessHandle interface. You can create a FileSystemSyncAccessHandle from a FileSystemFileHandle object using the createSyncAccessHandle() method. Since FileSystemSyncAccessHandle is only available in Worker contexts, you will need to create a dedicated Worker first.

Unlike getFile() that returns a Promise, read() is synchronous, and thus provides better performance. If you’re aiming for the most efficient file access, FileSystemSyncAccessHandle is the way to go.

To write a file, you can use the synchronous write() method of FileSystemSyncAccessHandle. In the current implementation, this is the only way to write a file in WebKit.

To implement synchronous read and write operations, a FileSystemSyncAccessHandle must have exclusive access to a file entry. Therefore, the attempt to create a second FileSystemSyncAccessHandle on an entry will fail, if the previous FileSystemSyncAccessHandle is not closed properly.

// Get access to the existing Draft.txt file.
const root = await navigator.storage.getDirectory();
const draftFile = await root.getFileHandle("Draft.txt");
// Create FileSystemSyncAccessHandle on the file.
const accessHandle = await draftFile.createSyncAccessHandle();
// Get size of the file.
const fileSize = await accessHandle.getSize();
// Read file content to a buffer.
const readBuffer = new ArrayBuffer(fileSize);
const readSize = accessHandle.read(readBuffer, { "at": 0 });
// Write a sentence to the end of the file.
const encoder = new TextEncoder();
const writeBuffer = encoder.encode("Thank you for reading this.");
const writeSize = accessHandle.write(writeBuffer, { "at" : readSize });
// Truncate file to 1 byte.
await accessHandle.truncate(1);
// Persist changes to disk.
await accessHandle.flush();
// Always close FileSystemSyncAccessHandle if done.
await accessHandle.close();

Summary

If your web app needs to interact with files, you should try the new File System Access API. It provides interfaces that are similar to the native file system API, with optimized performance.

As the standard evolves and development goes on, we will keep adding or updating interfaces and methods according to the File System Access API spec. If you encounter any issue when using this API, please file a bug on bugs.webkit.org under the “Website Storage” component. You may also create a new bug report for feature requests, describing your use case and why the feature is important. If you have any question or suggestion about the API itself, you can file a spec issue in the WICG repo. Your feedback is very important to us.