Native File System for the Web

Native file system access for web apps - this sounds a little clickbaity, doesn’t it? Many have dreamed about it, some during development, others under the shower (myself included). But it’s true, web applications can now securely (that is, with full consent of the user & for https-only origins) not only download and read files in a stateless manner. What I mean by that is that through the File System Access API, developers can read and update the same file on disk, once access is granted. Only caveat for now: it’s available in Chrome 86+. The draft is stable and available for all teams (Gecko, WebKit) to implement.

The file system has never been more thrilling

Here’s the tl;dr:

  • File System Access API is built around handlers: you get a handler to a file or directory by prompting the user to gain access via a standard file-picker dialog
  • Once the user finishes the selection (file, files or directories), the API-call returns the handler, which you store in a variable
  • Throughout the user’s active session (e.g. your web app’s tab doesn’t get closed), you can use this handler to read and update files or the directory - sounds legit, as the handler is now available as a variable.
  • Best of all: the handlers are serializable, which means that storing them in IndexDB is a thing, which means you can persist access to the selection & even share the handles with your service worker and web workers
  • File System Access API also gives you a private space for your origin, where no user interaction is necessary - but in turn, the files and directories created there are completely isolated from the rest of the system

A closer look (with consent)

Ok, let's visit some code examples. All of the following outlines are also available at the original announcement from the dev team at web.dev.

First example is the most basic one: gaining access to and reading from a file:

const onClick = async () => {
  // 'showOpenFilePicker' is one of the new API calls
  // from File System Access API. Call it and await
  // the result.
  const res = await window.showOpenFilePicker();
  
  // We can destruct the result, which is an array,
  // to simplfy access to its first element - the handle.
  const [fileHandle] = res;
  
  // Simple access to the file's content. Nice!
  const file = await fileHandle.getFile();
  const contents = await file.text();
}

// ... some code later ...

// Only real user gestures enable you usage of the
// new API for stored data on the disk.
const button = document.getElementById('btn')
button.addEventListener('click', onClick);

Here's how to do the same with a directory and even more:

const onCLick = async () => {
  // 'showDirectoryPicker' gives us access to a
  // directory the user selected.
  const dirHandle = await window.showDirectoryPicker();
  for await (const entry of dirHandle.values()) {
    console.log(entry.kind, entry.name);
  }
  
  // Oh yeah, creating new directories with the given handle
  // is possible - we're talking about true file system
  // access here!
  const newDirHandle = await dirHandle.getDirectoryHandle('Folder by my PWA', { create: true });

  // Same for files in the directory. Of course.
  const newFileHandle = await newDirectoryHandle.getFileHandle('PWA_with_fs_access.txt', { create: true });
  
  // Deleting is done via 'removeEntry'. Here, we're deleting
  // our newly created empty txt-file.
  await newDirHandle.removeEntry('Abandoned Masterplan.txt');
  
  // And for demo purposees, let's also delete the new
  // directory itself and any of its contents (which at
  // this point don't exist as the only file got just deleted).
  await dirHandle.removeEntry('Folder by my PWA', { recursive: true });
}


const button = document.getElementById('btn')
button.addEventListener('click', onClick);

Let's see how to save changes. This done via streams, and streams only, which means that you have to close the stream to successfully complete a write operation.

const onClick = async () => {
  // You already know that code from the first example.
  const res = await window.showOpenFilePicker();
  const [fileHandle] = res;

 // Use FileSystemWritableFileStream to write to the file.
  const writable = await fileHandle.createWritable();
  // Write the contents of the file to the stream.
  await writable.write("PWAs are the future, and the future is now");
  // Close the file and write the contents to disk.
  await writable.close();
}

// ... attach function to onclick-event ...

Finally, let's take a look at origin-private file system access:

async function fn(){
  const root = await navigator.storage.getDirectory();
  
   // Here's the same code from the second example
   // to illustrate that there's no difference in 
   // calling the APIs in an origin-private context.
  const dirHandle = await root.getDirectoryHandle('Folder by my PWA', { create: true });
  const newFileHandle = await dirHandle.getFileHandle('PWA_with_fs_access.txt', { create: true });
  await dirHandle.removeEntry('Abandoned Masterplan.txt');
  await root.removeEntry('Folder by my PWA', { recursive: true });
}

That's it for a first glance at the new File System Access API. As you've seen, web development on the frontend has just improved by an order of magnitude. Assuming the correct browser is used, your PWA can be supercharged to provide a native usability unheard of until now. With every new stable release of Chrome, there are less and less reasons left to force the development of native apps - we're coming close to a future where this distinction won't be necessary any more.

I've linked the blog post on web.dev below for a detailed description around the security model that got implemented. There's also a polyfill (albeit not on feature-parity with the real API) available, which is linked below, too.

- Tom