diff --git a/package.json b/package.json new file mode 100644 index 0000000..94add39 --- /dev/null +++ b/package.json @@ -0,0 +1,19 @@ +{ + "name": "node-nodejs-basics", + "version": "1.0.0", + "description": "This repository is the part of nodejs-assignments https://github.com/AlreadyBored/nodejs-assignments", + "type": "module", + "scripts": { + "start": "node src/file_manager.js", + "test": "node src/file_manager.js -- --username=docroot" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/docroot/nodejs_file_manager.git" + }, + "author": "docroot", + "bugs": { + "url": "https://github.com/docroot/nodejs_file_manager/issues" + }, + "homepage": "https://github.com/docroot/nodejs_file_manager.git#readme" +} diff --git a/src/commands/add.js b/src/commands/add.js new file mode 100644 index 0000000..642d0eb --- /dev/null +++ b/src/commands/add.js @@ -0,0 +1,19 @@ +import fs from 'fs/promises'; +import path from 'path'; + +const cmd_add = async (ctx, args) => { + if (args.length !== 1) throw new Error(); + let filePath; + if (path.isAbsolute(args[0])) { + filePath = args[0]; + } + else { + filePath = path.join(ctx.cwd, args[0]); + } + + await fs.writeFile(filePath, '', { flag: 'wx' }); +} + +export { + cmd_add +} diff --git a/src/commands/arch.js b/src/commands/arch.js new file mode 100644 index 0000000..60210ca --- /dev/null +++ b/src/commands/arch.js @@ -0,0 +1,93 @@ +import { error } from 'console'; +import fs, { lstat } from 'fs'; +import path from 'path'; +import zlib from 'zlib'; + +const cmd_compress = async (ctx, args) => { + if (args.length !== 1) throw new Error(); + let filePath; + if (path.isAbsolute(args[0])) { + filePath = args[0]; + } + else { + filePath = path.join(ctx.cwd, args[0]); + } + + const archPath = filePath + '.br'; + + const inputStream = fs.createReadStream(filePath); + const writeStream = fs.createWriteStream(archPath, { flags: 'wx' }); + + const brotliOptions = { + chunkSize: 64 * 1024, + params: { + [zlib.constants.BROTLI_PARAM_MODE]: zlib.constants.BROTLI_MODE_GENERIC, + [zlib.constants.BROTLI_PARAM_QUALITY]: zlib.constants.BROTLI_MAX_QUALITY, + }, + }; + const brotliStream = zlib.createBrotliCompress(brotliOptions); + + await new Promise((resolve, reject) => { + inputStream.on('error', (error) => { + reject(error); + }) + brotliStream.on('error', (error) => { + reject(error); + }) + writeStream.on('error', (error) => { + reject(error); + }); + writeStream.on('finish', () => { + resolve(); + }); + inputStream.pipe(brotliStream).pipe(writeStream); + } + ).catch((error) => { throw error; }); +} + +const cmd_decompress = async (ctx, args) => { + if (args.length !== 1) throw new Error(); + let archPath; + if (path.isAbsolute(args[0])) { + archPath = args[0]; + } + else { + archPath = path.join(ctx.cwd, args[0]); + } + + const re = /\.br$/; + let filePath = archPath; + if (re.test(filePath)) { + filePath = filePath.slice(0, -3); + } + else { + filePath = filePath + '.unpkd'; + } + + const inputStream = fs.createReadStream(archPath); + const writeStream = fs.createWriteStream(filePath, { flags: 'wx' }); + + const brotliStream = zlib.createBrotliDecompress(); + + await new Promise((resolve, reject) => { + inputStream.on('error', (error) => { + reject(error); + }) + brotliStream.on('error', (error) => { + reject(error); + }) + writeStream.on('error', (error) => { + reject(error); + }); + writeStream.on('finish', () => { + resolve(); + }); + inputStream.pipe(brotliStream).pipe(writeStream); + } + ).catch((error) => { throw error; }); +} + +export { + cmd_compress, + cmd_decompress +} diff --git a/src/commands/cat.js b/src/commands/cat.js new file mode 100644 index 0000000..5ab9e64 --- /dev/null +++ b/src/commands/cat.js @@ -0,0 +1,30 @@ +import fs from 'fs'; +import path from 'path'; + +const cmd_cat = async (ctx, args) => { + if (args.length !== 1) throw new Error(); + let filePath; + if (path.isAbsolute(args[0])) { + filePath = args[0]; + } + else { + filePath = path.join(ctx.cwd, args[0]); + } + + await new Promise((resolve, reject) => { + const inputStream = fs.createReadStream(filePath, 'utf8'); + inputStream.pipe(process.stdout); + inputStream.on('end', () => { + inputStream.close(); + resolve(); + }) + inputStream.on('error', (error) => { + reject(error); + }) + } + ).catch((error) => { throw error; }); +} + +export { + cmd_cat +} diff --git a/src/commands/cd.js b/src/commands/cd.js new file mode 100644 index 0000000..a5f56c9 --- /dev/null +++ b/src/commands/cd.js @@ -0,0 +1,18 @@ +import path from 'path'; + +const cmd_cd = (ctx, args) => { + if (args.length > 1) throw new Error(); + let newDir; + if (path.isAbsolute(args[0])) { + newDir = args[0]; + } + else { + newDir = path.join(ctx.cwd, args[0]); + } + process.chdir(newDir); + ctx.cwd = process.cwd(); +} + +export { + cmd_cd +} \ No newline at end of file diff --git a/src/commands/cp.js b/src/commands/cp.js new file mode 100644 index 0000000..bf4c200 --- /dev/null +++ b/src/commands/cp.js @@ -0,0 +1,45 @@ +import fs from 'fs'; +import path from 'path'; + +const cmd_cp = async (ctx, args) => { + if (args.length !== 2) throw new Error(); + let filePaths = []; + const fileName = path.basename(args[0]); + + if (path.isAbsolute(args[0])) { + filePaths.push(args[0]); + } + else { + filePaths.push(path.join(ctx.cwd, args[0])); + } + + if (path.isAbsolute(args[1])) { + filePaths.push(path.join(args[1], fileName)); + } + else { + filePaths.push(path.join(ctx.cwd, args[1], fileName)); + } + + const srcStream = fs.createReadStream(filePaths[0], { flags: 'r' }); + const dstStream = fs.createWriteStream(filePaths[1], { flags: 'wx' }); + + await new Promise((resolve, reject) => { + dstStream.on('finish', () => { + dstStream.close(); + srcStream.close(); + resolve(); + }); + srcStream.on('error', (error) => { + reject(error); + }); + dstStream.on('error', (error) => { + reject(error); + }); + srcStream.pipe(dstStream); + } + ).catch((error) => { srcStream.close(); dstStream.close(); throw error; }); +} + +export { + cmd_cp +} diff --git a/src/commands/exit.js b/src/commands/exit.js new file mode 100644 index 0000000..620d144 --- /dev/null +++ b/src/commands/exit.js @@ -0,0 +1,7 @@ +const cmd_exit = (ctx, args) => { + process.exit(0); +} + +export { + cmd_exit +} diff --git a/src/commands/hash.js b/src/commands/hash.js new file mode 100644 index 0000000..67f55c9 --- /dev/null +++ b/src/commands/hash.js @@ -0,0 +1,37 @@ +import fs from 'fs'; +import path from 'path'; +import crypto from 'crypto'; + +const cmd_hash = async (ctx, args) => { + if (args.length !== 1) throw new Error(); + let filePath; + if (path.isAbsolute(args[0])) { + filePath = args[0]; + } + else { + filePath = path.join(ctx.cwd, args[0]); + } + + const hash = crypto.createHash('sha256'); + const inputStream = fs.createReadStream(filePath); + + await new Promise((resolve, reject) => { + inputStream.on('end', () => { + inputStream.close(); + }) + inputStream.on('error', (error) => { + reject(error); + }) + hash.on('finish', () => { + resolve(); + }); + inputStream.pipe(hash); + } + ).catch((error) => { throw error; }); + + console.log(hash.digest('hex')); +} + +export { + cmd_hash +} diff --git a/src/commands/ls.js b/src/commands/ls.js new file mode 100644 index 0000000..94288ac --- /dev/null +++ b/src/commands/ls.js @@ -0,0 +1,44 @@ +import fs from 'fs/promises'; +import path from 'path'; + +const cmd_ls = async (ctx, args) => { + if (args.length > 1) throw new Error(); + let dir = ctx.cwd; + if (args.length === 1) { + if (path.isAbsolute(args[0])) { + dir = args[0]; + } + else { + dir = path.join(ctx.cwd, args[0]); + } + } + try { + const files = await fs.readdir(dir); + const filesInfo = []; + for (let i = 0; i < files.length; i++) { + const file = files[i]; + const filePath = path.join(dir, file); + try { + const fileStats = await fs.stat(filePath); + filesInfo.push({ 'Name': file, 'Type': (fileStats.isDirectory() ? 'DIR' : 'File'), 'size': fileStats.size, }); + } catch (error) { + } + } + filesInfo.sort((a, b) => { + if (a.Type === b.Type) { + return a.Name.localeCompare(b.Name); + } + if (a.Type === 'DIR') { + return -1; + } + return 1; + }); + console.table(filesInfo); + } catch (error) { + throw error; + } +} + +export { + cmd_ls +} diff --git a/src/commands/mv.js b/src/commands/mv.js new file mode 100644 index 0000000..bfea544 --- /dev/null +++ b/src/commands/mv.js @@ -0,0 +1,12 @@ +import { cmd_cp } from './cp.js'; +import { cmd_rm } from './rm.js'; + +const cmd_mv = async (ctx, args) => { + if (args.length !== 2) throw new Error(); + await cmd_cp(ctx, args); + await cmd_rm(ctx, [args[0]]); +} + +export { + cmd_mv +} diff --git a/src/commands/os.js b/src/commands/os.js new file mode 100644 index 0000000..cff9145 --- /dev/null +++ b/src/commands/os.js @@ -0,0 +1,43 @@ +import os from 'os'; + +const cmd_os = async (ctx, args) => { + if (args.length !== 1) throw new Error(); + + const re = /^--.+/; + if (!re.test(args[0])) throw new Error(); + + const opt = args[0].substring(2); + + switch (opt) { + case 'EOL': + const eol = JSON.stringify(os.EOL); + console.log(`Default system End-Of-Line is \x1b[1m\x1b[32m${eol}\x1b[0m`); + break; + case 'cpus': + const cpus = os.cpus(); + console.log(`Number of CPUs is [\x1b[1m\x1b[32m${cpus.length}\x1b[0m]`); + const cpuInfo = []; + cpus.forEach(cpu => { + cpuInfo.push({ 'Model': cpu.model, 'Clock rate (GHz)': (cpu.speed / 1000).toFixed(2) }); + }); + console.table(cpuInfo); + break; + case 'homedir': + console.log(`The HOMEDIR is [\x1b[1m\x1b[32m${ctx.homedir}\x1b[0m]`); + break; + case 'username': + const username = os.userInfo().username; + console.log(`The USERNAME is [\x1b[1m\x1b[32m${username}\x1b[0m]`); + break; + case 'architecture': + console.log(`The ARCHITECTURE is [\x1b[1m\x1b[32m${process.arch}\x1b[0m]`); + break; + + default: + throw new Error(); + } +} + +export { + cmd_os +} diff --git a/src/commands/rm.js b/src/commands/rm.js new file mode 100644 index 0000000..1076ddd --- /dev/null +++ b/src/commands/rm.js @@ -0,0 +1,21 @@ +import fs from 'fs/promises'; +import path from 'path'; + +const cmd_rm = async (ctx, args) => { + if (args.length !== 1) throw new Error(); + let filePath; + if (path.isAbsolute(args[0])) { + filePath = args[0]; + } + else { + filePath = path.join(ctx.cwd, args[0]); + } + + const fileInfo = await fs.stat(filePath); + if (fileInfo.isDirectory()) throw new Error(); + await fs.unlink(filePath); +} + +export { + cmd_rm +} diff --git a/src/commands/rn.js b/src/commands/rn.js new file mode 100644 index 0000000..ad99b6f --- /dev/null +++ b/src/commands/rn.js @@ -0,0 +1,24 @@ +import fs from 'fs/promises'; +import path from 'path'; + +const cmd_rn = async (ctx, args) => { + if (args.length !== 2) throw new Error(); + let filePaths = []; + args.forEach(file => { + if (path.isAbsolute(file)) { + filePaths.push(file); + } + else { + filePaths.push(path.join(ctx.cwd, file)); + } + }); + await fs.access(filePaths[1], fs.constants.F_OK) + .then(() => { + throw new Error(); + }); + await fs.rename(filePaths[0], filePaths[1]); +} + +export { + cmd_rn +} diff --git a/src/commands/up.js b/src/commands/up.js new file mode 100644 index 0000000..819a9b0 --- /dev/null +++ b/src/commands/up.js @@ -0,0 +1,14 @@ +import path from 'path'; + +const cmd_up = (ctx, args) => { + if (ctx.cwd !== ctx.rootDir) { + const upDir = path.dirname(ctx.cwd); + process.chdir(upDir); + ctx.cwd = process.cwd(); + } +} + +export { + cmd_up +} + diff --git a/src/file_manager.js b/src/file_manager.js new file mode 100644 index 0000000..4acf90b --- /dev/null +++ b/src/file_manager.js @@ -0,0 +1,178 @@ +import os from 'os'; +import path from 'path'; +import readline from 'readline/promises'; + +import { cmd_exit } from "./commands/exit.js"; +import { cmd_up } from "./commands/up.js"; +import { cmd_cd } from "./commands/cd.js"; +import { cmd_ls } from "./commands/ls.js"; +import { cmd_cat } from "./commands/cat.js"; +import { cmd_add } from "./commands/add.js"; +import { cmd_rn } from "./commands/rn.js"; +import { cmd_cp } from "./commands/cp.js"; +import { cmd_rm } from "./commands/rm.js"; +import { cmd_mv } from "./commands/mv.js"; +import { cmd_os } from "./commands/os.js"; +import { cmd_hash } from "./commands/hash.js"; +import { cmd_compress, cmd_decompress } from "./commands/arch.js"; + + +const sigint = process.platform === 'win32' ? 'SIGBREAK' : 'SIGINT'; + +const invInputStr = '\x1b[1m\x1b[33mInvalid input\x1b[0m'; +const operationFailed = '\x1b[1m\x1b[31mOperation failed\x1b[0m'; + +const commands = { + '.exit': { 'cmd': 'cmd_exit', 'min_args': 0 }, + 'up': { 'cmd': 'cmd_up', 'min_args': 0 }, + 'cd': { 'cmd': 'cmd_cd', 'min_args': 1 }, + 'ls': { 'cmd': 'cmd_ls', 'min_args': 0 }, + 'cat': { 'cmd': 'cmd_cat', 'min_args': 1 }, + 'add': { 'cmd': 'cmd_add', 'min_args': 1 }, + 'rn': { 'cmd': 'cmd_rn', 'min_args': 2 }, + 'cp': { 'cmd': 'cmd_cp', 'min_args': 2 }, + 'rm': { 'cmd': 'cmd_rm', 'min_args': 1 }, + 'mv': { 'cmd': 'cmd_mv', 'min_args': 1 }, + 'os': { 'cmd': 'cmd_os', 'min_args': 1 }, + 'hash': { 'cmd': 'cmd_hash', 'min_args': 1 }, + 'compress': { 'cmd': 'cmd_compress', 'min_args': 1 }, + 'decompress': { 'cmd': 'cmd_decompress', 'min_args': 1 }, +}; + + +const printPromt = (ctx) => { + process.stdout.write(`\x1b[37mYou are currently in \x1b[1m${ctx['cwd']}\x1b[0m\n`); +} + + +const getUsernameFromArgs = () => { + const re = /^--username=.+/; + const args = process.argv; + for (let i = 0; i < args.length; i++) { + if (re.test(args[i])) { + return args[i].split(/=/)[1]; + } + } + return 'Anonymous'; +}; + + +const homedir = os.homedir(); +const rootDir = path.parse(process.cwd()).root; +const pathDelimiter = path.delimiter; +const dirSeparator = path.sep; +const username = getUsernameFromArgs(); + +const context = { + 'cwd': homedir, + 'homedir': homedir, + 'username': username, + 'rootDir': rootDir, + 'pathDelimiter': pathDelimiter, + 'dirSeparator': dirSeparator, +} + + +const parseCommand = (command) => { + const args = []; + let curArg = ''; + let i = 0; + let p = ''; + let s = ''; + while (i < command.length) { + const char = command[i]; + if (s == "'") { + if (char !== "'") { + curArg = curArg + char; + } + else { + if (curArg.length > 0) args.push(curArg); + curArg = ''; + s = ''; + } + } + else if (char !== ' ') { + if (char === "'" && (p === '' || p === ' ')) { + s = char; + } + else { + curArg = curArg + char; + } + } + else { + if (curArg.length > 0) args.push(curArg); + curArg = ''; + s = ''; + } + p = char; + i++; + continue; + } + if (curArg.length > 0) { + if (s != "'") { + args.push(curArg); + } + else { + throw new Error(invInputStr); + } + } + return args; +} + +const execCmd = async (cmd, context, args) => { + const cmdFunc = eval(cmd); + await cmdFunc(context, args); +} + +const processUserInput = async (chunk) => { + const cmdString = chunk.toString().replace(/[\r\n]+$/g, '').replace(/^\s+/, '').replace(/\s+$/, ''); + if (cmdString.length === 0) return; + try { + const args = parseCommand(cmdString); + if (args.length === 0 || args[0].length === 0) throw new Error(); + + let cmd = args[0]; + // console.debug('command: [' + cmd + ']'); + + if (!commands.hasOwnProperty(cmd)) throw new Error(); + + if (commands[cmd]['min_args'] > args.length - 1) throw new Error(); + + try { + await execCmd(commands[cmd]['cmd'], context, args.slice(1)); + } catch (error) { + // console.log(error); + console.log(operationFailed); + } + } catch (error) { + console.log(invInputStr); + } + printPromt(context); +}; + + +process.chdir(homedir); +console.log(`Welcome to the File Manager, ${username}!`); +printPromt(context); + +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout +}); + +process.on('exit', (code) => { + console.log(`\nThank you for using File Manager, ${username}, goodbye!`); + rl.close(); +}); + +// process.on('SIGINT', () => { +// console.log('\n'); +// process.exit(0); +// }); + + +do { + const answer = await rl.question('Input a command > '); + await processUserInput(answer); +} while (1); +