Manage files in the browser in JS

Modern browsers can do a lot more than we commonly know. Today, I’m going to introduce a topic that is familiar in server-side programming, but lesser-known in frontend development: accessing the file system and managing files and directories.

Yes, you’re not misread. File system management is possible in modern browsers by JavaScript, via the File System API. However, this API is very verbose and low-level, it would be much easier if we could manipulate files as we do in Node.js. In other words, we need a higher-level API to operate on the file system. And we’ll introduce it in the following examples.

Most browsers support the Origin Private File System, which is a virtual file system in the browser. But today I’m not going to talk about it. Instead, I choose to operate on the real file system of the physical device. Currently, only Chrome allows us to access the device’s file system. So we’ll be using Chrome to demonstrate its ability.

If you’re already in the desktop Chrome browser, you can press F12 to open the Chrome Devtools and switch to the Console tab, because the following examples can be directly run in the Chrome Console, you can just copy and paste the code to the console and see the actual effect immediately yourself. If you’re not in a desktop version of Chrome, you may just read the examples, they’re easy to understand as well.

Import the fs module

Since we’re in the Chrome console, we’ll use the import() function to import the fs module.

const fs = await import("https://ayonli.github.io/jsext/esm/fs.js")
TypeScript

If we’re writing a JavaScript module file, we should use the import statement instead.

import * as fs from "https://ayonli.github.io/jsext/esm/fs.js"
// or with a package manager
import * as fs from "@ayonli/jsext/fs"
TypeScript

Obtain the root directory

In the browser, the file system APIs by default use Origin Private File System, we don’t need to do anything to use it, just like in Node.js. However, as I’ve said earlier, in this article, we’re going to operate on the real file system of the computer, so we need to obtain a directory handle and pass it into the functions we’re going to use.

const root = await window.showDirectoryPicker()
TypeScript

This code will open a dialog allowing us to choose a directory to operate in. Here, I choose a folder in the ~/Public directory named demo, for demonstration. It will be used as the root directory for the rest examples of this article.

The browser will pop up a message box asking for permission to view files in the selected folder, just click View files to allow.

Read the entries of the root directory

Now we have a root directory handle, we can use the familiar file system APIs to access its contents. First, let’s see what’s inside the root directory now.

const entries = await Array.fromAsync(fs.readDir("/", { root }))
console.log(entries)
TypeScript

The above code will print all the files and folders in the root directory. Since the directory I chose is empty now, the code prints an empty array in the console.

Additionally, we can use the readTree function to retrieve a tree structure of the directory instead.

const tree = await fs.readTree("/", { root })
console.log(tree)
TypeScript

My console printed something like this:

Create a sub-directory

For demonstration, we’ll create a folder in the root directory named foo, and place other files inside it.

await fs.mkdir("/foo", { root })
TypeScript

The browser will pop up another message box asking for permission to save changes to the root directory, just click Save changes to allow.

Now if I run the readTree function again, we’ll see something like this:

Now let’s see the real effect through the file browser.

The new folder is written to the local file system as expected.

Create a file and read/write the content of it

Now let’s explore the ability to read and write files in the file system.

await fs.writeFile("/foo/hello.txt", "Hello, World!", { root })
TypeScript
const content = await fs.readFileAsText("/foo/hello.txt", { root })
console.log(content)
TypeScript

We will see the following output in the console:

And we can see the file in the file browser as well:

Final result

Now let’s run readTree again, we’ll see something like this, showing all the entries in the root directory in a tree structure.

console.log(await fs.readTree("/", { root }))
TypeScript

That’s it. Managing files in the browser is as easy as in Node.js (actually easier since this fs module provides more handy functions than Node.js).

Feel free to check the @ayonli/jsext/fs module for more functions to work with, or explore the @ayonli/jsext library for other features it provides.

Comments

Leave a comment