Skip to main content




Browsers have been able to deal with files and directories for a long time. the File API
provides functions for representing file objects in web applications, as well as for selecting them programmatically and entering their data. Regardless, when you look closer, all that glitters is not gold.

The traditional way of handling files

Open files

As a developer, you can open and read files through the

element. In its simplest form, opening a file may resemble the following code example. the input the object gives you a FileList, which in the following case consists of only one
File. A File is a specific type 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 ( ) ;
} ) ;
} ;

Open directories

To open folders (or directories), you can configure the

attribute. Other than that, everything else works the same as above. Despite its name with a supplier prefix,
webkitdirectory It can be used not only in Chromium and WebKit browsers, but also in legacy EdgeHTML-based Edge and Firefox.

Save (rather: download) files

To store a file, traditionally, you are limited to downloading a file, which works thanks to the

attribute. Given a Blob, you can determine the anchor href attribute to a blob: URL you can get from
URL.createObjectURL ()

method.

Caution:
To avoid memory leaks, always revoke the URL after downloading.

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 to to download approach is that there is no way to make a classic open → edit → save flow happen, in other words, there is no way to Overwrite the original file. Instead, you end up with a new Copy from the original file in the default download folder of the operating system every time you "save".

The native filesystem API

The native file system API makes both open and save operations much easier. Furthermore, it enables real savingsIn other words, you can not only choose where to save a file, but also overwrite an existing file.

Open files

With the Native file system API, opening a file is a matter of a call to the window.showOpenFilePicker () method. This call returns a file handle, from which you can get the File through him getFile () method.

const openFile = async ( ) => {
try {
const [ handle ] = await window . showOpenFilePicker ( ) ;
return handle . getFile ( ) ;
} catch ( err ) {
console . error ( err . name , err . message ) ;
}
} ;

Open directories

Open a directory by calling
window.showDirectoryPicker () which makes directories selectable in the file dialog.

Save files

Saving files is equally simple. From a file handle, create a write stream using createWritable (), then write the Blob data by calling the flow write () method, and in conclusion closes the sequence by calling its close () method.

const saveFile = async ( blob ) => {
try {
const handle = await window . showSaveFilePicker ( {
types : [ {
accept : {
} ,
} ] ,
} ) ;
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 stupendously good as the native file system API, it is not yet widely available.

caniuse-5305250

Browser compatibility table for the native file system API. (Source)

This is why I see the native file system API as an incremental improvement. As such, I want to use it when the browser supports it, and use the traditional approach if not; all this without ever punishing the user with unnecessary downloads of unsupported JavaScript code. the browser-nativefs
Library is my answer to this challenge.

Design philosophy

Since the native filesystem API is likely to change in the future, the browser-nativefs API is not based on it. In other words, the library is not a polyfill, but rather a ponyfill. You can (statically or dynamically) exclusively import whatever functionality you need to keep your application as small as possible. The available methods are those appropriately named
fileOpen (),
directoryOpen ()and
fileSave (). Internally, the library function detects whether the native file system API is supported and then imports the respective code path.

Using the browser-nativefs library

All three methods are intuitive to use. You can specify acceptance of your application mimeTypes or file extensionsand establish a multiple Check to allow or disallow selection of multiple files or directories. For complete details, see the
browser-nativefs API documentation. The following code example shows how you can open and save image files.


import {
fileOpen ,
directoryOpen ,
fileSave ,
} from 'https://unpkg.com/browser-nativefs' ;

( async ( ) => {
const blob = await fileOpen ( {
mimeTypes : [ 'image / *' ] ,
} ) ;


const blobs = await fileOpen ( {
mimeTypes : [ 'image / *' ] ,
multiple : true ,
} ) ;


const blobsInDirectory = await directoryOpen ( {
recursive : true
} ) ;


await fileSave ( blob , {
fileName : 'Untitled.png' ,
} ) ;
} ) ( ) ;

Manifestation

You can see the above code in action in a manifestation in Glitch. Their source code It is also available there. Since, for security reasons, cross-origin subplots cannot display a file picker, the demo cannot be embedded in this post.

The browser-nativefs library in nature

In my spare time, I contribute a little 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 quite well on a range of devices, from small mobile phones to computers with large screens. This means that you must handle files on all the various platforms, whether or not they support the native file system API. This makes it a great candidate for the browser-nativefs library.

I can, as an example, start a drawing on my iPhone, save it (technically: download it, since Safari doesn't support the native file system API) to the Downloads folder on my iPhone, 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.

iphone-original-5647631

Launch an Excalidraw drawing on an iPhone where the native file system API is not supported, but where a file can be saved (downloaded) in the Downloads folder).

chrome-modify-4721146

Open and modify the Excalidraw drawing on the desktop where the native file system API is supported, so the file can be accessed using the API.

chrome-oversave-2788546

Overwriting the original file with modifications to the original Excalidraw drawing file. The browser shows a dialog asking me if it's okay.

chrome-save-as-8162641

Save the modifications in a new Excalidraw file. The original file remains intact.

Real life code example

Below you can see a real example of browser-nativefs as used in Excalidraw. This extract is taken from
/src/data/json.ts. Of special interest is how saveAsJSON () pass a file handle or null to browser-nativefs'
fileSave () , which causes it to be overwritten when a handle is assigned, or to be saved 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 ) ;
} ;

User interface considerations

Whether in Excalidraw or in your application, the user interface must be adapted to the browser support situation. If the native file system API (if ('showOpenFilePicker' in window) {}) you can show a Save as button at the same time of a Save button. The screenshots below show the difference between Excalidraw's responsive main app toolbar on the iPhone and the Chrome desktop. Note how on iPhone the Save as The button is missing.

save-1154063

Excalidraw app toolbar on iPhone with just one Save button.

save-save-as-7805564

Excalidraw application toolbar in Chrome with Save and a focused Save as button.

Conclusions

Working with native files technically works in all modern browsers. In browsers that support the Native File System API, you can improve the experience by allowing true saving and overwriting (not just downloading) of files and by allowing your users to create new files wherever they want, all while remaining functional in browsers that do. it does not support the native file system API. the browser-nativefs It makes your life easier by dealing with the subtleties of progressive enhancement and making your code as simple as possible.

Thanks

This post was reviewed by Joe medley and
Kayce Basques. Thanks to Excalidraw collaborators
for your work on the project and for reviewing my Pull Requests.
Hero image for
Ilya Pavlov on Unsplash.

R Marketing Digital