Skip to content

Commit ee8cf2d

Browse files
Migrating Chromium-only articles (GoogleChrome#4412)
* Migration of articles * Migration of articles * Migration of articles * Migration of articles * Migration of articles * Migration of articles * Migration of articles * Linter.......... * Linter.......... * Linter.......... * Fix copy + paste error * Update site/en/capabilities/index.njk Co-authored-by: Rachel Andrew <rachelandrew@google.com> Co-authored-by: Rachel Andrew <rachelandrew@google.com>
1 parent 4d50d33 commit ee8cf2d

File tree

17 files changed

+4790
-14
lines changed

17 files changed

+4790
-14
lines changed

site/de/articles/user-agent-client-hints/index.md

Lines changed: 348 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 355 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,355 @@
1+
---
2+
layout: 'layouts/blog-post.njk'
3+
title: Reading and writing files and directories with the browser-fs-access library
4+
authors:
5+
- thomassteiner
6+
description: All modern browsers can read local files and directories; however, true write access, that is, more than just downloading files, is limited to browsers that implement the File System Access API. This post introduces a support library called browser-fs-access that acts as an abstraction layer on top of the File System Access API and that transparently falls back to legacy approaches for dealing with files.
7+
date: 2020-07-27
8+
updated: 2022-01-25
9+
hero: image/admin/Y4wGmGP8P0Dc99c3eKkT.jpg
10+
alt: ''
11+
tags:
12+
- progressive-web-apps
13+
- capabilities
14+
feedback:
15+
- api
16+
---
17+
18+
Browsers have been able to deal with files and directories for a long time.
19+
The [File API](https://w3c.github.io/FileAPI/)
20+
provides features for representing file objects in web applications,
21+
as well as programmatically selecting them and accessing their data.
22+
The moment you look closer, though, all that glitters is not gold.
23+
24+
## The traditional way of dealing with files
25+
26+
{% Aside %}
27+
If you know how it used to work the old way, you can
28+
[jump down straight to the new way](#the-file-system-access-api).
29+
{% endAside %}
30+
31+
### Opening files
32+
33+
As a developer, you can open and read files via the
34+
[`<input type="file">`](https://developer.mozilla.org/docs/Web/HTML/Element/input/file)
35+
element.
36+
In its simplest form, opening a file can look something like the code sample below.
37+
The `input` object gives you a [`FileList`](https://developer.mozilla.org/docs/Web/API/FileList),
38+
which in the case below consists of just one
39+
[`File`](https://developer.mozilla.org/docs/Web/API/File).
40+
A `File` is a specific kind of [`Blob`](https://developer.mozilla.org/docs/Web/API/Blob),
41+
and can be used in any context that a Blob can.
42+
43+
```js
44+
const openFile = async () => {
45+
return new Promise((resolve) => {
46+
const input = document.createElement('input');
47+
input.type = 'file';
48+
input.addEventListener('change', () => {
49+
resolve(input.files[0]);
50+
});
51+
input.click();
52+
});
53+
};
54+
```
55+
56+
### Opening directories
57+
58+
For opening folders (or directories), you can set the
59+
[`<input webkitdirectory>`](https://developer.mozilla.org/docs/Web/HTML/Element/input#attr-webkitdirectory)
60+
attribute.
61+
Apart from that, everything else works the same as above.
62+
Despite its vendor-prefixed name,
63+
`webkitdirectory` is not only usable in Chromium and WebKit browsers, but also in the legacy EdgeHTML-based Edge as well as in Firefox.
64+
65+
### Saving (rather: downloading) files
66+
67+
For saving a file, traditionally, you are limited to *downloading* a file,
68+
which works thanks to the
69+
[`<a download>`](https://developer.mozilla.org/docs/Web/HTML/Element/a#attr-download:~:text=download)
70+
attribute.
71+
Given a Blob, you can set the anchor's `href` attribute to a `blob:` URL that you can get from the
72+
[`URL.createObjectURL()`](https://developer.mozilla.org/docs/Web/API/URL/createObjectURL)
73+
method.
74+
{% Aside 'caution' %}
75+
To prevent memory leaks, always revoke the URL after the download.
76+
{% endAside %}
77+
78+
```js
79+
const saveFile = async (blob) => {
80+
const a = document.createElement('a');
81+
a.download = 'my-file.txt';
82+
a.href = URL.createObjectURL(blob);
83+
a.addEventListener('click', (e) => {
84+
setTimeout(() => URL.revokeObjectURL(a.href), 30 * 1000);
85+
});
86+
a.click();
87+
};
88+
```
89+
90+
### The problem
91+
92+
A massive downside of the *download* approach is that there is no way to make a classic
93+
open→edit→save flow happen, that is, there is no way to *overwrite* the original file.
94+
Instead, you end up with a new *copy* of the original file
95+
in the operating system's default Downloads folder whenever you "save".
96+
97+
## The File System Access API
98+
99+
The File System Access API makes both operations, opening and saving, a lot simpler.
100+
It also enables *true saving*, that is, you can not only choose where to save a file,
101+
but also overwrite an existing file.
102+
103+
{% Aside %}
104+
For a more thorough introduction to the File System Access API, see the article
105+
[The File System Access API: simplifying access to local files](/file-system-access/).
106+
{% endAside %}
107+
108+
### Opening files
109+
110+
With the [File System Access API](https://wicg.github.io/file-system-access/),
111+
opening a file is a matter of one call to the `window.showOpenFilePicker()` method.
112+
This call returns a file handle, from which you can get the actual `File` via the `getFile()` method.
113+
114+
```js
115+
const openFile = async () => {
116+
try {
117+
// Always returns an array.
118+
const [handle] = await window.showOpenFilePicker();
119+
return handle.getFile();
120+
} catch (err) {
121+
console.error(err.name, err.message);
122+
}
123+
};
124+
```
125+
126+
### Opening directories
127+
128+
Open a directory by calling
129+
`window.showDirectoryPicker()` that makes directories selectable in the file dialog box.
130+
131+
### Saving files
132+
133+
Saving files is similarly straightforward.
134+
From a file handle, you create a writable stream via `createWritable()`,
135+
then you write the Blob data by calling the stream's `write()` method,
136+
and finally you close the stream by calling its `close()` method.
137+
138+
```js
139+
const saveFile = async (blob) => {
140+
try {
141+
const handle = await window.showSaveFilePicker({
142+
types: [{
143+
accept: {
144+
// Omitted
145+
},
146+
}],
147+
});
148+
const writable = await handle.createWritable();
149+
await writable.write(blob);
150+
await writable.close();
151+
return handle;
152+
} catch (err) {
153+
console.error(err.name, err.message);
154+
}
155+
};
156+
```
157+
158+
## Introducing browser-fs-access
159+
160+
As perfectly fine as the File System Access API is,
161+
it's [not yet widely available](https://caniuse.com/native-filesystem-api).
162+
163+
<figure>
164+
{% Img src="image/tcFciHGuF3MxnTr1y5ue01OGLBn2/G1jsSjCBR871W1uKQWeN.png", alt="Browser support table for the File System Access API. All browsers are marked as 'no support' or 'behind a flag'.", width="800", height="224" %}
165+
<figcaption>
166+
Browser support table for the File System Access API.
167+
(<a href="https://caniuse.com/native-filesystem-api">Source</a>)
168+
</figcaption>
169+
</figure>
170+
171+
This is why I see the File System Access API as a [progressive enhancement](https://web.dev/progressively-enhance-your-pwa).
172+
As such, I want to use it when the browser supports it,
173+
and use the traditional approach if not;
174+
all while never punishing the user with unnecessary downloads of unsupported JavaScript code.
175+
The [browser-fs-access](https://github.com/GoogleChromeLabs/browser-fs-access)
176+
library is my answer to this challenge.
177+
178+
### Design philosophy
179+
180+
Since the File System Access API is still likely to change in the future,
181+
the browser-fs-access API is not modeled after it.
182+
That is, the library is not a [polyfill](https://developer.mozilla.org/docs/Glossary/Polyfill),
183+
but rather a [ponyfill](https://github.com/sindresorhus/ponyfill).
184+
You can (statically or dynamically) exclusively import whatever functionality you need to keep your app as small as possible.
185+
The available methods are the aptly named
186+
[`fileOpen()`](https://github.com/GoogleChromeLabs/browser-fs-access#opening-files),
187+
[`directoryOpen()`](https://github.com/GoogleChromeLabs/browser-fs-access#opening-directories), and
188+
[`fileSave()`](https://github.com/GoogleChromeLabs/browser-fs-access#saving-files).
189+
Internally, the library feature-detects if the File System Access API is supported,
190+
and then imports the corresponding code path.
191+
192+
### Using the browser-fs-access library
193+
194+
The three methods are intuitive to use.
195+
You can specify your app's accepted `mimeTypes` or file `extensions`, and set a `multiple` flag
196+
to allow or disallow selection of multiple files or directories.
197+
For full details, see the
198+
[browser-fs-access API documentation](https://github.com/GoogleChromeLabs/browser-fs-access#api-documentation).
199+
The code sample below shows how you can open and save image files.
200+
201+
```js
202+
// The imported methods will use the File
203+
// System Access API or a fallback implementation.
204+
import {
205+
fileOpen,
206+
directoryOpen,
207+
fileSave,
208+
} from 'https://unpkg.com/browser-fs-access';
209+
210+
(async () => {
211+
// Open an image file.
212+
const blob = await fileOpen({
213+
mimeTypes: ['image/*'],
214+
});
215+
216+
// Open multiple image files.
217+
const blobs = await fileOpen({
218+
mimeTypes: ['image/*'],
219+
multiple: true,
220+
});
221+
222+
// Open all files in a directory,
223+
// recursively including subdirectories.
224+
const blobsInDirectory = await directoryOpen({
225+
recursive: true
226+
});
227+
228+
// Save a file.
229+
await fileSave(blob, {
230+
fileName: 'Untitled.png',
231+
});
232+
})();
233+
```
234+
235+
### Demo
236+
237+
You can see the above code in action in a [demo](https://browser-fs-access.glitch.me/) on Glitch.
238+
Its [source code](https://glitch.com/edit/#!/browser-fs-access) is likewise available there.
239+
Since for security reasons cross origin sub frames are not allowed to show a file picker,
240+
the demo cannot be embedded in this article.
241+
242+
## The browser-fs-access library in the wild
243+
244+
In my free time, I contribute a tiny bit to an
245+
[installable PWA](https://web.dev/progressive-web-apps/#make-it-installable)
246+
called [Excalidraw](https://excalidraw.com/),
247+
a whiteboard tool that lets you easily sketch diagrams with a hand-drawn feel.
248+
It is fully responsive and works well on a range of devices from small mobile phones to computers with large screens.
249+
This means it needs to deal with files on all the various platforms
250+
whether or not they support the File System Access API.
251+
This makes it a great candidate for the browser-fs-access library.
252+
253+
I can, for example, start a drawing on my iPhone,
254+
save it (technically: download it, since Safari does not support the File System Access API)
255+
to my iPhone Downloads folder, open the file on my desktop (after transferring it from my phone),
256+
modify the file, and overwrite it with my changes, or even save it as a new file.
257+
258+
<figure>
259+
{% Img src="image/admin/u1Gwxp5MxS39wl8PW2vz.png", alt="An Excalidraw drawing on an iPhone.", width="300", height="649" %}
260+
<figcaption>Starting an Excalidraw drawing on an iPhone where the File System Access API is not supported, but where a file can be saved (downloaded) to the Downloads folder.</figcaption>
261+
</figure>
262+
263+
<figure>
264+
{% Img src="image/admin/W1lt36DtKuveBJJTzonC.png", alt="The modified Excalidraw drawing on Chrome on the desktop.", width="800", height="592" %}
265+
<figcaption>Opening and modifying the Excalidraw drawing on the desktop where the File System Access API is supported and thus the file can be accessed via the API.</figcaption>
266+
</figure>
267+
268+
<figure>
269+
{% Img src="image/admin/srqhiMKy2i9UygEP4t8e.png", alt="Overwriting the original file with the modifications.", width="800", height="585" %}
270+
<figcaption>Overwriting the original file with the modifications to the original Excalidraw drawing file. The browser shows a dialog asking me whether this is fine.</figcaption>
271+
</figure>
272+
273+
<figure>
274+
{% Img src="image/admin/FLzOZ4eXZ1lbdQaA4MQi.png", alt="Saving the modifications to a new Excalidraw drawing file.", width="800", height="592" %}
275+
<figcaption>Saving the modifications to a new Excalidraw file. The original file remains untouched.</figcaption>
276+
</figure>
277+
278+
### Real life code sample
279+
280+
Below, you can see an actual example of browser-fs-access as it is used in Excalidraw.
281+
This excerpt is taken from
282+
[`/src/data/json.ts`](https://github.com/excalidraw/excalidraw/blob/cd87bd6901b47430a692a06a8928b0f732d77097/src/data/json.ts#L24-L52).
283+
Of special interest is how the `saveAsJSON()` method passes either a file handle or `null` to browser-fs-access'
284+
`fileSave()` method, which causes it to overwrite when a handle is given,
285+
or to save to a new file if not.
286+
287+
```js
288+
export const saveAsJSON = async (
289+
elements: readonly ExcalidrawElement[],
290+
appState: AppState,
291+
fileHandle: any,
292+
) => {
293+
const serialized = serializeAsJSON(elements, appState);
294+
const blob = new Blob([serialized], {
295+
type: "application/json",
296+
});
297+
const name = `${appState.name}.excalidraw`;
298+
(window as any).handle = await fileSave(
299+
blob,
300+
{
301+
fileName: name,
302+
description: "Excalidraw file",
303+
extensions: ["excalidraw"],
304+
},
305+
fileHandle || null,
306+
);
307+
};
308+
309+
export const loadFromJSON = async () => {
310+
const blob = await fileOpen({
311+
description: "Excalidraw files",
312+
extensions: ["json", "excalidraw"],
313+
mimeTypes: ["application/json"],
314+
});
315+
return loadFromBlob(blob);
316+
};
317+
```
318+
319+
### UI considerations
320+
321+
Whether in Excalidraw or your app,
322+
the UI should adapt to the browser's support situation.
323+
If the File System Access API is supported (`if ('showOpenFilePicker' in window) {}`)
324+
you can show a **Save As** button in addition to a **Save** button.
325+
The screenshots below show the difference between Excalidraw's responsive main app toolbar on iPhone and on Chrome desktop.
326+
Note how on iPhone the **Save As** button is missing.
327+
328+
<figure>
329+
{% Img src="image/admin/c2sjjj86zh53VDrPIo6M.png", alt="Excalidraw app toolbar on iPhone with just a 'Save' button.", width="300", height="226" %}
330+
<figcaption>Excalidraw app toolbar on iPhone with just a <strong>Save</strong> button.</figcaption>
331+
</figure>
332+
333+
<figure>
334+
{% Img src="image/admin/unUUghwH5mG2hLnaViHK.png", alt="Excalidraw app toolbar on Chrome desktop with a 'Save' and a 'Save As' button.", width="300", height="66" %}
335+
<figcaption>Excalidraw app toolbar on Chrome with a <strong>Save</strong> and a focused <strong>Save As</strong> button.</figcaption>
336+
</figure>
337+
338+
## Conclusions
339+
340+
Working with system files technically works on all modern browsers.
341+
On browsers that support the File System Access API, you can make the experience better by allowing
342+
for true saving and overwriting (not just downloading) of files and
343+
by letting your users create new files wherever they want,
344+
all while remaining functional on browsers that do not support the File System Access API.
345+
The [browser-fs-access](https://github.com/GoogleChromeLabs/browser-fs-access) makes your life easier
346+
by dealing with the subtleties of progressive enhancement and making your code as simple as possible.
347+
348+
## Acknowledgements
349+
350+
This article was reviewed by [Joe Medley](https://github.com/jpmedley) and
351+
[Kayce Basques](https://github.com/kaycebasques).
352+
Thanks to the [contributors to Excalidraw](https://github.com/excalidraw/excalidraw/graphs/contributors)
353+
for their work on the project and for reviewing my Pull Requests.
354+
[Hero image](https://unsplash.com/photos/hXrPSgGFpqQ) by
355+
[Ilya Pavlov](https://unsplash.com/@ilyapavlov) on Unsplash.

0 commit comments

Comments
 (0)