From f8fd158135632c2f33b20c9fc76655c038cef503 Mon Sep 17 00:00:00 2001 From: Felix Dekker Date: Wed, 28 Nov 2018 22:23:11 +0100 Subject: [PATCH] Classify FileSystem --- js/commands.js | 46 ++--- js/fs.js | 468 ++++++++++++++++++++++++------------------------- js/shared.js | 8 + js/terminal.js | 24 ++- 4 files changed, 273 insertions(+), 273 deletions(-) diff --git a/js/commands.js b/js/commands.js index d23adf8..91ec398 100644 --- a/js/commands.js +++ b/js/commands.js @@ -1,14 +1,17 @@ class Commands { - constructor() { + constructor(terminal, fileSystem) { + this._terminal = terminal; + this._fileSystem = fileSystem; + this._list = { clear: { - fun: Commands.clear, + fun: this.clear, summary: `clear terminal output`, usage: `clear`, desc: `Clears all previous terminal output.`.trimLines() }, cd: { - fun: Commands.cd, + fun: this.cd, summary: `change directory`, usage: `cd [DIRECTORY]`, desc: "" + @@ -44,25 +47,25 @@ class Commands { If [DIRECTORY] is empty, the files and directories in the current working directory are shown.`.trimLines() }, mkdir: { - fun: Commands.mkdir, + fun: this.mkdir, summary: `create directory`, usage: `mkdir [DIRECTORY]`, desc: `Creates a directory with name [DIRECTORY].`.trimLines() }, pwd: { - fun: Commands.pwd, + fun: this.pwd, summary: `print working directory`, usage: `pwd`, desc: `Displays the current working directory.`.trimLines() }, rm: { - fun: Commands.rm, + fun: this.rm, summary: `remove file`, usage: `rm [-f | --force] FILE`, desc: `Removes FILE if it is a file.`.trimLines() }, rmdir: { - fun: Commands.rmdir, + fun: this.rmdir, summary: `remove directory`, usage: `rmdir [-f | --force] DIR`, desc: `Removes DIR if it is a directory.`.trimLines() @@ -85,12 +88,12 @@ class Commands { } - static cd(args) { - return fs.cd(args[1]); + cd(args) { + return this._fileSystem.cd(args[1]); } - static clear() { - Commands.clear(); + clear() { + this._terminal.clear(); return ``; } @@ -135,7 +138,7 @@ class Commands { } ls(args) { - const files = fs.ls(args[1]); + const files = this._fileSystem.ls(args[1]); if (files === undefined) { return `The directory '${args[1]}' does not exist`; } @@ -156,19 +159,19 @@ class Commands { return dirList.concat(fileList).join(`\n`); } - static mkdir(args) { - return fs.mkdir(args[1]); + mkdir(args) { + return this._fileSystem.mkdir(args[1]); } - static pwd() { - return fs.pwd; + pwd() { + return this._fileSystem.pwd; } - static rm(args) { - return fs.rm(args[1]); + rm(args) { + return this._fileSystem.rm(args[1]); } - static rmdir(args) { + rmdir(args) { let path; let force; if (args[1] === `-f` || args[1] === `--force`) { @@ -179,9 +182,6 @@ class Commands { force = false; } - return fs.rmdir(path, force); + return this._fileSystem.rmdir(path, force); } } - - -const commands = new Commands(); diff --git a/js/fs.js b/js/fs.js index c82c69b..54ccd8f 100644 --- a/js/fs.js +++ b/js/fs.js @@ -1,245 +1,241 @@ -const fs = {}; +class FileSystem { + constructor() { + this._root = { + personal: { + steam: `steam`, + nukapedia: `nukapedia` + }, + projects: { + minor: { + dice: `dice_probabilities` + }, + randomness: `randomness`, + schaapi: `schaapi` + }, + social: { + github: `github`, + stackoverflow: `stackoverflow`, + linkedin: `linkedin` + }, + "resume.pdf": `resume.pdf` + }; + this.pwd = `/`; -// Layout -fs.pwd = `/`; -fs.root = { - personal: { - steam: `steam`, - nukapedia: `nukapedia` - }, - projects: { - minor: { - dice: `dice_probabilities` - }, - randomness: `randomness`, - schaapi: `schaapi` - }, - social: { - github: `github`, - stackoverflow: `stackoverflow`, - linkedin: `linkedin` - }, - "resume.pdf": `resume.pdf` -}; -fs.files = fs.root; + const visited = []; + const queue = [this._root]; + this._root[`.`] = this._root; -// Functions -fs._absolutePath = function (path) { - if (path.startsWith(`/`)) { - return path; - } else { - return `${fs.pwd}/${path}`; - } -}; - -fs._filePath = function (path) { - return fs._normalisePath(path).split(`/`).slice(0, -1).slice(-1).join(`/`); -}; - -fs._getFile = function (path) { - const abPath = fs._normalisePath(path); - - let file = fs.root; - abPath.split(`/`).forEach(part => { - if (part === ``) { - return; - } - if (file === undefined) { - return; - } - - file = file[part]; - }); - - return file; -}; - -fs._normalisePath = function (path) { - return fs._sanitisePath(fs._absolutePath(path)); -}; - -fs._parentPath = function (path) { - return fs._normalisePath(path).split(`/`).slice(0, -1).join(`/`); -}; - -fs._sanitisePath = function (path) { - const selfRegex = /\/\.\//; // Match `./` - const upRegex = /(\/+)([^./]+)(\/+)(\.\.)(\/+)/; // Match `/directory/../ - const doubleRegex = /\/{2,}/; // Match `///` - - return path - .replaceAll(selfRegex, `/`) - .replaceAll(upRegex, `/`) - .replaceAll(doubleRegex, `/`) - .toString(); -}; - - -/** - * 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 - */ -fs.isDirectory = function (file) { - return (file !== undefined && typeof file !== `string`); -}; - -/** - * 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 - */ -fs.isFile = function (file) { - return (file !== undefined && typeof file === `string`); -}; - -/** - * 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 - */ -fs.cd = function (path) { - if (path === undefined) { - return ``; - } - - const file = fs._getFile(path); - if (file === undefined || !fs.isDirectory(file)) { - return `The directory '${path}' does not exist`; - } - - fs.pwd = fs._normalisePath(path); - fs.files = file; - - return ``; -}; - -/** - * 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 - */ -fs.ls = function (path) { - path = (path || fs.pwd); - - return fs._getFile(path); -}; - -/** - * 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 - */ -fs.mkdir = function (path) { - const parentDirName = fs._parentPath(path); - const childDirName = fs._filePath(path); - - const parentDir = fs._getFile(parentDirName); - if (!fs.isDirectory(parentDir)) { - return `The directory '${parentDirName}' does not exist`; - } - if (parentDir[childDirName] !== undefined) { - return `The directory '${childDirName}' already exists`; - } - - parentDir[childDirName] = {}; - parentDir[childDirName][`.`] = parentDir[childDirName]; - parentDir[childDirName][`..`] = parentDir; - return ``; -}; - -/** - * Resets navigation in the file system. - */ -fs.reset = function () { - fs.pwd = `/`; - fs.files = fs.root; -}; - -/** - * Removes a file from the file system. - * - * @param path {string} the absolute or relative path to the file to be removed - * @returns {string} an empty string if the removal was successful, or a message explaining what went wrong - */ -fs.rm = function (path) { - const dirName = fs._parentPath(path); - const fileName = fs._filePath(path); - - const dir = fs._getFile(dirName); - if (!fs.isDirectory(dir)) { - return `The directory '${dirName}' does not exist`; - } - - const file = dir[fileName]; - if (!fs.isFile(file)) { - return `The file '${fileName}' does not exist`; - } - - delete dir[fileName]; - return ``; -}; - -/** - * 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 - */ -fs.rmdir = function (path, force) { - force = (force || false); - - const parentDirName = fs._parentPath(path); - const childDirName = fs._filePath(path); - - const parentDir = fs._getFile(parentDirName); - if (!fs.isDirectory(parentDir)) { - return `The directory '${parentDirName}' does not exist`; - } - - const childDir = parentDir[childDirName]; - if (!fs.isDirectory(childDir)) { - return `The directory '${childDirName}' does not exist`; - } - if (!force && Object.keys(childDir).length > 2) { - return `The directory is not empty`; - } - - delete parentDir[childDirName]; - return ``; -}; - - -// Init -(() => { - const visited = []; - const queue = [fs.files]; - - fs.files[`.`] = fs.files; - - while (queue.length !== 0) { - const next = queue.pop(); - if (visited.indexOf(next) >= 0) { - continue; - } - - visited.push(next); - for (const key in next) { - if (key === `.` || key === `..`) { + while (queue.length !== 0) { + const next = queue.pop(); + if (visited.indexOf(next) >= 0) { continue; } - next[key][`.`] = next[key]; - next[key][`..`] = next; - queue.push(next[key]); + visited.push(next); + for (const key in next) { + if (key === `.` || key === `..` || FileSystem.isFile(next[key])) { + continue; + } + + next[key][`.`] = next[key]; + next[key][`..`] = next; + queue.push(next[key]); + } } } -})(); + + + _absolutePath(path) { + if (path.startsWith(`/`)) { + return path; + } else { + return `${this.pwd}/${path}`; + } + } + + _filePath(path) { + return this._normalisePath(path).split(`/`).slice(0, -1).slice(-1).join(`/`); + } + + _getFile(path) { + path = this._normalisePath(path); + + let file = this._root; + path.split(`/`).forEach(part => { + if (part === ``) { + return; + } + if (file === undefined) { + return; + } + + file = file[part]; + }); + + return file; + } + + _normalisePath(path) { + return FileSystem._sanitisePath(this._absolutePath(path)); + } + + _parentPath(path) { + return this._normalisePath(path).split(`/`).slice(0, -1).join(`/`); + } + + static _sanitisePath(path) { + const selfRegex = /\/\.\//; // Match `./` + const upRegex = /(\/+)([^./]+)(\/+)(\.\.)(\/+)/; // Match `/directory/../ + const doubleRegex = /\/{2,}/; // Match `///` + + return path + .replaceAll(selfRegex, `/`) + .replaceAll(upRegex, `/`) + .replaceAll(doubleRegex, `/`) + .toString(); + } + + + /** + * 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) { + return (file !== undefined && typeof file !== `string`); + } + + /** + * 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) { + return (file !== undefined && typeof file === `string`); + } + + + /** + * 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) { + return ``; + } + + 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; + + return ``; + } + + /** + * 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); + + return this._getFile(path); + } + + /** + * 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); + const childDirName = this._filePath(path); + + 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`; + } + + parentDir[childDirName] = {}; + parentDir[childDirName][`.`] = parentDir[childDirName]; + parentDir[childDirName][`..`] = parentDir; + return ``; + } + + /** + * Resets navigation in the file system. + */ + reset() { + this.pwd = `/`; + this.files = this._root; + } + + /** + * Removes a file from the file system. + * + * @param path {string} the absolute or relative path to the file to be removed + * @returns {string} an empty string if the removal was successful, or a message explaining what went wrong + */ + rm(path) { + const dirName = this._parentPath(path); + const fileName = this._filePath(path); + + const dir = this._getFile(dirName); + if (!FileSystem.isDirectory(dir)) { + return `The directory '${dirName}' does not exist`; + } + + const file = dir[fileName]; + if (!FileSystem.isFile(file)) { + return `The file '${fileName}' does not exist`; + } + + delete dir[fileName]; + return ``; + } + + /** + * 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); + + const parentDirName = this._parentPath(path); + const childDirName = this._filePath(path); + + const parentDir = this._getFile(parentDirName); + if (!FileSystem.isDirectory(parentDir)) { + return `The directory '${parentDirName}' does not exist`; + } + + const childDir = parentDir[childDirName]; + if (!FileSystem.isDirectory(childDir)) { + return `The directory '${childDirName}' does not exist`; + } + if (!force && Object.keys(childDir).length > 2) { + return `The directory is not empty`; + } + + delete parentDir[childDirName]; + return ``; + } +} diff --git a/js/shared.js b/js/shared.js index 338b38c..b5d8f5d 100644 --- a/js/shared.js +++ b/js/shared.js @@ -1,3 +1,11 @@ +const asciiHeader = ` ________ _______ _ _ +| ____\\ \\ / / __ \\ | | | | +| |__ \\ \\ /\\ / /| | | | ___| | _| | _____ _ __ +| __| \\ \\/ \\/ / | | | |/ _ \\ |/ / |/ / _ \\ '__| +| | \\ /\\ / | |__| | __/ <| < __/ | +|_| \\/ \\/ |_____/ \\___|_|\\_\\_|\\_\\___|_| `; + + String.prototype.replaceAll = function (regex, replacement) { let string = this; diff --git a/js/terminal.js b/js/terminal.js index e2bae58..441e729 100644 --- a/js/terminal.js +++ b/js/terminal.js @@ -1,11 +1,3 @@ -const asciiHeader = ` ________ _______ _ _ -| ____\\ \\ / / __ \\ | | | | -| |__ \\ \\ /\\ / /| | | | ___| | _| | _____ _ __ -| __| \\ \\/ \\/ / | | | |/ _ \\ |/ / |/ / _ \\ '__| -| | \\ /\\ / | |__| | __/ <| < __/ | -|_| \\/ \\/ |_____/ \\___|_|\\_\\_|\\_\\___|_| `; - - class InputHistory { constructor() { this._history = []; @@ -56,6 +48,9 @@ class Terminal { this._prefixDiv = prefixDiv; this._inputHistory = new InputHistory(); + this._fs = new FileSystem(); + this._commands = new Commands(this, this._fs); + this._terminal.addEventListener("click", this._onclick.bind(this)); this._terminal.addEventListener("keypress", this._onkeypress.bind(this)); this._input.addEventListener("keydown", this._onkeydown.bind(this)); @@ -103,8 +98,8 @@ class Terminal { `.trimLines(); } - static generatePrefix() { - return `felix@fwdekker.com ${fs.pwd}> `; + generatePrefix() { + return `felix@fwdekker.com ${this._fs.pwd}> `; } processInput(input) { @@ -112,19 +107,19 @@ class Terminal { this.inputText = ``; this.outputText += `${this.prefixText}${input}\n`; - const output = commands.parse(input.trim()); + const output = this._commands.parse(input.trim()); if (output !== ``) { this.outputText += output + `\n`; } - this.prefixText = Terminal.generatePrefix(); + this.prefixText = this.generatePrefix(); } reset() { - fs.reset(); + this._fs.reset(); this.outputText = Terminal.generateHeader(); - this.prefixText = Terminal.generatePrefix(); + this.prefixText = this.generatePrefix(); } @@ -153,6 +148,7 @@ class Terminal { } + let terminal; addOnLoad(() => {