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;
};