The File System 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 “file” type, an HTML anchor element with the download attribute, etc, but that would require good understanding of these standards and careful design for good user experience. Also, the performance may not be satisfactory for frequent operations and large files.

The File System 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. Besides, it allows apps to create directories and enumerate their contents.

Origin Private File System

WebKit has added support for the File System Standard. This provides an 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 implementation of different browsers, one entry in the origin private file system does not necessarily map to an entry in user’s local filesystem — it can be an object stored in some database. That means a file or directory created via the File System 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

File System with origin private file system is enabled in WebKit from 242951@main. 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 introduce a method getFile() of FileSystemFileHandle.

The API

WebKit currently supports four interfaces of the File System Standard:

  • 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 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 target parent directory, and the second parameter is a USVString representing 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 existing FileSystemDirectoryHandle, and to get their relative path, you can use 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 content of existing directory with async iterators returned by keys(), values(), and entries() methods.

// Create a directory named `*Trash*` under 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 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](https://w3c.github.io/FileAPI/#file-section) object using 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 read() method of FileSystemSyncAccessHandle interface. You can create FileSystemSyncAccessHandle from a FileSystemFileHandle object using 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 aim for most efficient file access, FileSystemSyncAccessHandle is the way to go.

To write a file, you can use 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 = 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.
accessHandle.truncate(1);
// Persist changes to disk.
accessHandle.flush();
// Always close FileSystemSyncAccessHandle if done.
accessHandle.close();

Summary

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

As the standard evolves and our developments goes on, we will keep adding or updating interfaces and methods according to the File System Standard. If you encounter any issue when using this API, please file a bug on bugs.webkit.org under component “Website Storage”. You may also create a new bug report for feature request, 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.