diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..8ee7c22 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "editor.formatOnSave": true, + "[javascript]": { + "editor.codeActionsOnSave": { + "source.organizeImports": "explicit" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..7c2d1e0 --- /dev/null +++ b/package.json @@ -0,0 +1,13 @@ +{ + "name": "file-manager", + "version": "1.0.0", + "description": "", + "main": "src/index.js", + "type": "module", + "scripts": { + "start": "node src/index.js", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "Dzmitry Yarmoshkin", + "license": "ISC" +} diff --git a/src/commands/archiving/compress.js b/src/commands/archiving/compress.js new file mode 100644 index 0000000..3fe1f62 --- /dev/null +++ b/src/commands/archiving/compress.js @@ -0,0 +1,17 @@ +import { createReadStream, createWriteStream } from 'node:fs'; +import { pipeline } from 'node:stream/promises'; +import { createBrotliCompress } from 'node:zlib'; +import { checkFileExists, extractArgument } from '../../utils/utils.js'; + +export const compress = async (userInput) => { + const sourceFilePath = extractArgument(userInput, 1); + const destFilePath = extractArgument(userInput, 2); + + await checkFileExists(sourceFilePath); + + const gzip = createBrotliCompress(); + const sourceStream = createReadStream(sourceFilePath); + const destStream = createWriteStream(destFilePath); + + await pipeline(sourceStream, gzip, destStream); +}; diff --git a/src/commands/archiving/decompress.js b/src/commands/archiving/decompress.js new file mode 100644 index 0000000..e1a7f38 --- /dev/null +++ b/src/commands/archiving/decompress.js @@ -0,0 +1,17 @@ +import { createReadStream, createWriteStream } from 'node:fs'; +import { pipeline } from 'node:stream/promises'; +import { createBrotliDecompress } from 'node:zlib'; +import { checkFileExists, extractArgument } from '../../utils/utils.js'; + +export const decompress = async (userInput) => { + const sourceFilePath = extractArgument(userInput, 1); + const destFilePath = extractArgument(userInput, 2); + + await checkFileExists(sourceFilePath); + + const gzip = createBrotliDecompress(); + const sourceStream = createReadStream(sourceFilePath); + const destStream = createWriteStream(destFilePath); + + await pipeline(sourceStream, gzip, destStream); +}; diff --git a/src/commands/commands.js b/src/commands/commands.js new file mode 100644 index 0000000..714c424 --- /dev/null +++ b/src/commands/commands.js @@ -0,0 +1,49 @@ +import { extractArgument, extractArguments } from '../utils/utils.js'; + +export const COMMANDS = { + UP: { commandName: 'up', expectedArgs: 0 }, + CD: { commandName: 'cd', expectedArgs: 1 }, + LS: { commandName: 'ls', expectedArgs: 0 }, + CAT: { commandName: 'cat', expectedArgs: 1 }, + ADD: { commandName: 'add', expectedArgs: 1 }, + RN: { commandName: 'rn', expectedArgs: 2 }, + CP: { commandName: 'cp', expectedArgs: 2 }, + MV: { commandName: 'mv', expectedArgs: 2 }, + RM: { commandName: 'rm', expectedArgs: 1 }, + HASH: { commandName: 'hash', expectedArgs: 1 }, + COMPRESS: { commandName: 'compress', expectedArgs: 2 }, + DECOMPRESS: { commandName: 'decompress', expectedArgs: 2 }, + EXIT: { commandName: '.exit', expectedArgs: 0 }, + OS: { commandName: 'os', expectedArgs: 1 }, + OS_EOL: { commandName: '--EOL' }, + OS_CPUS: { commandName: '--cpus' }, + OS_HOMEDIR: { commandName: '--homedir' }, + OS_USERNAME: { commandName: '--username' }, + OS_ARCH: { commandName: '--architecture' }, +}; + +export const checkArgumentCount = (command, userInput) => { + for (const key in COMMANDS) { + if (COMMANDS[key].commandName === command) { + const expectedArgs = COMMANDS[key].expectedArgs; + const args = extractArguments(userInput); + + if (args.length - 1 !== expectedArgs) { + throw new Error(`Incorrect number of arguments provided. Expected ${expectedArgs} arguments.`); + } + } + } +}; + +export const parseCommand = (userInput) => { + const command = extractArgument(userInput, 0); + + checkArgumentCount(command, userInput); + + if (command === COMMANDS.OS.commandName) { + const argument = extractArgument(userInput, 1); + return argument; + } + + return command; +}; diff --git a/src/commands/filesystem/add.js b/src/commands/filesystem/add.js new file mode 100644 index 0000000..b92dd6d --- /dev/null +++ b/src/commands/filesystem/add.js @@ -0,0 +1,12 @@ +import fs from 'node:fs/promises'; +import path from 'node:path'; +import { getCurrentDirectory } from '../../directory.js'; +import { extractArgument } from '../../utils/utils.js'; + +export const add = async (userInput) => { + const parsedFilename = extractArgument(userInput, 1); + const filename = path.basename(parsedFilename); + const filePath = path.resolve(getCurrentDirectory(), filename); + + await fs.writeFile(filePath, '', { flag: 'wx' }); +}; diff --git a/src/commands/filesystem/cat.js b/src/commands/filesystem/cat.js new file mode 100644 index 0000000..2caf0a2 --- /dev/null +++ b/src/commands/filesystem/cat.js @@ -0,0 +1,14 @@ +import { createReadStream } from 'node:fs'; +import path from 'node:path'; +import { stdout } from 'node:process'; +import { pipeline } from 'node:stream/promises'; +import { getCurrentDirectory } from '../../directory.js'; +import { extractArgument } from '../../utils/utils.js'; + +export const cat = async (userInput) => { + const parsedPath = extractArgument(userInput, 1); + const sourceFilePath = path.resolve(getCurrentDirectory(), parsedPath); + const sourceStream = createReadStream(sourceFilePath); + + await pipeline(sourceStream, stdout, { end: false }); +}; diff --git a/src/commands/filesystem/cd.js b/src/commands/filesystem/cd.js new file mode 100644 index 0000000..17a4145 --- /dev/null +++ b/src/commands/filesystem/cd.js @@ -0,0 +1,8 @@ +import { chdir } from 'node:process'; +import { extractArgument } from '../../utils/utils.js'; + +export const cd = (userInput) => { + const parsedPath = extractArgument(userInput, 1); + + chdir(parsedPath); +}; diff --git a/src/commands/filesystem/cp.js b/src/commands/filesystem/cp.js new file mode 100644 index 0000000..2858e99 --- /dev/null +++ b/src/commands/filesystem/cp.js @@ -0,0 +1,21 @@ +import { createReadStream, createWriteStream } from 'node:fs'; +import path from 'node:path'; +import { pipeline } from 'node:stream/promises'; +import { getCurrentDirectory } from '../../directory.js'; +import { checkFileExists, extractArgument } from '../../utils/utils.js'; + +export const cp = async (userInput) => { + const parsedSourcePath = extractArgument(userInput, 1); + const parsedDestPath = extractArgument(userInput, 2); + const sourceFilePath = path.resolve(getCurrentDirectory(), parsedSourcePath); + const destFolderPath = path.resolve(getCurrentDirectory(), parsedDestPath); + + await checkFileExists(sourceFilePath); + + const sourceFilename = path.basename(sourceFilePath); + const destFilePath = path.resolve(destFolderPath, sourceFilename); + const sourceStream = createReadStream(sourceFilePath); + const destStream = createWriteStream(destFilePath); + + await pipeline(sourceStream, destStream); +}; diff --git a/src/commands/filesystem/ls.js b/src/commands/filesystem/ls.js new file mode 100644 index 0000000..47998b7 --- /dev/null +++ b/src/commands/filesystem/ls.js @@ -0,0 +1,30 @@ +import fs from 'node:fs/promises'; +import path from 'node:path'; +import { getCurrentDirectory } from '../../directory.js'; + +export const ls = async () => { + const files = await fs.readdir(getCurrentDirectory()); + const fileInfoArray = []; + + const folders = []; + const filesList = []; + for (const file of files) { + const fileStat = await fs.stat(path.resolve(getCurrentDirectory(), file)); + const isFile = fileStat.isFile(); + const fileType = isFile ? 'file' : 'directory'; + const fileInfo = { Name: file, Type: fileType }; + + if (fileType === 'directory') { + folders.push(fileInfo); + } else { + filesList.push(fileInfo); + } + } + + folders.sort((a, b) => a.Name.localeCompare(b.Name)); + filesList.sort((a, b) => a.Name.localeCompare(b.Name)); + + fileInfoArray.push(...folders, ...filesList); + + console.table(fileInfoArray, ['Name', 'Type']); +}; diff --git a/src/commands/filesystem/mv.js b/src/commands/filesystem/mv.js new file mode 100644 index 0000000..573d704 --- /dev/null +++ b/src/commands/filesystem/mv.js @@ -0,0 +1,7 @@ +import { cp } from './cp.js'; +import { rm } from './rm.js'; + +export const mv = async (userInput) => { + await cp(userInput); + await rm(userInput); +}; diff --git a/src/commands/filesystem/rm.js b/src/commands/filesystem/rm.js new file mode 100644 index 0000000..8286ca9 --- /dev/null +++ b/src/commands/filesystem/rm.js @@ -0,0 +1,11 @@ +import fs from 'node:fs/promises'; +import path from 'node:path'; +import { getCurrentDirectory } from '../../directory.js'; +import { extractArgument } from '../../utils/utils.js'; + +export const rm = async (userInput) => { + const parsedSourcePath = extractArgument(userInput, 1); + const sourceFilePath = path.resolve(getCurrentDirectory(), parsedSourcePath); + + await fs.unlink(sourceFilePath); +}; diff --git a/src/commands/filesystem/rn.js b/src/commands/filesystem/rn.js new file mode 100644 index 0000000..2657995 --- /dev/null +++ b/src/commands/filesystem/rn.js @@ -0,0 +1,16 @@ +import fs from 'node:fs/promises'; +import path from 'node:path'; +import { getCurrentDirectory } from '../../directory.js'; +import { extractArgument } from '../../utils/utils.js'; + +export const rn = async (userInput) => { + const parsedSourcePath = extractArgument(userInput, 1); + const parsedDestPath = extractArgument(userInput, 2); + + const destFilename = path.basename(parsedDestPath); + const sourceFilePath = path.resolve(getCurrentDirectory(), parsedSourcePath); + const sourceFileDir = path.dirname(sourceFilePath); + const renamedFilePath = path.resolve(sourceFileDir, destFilename); + + await fs.rename(sourceFilePath, renamedFilePath); +}; diff --git a/src/commands/filesystem/up.js b/src/commands/filesystem/up.js new file mode 100644 index 0000000..e7d1043 --- /dev/null +++ b/src/commands/filesystem/up.js @@ -0,0 +1,5 @@ +import { chdir } from 'node:process'; + +export const up = () => { + chdir('..'); +}; diff --git a/src/commands/hash/hash.js b/src/commands/hash/hash.js new file mode 100644 index 0000000..a5c03bd --- /dev/null +++ b/src/commands/hash/hash.js @@ -0,0 +1,13 @@ +import { createHash } from 'node:crypto'; +import { createReadStream } from 'node:fs'; +import { stdout } from 'node:process'; +import { pipeline } from 'node:stream/promises'; +import { extractArgument } from '../../utils/utils.js'; + +export const calculateHash = async (userInput) => { + const sourceFilePath = extractArgument(userInput, 1); + const hash = createHash('sha256'); + const input = createReadStream(sourceFilePath); + + await pipeline(input, hash.setEncoding('hex'), stdout, { end: false }); +}; diff --git a/src/commands/os-info/arch.js b/src/commands/os-info/arch.js new file mode 100644 index 0000000..8c5d7ce --- /dev/null +++ b/src/commands/os-info/arch.js @@ -0,0 +1,5 @@ +import { arch as nodeArch } from 'node:os'; + +export const arch = () => { + console.log(`Node.js binary architecture: ${nodeArch()}`); +}; diff --git a/src/commands/os-info/cpus.js b/src/commands/os-info/cpus.js new file mode 100644 index 0000000..600d882 --- /dev/null +++ b/src/commands/os-info/cpus.js @@ -0,0 +1,14 @@ +import { availableParallelism, cpus as nodeCPUS } from 'os'; + +export const cpus = () => { + const cpuData = nodeCPUS(); + const coreCount = availableParallelism(); + const cpuInfoArray = []; + + console.log(`Number of cores: ${coreCount}`); + cpuData.forEach((core) => { + cpuInfoArray.push({ Model: core.model, Speed: `${(core.speed / 1000).toFixed(2)} GHz` }); + }); + + console.table(cpuInfoArray, ['Model', 'Speed']); +}; diff --git a/src/commands/os-info/eol.js b/src/commands/os-info/eol.js new file mode 100644 index 0000000..21e8c0b --- /dev/null +++ b/src/commands/os-info/eol.js @@ -0,0 +1,5 @@ +import { EOL } from 'os'; + +export const eol = () => { + console.log(`System End-Of-Line (EOL): ${JSON.stringify(EOL)}`); +}; diff --git a/src/commands/os-info/homedir.js b/src/commands/os-info/homedir.js new file mode 100644 index 0000000..c881ef6 --- /dev/null +++ b/src/commands/os-info/homedir.js @@ -0,0 +1,5 @@ +import { getHomeDir } from '../../utils/utils.js'; + +export const homedir = () => { + console.log(`Home directory: ${getHomeDir()}`); +}; diff --git a/src/commands/os-info/username.js b/src/commands/os-info/username.js new file mode 100644 index 0000000..4764498 --- /dev/null +++ b/src/commands/os-info/username.js @@ -0,0 +1,6 @@ +import { userInfo } from 'node:os'; + +export const username = () => { + const user = userInfo(); + console.log(`Current system user name: ${user.username}`); +}; diff --git a/src/directory.js b/src/directory.js new file mode 100644 index 0000000..1927138 --- /dev/null +++ b/src/directory.js @@ -0,0 +1,12 @@ +import { chdir, cwd } from 'node:process'; +import { getHomeDir } from './utils/utils.js'; + +export const setStartingDir = () => { + const startingDir = getHomeDir(); + + chdir(startingDir); +}; + +export const getCurrentDirectory = () => { + return cwd(); +}; diff --git a/src/events/exit.js b/src/events/exit.js new file mode 100644 index 0000000..918c20f --- /dev/null +++ b/src/events/exit.js @@ -0,0 +1,13 @@ +import { logWithColor } from '../utils/utils.js'; + +export const setExitEvents = (username) => { + const message = `Thank you for using File Manager, ${username}, goodbye!`; + + process.on('SIGINT', () => { + process.exit(0); + }); + + process.on('exit', () => { + logWithColor(message, 'yellow'); + }); +}; diff --git a/src/events/input.js b/src/events/input.js new file mode 100644 index 0000000..a6a6bde --- /dev/null +++ b/src/events/input.js @@ -0,0 +1,110 @@ +import { compress } from '../commands/archiving/compress.js'; +import { decompress } from '../commands/archiving/decompress.js'; +import { COMMANDS, parseCommand } from '../commands/commands.js'; +import { add } from '../commands/filesystem/add.js'; +import { cat } from '../commands/filesystem/cat.js'; +import { cd } from '../commands/filesystem/cd.js'; +import { cp } from '../commands/filesystem/cp.js'; +import { ls } from '../commands/filesystem/ls.js'; +import { mv } from '../commands/filesystem/mv.js'; +import { rm } from '../commands/filesystem/rm.js'; +import { rn } from '../commands/filesystem/rn.js'; +import { up } from '../commands/filesystem/up.js'; +import { calculateHash } from '../commands/hash/hash.js'; +import { arch } from '../commands/os-info/arch.js'; +import { cpus } from '../commands/os-info/cpus.js'; +import { eol } from '../commands/os-info/eol.js'; +import { homedir } from '../commands/os-info/homedir.js'; +import { username } from '../commands/os-info/username.js'; +import { getCurrentDirectory } from '../directory.js'; +import { logWithColor } from '../utils/utils.js'; + +export const setInputEvent = () => { + process.stdin.on('data', async (data) => { + try { + const userInput = data.toString().replace(/(\r\n|\n|\r)/gm, ''); + const command = parseCommand(userInput); + + switch (command) { + case COMMANDS.UP.commandName: + up(); + break; + + case COMMANDS.CD.commandName: + cd(userInput); + break; + + case COMMANDS.LS.commandName: + await ls(); + break; + + case COMMANDS.CAT.commandName: + await cat(userInput); + break; + + case COMMANDS.ADD.commandName: + await add(userInput); + break; + + case COMMANDS.RN.commandName: + await rn(userInput); + break; + + case COMMANDS.CP.commandName: + await cp(userInput); + break; + + case COMMANDS.MV.commandName: + await mv(userInput); + break; + + case COMMANDS.RM.commandName: + await rm(userInput); + break; + + case COMMANDS.HASH.commandName: + await calculateHash(userInput); + break; + + case COMMANDS.COMPRESS.commandName: + await compress(userInput); + break; + + case COMMANDS.DECOMPRESS.commandName: + await decompress(userInput); + break; + + case COMMANDS.OS_EOL.commandName: + eol(); + break; + + case COMMANDS.OS_CPUS.commandName: + cpus(); + break; + + case COMMANDS.OS_HOMEDIR.commandName: + homedir(); + break; + + case COMMANDS.OS_USERNAME.commandName: + username(); + break; + + case COMMANDS.OS_ARCH.commandName: + arch(); + break; + + case COMMANDS.EXIT.commandName: + process.exit(0); + + default: + logWithColor('Invalid input', 'red'); + break; + } + } catch (err) { + logWithColor(`Operation failed\n${err.message}`, 'red'); + } + + console.log(`\nYou are currently in ${getCurrentDirectory()}`); + }); +}; diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..8b5a15d --- /dev/null +++ b/src/index.js @@ -0,0 +1,15 @@ +import { setExitEvents } from './events/exit.js'; +import { setInputEvent } from './events/input.js'; +import { setStartingDir } from './directory.js'; +import { getUsernameFromArgs, greetUser } from './user.js'; + +const init = async () => { + const username = getUsernameFromArgs(); + + setStartingDir(); + setExitEvents(username); + greetUser(username); + setInputEvent(); +}; + +init(); diff --git a/src/user.js b/src/user.js new file mode 100644 index 0000000..2eb2f15 --- /dev/null +++ b/src/user.js @@ -0,0 +1,29 @@ +import { getCurrentDirectory } from './directory.js'; +import { logWithColor } from './utils/utils.js'; + +export const getUsernameFromArgs = () => { + const args = process.argv.slice(2); + const usernameArg = args.find((arg) => arg.startsWith('--username=')); + + if (!usernameArg) { + logWithColor( + `Oops! It looks like you forgot to provide a username argument. Please start the program again using the following command:\nnpm run start -- --username=your_username`, + 'red' + ); + process.exit(1); + } + + const username = usernameArg.substring(usernameArg.indexOf('=') + 1); + + if (!username) { + logWithColor(`Oops! It looks like you provided an empty username. Please provide a valid username.`, 'red'); + process.exit(1); + } + + return username; +}; + +export const greetUser = (username) => { + console.log(`Welcome to the File Manager, ${username}!`); + console.log(`You are currently in ${getCurrentDirectory()}`); +}; diff --git a/src/utils/utils.js b/src/utils/utils.js new file mode 100644 index 0000000..c30f63f --- /dev/null +++ b/src/utils/utils.js @@ -0,0 +1,41 @@ +import fs from 'node:fs/promises'; +import { homedir } from 'node:os'; + +export const extractArguments = (userInput) => { + return userInput.trim().match(/(?:[^\s"]+|"[^"]*")/g) || []; +}; + +export const extractArgument = (userInput, argNumber) => { + const args = extractArguments(userInput); + let argument = args[argNumber] || ''; + if (argument.startsWith('"') && argument.endsWith('"')) { + argument = argument.substring(1, argument.length - 1); + } + + return argument; +}; + +export const logWithColor = (text, color) => { + const colors = { + reset: '\x1b[0m', + black: '\x1b[30m', + red: '\x1b[31m', + green: '\x1b[32m', + yellow: '\x1b[33m', + blue: '\x1b[34m', + }; + + console.log(`${colors[color]}${text}${colors.reset}`); +}; + +export const getHomeDir = () => { + return homedir(); +}; + +export const checkFileExists = async (filePath) => { + try { + await fs.access(filePath); + } catch (error) { + throw new Error('File does not exist.'); + } +};