securityos/contexts/fileSystem/useAsyncFs.ts

250 lines
7.8 KiB
TypeScript

import type * as IBrowserFS from "browserfs";
import type MountableFileSystem from "browserfs/dist/node/backend/MountableFileSystem";
import type { FSModule } from "browserfs/dist/node/core/FS";
import type Stats from "browserfs/dist/node/core/node_fs_stats";
import FileSystemConfig from "contexts/fileSystem/FileSystemConfig";
import { resetStorage, supportsIndexedDB } from "contexts/fileSystem/functions";
import { join } from "path";
import * as BrowserFS from "public/System/BrowserFS/browserfs.min.js";
import { useEffect, useMemo, useRef, useState } from "react";
import {
ICON_CACHE,
ICON_CACHE_EXTENSION,
SESSION_FILE,
} from "utils/constants";
export type AsyncFS = {
exists: (path: string) => Promise<boolean>;
lstat: (path: string) => Promise<Stats>;
mkdir: (path: string, overwrite?: boolean) => Promise<boolean>;
readFile: (path: string) => Promise<Buffer>;
readdir: (path: string) => Promise<string[]>;
rename: (oldPath: string, newPath: string) => Promise<boolean>;
rmdir: (path: string) => Promise<boolean>;
stat: (path: string) => Promise<Stats>;
unlink: (path: string) => Promise<boolean>;
writeFile: (
path: string,
data: Buffer | string,
overwrite?: boolean
) => Promise<boolean>;
};
const { BFSRequire, configure } = BrowserFS as typeof IBrowserFS;
export type RootFileSystem = Omit<
MountableFileSystem,
"mntMap" | "mountList"
> & {
mntMap: Record<
string,
{
_data: Buffer;
getName: () => string;
}
>;
mountList: string[];
};
type AsyncFSModule = AsyncFS & {
fs?: FSModule;
rootFs?: RootFileSystem;
};
type FsQueueCall = [string, unknown[]];
const mockFsCallQueue: FsQueueCall[] = [];
const runQueuedFsCalls = (fs: FSModule): void => {
if (mockFsCallQueue.length > 0) {
const [name, args] = mockFsCallQueue.shift() as FsQueueCall;
if (name in fs) {
const fsCall = fs[name as keyof FSModule];
if (typeof fsCall === "function") {
// eslint-disable-next-line @typescript-eslint/ban-types
(fsCall as unknown as Function)(...args);
}
}
runQueuedFsCalls(fs);
}
};
const useAsyncFs = (): AsyncFSModule => {
const [fs, setFs] = useState<FSModule>();
const fsRef = useRef<FSModule>();
const [rootFs, setRootFs] = useState<RootFileSystem>();
const asyncFs: AsyncFS = useMemo(
() => ({
exists: (path) =>
new Promise((resolve) => {
fs?.exists(path, resolve);
}),
lstat: (path) =>
new Promise((resolve, reject) => {
fs?.lstat(path, (error, stats = Object.create(null) as Stats) =>
error ? reject(error) : resolve(stats)
);
}),
mkdir: (path, overwrite = false) =>
new Promise((resolve, reject) => {
fs?.mkdir(path, { flag: overwrite ? "w" : "wx" }, (error) =>
error ? reject(error) : resolve(true)
);
}),
readFile: (path) =>
new Promise((resolve, reject) => {
fs?.readFile(path, (error, data = Buffer.from("")) => {
if (!error) return resolve(data);
if (error.code === "EISDIR" && rootFs?.mntMap[path]?._data) {
return resolve(rootFs.mntMap[path]._data);
}
return reject(error);
});
}),
readdir: (path) =>
new Promise((resolve, reject) => {
fs?.readdir(path, (error, data = []) =>
error ? reject(error) : resolve(data)
);
}),
rename: (oldPath, newPath) =>
new Promise((resolve, reject) => {
fs?.rename(oldPath, newPath, (renameError) => {
if (!renameError) {
resolve(true);
} else if (renameError.code === "ENOTSUP") {
fs.lstat(
oldPath,
(_statsError, stats = Object.create(null) as Stats) => {
if (stats.isDirectory()) {
reject();
} else {
fs.readFile(oldPath, (readError, data) =>
fs.writeFile(newPath, data, (writeError) =>
readError || writeError
? reject(readError || writeError)
: resolve(false)
)
);
}
}
);
} else if (renameError.code === "EISDIR") {
rootFs?.umount(oldPath);
asyncFs.rename(oldPath, newPath).then(resolve, reject);
} else {
reject(renameError);
}
});
}),
rmdir: (path) =>
new Promise((resolve, reject) => {
fs?.rmdir(path, (error) => (error ? reject(error) : resolve(true)));
}),
stat: (path) =>
new Promise((resolve, reject) => {
fs?.stat(path, (error, stats = Object.create(null) as Stats) =>
error ? reject(error) : resolve(stats)
);
}),
unlink: (path) =>
new Promise((resolve, reject) => {
fs?.unlink(path, (error) => (error ? reject(error) : resolve(true)));
}),
writeFile: (path, data, overwrite = false) =>
new Promise((resolve, reject) => {
fs?.writeFile(
path,
data,
{ flag: overwrite ? "w" : "wx" },
(error) => {
if (error && (!overwrite || error.code !== "EEXIST")) {
if (error.code === "ENOENT" && error.path === "/") {
resetStorage(rootFs).finally(() => window.location.reload());
}
reject(error);
} else {
resolve(!error);
try {
if (path !== SESSION_FILE) {
const cachedIconPath = join(
ICON_CACHE,
`${path}${ICON_CACHE_EXTENSION}`
);
fs?.exists(
cachedIconPath,
(exists) => exists && fs?.unlink(cachedIconPath)
);
}
} catch {
// Ignore icon cache issues
}
}
}
);
}),
}),
[fs, rootFs]
);
useEffect(() => {
if (!fs) {
const queueFsCall =
(name: string) =>
(...args: unknown[]) => {
if (fsRef.current) {
// eslint-disable-next-line @typescript-eslint/ban-types
(fsRef.current[name as keyof FSModule] as unknown as Function)(
...args
);
} else mockFsCallQueue.push([name, args]);
};
setFs({
exists: queueFsCall("exists"),
lstat: queueFsCall("lstat"),
mkdir: queueFsCall("mkdir"),
readFile: queueFsCall("readFile"),
readdir: queueFsCall("readdir"),
rename: queueFsCall("rename"),
rmdir: queueFsCall("rmdir"),
stat: queueFsCall("stat"),
unlink: queueFsCall("unlink"),
writeFile: queueFsCall("writeFile"),
} as Partial<FSModule> as FSModule);
} else if ("getRootFS" in fs) {
runQueuedFsCalls(fs);
} else {
const setupFs = (writeToIndexedDB: boolean): void =>
configure(FileSystemConfig(!writeToIndexedDB), () => {
const loadedFs = BFSRequire("fs");
fsRef.current = loadedFs;
setFs(loadedFs);
setRootFs(loadedFs.getRootFS() as RootFileSystem);
});
supportsIndexedDB().then(setupFs);
}
}, [fs]);
return useMemo(
() => ({
...asyncFs,
fs,
rootFs,
}),
[asyncFs, fs, rootFs]
);
};
export default useAsyncFs;