A Weird Imagination

Debugging OPFS

The problem#

While the web developer tools in Firefox and Chrome provide a Storage/Application tab for inspecting the local data stored by a web app, neither shows OPFS files there, making it difficult to tell what's going wrong when you have a bug (which was a problem when writting my recent blog posts about OPFS). There's open Firefox and Chromium bugs about the missing feature, so if it's been a while since this was posted when you're reading this, hopefully this is no longer a problem.

Additionally, the tools I did find all use recursion, resulting in them failing to work on the deeply nested directory tree I created by accident.

The solution#

If you don't have several hundred levels deep of nested directories, you can just use this Chrome extension or this script (or probably this web component, although I couldn't get it to install), all named "opfs-explorer".

The following AsyncIterator returns all of the files in OPFS without using recursion and adds properties to include their full path and parent directory:

async function* getFilesNonRecursively(dir) {
  const stack = [[dir, "", undefined, 0]];
  while (stack.length) {
    const [current, prefix, parentDir] = stack.pop();
    current.relativePath = prefix + current.name;
    current.parentDir = parentDir;
    current.depth = depth;
    yield current;

    if (current.kind === "directory") {
      for await (const handle of current.values()) {
        stack.push([handle,
                    prefix + current.name + "/",
                    current,
                    depth + 1]);
      }
    }
  }
}

And here's the simple HTML display function I've been using that calls that (you will likely want to modify this to your preferences):

async function displayOPFSFileList() {
  const existing = document.getElementById("opfs-file-list");
  const l = document.createElement('ol');
  l.id = "opfs-file-list";
  if (existing) existing.replaceWith(l);
  else document.body.appendChild(l);

  const root = await navigator.storage.getDirectory();
  for await (const fileHandle
             of getFilesNonRecursively(root)) {
    const i = document.createElement("li");
    i.innerText = fileHandle.kind + ": "
                  + (fileHandle.relativePath ?? "(root)");
    if (fileHandle.kind === "file") {
      const content = await fileHandle.getFile();
      const contentStr = content.type.length === 0
                      || content.type.startsWith("text/")
        ? ("\"" + (await content.slice(0, 100).text()).trim()
          + "\"")
        : content.type;
      i.innerText += ": (" + content.size + " bytes) "
                     + contentStr;
    }
    l.appendChild(i);
  }
}

The details#

Creating deeply nested directories#

I actually ran into the problem due to a bug in my unzipping code that created a directory in OPFS, descended into it, opened the subdirectory in the ZIP file… and then accidentally used the parent directory instead of that subdirectory, resulting in an infinite loop that produced a directory structure nested 10 000 levels deep before crashing. If we want to replicate that result for testing, intentionally creating a deeply nested directory is straightforward:

async function buildNested(dir, name, depth) {
  for (let i = 0; i < depth; i++) {
    dir = await dir.getDirectoryHandle(name, {create: true});
  }
}
// Create enough levels of nesting to cause problems.
await buildNested(root, "z", 1000);

Detecting stack overflow#

Stack overflow errors tend to be annoying to debug since they often give weird, uninformative errors and debugging tools don't tend to handle them well. For instance, if we try to delete that 1000-entry-deep directory created above, Firefox just tells us:

》 await root.removeEntry('z', {recursive: true})
Uncaught (in promise) DOMException: Unknown failure

At least it fails quickly. But that also makes it's harder to tell what's going on because one common way to notice recursion getting out of hand is to use the pause feature of the debugger on a program taking too long to run and seeing a surprisingly large and repetitive call stack.

Luckily, for a user-written recursive function, Firefox will show the message "InternalError: too much recursion [Learn More]" with that informative help page link, although it may take a while for the function to run long enough to generate that error.

Avoiding stack overflow#

Avoiding the stack overflow here is straightforward: we're doing a simple depth-first search, which has well-known recursive and iterative implementations: we just use the iterative one instead. For comparison, here's the recursive version of the iterator using yield* to recurse:

async function* getFilesRecursive(entry, prefix,
                                  parentDir, depth) {
  prefix = prefix ?? "";
  depth = depth ?? 0;
  entry.relativePath = prefix + entry.name;
  entry.parentDir = parentDir;
  entry.depth = depth;
  yield entry;
  if (entry.kind === "directory") {
    for await (const handle of entry.values()) {
      yield* getFilesRecursive(handle,
                               prefix + entry.name + "/",
                               entry,
                               depth + 1);
    }
  }
}

OS directory nesting limits#

Having real files nested this deep would also cause problems. Windows, by default, doesn't allow paths longer than 260 characters, while Linux and Mac OS X generally won't stop you from creating nested directories, apparently OS X complains if you try to work with a directory nested 256 levels deep and neither is generally happy with paths over 4096 characters, although exactly what that limit means is complicated.

But on any OS, OPFS somehow lets us pretend to have a filesystem that doesn't have those restrictions, which is further evidence that it's not actually giving us real filesystem access.

Where are OPFS files?#

To see how OPFS file are actually stored, you can look under your browser's user data directory (instructions: Firefox or Chromium). For Firefox, the data appears to be in storage/default/SITE/fs/ where SITE looks like https+++example.com. For Chromium, the data appears to be in File System/, but the directory names are just numbers, so it's not straightforward to figure out which directory corresponds to which site. For both, large files appear to actually get stored as normal files on the filesystem, while the directory structure information and smaller files appear to get stored inside a database. For Firefox, the database is an SQLite database named metadata.sqlite, although reading it with sqlite is unlikely to be very useful; I don't know how to read the Chromium database at all.

Other than curiosity, finding these files on disk is probably mostly useful to delete them to reset OPFS if while experimenting you manage to get it into a state where doing so through the browser isn't working. Needless to say, be very careful deleting files here if you're on a browser profile that you use for anything important, and generally try to go through the browser if possible.

Comments

Have something to add? Post a comment by sending an email to comments@aweirdimagination.net. You may use Markdown for formatting.

There are no comments yet.