diff --git a/common/colors.js b/common/colors.js new file mode 100644 index 0000000..873e8fe --- /dev/null +++ b/common/colors.js @@ -0,0 +1,15 @@ +const COLORS = { + RED: '\x1b[31m', + GREEN: '\x1b[32m', + YELLOW: '\x1b[33m', + BLUE: '\x1b[34m', + MAGENTA: '\x1b[35m', + CYAN: '\x1b[36m', + WHITE: '\x1b[37m', + RESET: '\x1b[0m', +}; + +export default function logColoredMessage(message, color = 'white') { + const currentColor = COLORS[color.toUpperCase()] ?? COLORS.WHITE; + console.log(`${currentColor}${message}${COLORS.RESET}\n`) +}; \ No newline at end of file diff --git a/package.json b/package.json index 1a81b81..d791bdc 100644 --- a/package.json +++ b/package.json @@ -3,8 +3,10 @@ "version": "1.0.0", "description": "", "main": "index.js", + "type": "module", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "test": "echo \"Error: no test specified\" && exit 1", + "start": "node src/index.js -- --username=your_username" }, "repository": { "type": "git", diff --git a/src/index.js b/src/index.js index 2934828..ce7adf6 100644 --- a/src/index.js +++ b/src/index.js @@ -1 +1,147 @@ -console.log(1) \ No newline at end of file +import logColoredMessage from "../common/colors.js"; +import os from "os"; +import toUpperDirectory from "../utils/to-upper-directory.js"; +import { changeDirectory } from "../utils/change-directory.js"; +import list from "../utils/list.js"; +import showFileContent from "../utils/showFileContent.js"; +import createFile from "../utils/createFile.js"; +import createFolder from "../utils/createFolder.js"; +import renameFile from "../utils/renameFile.js"; +import copyFile from "../utils/copyFile.js"; +import removeFile from "../utils/removeFile.js"; +import handleOSCommands from "../utils/handleOSCommands.js"; +import calculateHash from "../utils/calculateHash.js"; +import compressFile from "../utils/compressFile.js"; +import decompressFile from "../utils/decompressFile.js"; + +const fileManager = async () => { + const argsArray = (process.argv).slice(2); + const userName = argsArray.find((arg) => arg.startsWith('--username'))?.split('=')[1] || 'guest'; + const homeDir = os.homedir() + let currentDir = homeDir; + logColoredMessage(`Welcome to the File Manager, ${userName}!`, 'magenta'); + logColoredMessage(`You are currently in ${currentDir}`, 'green'); + + process.stdin.on('data', async (chunk) => { + const input = chunk.toString().trim().split(' '); + const command = input[0]; + const options = input.slice(1); + + switch (command) { + + case ('up'): + if (currentDir === homeDir) { + logColoredMessage(`\nAlready in home directory: ${currentDir}`, 'red'); + } else { + currentDir = toUpperDirectory(currentDir); + } + break; + case ('cd'): + if (options[0]) { + currentDir = await changeDirectory(currentDir, options[0]); + } else { + logColoredMessage(`Invalid input`, 'red'); + } + break; + case ('ls'): + await list(currentDir); + break; + case ('cat'): + if (options[0]) { + await showFileContent(currentDir, options[0]); + } else { + logColoredMessage(`Invalid input`, 'red'); + } + break; + case ('add'): + if (options[0]) { + await createFile(currentDir, options[0]); + } else { + logColoredMessage(`Invalid input`, 'red'); + } + break; + case ('mkdir'): + if (options[0]) { + await createFolder(currentDir, options[0]); + } else { + logColoredMessage(`Invalid input`, 'red'); + } + break; + case ('rn'): + if (options[0] && options[1]) { + await renameFile(currentDir, options[0], options[1]); + } else { + logColoredMessage(`Invalid input`, 'red'); + } + break; + case ('cp'): + if (options[0] && options[1]) { + await copyFile(currentDir, options[0], options[1]); + } else { + logColoredMessage(`Invalid input`, 'red'); + } + break; + case ('mv'): + if (options[0] && options[1]) { + await copyFile(currentDir, options[0], options[1], { deleteSource: true }); + } else { + logColoredMessage(`Invalid input`, 'red'); + } + break; + case ('rm'): + if (options[0]) { + await removeFile(currentDir, options[0]); + } else { + logColoredMessage(`Invalid input`, 'red'); + } + break; + case ('os'): + if (options[0] && options[0].startsWith('--')) { + handleOSCommands(options[0]); + } else { + logColoredMessage('Invalid input', 'red'); + } + break; + case ('hash'): + if (options[0]) { + await calculateHash(currentDir, options[0]); + } else { + logColoredMessage('Invalid input', 'red'); + } + break; + case ('compress'): + if (options[0] && options[1]) { + await compressFile(currentDir, options[0], options[1]); + } else { + logColoredMessage(`Invalid input`, 'red'); + } + + break; + case ('decompress'): + if (options[0] && options[1]) { + await decompressFile(currentDir, options[0], options[1]); + } else { + logColoredMessage(`Invalid input`, 'red'); + } + break; + case ('.exit'): + process.exit(); + default: + logColoredMessage(`Invalid input`, 'red'); + } + logColoredMessage(`\nYou are currently in ${currentDir}`, 'green'); + }) + + + + process.on('SIGINT', () => { + process.exit(); + }); + + process.on('exit', () => { + logColoredMessage(`Thank you for using File Manager, ${userName}, goodbye!`, 'magenta'); + }); + +}; + +await fileManager(); \ No newline at end of file diff --git a/utils/calculateHash.js b/utils/calculateHash.js new file mode 100644 index 0000000..d10e680 --- /dev/null +++ b/utils/calculateHash.js @@ -0,0 +1,41 @@ + +import path from 'path'; +import crypto from 'crypto'; +import fs from 'fs'; +import { isFile } from './is-file.js'; +import logColoredMessage from "../common/colors.js"; + + +export default async function calculateHash(currentDirectory, pathToFile) { + try { + + const normalizedFilePath = path.normalize(pathToFile); + let resolvedPathToFile = path.resolve(currentDirectory, normalizedFilePath); + if (await isFile(normalizedFilePath)) { + resolvedPathToFile = normalizedFilePath; + } + + const readStream = fs.createReadStream(resolvedPathToFile); + const hash = crypto.createHash('sha256'); + const hashPromise = new Promise((resolve, reject) => { + readStream.on('data', (chunk) => { + hash.update(chunk); + }); + readStream.on('end', () => { + resolve(hash.digest('hex')); + }); + readStream.on('error', (error) => { + reject(error); + }); + + }); + + const fileHash = await hashPromise; + + logColoredMessage(`File hash is: ${fileHash}`, 'yellow'); + } catch { + logColoredMessage('Invalid input', 'red'); + } + +} + diff --git a/utils/change-directory.js b/utils/change-directory.js new file mode 100644 index 0000000..3f29468 --- /dev/null +++ b/utils/change-directory.js @@ -0,0 +1,26 @@ +import path from 'path'; +import logColoredMessage from "../common/colors.js"; +import { isDirectory } from './is-directory.js'; + +export async function changeDirectory(currentDirectory, destinationDirectory) { + const relativePath = path.join(currentDirectory, destinationDirectory); + + try { + const isRelativePath = await isDirectory(relativePath); + const isAbsolutePath = await isDirectory(destinationDirectory); + + if (isRelativePath) { + return relativePath; + } + + if (isAbsolutePath) { + return destinationDirectory; + } else { + throw new Error("Path doesn't exist"); + } + } catch (err) { + logColoredMessage(err, 'red'); + logColoredMessage('Operation failed', 'red'); + return currentDirectory; + } +} \ No newline at end of file diff --git a/utils/compressFile.js b/utils/compressFile.js new file mode 100644 index 0000000..bad801e --- /dev/null +++ b/utils/compressFile.js @@ -0,0 +1,59 @@ +import fs from 'fs'; +import zlib from 'zlib'; +import path from 'path'; +import logColoredMessage from "../common/colors.js"; +import { isDirectory } from './is-directory.js'; +import { isFile } from './is-file.js'; + + +export default async function compressFile(currentDirectory, inputFilePath, destinationPath) { + try { + let resolvedPathToFile = path.resolve(currentDirectory, inputFilePath); + if (await isFile(inputFilePath)) { + resolvedPathToFile = inputFilePath; + } + + let resolvedDestinationPath = path.resolve(currentDirectory, destinationPath); + if (await isDirectory(destinationPath)) { + resolvedDestinationPath = destinationPath; + } + + const resolvedDestinationFile = path.resolve(resolvedDestinationPath, path.basename(resolvedPathToFile) + '.br'); + + if (await isFile(resolvedDestinationFile)) { + logColoredMessage('Invalid input', 'red'); + return; + } + + if (!(await isFile(resolvedPathToFile))) { + logColoredMessage('Invalid input', 'red'); + return; + } + + const readStream = fs.createReadStream(resolvedPathToFile); + const writeStream = fs.createWriteStream(resolvedDestinationFile); + + const brotliStream = zlib.createBrotliCompress(); + + readStream.pipe(brotliStream).pipe(writeStream); + + const fileCompressed = new Promise((resolve, reject) => { + + writeStream.on('finish', () => { + resolve(); + }); + + writeStream.on('error', (error) => { + reject(error); + } + ); + } + ); + + await fileCompressed; + logColoredMessage(`File ${resolvedPathToFile} compressed to ${resolvedDestinationFile}`, 'yellow'); + + } catch (error) { + logColoredMessage('Invalid input', 'red'); + } +} diff --git a/utils/copyFile.js b/utils/copyFile.js new file mode 100644 index 0000000..1766743 --- /dev/null +++ b/utils/copyFile.js @@ -0,0 +1,51 @@ + +import path from 'path'; +import logColoredMessage from "../common/colors.js"; +import fs from 'fs'; +import { isFile } from './is-file.js'; +import { isDirectory } from './is-directory.js'; + + +export default async function copyFile(currentDirectory, pathToFile, destinationDirectory, options = { deleteSource: false }) { + try { + const normalizedFilePath = path.normalize(pathToFile); + let resolvedPathToFile = path.resolve(currentDirectory, normalizedFilePath); + if (await isFile(normalizedFilePath)) { + resolvedPathToFile = normalizedFilePath; + } + + + const normalizedDirectoryPath = path.normalize(destinationDirectory); + let resolvedPathToDirectory = path.resolve(currentDirectory, normalizedDirectoryPath); + if (await isDirectory(normalizedDirectoryPath)) { + resolvedPathToDirectory = normalizedDirectoryPath; + } + const parsedFilePath = path.parse(resolvedPathToFile); + const destinationFilePath = path.resolve(resolvedPathToDirectory, parsedFilePath.base); + + const isOriginalFileExist = await isFile(resolvedPathToFile); + const isDirectoryExist = await isDirectory(resolvedPathToDirectory); + const isDestinationFileExist = await isFile(destinationFilePath); + if (!isOriginalFileExist || !isDirectoryExist || isDestinationFileExist) { + logColoredMessage(`Invalid input`, 'red'); + return; + } + + const readStream = fs.createReadStream(resolvedPathToFile); + const writeStream = fs.createWriteStream(destinationFilePath); + readStream.pipe(writeStream); + const copyPromise = new Promise((resolve, reject) => { + readStream.on('end', () => { + resolve(); + }) + }); + + await copyPromise; + if (options.deleteSource) { + fs.promises.unlink(resolvedPathToFile); + } + logColoredMessage(`File ${resolvedPathToFile} copied to ${resolvedPathToDirectory}`, 'yellow'); + } catch (err) { + logColoredMessage(`Invalid input`, 'red'); + } +} \ No newline at end of file diff --git a/utils/createFile.js b/utils/createFile.js new file mode 100644 index 0000000..ef66e40 --- /dev/null +++ b/utils/createFile.js @@ -0,0 +1,15 @@ +import path from 'path'; +import fs from 'fs'; +import logColoredMessage from "../common/colors.js"; + + +export default async function createFile(currentPath, fileName) { + const filePath = path.resolve(currentPath, fileName); + + try { + await fs.promises.writeFile(filePath, '', { flag: 'wx' }); + logColoredMessage(`File ${fileName} created`, 'yellow'); + } catch (err) { + logColoredMessage(`Invalid input`, 'red'); + } +} \ No newline at end of file diff --git a/utils/createFolder.js b/utils/createFolder.js new file mode 100644 index 0000000..1c87f7e --- /dev/null +++ b/utils/createFolder.js @@ -0,0 +1,15 @@ +import path from 'path'; +import fs from 'fs'; +import logColoredMessage from "../common/colors.js"; + + +export default async function createFolder(currentPath, folderName) { + const folderPath = path.resolve(currentPath, folderName); + + try { + await fs.promises.mkdir(folderPath); + logColoredMessage(`Folder ${folderPath} created`, 'yellow'); + } catch (err) { + logColoredMessage(`Invalid input`, 'red'); + } +} \ No newline at end of file diff --git a/utils/decompressFile.js b/utils/decompressFile.js new file mode 100644 index 0000000..992bba3 --- /dev/null +++ b/utils/decompressFile.js @@ -0,0 +1,56 @@ +import fs from 'fs'; +import zlib from 'zlib'; +import path from 'path'; +import logColoredMessage from "../common/colors.js"; +import { isFile } from './is-file.js'; +import { isDirectory } from './is-directory.js'; + +export default async function decompressFile(currentDirectory, inputFilePath, destinationPath) { + try { + let resolvedPathToFile = path.resolve(currentDirectory, inputFilePath); + if (await isFile(inputFilePath)) { + resolvedPathToFile = inputFilePath; + } + + let resolvedDestinationPath = path.resolve(currentDirectory, destinationPath); + if (await isDirectory(destinationPath)) { + resolvedDestinationPath = destinationPath; + } + + const resolvedDestinationFile = path.resolve(resolvedDestinationPath, path.basename(resolvedPathToFile).slice(0, -3)); + + if (await isFile(resolvedDestinationFile)) { + logColoredMessage('Invalid input', 'red'); + return; + } + + if (!(await isFile(resolvedPathToFile))) { + logColoredMessage('Invalid input', 'red'); + return; + } + + const readStream = fs.createReadStream(resolvedPathToFile); + const writeStream = fs.createWriteStream(resolvedDestinationFile); + + const brotliStream = zlib.createBrotliDecompress(); + + readStream.pipe(brotliStream).pipe(writeStream); + + const fileCompressed = new Promise((resolve, reject) => { + + writeStream.on('finish', () => { + resolve(); + }); + + writeStream.on('error', () => { + reject(); + }); + }); + + await fileCompressed; + logColoredMessage(`File ${resolvedPathToFile} decompressed to ${resolvedDestinationFile}`, 'yellow'); + + } catch { + logColoredMessage('Invalid input', 'red'); + } +} diff --git a/utils/handleOSCommands.js b/utils/handleOSCommands.js new file mode 100644 index 0000000..d76d343 --- /dev/null +++ b/utils/handleOSCommands.js @@ -0,0 +1,32 @@ +import os from 'os'; +import logColoredMessage from "../common/colors.js"; + +export default function handleOSCommands(arg) { + const command = arg.slice(2); + switch (command) { + case 'EOL': + const eol = os.EOL; + logColoredMessage(`The end of a line in the current OS is: ${JSON.stringify(eol)}`, 'yellow'); + break; + case 'cpus': + const cpus = os.cpus(); + logColoredMessage(`The number of CPUs is: ${cpus.length}`, 'yellow'); + logColoredMessage(`The model of the CPU is: ${cpus[0].model}`, 'yellow'); + logColoredMessage(`The clock rate of the CPU is: ${(cpus[0].speed/1000).toFixed(2)} GHz`, 'yellow'); + break; + case 'homedir': + const homedir = os.homedir(); + logColoredMessage(`The home directory is: ${homedir}`, 'yellow'); + break; + case 'username': + const username = os.userInfo().username; + logColoredMessage(`The current system username is: ${username}`, 'yellow'); + break; + case 'architecture': + const architecture = os.arch(); + logColoredMessage(`The architecture is: ${architecture}`, 'yellow'); + break; + default: + logColoredMessage('Invalid input', 'red'); + } +} \ No newline at end of file diff --git a/utils/is-directory.js b/utils/is-directory.js new file mode 100644 index 0000000..2b82a31 --- /dev/null +++ b/utils/is-directory.js @@ -0,0 +1,14 @@ +import fs from 'fs'; + +export async function isDirectory(pathToCheck) { + try { + const pathStat = await fs.promises.stat(pathToCheck); + if(pathStat.isDirectory()) { + return true; + } else { + return false; + } + } catch { + return false; + } +} \ No newline at end of file diff --git a/utils/is-file.js b/utils/is-file.js new file mode 100644 index 0000000..e15a847 --- /dev/null +++ b/utils/is-file.js @@ -0,0 +1,14 @@ +import fs from 'fs'; + +export async function isFile(pathToCheck) { + try { + const pathStat = await fs.promises.stat(pathToCheck); + if(pathStat.isFile()) { + return true; + } else { + return false; + } + } catch { + return false; + } +} \ No newline at end of file diff --git a/utils/list.js b/utils/list.js new file mode 100644 index 0000000..4ffb4f2 --- /dev/null +++ b/utils/list.js @@ -0,0 +1,39 @@ +import fs from 'fs'; +import logColoredMessage from "../common/colors.js"; +import { isDirectory } from './is-directory.js'; +import { isFile } from './is-file.js'; + +function sortByName(a, b) { + return a.Name.localeCompare(b.Name); +} + +export default async function list(currentDirectory) { + try { + const contents = await fs.promises.readdir(currentDirectory); + + const directories = []; + const files = []; + + for (const content of contents) { + const contentPath = `${currentDirectory}/${content}`; + if (await isDirectory(contentPath)) { + directories.push({ Name: content, Type: 'directory' }); + } + + if (await isFile(contentPath)) { + files.push({ Name: content, Type: 'file' }); + } + } + + directories.sort(sortByName); + files.sort(sortByName); + + const allContents = directories.concat(files); + + console.table(allContents); + + } catch (err) { + logColoredMessage(err, 'red'); + logColoredMessage('Operation failed', 'red'); + } +} \ No newline at end of file diff --git a/utils/removeFile.js b/utils/removeFile.js new file mode 100644 index 0000000..d5a5e6b --- /dev/null +++ b/utils/removeFile.js @@ -0,0 +1,20 @@ + +import path from 'path'; +import fs from 'fs'; +import logColoredMessage from "../common/colors.js"; +import { isFile } from './is-file.js'; + +export default async function removeFile(currentDirectory, pathToFile) { + let resolvedPathToFile = path.resolve(currentDirectory, pathToFile); + if (await isFile(pathToFile)) { + resolvedPathToFile = pathToFile; + } + + try { + await fs.promises.unlink(resolvedPathToFile); + logColoredMessage(`File ${pathToFile} removed`, 'yellow'); + } catch { + logColoredMessage('Operation failed', 'red'); + } + +} diff --git a/utils/renameFile.js b/utils/renameFile.js new file mode 100644 index 0000000..94b7693 --- /dev/null +++ b/utils/renameFile.js @@ -0,0 +1,25 @@ +import fs from 'fs'; +import logColoredMessage from "../common/colors.js"; +import path from 'path'; +import { isFile } from './is-file.js'; + +export default async function renameFile(currentDirectory, pathToFile, newFilename) { + const normalizedPathToFile = path.normalize(pathToFile); + const isPathCorrect = await isFile(normalizedPathToFile); + let pathToFileResolved = path.resolve(currentDirectory, pathToFile); + + if (isPathCorrect) { + pathToFileResolved = pathToFile; + } + + const parsedPath = path.parse(pathToFileResolved); + parsedPath.base = newFilename; + const pathToRenamedFile = path.format(parsedPath); + + try { + await fs.promises.rename(pathToFileResolved, pathToRenamedFile); + logColoredMessage(`File ${pathToFile} renamed to ${newFilename}`, 'yellow') + } catch (err) { + logColoredMessage(`Invalid input`, 'red'); + } +} \ No newline at end of file diff --git a/utils/showFileContent.js b/utils/showFileContent.js new file mode 100644 index 0000000..42c27ef --- /dev/null +++ b/utils/showFileContent.js @@ -0,0 +1,40 @@ + + +import path from 'path'; +import fs from 'fs'; +import logColoredMessage from "../common/colors.js"; +import { isFile } from './is-file.js'; + +export default async function showFileContent(currentPath, targetPath) { + const filePath = path.resolve(currentPath, targetPath); + const notFile = !(await isFile(filePath)); + + if (notFile) { + logColoredMessage(`${targetPath} is not a file`, 'red'); + logColoredMessage(`Invalid input`, 'red'); + return; + } + try { + const readable = fs.createReadStream(filePath, 'utf-8'); + const fileContentPromise = new Promise((resolve, reject) => { + let fileContent = ''; + + readable.on('data', (chunk) => { + fileContent += chunk; + }); + + readable.on('end', () => { + resolve(fileContent); + }); + + readable.on('error', (error) => { + reject(error); + }); + }); + + const fileContent = await fileContentPromise; + logColoredMessage(fileContent, 'yellow'); + } catch { + logColoredMessage(`Invalid input`, 'red'); + } +} \ No newline at end of file diff --git a/utils/to-upper-directory.js b/utils/to-upper-directory.js new file mode 100644 index 0000000..43d555a --- /dev/null +++ b/utils/to-upper-directory.js @@ -0,0 +1,18 @@ +import path from 'path'; +import logColoredMessage from "../common/colors.js"; + +export default function toUpperDirectory(currentDirectory) { + try { + const directoriesArray = currentDirectory.split(path.sep); + if (directoriesArray.length === 1) { + return currentDirectory; + } + const newDirectory = directoriesArray.slice(0, -1).join(path.sep); + return newDirectory; + } catch (err) { + logColoredMessage(err, 'red'); + logColoredMessage('Operation failed', 'red'); + return currentDirectory; + } + +} \ No newline at end of file