The problem#
Last time, I showed how you can let a user have control over their data stored in a web app's OPFS by transferring directories in or out of the browser as ZIP files. But it would be more convenient if the user could just transfer folders instead without needing the extra step of going through an archive manager. There's no shortcut for getting data out of the browser: we can only save one file at a time (unless we use the Chrome-only File System Access API). But there is a cross-browser way to load multiple files, or even nested directories, into the browser.
The solution#
The HTML Drag and Drop API supports transferring multiple files and directories, although the details are a bit messy:
const target = document.getElementById("dropTarget");
// Required to make drop work.
target.addEventListener("dragover", (e) => e.preventDefault());
target.addEventListener("drop", async (e) => {
e.preventDefault();
await Promise.allSettled([...e.dataTransfer.items]
.map(async (item) => {
if (item.getAsFileSystemHandle) {
await processFileSystemHandle(dir,
await item.getAsFileSystemHandle());
} else {
await processFileSystemEntry(dir,
item.webkitGetAsEntry());
}
})
);
});
As you can see, for cross-browser support, we need to handle both
getAsFileSystemHandle()
and
webkitGetAsEntry()
. And, unfortunately, they
return different types, so those two process*()
functions really are
pretty different:
async function processFileSystemHandle(dir, handle) {
if (handle.kind === "directory") {
const subdir = await dir.getDirectoryHandle(handle.name,
{create: true});
for await (const entry of handle.values()) {
await processFileSystemHandle(subdir, entry);
}
} else /* handle.kind === "file" */ {
await writeFile(await dir.getFileHandle(handle.name,
{create: true}),
await handle.getFile());
}
}
async function processFileSystemEntry(dir, entry) {
async function readDirectory(directory) {
let dirReader = directory.createReader();
let getEntries = async () => {
const results = await (new Promise((resolve, reject) =>
dirReader.readEntries(resolve, reject)));
if (results.length) {
return [...results, ...await getEntries()];
}
return [];
};
return await getEntries();
}
if (entry.isDirectory) {
const subdir = await dir.getDirectoryHandle(entry.name,
{create: true});
for (const el of await readDirectory(entry)) {
await processFileSystemEntry(subdir, el);
}
} else /* entry.isFile */ {
const file = new Promise((resolve, reject) =>
entry.file(resolve, reject));
await writeFile(await dir.getFileHandle(entry.name,
{create: true}),
await file);
}
}
(These assume the writeFile()
helper from last week's post
to handle writing inside a Web Worker as necessary.)