The problem#
In my previous post, I gave some tips for making a web app save and load its data as a file to give the user control over their data. But for many applications, it's useful to think of the user's data as multiple files, possibly organized into directories. OPFS lets a web app store local data with a filesystem-like API, but, due to security concerns, there's no direct access to the user's real filesystem, so there's no straightforward way for the user to view or manipulate that data.
The solution#
The common way to deal with this kind of issue is to stuff
all of the files into one file, reducing it to a solved
problem. We'll use the ZIP archive file format
as it's pretty universally supported, so the user can likely use such
files. In these examples, I use the zip.js library, so you'll
have to import zip-fs.min.js
(or equivalent) to use them.
In these functions, dir
is an OPFS directory: either the root
directory from navigator.storage.getDirectory()
or a subdirectory's
FileSystemDirectoryHandle
. They
input/output the ZIP files as Blob
s; use the helpers from
my previous post to actually connect to the user's
filesystem.
Downloading a directory as a ZIP is simple:
async function zipDirectory(dir) {
const zipFs = new zip.fs.FS();
await zipFs.root.addFileSystemHandle(dir);
return await zipFs.exportBlob();
}
Reading a ZIP file into OPFS is more complicated and must
be done inside a Web Worker (due to using
createSyncAccessHandle()
):
async function unzipToDirectory(zipfile, dir) {
const z = new zip.fs.FS();
await z.importBlob(zipfile);
async function extract(z, dir) {
if (z.directory) {
const childDir = z.name
? await dir.getDirectoryHandle(z.name,
{ create: true })
: dir;
for (const child of z.children) {
await extract(child, childDir);
}
} else {
await writeFile(
await dir.getFileHandle(z.name, { create: true }),
await (await z.getBlob()).arrayBuffer());
}
}
await extract(z.root, dir);
}
async function writeFile(file, contents) {
const handle = await file.createSyncAccessHandle();
handle.truncate(0);
if (contents.arrayBuffer) contents = await contents.arrayBuffer();
handle.write(contents);
handle.flush();
handle.close();
}