@terbiumos/tfs
Version:
The drop in Filer replacement you have been waiting for. Completely Typed and Built with TypeScript
642 lines (627 loc) • 17.9 kB
text/typescript
import { createFSError, genError } from "../fs/errors";
import { Path } from "../path/index";
import { FS } from "../fs/index";
import { minimatch } from "minimatch";
/**
* The TFS Shell Operations Class
*/
export class Shell {
handle: FileSystemDirectoryHandle;
cwd: string;
private fs: FS;
private path: Path;
constructor(handle: FileSystemDirectoryHandle, fs?: FS) {
if (!handle) {
handle = navigator.storage
.getDirectory()
.then(h => (this.handle = h))
.catch(() => {
throw new Error("Failed to get a handle. Try defining one?");
}) as unknown as FileSystemDirectoryHandle;
}
this.handle = handle;
this.cwd = "/";
this.fs = fs || new FS(this.handle);
this.path = new Path();
}
/**
* Changes the current working directory.
* @param path - The new directory path.
* @throws Will throw an error if the directory does not exist.
* @example
* tfs.shell.cd('/documents'); // Changes the current directory to '/documents'
*/
cd(path: string) {
const newPath = this.path.join(this.cwd, path);
this.fs.exists(newPath, exists => {
if (exists) {
this.cwd = newPath;
} else {
throw createFSError("ENOENT", `No such file or directory: ${newPath}`);
}
});
}
/**
* Prints the current working directory.
* @returns The current working directory as a string.
* @example
* const cwd = tfs.shell.pwd();
* console.log(cwd); // Returns "/" by default
*/
pwd() {
return this.cwd;
}
/**
* Reads the contents of a file.
* @param path - The path to the file to read.
* @param callback - Callback function called with the result.
* @returns The contents of the file as a string.
* @example
* tfs.shell.cat('/documents/file.txt', (err, data) => {
* if (err) {
* console.error(err);
* }
* console.log(data); // File Contents
* });
*
* Alternatively, you can also provide muliple files:
*
* tfs.shell.cat(['/documents/file1.txt', '/documents/file2.txt'], (err, data) => {
* if (err) {
* console.error(err);
* }
* console.log(data); // Both of the File's Contents
* });
*/
cat(path: string | string[], callback: (error: Error | null, data: string | null) => void) {
const paths = Array.isArray(path) ? path : [path];
const fp = paths.map(p => this.path.join(this.cwd, p));
let results: string[] = [];
let completed = 0;
if (fp.length === 0) {
callback(genError("NotFoundError"), null);
return;
}
fp.forEach((p, idx) => {
this.fs.readFile(p, "utf8", (err: Error | null, data: string | null) => {
if (err) {
callback(genError(err), null);
} else {
results[idx] = data as string;
}
completed++;
if (completed === fp.length) {
callback(null, results.join(""));
}
});
});
}
/**
* Lists the contents of a directory.
* @param path - The path to the directory to list.
* @param callback - Callback function called with the result.
* @returns An array of file and directory names in the specified directory.
* @example
* // Similar usage to tfs.fs.readdir
* tfs.shell.ls('/documents', (err, entries) => {
* if (err) {
* console.error(err);
* }
* console.log(entries);
* });
*/
ls(path: string, callback: (error: Error | null, entries: string[] | null) => void) {
const newPath = this.path.join(this.cwd, path);
this.fs.readdir(newPath, (err, entries) => {
if (err) {
callback(genError(err), null);
} else {
callback(null, entries as string[]);
}
});
}
/**
* Executes a JS File using Eval.
* @param path - The path to the JS file to execute.
* @param args - Optional args to pass to the script.
* @param callback - Callback function called with the result or error.
* @example
* tfs.shell.exec('/scripts/myscript.js', ['arg1', 'arg2'], (err, result) => {
* if (err) {
* console.error(err);
* }
* console.log(result); // Result from the script
* });
*/
exec(path: string, args: any[] = [], callback: (error: Error | null, result: any) => void) {
const scriptPath = this.path.join(this.cwd, path);
this.fs.readFile(scriptPath, "utf8", (err: Error | null, code: string | null) => {
if (err || !code) {
callback(genError(err || "NotFoundError"), null);
return;
}
try {
const context = {
fs: this.fs,
args,
callback,
};
const func = new Function("fs", "args", "callback", code);
func(context.fs, context.args, context.callback);
} catch (e) {
callback(e as Error, null);
}
});
}
/**
* Creates a new empty file.
* @param path - The path to the file to create.
* @param callback - Callback function called with the result or error.
* @example
* tfs.shell.touch('/documents/newfile.txt', (err) => {
* if (err) {
* console.error(err);
* }
* console.log('File created');
* });
*/
touch(path: string, callback: (error: Error | null) => void) {
const newPath = this.path.join(this.cwd, path);
this.fs.writeFile(newPath, "", "utf8", (err: Error | null) => {
if (err) {
callback(genError(err));
} else {
callback(null);
}
});
}
/**
* Finds files in a directory.
* @param path - The path to the directory to search.
* @param options - Options for the search.
* @param callback - Callback function called with the results or error.
* @returns An array of file paths that match the search criteria.
* @example
* tfs.shell.find('/documents', { name: '*.txt' }, (err, results) => {
* if (err) {
* console.error(err);
* }
* console.log(results); // Array of .txt files and directories in /documents
* });
*
* You can also use other glob patterns:
*
* tfs.shell.find('/documents', { name: 'file?.js' }, (err, results) => {
* if (err) {
* console.error(err);
* }
* console.log(results); // Array of files and directories in /documents
* });
*/
find(path: string, options: { name: string }, callback: (error: Error | null, results: string[] | null) => void) {
const newPath = this.path.join(this.cwd, path);
let results: string[] = [];
let pendingDirs = 0;
let finished = false;
const walk = (currentPath: string) => {
pendingDirs++;
this.fs.readdir(currentPath, (err, entries) => {
if (err) {
if (!finished) {
finished = true;
callback(genError(err), null);
}
return;
}
let pending = (entries as string[]).length;
if (!pending) {
if (--pendingDirs === 0 && !finished) {
finished = true;
callback(null, results);
}
return;
}
(entries as string[]).forEach(entry => {
const entryPath = this.path.join(currentPath, entry);
this.fs.stat(entryPath, (err, stats) => {
if (err) {
if (!finished) {
finished = true;
callback(genError(err, entryPath), null);
}
return;
}
if (minimatch(entry, options.name)) {
results.push(entryPath);
}
if (stats && stats.type === "DIRECTORY") {
walk(entryPath);
}
if (!--pending) {
if (--pendingDirs === 0 && !finished) {
finished = true;
callback(null, results);
}
}
});
});
});
};
walk(newPath);
}
/**
* Removes a file or directory.
* @param path - The path to the file or directory to remove.
* @param options - Options for the removal.
* @param callback - Callback function called with the result or error.
* @example
* tfs.shell.rm('/documents/oldfile.txt', {}, (err) => {
* if (err) {
* console.error(err);
* }
* console.log('File removed');
* });
*
* To remove directories and their contents, use the recursive option:
*
* tfs.shell.rm('/documents/fulldir', { recursive: true }, (err) => {
* if (err) {
* console.error(err);
* }
* console.log('Directory and its contents are deleted');
* });
*/
rm(path: string, options: { recursive: boolean }, callback: (error: Error | null) => void) {
const newPath = this.path.join(this.cwd, path);
if (options.recursive) {
this.fs.readdir(newPath, (err, entries) => {
if (err) {
callback(genError(err));
return;
}
let pending = (entries as string[]).length;
if (!pending) {
this.fs.rmdir(newPath, err => {
if (err) {
callback(genError(err));
} else {
callback(null);
}
});
return;
}
let errorOccurred = false;
(entries as string[]).forEach(entry => {
const entryPath = this.path.join(newPath, entry);
this.fs.stat(entryPath, (err, stats) => {
if (errorOccurred) return;
if (err) {
errorOccurred = true;
callback(genError(err, entryPath));
return;
}
if (stats && stats.type === "DIRECTORY") {
this.rm(this.path.join(path, entry), { recursive: true }, err => {
if (errorOccurred) return;
if (err) {
errorOccurred = true;
callback(err);
return;
}
if (!--pending) {
this.fs.rmdir(newPath, err => {
if (err) {
callback(genError(err));
} else {
callback(null);
}
});
}
});
} else {
this.fs.unlink(entryPath, err => {
if (errorOccurred) return;
if (err) {
errorOccurred = true;
callback(genError(err, entryPath));
return;
}
if (!--pending) {
this.fs.rmdir(newPath, err => {
if (err) {
callback(genError(err));
} else {
callback(null);
}
});
}
});
}
});
});
});
} else {
this.fs.unlink(newPath, err => {
if (err) {
callback(genError(err));
} else {
callback(null);
}
});
}
}
/**
* Creates a directory and any necessary parent directories.
* @param path - The path to the directory to create.
* @param callback - Callback function called with the result or error.
* @example
* tfs.shell.mkdirp('/documents/newdir/subdir', (err) => {
* if (err) {
* console.error(err);
* }
* console.log('Directories created');
* });
*/
mkdirp(path: string, callback: (error: Error | null) => void) {
this.fs.mkdir(path, callback);
}
/**
* Creates a temporary directory.
* @param callback - Callback function called with the result or error.
* @example
* tfs.shell.tempDir((err, dirPath) => {
* if (err) {
* console.error(err);
* }
* console.log(dirPath);
* });
*/
tempDir(callback: (error: Error | null, dirPath?: string) => void) {
const name = `temp-${Date.now()}-${Math.floor(Math.random() * 1000)}`;
const path = this.path.join(this.cwd, name);
this.fs.mkdir(path, err => {
if (err) {
callback(genError(err, path));
} else {
callback(null, path);
}
});
}
/**
* Formats the File System (Deletes all files and directories).
* NOTE this is not reversible and should be used with caution.
* Also note that this is not in the Filer or NodeFS spec and is a TFS Specific method.
* @param callback - Callback function called with the result or error.
* @example
* tfs.shell.format((err) => {
* if (err) {
* console.error(err);
* }
* console.log('File system formatted');
* });
*/
format(callback: (error: Error | null) => void) {
this.fs.readdir("/", (err, entries) => {
if (err) {
callback(genError(err));
return;
}
for (const entry of entries as string[]) {
this.fs.stat(entry, (err, stats) => {
if (err) {
callback(genError(err, entry));
return;
}
if (stats && stats.type === "DIRECTORY") {
this.rm(entry, { recursive: true }, err => {
if (err) {
callback(genError(err, entry));
return;
}
});
} else {
this.fs.unlink(entry, err => {
if (err) {
callback(genError(err, entry));
return;
}
});
}
});
}
console.log(`[TFS] Operation Completed at: ${new Date().toISOString()}`);
callback(null);
});
}
promises = {
/**
* Changes the current working directory.
* @param path - The path to the new working directory.
* @returns A promise that resolves when the directory has been changed.
* @example
* await tfs.shell.promises.cd('/documents');
*/
cd: (path: string): Promise<void> => {
return new Promise(resolve => {
this.cd(path);
resolve();
});
},
/**
* Reads the contents of a file.
* @param path - The path to the file to read.
* @returns A promise that resolves with the file contents as a string.
* @example
* const data = await tfs.shell.promises.cat('/documents/file.txt');
* console.log(data); // File Contents
*/
cat: (path: string): Promise<string> => {
return new Promise((resolve, reject) => {
this.cat(path, (err, data) => {
if (err) {
reject(err);
} else {
resolve(data as string);
}
});
});
},
/**
* Lists the contents of a directory.
* @param path - The path to the directory to list.
* @returns A promise that resolves with an array of file and directory names.
* @example
* const entries = await tfs.shell.promises.ls('/documents');
* console.log(entries); // Array of files and directories in /documents
*/
ls: (path: string): Promise<string[]> => {
return new Promise((resolve, reject) => {
this.ls(path, (err, entries) => {
if (err) {
reject(err);
} else {
resolve(entries as string[]);
}
});
});
},
/**
* Executes a command in the shell.
* @param path - The path to the command to execute.
* @param args - The arguments to pass to the command.
* @returns A promise that resolves with the command output.
* @example
* const result = await tfs.shell.promises.exec('/scripts/myScript.js', ['-help']);
* console.log(result); // Command output
*/
exec: (path: string, args: any[] = []): Promise<any> => {
return new Promise((resolve, reject) => {
this.exec(path, args, (err, result) => {
if (err) {
reject(err);
} else {
resolve(result);
}
});
});
},
/**
* Creates a new empty file.
* @param path - The path to the file to create.
* @returns A promise that resolves when the file has been created.
* @example
* await tfs.shell.promises.touch('/documents/newfile.txt');
*/
touch: (path: string): Promise<void> => {
return new Promise((resolve, reject) => {
this.touch(path, err => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
},
/**
* Finds files in a directory.
* @param path - The path to the directory to search.
* @param options - The search options.
* @returns A promise that resolves with an array of matching file paths.
* @example
* const results = await tfs.shell.promises.find('/documents', { name: '*.txt' });
* console.log(results); // Array of .txt files and directories in /documents
*
* You can also use other glob patterns:
*
* const results = await tfs.shell.promises.find('/documents', { name: 'file?.js' });
* console.log(results); // Array of files and directories in /documents
*/
find: (path: string, options: { name: string }): Promise<string[]> => {
return new Promise((resolve, reject) => {
this.find(path, options, (err, results) => {
if (err) {
reject(err);
} else {
resolve(results as string[]);
}
});
});
},
/**
* Removes a file or directory.
* @param path - The path to the file or directory to remove.
* @param options - The options for the removal. (Use `{ recursive: true }` to remove directories and their contents)
* @returns A promise that resolves when the file or directory has been removed.
* @example
* await tfs.shell.promises.rm('/documents/oldfile.txt');
* await tfs.shell.promises.rm('/documents/fulldir', { recursive: true });
*/
rm: (path: string, options?: { recursive: boolean }): Promise<void> => {
return new Promise((resolve, reject) => {
this.rm(path, options!, err => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
},
/**
* Creates a directory and any necessary parent directories.
* @param path - The path to the directory to create.
* @returns A promise that resolves when the directory has been created.
* @example
* await tfs.shell.promises.mkdirp('/documents/newdir/subdir');
*/
mkdirp: (path: string): Promise<void> => {
return new Promise((resolve, reject) => {
this.mkdirp(path, err => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
},
/**
* Creates a temporary directory.
* @returns A promise that resolves with the path of the created temporary directory.
* @example
* const dirPath = await tfs.shell.promises.tempDir();
* console.log(dirPath); // Path of the created temporary directory
*/
tempDir: (): Promise<string> => {
return new Promise((resolve, reject) => {
this.tempDir((err, dirPath) => {
if (err) {
reject(err);
} else {
resolve(dirPath as string);
}
});
});
},
/**
* Formats the File System (Deletes all files and directories).
* NOTE this is not reversible and should be used with caution.
* Also note that this is not in the Filer or NodeFS spec and is a TFS Specific method.
* @returns A promise that resolves when the file system has been formatted.
* @example
* await tfs.shell.promises.format();
*/
format: (): Promise<void> => {
return new Promise((resolve, reject) => {
this.format(err => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
},
};
}