From 805ca06922658b62ef9b1187b7c384c1e7cf3f94 Mon Sep 17 00:00:00 2001 From: G4mingJon4s <40526179+G4mingJon4s@users.noreply.github.com> Date: Thu, 13 Jun 2024 04:17:39 +0200 Subject: [PATCH] TERMINAL: Added deleting entire directories using rm (#1378) --- src/Terminal/HelpText.ts | 17 +++++-- src/Terminal/commands/rm.ts | 91 +++++++++++++++++++++++++++++++++++-- 2 files changed, 99 insertions(+), 9 deletions(-) diff --git a/src/Terminal/HelpText.ts b/src/Terminal/HelpText.ts index 2de92ba29..17ecaee68 100644 --- a/src/Terminal/HelpText.ts +++ b/src/Terminal/HelpText.ts @@ -29,7 +29,7 @@ export const TerminalHelpText: string[] = [ " mv [src] [dest] Move/rename a text or script file", " nano [files...] Text editor - Open up and edit one or more scripts or text files", " ps Display all scripts that are currently running", - " rm [file] Delete a file from the server", + " rm [OPTIONS]... [FILE]... Delete a file from the server", " run [script] [-t n] [--tail] Execute a program or script", " [--ram-override n] [args...]", " scan Prints all immediately-available network connections", @@ -358,12 +358,21 @@ export const HelpTexts: Record = { nano: TemplatedHelpTexts.scriptEditor("nano"), ps: ["Usage: ps", " ", "Prints all scripts that are running on the current server", " "], rm: [ - "Usage: rm [file name]", + "Usage: rm [OPTION]... [FILE]...", " ", - "Removes the specified file from the current server. This command doesn't work for message (.msg) files.", + "Remove the FILE(s).", " ", - "WARNING: This is permanent and cannot be undone", + "-f, --force Force removal of multiple files.", + "-r, -R, --recursive Remove directories and their contents recursively.", + "--no-preserve-root Do not treat '/' specially.", " ", + "By default, rm does not operate on directories. To remove entire directories, use the --recursive (-r or -R) option.", + " ", + "To remove a file whose name starts with a '-', for example '-foo.js', use one of these commands:", + "rm -- -foo.js", + "rm ./-foo.js", + " ", + "Note that if you use rm to remove a file, the contents of the file will be lost. This is irreversible.", ], run: [ "Usage: run [file name] [-t num_threads] [--tail] [--ram-override ram_in_GBs] [args...]", diff --git a/src/Terminal/commands/rm.ts b/src/Terminal/commands/rm.ts index 9d9cb2b3f..3591d6a40 100644 --- a/src/Terminal/commands/rm.ts +++ b/src/Terminal/commands/rm.ts @@ -1,10 +1,91 @@ import { Terminal } from "../../Terminal"; import { BaseServer } from "../../Server/BaseServer"; +import { PromptEvent } from "../../ui/React/PromptManager"; +import { hasScriptExtension } from "../../Paths/ScriptFilePath"; +import { hasTextExtension } from "../../Paths/TextFilePath"; +import type { Directory } from "../../Paths/Directory"; +import type { IReturnStatus } from "../../types"; +import type { FilePath } from "../../Paths/FilePath"; export function rm(args: (string | number | boolean)[], server: BaseServer): void { - if (args.length !== 1) return Terminal.error("Incorrect number of arguments. Usage: rm [program/script]"); - const delTarget = Terminal.getFilepath(args[0] + ""); - if (!delTarget) return Terminal.error(`Invalid filename: ${args[0]}`); - const status = server.removeFile(delTarget); - if (!status.res && status.msg) Terminal.error(status.msg); + const errors = { + arg: (reason: string) => `Incorrect usage of rm command. ${reason}. Usage: rm [OPTION]... [FILE]...`, + dirsProvided: () => "Incorrect usage of rm command. To delete directories, use the -r flag", + invalidDir: (name: string) => `Invalid directory: ${name}`, + invalidFile: (name: string) => `Invalid file: ${name}`, + deleteFailed: (name: string, reason?: string) => `Failed to delete "${name}". ${reason ?? "Uncaught error"}`, + rootDeletion: () => + "You are trying to delete all files within the root directory. If this is intentional, use the --no-preserve-root flag", + } as const; + + if (args.length === 0) return Terminal.error(errors["arg"]("No arguments provided")); + + const recursive = args.includes("-r") || args.includes("-R") || args.includes("--recursive") || args.includes("-rf"); + const force = args.includes("-f") || args.includes("--force") || args.includes("-rf"); + const ignoreSpecialRoot = args.includes("--no-preserve-root"); + + const isTargetString = ( + arg: string | number | boolean, + index: number, + array: (string | number | boolean)[], + ): arg is string => + typeof arg === "string" && (!arg.startsWith("-") || (index - 1 >= 0 && array[index - 1] === "--")); + const targets = args.filter(isTargetString); + + if (targets.length === 0) return Terminal.error(errors["arg"]("No targets provided")); + if (!ignoreSpecialRoot && targets.includes("/")) return Terminal.error(errors["rootDeletion"]()); + + const directories: Directory[] = []; + const files: FilePath[] = []; + + for (const target of targets) { + if (!hasTextExtension(target) && !hasScriptExtension(target)) { + const dirPath = Terminal.getDirectory(target); + if (dirPath === null) return Terminal.error(errors["invalidDir"](target)); + if (!recursive) return Terminal.error(errors["dirsProvided"]()); + directories.push(dirPath); + continue; + } + + const file = Terminal.getFilepath(target); + if (file === null) return Terminal.error(errors["invalidFile"](target)); + + files.push(file); + } + + for (const dir of directories) { + for (const file of server.scripts.keys()) { + if (file.startsWith(dir)) files.push(file); + } + } + + const targetList = files.map((file) => "* " + file.toString()).join("\n"); + + const reports: { target: string; result: IReturnStatus }[] = []; + + const deleteSelectedTargets = () => { + for (const file of files) { + reports.push({ target: file, result: server.removeFile(file) }); + } + + for (const report of reports) { + if (report.result.res) { + Terminal.success(`Deleted: ${report.target}`); + } else { + Terminal.error(errors["deleteFailed"](report.target, report.result.msg)); + } + } + }; + + if (force || files.length === 1) { + deleteSelectedTargets(); + } else { + PromptEvent.emit({ + txt: "Are you sure you want to delete these files? This is irreversible.\n\nDeleting:\n" + targetList, + resolve: (value: string | boolean) => { + if (typeof value === "string") throw new Error("PromptEvent got a string, expected boolean"); + if (value) deleteSelectedTargets(); + }, + }); + } }