A primitive that allows to manage different file system access methods:
-
createFileSystem
- Provides a reactive interface to one of the file system adapters - a convenience method that calls the correct wrapper -
createSyncFileSystem
- Wraps a synchronous file system adapter likemakeNoFileSystem
ormakeVirtualFileSystem
in a reactive interface -
createAsyncFileSystem
- Adds a reactive layer to an asynchronous file system adapter -
makeNoFileSystem
- Adapter that provides a synchronous mock file system -
makeNoAsyncFileSystem
- Adapter that provides an asynchronous mock file system -
makeVirtualFileSystem
- Adapter that provides a virtual file system that doubles asFsMap
fortypescript-vfs
with its.toMap()
method. -
makeWebAccessFileSystem
(client only) - Adapter that provides access to the actual file system in the browser using a directory picker or a givenwebkitEntry
from an input with thewebkitdirectory
attribute set -
makeNodeFileSystem
(server only) - Adapter that abstracts the nodefs/promises
module for the use with this primitive -
makeTauriFileSystem
(tauri
withfs
access enabled only) - Adapter that connects to thetauri fs
module -
makeChokidarWatcher
- (experimental): usechokidar
to watch for file system changes and trigger reactive updates -
rsync
- small tool to copy over recursively from one file system or adapter to another
npm install @solid-primitives/filesystem
# or
yarn add @solid-primitives/filesystem
# or
pnpm add @solid-primitives/filesystem
The synchronous adapters, which are primarily meant for virtual file systems or in-memory-databases, have the following interface:
export type SyncFileSystemAdapter = {
async: false;
getType: (path: string) => FileType;
mkdir: (path: string) => void;
readdir: (path: string) => [] | [string, ...string[]];
readFile: (path: string) => string;
rename?: (previous: string, next: string) => void;
rm: (path: string) => void;
writeFile: (path: string, data: string) => void;
};
The asynchronous adapters are returning the same values wrapped in promises:
export type AsyncFileSystemAdapter = {
async: true;
getType: (path: string) => Promise<FileType>;
mkdir: (path: string) => Promise<void>;
readdir: (path: string) => Promise<[] | [string, ...string[]]>;
readFile: (path: string) => Promise<string>;
rename?: (previous: string, next: string) => Promise<void>;
rm: (path: string) => Promise<void>;
writeFile: (path: string, data: string) => Promise<void>;
};
To support an unsupported file system, you can provide a wrapper with one of the same APIs.
If no rename
method is given, mkdir/writeFile/rm
are used to achieve the same effect. An actual rename
call will be more performant, though.
The createFileSystem
call then creates a reactive surface above each of these APIs so that the return values of all reading calls are signals for synchronous and resources for asynchronous file system adapters; writing methods for asynchronous APIs will return the same promise for convenience reasons.
There is experimental support for a watcher as second argument in createFileSystems
, which triggers reactive updates on external file system changes. Currently, there is only experimental support for chokidar
for the node file system.
These getters returned from reading methods are bound to Solid's reactivity so that they will automatically cause effects using them outside untrack()
to re-run on updates. Getters may initially return undefined, but will update to the correct value once evaluated.
const vfs = makeVirtualFileSystem({});
const fs = createFileSystem(vfs);
// create a directory
fs.mkdir("/src");
// create or overwrite a file
fs.writeFile("/src/index.ts", "console.log(0);");
// checking entry type: "file" | "dir" | null (file not found) | undefined (not yet ready)
// will be called again if the file or directory is deleted (-> null)
createEffect(() => console.log("/src type", fs.getType("/src")));
// read a directory
// will be called again if the contents of "/" change due to
// writing a new file or deleting an existing file or directory
createEffect(() => console.log("/", fs.readdir("/")));
// reading files
// this signal (or resource for async adapters) will update if
// the file is written by the same fs
createEffect(() => console.log("/src/index.ts", fs.readFile("/src/index.ts")));
// isomorphic file system reader with lazy evaluation
const Item = (props: { path: string; fs: SyncFileSystem | AsyncFileSystem }) => {
const itemType = () => props.fs.getType(props.path);
const name = () => getItemName(props.path);
const [open, setOpen] = createSignal(false);
const content = () =>
open()
? itemType() === "dir"
? props.fs.readdir(props.path)
: props.fs.readFile(props.path)
: undefined;
return (
<>
<button onClick={() => setOpen(!open())}>{open() ? "-" : "+"}</button>
{itemType() === "dir" ? "[DIR]" : "[FILE]"} {name()}
<Switch>
<Match when={open() && itemType() === "file"}>
<pre>{content()?.()}</pre>
</Match>
<Match when={open() && itemType() === "dir"}>
<For each={content() || []}>
{entry => (
<div>
<Item path={entry} fs={props.fs} />
</div>
)}
</For>
</Match>
</Switch>
</>
);
};
The previous <Item>
component can also be used in combination with a file input with the webkitdirectory
attribute set:
const [webkitEntry, setWebkitEntry] = createSignal();
const fs = createMemo(
() => webkitEntry() && createFileSystem(makeWebAccessFileSystem({ webkitEntry: webkitEntry() })),
);
return (
<>
<input
type="file"
webkitdirectory
onChange={e => setWebkitEntry(e.currentTarget.webkitEntry)}
/>
<Show when={fs()}>
<Item fs={fs()} path="/" />
</Show>
</>
);
In some cases, you might need to move data from one file system (or adapter) to another one. In order to do so, this package comes with a rsync utility:
rsync(srcFs, srcPath, destFs, destPath): Promise<void>;
You may view a working example of createFileSystem/makeVirtualFileSystem/makeWebAccessFileSystem
here:
https://primitives.solidjs.community/playground/filesystem/
See CHANGELOG.md