Skip to content

Milan-Kovacevic/use-fs-access

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

26 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

use-fs-access

πŸ—‚οΈ React hook library that builds on top of the File System Access API, offering a clean and simple way to interact with the user's local file system from within a React application.

This hook enables React developers to easily open directories, read and write files, create or delete files and directories, and build powerful file-based workflows directly in the browser β€” all without leaving the comfort of React's ecosystem.

Additional advanced features include lazy-loading directory structures, file watching with a polling mechanism, and batch file processing. The library also supports persisting access to previously opened directories via built-in IndexedDB storage, and offers customizable file and directory filtering (with default filters for node_modules, .git, and dist). These features make it ideal for a variety of use cases, including file managers, code editors, offline-first applications, and any other app that requires seamless local file access.

⚠️ Please note that the File System Access API is not supported in all browsers. It is currently supported in modern Chromium-based browsers (e.g., Google Chrome, Microsoft Edge) and a few others. Be sure to check the compatibility table for the most up-to-date information on supported browsers.


πŸ“¦ Installation

npm install use-fs-access
# or
yarn add use-fs-access

✨ Features

  • Open, expand, create, or delete directories
  • Create, read, write, and delete files
  • Watch files and directories (via polling)
  • Lazy-load directory contents
  • Save and access previously opened directories
  • Filter files and directories
  • Fully extensible filter and storage mechanism
  • Built-in TypeScript support

πŸš€ Quick Start

Here's a full demo component showcasing how to use use-fs-access to open directories, view and modify files, and interact with the file system.

βœ… Make sure the browser supports the File System Access API.

import { useState } from "react";
import useFileSystemAccess from "use-fs-access";
import {
  FileOrDirectoryInfo,
  isApiSupported,
  showDirectoryPicker,
} from "use-fs-access/core";

function FileSystemAccessDemo() {
  const {
    files,
    openDirectory,
    expandDirectory,
    openFile,
    closeFile,
    deleteFile,
    writeFile,
    createDirectory,
    renameFile,
    copyFile,
  } = useFileSystemAccess({
    filters: [
      // - gitIgnoreFilter, (apply .gitignore rules)
      // - gitFolderFilter, (excludes .git folder)
      // - distFilter       (excludes node_modules, dist, ...)
      // - defaultFilters,  (includes .git folder and .gitignore filters by default)
    ],
    enableFileWatcher: true,
    fileWatcherOptions: {
      debug: true,
      pollInterval: 250, // [ms]
      // batchSize: 50, [ms]
      // cacheTime: 5000, [ms]
    },
    // FILE WATCHER CALLBACKS
    onFilesAdded: (newFiles: Map<string, FileOrDirectoryInfo>) => {}, // - Track when new files are added
    onFilesDeleted: (deletedFiles: Map<string, FileOrDirectoryInfo>) => {}, // - Track when files are deleted
    onFilesModified: (modifiledFiles: Map<string, FileOrDirectoryInfo>) => {}, // - Track when files are modified
  });

  const welcomeMessage =
    "Hello from the File System Access API Demo!\nClick 'Open Directory' to select a folder and start exploring its contents\n\n" +
    "Click on a file to view it, or on a folder to expand its content.";
  const [fileContent, setFileContent] = useState(welcomeMessage);

  const fileTree: FileTreeNode = buildFileTree(files);

  return (
    <div
      style={{
        height: "100vh",
        overflow: "hidden",
        padding: "10px",
        boxSizing: "border-box",
      }}
    >
      <h1>File System Access API Demo</h1>
      <hr />
      {!isApiSupported ? (
        <p style={{ color: "red" }}>API not supported on this browser</p>
      ) : (
        <>
          <div>
            <button
              onClick={async () => {
                const dir = await showDirectoryPicker();
                await openDirectory(dir);
              }}
            >
              πŸ“‚ Open Directory
            </button>
          </div>
          <div
            style={{
              display: "flex",
              height: "90%",
              gap: "20px",
            }}
          >
            {fileContent != undefined && (
              <textarea
                readOnly
                style={{
                  border: "0",
                  flex: "1",
                  width: "100%",
                  height: "100%",
                  marginTop: "20px",
                }}
                value={fileContent}
              />
            )}
            <hr dir="vertical" />
            <div
              style={{
                overflowY: "hidden",
                display: "flex",
                flexDirection: "column",
                flex: "1",
              }}
            >
              <h2>File Tree:</h2>
              {files.size === 0 ? (
                <i>No directory opened yet.</i>
              ) : (
                <div
                  style={{
                    overflowY: "auto",
                    flex: "1",
                    paddingBottom: "20px",
                  }}
                >
                  <FileTreeContent
                    node={fileTree}
                    depth={0}
                    onDelete={async (node) => {
                      await deleteFile(node.path, node.kind == "directory");
                    }}
                    expandDirectory={async (path) => {
                      await expandDirectory(path);
                    }}
                    onCloseFile={async (node) => {
                      await closeFile(node.path);
                      setFileContent(welcomeMessage);
                    }}
                    onOpenFile={async (node) => {
                      const f = await openFile(node.path);
                      setFileContent(f.content);
                    }}
                    onCreate={async (path, isDir) => {
                      if (isDir) await createDirectory("New Folder", path);
                      else await writeFile(path + "/New File");
                    }}
                  />
                </div>
              )}
            </div>
          </div>
        </>
      )}
    </div>
  );
}

export default FileSystemAccessDemo;

type FileTreeNode = FileOrDirectoryInfo & {
  children: FileTreeNode[];
};

const FileTreeContent = ({
  node,
  depth,
  onOpenFile,
  onCloseFile,
  expandDirectory,
  onDelete,
  onCreate,
}: {
  node: FileTreeNode;
  depth: number;
  onOpenFile: (node) => Promise<void>;
  onCloseFile: (node) => Promise<void>;
  expandDirectory: (path: string) => Promise<void>;
  onDelete: (node) => Promise<void>;
  onCreate: (path, isDir) => Promise<void>;
}) => {
  const [expanded, setExpanded] = useState(false);
  const isDir = node.kind === "directory";
  const indent = { paddingLeft: `${depth * 10}px` };

  return (
    <div key={node.path} style={indent}>
      <strong
        style={{ cursor: "pointer" }}
        onClick={async () => {
          if (isDir) {
            setExpanded((prev) => !prev);
            if (!expanded) await expandDirectory(node.path);
          } else if (!isDir) {
            await onOpenFile(node);
          }
        }}
      >
        {isDir ? "πŸ“" : "πŸ“„"} {node.name} {!isDir && node.opened && "(opened)"}
        {isDir && !node.loaded && "(not-loaded)"}
      </strong>
      <span style={{ marginLeft: "5px" }}>
        <>
          (
          {!isDir && node.opened && (
            <button
              onClick={async () => {
                if (node.opened) {
                  await onCloseFile(node);
                }
              }}
            >
              close
            </button>
          )}
          {isDir && (
            <>
              <button
                style={{ marginLeft: "3px" }}
                onClick={async () => {
                  await onCreate(node.path, true);
                }}
              >
                +d
              </button>
              <button
                style={{ marginLeft: "3px" }}
                onClick={async () => {
                  await onCreate(node.path, false);
                }}
              >
                +f
              </button>
            </>
          )}
          <button
            style={{ marginLeft: "3px" }}
            onClick={async () => {
              if (
                confirm(
                  "Are you sure you want to delete?\nThis cannot be undone."
                )
              )
                await onDelete(node);
            }}
          >
            x
          </button>
          )
        </>
      </span>

      {expanded &&
        isDir &&
        node.children?.map((child) => (
          <FileTreeContent
            node={child}
            depth={depth + 1}
            onCloseFile={onCloseFile}
            onOpenFile={onOpenFile}
            onDelete={onDelete}
            expandDirectory={expandDirectory}
            onCreate={onCreate}
          />
        ))}
    </div>
  );
};

const buildFileTree = (
  map: Map<string, FileOrDirectoryInfo>
): FileTreeNode | null => {
  const pathToTreeNode = new Map<string, FileTreeNode>();

  for (const [path, info] of map.entries()) {
    pathToTreeNode.set(path, { ...info, children: [] });
  }

  let root: FileTreeNode | null = null;
  for (const [path, dirNode] of pathToTreeNode.entries()) {
    if (!path.includes("/")) {
      root = dirNode;
    } else {
      const parentPath = path.split("/").slice(0, -1).join("/");
      const parentNode = pathToTreeNode.get(parentPath);
      if (parentNode) {
        parentNode.children.push(dirNode);
      }
    }
  }
  return root;
};

About

A React hook that builds on top of the File System Access API to enable easy file and directory operations in modern browsers.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors