Sean M Clements

File System Access API - Utlity Functions

Intro

The File System Access API allows client-side Javascript to access a user’s local filesystem. You can “open” a file and save edits to it, or open a folder and add/remove/edit files inside it. All without uploading or downloading the files/folders. Like you never could before on the web. Now a website can behave a bit more like an Electron app, or a native application, without requiring a download, installation, or server side code required.

This post will go over some utility functions that I have recently written to make working with this API a bit easier.

Open Folder

Before we start with utility functions, we need to actually open a directory. We can use the File System Access API to open a directory.

const directoryHandle = await window.showDirectoryPicker();

If you log this FileSystemDirectoryHandle, you’ll see it only has kind:"directory" and name: "The_Folders_Name", but it has many other functions available on it to get and manipulate the contents.

When working with a directoryHandle like this, I would normally want a list of the folders, and files inside of it. Or maybe I would want to access a file or folder that is deeply nested within other folders inside. This process can be a bit tedious so I’ve written up a few functions to take easier advantage of the API.

First we will get a list of the directory contents by passing our directoryHandle to getDirectoryContents:

const folder_contents = await getDirectoryContents({ directoryHandle });

With this function:

export const getDirectoryContents = async ({
  directoryHandle,
  sort = true,
}: {
  directoryHandle: FileSystemDirectoryHandle;
  sort?: boolean;
}): Promise<EntryType[]> => {
  const handlesEntriesIterator = directoryHandle.entries();
  return asyncIteratorToArray(handlesEntriesIterator, sort);
};

// With supporting functions and types
export type EntryType = [string, FileSystemHandle];

const asyncIteratorToArray = async (iterator: any, sort?: boolean) => {
  const array = [];
  for await (const handle of iterator) {
    array.push(handle);
  }

  sort && array.sort(comparator);
  return array;
};

function comparator(a: string, b: string) {
  if (a[0] < b[0]) return -1;
  if (a[0] > b[0]) return 1;
  return 0;
}

directoryHandle.entries() returns an AsyncIterableIterator. So it’s an array of File System Handles, each one you would have to await to get the file or folder. So by using the asyncIteratorToArray function -we end up with an array of arrays of already awaited and usable File System Handles, and their names. These come in the form of an array of EntryType, (Array<[string, FileSystemHandle]>). The comparator function allows us to handle the optional alphanumeric sort. Since getDirectoryContents can receive a boolean for sort, to sort the contents of the directory into the array results.

Example results for getDirectoryContents

results = [
    [".gitignore", FileSystemFileHandle]
    ["README.md", FileSystemFileHandle],
    ["src", FileSystemDirectoryHandle]
];

Getting a deeply nested folder

This function is called traverse_folder_paths. It takes a folder’s contents, and an array of nested folder names in order of their path, and returns the EntryType [name, FileSystemHandle] at the final path.

So say I wanted to access src/components/BlogCard/Heading, I would call my function like this

const folder_handle = await traverse_folder_paths({
  folder_contents,
  paths: ["src", "components", "BlogCard", "Heading"],
});

Here is how the function is defined. It relies on a recursive function called loopPaths, which also utilizes getDirectoryContents.

export const traverse_folder_paths = async ({
  folder_contents,
  paths,
}: {
  folder_contents: EntryType[];
  paths: string[];
}) => {
  const result = await loop_paths({
    folder_contents,
    paths,
    current_path: paths[0],
  });

  return result;
};

const loop_paths = async ({
  folder_contents,
  paths,
  current_path,
}: {
  folder_contents: EntryType[];
  paths: string[];
  current_path: string;
}): Promise<EntryType | undefined> => {
  if (paths.length === 1) {
    return folder_contents.find((entry) => entry[0] === current_path);
  }

  const next_folder = folder_contents.find(
    (entry) => entry[0] === current_path
  );
  if (!next_folder) {
    console.error("loop_paths: Failed to find path", {
      folder_contents,
      paths,
      current_path,
    });
    return undefined;
  }

  const next_folder_handle = next_folder[1];

  if (next_folder_handle.kind !== "directory") {
    console.error("loop_paths: Entry handle is not a directory", {
      folder_contents,
      paths,
      current_path,
    });
  }

  const next_folder_handle_contents = await getDirectoryContents({
    directoryHandle: next_folder_handle as FileSystemDirectoryHandle,
  });

  const next_paths = paths.splice(1);
  const next_current_path = next_paths[0];

  return await loop_paths({
    folder_contents: next_folder_handle_contents,
    paths: next_paths,
    current_path: next_current_path,
  });
};

I was really happy with that one. Here are a few more just for fun and utility.

Additional Functions

export const writeFile = async ({
  fileHandle,
  contents,
}: {
  fileHandle: FileSystemFileHandle;
  contents: FileSystemWriteChunkType;
}) => {
  const writer = await fileHandle.createWritable();
  await writer.truncate(0); // Make sure we start with an empty file
  await writer.write(contents);
  await writer.close();
};

export const update_File_In_Directory = async ({
  directoryHandle,
  filename,
  contents,
}: {
  directoryHandle: FileSystemDirectoryHandle;
  filename: string;
  contents: string;
}) => {
  const fileHandle = await directoryHandle.getFileHandle(filename);
  await writeFile({ fileHandle, contents });
  return fileHandle;
};

export const createEmptyFileInDirectory = async ({
  directoryHandle,
  filename,
}: {
  directoryHandle: FileSystemDirectoryHandle;
  filename: string;
}) => {
  const fileHandle = await directoryHandle.getFileHandle(filename, {
    create: true,
  });
  await writeFile({ fileHandle, contents: " " });
  return fileHandle;
};

export const createDirectory = async ({
  parentDirectoryHandle,
  newDirectoryName,
}: {
  parentDirectoryHandle: FileSystemDirectoryHandle;
  newDirectoryName: string;
}) =>
  await parentDirectoryHandle.getDirectoryHandle(newDirectoryName, {
    create: true,
  });

export const openTextFile = async () => {
  // https://wicg.github.io/file-system-access/#api-filpickeroptions-types
  const options: OpenFilePickerOptions = {
    types: [
      {
        description: "Text Files",
        accept: {
          "text/plain": [".txt", ".text"],
          "text/html": [".html", ".htm"],
        },
      },
    ],
    multiple: false,
  };
  return window.showOpenFilePicker(options);
};

export const getTextFileContents = async (fileHandle: FileSystemFileHandle) => {
  const file: File = await fileHandle.getFile();
  const fileText: string = await file.text();
  return fileText;
};