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.
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 extensions
and 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.
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.
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.