From a5667a064f3949fce87a2d9a7e044f5445d05323 Mon Sep 17 00:00:00 2001 From: BorysovskaOA Date: Thu, 12 Mar 2026 22:52:34 +0200 Subject: [PATCH 01/14] Added initialization of repl and handling default comands --- src/commands.js | 7 +++++ src/main.js | 7 +++++ src/navigation.js | 24 +++++++++++++++ src/repl.js | 72 +++++++++++++++++++++++++++++++++++++++++++++ src/utils/errors.js | 11 +++++++ 5 files changed, 121 insertions(+) create mode 100644 src/commands.js create mode 100644 src/utils/errors.js diff --git a/src/commands.js b/src/commands.js new file mode 100644 index 0000000..84fa287 --- /dev/null +++ b/src/commands.js @@ -0,0 +1,7 @@ +import { upHandler, cdHandler, lsHandler } from './navigation.js' + +export const COMMAND_HANDLERS_MAP = { + up: upHandler, + cd: cdHandler, + ls: lsHandler +} \ No newline at end of file diff --git a/src/main.js b/src/main.js index e69de29..c344a62 100644 --- a/src/main.js +++ b/src/main.js @@ -0,0 +1,7 @@ +import { initRepl } from './repl.js'; + +const init = () => { + initRepl(); +} + +init(); \ No newline at end of file diff --git a/src/navigation.js b/src/navigation.js index e69de29..8efe0f8 100644 --- a/src/navigation.js +++ b/src/navigation.js @@ -0,0 +1,24 @@ +import os from 'node:os'; + +let currentDirectory = os.homedir(); + +export const onChangeCurrentDirectory = (newDirectory) => { + currentDirectory = newDirectory; +} + +export const getCurrentDirectory = () => { + return currentDirectory; +} + +export const upHandler = () => { + console.log('upHandler'); + +} + +export const cdHandler = () => { + console.log('cdHandler'); +} + +export const lsHandler = () => { + console.log('lsHandler'); +} \ No newline at end of file diff --git a/src/repl.js b/src/repl.js index e69de29..07c11e5 100644 --- a/src/repl.js +++ b/src/repl.js @@ -0,0 +1,72 @@ + + +import readline from 'node:readline'; +import { InvalidInputError, INVALID_INPUT_ERROR_CODE } from './utils/errors.js'; +import { getCurrentDirectory } from './navigation.js'; +import { COMMAND_HANDLERS_MAP } from './commands.js'; + +const WELCOME_TEXT = 'Welcome to Data Processing CLI!'; +const EXIT_TEXT = 'Thank you for using Data Processing CLI!'; +const INVALID_COMMAND_TEXT = 'Invalid input'; +const OPERATION_FAILED_TEXT = 'Operation failed'; +const CURRENT_DIRECTORY_PREFIX = 'You are currently in'; + +export const initRepl = () => { + const onSuccess = () => { + console.log(`${CURRENT_DIRECTORY_PREFIX} ${getCurrentDirectory()}`); + } + + const onError = (err) => { + if (err.code === INVALID_INPUT_ERROR_CODE) { + console.log(INVALID_COMMAND_TEXT); + } else { + console.log(OPERATION_FAILED_TEXT); + } + } + + const handleCommand = async(command, commandArgs) => { + if (command in COMMAND_HANDLERS_MAP) { + try { + await COMMAND_HANDLERS_MAP[command](commandArgs); + onSuccess() + } catch (err) { + onError(err) + } + + } else { + onError(new InvalidInputError()); + } + } + + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + prompt: '> ' + }); + + rl.on('line', async (line) => { + const lineTrimmed = line.trim().toLowerCase(); + const [command, ...commandArgs] = lineTrimmed.split(' '); + + if (command === '.exit' && commandArgs.length === 0) { + rl.close() + } else { + await handleCommand(command, commandArgs); + rl.prompt(); + } + }); + + rl.on('SIGINT', () => { + rl.close() + }); + + rl.on('close', () => { + console.log(EXIT_TEXT) + process.exit(0); + }); + + console.log(WELCOME_TEXT); + // Initial display of directory + onSuccess(); + rl.prompt(); +} diff --git a/src/utils/errors.js b/src/utils/errors.js new file mode 100644 index 0000000..4402dc9 --- /dev/null +++ b/src/utils/errors.js @@ -0,0 +1,11 @@ +export const INVALID_INPUT_ERROR_CODE = 'INVALID_INPUT'; + +export class InvalidInputError extends Error { + constructor(message) { + super(message); + this.name = 'InvalidInputError'; + this.code = INVALID_INPUT_ERROR_CODE; + + Error.captureStackTrace(this, this.constructor); + } +} \ No newline at end of file From 5a4d9ac03a79f54c81f6c5f4358819587fe8e250 Mon Sep 17 00:00:00 2001 From: BorysovskaOA Date: Thu, 12 Mar 2026 23:14:31 +0200 Subject: [PATCH 02/14] Added up commad --- src/main.js | 4 ++++ src/navigation.js | 22 ++++++++++------------ src/repl.js | 48 +++++++++++++++++++++++------------------------ 3 files changed, 38 insertions(+), 36 deletions(-) diff --git a/src/main.js b/src/main.js index c344a62..628c711 100644 --- a/src/main.js +++ b/src/main.js @@ -1,6 +1,10 @@ +import process from 'node:process'; +import os from 'node:os'; import { initRepl } from './repl.js'; const init = () => { + process.chdir(os.homedir()); + initRepl(); } diff --git a/src/navigation.js b/src/navigation.js index 8efe0f8..ff53143 100644 --- a/src/navigation.js +++ b/src/navigation.js @@ -1,18 +1,16 @@ import os from 'node:os'; - -let currentDirectory = os.homedir(); - -export const onChangeCurrentDirectory = (newDirectory) => { - currentDirectory = newDirectory; -} - -export const getCurrentDirectory = () => { - return currentDirectory; -} +import path from 'node:path'; +import process from 'node:process'; export const upHandler = () => { - console.log('upHandler'); - + if (process.cwd() !== os.homedir()) { + const newDirectory = process.cwd() + .split(path.sep) + .slice(0, -1) + .join(path.sep); + + process.chdir(newDirectory); + } } export const cdHandler = () => { diff --git a/src/repl.js b/src/repl.js index 07c11e5..83b8505 100644 --- a/src/repl.js +++ b/src/repl.js @@ -1,8 +1,8 @@ import readline from 'node:readline'; +import process from 'node:process'; import { InvalidInputError, INVALID_INPUT_ERROR_CODE } from './utils/errors.js'; -import { getCurrentDirectory } from './navigation.js'; import { COMMAND_HANDLERS_MAP } from './commands.js'; const WELCOME_TEXT = 'Welcome to Data Processing CLI!'; @@ -11,33 +11,33 @@ const INVALID_COMMAND_TEXT = 'Invalid input'; const OPERATION_FAILED_TEXT = 'Operation failed'; const CURRENT_DIRECTORY_PREFIX = 'You are currently in'; -export const initRepl = () => { - const onSuccess = () => { - console.log(`${CURRENT_DIRECTORY_PREFIX} ${getCurrentDirectory()}`); - } +const onSuccess = () => { + console.log(`${CURRENT_DIRECTORY_PREFIX} ${process.cwd()}`); +} - const onError = (err) => { - if (err.code === INVALID_INPUT_ERROR_CODE) { - console.log(INVALID_COMMAND_TEXT); - } else { - console.log(OPERATION_FAILED_TEXT); - } +const onError = (err) => { + if (err.code === INVALID_INPUT_ERROR_CODE) { + console.log(INVALID_COMMAND_TEXT); + } else { + console.log(OPERATION_FAILED_TEXT); } - - const handleCommand = async(command, commandArgs) => { - if (command in COMMAND_HANDLERS_MAP) { - try { - await COMMAND_HANDLERS_MAP[command](commandArgs); - onSuccess() - } catch (err) { - onError(err) - } - - } else { - onError(new InvalidInputError()); +} + +const handleCommand = async(command, commandArgs) => { + if (command in COMMAND_HANDLERS_MAP) { + try { + await COMMAND_HANDLERS_MAP[command](commandArgs); + onSuccess() + } catch (err) { + onError(err) } + + } else { + onError(new InvalidInputError()); } +} +export const initRepl = () => { const rl = readline.createInterface({ input: process.stdin, output: process.stdout, @@ -57,7 +57,7 @@ export const initRepl = () => { }); rl.on('SIGINT', () => { - rl.close() + rl.close(); }); rl.on('close', () => { From 41222974a3fa65c0f9caf5c946b71b1ba086a585 Mon Sep 17 00:00:00 2001 From: BorysovskaOA Date: Fri, 13 Mar 2026 14:14:26 +0200 Subject: [PATCH 03/14] Added cd command --- src/commands.js | 2 +- src/main.js | 2 +- src/navigation.js | 22 ++++++++++++++++++---- src/repl.js | 4 +++- src/utils/errors.js | 2 +- 5 files changed, 24 insertions(+), 8 deletions(-) diff --git a/src/commands.js b/src/commands.js index 84fa287..1b97e71 100644 --- a/src/commands.js +++ b/src/commands.js @@ -4,4 +4,4 @@ export const COMMAND_HANDLERS_MAP = { up: upHandler, cd: cdHandler, ls: lsHandler -} \ No newline at end of file +} diff --git a/src/main.js b/src/main.js index 628c711..9e469e6 100644 --- a/src/main.js +++ b/src/main.js @@ -8,4 +8,4 @@ const init = () => { initRepl(); } -init(); \ No newline at end of file +init(); diff --git a/src/navigation.js b/src/navigation.js index ff53143..0f6011d 100644 --- a/src/navigation.js +++ b/src/navigation.js @@ -1,6 +1,8 @@ import os from 'node:os'; import path from 'node:path'; import process from 'node:process'; +import fs from 'node:fs/promises'; +import { InvalidInputError } from './utils/errors.js'; export const upHandler = () => { if (process.cwd() !== os.homedir()) { @@ -8,15 +10,27 @@ export const upHandler = () => { .split(path.sep) .slice(0, -1) .join(path.sep); - + process.chdir(newDirectory); } } -export const cdHandler = () => { - console.log('cdHandler'); +export const cdHandler = async (args) => { + const pathToDirectory = args[0]; + if (!pathToDirectory) { + throw new InvalidInputError('No path/to/directory'); + } + const newDirPath = path.resolve(process.cwd(), pathToDirectory); + + const stats = await fs.stat(newDirPath); + + if (stats.isDirectory()) { + process.chdir(newDirPath); + } else { + throw new InvalidInputError('Path is not a directory'); + } } export const lsHandler = () => { console.log('lsHandler'); -} \ No newline at end of file +} diff --git a/src/repl.js b/src/repl.js index 83b8505..949eedf 100644 --- a/src/repl.js +++ b/src/repl.js @@ -16,6 +16,8 @@ const onSuccess = () => { } const onError = (err) => { + // For testing only, TODO: delete before review + console.log(err); if (err.code === INVALID_INPUT_ERROR_CODE) { console.log(INVALID_COMMAND_TEXT); } else { @@ -33,7 +35,7 @@ const handleCommand = async(command, commandArgs) => { } } else { - onError(new InvalidInputError()); + onError(new InvalidInputError('Invalid command')); } } diff --git a/src/utils/errors.js b/src/utils/errors.js index 4402dc9..21e3055 100644 --- a/src/utils/errors.js +++ b/src/utils/errors.js @@ -8,4 +8,4 @@ export class InvalidInputError extends Error { Error.captureStackTrace(this, this.constructor); } -} \ No newline at end of file +} From bcd94b44c336cdd49416ec6d6a27fcafb400d245 Mon Sep 17 00:00:00 2001 From: BorysovskaOA Date: Fri, 13 Mar 2026 14:56:58 +0200 Subject: [PATCH 04/14] Added console color and ls command --- src/navigation.js | 28 ++++++++++++++++++++++++++-- src/repl.js | 14 ++++++++++---- 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/src/navigation.js b/src/navigation.js index 0f6011d..0ca8dee 100644 --- a/src/navigation.js +++ b/src/navigation.js @@ -31,6 +31,30 @@ export const cdHandler = async (args) => { } } -export const lsHandler = () => { - console.log('lsHandler'); +export const lsHandler = async () => { + const dirEntities = await fs.readdir(process.cwd(), { withFileTypes: true }); + + const folders = []; + const files = []; + + let maxEntitName = 0; + + dirEntities.forEach((dirEnt) => { + maxEntitName = Math.max(maxEntitName, dirEnt.name.length); + + if (dirEnt.isDirectory()) { + folders.push(dirEnt.name); + } else if (dirEnt.isFile()) { + files.push(dirEnt.name); + } + }); + + const formatEntityName = (entitiName) => `${entitiName}${' '.repeat(maxEntitName - entitiName.length)}` + + folders.sort((a, b) => a.localeCompare(b)).forEach((ent) => { + console.log(`${formatEntityName(ent)} [folder]`) + }); + files.sort((a, b) => a.localeCompare(b)).forEach((ent) => { + console.log(`${formatEntityName(ent)} [file]`) + }); } diff --git a/src/repl.js b/src/repl.js index 949eedf..9c2bf71 100644 --- a/src/repl.js +++ b/src/repl.js @@ -11,17 +11,23 @@ const INVALID_COMMAND_TEXT = 'Invalid input'; const OPERATION_FAILED_TEXT = 'Operation failed'; const CURRENT_DIRECTORY_PREFIX = 'You are currently in'; +const ANSI_COLORS = { + red: '\x1b[31m', + green: '\x1b[32m', +}; +const ANSI_COLOR_RESET = '\x1b[0m'; + const onSuccess = () => { - console.log(`${CURRENT_DIRECTORY_PREFIX} ${process.cwd()}`); + console.log(`${ANSI_COLORS.green}${CURRENT_DIRECTORY_PREFIX} ${process.cwd()}${ANSI_COLOR_RESET}`); } const onError = (err) => { - // For testing only, TODO: delete before review + // For testing only, TODO: delete before review next line console.log(err); if (err.code === INVALID_INPUT_ERROR_CODE) { - console.log(INVALID_COMMAND_TEXT); + console.log(`${ANSI_COLORS.red}${INVALID_COMMAND_TEXT}${ANSI_COLOR_RESET}`); } else { - console.log(OPERATION_FAILED_TEXT); + console.log(`${ANSI_COLORS.red}${OPERATION_FAILED_TEXT}${ANSI_COLOR_RESET}`); } } From f65014225937a04eb2b2da18713697c25e64ef4c Mon Sep 17 00:00:00 2001 From: BorysovskaOA Date: Fri, 13 Mar 2026 18:02:57 +0200 Subject: [PATCH 05/14] Added csv to json, handling of custom cwd --- src/commands.js | 6 ++- src/commands/csvToJson.js | 90 +++++++++++++++++++++++++++++++++++++++ src/commands/jsonToCsv.js | 3 ++ src/cwdState.js | 17 ++++++++ src/main.js | 4 -- src/navigation.js | 22 ++++------ src/repl.js | 3 +- src/utils/argParser.js | 32 ++++++++++++++ src/utils/pathResolver.js | 6 +++ 9 files changed, 164 insertions(+), 19 deletions(-) create mode 100644 src/cwdState.js diff --git a/src/commands.js b/src/commands.js index 1b97e71..24a69db 100644 --- a/src/commands.js +++ b/src/commands.js @@ -1,7 +1,11 @@ +import { csvToJsonHandler } from './commands/csvToJson.js' +import { jsonToCsvHandler } from './commands/jsonToCsv.js' import { upHandler, cdHandler, lsHandler } from './navigation.js' export const COMMAND_HANDLERS_MAP = { up: upHandler, cd: cdHandler, - ls: lsHandler + ls: lsHandler, + 'csv-to-json': csvToJsonHandler, + 'json-to-csv': jsonToCsvHandler, } diff --git a/src/commands/csvToJson.js b/src/commands/csvToJson.js index e69de29..ff0522b 100644 --- a/src/commands/csvToJson.js +++ b/src/commands/csvToJson.js @@ -0,0 +1,90 @@ +import { Transform } from 'node:stream'; +import { pipeline } from 'node:stream/promises'; +import { createReadStream, createWriteStream } from 'node:fs'; +import { argParser } from '../utils/argParser.js'; +import { pathResolver } from '../utils/pathResolver.js'; + +const EXTRA_HEADER_NAME = 'Extra'; +const INDENT = 2; + +export const csvToJsonHandler = async (args) => { + const inputArg = argParser(args, 'input', true); + const outputArg = argParser(args, 'output', true); + + const inputPath = pathResolver(inputArg); + const outputPath = pathResolver(outputArg); + + const inputStream = createReadStream(inputPath, { encoding: 'utf-8' }); + const outputStream = createWriteStream(outputPath, { encoding: 'utf-8' }); + + let transformBuffer = ''; + let headers = []; + let isFirstDataRow = true; + + const getDataPrefix = () => { + let prefix = ',\n'; + if (isFirstDataRow) { + prefix = ''; + isFirstDataRow = false; + } + return prefix; + } + + const transformDataToJsonString = (data) => { + const obj = {}; + let unknownHeaderCounter = 1; + data.forEach((d, i) => { + let header; + if (headers[i]) { + header = headers[i]; + } else { + header = `${EXTRA_HEADER_NAME}${unknownHeaderCounter}`; + unknownHeaderCounter++; + } + obj[header] = d; + }); + return JSON.stringify(obj, null, INDENT).replace(/^/gm, ' '.repeat(INDENT)); + } + + const transformStream = new Transform({ + transform(chunk, _, callback) { + transformBuffer += chunk; + + const lines = transformBuffer.split(/\r?\n/); + transformBuffer = lines.pop(); + + lines.forEach((line) => { + if (!line.trim()) { + return; + } + + const parsedLineData = line.split(','); + + if (!headers.length) { + headers = parsedLineData.map(h => h.trim()); + this.push('[\n'); + } else { + this.push(`${getDataPrefix()}${transformDataToJsonString(parsedLineData)}`); + } + }); + + callback(); + }, + flush(callback) { + if (!transformBuffer.trim()) { + return '\n]'; + } + + const parsedLineData = transformBuffer.split(','); + this.push(`${getDataPrefix()}${transformDataToJsonString(parsedLineData)}\n]`); + + transformBuffer = ''; + callback(); + } + }) + inputStream.on('error', (err) => { throw err }); + transformStream.on('error', (err) => { throw err }); + outputStream.on('error', (err) => { throw err }); + + await pipeline(inputStream, transformStream, outputStream); +} \ No newline at end of file diff --git a/src/commands/jsonToCsv.js b/src/commands/jsonToCsv.js index e69de29..b659f52 100644 --- a/src/commands/jsonToCsv.js +++ b/src/commands/jsonToCsv.js @@ -0,0 +1,3 @@ +export const jsonToCsvHandler = async () => { + +} \ No newline at end of file diff --git a/src/cwdState.js b/src/cwdState.js new file mode 100644 index 0000000..e118cd3 --- /dev/null +++ b/src/cwdState.js @@ -0,0 +1,17 @@ +import os from 'node:os'; +import { InvalidInputError } from './utils/errors.js'; + +export const initialCwd = os.homedir(); +let cwd = initialCwd; + +export const getCwd = () => { + return cwd; +} + +// dir - absolute path +export const chCwd = (dir) => { + if (!dir.startsWith(initialCwd)) { + throw new InvalidInputError('Cannot go higher than initial directory'); + } + cwd = dir; +} \ No newline at end of file diff --git a/src/main.js b/src/main.js index 9e469e6..acff6a3 100644 --- a/src/main.js +++ b/src/main.js @@ -1,10 +1,6 @@ -import process from 'node:process'; -import os from 'node:os'; import { initRepl } from './repl.js'; const init = () => { - process.chdir(os.homedir()); - initRepl(); } diff --git a/src/navigation.js b/src/navigation.js index 0ca8dee..bbac816 100644 --- a/src/navigation.js +++ b/src/navigation.js @@ -1,17 +1,13 @@ -import os from 'node:os'; -import path from 'node:path'; -import process from 'node:process'; import fs from 'node:fs/promises'; +import { getCwd, chCwd, initialCwd } from './cwdState.js'; import { InvalidInputError } from './utils/errors.js'; +import { pathResolver } from './utils/pathResolver.js'; -export const upHandler = () => { - if (process.cwd() !== os.homedir()) { - const newDirectory = process.cwd() - .split(path.sep) - .slice(0, -1) - .join(path.sep); - process.chdir(newDirectory); +export const upHandler = () => { + if (getCwd() !== initialCwd) { + const newDirectory = pathResolver(`${getCwd}/..`); + chDir(newDirectory); } } @@ -20,19 +16,19 @@ export const cdHandler = async (args) => { if (!pathToDirectory) { throw new InvalidInputError('No path/to/directory'); } - const newDirPath = path.resolve(process.cwd(), pathToDirectory); + const newDirPath = pathResolver(pathToDirectory); const stats = await fs.stat(newDirPath); if (stats.isDirectory()) { - process.chdir(newDirPath); + chCwd(newDirPath) } else { throw new InvalidInputError('Path is not a directory'); } } export const lsHandler = async () => { - const dirEntities = await fs.readdir(process.cwd(), { withFileTypes: true }); + const dirEntities = await fs.readdir(getCwd(), { withFileTypes: true }); const folders = []; const files = []; diff --git a/src/repl.js b/src/repl.js index 9c2bf71..a41c61f 100644 --- a/src/repl.js +++ b/src/repl.js @@ -2,6 +2,7 @@ import readline from 'node:readline'; import process from 'node:process'; +import { getCwd } from './cwdState.js'; import { InvalidInputError, INVALID_INPUT_ERROR_CODE } from './utils/errors.js'; import { COMMAND_HANDLERS_MAP } from './commands.js'; @@ -18,7 +19,7 @@ const ANSI_COLORS = { const ANSI_COLOR_RESET = '\x1b[0m'; const onSuccess = () => { - console.log(`${ANSI_COLORS.green}${CURRENT_DIRECTORY_PREFIX} ${process.cwd()}${ANSI_COLOR_RESET}`); + console.log(`${ANSI_COLORS.green}${CURRENT_DIRECTORY_PREFIX} ${getCwd()}${ANSI_COLOR_RESET}`); } const onError = (err) => { diff --git a/src/utils/argParser.js b/src/utils/argParser.js index e69de29..9dddadd 100644 --- a/src/utils/argParser.js +++ b/src/utils/argParser.js @@ -0,0 +1,32 @@ +import { InvalidInputError } from './errors.js'; + +export const argParser = (args, argName, required = false) => { + const argIndex = args.findIndex(arg => arg.startsWith(`--${argName}`)); + + if (argIndex === -1) { + if (required) { + throw new InvalidInputError('Argument is required'); + } else { + return; + } + } + + const arg = args[argIndex]; + let argValue; + + if (arg.includes('=')) { + argValue = arg.split('=')[1]; + } else { + const nextArgvValue = args[argIndex + 1]; + + if (nextArgvValue && !nextArgvValue.startsWith('--')) { + argValue = nextArgvValue; + } + } + + if (!argValue && required) { + throw new InvalidInputError('Argument is required'); + } + + return argValue; +} \ No newline at end of file diff --git a/src/utils/pathResolver.js b/src/utils/pathResolver.js index e69de29..2b2cb9c 100644 --- a/src/utils/pathResolver.js +++ b/src/utils/pathResolver.js @@ -0,0 +1,6 @@ +import path from 'node:path'; +import { getCwd } from '../cwdState.js'; + +export const pathResolver = (pathArg) => { + return path.resolve(getCwd(), pathArg); +} \ No newline at end of file From 745d526a428273d821861f2673b8e57fad6c1af8 Mon Sep 17 00:00:00 2001 From: BorysovskaOA Date: Fri, 13 Mar 2026 21:04:13 +0200 Subject: [PATCH 06/14] Added json to csv --- src/commands/csvToJson.js | 51 ++++++++++++------------------ src/commands/jsonToCsv.js | 37 ++++++++++++++++++++-- src/repl.js | 2 +- src/utils/argParser.js | 2 +- src/utils/fileConversionHandler.js | 25 +++++++++++++++ 5 files changed, 82 insertions(+), 35 deletions(-) create mode 100644 src/utils/fileConversionHandler.js diff --git a/src/commands/csvToJson.js b/src/commands/csvToJson.js index ff0522b..af3c1bb 100644 --- a/src/commands/csvToJson.js +++ b/src/commands/csvToJson.js @@ -1,30 +1,19 @@ import { Transform } from 'node:stream'; -import { pipeline } from 'node:stream/promises'; -import { createReadStream, createWriteStream } from 'node:fs'; -import { argParser } from '../utils/argParser.js'; -import { pathResolver } from '../utils/pathResolver.js'; +import { fileConfersionHandler } from '../utils/fileConversionHandler.js'; const EXTRA_HEADER_NAME = 'Extra'; const INDENT = 2; +const LINE_SEPARATOR = /\r?\n/; -export const csvToJsonHandler = async (args) => { - const inputArg = argParser(args, 'input', true); - const outputArg = argParser(args, 'output', true); - - const inputPath = pathResolver(inputArg); - const outputPath = pathResolver(outputArg); - - const inputStream = createReadStream(inputPath, { encoding: 'utf-8' }); - const outputStream = createWriteStream(outputPath, { encoding: 'utf-8' }); - +const getCsvToJsonTransformSteam = () => { let transformBuffer = ''; let headers = []; let isFirstDataRow = true; const getDataPrefix = () => { - let prefix = ',\n'; + let prefix = `,\n${' '.repeat(INDENT)}`; if (isFirstDataRow) { - prefix = ''; + prefix = ' '.repeat(INDENT); isFirstDataRow = false; } return prefix; @@ -43,23 +32,23 @@ export const csvToJsonHandler = async (args) => { } obj[header] = d; }); - return JSON.stringify(obj, null, INDENT).replace(/^/gm, ' '.repeat(INDENT)); + return JSON.stringify(obj); } - - const transformStream = new Transform({ + + return new Transform({ transform(chunk, _, callback) { transformBuffer += chunk; - const lines = transformBuffer.split(/\r?\n/); + const lines = transformBuffer.split(LINE_SEPARATOR); transformBuffer = lines.pop(); - + lines.forEach((line) => { if (!line.trim()) { return; } - + const parsedLineData = line.split(','); - + if (!headers.length) { headers = parsedLineData.map(h => h.trim()); this.push('[\n'); @@ -74,17 +63,17 @@ export const csvToJsonHandler = async (args) => { if (!transformBuffer.trim()) { return '\n]'; } - + const parsedLineData = transformBuffer.split(','); - this.push(`${getDataPrefix()}${transformDataToJsonString(parsedLineData)}\n]`); + this.push(`${getDataPrefix()}${transformDataToJsonString(parsedLineData)}\n]\n`); transformBuffer = ''; callback(); } - }) - inputStream.on('error', (err) => { throw err }); - transformStream.on('error', (err) => { throw err }); - outputStream.on('error', (err) => { throw err }); + }); +} - await pipeline(inputStream, transformStream, outputStream); -} \ No newline at end of file +export const csvToJsonHandler = async (args) => { + const transformStream = getCsvToJsonTransformSteam(); + return fileConfersionHandler(args, '.csv', transformStream); +} diff --git a/src/commands/jsonToCsv.js b/src/commands/jsonToCsv.js index b659f52..d2cc13c 100644 --- a/src/commands/jsonToCsv.js +++ b/src/commands/jsonToCsv.js @@ -1,3 +1,36 @@ -export const jsonToCsvHandler = async () => { +import { Transform } from 'node:stream'; +import { fileConfersionHandler } from '../utils/fileConversionHandler.js'; -} \ No newline at end of file +const getJsonToCsvTransformSteam = () => { + let jsonStringBuffer = ''; + + return new Transform({ + transform(chunk, _, callback) { + jsonStringBuffer += chunk; + callback(); + }, + flush(callback) { + const jsonData = JSON.parse(jsonStringBuffer); + if (!Array.isArray(jsonData)) { + throw new Error('Invalid json type'); + } + + const headerSet = new Set(); + jsonData.map((row) => Object.keys(row).forEach(h => headerSet.add(h))); + const headers = Array.from(headerSet); + + this.push(`${headers.join(',')}\n`) + + jsonData.forEach(row => { + const valuesInHeadersOrder = headers.map((header) => row[header] || ''); + this.push(`${valuesInHeadersOrder.join(',')}\n`); + }) + callback(); + } + }); +} + +export const jsonToCsvHandler = async (args) => { + const transformStream = getJsonToCsvTransformSteam(); + return fileConfersionHandler(args, '.json', transformStream); +} diff --git a/src/repl.js b/src/repl.js index a41c61f..57492f6 100644 --- a/src/repl.js +++ b/src/repl.js @@ -54,7 +54,7 @@ export const initRepl = () => { }); rl.on('line', async (line) => { - const lineTrimmed = line.trim().toLowerCase(); + const lineTrimmed = line.trim(); const [command, ...commandArgs] = lineTrimmed.split(' '); if (command === '.exit' && commandArgs.length === 0) { diff --git a/src/utils/argParser.js b/src/utils/argParser.js index 9dddadd..8051e95 100644 --- a/src/utils/argParser.js +++ b/src/utils/argParser.js @@ -2,7 +2,7 @@ import { InvalidInputError } from './errors.js'; export const argParser = (args, argName, required = false) => { const argIndex = args.findIndex(arg => arg.startsWith(`--${argName}`)); - + if (argIndex === -1) { if (required) { throw new InvalidInputError('Argument is required'); diff --git a/src/utils/fileConversionHandler.js b/src/utils/fileConversionHandler.js new file mode 100644 index 0000000..5320df4 --- /dev/null +++ b/src/utils/fileConversionHandler.js @@ -0,0 +1,25 @@ +import { pipeline } from 'node:stream/promises'; +import { extname } from 'node:path'; +import { createReadStream, createWriteStream } from 'node:fs'; +import { argParser } from '../utils/argParser.js'; +import { pathResolver } from '../utils/pathResolver.js'; +import { InvalidInputError } from '../utils/errors.js'; + +export const fileConfersionHandler = async (args, fileExtention, transformStream) => { + const inputArg = argParser(args, 'input', true); + const outputArg = argParser(args, 'output', true); + + const inputPath = pathResolver(inputArg); + const outputPath = pathResolver(outputArg); + + const inputFileExt = extname(inputPath).toLowerCase(); + + if (inputFileExt !== fileExtention) { + throw new InvalidInputError('Invalid file extention'); + } + + const inputStream = createReadStream(inputPath, { encoding: 'utf-8' }); + const outputStream = createWriteStream(outputPath, { encoding: 'utf-8' }); + + await pipeline(inputStream, transformStream, outputStream); +}; \ No newline at end of file From f138d1880e08e62cd1050d5d9a03ff4dc22c7f77 Mon Sep 17 00:00:00 2001 From: BorysovskaOA Date: Fri, 13 Mar 2026 22:13:33 +0200 Subject: [PATCH 07/14] Added count --- src/commands.js | 2 ++ src/commands/count.js | 56 +++++++++++++++++++++++++++++++++++++++ src/commands/jsonToCsv.js | 2 ++ 3 files changed, 60 insertions(+) diff --git a/src/commands.js b/src/commands.js index 24a69db..72f1082 100644 --- a/src/commands.js +++ b/src/commands.js @@ -1,3 +1,4 @@ +import { countHandler } from './commands/count.js' import { csvToJsonHandler } from './commands/csvToJson.js' import { jsonToCsvHandler } from './commands/jsonToCsv.js' import { upHandler, cdHandler, lsHandler } from './navigation.js' @@ -8,4 +9,5 @@ export const COMMAND_HANDLERS_MAP = { ls: lsHandler, 'csv-to-json': csvToJsonHandler, 'json-to-csv': jsonToCsvHandler, + count: countHandler, } diff --git a/src/commands/count.js b/src/commands/count.js index e69de29..bc49c3d 100644 --- a/src/commands/count.js +++ b/src/commands/count.js @@ -0,0 +1,56 @@ + +import { extname } from 'node:path'; +import { createReadStream } from 'node:fs'; +import { argParser } from '../utils/argParser.js'; +import { pathResolver } from '../utils/pathResolver.js'; +import { InvalidInputError } from '../utils/errors.js'; + +const LINE_SEPARATOR = /\r?\n/; +const WORD_SEPARATOR = /\s+/; + +const getWordsFromString = (str) => { + return str.split(WORD_SEPARATOR).filter(w => w.length > 0); +} + +export const countHandler = async (args) => { + const inputArg = argParser(args, 'input', true); + const inputPath = pathResolver(inputArg); + + const inputFileExt = extname(inputPath).toLowerCase(); + + if (inputFileExt !== '.txt') { + throw new InvalidInputError('Invalid file extention'); + } + + const readableStream = createReadStream(inputPath, { encoding: 'utf-8' }); + + let buffer = ''; + let linesCount = 0; + let wordsCount = 0; + let charsCount = 0; + + for await (const chunk of readableStream) { + buffer += chunk; + charsCount += chunk.length; + + const lines = buffer.split(LINE_SEPARATOR); + buffer = lines.pop(); + + linesCount += lines.length; + + const words = getWordsFromString(lines.join(' ')); + wordsCount += words.length; + } + + if (buffer.length) { + charsCount += buffer.length; + linesCount++; + + const words = getWordsFromString(buffer); + wordsCount += words.length; + } + + console.log(`Lines: ${linesCount}`); + console.log(`Words: ${wordsCount}`); + console.log(`Characters: ${charsCount}`); +} \ No newline at end of file diff --git a/src/commands/jsonToCsv.js b/src/commands/jsonToCsv.js index d2cc13c..34ea0ee 100644 --- a/src/commands/jsonToCsv.js +++ b/src/commands/jsonToCsv.js @@ -11,6 +11,7 @@ const getJsonToCsvTransformSteam = () => { }, flush(callback) { const jsonData = JSON.parse(jsonStringBuffer); + jsonStringBuffer = ''; if (!Array.isArray(jsonData)) { throw new Error('Invalid json type'); } @@ -25,6 +26,7 @@ const getJsonToCsvTransformSteam = () => { const valuesInHeadersOrder = headers.map((header) => row[header] || ''); this.push(`${valuesInHeadersOrder.join(',')}\n`); }) + callback(); } }); From 81196add3c9e4ad3c5787f21bef2a84ca031095c Mon Sep 17 00:00:00 2001 From: BorysovskaOA Date: Sat, 14 Mar 2026 12:36:20 +0200 Subject: [PATCH 08/14] Added hash and hashCompare, fixed parseArgs function --- src/commands.js | 12 ++++--- src/commands/count.js | 31 ++++++++-------- src/commands/hash.js | 34 ++++++++++++++++++ src/commands/hashCompare.js | 32 +++++++++++++++++ src/navigation.js | 6 ++-- src/utils/argParser.js | 57 +++++++++++++++++++----------- src/utils/fileConversionHandler.js | 17 ++++----- 7 files changed, 134 insertions(+), 55 deletions(-) diff --git a/src/commands.js b/src/commands.js index 72f1082..8ea52c2 100644 --- a/src/commands.js +++ b/src/commands.js @@ -1,7 +1,9 @@ -import { countHandler } from './commands/count.js' -import { csvToJsonHandler } from './commands/csvToJson.js' -import { jsonToCsvHandler } from './commands/jsonToCsv.js' -import { upHandler, cdHandler, lsHandler } from './navigation.js' +import { countHandler } from './commands/count.js'; +import { hashHandler } from './commands/hash.js'; +import { hashCompareHandler } from './commands/hashCompare.js'; +import { csvToJsonHandler } from './commands/csvToJson.js'; +import { jsonToCsvHandler } from './commands/jsonToCsv.js'; +import { upHandler, cdHandler, lsHandler } from './navigation.js'; export const COMMAND_HANDLERS_MAP = { up: upHandler, @@ -10,4 +12,6 @@ export const COMMAND_HANDLERS_MAP = { 'csv-to-json': csvToJsonHandler, 'json-to-csv': jsonToCsvHandler, count: countHandler, + hash: hashHandler, + 'hash-compare': hashCompareHandler } diff --git a/src/commands/count.js b/src/commands/count.js index bc49c3d..ceab0c2 100644 --- a/src/commands/count.js +++ b/src/commands/count.js @@ -2,27 +2,25 @@ import { extname } from 'node:path'; import { createReadStream } from 'node:fs'; import { argParser } from '../utils/argParser.js'; -import { pathResolver } from '../utils/pathResolver.js'; import { InvalidInputError } from '../utils/errors.js'; const LINE_SEPARATOR = /\r?\n/; const WORD_SEPARATOR = /\s+/; -const getWordsFromString = (str) => { - return str.split(WORD_SEPARATOR).filter(w => w.length > 0); -} +const getWordsFromString = (str) => str.split(WORD_SEPARATOR).filter(w => w.length > 0); +const getSum = (arr) => arr.reduce((acc, wc) => acc + wc, 0); export const countHandler = async (args) => { - const inputArg = argParser(args, 'input', true); - const inputPath = pathResolver(inputArg); - - const inputFileExt = extname(inputPath).toLowerCase(); + const parsedArgs = argParser(args, { + input: { type: 'path', required: true }, + }); + const inputFileExt = extname(parsedArgs.input).toLowerCase(); if (inputFileExt !== '.txt') { throw new InvalidInputError('Invalid file extention'); } - const readableStream = createReadStream(inputPath, { encoding: 'utf-8' }); + const readableStream = createReadStream(parsedArgs.input, { encoding: 'utf-8' }); let buffer = ''; let linesCount = 0; @@ -31,23 +29,22 @@ export const countHandler = async (args) => { for await (const chunk of readableStream) { buffer += chunk; - charsCount += chunk.length; const lines = buffer.split(LINE_SEPARATOR); + // Count all symbols in line and single line separator as 1 char + charsCount += getSum(lines.map((line) => line.length)) + (lines.length - 1); buffer = lines.pop(); linesCount += lines.length; - const words = getWordsFromString(lines.join(' ')); - wordsCount += words.length; + wordsCount += getSum(lines.map(line => getWordsFromString(line).length)); } - if (buffer.length) { - charsCount += buffer.length; - linesCount++; + // Add last line, even it's empty + linesCount++; - const words = getWordsFromString(buffer); - wordsCount += words.length; + if (buffer.length) { + wordsCount += getWordsFromString(buffer).length; } console.log(`Lines: ${linesCount}`); diff --git a/src/commands/hash.js b/src/commands/hash.js index e69de29..7406009 100644 --- a/src/commands/hash.js +++ b/src/commands/hash.js @@ -0,0 +1,34 @@ +import { createHash } from 'node:crypto'; +import { pipeline } from 'node:stream/promises'; +import { Readable } from 'node:stream'; +import { createReadStream, createWriteStream } from 'node:fs'; +import { argParser } from '../utils/argParser.js'; + +const SUPPORTED_ALGORYTHMS = ['sha256', 'md5', 'sha512']; + +export const hashHandler = async (args) => { + const parsedArgs = argParser(args, { + input: { type: 'path', required: true }, + algorithm: { type: 'string', default: 'sha256' }, + save: { type: 'boolean', default: false }, + }); + + if (!SUPPORTED_ALGORYTHMS.includes(parsedArgs.algorithm)) { + throw new Error('Algorithm is not supported'); + } + + const hash = createHash(parsedArgs.algorithm); + const readableStream = createReadStream(parsedArgs.input); + + await pipeline(readableStream, hash); + + const hashedValue = hash.digest('hex'); + + if (parsedArgs.save) { + const outputPath = `${parsedArgs.input}.${parsedArgs.algorithm}`; + + await pipeline(Readable.from(hashedValue), createWriteStream(outputPath)); + } + + console.log(`${parsedArgs.algorithm}: ${hashedValue}`); +} \ No newline at end of file diff --git a/src/commands/hashCompare.js b/src/commands/hashCompare.js index e69de29..13186ec 100644 --- a/src/commands/hashCompare.js +++ b/src/commands/hashCompare.js @@ -0,0 +1,32 @@ +import { createHash } from 'node:crypto'; +import { pipeline } from 'node:stream/promises'; +import { readFile } from 'node:fs/promises'; +import { createReadStream } from 'node:fs'; +import { argParser } from '../utils/argParser.js'; + +const SUPPORTED_ALGORYTHMS = ['sha256', 'md5', 'sha512']; + +export const hashCompareHandler = async (args) => { + const parsedArgs = argParser(args, { + input: { type: 'path', required: true }, + hash: { type: 'path', required: true }, + algorithm: { type: 'string', default: 'sha256' }, + }); + + if (!SUPPORTED_ALGORYTHMS.includes(parsedArgs.algorithm)) { + throw new Error('Algorithm is not supported'); + } + + const hash = createHash(parsedArgs.algorithm); + const readableStream = createReadStream(parsedArgs.input); + + await pipeline(readableStream, hash); + + const hashedValue = hash.digest('hex'); + + const savedHash = await readFile(parsedArgs.hash, { encoding: 'utf-8' }); + + const isValid = hashedValue === savedHash.trim().toLowerCase(); + + console.log(isValid ? 'OK' : ' MISMATCH'); +} \ No newline at end of file diff --git a/src/navigation.js b/src/navigation.js index bbac816..4bc0c5b 100644 --- a/src/navigation.js +++ b/src/navigation.js @@ -6,8 +6,8 @@ import { pathResolver } from './utils/pathResolver.js'; export const upHandler = () => { if (getCwd() !== initialCwd) { - const newDirectory = pathResolver(`${getCwd}/..`); - chDir(newDirectory); + const newDirectory = pathResolver(`${getCwd()}/..`); + chCwd(newDirectory); } } @@ -19,7 +19,7 @@ export const cdHandler = async (args) => { const newDirPath = pathResolver(pathToDirectory); const stats = await fs.stat(newDirPath); - + if (stats.isDirectory()) { chCwd(newDirPath) } else { diff --git a/src/utils/argParser.js b/src/utils/argParser.js index 8051e95..4d4f250 100644 --- a/src/utils/argParser.js +++ b/src/utils/argParser.js @@ -1,32 +1,47 @@ +import { parseArgs } from 'node:util'; +import { pathResolver } from './pathResolver.js'; import { InvalidInputError } from './errors.js'; -export const argParser = (args, argName, required = false) => { - const argIndex = args.findIndex(arg => arg.startsWith(`--${argName}`)); - - if (argIndex === -1) { - if (required) { - throw new InvalidInputError('Argument is required'); - } else { - return; +const formatArgValue = (type, value) => { + switch (type) { + case 'path': { + return pathResolver(value); } + default: + return value; } +} - const arg = args[argIndex]; - let argValue; +export const argParser = (args, options) => { + const argOptions = Object.keys(options) + .reduce((acc, key) => { + return { + ...acc, + [key]: { + type: options[key].type === 'boolean' ? 'boolean' : 'string', + default: options[key].default + } + } + }, {}); - if (arg.includes('=')) { - argValue = arg.split('=')[1]; - } else { - const nextArgvValue = args[argIndex + 1]; + let parsedArgs; - if (nextArgvValue && !nextArgvValue.startsWith('--')) { - argValue = nextArgvValue; - } + try { + parsedArgs = parseArgs({ args, options: argOptions }).values; + } catch (err) { + throw new InvalidInputError(err); } - if (!argValue && required) { - throw new InvalidInputError('Argument is required'); + const parsedArgsKeys = Object.keys(parsedArgs); + + if (parsedArgsKeys.some((key) => (options[key].required && parsedArgs[key] === undefined))) { + throw new InvalidInputError('Invalid input'); } - return argValue; -} \ No newline at end of file + return parsedArgsKeys.reduce((acc, key) => { + return { + ...acc, + [key]: formatArgValue(options[key].type, parsedArgs[key]) + } + }, {}) +} diff --git a/src/utils/fileConversionHandler.js b/src/utils/fileConversionHandler.js index 5320df4..10f2e81 100644 --- a/src/utils/fileConversionHandler.js +++ b/src/utils/fileConversionHandler.js @@ -2,24 +2,21 @@ import { pipeline } from 'node:stream/promises'; import { extname } from 'node:path'; import { createReadStream, createWriteStream } from 'node:fs'; import { argParser } from '../utils/argParser.js'; -import { pathResolver } from '../utils/pathResolver.js'; import { InvalidInputError } from '../utils/errors.js'; export const fileConfersionHandler = async (args, fileExtention, transformStream) => { - const inputArg = argParser(args, 'input', true); - const outputArg = argParser(args, 'output', true); - - const inputPath = pathResolver(inputArg); - const outputPath = pathResolver(outputArg); - - const inputFileExt = extname(inputPath).toLowerCase(); + const parsedArgs = argParser(args, { + input: { type: 'path', required: true }, + output: { type: 'path', required: true }, + }); + const inputFileExt = extname(parsedArgs.input).toLowerCase(); if (inputFileExt !== fileExtention) { throw new InvalidInputError('Invalid file extention'); } - const inputStream = createReadStream(inputPath, { encoding: 'utf-8' }); - const outputStream = createWriteStream(outputPath, { encoding: 'utf-8' }); + const inputStream = createReadStream(parsedArgs.input, { encoding: 'utf-8' }); + const outputStream = createWriteStream(parsedArgs.output, { encoding: 'utf-8' }); await pipeline(inputStream, transformStream, outputStream); }; \ No newline at end of file From 5374b649502e4d39b5938d0303d4191c8b0593f0 Mon Sep 17 00:00:00 2001 From: BorysovskaOA Date: Sat, 14 Mar 2026 12:43:50 +0200 Subject: [PATCH 09/14] Move constants to separate file, formatting --- src/commands.js | 1 + src/commands/count.js | 3 +-- src/commands/csvToJson.js | 3 ++- src/commands/hash.js | 2 +- src/commands/hashCompare.js | 2 +- src/commands/jsonToCsv.js | 1 + src/constants.js | 14 ++++++++++++++ src/cwdState.js | 1 + src/main.js | 1 + src/repl.js | 14 +++++--------- src/utils/argParser.js | 1 + src/utils/errors.js | 3 ++- src/utils/fileConversionHandler.js | 1 + src/utils/pathResolver.js | 1 + 14 files changed, 33 insertions(+), 15 deletions(-) create mode 100644 src/constants.js diff --git a/src/commands.js b/src/commands.js index 8ea52c2..35b0b1c 100644 --- a/src/commands.js +++ b/src/commands.js @@ -5,6 +5,7 @@ import { csvToJsonHandler } from './commands/csvToJson.js'; import { jsonToCsvHandler } from './commands/jsonToCsv.js'; import { upHandler, cdHandler, lsHandler } from './navigation.js'; + export const COMMAND_HANDLERS_MAP = { up: upHandler, cd: cdHandler, diff --git a/src/commands/count.js b/src/commands/count.js index ceab0c2..b4d4d49 100644 --- a/src/commands/count.js +++ b/src/commands/count.js @@ -3,9 +3,8 @@ import { extname } from 'node:path'; import { createReadStream } from 'node:fs'; import { argParser } from '../utils/argParser.js'; import { InvalidInputError } from '../utils/errors.js'; +import { LINE_SEPARATOR, WORD_SEPARATOR } from '../constants.js'; -const LINE_SEPARATOR = /\r?\n/; -const WORD_SEPARATOR = /\s+/; const getWordsFromString = (str) => str.split(WORD_SEPARATOR).filter(w => w.length > 0); const getSum = (arr) => arr.reduce((acc, wc) => acc + wc, 0); diff --git a/src/commands/csvToJson.js b/src/commands/csvToJson.js index af3c1bb..e706ace 100644 --- a/src/commands/csvToJson.js +++ b/src/commands/csvToJson.js @@ -1,9 +1,10 @@ import { Transform } from 'node:stream'; import { fileConfersionHandler } from '../utils/fileConversionHandler.js'; +import { LINE_SEPARATOR } from '../constants.js'; + const EXTRA_HEADER_NAME = 'Extra'; const INDENT = 2; -const LINE_SEPARATOR = /\r?\n/; const getCsvToJsonTransformSteam = () => { let transformBuffer = ''; diff --git a/src/commands/hash.js b/src/commands/hash.js index 7406009..eec3b5d 100644 --- a/src/commands/hash.js +++ b/src/commands/hash.js @@ -3,8 +3,8 @@ import { pipeline } from 'node:stream/promises'; import { Readable } from 'node:stream'; import { createReadStream, createWriteStream } from 'node:fs'; import { argParser } from '../utils/argParser.js'; +import { SUPPORTED_ALGORYTHMS } from '../constants.js'; -const SUPPORTED_ALGORYTHMS = ['sha256', 'md5', 'sha512']; export const hashHandler = async (args) => { const parsedArgs = argParser(args, { diff --git a/src/commands/hashCompare.js b/src/commands/hashCompare.js index 13186ec..6d341c6 100644 --- a/src/commands/hashCompare.js +++ b/src/commands/hashCompare.js @@ -3,8 +3,8 @@ import { pipeline } from 'node:stream/promises'; import { readFile } from 'node:fs/promises'; import { createReadStream } from 'node:fs'; import { argParser } from '../utils/argParser.js'; +import { SUPPORTED_ALGORYTHMS } from '../constants.js'; -const SUPPORTED_ALGORYTHMS = ['sha256', 'md5', 'sha512']; export const hashCompareHandler = async (args) => { const parsedArgs = argParser(args, { diff --git a/src/commands/jsonToCsv.js b/src/commands/jsonToCsv.js index 34ea0ee..24ff7a5 100644 --- a/src/commands/jsonToCsv.js +++ b/src/commands/jsonToCsv.js @@ -1,6 +1,7 @@ import { Transform } from 'node:stream'; import { fileConfersionHandler } from '../utils/fileConversionHandler.js'; + const getJsonToCsvTransformSteam = () => { let jsonStringBuffer = ''; diff --git a/src/constants.js b/src/constants.js new file mode 100644 index 0000000..78f3d41 --- /dev/null +++ b/src/constants.js @@ -0,0 +1,14 @@ + +export const LINE_SEPARATOR = /\r?\n/; +export const WORD_SEPARATOR = /\s+/; + +export const SUPPORTED_ALGORYTHMS = ['sha256', 'md5', 'sha512']; + +export const INVALID_INPUT_ERROR_CODE = 'INVALID_INPUT'; + +export const ANSI_COLORS = { + red: '\x1b[31m', + green: '\x1b[32m', +}; + +export const ANSI_COLOR_RESET = '\x1b[0m'; diff --git a/src/cwdState.js b/src/cwdState.js index e118cd3..d45ab00 100644 --- a/src/cwdState.js +++ b/src/cwdState.js @@ -1,6 +1,7 @@ import os from 'node:os'; import { InvalidInputError } from './utils/errors.js'; + export const initialCwd = os.homedir(); let cwd = initialCwd; diff --git a/src/main.js b/src/main.js index acff6a3..6490aaf 100644 --- a/src/main.js +++ b/src/main.js @@ -1,5 +1,6 @@ import { initRepl } from './repl.js'; + const init = () => { initRepl(); } diff --git a/src/repl.js b/src/repl.js index 57492f6..fe6822f 100644 --- a/src/repl.js +++ b/src/repl.js @@ -5,6 +5,8 @@ import process from 'node:process'; import { getCwd } from './cwdState.js'; import { InvalidInputError, INVALID_INPUT_ERROR_CODE } from './utils/errors.js'; import { COMMAND_HANDLERS_MAP } from './commands.js'; +import { ANSI_COLORS, ANSI_COLOR_RESET } from './constants.js'; + const WELCOME_TEXT = 'Welcome to Data Processing CLI!'; const EXIT_TEXT = 'Thank you for using Data Processing CLI!'; @@ -12,19 +14,13 @@ const INVALID_COMMAND_TEXT = 'Invalid input'; const OPERATION_FAILED_TEXT = 'Operation failed'; const CURRENT_DIRECTORY_PREFIX = 'You are currently in'; -const ANSI_COLORS = { - red: '\x1b[31m', - green: '\x1b[32m', -}; -const ANSI_COLOR_RESET = '\x1b[0m'; - const onSuccess = () => { console.log(`${ANSI_COLORS.green}${CURRENT_DIRECTORY_PREFIX} ${getCwd()}${ANSI_COLOR_RESET}`); } const onError = (err) => { // For testing only, TODO: delete before review next line - console.log(err); + console.log(err); if (err.code === INVALID_INPUT_ERROR_CODE) { console.log(`${ANSI_COLORS.red}${INVALID_COMMAND_TEXT}${ANSI_COLOR_RESET}`); } else { @@ -32,7 +28,7 @@ const onError = (err) => { } } -const handleCommand = async(command, commandArgs) => { +const handleCommand = async (command, commandArgs) => { if (command in COMMAND_HANDLERS_MAP) { try { await COMMAND_HANDLERS_MAP[command](commandArgs); @@ -48,7 +44,7 @@ const handleCommand = async(command, commandArgs) => { export const initRepl = () => { const rl = readline.createInterface({ - input: process.stdin, + input: process.stdin, output: process.stdout, prompt: '> ' }); diff --git a/src/utils/argParser.js b/src/utils/argParser.js index 4d4f250..8b3adfa 100644 --- a/src/utils/argParser.js +++ b/src/utils/argParser.js @@ -2,6 +2,7 @@ import { parseArgs } from 'node:util'; import { pathResolver } from './pathResolver.js'; import { InvalidInputError } from './errors.js'; + const formatArgValue = (type, value) => { switch (type) { case 'path': { diff --git a/src/utils/errors.js b/src/utils/errors.js index 21e3055..cebad98 100644 --- a/src/utils/errors.js +++ b/src/utils/errors.js @@ -1,4 +1,5 @@ -export const INVALID_INPUT_ERROR_CODE = 'INVALID_INPUT'; +import { INVALID_INPUT_ERROR_CODE } from '../constants.js'; + export class InvalidInputError extends Error { constructor(message) { diff --git a/src/utils/fileConversionHandler.js b/src/utils/fileConversionHandler.js index 10f2e81..e763731 100644 --- a/src/utils/fileConversionHandler.js +++ b/src/utils/fileConversionHandler.js @@ -4,6 +4,7 @@ import { createReadStream, createWriteStream } from 'node:fs'; import { argParser } from '../utils/argParser.js'; import { InvalidInputError } from '../utils/errors.js'; + export const fileConfersionHandler = async (args, fileExtention, transformStream) => { const parsedArgs = argParser(args, { input: { type: 'path', required: true }, diff --git a/src/utils/pathResolver.js b/src/utils/pathResolver.js index 2b2cb9c..17437ba 100644 --- a/src/utils/pathResolver.js +++ b/src/utils/pathResolver.js @@ -1,6 +1,7 @@ import path from 'node:path'; import { getCwd } from '../cwdState.js'; + export const pathResolver = (pathArg) => { return path.resolve(getCwd(), pathArg); } \ No newline at end of file From e70bfdaf227f8954b3c4dd9f24525157161c51fe Mon Sep 17 00:00:00 2001 From: BorysovskaOA Date: Sun, 15 Mar 2026 15:19:03 +0200 Subject: [PATCH 10/14] Added log-stats command --- src/commands.js | 8 +++- src/commands/decrypt.js | 3 ++ src/commands/encrypt.js | 3 ++ src/commands/logStats.js | 74 ++++++++++++++++++++++++++++++ src/repl.js | 4 +- src/utils/mergeLogStats.js | 30 ++++++++++++ src/workers/logWorker.js | 94 ++++++++++++++++++++++++++++++++++++++ 7 files changed, 213 insertions(+), 3 deletions(-) create mode 100644 src/utils/mergeLogStats.js diff --git a/src/commands.js b/src/commands.js index 35b0b1c..6d8b0a9 100644 --- a/src/commands.js +++ b/src/commands.js @@ -4,6 +4,9 @@ import { hashCompareHandler } from './commands/hashCompare.js'; import { csvToJsonHandler } from './commands/csvToJson.js'; import { jsonToCsvHandler } from './commands/jsonToCsv.js'; import { upHandler, cdHandler, lsHandler } from './navigation.js'; +import { logStatsHandler } from './commands/logStats.js'; +import { encryptHandler } from './commands/encrypt.js'; +import { decryptHandler } from './commands/decrypt.js'; export const COMMAND_HANDLERS_MAP = { @@ -14,5 +17,8 @@ export const COMMAND_HANDLERS_MAP = { 'json-to-csv': jsonToCsvHandler, count: countHandler, hash: hashHandler, - 'hash-compare': hashCompareHandler + 'hash-compare': hashCompareHandler, + 'log-stats': logStatsHandler, + encrypt: encryptHandler, + decrypt: decryptHandler, } diff --git a/src/commands/decrypt.js b/src/commands/decrypt.js index e69de29..e5cbb82 100644 --- a/src/commands/decrypt.js +++ b/src/commands/decrypt.js @@ -0,0 +1,3 @@ +export const decryptHandler = async () => { + +} \ No newline at end of file diff --git a/src/commands/encrypt.js b/src/commands/encrypt.js index e69de29..0084404 100644 --- a/src/commands/encrypt.js +++ b/src/commands/encrypt.js @@ -0,0 +1,3 @@ +export const encryptHandler = async () => { + +} \ No newline at end of file diff --git a/src/commands/logStats.js b/src/commands/logStats.js index e69de29..dc84f0c 100644 --- a/src/commands/logStats.js +++ b/src/commands/logStats.js @@ -0,0 +1,74 @@ +import { stat } from 'node:fs/promises'; +import { extname } from 'node:path'; +import { availableParallelism } from 'node:os'; +import { Worker } from 'node:worker_threads'; +import { createWriteStream } from 'node:fs'; +import { Readable } from 'node:stream'; +import { pipeline } from 'node:stream/promises'; +import { InvalidInputError } from '../utils/errors.js'; +import { argParser } from '../utils/argParser.js'; +import { INITIAL_LOG_STATS, mergeStats } from '../utils/mergeLogStats.js'; + + +const runWorker = (data) => { + const workerPath = new URL('../workers/logWorker.js', import.meta.url); + + return new Promise((resolve, reject) => { + const worker = new Worker(workerPath); + worker.on('message', (data) => { + resolve(data); + worker.terminate(); + }); + worker.on('error', (error) => { + reject(error); + worker.terminate(); + }); + worker.on('exit', (code) => { + if (code !== 0) reject(new Error(`Worker stopped with exit code ${code}`)); + }); + + worker.postMessage(data); + }); +} + +export const logStatsHandler = async (args) => { + const parsedArgs = argParser(args, { + input: { type: 'path', required: true }, + output: { type: 'path', required: true }, + }); + + const inputFileExt = extname(parsedArgs.input).toLowerCase(); + if (!['.txt', '.log'].includes(inputFileExt)) { + throw new InvalidInputError('Invalid file extention'); + } + + const maxWorkersAvailable = availableParallelism(); + const inputFileStats = await stat(parsedArgs.input); + const inputFileBitesLength = inputFileStats.size; + + const bytesPerWorker = Math.floor(inputFileBitesLength / maxWorkersAvailable); + const positionsToReadByWorker = Array.from({ length: maxWorkersAvailable }).map((_, i) => ({ + filePath: parsedArgs.input, + start: i * bytesPerWorker, + end: (i === (maxWorkersAvailable - 1)) ? inputFileBitesLength : i * bytesPerWorker + bytesPerWorker, + })); + + const workerChunkStats = await Promise.all(positionsToReadByWorker.map(runWorker)); + const mergedStats = workerChunkStats.reduce(mergeStats, INITIAL_LOG_STATS); + + // Need just 2 most popular + const topPaths = Object.entries(mergedStats.paths) + .sort(([, a], [, b]) => b - a) + .slice(0, 2) + .map(([path, count]) => ({ path, count })) + + const mergedFormattedStats = { + total: mergedStats.total, + levels: mergedStats.levels, + status: mergedStats.status, + topPaths, + avgResponseTimeMs: mergedStats.avgResponseTimeMs.toFixed(2), + } + + await pipeline(Readable.from(JSON.stringify(mergedFormattedStats, null, 2)), createWriteStream(parsedArgs.output, 'utf-8')); +} \ No newline at end of file diff --git a/src/repl.js b/src/repl.js index fe6822f..98de6ce 100644 --- a/src/repl.js +++ b/src/repl.js @@ -3,9 +3,9 @@ import readline from 'node:readline'; import process from 'node:process'; import { getCwd } from './cwdState.js'; -import { InvalidInputError, INVALID_INPUT_ERROR_CODE } from './utils/errors.js'; +import { InvalidInputError } from './utils/errors.js'; import { COMMAND_HANDLERS_MAP } from './commands.js'; -import { ANSI_COLORS, ANSI_COLOR_RESET } from './constants.js'; +import { ANSI_COLORS, ANSI_COLOR_RESET, INVALID_INPUT_ERROR_CODE } from './constants.js'; const WELCOME_TEXT = 'Welcome to Data Processing CLI!'; diff --git a/src/utils/mergeLogStats.js b/src/utils/mergeLogStats.js new file mode 100644 index 0000000..dbcf4e6 --- /dev/null +++ b/src/utils/mergeLogStats.js @@ -0,0 +1,30 @@ +export const INITIAL_LOG_STATS = { + total: 0, + levels: { INFO: 0, WARN: 0, ERROR: 0 }, + status: { '2xx': 0, '3xx': 0, '4xx': 0, '5xx': 0 }, + paths: {}, + avgResponseTimeMs: 0 +}; + +export const mergeStats = (stat1, stat2) => { + const combinesStats = structuredClone(INITIAL_LOG_STATS); + + combinesStats.total = stat1.total + stat2.total; + combinesStats.avgResponseTimeMs = (stat1.avgResponseTimeMs + stat2.avgResponseTimeMs) / 2 + + Object.keys(combinesStats.levels).forEach((l) => { + combinesStats.levels[l] = stat1.levels[l] + stat2.levels[l] + }) + + Object.keys(combinesStats.status).forEach((s) => { + combinesStats.status[s] = stat1.status[s] + stat2.status[s] + }); + + const pathsSet = new Set([...Object.keys(stat1.paths), ...Object.keys(stat2.paths)]); + + Array.from(pathsSet).forEach((p) => { + combinesStats.paths[p] = (stat1.paths[p] || 0) + (stat2.paths[p] || 0) + }); + + return combinesStats; +} diff --git a/src/workers/logWorker.js b/src/workers/logWorker.js index e69de29..ef0d906 100644 --- a/src/workers/logWorker.js +++ b/src/workers/logWorker.js @@ -0,0 +1,94 @@ +import { parentPort } from 'worker_threads'; +import { createReadStream } from 'node:fs'; +import { LINE_SEPARATOR } from '../constants.js'; +import { mergeStats, INITIAL_LOG_STATS } from '../utils/mergeLogStats.js'; + + +let readBufferLength = 0; +let isBufferRead = false; + +const getBuffersToProcess = (chunk, maxBufferLength) => { + const newReadBufferLength = readBufferLength + chunk.length; + + let chunkWithinRange; + let chunkToFinishLastLine; + + if (newReadBufferLength < maxBufferLength) { + chunkWithinRange = chunk; + readBufferLength = newReadBufferLength; + } else { + if (!isBufferRead) { + const requiredLenghtFromChunk = maxBufferLength - readBufferLength; + chunkWithinRange = chunk.subarray(0, requiredLenghtFromChunk); + readBufferLength += requiredLenghtFromChunk; + chunkToFinishLastLine = chunk.subarray(requiredLenghtFromChunk); + + isBufferRead = true; + } else { + chunkToFinishLastLine = chunk; + } + } + + return [chunkWithinRange, chunkToFinishLastLine]; +} + +const processLine = (stats, line) => { + const [, level, , statusCode, responseTime, , path] = line.split(' '); + + const lineStat = structuredClone(INITIAL_LOG_STATS); + lineStat.total++; + lineStat.levels[level]++; + lineStat.status[`${statusCode.toString().charAt(0)}xx`]++; + lineStat.avgResponseTimeMs = Number(responseTime); + lineStat.paths = { [path]: 1 } + + return mergeStats(stats, lineStat); +} + + +parentPort.on('message', async ({ filePath, start, end }) => { + let stats = INITIAL_LOG_STATS; + + const readableStream = createReadStream(filePath, { start }); + const maBufferLength = end - start; + let shouldExcluseFirstLine = start !== 0; + let buffer = ''; + + for await (const chunk of readableStream) { + const [chunkWithinRange, chunkToFinishLastLine] = getBuffersToProcess(chunk, maBufferLength); + + if (chunkWithinRange) { + buffer += chunkWithinRange.toString('utf-8'); + let lines = buffer.split(LINE_SEPARATOR); + buffer = lines.pop(); + + // We don't count first line if not first part worker + if (shouldExcluseFirstLine) { + lines.shift(); + shouldExcluseFirstLine = false + } + + lines.forEach(l => { + stats = processLine(stats, l); + }); + } + + if (chunkToFinishLastLine) { + const lastPieceBuffer = buffer + chunkToFinishLastLine.toString('utf-8'); + let lines = lastPieceBuffer.split(LINE_SEPARATOR); + buffer = lines.shift(); + + // If we no lines left - we din't have last new line in buffer + if (lines.length !== 0) { + break; + } + } + } + + // Processing of last line + if (buffer.length) { + stats = processLine(stats, buffer) + } + + parentPort.postMessage(stats); +}); From 94c8f4158e5992769ac47de3516bb0f2f7720f01 Mon Sep 17 00:00:00 2001 From: BorysovskaOA Date: Sun, 15 Mar 2026 16:22:09 +0200 Subject: [PATCH 11/14] Added encryptHandler --- src/commands/encrypt.js | 33 ++++++++++++++++++++++++++++++++- src/constants.js | 6 ++++++ src/utils/argParser.js | 2 +- 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/src/commands/encrypt.js b/src/commands/encrypt.js index 0084404..f7ba9a5 100644 --- a/src/commands/encrypt.js +++ b/src/commands/encrypt.js @@ -1,3 +1,34 @@ -export const encryptHandler = async () => { +import { createCipheriv, scrypt as scryptCb, randomBytes } from 'node:crypto'; +import { createReadStream, createWriteStream } from 'node:fs'; +import { pipeline, finished } from 'node:stream/promises'; +import { promisify } from 'node:util'; +import { ENCRYPTION_ALGORITHM, IV_SIZE, KEY_SIZE, SALT_SIZE } from '../constants.js'; +import { argParser } from '../utils/argParser.js' +const scrypt = promisify(scryptCb); + +export const encryptHandler = async (args) => { + const parsedArgs = argParser(args, { + input: { type: 'path', required: true }, + output: { type: 'path', required: true }, + password: { type: 'string', required: true }, + }); + + const salt = randomBytes(SALT_SIZE); + const iv = randomBytes(IV_SIZE); + const key = await scrypt(parsedArgs.password, salt, KEY_SIZE); + const cipher = createCipheriv(ENCRYPTION_ALGORITHM, key, iv); + + const output = createWriteStream(parsedArgs.output); + + output.write(salt); + output.write(iv); + + await pipeline(createReadStream(parsedArgs.input), cipher, output, { end: false }); + + const authTag = cipher.getAuthTag(); + output.write(authTag); + output.end(); + + await finished(output); } \ No newline at end of file diff --git a/src/constants.js b/src/constants.js index 78f3d41..8f0374a 100644 --- a/src/constants.js +++ b/src/constants.js @@ -12,3 +12,9 @@ export const ANSI_COLORS = { }; export const ANSI_COLOR_RESET = '\x1b[0m'; + +export const ENCRYPTION_ALGORITHM = 'AES-256-GCM'; +export const SALT_SIZE = 16; +export const IV_SIZE = 12; +export const AUTH_TAG_SIZE = 16; +export const KEY_SIZE = 32; \ No newline at end of file diff --git a/src/utils/argParser.js b/src/utils/argParser.js index 8b3adfa..53e10e9 100644 --- a/src/utils/argParser.js +++ b/src/utils/argParser.js @@ -35,7 +35,7 @@ export const argParser = (args, options) => { const parsedArgsKeys = Object.keys(parsedArgs); - if (parsedArgsKeys.some((key) => (options[key].required && parsedArgs[key] === undefined))) { + if (Object.keys(options).some((key) => (options[key].required && parsedArgs[key] === undefined))) { throw new InvalidInputError('Invalid input'); } From 0ffd719b88e1000eaa675e824b6ad9fb84ff2a1d Mon Sep 17 00:00:00 2001 From: BorysovskaOA Date: Sun, 15 Mar 2026 16:46:15 +0200 Subject: [PATCH 12/14] Added decrypt handle --- src/commands/decrypt.js | 55 +++++++++++++++++++++++++++++++++++++++-- src/commands/encrypt.js | 6 ++--- 2 files changed, 55 insertions(+), 6 deletions(-) diff --git a/src/commands/decrypt.js b/src/commands/decrypt.js index e5cbb82..d9c3a36 100644 --- a/src/commands/decrypt.js +++ b/src/commands/decrypt.js @@ -1,3 +1,54 @@ -export const decryptHandler = async () => { +import { createDecipheriv, scrypt } from 'node:crypto'; +import { createReadStream, createWriteStream } from 'node:fs'; +import { open, stat } from 'node:fs/promises'; +import { pipeline } from 'node:stream/promises'; +import { promisify } from 'node:util'; +import { SALT_SIZE, IV_SIZE, AUTH_TAG_SIZE, KEY_SIZE, ENCRYPTION_ALGORITHM } from '../constants.js'; +import { argParser } from '../utils/argParser.js'; -} \ No newline at end of file + +const getDecryptEntities = async (inputFilePath, size) => { + const handle = await open(inputFilePath, 'r'); + + const salt = Buffer.alloc(SALT_SIZE); + const iv = Buffer.alloc(IV_SIZE); + const authTag = Buffer.alloc(AUTH_TAG_SIZE); + + try { + + await handle.read(salt, 0, SALT_SIZE, 0); + await handle.read(iv, 0, IV_SIZE, SALT_SIZE); + await handle.read(authTag, 0, AUTH_TAG_SIZE, size - AUTH_TAG_SIZE); + } finally { + if (handle) await handle.close(); + } + + return { + salt, + iv, + authTag, + } +} + +export const decryptHandler = async (args) => { + const parsedArgs = argParser(args, { + input: { type: 'path', required: true }, + output: { type: 'path', required: true }, + password: { type: 'string', required: true }, + }); + + const { size } = await stat(parsedArgs.input); + const { salt, iv, authTag } = await getDecryptEntities(parsedArgs.input, size); + + const key = await promisify(scrypt)(parsedArgs.password, salt, KEY_SIZE); + const decipher = createDecipheriv(ENCRYPTION_ALGORITHM, key, iv); + decipher.setAuthTag(authTag); + + const input = createReadStream(parsedArgs.input, { + start: SALT_SIZE + IV_SIZE, + end: size - AUTH_TAG_SIZE - 1 + }); + const output = createWriteStream(parsedArgs.output); + + await pipeline(input, decipher, output); +}; \ No newline at end of file diff --git a/src/commands/encrypt.js b/src/commands/encrypt.js index f7ba9a5..0cd45fd 100644 --- a/src/commands/encrypt.js +++ b/src/commands/encrypt.js @@ -1,12 +1,10 @@ -import { createCipheriv, scrypt as scryptCb, randomBytes } from 'node:crypto'; +import { createCipheriv, scrypt, randomBytes } from 'node:crypto'; import { createReadStream, createWriteStream } from 'node:fs'; import { pipeline, finished } from 'node:stream/promises'; import { promisify } from 'node:util'; import { ENCRYPTION_ALGORITHM, IV_SIZE, KEY_SIZE, SALT_SIZE } from '../constants.js'; import { argParser } from '../utils/argParser.js' -const scrypt = promisify(scryptCb); - export const encryptHandler = async (args) => { const parsedArgs = argParser(args, { input: { type: 'path', required: true }, @@ -16,7 +14,7 @@ export const encryptHandler = async (args) => { const salt = randomBytes(SALT_SIZE); const iv = randomBytes(IV_SIZE); - const key = await scrypt(parsedArgs.password, salt, KEY_SIZE); + const key = await promisify(scrypt)(parsedArgs.password, salt, KEY_SIZE); const cipher = createCipheriv(ENCRYPTION_ALGORITHM, key, iv); const output = createWriteStream(parsedArgs.output); From d87f40bcbb7b4b57f8197d15179f0362e6fd849b Mon Sep 17 00:00:00 2001 From: BorysovskaOA Date: Sun, 15 Mar 2026 16:48:53 +0200 Subject: [PATCH 13/14] Removed testing error display --- src/repl.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/repl.js b/src/repl.js index 98de6ce..da0de2b 100644 --- a/src/repl.js +++ b/src/repl.js @@ -19,8 +19,6 @@ const onSuccess = () => { } const onError = (err) => { - // For testing only, TODO: delete before review next line - console.log(err); if (err.code === INVALID_INPUT_ERROR_CODE) { console.log(`${ANSI_COLORS.red}${INVALID_COMMAND_TEXT}${ANSI_COLOR_RESET}`); } else { From 4a1111bfa1da488e92cc3d0b748ffda49324a682 Mon Sep 17 00:00:00 2001 From: BorysovskaOA Date: Sun, 15 Mar 2026 18:13:38 +0200 Subject: [PATCH 14/14] Errors correction after checks --- src/commands/count.js | 8 ++------ src/commands/csvToJson.js | 5 +++-- src/commands/decrypt.js | 1 - src/commands/hashCompare.js | 3 +++ src/commands/jsonToCsv.js | 2 +- src/commands/logStats.js | 8 ++------ src/constants.js | 1 + src/repl.js | 6 +++--- src/utils/fileConversionHandler.js | 11 ++++------- src/utils/validateFileExtention.js | 11 +++++++++++ src/workers/logWorker.js | 4 ++-- 11 files changed, 32 insertions(+), 28 deletions(-) create mode 100644 src/utils/validateFileExtention.js diff --git a/src/commands/count.js b/src/commands/count.js index b4d4d49..ab1757b 100644 --- a/src/commands/count.js +++ b/src/commands/count.js @@ -1,9 +1,8 @@ -import { extname } from 'node:path'; import { createReadStream } from 'node:fs'; import { argParser } from '../utils/argParser.js'; -import { InvalidInputError } from '../utils/errors.js'; import { LINE_SEPARATOR, WORD_SEPARATOR } from '../constants.js'; +import { validateFileExtention } from '../utils/validateFileExtention.js'; const getWordsFromString = (str) => str.split(WORD_SEPARATOR).filter(w => w.length > 0); @@ -14,10 +13,7 @@ export const countHandler = async (args) => { input: { type: 'path', required: true }, }); - const inputFileExt = extname(parsedArgs.input).toLowerCase(); - if (inputFileExt !== '.txt') { - throw new InvalidInputError('Invalid file extention'); - } + validateFileExtention(parsedArgs.input, '.txt'); const readableStream = createReadStream(parsedArgs.input, { encoding: 'utf-8' }); diff --git a/src/commands/csvToJson.js b/src/commands/csvToJson.js index e706ace..4e541d1 100644 --- a/src/commands/csvToJson.js +++ b/src/commands/csvToJson.js @@ -62,7 +62,8 @@ const getCsvToJsonTransformSteam = () => { }, flush(callback) { if (!transformBuffer.trim()) { - return '\n]'; + this.push('\n]'); + return callback(); } const parsedLineData = transformBuffer.split(','); @@ -76,5 +77,5 @@ const getCsvToJsonTransformSteam = () => { export const csvToJsonHandler = async (args) => { const transformStream = getCsvToJsonTransformSteam(); - return fileConfersionHandler(args, '.csv', transformStream); + return fileConfersionHandler(args, '.csv', '.json', transformStream); } diff --git a/src/commands/decrypt.js b/src/commands/decrypt.js index d9c3a36..de02b7b 100644 --- a/src/commands/decrypt.js +++ b/src/commands/decrypt.js @@ -15,7 +15,6 @@ const getDecryptEntities = async (inputFilePath, size) => { const authTag = Buffer.alloc(AUTH_TAG_SIZE); try { - await handle.read(salt, 0, SALT_SIZE, 0); await handle.read(iv, 0, IV_SIZE, SALT_SIZE); await handle.read(authTag, 0, AUTH_TAG_SIZE, size - AUTH_TAG_SIZE); diff --git a/src/commands/hashCompare.js b/src/commands/hashCompare.js index 6d341c6..6b050f2 100644 --- a/src/commands/hashCompare.js +++ b/src/commands/hashCompare.js @@ -4,6 +4,7 @@ import { readFile } from 'node:fs/promises'; import { createReadStream } from 'node:fs'; import { argParser } from '../utils/argParser.js'; import { SUPPORTED_ALGORYTHMS } from '../constants.js'; +import { validateFileExtention } from '../utils/validateFileExtention.js'; export const hashCompareHandler = async (args) => { @@ -17,6 +18,8 @@ export const hashCompareHandler = async (args) => { throw new Error('Algorithm is not supported'); } + validateFileExtention(parsedArgs.hash, `.${parsedArgs.algorithm.toLowerCase()}`); + const hash = createHash(parsedArgs.algorithm); const readableStream = createReadStream(parsedArgs.input); diff --git a/src/commands/jsonToCsv.js b/src/commands/jsonToCsv.js index 24ff7a5..a15b970 100644 --- a/src/commands/jsonToCsv.js +++ b/src/commands/jsonToCsv.js @@ -35,5 +35,5 @@ const getJsonToCsvTransformSteam = () => { export const jsonToCsvHandler = async (args) => { const transformStream = getJsonToCsvTransformSteam(); - return fileConfersionHandler(args, '.json', transformStream); + return fileConfersionHandler(args, '.json', '.csv', transformStream); } diff --git a/src/commands/logStats.js b/src/commands/logStats.js index dc84f0c..9135847 100644 --- a/src/commands/logStats.js +++ b/src/commands/logStats.js @@ -1,13 +1,12 @@ import { stat } from 'node:fs/promises'; -import { extname } from 'node:path'; import { availableParallelism } from 'node:os'; import { Worker } from 'node:worker_threads'; import { createWriteStream } from 'node:fs'; import { Readable } from 'node:stream'; import { pipeline } from 'node:stream/promises'; -import { InvalidInputError } from '../utils/errors.js'; import { argParser } from '../utils/argParser.js'; import { INITIAL_LOG_STATS, mergeStats } from '../utils/mergeLogStats.js'; +import { validateFileExtention } from '../utils/validateFileExtention.js'; const runWorker = (data) => { @@ -37,10 +36,7 @@ export const logStatsHandler = async (args) => { output: { type: 'path', required: true }, }); - const inputFileExt = extname(parsedArgs.input).toLowerCase(); - if (!['.txt', '.log'].includes(inputFileExt)) { - throw new InvalidInputError('Invalid file extention'); - } + validateFileExtention(parsedArgs.input, ['.txt', '.log']); const maxWorkersAvailable = availableParallelism(); const inputFileStats = await stat(parsedArgs.input); diff --git a/src/constants.js b/src/constants.js index 8f0374a..800cdea 100644 --- a/src/constants.js +++ b/src/constants.js @@ -1,4 +1,5 @@ +export const SPACE_SEPARATOR = /\s+/; export const LINE_SEPARATOR = /\r?\n/; export const WORD_SEPARATOR = /\s+/; diff --git a/src/repl.js b/src/repl.js index da0de2b..0472fc8 100644 --- a/src/repl.js +++ b/src/repl.js @@ -5,7 +5,7 @@ import process from 'node:process'; import { getCwd } from './cwdState.js'; import { InvalidInputError } from './utils/errors.js'; import { COMMAND_HANDLERS_MAP } from './commands.js'; -import { ANSI_COLORS, ANSI_COLOR_RESET, INVALID_INPUT_ERROR_CODE } from './constants.js'; +import { ANSI_COLORS, ANSI_COLOR_RESET, INVALID_INPUT_ERROR_CODE, SPACE_SEPARATOR } from './constants.js'; const WELCOME_TEXT = 'Welcome to Data Processing CLI!'; @@ -49,9 +49,9 @@ export const initRepl = () => { rl.on('line', async (line) => { const lineTrimmed = line.trim(); - const [command, ...commandArgs] = lineTrimmed.split(' '); + const [command, ...commandArgs] = lineTrimmed.split(SPACE_SEPARATOR); - if (command === '.exit' && commandArgs.length === 0) { + if (command === '.exit') { rl.close() } else { await handleCommand(command, commandArgs); diff --git a/src/utils/fileConversionHandler.js b/src/utils/fileConversionHandler.js index e763731..43a5ace 100644 --- a/src/utils/fileConversionHandler.js +++ b/src/utils/fileConversionHandler.js @@ -1,20 +1,17 @@ import { pipeline } from 'node:stream/promises'; -import { extname } from 'node:path'; import { createReadStream, createWriteStream } from 'node:fs'; import { argParser } from '../utils/argParser.js'; -import { InvalidInputError } from '../utils/errors.js'; +import { validateFileExtention } from './validateFileExtention.js'; -export const fileConfersionHandler = async (args, fileExtention, transformStream) => { +export const fileConfersionHandler = async (args, inputExt, outputExt, transformStream) => { const parsedArgs = argParser(args, { input: { type: 'path', required: true }, output: { type: 'path', required: true }, }); - const inputFileExt = extname(parsedArgs.input).toLowerCase(); - if (inputFileExt !== fileExtention) { - throw new InvalidInputError('Invalid file extention'); - } + validateFileExtention(parsedArgs.input, inputExt); + validateFileExtention(parsedArgs.output, outputExt); const inputStream = createReadStream(parsedArgs.input, { encoding: 'utf-8' }); const outputStream = createWriteStream(parsedArgs.output, { encoding: 'utf-8' }); diff --git a/src/utils/validateFileExtention.js b/src/utils/validateFileExtention.js new file mode 100644 index 0000000..fa49396 --- /dev/null +++ b/src/utils/validateFileExtention.js @@ -0,0 +1,11 @@ +import { extname } from 'node:path'; +import { InvalidInputError } from './errors.js'; + + +export const validateFileExtention = (fileName, ext) => { + const allowedExt = Array.isArray(ext) ? ext.map((e) => e.toLowerCase()) : [ext.toLowerCase()]; + const fileExt = extname(fileName).toLowerCase(); + if (!allowedExt.includes(fileExt)) { + throw new InvalidInputError('Invalid file extention'); + } +} diff --git a/src/workers/logWorker.js b/src/workers/logWorker.js index ef0d906..7d691ad 100644 --- a/src/workers/logWorker.js +++ b/src/workers/logWorker.js @@ -1,6 +1,6 @@ import { parentPort } from 'worker_threads'; import { createReadStream } from 'node:fs'; -import { LINE_SEPARATOR } from '../constants.js'; +import { LINE_SEPARATOR, SPACE_SEPARATOR } from '../constants.js'; import { mergeStats, INITIAL_LOG_STATS } from '../utils/mergeLogStats.js'; @@ -33,7 +33,7 @@ const getBuffersToProcess = (chunk, maxBufferLength) => { } const processLine = (stats, line) => { - const [, level, , statusCode, responseTime, , path] = line.split(' '); + const [, level, , statusCode, responseTime, , path] = line.split(SPACE_SEPARATOR); const lineStat = structuredClone(INITIAL_LOG_STATS); lineStat.total++;