2018-11-28 22:23:11 +01:00
|
|
|
class FileSystem {
|
|
|
|
constructor() {
|
|
|
|
this._root = {
|
|
|
|
personal: {
|
2018-11-29 00:05:37 +01:00
|
|
|
steam: {
|
|
|
|
type: "link",
|
|
|
|
link: "https://steamcommunity.com/id/Waflix"
|
|
|
|
},
|
|
|
|
nukapedia: {
|
|
|
|
type: "link",
|
|
|
|
link: "http://fallout.wikia.com/wiki/User:FDekker"
|
|
|
|
}
|
2018-11-28 22:23:11 +01:00
|
|
|
},
|
|
|
|
projects: {
|
|
|
|
minor: {
|
2018-11-29 00:05:37 +01:00
|
|
|
dice: {
|
|
|
|
type: "link",
|
|
|
|
link: "https://fwdekker.com/dice"
|
|
|
|
}
|
|
|
|
},
|
|
|
|
randomness: {
|
|
|
|
type: "link",
|
|
|
|
link: "https://github.com/FWDekker/intellij-randomness"
|
2018-11-28 22:23:11 +01:00
|
|
|
},
|
2018-11-29 00:05:37 +01:00
|
|
|
schaapi: {
|
|
|
|
type: "link",
|
|
|
|
link: "http://cafejojo.org/schaapi"
|
|
|
|
}
|
2018-11-28 22:23:11 +01:00
|
|
|
},
|
|
|
|
social: {
|
2018-11-29 00:05:37 +01:00
|
|
|
github: {
|
|
|
|
type: "link",
|
|
|
|
link: "https://github.com/FWDekker/"
|
|
|
|
},
|
|
|
|
stackoverflow: {
|
|
|
|
type: "link",
|
|
|
|
link: "https://stackoverflow.com/u/3307872"
|
|
|
|
},
|
|
|
|
linkedin: {
|
|
|
|
type: "link",
|
|
|
|
link: "https://www.linkedin.com/in/fwdekker/"
|
|
|
|
}
|
2018-11-28 22:23:11 +01:00
|
|
|
},
|
2018-11-29 00:05:37 +01:00
|
|
|
"resume.pdf": {
|
|
|
|
type: "link",
|
|
|
|
link: "https://fwdekker.com/resume.pdf"
|
|
|
|
}
|
2018-11-28 22:23:11 +01:00
|
|
|
};
|
2018-11-28 22:41:59 +01:00
|
|
|
this.pwd = "/";
|
2018-11-28 22:23:11 +01:00
|
|
|
|
|
|
|
|
|
|
|
const visited = [];
|
|
|
|
const queue = [this._root];
|
|
|
|
|
2018-11-28 22:41:59 +01:00
|
|
|
this._root["."] = this._root;
|
2018-11-29 10:21:48 +01:00
|
|
|
this._root[".."] = this._root;
|
2018-11-28 22:23:11 +01:00
|
|
|
|
|
|
|
while (queue.length !== 0) {
|
|
|
|
const next = queue.pop();
|
|
|
|
if (visited.indexOf(next) >= 0) {
|
|
|
|
continue;
|
|
|
|
}
|
2018-11-28 19:51:48 +01:00
|
|
|
|
2018-11-28 22:23:11 +01:00
|
|
|
visited.push(next);
|
|
|
|
for (const key in next) {
|
2018-11-28 22:41:59 +01:00
|
|
|
if (key === "." || key === ".." || FileSystem.isFile(next[key])) {
|
2018-11-28 22:23:11 +01:00
|
|
|
continue;
|
|
|
|
}
|
2018-11-28 19:51:48 +01:00
|
|
|
|
2018-11-28 22:41:59 +01:00
|
|
|
next[key]["."] = next[key];
|
|
|
|
next[key][".."] = next;
|
2018-11-28 22:23:11 +01:00
|
|
|
queue.push(next[key]);
|
|
|
|
}
|
2018-11-28 19:51:48 +01:00
|
|
|
}
|
2018-11-28 22:23:11 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
_absolutePath(path) {
|
2018-11-28 22:41:59 +01:00
|
|
|
if (path.startsWith("/")) {
|
2018-11-28 22:23:11 +01:00
|
|
|
return path;
|
|
|
|
} else {
|
|
|
|
return `${this.pwd}/${path}`;
|
2018-11-28 19:51:48 +01:00
|
|
|
}
|
2018-11-28 22:23:11 +01:00
|
|
|
}
|
2018-11-28 19:51:48 +01:00
|
|
|
|
2018-11-28 22:41:59 +01:00
|
|
|
_childPath(path) {
|
2018-11-29 10:16:31 +01:00
|
|
|
const childPath = this._normalisePath(path).split("/").slice(0, -1).slice(-1).join("/");
|
|
|
|
return (childPath === "")
|
|
|
|
? "."
|
|
|
|
: childPath;
|
2018-11-28 19:51:48 +01:00
|
|
|
}
|
|
|
|
|
2018-11-29 09:37:42 +01:00
|
|
|
_executeForEach(inputs, fun) {
|
|
|
|
const outputs = [];
|
|
|
|
|
|
|
|
inputs.forEach(input => {
|
|
|
|
const output = fun(input);
|
|
|
|
|
|
|
|
if (output !== "") {
|
|
|
|
outputs.push(output);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
return outputs.join("\n");
|
|
|
|
}
|
|
|
|
|
2018-11-28 22:23:11 +01:00
|
|
|
_getFile(path) {
|
|
|
|
path = this._normalisePath(path);
|
|
|
|
|
|
|
|
let file = this._root;
|
2018-11-28 22:41:59 +01:00
|
|
|
path.split("/").forEach(part => {
|
|
|
|
if (part === "") {
|
2018-11-28 22:23:11 +01:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (file === undefined) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
file = file[part];
|
|
|
|
});
|
|
|
|
|
|
|
|
return file;
|
2018-11-28 19:51:48 +01:00
|
|
|
}
|
|
|
|
|
2018-11-28 22:23:11 +01:00
|
|
|
_normalisePath(path) {
|
|
|
|
return FileSystem._sanitisePath(this._absolutePath(path));
|
2018-11-28 19:51:48 +01:00
|
|
|
}
|
2018-11-28 22:23:11 +01:00
|
|
|
|
|
|
|
_parentPath(path) {
|
2018-11-29 10:16:31 +01:00
|
|
|
const parentPath = this._normalisePath(path).split("/").slice(0, -2).join("/");
|
|
|
|
return (parentPath === "")
|
|
|
|
? "/"
|
|
|
|
: parentPath;
|
2018-11-28 19:51:48 +01:00
|
|
|
}
|
|
|
|
|
2018-11-28 22:23:11 +01:00
|
|
|
static _sanitisePath(path) {
|
2018-11-28 22:41:59 +01:00
|
|
|
const selfRegex = /\/\.\//; // Match "./"
|
|
|
|
const upRegex = /(\/+)([^./]+)(\/+)(\.\.)(\/+)/; // Match "/directory/../"
|
|
|
|
const doubleRegex = /\/{2,}/; // Match "///"
|
2018-11-28 22:23:11 +01:00
|
|
|
|
2018-11-28 23:22:17 +01:00
|
|
|
return `${path}/`
|
2018-11-28 22:41:59 +01:00
|
|
|
.replaceAll(selfRegex, "/")
|
|
|
|
.replaceAll(upRegex, "/")
|
|
|
|
.replaceAll(doubleRegex, "/")
|
2018-11-28 22:23:11 +01:00
|
|
|
.toString();
|
2018-11-28 19:51:48 +01:00
|
|
|
}
|
|
|
|
|
2018-11-28 22:23:11 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns true iff {@code file} represents a directory.
|
|
|
|
*
|
|
|
|
* @param file {Object} an object from the file system
|
|
|
|
* @returns {boolean} true iff {@code file} represents a directory
|
|
|
|
*/
|
|
|
|
static isDirectory(file) {
|
2018-11-29 00:05:37 +01:00
|
|
|
return (file !== undefined && typeof file.type !== "string");
|
2018-11-28 19:51:48 +01:00
|
|
|
}
|
|
|
|
|
2018-11-28 22:23:11 +01:00
|
|
|
/**
|
|
|
|
* Returns true iff {@code file} represents a file.
|
|
|
|
*
|
|
|
|
* @param file {Object} an object from the file system
|
|
|
|
* @returns {boolean} true iff {@code file} represents a file
|
|
|
|
*/
|
|
|
|
static isFile(file) {
|
2018-11-29 00:05:37 +01:00
|
|
|
return (file !== undefined && typeof file.type === "string");
|
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 path the absolute or relative path to change the current directory to
|
|
|
|
* @returns {string} an empty string if the change was successful, or an error message explaining what went wrong
|
|
|
|
*/
|
|
|
|
cd(path) {
|
|
|
|
if (path === undefined) {
|
2018-11-28 22:41:59 +01:00
|
|
|
return "";
|
2018-11-28 22:23:11 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
const file = this._getFile(path);
|
|
|
|
if (file === undefined || !FileSystem.isDirectory(file)) {
|
|
|
|
return `The directory '${path}' does not exist`;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.pwd = this._normalisePath(path);
|
|
|
|
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
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the directory at {@code path}, or the current directory if no path is given.
|
|
|
|
*
|
|
|
|
* @param path {string} the absolute or relative path to the directory to return
|
|
|
|
* @returns {Object} the directory at {@code path}, or the current directory if no path is given
|
|
|
|
*/
|
|
|
|
ls(path) {
|
|
|
|
path = (path || this.pwd);
|
|
|
|
|
2018-11-29 00:05:37 +01:00
|
|
|
const files = this._getFile(path);
|
|
|
|
if (files === undefined) {
|
|
|
|
return `The directory '${path}' does not exist`;
|
|
|
|
}
|
|
|
|
|
|
|
|
const dirList = [];
|
|
|
|
const fileList = [];
|
|
|
|
|
|
|
|
Object.keys(files).sort().forEach(fileName => {
|
|
|
|
const file = files[fileName];
|
|
|
|
|
|
|
|
if (FileSystem.isFile(file)) {
|
|
|
|
fileList.push(fileToString(fileName, file));
|
|
|
|
} else if (FileSystem.isDirectory(file)) {
|
|
|
|
dirList.push(`${fileName}/`);
|
|
|
|
} else {
|
|
|
|
throw `${fileName} is neither a file nor a directory!`;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
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 path {string} the absolute or relative path to the directory to create
|
|
|
|
* @returns {string} an empty string if the removal was successful, or a message explaining what went wrong
|
|
|
|
*/
|
|
|
|
mkdir(path) {
|
|
|
|
const parentDirName = this._parentPath(path);
|
2018-11-28 22:41:59 +01:00
|
|
|
const childDirName = this._childPath(path);
|
2018-11-28 22:23:11 +01:00
|
|
|
|
|
|
|
const parentDir = this._getFile(parentDirName);
|
|
|
|
if (!FileSystem.isDirectory(parentDir)) {
|
|
|
|
return `The directory '${parentDirName}' does not exist`;
|
|
|
|
}
|
|
|
|
if (parentDir[childDirName] !== undefined) {
|
|
|
|
return `The directory '${childDirName}' already exists`;
|
|
|
|
}
|
2018-11-28 19:51:48 +01:00
|
|
|
|
2018-11-28 22:23:11 +01:00
|
|
|
parentDir[childDirName] = {};
|
2018-11-28 22:41:59 +01:00
|
|
|
parentDir[childDirName]["."] = parentDir[childDirName];
|
|
|
|
parentDir[childDirName][".."] = parentDir;
|
|
|
|
return "";
|
2018-11-28 22:23:11 +01:00
|
|
|
}
|
2018-11-28 19:51:48 +01:00
|
|
|
|
2018-11-29 09:08:24 +01:00
|
|
|
mkdirs(paths) {
|
2018-11-29 09:37:42 +01:00
|
|
|
return this._executeForEach(paths, this.mkdir.bind(this));
|
2018-11-29 09:08:24 +01:00
|
|
|
}
|
|
|
|
|
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 path {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-28 22:23:11 +01:00
|
|
|
* @returns {string} an empty string if the removal was successful, or a message explaining what went wrong
|
|
|
|
*/
|
2018-11-29 09:37:42 +01:00
|
|
|
rm(path, force) {
|
2018-11-28 22:23:11 +01:00
|
|
|
const dirName = this._parentPath(path);
|
2018-11-28 22:41:59 +01:00
|
|
|
const fileName = this._childPath(path);
|
2018-11-28 22:23:11 +01:00
|
|
|
|
|
|
|
const dir = this._getFile(dirName);
|
|
|
|
if (!FileSystem.isDirectory(dir)) {
|
2018-11-29 09:37:42 +01:00
|
|
|
return force
|
|
|
|
? ""
|
|
|
|
: `The directory '${dirName}' does not exist`;
|
2018-11-28 22:23:11 +01:00
|
|
|
}
|
2018-11-28 19:51:48 +01:00
|
|
|
|
2018-11-28 22:23:11 +01:00
|
|
|
const file = dir[fileName];
|
|
|
|
if (!FileSystem.isFile(file)) {
|
2018-11-29 09:37:42 +01:00
|
|
|
return force
|
|
|
|
? ""
|
|
|
|
: `The file '${fileName}' does not exist`;
|
2018-11-28 19:51:48 +01:00
|
|
|
}
|
|
|
|
|
2018-11-28 22:23:11 +01:00
|
|
|
delete dir[fileName];
|
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 09:37:42 +01:00
|
|
|
rms(paths, force) {
|
|
|
|
return this._executeForEach(paths, path => {
|
|
|
|
this.rm(path, force);
|
|
|
|
});
|
2018-11-29 09:20:23 +01:00
|
|
|
}
|
|
|
|
|
2018-11-28 22:23:11 +01:00
|
|
|
/**
|
|
|
|
* Removes a directory from the file system.
|
|
|
|
*
|
|
|
|
* @param path {string} the absolute or relative path to the directory to be removed
|
|
|
|
* @param force {boolean} true iff the directory should be removed regardless of whether it is empty
|
|
|
|
* @returns {string} an empty string if the removal was successful, or a message explaining what went wrong
|
|
|
|
*/
|
|
|
|
rmdir(path, force) {
|
|
|
|
force = (force || false);
|
|
|
|
|
2018-11-29 00:21:35 +01:00
|
|
|
if (this._normalisePath(path) === "/") {
|
|
|
|
if (!force && Object.keys(this._root).length > 1) {
|
|
|
|
return `The directory is not empty.`;
|
|
|
|
} else {
|
|
|
|
this._root = {};
|
|
|
|
this._root["."] = this._root;
|
|
|
|
return ``;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-11-28 22:23:11 +01:00
|
|
|
const parentDirName = this._parentPath(path);
|
2018-11-28 22:41:59 +01:00
|
|
|
const childDirName = this._childPath(path);
|
2018-11-28 22:23:11 +01:00
|
|
|
|
|
|
|
const parentDir = this._getFile(parentDirName);
|
|
|
|
if (!FileSystem.isDirectory(parentDir)) {
|
2018-11-29 09:37:42 +01:00
|
|
|
return force
|
|
|
|
? ""
|
|
|
|
: `The directory '${parentDirName}' does not exist`;
|
2018-11-28 19:51:48 +01:00
|
|
|
}
|
2018-11-28 22:23:11 +01:00
|
|
|
|
|
|
|
const childDir = parentDir[childDirName];
|
|
|
|
if (!FileSystem.isDirectory(childDir)) {
|
2018-11-29 09:37:42 +01:00
|
|
|
return force
|
|
|
|
? ""
|
|
|
|
: `The directory '${childDirName}' does not exist`;
|
2018-11-28 22:23:11 +01:00
|
|
|
}
|
|
|
|
if (!force && Object.keys(childDir).length > 2) {
|
|
|
|
return `The directory is not empty`;
|
|
|
|
}
|
|
|
|
|
|
|
|
delete parentDir[childDirName];
|
2018-11-28 22:41:59 +01:00
|
|
|
return "";
|
2018-11-28 19:51:48 +01:00
|
|
|
}
|
2018-11-29 09:08:24 +01:00
|
|
|
|
|
|
|
rmdirs(paths, force) {
|
2018-11-29 09:37:42 +01:00
|
|
|
return this._executeForEach(paths, path => {
|
|
|
|
return this.rmdir(path, force);
|
|
|
|
});
|
2018-11-29 09:08:24 +01:00
|
|
|
}
|
2018-11-28 22:23:11 +01:00
|
|
|
}
|
2018-11-29 00:05:37 +01:00
|
|
|
|
|
|
|
|
2018-11-29 00:21:35 +01:00
|
|
|
const fileToString = function (fileName, file) {
|
2018-11-29 00:05:37 +01:00
|
|
|
switch (file.type) {
|
|
|
|
case "link":
|
|
|
|
return `<a href="${file.link}">${fileName}</a>`;
|
|
|
|
default:
|
|
|
|
return fileName;
|
|
|
|
}
|
|
|
|
};
|