forked from tools/josh
1
0
Fork 0
josh/js/fs.js

613 lines
18 KiB
JavaScript
Raw Normal View History

2018-11-28 22:23:11 +01:00
class FileSystem {
constructor() {
2018-11-28 22:41:59 +01:00
this.pwd = "/";
2018-11-30 15:23:03 +01:00
this._root = new Directory({
personal: new Directory({
2018-11-30 17:11:56 +01:00
steam: new UrlFile("https://steamcommunity.com/id/Waflix"),
nukapedia: new UrlFile("http://fallout.wikia.com/wiki/User:FDekker")
2018-11-30 15:23:03 +01:00
}),
projects: new Directory({
minor: new Directory({
2018-11-30 17:11:56 +01:00
dice: new UrlFile("https://fwdekker.com/dice")
2018-11-30 15:23:03 +01:00
}),
2018-11-30 17:11:56 +01:00
randomness: new UrlFile("https://github.com/FWDekker/intellij-randomness"),
schaapi: new UrlFile("http://cafejojo.org/schaapi")
2018-11-30 15:23:03 +01:00
}),
social: new Directory({
2018-11-30 17:11:56 +01:00
github: new UrlFile("https://github.com/FWDekker/"),
stackoverflow: new UrlFile("https://stackoverflow.com/u/3307872"),
linkedin: new UrlFile("https://www.linkedin.com/in/fwdekker/")
2018-11-30 15:23:03 +01:00
}),
2018-11-30 17:11:56 +01:00
"resume.pdf": new UrlFile("resume.pdf")
2018-11-30 15:23:03 +01:00
});
2018-11-28 22:23:11 +01:00
}
_getFile(pathString) {
2019-02-09 14:14:53 +01:00
const path = new Path(this.pwd, pathString);
2018-11-28 22:23:11 +01:00
let file = this._root;
path.parts.forEach(part => {
2018-11-28 22:41:59 +01:00
if (part === "") {
2018-11-28 22:23:11 +01:00
return;
}
if (file === undefined) {
return;
}
if (FileSystem.isFile(file)) {
file = undefined;
2018-11-30 15:23:03 +01:00
return;
}
2018-11-28 22:23:11 +01:00
file = file.getNode(part);
2018-11-28 22:23:11 +01:00
});
return file;
2018-11-28 19:51:48 +01:00
}
2018-11-28 22:23:11 +01:00
2018-11-29 13:01:55 +01:00
_executeForEach(inputs, fun) {
const outputs = [];
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
2018-11-29 13:01:55 +01:00
if (output !== "") {
outputs.push(output);
}
});
return outputs.join("\n");
2018-11-28 19:51:48 +01:00
}
2018-11-28 22:23:11 +01:00
/**
* Returns true iff {@code node} represents a directory.
2018-11-28 22:23:11 +01:00
*
* @param node {Object} a node from the file system
* @returns {boolean} true iff {@code node} represents a directory
2018-11-28 22:23:11 +01:00
*/
static isDirectory(node) {
return node instanceof Directory;
2018-11-28 19:51:48 +01:00
}
2018-11-28 22:23:11 +01:00
/**
* Returns true iff {@code node} represents a file.
2018-11-28 22:23:11 +01:00
*
* @param node {Object} an object from the file system
* @returns {boolean} true iff {@code node} represents a file
2018-11-28 22:23:11 +01:00
*/
static isFile(node) {
return node instanceof File;
2018-11-28 19:51:48 +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
2018-11-28 22:23:11 +01:00
* @returns {string} an empty string if the change was successful, or an error message explaining what went wrong
*/
cd(pathString) {
if (pathString === undefined) {
2018-11-28 22:41:59 +01:00
return "";
2018-11-28 22:23:11 +01:00
}
const path = new Path(this.pwd, pathString);
const file = this._getFile(path.path);
if (file === undefined) {
return `The directory '${path.path}' does not exist`;
2018-11-28 22:23:11 +01:00
}
if (!FileSystem.isDirectory(file)) {
return `'${path.path}' is not a directory`;
}
2018-11-28 22:23:11 +01:00
this.pwd = path.path;
2018-11-28 22:23:11 +01:00
this.files = file;
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
/**
* Creates an empty file at {@code path} if it does not exist.
*
* @param path the path to create a file at if it does not exist
* @returns {string} an empty string if the removal was successful, or a message explaining what went wrong
*/
createFile(path) {
path = new Path(this.pwd, path);
2018-11-29 20:56:27 +01:00
const headNode = this._getFile(path.head);
if (headNode === undefined) {
return `The directory '${path.head}' does not exist`;
}
if (!FileSystem.isDirectory(headNode)) {
return `${path.head} is not a directory`;
2018-11-29 20:56:27 +01:00
}
const tailNode = headNode.getNode(path.tail);
if (tailNode !== undefined) {
2018-11-29 20:56:27 +01:00
return "";
}
headNode.addNode(path.tail, new File(path.tail));
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
* @returns {string} the warnings generated during creation of the files
*/
createFiles(paths) {
return this._executeForEach(paths, path => {
return this.createFile(path);
});
}
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.
*
* @param sourceString {string} the absolute or relative path to the file or directory to copy
* @param destinationString {string} the absolute or relative path to the destination
2018-11-29 13:34:46 +01:00
* @returns {string} an empty string if the copy was successful, or a message explaining what went wrong
*/
cp(sourceString, destinationString) {
const sourcePath = new Path(this.pwd, sourceString);
const sourceTailNode = this._getFile(sourcePath.path);
2018-11-29 13:34:46 +01:00
const destinationPath = new Path(this.pwd, destinationString);
const destinationHeadNode = this._getFile(destinationPath.head);
const destinationTailNode = this._getFile(destinationPath.path);
2018-11-29 13:34:46 +01:00
if (sourceTailNode === undefined) {
return `The file '${sourcePath.path}' does not exist`;
2018-11-29 13:34:46 +01:00
}
if (!(sourceTailNode instanceof File)) {
2018-11-29 13:34:46 +01:00
return `Cannot copy directory.`;
}
if (destinationHeadNode === undefined) {
return `The directory '${destinationPath.head}' does not exist`;
2018-11-29 13:34:46 +01:00
}
let targetNode;
let targetName;
if (destinationTailNode === undefined) {
targetNode = destinationHeadNode;
targetName = destinationPath.tail;
2018-11-29 13:34:46 +01:00
} else {
targetNode = destinationTailNode;
targetName = sourcePath.tail;
2018-11-29 13:34:46 +01:00
}
if (targetNode.getNode(targetName) !== undefined) {
return `The file '${targetName}' already exists`;
}
targetNode.addNode(targetName, sourceTailNode.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
2018-11-28 22:23:11 +01:00
* @returns {Object} the directory at {@code path}, or the current directory if no path is given
*/
ls(pathString) {
const path = new Path(this.pwd, pathString);
2018-11-28 22:23:11 +01:00
const dir = this._getFile(path.path);
2018-11-30 15:23:03 +01:00
if (dir === undefined) {
return `The directory '${path.path}' does not exist`;
2018-11-29 00:05:37 +01:00
}
2018-11-30 15:23:03 +01:00
if (!FileSystem.isDirectory(dir)) {
return `'${path.path}' is not a directory`;
}
2018-11-29 00:05:37 +01:00
const dirList = ["./", "../"];
2018-11-29 00:05:37 +01:00
const fileList = [];
2018-11-30 15:23:03 +01:00
const nodes = dir.getNodes();
Object.keys(nodes)
.sortAlphabetically()
.forEach(name => {
const node = nodes[name];
if (FileSystem.isDirectory(node)) {
2018-12-04 12:54:04 +01:00
dirList.push(node.nameString(name));
2018-11-30 15:23:03 +01:00
} else if (FileSystem.isFile(node)) {
2018-12-04 12:54:04 +01:00
fileList.push(node.nameString(name));
2018-11-30 15:23:03 +01:00
} else {
throw `${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
2018-11-28 22:23:11 +01:00
* @returns {string} an empty string if the removal was successful, or a message explaining what went wrong
*/
mkdir(pathString) {
const path = new Path(pathString);
2018-11-28 22:23:11 +01:00
const headNode = this._getFile(path.head);
if (headNode === undefined) {
return `The directory '${path.head}' does not exist`;
2018-11-28 22:23:11 +01:00
}
if (!FileSystem.isDirectory(headNode)) {
return `'${path.head}' is not a directory`;
}
if (headNode.getNode(path.tail)) {
return `The directory '${path.tail}' already exists`;
2018-11-28 22:23:11 +01:00
}
2018-11-28 19:51:48 +01:00
headNode.addNode(path.tail, 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
* @returns {string} the warnings generated during creation of the directories
*/
mkdirs(paths) {
2018-11-29 09:37:42 +01:00
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
2018-11-29 13:34:46 +01:00
* @returns {string} an empty string if the move was successful, or a message explaining what went wrong
2018-11-29 13:18:48 +01:00
*/
mv(sourceString, destinationString) {
const sourcePath = new Path(sourceString);
const sourceHeadNode = this._getFile(sourcePath.head);
const sourceTailNode = this._getFile(sourcePath.path);
const destinationPath = new Path(destinationString);
const destinationHeadNode = this._getFile(destinationPath.head);
const destinationTailNode = this._getFile(destinationPath.path);
if (sourceTailNode === undefined) {
return `The file '${sourcePath.path}' does not exist`;
2018-11-29 13:34:46 +01:00
}
if (destinationHeadNode === undefined) {
return `The directory '${destinationPath.head}' does not exist`;
2018-11-29 13:18:48 +01:00
}
2018-11-29 13:34:46 +01:00
let targetNode;
let targetName;
if (destinationTailNode === undefined) {
targetNode = destinationHeadNode;
targetName = destinationPath.tail;
2018-11-29 13:18:48 +01:00
} else {
targetNode = destinationTailNode;
targetName = sourcePath.tail;
2018-11-29 13:34:46 +01:00
}
if (targetNode.getNode(targetName) !== undefined) {
return `The file '${targetName}' already exists`;
2018-11-29 13:18:48 +01:00
}
sourceHeadNode.removeNode(sourceTailNode);
targetNode.addNode(sourceTailNode);
sourceTailNode.name = targetName;
2018-11-29 13:34:46 +01:00
2018-11-29 13:18:48 +01:00
return "";
}
2018-11-28 22:23:11 +01:00
/**
* Resets navigation in the file system.
*/
reset() {
2018-11-28 22:41:59 +01:00
this.pwd = "/";
2018-11-28 22:23:11 +01:00
this.files = this._root;
}
2018-11-28 19:51: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
2018-11-28 22:23:11 +01:00
* @returns {string} an empty string if the removal was successful, or a message explaining what went wrong
*/
rm(pathString, force = false, recursive = false, noPreserveRoot = false) {
const path = new Path(pathString);
2018-11-28 22:23:11 +01:00
const parentNode = this._getFile(path.head);
2018-11-29 13:50:36 +01:00
if (parentNode === undefined) {
2018-11-29 09:37:42 +01:00
return force
? ""
: `The directory '${path.head}' does not exist`;
2018-11-28 22:23:11 +01:00
}
2018-11-29 13:50:36 +01:00
if (!FileSystem.isDirectory(parentNode)) {
return force
? ""
: `'${path.head}' is not a directory`;
}
2018-11-28 19:51:48 +01:00
const childNode = this._getFile(path.path);
2018-11-29 13:50:36 +01:00
if (childNode === undefined) {
return force
? ""
: `The file '${path.path}' does not exist`;
}
2018-11-28 19:51:48 +01:00
2018-11-29 13:50:36 +01:00
if (recursive) {
if (path.path === "/") {
2018-11-29 13:50:36 +01:00
if (noPreserveRoot) {
2018-11-30 15:23:03 +01:00
this._root = new Directory();
2018-11-29 13:50:36 +01:00
} else {
return "'/' cannot be removed";
}
} else {
parentNode.removeNode(childNode);
}
} else {
if (!(childNode instanceof File)) {
return force
? ""
: `'${path.tail}' 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
2018-11-29 13:01:55 +01:00
* @returns {string} the warnings generated during removal of the directories
*/
2018-11-29 13:50:36 +01:00
rms(paths, force = false, recursive = false, noPreserveRoot = false) {
2018-11-29 09:37:42 +01:00
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.
*
* @param pathString {string} the absolute or relative path to the directory to be removed
2018-11-28 22:23:11 +01:00
* @returns {string} an empty string if the removal was successful, or a message explaining what went wrong
*/
rmdir(pathString) {
const path = new Path(pathString);
if (path.path === "/") {
2018-11-29 13:38:00 +01:00
if (this._root.getNodeCount() > 0) {
2018-11-29 00:21:35 +01:00
return `The directory is not empty.`;
} else {
return "";
2018-11-29 00:21:35 +01:00
}
}
const parentDir = this._getFile(path.head);
if (parentDir === undefined) {
return `The directory '${path.head}' does not exist`;
2018-11-28 19:51:48 +01:00
}
if (!FileSystem.isDirectory(parentDir)) {
return `'${path.head}' is not a directory`;
}
2018-11-28 22:23:11 +01:00
const childDir = parentDir.getNode(path.tail);
if (childDir === undefined) {
return `The directory '${path.tail}' does not exist`;
2018-11-28 22:23:11 +01:00
}
if (!FileSystem.isDirectory(childDir)) {
return `'${path.tail}' is not a directory`;
}
2018-11-29 13:38:00 +01:00
if (childDir.getNodeCount() > 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
* @returns {string} the warnings generated during removal of the directories
*/
2018-11-29 13:38:00 +01:00
rmdirs(paths) {
2018-11-29 09:37:42 +01:00
return this._executeForEach(paths, path => {
2018-11-29 13:38:00 +01:00
return this.rmdir(path);
2018-11-29 09:37:42 +01:00
});
}
2018-11-28 22:23:11 +01:00
}
2018-11-29 00:05:37 +01:00
class Path {
constructor(currentPath, relativePath) {
let path;
if (relativePath === undefined) {
path = currentPath;
} else if (relativePath.startsWith("/")) {
path = relativePath;
} else {
path = `${currentPath}/${relativePath}`;
}
this._path = `${path}/`
.replaceAll(/\/\.\//, "/")
.replaceAll(/(\/+)([^./]+)(\/+)(\.\.)(\/+)/, "/")
.replaceAll(/\/{2,}/, "/")
.replace(/^\.?\.\/$/, "/")
.toString();
this._parts = this._path.split("/");
this._head = this._parts.slice(0, -2).join("/");
this._tail = this._parts.slice(0, -1).slice(-1).join("/");
}
get path() {
return this._path;
}
get parts() {
return this._parts.slice();
}
get head() {
return this._head;
}
get tail() {
return this._tail;
}
}
class Node {
copy() {
throw "Cannot execute abstract method!";
}
2018-12-04 12:54:04 +01:00
nameString(name) {
throw "Cannot execute abstract method!";
}
visit(fun, pre, post) {
throw "Cannot execute abstract method!";
}
}
class Directory extends Node {
2018-11-30 15:23:03 +01:00
constructor(nodes = {}) {
super();
2018-11-30 15:23:03 +01:00
this._parent = this;
this._nodes = nodes;
2018-11-30 15:23:03 +01:00
Object.keys(this._nodes).forEach(name => {
this._nodes[name]._parent = this
});
}
getNodes() {
2018-11-30 15:23:03 +01:00
return Object.assign({}, this._nodes);
}
getNodeCount() {
2018-11-30 15:23:03 +01:00
return Object.keys(this._nodes).length;
}
getNode(name) {
switch (name) {
case ".":
return this;
case "..":
return this._parent;
default:
2018-11-30 15:23:03 +01:00
return this._nodes[name];
}
}
2018-11-30 15:23:03 +01:00
addNode(name, node) {
2018-11-29 13:34:46 +01:00
if (node instanceof Directory) {
node._parent = this;
}
2018-11-30 15:23:03 +01:00
this._nodes[name] = node;
}
2018-11-30 15:23:03 +01:00
removeNode(nodeOrName) {
if (nodeOrName instanceof Node) {
const name = Object.keys(this._nodes).find(key => this._nodes[key] === nodeOrName);
delete this._nodes[name];
} else {
2018-11-30 15:23:03 +01:00
delete this._nodes[name];
}
}
copy() {
2018-11-30 15:23:03 +01:00
const copy = new Directory(Object.assign({}, this._nodes));
copy._parent = undefined;
return copy;
}
2018-12-04 12:54:04 +01:00
nameString(name) {
2018-11-30 15:23:03 +01:00
return `${name}/`;
}
visit(fun, pre = emptyFunction, post = emptyFunction) {
pre(this);
fun(this);
2018-11-30 15:23:03 +01:00
Object.keys(this._nodes).forEach(name => {
this._nodes[name].visit(fun, pre, post);
});
post(this);
}
}
class File extends Node {
2018-11-30 15:23:03 +01:00
constructor() {
super();
}
copy() {
2018-11-30 15:23:03 +01:00
return new File();
}
2018-12-04 12:54:04 +01:00
nameString(name) {
2018-11-30 15:23:03 +01:00
return name;
}
visit(fun, pre = emptyFunction, post = emptyFunction) {
pre(this);
fun(this);
post(this);
}
}
2018-11-30 17:11:56 +01:00
class UrlFile extends File {
2018-11-30 15:23:03 +01:00
constructor(url) {
super();
this.url = url;
2018-11-29 00:05:37 +01:00
}
copy() {
2018-11-30 17:11:56 +01:00
return new UrlFile(this.url);
}
2018-12-04 12:54:04 +01:00
nameString(name) {
2018-11-30 15:23:03 +01:00
return `<a href="${this.url}">${name}</a>`;
}
}