forked from tools/josh
1
0
Fork 0
josh/src/main/js/FileSystem.ts

809 lines
27 KiB
TypeScript
Raw Normal View History

2019-11-01 11:59:33 +01:00
import {emptyFunction, getFileExtension, IllegalStateError} from "./Shared";
2019-10-29 12:36:03 +01:00
/**
* A file system.
*/
export class FileSystem {
2019-10-29 12:36:03 +01:00
/**
* The root directory.
*/
2019-10-21 02:25:42 +02:00
private root: Directory;
2019-10-29 12:36:03 +01:00
/**
* The current directory.
*/
private files: Directory;
/**
* The current working directory.
*/
2019-10-31 22:17:46 +01:00
private _cwd: Path;
2019-10-29 12:36:03 +01:00
/**
* Constructs a new file system.
*
2019-10-31 22:46:42 +01:00
* @param root the directory to set as the root
2019-10-29 12:36:03 +01:00
*/
2019-10-31 22:46:42 +01:00
constructor(root: Directory | undefined = undefined) {
if (root === undefined) {
this.root = new Directory({
2019-10-31 01:27:31 +01:00
"personal": new Directory({
"steam.lnk": new File("https://steamcommunity.com/id/Waflix"),
2019-10-31 01:34:36 +01:00
"nukapedia.lnk": new File("https://fallout.wikia.com/wiki/User:FDekker"),
2019-10-31 01:27:31 +01:00
"blog.lnk": new File("https://blog.fwdekker.com/"),
}),
2019-10-31 01:27:31 +01:00
"projects": new Directory({
"randomness.lnk": new File("https://github.com/FWDekker/intellij-randomness"),
2019-10-31 01:34:36 +01:00
"schaapi.lnk": new File("https://cafejojo.org/schaapi"),
2019-10-31 01:27:31 +01:00
"gitea.lnk": new File("https://git.fwdekker.com/explore/"),
"github.lnk": new File("https://github.com/FWDekker/"),
}),
2019-10-31 01:27:31 +01:00
"social": new Directory({
"github.lnk": new File("https://github.com/FWDekker/"),
"stackoverflow.lnk": new File("https://stackoverflow.com/u/3307872"),
"linkedin.lnk": new File("https://www.linkedin.com/in/fwdekker/")
}),
2019-10-31 01:27:31 +01:00
"resume.pdf": new File("https://static.fwdekker.com/misc/resume.pdf")
});
} else {
2019-10-31 22:46:42 +01:00
this.root = root;
}
2019-10-31 22:17:46 +01:00
this._cwd = new Path("/");
2019-10-21 17:07:16 +02:00
this.files = this.root;
2018-11-28 22:23:11 +01:00
}
2019-10-29 12:36:03 +01:00
/**
* Returns the current working directory.
*
* @return the current working directory
*/
get cwd(): string {
2019-10-31 22:17:46 +01:00
return this._cwd.toString();
}
/**
* Sets the current working directory.
*
* @param cwd the desired current working directory
* @throws if the desired current working directory does not point to an existing directory
*/
set cwd(cwd: string) {
2019-10-31 22:17:46 +01:00
const path = new Path(cwd);
2019-10-31 22:17:46 +01:00
const target = this.getNode(path);
if (!(target instanceof Directory))
2019-10-31 22:17:46 +01:00
throw `The directory \`${path}\` does not exist.`;
2019-10-31 22:17:46 +01:00
this._cwd = path;
this.files = target;
}
/**
* Returns the JSON serialization of the root node.
*
* @return the JSON serialization of the root node
*/
get serializedRoot(): string {
return this.root.serialize();
}
2019-11-02 18:19:10 +01:00
/**
* Converts a string to a path.
*
* @param path the string to convert; strings starting with `/` are interpreted as absolute paths and other strings
* s strings relative to the current working directory
* @return the path corresponding to the given string
*/
getPathTo(path: string): Path {
if (path.startsWith("/"))
return new Path(path);
return this._cwd.getChild(path);
}
2019-10-29 12:36:03 +01:00
/**
* Returns the node at the given path.
*
2019-10-31 22:17:46 +01:00
* @param target the path of the node to return; strings starting with `/` are interpreted as absolute paths and
* other strings as strings relative to the current working directory
2019-10-29 12:36:03 +01:00
* @return the node at the given path
*/
2019-10-31 22:17:46 +01:00
getNode(target: string | Path): Node {
if (typeof target === "string")
target = this.getPathTo(target);
2018-11-28 22:23:11 +01:00
2019-10-31 22:17:46 +01:00
const parts = target.toString().split("/");
2019-10-21 02:25:42 +02:00
let node: Node = this.root;
2019-10-31 22:17:46 +01:00
parts.forEach(part => {
2019-10-21 02:25:42 +02:00
if (part === "" || node === undefined || node instanceof File)
2018-11-28 22:23:11 +01:00
return;
2019-10-21 02:25:42 +02:00
if (node instanceof Directory)
node = node.getNode(part);
else
2019-10-31 22:46:42 +01:00
throw new IllegalStateError("Node must be file or directory.");
2018-11-28 22:23:11 +01:00
});
2019-10-21 02:25:42 +02:00
return node;
}
2019-10-29 12:36:03 +01:00
/**
* Executes the given function for each string in the given array.
*
* @param inputs the inputs to process using the given function
* @param fun the function to execute on each string in the given array
* @return the concatenation of outputs of the given function, separated by newlines
*/
2019-10-21 17:07:16 +02:00
private executeForEach(inputs: string[], fun: (_: string) => string): string {
const outputs: string[] = [];
2018-11-28 19:51:48 +01:00
2018-11-29 13:01:55 +01:00
inputs.forEach(input => {
const output = fun(input);
2018-11-28 22:23:11 +01:00
2019-06-10 15:31:46 +02:00
if (output !== "")
2018-11-29 13:01:55 +01:00
outputs.push(output);
});
return outputs.join("\n");
2018-11-28 19:51:48 +01:00
}
2019-10-31 22:17:46 +01:00
2018-11-28 22:23:11 +01:00
/**
* Changes the current directory to {@code path}, if it exists.
*
* @param pathString the absolute or relative path to change the current directory to
2019-10-30 23:17:59 +01:00
* @return an empty string if the change was successful, or an error message explaining what went wrong
2018-11-28 22:23:11 +01:00
*/
2019-10-31 00:29:55 +01:00
cd(pathString: string | undefined): string {
2019-10-21 02:25:42 +02:00
if (pathString === undefined)
2018-11-28 22:41:59 +01:00
return "";
2018-11-28 22:23:11 +01:00
2019-10-31 22:17:46 +01:00
const path = this.getPathTo(pathString);
2019-10-31 22:17:46 +01:00
const node = this.getNode(path);
2019-10-21 02:25:42 +02:00
if (node === undefined)
2019-10-31 22:17:46 +01:00
return `The directory '${path}' does not exist`;
2019-10-21 02:25:42 +02:00
if (!(node instanceof Directory))
2019-10-31 22:17:46 +01:00
return `'${path}' is not a directory.`;
2018-11-28 22:23:11 +01:00
2019-10-31 22:17:46 +01:00
this.cwd = path.toString();
2018-11-28 22:41:59 +01:00
return "";
2018-11-28 19:51:48 +01:00
}
2018-11-28 22:23:11 +01:00
2018-11-29 20:56:27 +01:00
/**
2019-10-31 22:17:46 +01:00
* Creates an empty file at `path` if it does not exist.
2018-11-29 20:56:27 +01:00
*
2019-10-21 02:25:42 +02:00
* @param pathString the path to create a file at if it does not exist
2019-10-30 23:17:59 +01:00
* @return an empty string if the removal was successful, or a message explaining what went wrong
2018-11-29 20:56:27 +01:00
*/
2019-10-21 02:25:42 +02:00
private createFile(pathString: string): string {
2019-10-31 22:17:46 +01:00
const path = this.getPathTo(pathString);
const parent = this.getNode(path.parent);
if (parent === undefined)
return `The directory '${path.parent}' does not exist`;
if (!(parent instanceof Directory))
return `${path.parent} is not a directory`;
if (parent.hasNode(path.fileName))
2019-10-21 02:25:42 +02:00
return ""; // File already exists
2018-11-29 20:56:27 +01:00
2019-10-31 22:17:46 +01:00
parent.addNode(path.fileName, new File());
2018-11-29 20:56:27 +01:00
return "";
}
/**
* Calls {@link createFile} on all elements in {@code paths}.
*
* @param paths {string[]} the absolute or relative paths to the files to be created
2019-10-30 23:17:59 +01:00
* @return the warnings generated during creation of the files
2018-11-29 20:56:27 +01:00
*/
2019-10-21 02:25:42 +02:00
createFiles(paths: string[]): string {
return this.executeForEach(paths, path => this.createFile(path));
2018-11-29 20:56:27 +01:00
}
2018-11-29 13:34:46 +01:00
/**
* Copies {@code source} to {@code destination}.
*
* If the destination does not exist, the source will be copied to that exact location. If the destination exists
* and is a directory, the source will be copied into the directory. If the destination exists but is not a
* directory, the copy will fail.
*
2019-10-31 14:15:27 +01:00
* @param sourceString the absolute or relative path to the file or directory to copy
* @param destinationString the absolute or relative path to the destination
* @param isRecursive if copying should happen recursively if the source is a directory
2019-10-30 23:17:59 +01:00
* @return an empty string if the copy was successful, or a message explaining what went wrong
2018-11-29 13:34:46 +01:00
*/
2019-10-31 14:15:27 +01:00
cp(sourceString: string, destinationString: string, isRecursive: boolean): string {
2019-10-31 22:17:46 +01:00
const sourcePath = this.getPathTo(sourceString);
const sourceNode = this.getNode(sourcePath);
2018-11-29 13:34:46 +01:00
2019-10-31 22:17:46 +01:00
const destinationPath = this.getPathTo(destinationString);
const destinationNode = this.getNode(destinationPath);
const destinationParentNode = this.getNode(destinationPath.parent);
2018-11-29 13:34:46 +01:00
2019-10-31 22:17:46 +01:00
if (sourceNode === undefined)
return `The file '${sourcePath}' does not exist`;
if (!(sourceNode instanceof File) && !isRecursive)
2018-11-29 13:34:46 +01:00
return `Cannot copy directory.`;
2019-10-31 22:17:46 +01:00
if (destinationParentNode === undefined)
return `The directory '${destinationPath.parent}' does not exist`;
let targetPath: Path;
let targetParentNode: Directory;
if (destinationNode === undefined) {
// Target does not exist, so user wants to copy into target, not retaining the source's name
if (!(destinationParentNode instanceof Directory))
return `The path '${destinationPath.parent}' does not point to a directory`;
targetParentNode = destinationParentNode;
targetPath = destinationPath;
2018-11-29 13:34:46 +01:00
} else {
2019-10-31 22:17:46 +01:00
// Target exists, so user wants to copy into child of target, retaining the source's name
if (!(destinationNode instanceof Directory))
return `The path '${destinationPath}' does not point to a directory`;
2019-10-21 17:07:16 +02:00
2019-10-31 22:17:46 +01:00
targetParentNode = destinationNode;
targetPath = destinationPath.getChild(sourcePath.fileName);
2018-11-29 13:34:46 +01:00
}
2019-10-31 22:17:46 +01:00
if (targetParentNode.hasNode(targetPath.fileName))
return `The file '${targetPath}' already exists`;
2018-11-29 13:34:46 +01:00
2019-10-31 22:17:46 +01:00
targetParentNode.addNode(targetPath.fileName, sourceNode.copy());
2018-11-29 13:34:46 +01:00
return "";
}
2018-11-28 22:23:11 +01:00
/**
* Returns the directory at {@code path}, or the current directory if no path is given.
*
* @param pathString {string} the absolute or relative path to the directory to return
2019-10-30 23:17:59 +01:00
* @return the directory at {@code path}, or the current directory if no path is given
2018-11-28 22:23:11 +01:00
*/
2019-10-21 02:25:42 +02:00
ls(pathString: string): string {
2019-10-31 22:17:46 +01:00
const path = this.getPathTo(pathString);
2018-11-28 22:23:11 +01:00
2019-10-31 22:17:46 +01:00
const node = this.getNode(path);
2019-10-21 02:25:42 +02:00
if (node === undefined)
2019-10-31 22:17:46 +01:00
return `The directory '${path}' does not exist`;
2019-10-21 02:25:42 +02:00
if (!(node instanceof Directory))
2019-10-31 22:17:46 +01:00
return `'${path}' is not a directory`;
2018-11-29 00:05:37 +01:00
2019-10-31 23:22:37 +01:00
const dirList = [new Directory({}).nameString(".", path), new Directory({}).nameString("..", path.parent)];
2019-10-21 17:07:16 +02:00
const fileList: string[] = [];
2018-11-29 00:05:37 +01:00
2019-10-21 02:25:42 +02:00
const nodes = node.nodes;
2018-11-30 15:23:03 +01:00
Object.keys(nodes)
2019-10-29 12:36:03 +01:00
.sortAlphabetically((x) => x, false)
2018-11-30 15:23:03 +01:00
.forEach(name => {
const node = nodes[name];
2019-10-21 02:25:42 +02:00
if (node instanceof Directory)
2019-10-31 23:22:37 +01:00
dirList.push(node.nameString(name, path.getChild(name)));
2019-10-21 02:25:42 +02:00
else if (node instanceof File)
2019-10-31 23:22:37 +01:00
fileList.push(node.nameString(name, path.getChild(name)));
2019-06-10 15:31:46 +02:00
else
2019-10-31 23:22:37 +01:00
throw new IllegalStateError(`'${path.getChild(name)}' is neither a file nor a directory.`);
2018-11-30 15:23:03 +01:00
});
2018-11-29 00:05:37 +01:00
return dirList.concat(fileList).join("\n");
2018-11-28 19:51:48 +01:00
}
2018-11-28 22:23:11 +01:00
/**
* Creates an empty directory in the file system.
*
* @param pathString {string} the absolute or relative path to the directory to create
2019-10-30 23:17:59 +01:00
* @return an empty string if the removal was successful, or a message explaining what went wrong
2018-11-28 22:23:11 +01:00
*/
2019-10-21 02:25:42 +02:00
private mkdir(pathString: string): string {
2019-10-31 22:17:46 +01:00
const path = this.getPathTo(pathString);
2018-11-28 22:23:11 +01:00
2019-10-31 22:17:46 +01:00
const parentNode = this.getNode(path.parent);
if (parentNode === undefined)
return `The directory '${path.parent}' does not exist`;
if (!(parentNode instanceof Directory))
return `'${path.parent}' is not a directory`;
if (parentNode.hasNode(path.fileName))
return `The directory '${path}' already exists`;
2018-11-28 19:51:48 +01:00
2019-10-31 22:17:46 +01:00
parentNode.addNode(path.fileName, new Directory());
2018-11-28 22:41:59 +01:00
return "";
2018-11-28 22:23:11 +01:00
}
2018-11-28 19:51:48 +01:00
2018-11-29 13:01:55 +01:00
/**
* Calls {@link mkdir} on all elements in {@code paths}.
*
* @param paths {string[]} the absolute or relative paths to the directories to create
2019-10-30 23:17:59 +01:00
* @return the warnings generated during creation of the directories
2018-11-29 13:01:55 +01:00
*/
2019-10-21 02:25:42 +02:00
mkdirs(paths: string[]): string {
return this.executeForEach(paths, this.mkdir.bind(this));
}
2018-11-29 13:18:48 +01:00
/**
* Moves {@code source} to {@code destination}.
*
* If the destination does not exist, the source will be moved to that exact location. If the destination exists and
* is a directory, the source will be moved into the directory. If the destination exists but is not a directory,
* the move will fail.
*
* @param sourceString {string} the absolute or relative path to the file or directory to move
* @param destinationString {string} the absolute or relative path to the destination
2019-10-30 23:17:59 +01:00
* @return an empty string if the move was successful, or a message explaining what went wrong
2018-11-29 13:18:48 +01:00
*/
2019-10-21 02:25:42 +02:00
mv(sourceString: string, destinationString: string): string {
2019-10-31 22:17:46 +01:00
const result = this.cp(sourceString, destinationString, true);
if (result === "")
this.rm(sourceString, true, true, true);
return result;
2018-11-29 13:18:48 +01:00
}
2018-11-28 22:23:11 +01:00
/**
* Removes a file from the file system.
*
* @param pathString {string} the absolute or relative path to the file to be removed
2018-11-29 09:37:42 +01:00
* @param force {boolean} true if no warnings should be given if removal is unsuccessful
2018-11-29 13:50:36 +01:00
* @param recursive {boolean} true if files and directories should be removed recursively
* @param noPreserveRoot {boolean} false if the root directory should not be removed
2019-10-30 23:17:59 +01:00
* @return an empty string if the removal was successful, or a message explaining what went wrong
2018-11-28 22:23:11 +01:00
*/
2019-10-21 02:25:42 +02:00
private rm(pathString: string, force: boolean = false, recursive: boolean = false, noPreserveRoot: boolean = false): string {
2019-10-31 22:17:46 +01:00
const path = this.getPathTo(pathString);
2018-11-28 22:23:11 +01:00
2019-10-31 22:17:46 +01:00
const parentNode = this.getNode(path.parent);
2019-06-10 15:31:46 +02:00
if (parentNode === undefined)
2018-11-29 09:37:42 +01:00
return force
? ""
2019-10-31 22:17:46 +01:00
: `The directory '${path.parent}' does not exist`;
2019-10-21 02:25:42 +02:00
if (!(parentNode instanceof Directory))
return force
? ""
2019-10-31 22:17:46 +01:00
: `'${path.parent}' is not a directory`;
2018-11-28 19:51:48 +01:00
2019-10-31 22:17:46 +01:00
const childNode = this.getNode(path);
2019-06-10 15:31:46 +02:00
if (childNode === undefined)
return force
? ""
2019-10-31 22:17:46 +01:00
: `The file '${path}' does not exist`;
2018-11-28 19:51:48 +01:00
2018-11-29 13:50:36 +01:00
if (recursive) {
2019-10-31 22:17:46 +01:00
if (path.toString() === "/")
2019-06-10 15:31:46 +02:00
if (noPreserveRoot)
2019-10-21 02:25:42 +02:00
this.root = new Directory();
2019-06-10 15:31:46 +02:00
else
2018-11-29 13:50:36 +01:00
return "'/' cannot be removed";
2019-06-10 15:31:46 +02:00
else
2018-11-29 13:50:36 +01:00
parentNode.removeNode(childNode);
} else {
2019-06-10 15:31:46 +02:00
if (!(childNode instanceof File))
2018-11-29 13:50:36 +01:00
return force
? ""
2019-10-31 22:17:46 +01:00
: `'${path.fileName}' is not a file`;
2018-11-29 13:50:36 +01:00
parentNode.removeNode(childNode);
}
2018-11-28 22:41:59 +01:00
return "";
2018-11-28 22:23:11 +01:00
}
2018-11-28 19:51:48 +01:00
2018-11-29 13:01:55 +01:00
/**
* Calls {@link rm} on all elements in {@code paths}.
*
* @param paths {string} the absolute or relative paths to the files to be removed
* @param force {boolean} true if no warnings should be given if removal is unsuccessful
2018-11-29 13:50:36 +01:00
* @param recursive {boolean} true if files and directories should be removed recursively
* @param noPreserveRoot {boolean} false if the root directory should not be removed
2019-10-30 23:17:59 +01:00
* @return the warnings generated during removal of the directories
2018-11-29 13:01:55 +01:00
*/
2019-10-21 02:25:42 +02:00
rms(paths: string[], force: boolean = false, recursive: boolean = false, noPreserveRoot: boolean = false): string {
return this.executeForEach(paths, path => {
2018-11-29 13:50:36 +01:00
return this.rm(path, force, recursive, noPreserveRoot);
2018-11-29 09:37:42 +01:00
});
2018-11-29 09:20:23 +01:00
}
2018-11-28 22:23:11 +01:00
/**
* Removes a directory from the file system.
*
2019-10-31 22:17:46 +01:00
* @param pathString the absolute or relative path to the directory to be removed
2019-10-30 23:17:59 +01:00
* @return an empty string if the removal was successful, or a message explaining what went wrong
2018-11-28 22:23:11 +01:00
*/
2019-10-21 02:25:42 +02:00
private rmdir(pathString: string): string {
2019-10-31 22:17:46 +01:00
const path = this._cwd.getChild(pathString);
2019-10-31 22:17:46 +01:00
if (path.toString() === "/") {
2019-10-21 02:25:42 +02:00
if (this.root.nodeCount > 0)
2018-11-29 00:21:35 +01:00
return `The directory is not empty.`;
2019-06-10 15:31:46 +02:00
else
return "";
2018-11-29 00:21:35 +01:00
}
2019-10-31 22:17:46 +01:00
const parentDir = this.getNode(path.parent);
2019-06-10 15:31:46 +02:00
if (parentDir === undefined)
2019-10-31 22:17:46 +01:00
return `The directory '${path.parent}' does not exist`;
2019-10-21 02:25:42 +02:00
if (!(parentDir instanceof Directory))
2019-10-31 22:17:46 +01:00
return `'${path.parent}' is not a directory`;
2018-11-28 22:23:11 +01:00
2019-10-31 22:17:46 +01:00
const childDir = parentDir.getNode(path.fileName);
2019-06-10 15:31:46 +02:00
if (childDir === undefined)
2019-10-31 22:17:46 +01:00
return `The directory '${path.fileName}' does not exist`;
2019-10-21 02:25:42 +02:00
if (!(childDir instanceof Directory))
2019-10-31 22:17:46 +01:00
return `'${path.fileName}' is not a directory`;
2019-10-21 02:25:42 +02:00
if (childDir.nodeCount > 0)
2018-11-28 22:23:11 +01:00
return `The directory is not empty`;
parentDir.removeNode(childDir);
2018-11-28 22:41:59 +01:00
return "";
2018-11-28 19:51:48 +01:00
}
2018-11-29 13:01:55 +01:00
/**
* Calls {@link rmdir} on all elements in {@code paths}.
*
* @param paths {string[]} the absolute or relative paths to the directories to be removed
2019-10-30 23:17:59 +01:00
* @return the warnings generated during removal of the directories
2018-11-29 13:01:55 +01:00
*/
2019-10-21 02:25:42 +02:00
rmdirs(paths: string[]): string {
return this.executeForEach(paths, path => this.rmdir(path));
}
2018-11-28 22:23:11 +01:00
}
2018-11-29 00:05:37 +01:00
2019-10-29 12:36:03 +01:00
/**
* A path to a node in the file system.
*/
export class Path {
2019-10-29 12:36:03 +01:00
/**
2019-10-31 22:17:46 +01:00
* The full absolute path to the node described by this path.
2019-10-29 12:36:03 +01:00
*/
2019-10-31 22:17:46 +01:00
private readonly path: string;
2019-10-29 12:36:03 +01:00
/**
2019-10-31 22:17:46 +01:00
* The absolute path to the parent node of the node described by this path.
2019-10-29 12:36:03 +01:00
*/
2019-10-31 22:17:46 +01:00
private readonly _parent: string;
2019-10-29 12:36:03 +01:00
/**
2019-10-31 22:17:46 +01:00
* The name of the node described by this path.
2019-10-29 12:36:03 +01:00
*/
2019-10-31 22:17:46 +01:00
readonly fileName: string;
2019-10-29 12:36:03 +01:00
/**
* Constructs a new path.
*
2019-10-31 22:17:46 +01:00
* @param path a string that describes the path
2019-10-29 12:36:03 +01:00
*/
2019-10-31 22:17:46 +01:00
constructor(path: string) {
this.path = `/${path}/`
.replaceAll(/\/\.\//, "/") // Replace `/./` with `/`
.replaceAll(/(\/+)([^./]+)(\/+)(\.\.)(\/+)/, "/") // Replace `/x/../` with `/`
.replaceAll(/\/{2,}/, "/") // Replace `//` with `/`
.replaceAll(/^\/\.\.\//, "/") // Replace `/../` at start with `/`
.replace(/(.)\/$/, "$1"); // Remove trailing `/` if not last character
2019-10-31 22:17:46 +01:00
const parts = this.path.split("/");
this._parent = parts.slice(0, -1).join("/");
this.fileName = parts.slice(-1).join("");
}
2019-10-31 22:17:46 +01:00
/**
* Returns the path describing the parent directory.
*
* @return the path describing the parent directory
*/
get parent(): Path {
return new Path(this._parent);
}
2019-10-29 12:36:03 +01:00
/**
2019-10-31 22:17:46 +01:00
* Returns a path describing the path to the desired child node of `this` path.
*
* @param child the path to the desired node relative to `this` path
* @return a path describing the path to the desired child node of `this` path
*/
getChild(child: string): Path {
return new Path(this.path + "/" + child);
}
/**
* Returns the string representation of this path.
2019-10-29 12:36:03 +01:00
*
2019-10-31 22:17:46 +01:00
* @return the string representation of this path
2019-10-29 12:36:03 +01:00
*/
toString(escape: boolean = false): string {
if (!escape)
return this.path;
return this.path
.replaceAll(/'/, "\\\'")
.replaceAll(/"/, "\\\"")
.replaceAll(/\s/, "\\ ");
}
}
2019-10-29 12:36:03 +01:00
/**
* A node in the file system.
*
* Nodes do not know their own name, instead they are similar to how real file systems manage nodes. The name of a node
* is determined by the directory it is in.
*/
2019-10-21 02:25:42 +02:00
export abstract class Node {
2019-10-30 20:59:31 +01:00
/**
* A string describing what kind of node this is.
*
* This string is used to determine how to deserialize a JSON string. Yes, this violates the open/closed principle.
*/
protected abstract type: string;
2019-10-29 12:36:03 +01:00
/**
* Returns a deep copy of this node.
*/
2019-10-21 02:25:42 +02:00
abstract copy(): Node;
2019-10-29 12:36:03 +01:00
/**
2019-10-31 23:22:37 +01:00
* Returns a string representation of this node given the name of and path to this node.
2019-10-29 12:36:03 +01:00
*
* @param name the name of this node
2019-10-31 23:22:37 +01:00
* @param path the path to this node
* @return a string representation of this node given the name of and path to this node
2019-10-29 12:36:03 +01:00
*/
2019-10-31 23:22:37 +01:00
abstract nameString(name: string, path: Path): string;
2019-10-29 12:36:03 +01:00
/**
* Recursively visits all nodes contained within this node.
*
* @param fun the function to apply to each node, including this node
* @param pre the function to apply to the current node before applying the first `fun`
* @param post the function to apply to the current node after applying the last `fun`
*/
2019-10-21 17:07:16 +02:00
abstract visit(fun: (node: Node) => void, pre: (node: Node) => void, post: (node: Node) => void): void;
2019-10-30 20:59:31 +01:00
/**
* Returns the JSON serialization of this node.
*
* @return the JSON serialization of this node
*/
serialize(): string {
return JSON.stringify(this);
}
/**
* Returns the JSON deserialization of the given string as a node.
*
* This method will automatically detect what kind of node is described by the string and will call the
* corresponding parse method for that type.
*
* @param json a JSON string or object describing a node
* @return the JSON deserialization of the given string as a node
*/
static deserialize(json: string | any): Node {
if (typeof json === "string") {
return this.deserialize(JSON.parse(json));
} else {
switch (json["type"]) {
case "Directory":
return Directory.parse(json);
case "File":
return File.parse(json);
default:
throw `Unknown node type \`${json["type"]}\`.`;
}
}
}
}
2019-10-29 12:36:03 +01:00
/**
* A directory that can contain other nodes.
*/
export class Directory extends Node {
2019-10-30 20:59:31 +01:00
protected type: string = "Directory";
2019-10-29 12:36:03 +01:00
/**
* The nodes contained in this directory, indexed by name.
*
* The reflexive directory (`"."`) and parent directory (`".."`) are not stored in this field.
*/
private readonly _nodes: { [name: string]: Node };
2019-10-29 12:36:03 +01:00
/**
* Constructs a new directory with the given nodes.
*
* @param nodes the nodes the directory should contain; the directory stores a shallow copy of this object
2019-10-29 12:36:03 +01:00
*/
constructor(nodes: { [name: string]: Node } = {}) {
2018-11-30 15:23:03 +01:00
super();
this._nodes = Object.assign({}, nodes);
}
2019-10-29 12:36:03 +01:00
/**
* Returns a copy of all nodes contained in this directory.
*
* @return a copy of all nodes contained in this directory
*/
get nodes(): { [name: string]: Node } {
2018-11-30 15:23:03 +01:00
return Object.assign({}, this._nodes);
}
2019-10-29 12:36:03 +01:00
/**
* Returns the number of nodes in this directory.
*
* @return the number of nodes in this directory
*/
2019-10-21 02:25:42 +02:00
get nodeCount(): number {
2018-11-30 15:23:03 +01:00
return Object.keys(this._nodes).length;
}
2019-10-21 02:25:42 +02:00
2019-10-29 12:36:03 +01:00
/**
* Returns the node with the given name.
*
* @param name the name of the node to return
* @throws when there is no node with the given name in this directory
*/
2019-10-21 02:25:42 +02:00
getNode(name: string): Node {
return this._nodes[name];
}
2019-10-31 22:17:46 +01:00
/**
* Returns `true` if and only if this directory contains a node with the given name or the name refers to this
* directory.
*
* @param name the name to check
* @return `true` if and only if this directory contains a node with the given name or the name refers to this
* directory
*/
hasNode(name: string): boolean {
if (name === "." || name === ".." || new Path(`/${name}`).toString() === "/")
return true;
return this._nodes.hasOwnProperty(name);
}
2019-10-29 12:36:03 +01:00
/**
* Adds the given node with the given name to this directory.
*
* @param name the name of the node in this directory
* @param node the node to add to this directory
*/
addNode(name: string, node: Node): void {
2018-11-30 15:23:03 +01:00
this._nodes[name] = node;
}
2019-10-29 12:36:03 +01:00
/**
* Removes the given node or the node with the given name.
*
* @param nodeOrName the node to remove or the name of the node to remove
* @throws if the given node is not contained in this directory
*/
removeNode(nodeOrName: Node | string): void {
2018-11-30 15:23:03 +01:00
if (nodeOrName instanceof Node) {
const name = Object.keys(this._nodes).find(name => this._nodes[name] === nodeOrName);
2019-10-21 17:07:16 +02:00
if (name === undefined)
throw `Could not remove node '${nodeOrName}'.`;
2018-11-30 15:23:03 +01:00
delete this._nodes[name];
} else {
2018-11-30 15:23:03 +01:00
delete this._nodes[name];
}
}
2019-10-21 02:25:42 +02:00
copy(): Directory {
2019-10-31 14:15:27 +01:00
const nodes: { [name: string]: Node } = {};
for (const name in this._nodes)
nodes[name] = this._nodes[name].copy();
return new Directory(nodes);
}
2019-10-29 12:36:03 +01:00
/**
* Returns a string that contains an HTML hyperlink that runs a command to `cd` to this directory.
*
* @param name the name of this node
2019-10-31 23:22:37 +01:00
* @param path the path to this node
2019-10-29 12:36:03 +01:00
* @return a string that contains an HTML hyperlink that runs a command to `cd` to this directory
*/
2019-10-31 23:22:37 +01:00
nameString(name: string, path: Path): string {
return `<a href="#" class="dirLink" onclick="execute('cd ${path.toString(true)}');execute('ls')">${name}/</a>`;
}
2019-10-21 02:25:42 +02:00
visit(fun: (node: Node) => void,
pre: (node: Node) => void = emptyFunction,
post: (node: Node) => void = emptyFunction) {
pre(this);
fun(this);
2019-10-21 02:25:42 +02:00
Object.keys(this._nodes).forEach(name => this._nodes[name].visit(fun, pre, post));
post(this);
}
2019-10-30 20:59:31 +01:00
/**
* Parses the given object into a directory.
*
* The nodes inside the directory of the given object are also recursively parsed by this method.
*
* @param obj the object that describes a directory
* @return the directory described by the given object
*/
static parse(obj: any): Directory {
if (obj["type"] !== "Directory")
throw `Cannot deserialize node of type \`${obj["type"]}\`.`;
const nodes: { [name: string]: Node } = {};
2019-10-30 20:59:31 +01:00
for (const name in obj["_nodes"])
if (obj["_nodes"].hasOwnProperty(name))
nodes[name] = Node.deserialize(obj["_nodes"][name]);
2019-10-30 20:59:31 +01:00
return new Directory(nodes);
2019-10-30 20:59:31 +01:00
}
}
2019-10-29 12:36:03 +01:00
/**
* A simple file without contents.
*/
export class File extends Node {
2019-10-30 20:59:31 +01:00
protected type: string = "File";
2019-10-31 01:27:31 +01:00
/**
* The link to the external resource.
*/
readonly contents: string;
2019-10-30 20:59:31 +01:00
2019-10-29 12:36:03 +01:00
/**
* Constructs a new file.
*/
2019-10-31 01:27:31 +01:00
constructor(contents: string = "") {
2018-11-30 15:23:03 +01:00
super();
2019-10-31 01:27:31 +01:00
this.contents = contents;
}
2019-10-21 02:25:42 +02:00
copy(): File {
2019-10-31 22:17:46 +01:00
return new File(this.contents);
}
2019-10-31 23:22:37 +01:00
nameString(name: string, path: Path): string {
2019-10-31 01:27:31 +01:00
const extension = getFileExtension(name);
switch (extension) {
case "lnk":
case "pdf":
return `<a href="${this.contents}" class="fileLink">${name}</a>`;
default:
return name;
}
}
2019-10-21 02:25:42 +02:00
visit(fun: (node: Node) => void,
pre: (node: Node) => void = emptyFunction,
post: (node: Node) => void = emptyFunction) {
pre(this);
fun(this);
post(this);
}
2019-10-30 20:59:31 +01:00
/**
* Parses the given object into a file.
*
* @param obj the object that describes a file
* @return the file described by the given object
*/
static parse(obj: any): File {
if (obj["type"] !== "File")
throw `Cannot deserialize node of type \`${obj["type"]}\`.`;
2019-10-31 01:27:31 +01:00
return new File(obj["contents"]);
2019-10-30 20:59:31 +01:00
}
}