Origin private file system

Secure context: This feature is available only in secure contexts (HTTPS), in some or all supporting browsers.

Note: This feature is available in Web Workers.

The origin private file system (OPFS) is a storage endpoint provided as part of the File System API, which is private to the origin of the page and not visible to the user like the regular file system. It provides access to a special kind of file that is highly optimized for performance and offers in-place write access to its content.

Working with files using the File System Access API

The File System Access API, which extends the File System API, provides access to files using picker methods. For example:

  1. Window.showOpenFilePicker() allows the user to choose a file to access, which results in a FileSystemFileHandle object being returned.
  2. FileSystemFileHandle.getFile() is called to get access to the file's contents, the content is modified using FileSystemFileHandle.createWritable() / FileSystemWritableFileStream.write().
  3. FileSystemHandle.requestPermission({mode: 'readwrite'}) is used to request the user's permission to save the changes.
  4. If the user accepts the permission request, the changes are saved back to the original file.

This works, but it has some restrictions. These changes are being made to the user-visible file system, so there are a lot of security checks in place (for example, safe browsing in Chrome) to guard against malicious content being written to that file system. These writes are not in-place, and instead use a temporary file. The original is not modified unless it passes all the security checks.

As a result, these operations are fairly slow. It is not so noticeable when you are making small text updates, but the performance suffers when making more significant, large-scale file updates such as SQLite database modifications.

How does the OPFS solve such problems?

The OPFS offers low-level, byte-by-byte file access, which is private to the origin of the page and not visible to the user. As a result, it doesn't require the same series of security checks and permission grants and is therefore faster than File System Access API calls. It also has a set of synchronous calls available (other File System API calls are asynchronous) that can be run inside web workers only so as not to block the main thread.

To summarize how the OPFS differs from the user-visible file system:

  • The OPFS is subject to browser storage quota restrictions, just like any other origin-partitioned storage mechanism (for example IndexedDB API). You can access the amount of storage space the OPFS is using via navigator.storage.estimate().
  • Clearing storage data for the site deletes the OPFS.
  • Permission prompts and security checks are not required to access files in the OPFS.
  • Browsers persist the contents of the OPFS to disk somewhere, but you cannot expect to find the created files matched one-to-one. The OPFS is not intended to be visible to the user.

How do you access the OPFS?

To access the OPFS in the first place, you call the navigator.storage.getDirectory() method. This returns a reference to a FileSystemDirectoryHandle object that represents the root of the OPFS.

Manipulating the OPFS from the main thread

When accessing the OPFS from the main thread, you will use asynchronous, Promise-based APIs. You can access file (FileSystemFileHandle) and directory (FileSystemDirectoryHandle) handles by calling FileSystemDirectoryHandle.getFileHandle() and FileSystemDirectoryHandle.getDirectoryHandle() respectively on the FileSystemDirectoryHandle object representing the OPFS root (and child directories, as they are created).

Note: Passing { create: true } into the above methods causes the file or folder to be created if it doesn't exist.

js
// Create a hierarchy of files and folders
const fileHandle = await opfsRoot.getFileHandle("my first file", {
  create: true,
});
const directoryHandle = await opfsRoot.getDirectoryHandle("my first folder", {
  create: true,
});
const nestedFileHandle = await directoryHandle.getFileHandle(
  "my first nested file",
  { create: true },
);
const nestedDirectoryHandle = await directoryHandle.getDirectoryHandle(
  "my first nested folder",
  { create: true },
);

// Access existing files and folders via their names
const existingFileHandle = await opfsRoot.getFileHandle("my first file");
const existingDirectoryHandle =
  await opfsRoot.getDirectoryHandle("my first folder");

Reading a file

  1. Make a FileSystemDirectoryHandle.getFileHandle() call to return a FileSystemFileHandle object.
  2. Call the FileSystemFileHandle.getFile() object to return a File object. This is a specialized type of Blob, and as such can be manipulated just like any other Blob. For example, you could access the text content directly via Blob.text().

Writing a file

  1. Make a FileSystemDirectoryHandle.getFileHandle() call to return a FileSystemFileHandle object.
  2. Call FileSystemFileHandle.createWritable() to return a FileSystemWritableFileStream object, which is a specialized type of WritableStream.
  3. Write contents to it using a FileSystemWritableFilestream.write() call.
  4. Close the stream using WritableStream.close().

Deleting a file or folder

You can call FileSystemDirectoryHandle.removeEntry() on the parent directory, passing it the name of the item you want to remove:

js
directoryHandle.removeEntry("my first nested file");

You can also call FileSystemHandle.remove() on the FileSystemFileHandle or FileSystemDirectoryHandle representing the item you want to remove. To delete a folder including all subfolders, pass the { recursive: true } option.

js
await fileHandle.remove();
await directoryHandle.remove({ recursive: true });

The following provides a quick way to clear the entire OPFS:

js
await (await navigator.storage.getDirectory()).remove({ recursive: true });

Listing the contents of a folder

FileSystemDirectoryHandle is an asynchronous iterator. As such, you can iterate over it with a for await…of loop and standard methods such as entries(), values(), and keys().

For example:

js
for await (let [name, handle] of directoryHandle) {
}
for await (let [name, handle] of directoryHandle.entries()) {
}
for await (let handle of directoryHandle.values()) {
}
for await (let name of directoryHandle.keys()) {
}

Manipulating the OPFS from a web worker

Web Workers don't block the main thread, which means you can use the synchronous file access APIs in this context. Synchronous APIs are faster as they avoid having to deal with promises.

You can synchronously access a file by calling FileSystemFileHandle.createSyncAccessHandle() on a regular FileSystemFileHandle:

Note: Despite having "Sync" in its name, the createSyncAccessHandle() method itself is asynchronous.

js
const opfsRoot = await navigator.storage.getDirectory();
const fileHandle = await opfsRoot.getFileHandle("my highspeed file.txt", {
  create: true,
});
const syncAccessHandle = await fileHandle.createSyncAccessHandle();

There are a number of synchronous methods available on the returned FileSystemSyncAccessHandle:

  • getSize(): Returns the size of the file in bytes.
  • write(): Writes the content of a buffer into the file, optionally at a given offset, and returns the number of written bytes. Checking the returned number of written bytes allows callers to detect and handle errors and partial writes.
  • read(): Reads the contents of the file into a buffer, optionally at a given offset.
  • truncate(): Resizes the file to the given size.
  • flush(): Ensures that the file contents contain all the modifications done through write().
  • close(): Closes the access handle.

Here is an example that uses all the methods mentioned above:

js
const opfsRoot = await navigator.storage.getDirectory();
const fileHandle = await opfsRoot.getFileHandle("fast", { create: true });
const accessHandle = await fileHandle.createSyncAccessHandle();

const textEncoder = new TextEncoder();
const textDecoder = new TextDecoder();

// Initialize this variable for the size of the file.
let size;
// The current size of the file, initially `0`.
size = accessHandle.getSize();
// Encode content to write to the file.
const content = textEncoder.encode("Some text");
// Write the content at the beginning of the file.
accessHandle.write(content, { at: size });
// Flush the changes.
accessHandle.flush();
// The current size of the file, now `9` (the length of "Some text").
size = accessHandle.getSize();

// Encode more content to write to the file.
const moreContent = textEncoder.encode("More content");
// Write the content at the end of the file.
accessHandle.write(moreContent, { at: size });
// Flush the changes.
accessHandle.flush();
// The current size of the file, now `21` (the length of
// "Some textMore content").
size = accessHandle.getSize();

// Prepare a data view of the length of the file.
const dataView = new DataView(new ArrayBuffer(size));

// Read the entire file into the data view.
accessHandle.read(dataView);
// Logs `"Some textMore content"`.
console.log(textDecoder.decode(dataView));

// Read starting at offset 9 into the data view.
accessHandle.read(dataView, { at: 9 });
// Logs `"More content"`.
console.log(textDecoder.decode(dataView));

// Truncate the file after 4 bytes.
accessHandle.truncate(4);

Browser compatibility

BCD tables only load in the browser

See also