Reading and writing files and directories with the browser-nativefs library
December 09, 2020 • This post appeared first on web.dev.
Browsers have been able to deal with files and directories for a long time. The File API provides features for representing file objects in web applications, as well as programmatically selecting them and accessing their data. The moment you look closer, though, all that glitters is not gold.
The traditional way of dealing with files
If you know how it used to work the old way, you can jump down straight to the new way.
Opening files
As a developer, you can open and read files via the <input type="file">
element. In its simplest form, opening a file can look something like the code sample below. The input
object gives you a FileList
, which in the case below consists of just one File
. A File
is a specific kind of Blob
, and can be used in any context that a Blob can.
const openFile = async () => {
return new Promise((resolve) => {
const input = document.createElement("input");
input.type = "file";
input.addEventListener("change", () => {
resolve(input.files[0]);
});
input.click();
});
};
Opening directories
For opening folders (or directories), you can set the <input webkitdirectory>
attribute. Apart from that, everything else works the same as above. Despite its vendor-prefixed name, webkitdirectory
is not only usable in Chromium and WebKit browsers, but also in the legacy EdgeHTML-based Edge as well as in Firefox.
Saving (rather: downloading) files
For saving a file, traditionally, you are limited to downloading a file, which works thanks to the <a download>
attribute. Given a Blob, you can set the anchor’s href
attribute to a blob:
URL that you can get from the URL.createObjectURL()
method.
To prevent memory leaks, always revoke the URL after the download.
const saveFile = async (blob) => {
const a = document.createElement("a");
a.download = "my-file.txt";
a.href = URL.createObjectURL(blob);
a.addEventListener("click", (e) => {
setTimeout(() => URL.revokeObjectURL(a.href), 30 * 1000);
});
a.click();
};
The problem
A massive downside of the download approach is that there is no way to make a classic open→edit→save flow happen, that is, there is no way to overwrite the original file. Instead, you end up with a new copy of the original file in the operating system’s default Downloads folder whenever you “save”.
The File System Access API
The File System Access API makes both operations, opening and saving, a lot simpler. It also enables true saving, that is, you can not only choose where to save a file, but also overwrite an existing file.
For a more thorough introduction to the File System Access API, see the article The File System Access API: simplifying access to local files.
Opening files
With the File System Access API, opening a file is a matter of one call to the window.showOpenFilePicker()
method. This call returns a file handle, from which you can get the actual File
via the getFile()
method.
const openFile = async () => {
try {
// Always returns an array.
const [handle] = await window.showOpenFilePicker();
return handle.getFile();
} catch (err) {
console.error(err.name, err.message);
}
};
Opening directories
Open a directory by calling window.showDirectoryPicker()
that makes directories selectable in the file dialog box.
Saving files
Saving files is similarly straightforward. From a file handle, you create a writable stream via createWritable()
, then you write the Blob data by calling the stream’s write()
method, and finally you close the stream by calling its close()
method.
const saveFile = async (blob) => {
try {
const handle = await window.showSaveFilePicker({
types: [
{
accept: {
// Omitted
},
},
],
});
const writable = await handle.createWritable();
await writable.write(blob);
await writable.close();
return handle;
} catch (err) {
console.error(err.name, err.message);
}
};
Introducing browser-nativefs
As perfectly fine as the File System Access API is, it’s not yet widely available.
This is why I see the File System Access API as a progressive enhancement. As such, I want to use it when the browser supports it, and use the traditional approach if not; all while never punishing the user with unnecessary downloads of unsupported JavaScript code. The browser-nativefs library is my answer to this challenge.
Design philosophy
Since the File System Access API is still likely to change in the future, the browser-nativefs API is not modeled after it. That is, the library is not a polyfill, but rather a ponyfill. You can (statically or dynamically) exclusively import whatever functionality you need to keep your app as small as possible. The available methods are the aptly named fileOpen()
, directoryOpen()
, and fileSave()
. Internally, the library feature-detects if the File System Access API is supported, and then imports the corresponding code path.
Using the browser-nativefs library
The three methods are intuitive to use. You can specify your app’s accepted mimeTypes
or file extensions
, and set a multiple
flag to allow or disallow selection of multiple files or directories. For full details, see the browser-nativefs API documentation. The code sample below shows how you can open and save image files.
// The imported methods will use the File
// System Access API or a fallback implementation.
import {
fileOpen,
directoryOpen,
fileSave,
} from "https://unpkg.com/browser-nativefs";
(async () => {
// Open an image file.
const blob = await fileOpen({
mimeTypes: ["image/*"],
});
// Open multiple image files.
const blobs = await fileOpen({
mimeTypes: ["image/*"],
multiple: true,
});
// Open all files in a directory,
// recursively including subdirectories.
const blobsInDirectory = await directoryOpen({
recursive: true,
});
// Save a file.
await fileSave(blob, {
fileName: "Untitled.png",
});
})();
Demo
You can see the above code in action in a demo on Glitch. Its source code is likewise available there. Since for security reasons cross origin sub frames are not allowed to show a file picker, the demo cannot be embedded in this article.
The browser-nativefs library in the wild
In my free time, I contribute a tiny bit to an installable PWA called Excalidraw, a whiteboard tool that lets you easily sketch diagrams with a hand-drawn feel. It is fully responsive and works well on a range of devices from small mobile phones to computers with large screens. This means it needs to deal with files on all the various platforms whether or not they support the File System Access API. This makes it a great candidate for the browser-nativefs library.
I can, for example, start a drawing on my iPhone, save it (technically: download it, since Safari does not support the File System Access API) to my iPhone Downloads folder, open the file on my desktop (after transferring it from my phone), modify the file, and overwrite it with my changes, or even save it as a new file.
Real life code sample
Below, you can see an actual example of browser-nativefs as it is used in Excalidraw. This excerpt is taken from /src/data/json.ts
. Of special interest is how the saveAsJSON()
method passes either a file handle or null
to browser-nativefs’ fileSave()
method, which causes it to overwrite when a handle is given, or to save to a new file if not.
export const saveAsJSON = async (
elements: readonly ExcalidrawElement[],
appState: AppState,
fileHandle: any,
) => {
const serialized = serializeAsJSON(elements, appState);
const blob = new Blob([serialized], {
type: "application/json",
});
const name = `${appState.name}.excalidraw`;
(window as any).handle = await fileSave(
blob,
{
fileName: name,
description: "Excalidraw file",
extensions: ["excalidraw"],
},
fileHandle || null,
);
};
export const loadFromJSON = async () => {
const blob = await fileOpen({
description: "Excalidraw files",
extensions: ["json", "excalidraw"],
mimeTypes: ["application/json"],
});
return loadFromBlob(blob);
};
UI considerations
Whether in Excalidraw or your app, the UI should adapt to the browser’s support situation. If the File System Access API is supported (if ('showOpenFilePicker' in window) {}
) you can show a Save As button in addition to a Save button. The screenshots below show the difference between Excalidraw’s responsive main app toolbar on iPhone and on Chrome desktop. Note how on iPhone the Save As button is missing.
Conclusions
Working with native files technically works on all modern browsers. On browsers that support the File System Access API, you can make the experience better by allowing for true saving and overwriting (not just downloading) of files and by letting your users create new files wherever they want, all while remaining functional on browsers that do not support the File System Access API. The browser-nativefs makes your life easier by dealing with the subtleties of progressive enhancement and making your code as simple as possible.
Acknowledgements
This article was reviewed by Joe Medley and Kayce Basques. Thanks to the contributors to Excalidraw for their work on the project and for reviewing my Pull Requests.
by tomayac • Discuss on Twitter • Edit on GitHub