Intro

Over the years, the lines between web applications and desktop applications have blurred. With projects like Electron and Flutter allowing us to reuse code written for the web in some arguably more powerful desktop apps, JavaScript really has been primed to rule the world. In the past, we might have focused on a desktop application for various reasons:

  • Working with large amounts of data
  • Reading and writing to the file system
  • Access to more resources, such as CPU, Memory, and GPU

One of the key reasons one might have focused on writing a desktop application is because of the need to read and write to the local file system. If we wanted to do this with a web application, we might use FormData to allow a user to upload a file, parse it, edit it, and then provide an option for them to download the revised version. This is still a completely viable and useful option in some use cases.

There is, however, another more powerful and straightforward option using the File System Access API. The File System Access API provides some key features:

  • Access to file system directories
  • Filter by file types
  • Read and write to the file system

It’s also important to note that the File System Access API is supported in most browsers, but not in Firefox. This does however mean it will work in most mobile browsers. Like most other browser APIs, it is only available in a secure context, via HTTPS. It can also only be initiated via user interaction, so we can’t simply open a file immediately when loading a page.

Local Notes Application

Using the File System Access API, we can build some powerful applications that can work with local datasets. We might have an application that we use for taking notes in class, but we don’t want to expose our super secret notes to the internet, so we want to keep our secret notes on our local machines. We can never be too paranoid these days.

This application will give us the opportunity to use some of the most common features of the File System Access API.

  • Open a local file
  • Edit the file
  • Write the file back to the disk

What sets the File System Access API apart from other methods of adding and saving data with a web application is that the API provides a writable file stream. This means we can initialize the writable stream, save data to the stream, even incrementally, and then close the stream when done. We’ll even be prompted if we want to overwrite the previous file.

There are a number of note-taking applications available, but because we want full control over our notes, we can write our own. Our note-taking application has a very specific goal: save simple notes by subject that we can save to our device. Let’s take a look at how we might accomplish this. You can view the source for this demo application here.

Open Files

We can store our notes locally in a JSON file that might look like this:

{
  "notes": [
    {
        "name": "Intro to Web Development", 
        "content": "Was this a good idea?"
    }
  ]
}

The JSON file can be loaded into our note-taking application using showOpenFilePicker. The file picker takes some options as well to help users narrow down what they are looking for and even a description of the file type to add some context to the user interface.

const options = {
    types: [
        {
            description: "Notes File",
            accept: {
                "application/json": [".json"],
            },
        },
    ],
    excludeAcceptAllOption: true,
    multiple: false,
};

Using these options, you provide a description and allow or disallow multiple file selections. Maybe the most useful is to narrow the file extensions your application can handle. In this case: JSON files.

A FileSystemFileHandle is returned when the user closes the file picker. We’ll want to keep a reference to this FileHandle throughout the life of our application so we can save updates to our notes at a later time.

let fileHandle;

async function loadNotes() {
    [fileHandle] = await showOpenFilePicker(options);
    if (fileHandle.kind === "file") {
        const file = await fileHandle.getFile();
        const text = await file.text();
        const data = JSON.parse(text);
        const { notes } = data;
        // display the results
        for (let note of notes) {
            const elem = createNote(note);
            elem && list.appendChild(elem);
        }
    }
}

Notice that we need to check if the file picker was used to open a file or a directory. Once we verify we opened a file, we can load the file data. The File object can provide some meta information, such as size and last modified date if we need it.

Since we know we are working with JSON, we can load the contents of the file as a text string, and parse the results. If we were working with some other data type, like an image, we could load the data in an ArrayBuffer and handle it from there.

Save File

During the lifetime of our application, we can edit notes, add new notes, and maybe even delete those notes no one should ever see!

We can use the createWritable method of our file handle to write our notes back to the same file we opened.

async function saveNotes() {
    // create the updated data to write to disk
    const elements = [...document.querySelectorAll("details")];
    const notes = [];
    for (let elem of elements) {
        const summElem = elem.querySelector("summary");
        const contentElem = elem.querySelector("textarea");
        let note = {
            name: summElem.innerText,
            content: contentElem.value
        };
        notes.push(note);
    }
    const writable = await fileHandle.createWritable();
    const values = JSON.stringify({ notes: notes });
    await writable.write(values);
    await writable.close();
}

Notice that we write the data back to the disk as soon as we are done with it. We could keep it open if our workflow requires some intermediate updates, but in our case, we can open and close it pretty quickly. Congratulations, we now have an application that can open, edit, and save files to the local file system, directly from the browser!

Summary

The File System Access API allows you to provide your users with an experience that lets them work with data the way they want to. It’s another great feature in our developer utility belt to improve the use of persistent data in our web applications. We can use it for configuration files, parsing markdown, text editors, image editors, video and audio creation, and much more. It’s not just about providing a desktop experience on the web, but an improved creation experience for web applications.