The File System Standard introduces an origin private file system (OPFS) as a storage endpoint private to the origin of the page and not visible to the user that provides optional access to a special kind of file that is highly optimized for performance.
Browser support #
The origin private file system is supported by modern browsers and is standardized by the Web Hypertext Application Technology Working Group (WHATWG) in the File System Living Standard.
Browser support
- Chrome 86, Supported 86
- Firefox 111, Supported 111
- Edge 86, Supported 86
- Safari 15.2, Supported 15.2
Motivation #
When you think of files on your computer, you probably think about a file hierarchy: files organized in folders that you can explore with your operating system’s file explorer. For example, on Windows, for a user called Tom, their To Do list might live in C:UsersTomDocumentsToDo.txt
. In this example, ToDo.txt
is the file name, and Users
, Tom
, and Documents
are folder names. C:
on Windows represents the root directory of the drive.
Traditional way of working with files on the web #
To edit the To Do list in a web application, this is the traditional flow:
- The user uploads the file to a server or opens it on the client with
<input type="file">
. - The user makes their changes, and then downloads the resulting file with an injected
<a download="ToDo.txt>
that you programmaticallyclick()
via JavaScript. - For opening folders, you use a special attribute in
<input type="file" webkitdirectory>
, which, despite its proprietary name, has practically universal browser support.
Modern way of working with files on the web #
This flow is not representative of how users think of editing files, and means users end up with downloaded copies of their input files. Therefore, the File System Access API introduced three picker methods—showOpenFilePicker()
, showSaveFilePicker()
, and showDirectoryPicker()
—that do exactly what their name suggests. They enable a flow as follows:
- Open
ToDo.txt
withshowOpenFilePicker()
, and get aFileSystemFileHandle
object. - From the
FileSystemFileHandle
object, get aFile
by calling the file handle’sgetFile()
method. - Modify the file, then call
requestPermission({mode: 'readwrite'})
on the handle. - If the user accepts the permission request, save the changes back to the original file.
- Alternatively, call
showSaveFilePicker()
and let the user pick a new file. (If the user picks a previously opened file, its contents will be overwritten.) For repeat saves, you can keep the file handle around, so you don’t have to show the file save dialog again.
Restrictions of working with files on the web #
Files and folders that are accessible via these methods live in what can be called the user-visible file system. Files saved from the web, and executable files specifically, are marked with the mark of the web, so there’s an additional warning the operating system can show before a potentially dangerous file gets executed. As an additional security feature, files obtained from the web are also protected by Safe Browsing, which, for the sake of simplicity and in the context of this article, you can think of as a cloud-based virus scan. When you write data to a file using the File System Access API, writes are not in-place, but use a temporary file. The file itself is not modified unless it passes all these security checks. As you can imagine, this work makes file operations relatively slow, despite improvements applied where possible, for example, on macOS. Still every write()
call is self-contained, so under the hood it opens the file, seeks to the given offset, and finally writes data.
Files as the foundation of processing #
At the same time, files are an excellent way to record data. For example, SQLite stores entire databases in a single file. Another example are mipmaps used in image processing. Mipmaps are pre-calculated, optimized sequences of images, each of which is a progressively lower resolution representation of the previous, which makes many operations like zooming faster. So how can web applications get the benefits of files, but without the performance costs of traditional web-based file processing? The answer is the origin private file system.
The user-visible versus the origin private file system #
Unlike the user-visible file system browsed via the operating system’s file explorer, with files and folders you can read, write, move, and rename, the origin private file system is not meant to be seen by users. Files and folders in the origin private file system, as the name suggests, are private, and more concretely, private to the origin of a site. Discover the origin of a page by typing location.origin
in the DevTools Console. For example, the origin of the page https://developer.chrome.com/articles/
is https://developer.chrome.com
(that is, the part /articles
is not part of the origin). You can read more about the theory of origins in Understanding “same-site” and “same-origin”. All pages that share the same origin can see the same origin private file system data, so https://developer.chrome.com/docs/extensions/mv3/getstarted/extensions-101/
can see the same details as the previous example. Each origin has its own independent origin private file system, which means the origin private file system of https://developer.chrome.com
is completely distinct from the one of, say, https://web.dev
. On Windows, the root directory of the user-visible file system is C:
. The equivalent for the origin private file system is an initially empty root directory per origin accessed by calling the asynchronous method navigator.storage.getDirectory()
. For a comparison of the user-visible file system and the origin private file system, see the following diagram. The diagram shows that apart from the root directory, everything else is conceptually the same, with a hierarchy of files and folders to organize and arrange as needed for your data and storage needs.
Specifics of the origin private file system #
Just like other storage mechanisms in the browser (for example, localStorage or IndexedDB), the origin private file system is subject to browser quota restrictions. When a user clears all browsing data or all site data, the origin private file system will be deleted, too. Call navigator.storage.estimate()
and in the resulting response object see the usage
entry to see how much storage your app already consumes, which is broken down by storage mechanism in the usageDetails
object, where you want to look at the fileSystem
entry specifically. Since the origin private file system is not visible to the user, there are no permissions prompts and no Safe Browsing checks.
Getting access to the root directory #
To get access to the root directory, run the command below. You end up with an empty directory handle, more specifically, a FileSystemDirectoryHandle
.
const opfsRoot = await navigator.storage.getDirectory();
// A FileSystemDirectoryHandle whose type is "directory"
// and whose name is "".
console.log(opfsRoot);
Main thread or Web Worker #
There are two ways of using the origin private file system: on the main thread or in a Web Worker. Web Workers cannot block the main thread, which means in this context APIs can be synchronous, a pattern generally disallowed on the main thread. Synchronous APIs can be faster as they avoid having to deal with promises, and file operations are typically synchronous in languages like C that can be compiled to WebAssembly.
// This is synchronous C code.
FILE *f;
f = fopen("example.txt", "w+");
fputs("Some textn", f);
fclose(f);
If you need the fastest possible file operations and/or you deal with WebAssembly, skip down to Using the origin private file system in a Web Worker. Else, you can read on.
Using the origin private file system on the main thread #
Creating new files and folders #
Once you have a root folder, create files and folders using the getFileHandle()
and the getDirectoryHandle()
methods respectively. By passing {create: true}
, the file or folder will be created if it doesn’t exist. Build up a hierarchy of files by calling these functions using a newly created directory as the starting point.
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});
Accessing existing files and folders #
If you know their name, access previously created files and folders by calling the getFileHandle()
or the getDirectoryHandle()
methods, passing in the name of the file or folder.
const existingFileHandle = await opfsRoot.getFileHandle('my first file');
const existingDirectoryHandle = await opfsRoot
.getDirectoryHandle('my first folder);
Getting the file associated with a file handle for reading #
A FileSystemFileHandle
represents a file on the file system. To obtain the associated File
, use the getFile()
method. A File
object is a specific kind of Blob
, and can be used in any context that a Blob
can. In particular, FileReader
, URL.createObjectURL()
, createImageBitmap()
, and XMLHttpRequest.send()
accept both Blobs
and Files
. If you will, obtaining a File
from a FileSystemFileHandle
“frees” the data, so you can access it and make it available to the user-visible file system.
const file = await fileHandle.getFile();
console.log(await file.text());
Writing to a file by streaming #
Stream data into a file by calling createWritable()
which creates a FileSystemWritableFileStream
to that you then write()
the contents. At the end, you need to close()
the stream.
const contents = 'Some text';
// Get a writable stream.
const writable = await fileHandle.createWritable();
// Write the contents of the file to the stream.
await writable.write(contents);
// Close the stream, which persists the contents.
await writable.close();
Deleting files and folders #
Delete files and folders by calling their file or directory handle’s particular remove()
method. To delete a folder including all subfolders, pass the {recursive: true}
option.
await fileHandle.remove();
await directoryHandle.remove({recursive: true});
As an alternative, if you know the name of the to-be-deleted file or folder in a directory, use the removeEntry()
method.
directoryHandle.removeEntry('my first nested file');
Moving and renaming files and folders #
Rename and move files and folders using the move()
method. Moving and renaming can happen together or in isolation.
// Rename a file.
await fileHandle.move('my first renamed file');
// Move a file to another directory.
await fileHandle.move(nestedDirectoryHandle);
// Move a file to another directory and rename it.
await fileHandle
.move(nestedDirectoryHandle, 'my first renamed and now nested file');
Resolving the path of a file or folder #
To learn where a given file or folder is located in relation to a reference directory, use the resolve()
method, passing it a FileSystemHandle
as the argument. To obtain the full path of a file or folder in the origin private file system, use the root directory as the reference directory obtained via navigator.storage.getDirectory()
.
const relativePath = await opfsRoot.resolve(nestedDirectoryHandle);
// `relativePath` is `['my first folder', 'my first nested folder']`.
Checking if two file or folder handles point to the same file or folder #
Sometimes you have two handles and don’t know if they point at the same file or folder. To check whether this is the case, use the isSameEntry()
method.
fileHandle.isSameEntry(nestedFileHandle);
// Returns `false`.
Listing the contents of a folder #
FileSystemDirectoryHandle
is an asynchronous iterator that you iterate over with