From 92b8c506868ec3bc8990d770c757d3751627afef Mon Sep 17 00:00:00 2001 From: Nikita Date: Mon, 29 Jan 2024 17:48:16 +0300 Subject: [PATCH 01/24] init: repository, package.json, reading args --- .gitignore | 1 + .idea/.gitignore | 5 ----- .idea/file-manager.iml | 12 ------------ .idea/jsLibraryMappings.xml | 6 ------ .idea/modules.xml | 8 -------- .idea/vcs.xml | 6 ------ 6 files changed, 1 insertion(+), 37 deletions(-) delete mode 100644 .idea/.gitignore delete mode 100644 .idea/file-manager.iml delete mode 100644 .idea/jsLibraryMappings.xml delete mode 100644 .idea/modules.xml delete mode 100644 .idea/vcs.xml diff --git a/.gitignore b/.gitignore index 3c3629e..eb79dd5 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ node_modules +.idea diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index b58b603..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Editor-based HTTP Client requests -/httpRequests/ diff --git a/.idea/file-manager.iml b/.idea/file-manager.iml deleted file mode 100644 index 24643cc..0000000 --- a/.idea/file-manager.iml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/jsLibraryMappings.xml b/.idea/jsLibraryMappings.xml deleted file mode 100644 index d23208f..0000000 --- a/.idea/jsLibraryMappings.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 1a85d97..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 35eb1dd..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file From f3458df66a890a7b8edc42d4f5f9fa251bb58978 Mon Sep 17 00:00:00 2001 From: Nikita Date: Mon, 29 Jan 2024 18:10:37 +0300 Subject: [PATCH 02/24] feat: initialize class structure --- fileManager.js | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++ index.js | 14 +++++++++--- 2 files changed, 73 insertions(+), 3 deletions(-) create mode 100644 fileManager.js diff --git a/fileManager.js b/fileManager.js new file mode 100644 index 0000000..b7a802d --- /dev/null +++ b/fileManager.js @@ -0,0 +1,62 @@ +import os from 'os'; +import { createInterface } from 'readline/promises'; + +class FileManager { + #currentDirectory; + + constructor() { + this.#currentDirectory = os.homedir(); + } + + async run() { + const read = createInterface({ + input: process.stdin, + output: process.stdout + }); + + + while (true) { + const input = await read.question(''); + const splittedInput = input.split(' '); + const command = splittedInput.at(0); + + switch (command) { + case 'up': { + break; + } + case 'cd': { + break; + } + case 'ls': { + break; + } + case 'cat': { + break; + } + case 'rn': { + break; + } + case 'cp': { + break; + } + case 'mv': { + break; + } + case 'rm': { + break; + } + case 'os': { + break; + } + case 'compress': { + break; + } + case 'decompress': { + break; + } + } + } + } +} + +export default new FileManager(); diff --git a/index.js b/index.js index 8a5f682..ee670f8 100644 --- a/index.js +++ b/index.js @@ -1,9 +1,17 @@ +import FileManager from './fileManager.js'; + const main = async () => { - const username = process.argv[3]; + // TODO + const username = process.argv[2]; + console.log(`Welcome to the File Manager, ${username}!`); -}; + process.addListener('exit', () => { + console.log(`Thank you for using File Manager, ${username}, goodbye!`); + }); + await FileManager.run(); +}; -main(); +await main(); From 483378aae60c74d8fe1fc0ae811fe5d5b7693a8e Mon Sep 17 00:00:00 2001 From: Nikita Date: Mon, 29 Jan 2024 18:38:42 +0300 Subject: [PATCH 03/24] feat: implement some nwd methods --- fileManager.js | 70 ++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 68 insertions(+), 2 deletions(-) diff --git a/fileManager.js b/fileManager.js index b7a802d..889ddd9 100644 --- a/fileManager.js +++ b/fileManager.js @@ -1,15 +1,19 @@ import os from 'os'; -import { createInterface } from 'readline/promises'; +import readline from 'readline'; +import path from 'path'; +import fs from 'fs'; + class FileManager { #currentDirectory; constructor() { this.#currentDirectory = os.homedir(); + console.info(`Initial directory: ${this.#currentDirectory}`); } async run() { - const read = createInterface({ + const read = readline.promises.createInterface({ input: process.stdin, output: process.stdout }); @@ -22,12 +26,16 @@ class FileManager { switch (command) { case 'up': { + this.#up(); break; } case 'cd': { + const enteredPath = splittedInput.at(1); + await this.#cd(enteredPath); break; } case 'ls': { + await this.#ls(); break; } case 'cat': { @@ -54,9 +62,67 @@ class FileManager { case 'decompress': { break; } + case '.exit': { + read.close(); + return; + } } } } + + #up() { + this.#currentDirectory = path.resolve(this.#currentDirectory, '../'); + } + + async #cd(enteredPath) { + let absolutePath; + + if (path.isAbsolute(enteredPath)) { + absolutePath = enteredPath; + } else { + absolutePath = path.resolve(this.#currentDirectory, enteredPath); + } + + const doesPathExist = FileManager.#doesPathExist(absolutePath); + const isFolder = FileManager.#isDirectory(absolutePath); + + if (!isFolder) { + console.error('Can\'t move into non-directory'); + return; + } + + if (doesPathExist) { + this.#currentDirectory = absolutePath; + } else { + console.error('Path does not exist') + } + } + + async #ls() { + const list = new Promise(resolve => { + fs.readdir(this.#currentDirectory, resolve); + }); + + const directories = await Promise.all(list.map(entity => { + return new Promise(resolve => { + FileManager.#isDirectory(entity).then(resolve); + }); + })); + } + + static async #isDirectory(absolutePath) { + const stats = await new Promise(resolve => { + fs.lstat(absolutePath, (error, stats) => resolve(error)) + }); + + return stats.isDirectory(); + } + + static async #doesPathExist(absolutePath) { + return await new Promise(resolve => { + fs.exists(absolutePath, resolve); + }); + } } export default new FileManager(); From 1214344a158bbb60dde7dd196716773aa3321976 Mon Sep 17 00:00:00 2001 From: Nikita Date: Mon, 29 Jan 2024 17:47:26 +0300 Subject: [PATCH 04/24] init: repository, package.json, reading args --- .idea/.gitignore | 5 +++++ .idea/file-manager.iml | 12 ++++++++++++ .idea/jsLibraryMappings.xml | 6 ++++++ .idea/modules.xml | 8 ++++++++ .idea/vcs.xml | 6 ++++++ fileManager.js | 15 ++++++++------- 6 files changed, 45 insertions(+), 7 deletions(-) create mode 100644 .idea/.gitignore create mode 100644 .idea/file-manager.iml create mode 100644 .idea/jsLibraryMappings.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..b58b603 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,5 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/.idea/file-manager.iml b/.idea/file-manager.iml new file mode 100644 index 0000000..24643cc --- /dev/null +++ b/.idea/file-manager.iml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/jsLibraryMappings.xml b/.idea/jsLibraryMappings.xml new file mode 100644 index 0000000..d23208f --- /dev/null +++ b/.idea/jsLibraryMappings.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..1a85d97 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/fileManager.js b/fileManager.js index 889ddd9..d323628 100644 --- a/fileManager.js +++ b/fileManager.js @@ -9,7 +9,7 @@ class FileManager { constructor() { this.#currentDirectory = os.homedir(); - console.info(`Initial directory: ${this.#currentDirectory}`); + console.info(`Current directory: ${this.#currentDirectory}`); } async run() { @@ -20,6 +20,7 @@ class FileManager { while (true) { + console.info(`Current directory: ${this.#currentDirectory}`); const input = await read.question(''); const splittedInput = input.split(' '); const command = splittedInput.at(0); @@ -83,8 +84,8 @@ class FileManager { absolutePath = path.resolve(this.#currentDirectory, enteredPath); } - const doesPathExist = FileManager.#doesPathExist(absolutePath); - const isFolder = FileManager.#isDirectory(absolutePath); + const doesPathExist = await FileManager.#doesPathExist(absolutePath); + const isFolder = await FileManager.#isDirectory(absolutePath); if (!isFolder) { console.error('Can\'t move into non-directory'); @@ -99,8 +100,8 @@ class FileManager { } async #ls() { - const list = new Promise(resolve => { - fs.readdir(this.#currentDirectory, resolve); + const list = await new Promise(resolve => { + fs.readdir(this.#currentDirectory, (error, files) => resolve(files)); }); const directories = await Promise.all(list.map(entity => { @@ -112,10 +113,10 @@ class FileManager { static async #isDirectory(absolutePath) { const stats = await new Promise(resolve => { - fs.lstat(absolutePath, (error, stats) => resolve(error)) + fs.lstat(absolutePath, (error, stats) => resolve(stats)) }); - return stats.isDirectory(); + return stats?.isDirectory?.(); } static async #doesPathExist(absolutePath) { From 73280808150f19194de934b35cec0482402411a4 Mon Sep 17 00:00:00 2001 From: Nikita Date: Fri, 2 Feb 2024 17:54:46 +0300 Subject: [PATCH 05/24] feat: ls & cd --- fileManager.js | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/fileManager.js b/fileManager.js index d323628..567160c 100644 --- a/fileManager.js +++ b/fileManager.js @@ -31,7 +31,7 @@ class FileManager { break; } case 'cd': { - const enteredPath = splittedInput.at(1); + const enteredPath = splittedInput.slice(1).join(' '); await this.#cd(enteredPath); break; } @@ -104,11 +104,29 @@ class FileManager { fs.readdir(this.#currentDirectory, (error, files) => resolve(files)); }); - const directories = await Promise.all(list.map(entity => { + const directoriesFlags = await Promise.all(list.map(entity => { + const fullPath = path.resolve(this.#currentDirectory, entity); return new Promise(resolve => { - FileManager.#isDirectory(entity).then(resolve); + FileManager.#isDirectory(fullPath).then(resolve); }); })); + + const files = []; + const directories = []; + + for (let index = 0; index < list.length; ++index) { + if (directoriesFlags[index]) { + directories.push(list[index]); + } else { + files.push(list[index]); + } + } + + const table = []; + directories.forEach(directory => table.push([directory, 'directory'])); + files.forEach(file => table.push([file, 'file'])); + + console.table(table); } static async #isDirectory(absolutePath) { From f660a0d6dbf942a1297a9e6171214eabce9201ca Mon Sep 17 00:00:00 2001 From: Nikita Date: Fri, 2 Feb 2024 18:01:24 +0300 Subject: [PATCH 06/24] feat: cat --- fileManager.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/fileManager.js b/fileManager.js index 567160c..ddccddd 100644 --- a/fileManager.js +++ b/fileManager.js @@ -40,6 +40,8 @@ class FileManager { break; } case 'cat': { + const enteredPath = splittedInput.slice(1).join(' '); + await this.#cat(enteredPath); break; } case 'rn': { @@ -129,6 +131,24 @@ class FileManager { console.table(table); } + async #cat(filePath) { + const absolutePath = path.resolve(this.#currentDirectory, filePath); + + const data = await new Promise(resolve => { + let response = ''; + const readStream = fs.createReadStream(absolutePath, { encoding: 'utf-8' }); + readStream.on('data', chunk => { + response += chunk; + }); + + readStream.on('close', () => { + resolve(response); + }); + }); + + console.log(data); + } + static async #isDirectory(absolutePath) { const stats = await new Promise(resolve => { fs.lstat(absolutePath, (error, stats) => resolve(stats)) From 75f4623b882eb61667aaef91eed45e9062a316b0 Mon Sep 17 00:00:00 2001 From: Nikita Date: Fri, 2 Feb 2024 18:16:20 +0300 Subject: [PATCH 07/24] feat: add --- fileManager.js | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/fileManager.js b/fileManager.js index ddccddd..c3c7185 100644 --- a/fileManager.js +++ b/fileManager.js @@ -44,6 +44,11 @@ class FileManager { await this.#cat(enteredPath); break; } + case 'add': { + const enteredFilename = splittedInput.slice(1).join(' '); + await this.#add(enteredFilename); + break; + } case 'rn': { break; } @@ -149,6 +154,29 @@ class FileManager { console.log(data); } + async #add(filename) { + const absolutePath = path.resolve(this.#currentDirectory, filename); + + const doesAlreadyExist = await new Promise(resolve => fs.exists(absolutePath, resolve)); + if (doesAlreadyExist) { + console.warn('File with such name already exists. Aborting'); + return; + } + + await new Promise(resolve => { + fs.open(absolutePath, 'w', (err, descriptor) => { + if (err) { + console.log(err.message); + resolve(); + } + + fs.close(descriptor, () => { + resolve(); + }); + }); + }); + } + static async #isDirectory(absolutePath) { const stats = await new Promise(resolve => { fs.lstat(absolutePath, (error, stats) => resolve(stats)) From 77a87be4a05f4a277699f43a39741280dffaf28e Mon Sep 17 00:00:00 2001 From: Nikita Date: Fri, 2 Feb 2024 18:43:08 +0300 Subject: [PATCH 08/24] feat: rn --- fileManager.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/fileManager.js b/fileManager.js index c3c7185..8cbfdc5 100644 --- a/fileManager.js +++ b/fileManager.js @@ -50,6 +50,8 @@ class FileManager { break; } case 'rn': { + const input = splittedInput.slice(1).join(' '); + await this.#rn(input); break; } case 'cp': { @@ -177,6 +179,24 @@ class FileManager { }); } + async #rn(enteredInputSource) { + // TODO: what to do with filenames with space ? + const parsed = enteredInputSource.trim().split(' '); + const filenameInQuotesRegex = /"([^"])"/g; + const [source, destination] = [parsed[0], parsed[1]]; + const [sourceAbsolute, destinationAbsolute] = [path.resolve(this.#currentDirectory, source), path.resolve(this.#currentDirectory, destination)]; + + + await new Promise(resolve => { + fs.rename(sourceAbsolute, destinationAbsolute, error => { + if (error) { + console.log(error); + } + resolve(); + }); + }); + } + static async #isDirectory(absolutePath) { const stats = await new Promise(resolve => { fs.lstat(absolutePath, (error, stats) => resolve(stats)) From 65c522ab02a3bec83e8b341219e798bc1f421fcf Mon Sep 17 00:00:00 2001 From: Nikita Date: Fri, 2 Feb 2024 19:44:51 +0300 Subject: [PATCH 09/24] feat: os --- CompressService.js | 3 +++ StatsService.js | 3 +++ StreamsService.js | 3 +++ fileManager.js | 33 +++++++++++++++++++++++++++++++-- 4 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 CompressService.js create mode 100644 StatsService.js create mode 100644 StreamsService.js diff --git a/CompressService.js b/CompressService.js new file mode 100644 index 0000000..feb9ae1 --- /dev/null +++ b/CompressService.js @@ -0,0 +1,3 @@ +export class CompressService { + +} diff --git a/StatsService.js b/StatsService.js new file mode 100644 index 0000000..1883e0f --- /dev/null +++ b/StatsService.js @@ -0,0 +1,3 @@ +export class StatsService { + +} diff --git a/StreamsService.js b/StreamsService.js new file mode 100644 index 0000000..6fa464a --- /dev/null +++ b/StreamsService.js @@ -0,0 +1,3 @@ +export class StreamsService { + +} diff --git a/fileManager.js b/fileManager.js index 8cbfdc5..772e028 100644 --- a/fileManager.js +++ b/fileManager.js @@ -50,11 +50,12 @@ class FileManager { break; } case 'rn': { - const input = splittedInput.slice(1).join(' '); - await this.#rn(input); + //const input = splittedInput.slice(1).join(' '); + //await this.#rn(input); break; } case 'cp': { + break; } case 'mv': { @@ -64,6 +65,8 @@ class FileManager { break; } case 'os': { + const argument = splittedInput.at(1); + this.#os(argument); break; } case 'compress': { @@ -197,6 +200,32 @@ class FileManager { }); } + #os(argument) { + switch (argument) { + case '--EOL': { + console.log(os.EOL); + break; + } + case '--cpus': { + const table = os.cpus().map(cpu => [cpu.model, cpu.speed]); + console.log(`CPUs count: ${table.length}`); + console.table(table); + break; + } + case '--homedir': { + console.log(os.homedir()); + break; + } + case '--username': { + console.log(os.userInfo().username); + break; + } + case '--architecture': { + console.log(os.arch()) + } + } + } + static async #isDirectory(absolutePath) { const stats = await new Promise(resolve => { fs.lstat(absolutePath, (error, stats) => resolve(stats)) From d25665177a8786265265f6302d7bb92a5d53845a Mon Sep 17 00:00:00 2001 From: Nikita Date: Fri, 2 Feb 2024 21:15:35 +0300 Subject: [PATCH 10/24] feat: hash --- CompressService.js | 7 +++- PathService.js | 18 +++++++++ StatsService.js | 18 +++++++++ StreamsService.js | 20 +++++++++- fileManager.js | 91 ++++++++++++++++------------------------------ 5 files changed, 93 insertions(+), 61 deletions(-) create mode 100644 PathService.js diff --git a/CompressService.js b/CompressService.js index feb9ae1..26c73ed 100644 --- a/CompressService.js +++ b/CompressService.js @@ -1,3 +1,8 @@ -export class CompressService { +import crypto from 'crypto'; + +export class CompressService { + static hash(data) { + return crypto.createHash('md5').update(name).digest('hex'); + } } diff --git a/PathService.js b/PathService.js new file mode 100644 index 0000000..ac4e8c7 --- /dev/null +++ b/PathService.js @@ -0,0 +1,18 @@ +import path from 'path'; + +export class PathService { + static isAbsolute(checkPath) { + return path.isAbsolute(checkPath); + } + static toAbsolute(dirname, relative) { + if (PathService.isAbsolute(relative)) { + return relative; + } + + return path.resolve(dirname, relative); + } + + static extractFilePathFromString(fromInput) { + return fromInput.join(' '); + } +} diff --git a/StatsService.js b/StatsService.js index 1883e0f..1e28fc4 100644 --- a/StatsService.js +++ b/StatsService.js @@ -1,3 +1,21 @@ +import fs from 'fs'; + export class StatsService { + static async stats(absolutePath) { + return await new Promise(resolve => { + fs.lstat(absolutePath, (error, stats) => { + if (error) { + console.warn(error); + } + + resolve(stats); + }); + }); + } + static async doesPathExist(absolutePath) { + return await new Promise(resolve => { + fs.exists(absolutePath, resolve); + }); + } } diff --git a/StreamsService.js b/StreamsService.js index 6fa464a..c67ab2a 100644 --- a/StreamsService.js +++ b/StreamsService.js @@ -1,3 +1,21 @@ +import fs from 'fs'; + export class StreamsService { - + static async readFile(absolutePath) { + return await new Promise(resolve => { + let response = ''; + const readStream = fs.createReadStream(absolutePath, { encoding: 'utf-8' }); + readStream.on('data', chunk => { + response += chunk; + }); + + readStream.on('close', () => { + resolve(response); + }); + }); + } + + static async pipeFileToFile(input, output, pipeFunction) { + + } } diff --git a/fileManager.js b/fileManager.js index 772e028..0baa8d8 100644 --- a/fileManager.js +++ b/fileManager.js @@ -2,6 +2,10 @@ import os from 'os'; import readline from 'readline'; import path from 'path'; import fs from 'fs'; +import {StatsService} from './StatsService.js'; +import {StreamsService} from './StreamsService.js'; +import {PathService} from './PathService.js'; +import {CompressService} from './CompressService.js'; class FileManager { @@ -69,6 +73,11 @@ class FileManager { this.#os(argument); break; } + case 'hash': { + const filePath = PathService.extractFilePathFromString(splittedInput.slice(1)); + await this.#hashFile(filePath); + break; + } case 'compress': { break; } @@ -84,20 +93,13 @@ class FileManager { } #up() { - this.#currentDirectory = path.resolve(this.#currentDirectory, '../'); + this.#currentDirectory = PathService.toAbsolute(this.#currentDirectory, '../'); } async #cd(enteredPath) { - let absolutePath; - - if (path.isAbsolute(enteredPath)) { - absolutePath = enteredPath; - } else { - absolutePath = path.resolve(this.#currentDirectory, enteredPath); - } - - const doesPathExist = await FileManager.#doesPathExist(absolutePath); - const isFolder = await FileManager.#isDirectory(absolutePath); + const absolutePath = PathService.toAbsolute(this.#currentDirectory, enteredPath); + const doesPathExist = await StatsService.doesPathExist(absolutePath); + const isFolder = (await StatsService.stats(absolutePath)).isDirectory(); if (!isFolder) { console.error('Can\'t move into non-directory'); @@ -116,53 +118,32 @@ class FileManager { fs.readdir(this.#currentDirectory, (error, files) => resolve(files)); }); - const directoriesFlags = await Promise.all(list.map(entity => { - const fullPath = path.resolve(this.#currentDirectory, entity); - return new Promise(resolve => { - FileManager.#isDirectory(fullPath).then(resolve); - }); - })); - - const files = []; - const directories = []; + const table = await Promise.all(list.map(async entity => { + const fullPath = PathService.toAbsolute(this.#currentDirectory, entity); + const stats = await StatsService.stats(fullPath); - for (let index = 0; index < list.length; ++index) { - if (directoriesFlags[index]) { - directories.push(list[index]); + if (stats.isDirectory()) { + return [entity, 'directory']; + } else if (stats.isFile()) { + return [entity, 'file']; } else { - files.push(list[index]); + return [entity, 'other']; } - } - - const table = []; - directories.forEach(directory => table.push([directory, 'directory'])); - files.forEach(file => table.push([file, 'file'])); + })); console.table(table); } async #cat(filePath) { - const absolutePath = path.resolve(this.#currentDirectory, filePath); - - const data = await new Promise(resolve => { - let response = ''; - const readStream = fs.createReadStream(absolutePath, { encoding: 'utf-8' }); - readStream.on('data', chunk => { - response += chunk; - }); - - readStream.on('close', () => { - resolve(response); - }); - }); - + const absolutePath = PathService.toAbsolute(this.#currentDirectory, filePath); + const data = StreamsService.readFile(absolutePath); console.log(data); } async #add(filename) { - const absolutePath = path.resolve(this.#currentDirectory, filename); + const absolutePath = PathService.toAbsolute(this.#currentDirectory, filename); - const doesAlreadyExist = await new Promise(resolve => fs.exists(absolutePath, resolve)); + const doesAlreadyExist = await StatsService.doesPathExist(absolutePath); if (doesAlreadyExist) { console.warn('File with such name already exists. Aborting'); return; @@ -187,8 +168,7 @@ class FileManager { const parsed = enteredInputSource.trim().split(' '); const filenameInQuotesRegex = /"([^"])"/g; const [source, destination] = [parsed[0], parsed[1]]; - const [sourceAbsolute, destinationAbsolute] = [path.resolve(this.#currentDirectory, source), path.resolve(this.#currentDirectory, destination)]; - + const [sourceAbsolute, destinationAbsolute] = [PathService.toAbsolute(this.#currentDirectory, source), PathService.toAbsolute(this.#currentDirectory, destination)]; await new Promise(resolve => { fs.rename(sourceAbsolute, destinationAbsolute, error => { @@ -226,18 +206,11 @@ class FileManager { } } - static async #isDirectory(absolutePath) { - const stats = await new Promise(resolve => { - fs.lstat(absolutePath, (error, stats) => resolve(stats)) - }); - - return stats?.isDirectory?.(); - } - - static async #doesPathExist(absolutePath) { - return await new Promise(resolve => { - fs.exists(absolutePath, resolve); - }); + async #hashFile(filePath) { + const absolutePath = PathService.toAbsolute(this.#currentDirectory, filePath); + const fileData = await StreamsService.readFile(absolutePath); + const hashedValue = CompressService.hash(fileData); + console.log(hashedValue); } } From b84ad43ab4c58677ca3853e3912c286ee1aab302 Mon Sep 17 00:00:00 2001 From: Nikita Date: Sat, 3 Feb 2024 21:09:44 +0300 Subject: [PATCH 11/24] feat: PathService::extractPath + tests --- PathService.js | 44 ++++++++++++++++++++++++++++++++++++++++++-- PathService.test.js | 39 +++++++++++++++++++++++++++++++++++++++ package.json | 3 ++- 3 files changed, 83 insertions(+), 3 deletions(-) create mode 100644 PathService.test.js diff --git a/PathService.js b/PathService.js index ac4e8c7..c28a120 100644 --- a/PathService.js +++ b/PathService.js @@ -12,7 +12,47 @@ export class PathService { return path.resolve(dirname, relative); } - static extractFilePathFromString(fromInput) { - return fromInput.join(' '); + /** + PathService::extractFilePathFromString() receives a string and expected number of paths inside it + If path contains spaces, it must be wrapped with quotes, for example: cp "./path-to-file/file name with space.txt". + But if we are expecting only one file in function, for example cat new file.txt -- then we can not use quotes. + So quotes in required as wrapper for path/filename only if >= 2 entities are expected as arguments. + So acceptable combinations for entering paths: cp "folder 1" "folder 2", cp folder-no-space "folder 2", cp "folder space" folder-no-space, cp folder-no-space1 no-space2 + */ + static extractFilePathFromString(wholeStringFromInputSource, expectedPathsCount) { + const longPathsDelimiter = '"'; + const basePathsDelimiter = ' '; + + const wholeStringFromInput = wholeStringFromInputSource.trim(); + const splittedByQuotes = PathService.#splitPathsByDelimiter(wholeStringFromInput, longPathsDelimiter); + + if (expectedPathsCount === 1) { + if (splittedByQuotes.length !== 1) { + return {parseStatusSuccess: false, paths: []}; + } + + return {parseStatusSuccess: true, paths: splittedByQuotes}; + } + + if (splittedByQuotes.length === 1) { + // No long paths detected, for example: cp ./file1 ./file2 ==> split by space + const paths = PathService.#splitPathsByDelimiter(wholeStringFromInput, basePathsDelimiter); + if (paths.length !== expectedPathsCount) { + return {parseStatusSuccess: false, paths: []}; + } + + return { parseStatusSuccess: true, paths}; + } + + // one or all paths are in quotes, for example: cp "./file.txt" file2.txt + if (splittedByQuotes.length !== expectedPathsCount) { + return {parseStatusSuccess: false, paths: []}; + } + + return {parseStatusSuccess: true, paths: splittedByQuotes} + } + + static #splitPathsByDelimiter(input, delimiter) { + return input.split(delimiter).filter(path => path.trim() !== '').map(item => item.trim()); } } diff --git a/PathService.test.js b/PathService.test.js new file mode 100644 index 0000000..9be6f8d --- /dev/null +++ b/PathService.test.js @@ -0,0 +1,39 @@ +import {PathService} from './PathService.js'; + +/** + PathService::extractFilePathFromString() receives a string and expected number of paths inside it + If path contains spaces, it must be wrapped with quotes, for example: cp "./path-to-file/file name with space.txt". + But if we are expecting only one file in function, for example cat new file.txt -- then we can not use quotes. + So quotes in required as wrapper for path/filename only if >= 2 entities are expected as arguments. + So acceptable combinations for entering paths: cp "folder 1" "folder 2", cp folder-no-space "folder 2", cp "folder space" folder-no-space, cp folder-no-space1 no-space2 + */ + +const testCases = [ + [['"path-in-quotes" path-without-quotes', 2], {parseStatusSuccess: true, paths: ['path-in-quotes', 'path-without-quotes']}], + [['"./long path with space/path-in-quotes.txt" path-without-quotes', 2], {parseStatusSuccess: true, paths: ['./long path with space/path-in-quotes.txt', 'path-without-quotes']}], + [['"./long path with space/path-in-quotes.txt" path-without-quotes', 1], {parseStatusSuccess: false, paths: []}], + [[' "./long path with space/path-in-quotes.txt" ', 1], {parseStatusSuccess: true, paths: ['./long path with space/path-in-quotes.txt']}], + [[' "./long path with space/path-in-quotes.txt" ', 2], {parseStatusSuccess: false, paths: []}], + [[' "./long path with space/path-in-quotes.txt" ', 3], {parseStatusSuccess: false, paths: []}], + [[' "./long path with space/path-in-quotes.txt" "../../another-path.txt"', 2], {parseStatusSuccess: true, paths: ['./long path with space/path-in-quotes.txt', '../../another-path.txt']}], + [[' "./long path with space/path-in-quotes.txt" "../../another-path.txt"', 1], {parseStatusSuccess: false, paths: []}], + [['./long path with space/path-in-quotes.txt', 1], {parseStatusSuccess: true, paths: ['./long path with space/path-in-quotes.txt']}], + [['./long path with space/path-in-quotes.txt ./another-long-path/file with space.txt', 2], {parseStatusSuccess: false, paths: []}], + [['./long path with space/path-in-quotes.txt ./another-long-path/file with space.txt', 1], {parseStatusSuccess: true, paths: ['./long path with space/path-in-quotes.txt ./another-long-path/file with space.txt']}], + [['"folder 1" "./folder 2/file"', 2], {parseStatusSuccess: true, paths: ['folder 1', './folder 2/file']}], +]; + +const test = () => { + testCases.forEach((testCase, index) => { + console.log(`Test case #${index}`); + + const input = testCase[0]; + const expectedResult = testCase[1]; + const receivedResult = PathService.extractFilePathFromString(...input); + + console.assert(JSON.stringify(expectedResult) === JSON.stringify(receivedResult)); + }); +}; + + +test(); diff --git a/package.json b/package.json index 6992092..8e96777 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,8 @@ "type": "module", "main": "index.js", "scripts": { - "start": "node ./index.js" + "start": "node ./index.js", + "test:path-service": "node ./PathService.test.js" }, "keywords": [], "author": "Slutski Mikita (@user-of-github)", From e1180e9a8df502c3129eec4afe0f6be2c1450362 Mon Sep 17 00:00:00 2001 From: Nikita Date: Sat, 3 Feb 2024 21:11:11 +0300 Subject: [PATCH 12/24] docs: package.json author info --- package.json | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 8e96777..b045d1e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "file-manager", "version": "1.0.0", - "description": "File manager for Rolling Scopes", + "description": "File manager for Rolling Scopes Node.js 2024Q1", "type": "module", "main": "index.js", "scripts": { @@ -9,6 +9,9 @@ "test:path-service": "node ./PathService.test.js" }, "keywords": [], - "author": "Slutski Mikita (@user-of-github)", + "author": { + "name": "Slutski Mikita", + "url": "https://github.com/user-of-github" + }, "license": "ISC" } From bf54e6899fdc2ae0ae67226b6224ea96ffb56d1e Mon Sep 17 00:00:00 2001 From: Nikita Date: Sat, 3 Feb 2024 21:22:57 +0300 Subject: [PATCH 13/24] fix: cd --- fileManager.js => fileManager/FileManager.js | 38 ++++++++++++------- .../services/CompressService.js | 0 .../services/PathService.js | 0 .../services/StatsService.js | 0 .../services/StreamsService.js | 0 .../tests/PathService.test.js | 2 +- index.js | 2 +- package.json | 2 +- 8 files changed, 27 insertions(+), 17 deletions(-) rename fileManager.js => fileManager/FileManager.js (86%) rename CompressService.js => fileManager/services/CompressService.js (100%) rename PathService.js => fileManager/services/PathService.js (100%) rename StatsService.js => fileManager/services/StatsService.js (100%) rename StreamsService.js => fileManager/services/StreamsService.js (100%) rename PathService.test.js => fileManager/tests/PathService.test.js (97%) diff --git a/fileManager.js b/fileManager/FileManager.js similarity index 86% rename from fileManager.js rename to fileManager/FileManager.js index 0baa8d8..e61d1d8 100644 --- a/fileManager.js +++ b/fileManager/FileManager.js @@ -1,11 +1,10 @@ import os from 'os'; import readline from 'readline'; -import path from 'path'; import fs from 'fs'; -import {StatsService} from './StatsService.js'; -import {StreamsService} from './StreamsService.js'; -import {PathService} from './PathService.js'; -import {CompressService} from './CompressService.js'; +import {StatsService} from './services/StatsService.js'; +import {StreamsService} from './services/StreamsService.js'; +import {PathService} from './services/PathService.js'; +import {CompressService} from './services/CompressService.js'; class FileManager { @@ -22,12 +21,13 @@ class FileManager { output: process.stdout }); - while (true) { console.info(`Current directory: ${this.#currentDirectory}`); + const input = await read.question(''); const splittedInput = input.split(' '); const command = splittedInput.at(0); + const restPartOfInput = splittedInput.slice(1).join(' '); switch (command) { case 'up': { @@ -35,8 +35,12 @@ class FileManager { break; } case 'cd': { - const enteredPath = splittedInput.slice(1).join(' '); - await this.#cd(enteredPath); + const enteredPath = PathService.extractFilePathFromString(restPartOfInput, 1); + if (!enteredPath.parseStatusSuccess) { + console.warn('Invalid path argument'); + break; + } + await this.#cd(enteredPath.paths[0]); break; } case 'ls': { @@ -88,6 +92,10 @@ class FileManager { read.close(); return; } + default: { + console.warn('Unknown command'); + break; + } } } } @@ -99,18 +107,20 @@ class FileManager { async #cd(enteredPath) { const absolutePath = PathService.toAbsolute(this.#currentDirectory, enteredPath); const doesPathExist = await StatsService.doesPathExist(absolutePath); + + if (!doesPathExist) { + console.warn('Path does not exist'); + return; + } + const isFolder = (await StatsService.stats(absolutePath)).isDirectory(); if (!isFolder) { - console.error('Can\'t move into non-directory'); + console.warn('Can\'t move into non-directory'); return; } - if (doesPathExist) { - this.#currentDirectory = absolutePath; - } else { - console.error('Path does not exist') - } + this.#currentDirectory = absolutePath; } async #ls() { diff --git a/CompressService.js b/fileManager/services/CompressService.js similarity index 100% rename from CompressService.js rename to fileManager/services/CompressService.js diff --git a/PathService.js b/fileManager/services/PathService.js similarity index 100% rename from PathService.js rename to fileManager/services/PathService.js diff --git a/StatsService.js b/fileManager/services/StatsService.js similarity index 100% rename from StatsService.js rename to fileManager/services/StatsService.js diff --git a/StreamsService.js b/fileManager/services/StreamsService.js similarity index 100% rename from StreamsService.js rename to fileManager/services/StreamsService.js diff --git a/PathService.test.js b/fileManager/tests/PathService.test.js similarity index 97% rename from PathService.test.js rename to fileManager/tests/PathService.test.js index 9be6f8d..9e69ca2 100644 --- a/PathService.test.js +++ b/fileManager/tests/PathService.test.js @@ -1,4 +1,4 @@ -import {PathService} from './PathService.js'; +import {PathService} from '../services/PathService.js'; /** PathService::extractFilePathFromString() receives a string and expected number of paths inside it diff --git a/index.js b/index.js index ee670f8..9605da8 100644 --- a/index.js +++ b/index.js @@ -1,4 +1,4 @@ -import FileManager from './fileManager.js'; +import FileManager from './fileManager/FileManager.js'; const main = async () => { // TODO diff --git a/package.json b/package.json index b045d1e..5be4df2 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "main": "index.js", "scripts": { "start": "node ./index.js", - "test:path-service": "node ./PathService.test.js" + "test:path-service": "node fileManager/tests/PathService.test.js" }, "keywords": [], "author": { From bd506070a37de2c3a9def49823433875aa5cc423 Mon Sep 17 00:00:00 2001 From: Nikita Date: Sat, 3 Feb 2024 21:36:53 +0300 Subject: [PATCH 14/24] feat: fix bug for paths-extractor --- fileManager/FileManager.js | 37 +++++++++++++++++++---------- fileManager/services/PathService.js | 12 +++++++--- 2 files changed, 34 insertions(+), 15 deletions(-) diff --git a/fileManager/FileManager.js b/fileManager/FileManager.js index e61d1d8..26fe069 100644 --- a/fileManager/FileManager.js +++ b/fileManager/FileManager.js @@ -8,6 +8,9 @@ import {CompressService} from './services/CompressService.js'; class FileManager { + static #invalidSinglePathMessage = 'Invalid path argument'; + static #invalid2PathsMessage = 'Invalid paths argument. 2 paths must be provided. If file-path contains spaces, it must be wrapped with "quoues"'; + #currentDirectory; constructor() { @@ -37,9 +40,10 @@ class FileManager { case 'cd': { const enteredPath = PathService.extractFilePathFromString(restPartOfInput, 1); if (!enteredPath.parseStatusSuccess) { - console.warn('Invalid path argument'); + console.warn(FileManager.#invalidSinglePathMessage); break; } + await this.#cd(enteredPath.paths[0]); break; } @@ -48,18 +52,31 @@ class FileManager { break; } case 'cat': { - const enteredPath = splittedInput.slice(1).join(' '); - await this.#cat(enteredPath); + const enteredPath = PathService.extractFilePathFromString(restPartOfInput, 1); + if (!enteredPath.parseStatusSuccess) { + console.warn(FileManager.#invalidSinglePathMessage); + break; + } + + await this.#cat(enteredPath.paths[0]); break; } case 'add': { - const enteredFilename = splittedInput.slice(1).join(' '); - await this.#add(enteredFilename); + const enteredFilename = PathService.extractFilePathFromString(restPartOfInput, 1); + if (!enteredFilename.parseStatusSuccess) { + console.warn(FileManager.#invalidSinglePathMessage); + break; + } + await this.#add(enteredFilename.paths[0]); break; } case 'rn': { - //const input = splittedInput.slice(1).join(' '); - //await this.#rn(input); + const enteredFilename = PathService.extractFilePathFromString(restPartOfInput, 2); + if (!enteredFilename.parseStatusSuccess) { + console.warn(FileManager.#invalid2PathsMessage); + break; + } + await this.#rn(...enteredFilename.paths); break; } case 'cp': { @@ -173,11 +190,7 @@ class FileManager { }); } - async #rn(enteredInputSource) { - // TODO: what to do with filenames with space ? - const parsed = enteredInputSource.trim().split(' '); - const filenameInQuotesRegex = /"([^"])"/g; - const [source, destination] = [parsed[0], parsed[1]]; + async #rn(source, destination) { const [sourceAbsolute, destinationAbsolute] = [PathService.toAbsolute(this.#currentDirectory, source), PathService.toAbsolute(this.#currentDirectory, destination)]; await new Promise(resolve => { diff --git a/fileManager/services/PathService.js b/fileManager/services/PathService.js index c28a120..5351527 100644 --- a/fileManager/services/PathService.js +++ b/fileManager/services/PathService.js @@ -20,15 +20,21 @@ export class PathService { So acceptable combinations for entering paths: cp "folder 1" "folder 2", cp folder-no-space "folder 2", cp "folder space" folder-no-space, cp folder-no-space1 no-space2 */ static extractFilePathFromString(wholeStringFromInputSource, expectedPathsCount) { + const invalidPathResponse = {parseStatusSuccess: false, paths: []}; const longPathsDelimiter = '"'; const basePathsDelimiter = ' '; const wholeStringFromInput = wholeStringFromInputSource.trim(); + const quotesCount = wholeStringFromInput.split('').filter(symbol => symbol === longPathsDelimiter).length; + if (quotesCount % 2 !== 0) { // every filename must be wrapped from both sides ==> number of quotes is even + return invalidPathResponse; + } + const splittedByQuotes = PathService.#splitPathsByDelimiter(wholeStringFromInput, longPathsDelimiter); if (expectedPathsCount === 1) { if (splittedByQuotes.length !== 1) { - return {parseStatusSuccess: false, paths: []}; + return invalidPathResponse; } return {parseStatusSuccess: true, paths: splittedByQuotes}; @@ -38,7 +44,7 @@ export class PathService { // No long paths detected, for example: cp ./file1 ./file2 ==> split by space const paths = PathService.#splitPathsByDelimiter(wholeStringFromInput, basePathsDelimiter); if (paths.length !== expectedPathsCount) { - return {parseStatusSuccess: false, paths: []}; + return invalidPathResponse; } return { parseStatusSuccess: true, paths}; @@ -46,7 +52,7 @@ export class PathService { // one or all paths are in quotes, for example: cp "./file.txt" file2.txt if (splittedByQuotes.length !== expectedPathsCount) { - return {parseStatusSuccess: false, paths: []}; + return invalidPathResponse; } return {parseStatusSuccess: true, paths: splittedByQuotes} From d0de0b35c098941fab82451c1a91ddda6400e1fd Mon Sep 17 00:00:00 2001 From: Nikita Date: Sat, 3 Feb 2024 22:07:00 +0300 Subject: [PATCH 15/24] feat: rm, cp --- fileManager/FileManager.js | 73 +++++++++++++++++++++++++++++++++----- index.js | 4 +-- 2 files changed, 66 insertions(+), 11 deletions(-) diff --git a/fileManager/FileManager.js b/fileManager/FileManager.js index 26fe069..27c8219 100644 --- a/fileManager/FileManager.js +++ b/fileManager/FileManager.js @@ -9,6 +9,7 @@ import {CompressService} from './services/CompressService.js'; class FileManager { static #invalidSinglePathMessage = 'Invalid path argument'; + static #unknownAttributeMessage = 'Unknown attribute'; static #invalid2PathsMessage = 'Invalid paths argument. 2 paths must be provided. If file-path contains spaces, it must be wrapped with "quoues"'; #currentDirectory; @@ -80,13 +81,24 @@ class FileManager { break; } case 'cp': { - + const enteredFilename = PathService.extractFilePathFromString(restPartOfInput, 2); + if (!enteredFilename.parseStatusSuccess) { + console.warn(FileManager.#invalid2PathsMessage); + break; + } + await this.#cp(...enteredFilename.paths); break; } case 'mv': { break; } case 'rm': { + const enteredFilename = PathService.extractFilePathFromString(restPartOfInput, 1); + if (!enteredFilename.parseStatusSuccess) { + console.warn(FileManager.#invalid2PathsMessage); + break; + } + await this.#rm(enteredFilename.paths[0]); break; } case 'os': { @@ -164,7 +176,7 @@ class FileManager { async #cat(filePath) { const absolutePath = PathService.toAbsolute(this.#currentDirectory, filePath); const data = StreamsService.readFile(absolutePath); - console.log(data); + console.info(data); } async #add(filename) { @@ -179,7 +191,7 @@ class FileManager { await new Promise(resolve => { fs.open(absolutePath, 'w', (err, descriptor) => { if (err) { - console.log(err.message); + console.info(err.message); resolve(); } @@ -206,34 +218,77 @@ class FileManager { #os(argument) { switch (argument) { case '--EOL': { - console.log(os.EOL); + console.info(os.EOL); break; } case '--cpus': { const table = os.cpus().map(cpu => [cpu.model, cpu.speed]); - console.log(`CPUs count: ${table.length}`); + console.info(`CPUs count: ${table.length}`); console.table(table); break; } case '--homedir': { - console.log(os.homedir()); + console.info(os.homedir()); break; } case '--username': { - console.log(os.userInfo().username); + console.info(os.userInfo().username); break; } case '--architecture': { - console.log(os.arch()) + console.info(os.arch()); + break; + } + default: { + console.info(FileManager.#unknownAttributeMessage); + break; } } } + async #cp(source, destination) { + const absoluteSource = PathService.toAbsolute(this.#currentDirectory, source); + const absoluteDestination = PathService.toAbsolute(this.#currentDirectory, destination); + + return await new Promise(resolve => { + fs.cp(absoluteSource, absoluteDestination, {recursive: true}, error => { + if (error) { + console.warn(error); + resolve(false); + } + + resolve(true); + }); + }); + } + + async #rm(path) { + await new Promise(resolve => { + fs.rm(path, error => { + if (error) { + console.warn(error); + } + + resolve(); + }); + }); + } + + async mv(source, destination) { + const isCopySuccessful = await this.#cp(source, destination); + + if (isCopySuccessful) { + await this.#rm(source); + } else { + console.warn('Failed to execute mv command'); + } + } + async #hashFile(filePath) { const absolutePath = PathService.toAbsolute(this.#currentDirectory, filePath); const fileData = await StreamsService.readFile(absolutePath); const hashedValue = CompressService.hash(fileData); - console.log(hashedValue); + console.info(hashedValue); } } diff --git a/index.js b/index.js index 9605da8..5c3fc40 100644 --- a/index.js +++ b/index.js @@ -4,10 +4,10 @@ const main = async () => { // TODO const username = process.argv[2]; - console.log(`Welcome to the File Manager, ${username}!`); + console.info(`Welcome to the File Manager, ${username}!`); process.addListener('exit', () => { - console.log(`Thank you for using File Manager, ${username}, goodbye!`); + console.info(`Thank you for using File Manager, ${username}, goodbye!`); }); await FileManager.run(); From 3943b28158fb8a2a5054314e7867b86d3e792c30 Mon Sep 17 00:00:00 2001 From: Nikita Date: Sat, 3 Feb 2024 22:15:16 +0300 Subject: [PATCH 16/24] fix: remove .idea from staging --- .idea/.gitignore | 5 ----- .idea/file-manager.iml | 12 ------------ .idea/jsLibraryMappings.xml | 6 ------ .idea/modules.xml | 8 -------- .idea/vcs.xml | 6 ------ 5 files changed, 37 deletions(-) delete mode 100644 .idea/.gitignore delete mode 100644 .idea/file-manager.iml delete mode 100644 .idea/jsLibraryMappings.xml delete mode 100644 .idea/modules.xml delete mode 100644 .idea/vcs.xml diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index b58b603..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Editor-based HTTP Client requests -/httpRequests/ diff --git a/.idea/file-manager.iml b/.idea/file-manager.iml deleted file mode 100644 index 24643cc..0000000 --- a/.idea/file-manager.iml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/jsLibraryMappings.xml b/.idea/jsLibraryMappings.xml deleted file mode 100644 index d23208f..0000000 --- a/.idea/jsLibraryMappings.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 1a85d97..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 35eb1dd..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file From a886eeba9c4597f3391db8e0512df272f135cb97 Mon Sep 17 00:00:00 2001 From: Nikita Date: Sat, 3 Feb 2024 22:40:16 +0300 Subject: [PATCH 17/24] feat: compress+decompress --- fileManager/FileManager.js | 105 ++++++++++++++++-------- fileManager/services/CompressService.js | 23 ++++++ fileManager/services/StreamsService.js | 8 ++ 3 files changed, 102 insertions(+), 34 deletions(-) diff --git a/fileManager/FileManager.js b/fileManager/FileManager.js index 27c8219..a40687f 100644 --- a/fileManager/FileManager.js +++ b/fileManager/FileManager.js @@ -21,8 +21,7 @@ class FileManager { async run() { const read = readline.promises.createInterface({ - input: process.stdin, - output: process.stdout + input: process.stdin, output: process.stdout }); while (true) { @@ -90,6 +89,12 @@ class FileManager { break; } case 'mv': { + const enteredFilename = PathService.extractFilePathFromString(restPartOfInput, 2); + if (!enteredFilename.parseStatusSuccess) { + console.warn(FileManager.#invalid2PathsMessage); + break; + } + await this.#mv(...enteredFilename.paths); break; } case 'rm': { @@ -112,9 +117,21 @@ class FileManager { break; } case 'compress': { + const enteredFilename = PathService.extractFilePathFromString(restPartOfInput, 1); + if (!enteredFilename.parseStatusSuccess) { + console.warn(FileManager.#invalid2PathsMessage); + break; + } + await this.#compress(...enteredFilename.paths); break; } case 'decompress': { + const enteredFilename = PathService.extractFilePathFromString(restPartOfInput, 1); + if (!enteredFilename.parseStatusSuccess) { + console.warn(FileManager.#invalid2PathsMessage); + break; + } + await this.#decompress(...enteredFilename.paths); break; } case '.exit': { @@ -215,37 +232,6 @@ class FileManager { }); } - #os(argument) { - switch (argument) { - case '--EOL': { - console.info(os.EOL); - break; - } - case '--cpus': { - const table = os.cpus().map(cpu => [cpu.model, cpu.speed]); - console.info(`CPUs count: ${table.length}`); - console.table(table); - break; - } - case '--homedir': { - console.info(os.homedir()); - break; - } - case '--username': { - console.info(os.userInfo().username); - break; - } - case '--architecture': { - console.info(os.arch()); - break; - } - default: { - console.info(FileManager.#unknownAttributeMessage); - break; - } - } - } - async #cp(source, destination) { const absoluteSource = PathService.toAbsolute(this.#currentDirectory, source); const absoluteDestination = PathService.toAbsolute(this.#currentDirectory, destination); @@ -274,7 +260,7 @@ class FileManager { }); } - async mv(source, destination) { + async #mv(source, destination) { const isCopySuccessful = await this.#cp(source, destination); if (isCopySuccessful) { @@ -284,12 +270,63 @@ class FileManager { } } + #os(argument) { + switch (argument) { + case '--EOL': { + console.info(os.EOL); + break; + } + case '--cpus': { + const table = os.cpus().map(cpu => [cpu.model, cpu.speed]); + console.info(`CPUs count: ${table.length}`); + console.table(table); + break; + } + case '--homedir': { + console.info(os.homedir()); + break; + } + case '--username': { + console.info(os.userInfo().username); + break; + } + case '--architecture': { + console.info(os.arch()); + break; + } + default: { + console.info(FileManager.#unknownAttributeMessage); + break; + } + } + } + async #hashFile(filePath) { const absolutePath = PathService.toAbsolute(this.#currentDirectory, filePath); const fileData = await StreamsService.readFile(absolutePath); const hashedValue = CompressService.hash(fileData); console.info(hashedValue); } + + async #compress(source, destination) { + const absoluteSource = PathService.toAbsolute(this.#currentDirectory, source); + const absoluteDestination = PathService.toAbsolute(this.#currentDirectory, destination); + + const sourceStream = StreamsService.getReadStream(absoluteSource); + const destinationStream = StreamsService.getWriteStream(absoluteDestination); + + await CompressService.compressWithBrotli(sourceStream, destinationStream); + } + + async #decompress(source, destination) { + const absoluteSource = PathService.toAbsolute(this.#currentDirectory, source); + const absoluteDestination = PathService.toAbsolute(this.#currentDirectory, destination); + + const sourceStream = StreamsService.getReadStream(absoluteSource); + const destinationStream = StreamsService.getWriteStream(absoluteDestination); + + await CompressService.decompressWithBrotli(sourceStream, destinationStream); + } } export default new FileManager(); diff --git a/fileManager/services/CompressService.js b/fileManager/services/CompressService.js index 26c73ed..e6c67fa 100644 --- a/fileManager/services/CompressService.js +++ b/fileManager/services/CompressService.js @@ -1,8 +1,31 @@ import crypto from 'crypto'; +import zlib from 'zlib'; export class CompressService { static hash(data) { return crypto.createHash('md5').update(name).digest('hex'); } + + static async compressWithBrotli(sourceStream, destinationStream) { + const brotli = zlib.createBrotliCompress(); + + await new Promise(resolve => { + const stream = sourceStream.pipe(brotli).pipe(destinationStream); + stream.on('finish', () => { + resolve(); + }); + }); + } + + static async decompressWithBrotli(sourceStream, destinationStream) { + const brotli = zlib.createBrotliDecompress(); + + await new Promise(resolve => { + const stream = sourceStream.pipe(brotli).pipe(destinationStream); + stream.on('finish', () => { + resolve(); + }); + }); + } } diff --git a/fileManager/services/StreamsService.js b/fileManager/services/StreamsService.js index c67ab2a..86db14f 100644 --- a/fileManager/services/StreamsService.js +++ b/fileManager/services/StreamsService.js @@ -18,4 +18,12 @@ export class StreamsService { static async pipeFileToFile(input, output, pipeFunction) { } + + static getReadStream(source, options = {}) { + return fs.createReadStream(source, options); + } + + static getWriteStream(destination, options = {}) { + return fs.createWriteStream(destination, options); + } } From 19c2dcf56cf1d35bbcc78424aad0f4b2d94496ab Mon Sep 17 00:00:00 2001 From: Nikita Date: Sat, 3 Feb 2024 22:47:19 +0300 Subject: [PATCH 18/24] docs: readme; refactor: remove redundant code --- Readme.md | 13 +++++++++++++ fileManager/services/CompressService.js | 2 +- fileManager/services/PathService.js | 1 + fileManager/services/StreamsService.js | 4 ---- fileManager/tests/PathService.test.js | 1 - package.json | 2 +- 6 files changed, 16 insertions(+), 7 deletions(-) create mode 100644 Readme.md diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..b10ff02 --- /dev/null +++ b/Readme.md @@ -0,0 +1,13 @@ +## Simple File Manager +___ +### Manual: +___ +### Technologies stack: +- _Node.js_ +- _JavaScript_ + +___ + +###### Copyright © 2024 +###### Created by Slutski Mikita +###### Inspired by Rolling Scopes diff --git a/fileManager/services/CompressService.js b/fileManager/services/CompressService.js index e6c67fa..08fb9e7 100644 --- a/fileManager/services/CompressService.js +++ b/fileManager/services/CompressService.js @@ -4,7 +4,7 @@ import zlib from 'zlib'; export class CompressService { static hash(data) { - return crypto.createHash('md5').update(name).digest('hex'); + return crypto.createHash('md5').update(data).digest('hex'); } static async compressWithBrotli(sourceStream, destinationStream) { diff --git a/fileManager/services/PathService.js b/fileManager/services/PathService.js index 5351527..2c32df6 100644 --- a/fileManager/services/PathService.js +++ b/fileManager/services/PathService.js @@ -4,6 +4,7 @@ export class PathService { static isAbsolute(checkPath) { return path.isAbsolute(checkPath); } + static toAbsolute(dirname, relative) { if (PathService.isAbsolute(relative)) { return relative; diff --git a/fileManager/services/StreamsService.js b/fileManager/services/StreamsService.js index 86db14f..f96ba7e 100644 --- a/fileManager/services/StreamsService.js +++ b/fileManager/services/StreamsService.js @@ -15,10 +15,6 @@ export class StreamsService { }); } - static async pipeFileToFile(input, output, pipeFunction) { - - } - static getReadStream(source, options = {}) { return fs.createReadStream(source, options); } diff --git a/fileManager/tests/PathService.test.js b/fileManager/tests/PathService.test.js index 9e69ca2..815dfad 100644 --- a/fileManager/tests/PathService.test.js +++ b/fileManager/tests/PathService.test.js @@ -7,7 +7,6 @@ import {PathService} from '../services/PathService.js'; So quotes in required as wrapper for path/filename only if >= 2 entities are expected as arguments. So acceptable combinations for entering paths: cp "folder 1" "folder 2", cp folder-no-space "folder 2", cp "folder space" folder-no-space, cp folder-no-space1 no-space2 */ - const testCases = [ [['"path-in-quotes" path-without-quotes', 2], {parseStatusSuccess: true, paths: ['path-in-quotes', 'path-without-quotes']}], [['"./long path with space/path-in-quotes.txt" path-without-quotes', 2], {parseStatusSuccess: true, paths: ['./long path with space/path-in-quotes.txt', 'path-without-quotes']}], diff --git a/package.json b/package.json index 5be4df2..39d77ce 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "file-manager", - "version": "1.0.0", + "version": "0.1.0", "description": "File manager for Rolling Scopes Node.js 2024Q1", "type": "module", "main": "index.js", From 4d1aec2df194d13847f5d747bcee077912056c9e Mon Sep 17 00:00:00 2001 From: Nikita Date: Sun, 4 Feb 2024 10:56:05 +0300 Subject: [PATCH 19/24] feat: debug and fix some 'corner-cases' :) --- fileManager/FileManager.js | 43 +++++++++++++++++--------- fileManager/services/StatsService.js | 4 --- fileManager/services/StreamsService.js | 4 ++- fileManager/tests/PathService.test.js | 1 + 4 files changed, 33 insertions(+), 19 deletions(-) diff --git a/fileManager/FileManager.js b/fileManager/FileManager.js index a40687f..3a85f9e 100644 --- a/fileManager/FileManager.js +++ b/fileManager/FileManager.js @@ -10,7 +10,7 @@ import {CompressService} from './services/CompressService.js'; class FileManager { static #invalidSinglePathMessage = 'Invalid path argument'; static #unknownAttributeMessage = 'Unknown attribute'; - static #invalid2PathsMessage = 'Invalid paths argument. 2 paths must be provided. If file-path contains spaces, it must be wrapped with "quoues"'; + static #invalid2PathsMessage = 'Invalid paths argument. 2 paths must be provided. If file-path contains spaces, it should be wrapped with "quoues"'; #currentDirectory; @@ -138,8 +138,12 @@ class FileManager { read.close(); return; } + case '': { + console.warn('Empty command'); + break; + } default: { - console.warn('Unknown command'); + console.warn(`Unknown command "${command}"`); break; } } @@ -178,13 +182,20 @@ class FileManager { const fullPath = PathService.toAbsolute(this.#currentDirectory, entity); const stats = await StatsService.stats(fullPath); - if (stats.isDirectory()) { - return [entity, 'directory']; - } else if (stats.isFile()) { - return [entity, 'file']; - } else { - return [entity, 'other']; - } + try { + if (stats.isDirectory()) { + return [entity, 'directory']; + } else if (stats.isFile()) { + return [entity, 'file']; + } else if (stats.isSymbolicLink()) { + return [entity, 'symbolic link']; + } + else { + return [entity, 'other']; + } + } catch (error) { + return [entity, 'unknown']; + } })); console.table(table); @@ -192,8 +203,12 @@ class FileManager { async #cat(filePath) { const absolutePath = PathService.toAbsolute(this.#currentDirectory, filePath); - const data = StreamsService.readFile(absolutePath); - console.info(data); + try { + const data = await StreamsService.readFile(absolutePath); + console.info(data); + } catch { + console.warn('Unable to read contents by passed path'); + } } async #add(filename) { @@ -225,7 +240,7 @@ class FileManager { await new Promise(resolve => { fs.rename(sourceAbsolute, destinationAbsolute, error => { if (error) { - console.log(error); + console.log(error.message); } resolve(); }); @@ -239,7 +254,7 @@ class FileManager { return await new Promise(resolve => { fs.cp(absoluteSource, absoluteDestination, {recursive: true}, error => { if (error) { - console.warn(error); + console.warn(error.message); resolve(false); } @@ -252,7 +267,7 @@ class FileManager { await new Promise(resolve => { fs.rm(path, error => { if (error) { - console.warn(error); + console.warn(error.message); } resolve(); diff --git a/fileManager/services/StatsService.js b/fileManager/services/StatsService.js index 1e28fc4..836d37c 100644 --- a/fileManager/services/StatsService.js +++ b/fileManager/services/StatsService.js @@ -4,10 +4,6 @@ export class StatsService { static async stats(absolutePath) { return await new Promise(resolve => { fs.lstat(absolutePath, (error, stats) => { - if (error) { - console.warn(error); - } - resolve(stats); }); }); diff --git a/fileManager/services/StreamsService.js b/fileManager/services/StreamsService.js index f96ba7e..5384fb0 100644 --- a/fileManager/services/StreamsService.js +++ b/fileManager/services/StreamsService.js @@ -2,13 +2,15 @@ import fs from 'fs'; export class StreamsService { static async readFile(absolutePath) { - return await new Promise(resolve => { + return await new Promise((resolve, reject) => { let response = ''; const readStream = fs.createReadStream(absolutePath, { encoding: 'utf-8' }); readStream.on('data', chunk => { response += chunk; }); + readStream.on('error', reject); + readStream.on('close', () => { resolve(response); }); diff --git a/fileManager/tests/PathService.test.js b/fileManager/tests/PathService.test.js index 815dfad..5765323 100644 --- a/fileManager/tests/PathService.test.js +++ b/fileManager/tests/PathService.test.js @@ -20,6 +20,7 @@ const testCases = [ [['./long path with space/path-in-quotes.txt ./another-long-path/file with space.txt', 2], {parseStatusSuccess: false, paths: []}], [['./long path with space/path-in-quotes.txt ./another-long-path/file with space.txt', 1], {parseStatusSuccess: true, paths: ['./long path with space/path-in-quotes.txt ./another-long-path/file with space.txt']}], [['"folder 1" "./folder 2/file"', 2], {parseStatusSuccess: true, paths: ['folder 1', './folder 2/file']}], + [['file with long name/file.txt "./another folder 2/file"', 2], {parseStatusSuccess: true, paths: ['file with long name/file.txt', './another folder 2/file']}], // strange case, but actually it does not break anything :) ]; const test = () => { From 0a18e9d05e6d6ee21528a54990a9b54c77bbe483 Mon Sep 17 00:00:00 2001 From: Nikita Date: Sun, 4 Feb 2024 17:00:18 +0300 Subject: [PATCH 20/24] docs: readme --- Readme.md | 17 ++++++--- fileManager/FileManager.js | 49 +++++++++++++++++--------- fileManager/services/PathService.js | 4 +-- fileManager/services/StreamsService.js | 2 +- index.js | 3 +- package.json | 1 + 6 files changed, 50 insertions(+), 26 deletions(-) diff --git a/Readme.md b/Readme.md index b10ff02..5471011 100644 --- a/Readme.md +++ b/Readme.md @@ -1,8 +1,17 @@ -## Simple File Manager +## _Simple File Manager_ ___ -### Manual: +### _Manual:_ +To run this console application, use command `npm run start -- --username=YOUR_NAME`. Or just use `npm run start:with-default-user`. +#### _About paths:_ +1. If command requires only one path argument, for example `cd PATH`, `add PATH` etc., then you can optionally wrap path with quotes. +For example, `cd "./files"`, `cd ./files`, `cd ./folder with space/files`, `cd "./folder with space/files"`; +2. If command requires 2+ paths and one of them contains space — then you _must__ wrap this path with quotes. For example: +`cp ./folder1/file1.txt "../folder with space/folder2"`, `cp "../folder with space/folder2" ./folder1` or also wrap all: `cp "../folder with space/folder2" "./folder1"`. +3. If my parser considers you inserting invalid number of paths for command or inserting them in not valid way — it will abort operation and print warning about this. +4. _You can see parser's expected logic in `./FileManager/tests/PathService.test.js`_ + ___ -### Technologies stack: +### _Technologies stack:_ - _Node.js_ - _JavaScript_ @@ -10,4 +19,4 @@ ___ ###### Copyright © 2024 ###### Created by Slutski Mikita -###### Inspired by Rolling Scopes +###### Inspired by Rolling Scopes and RS NodeJS 2024 Q1 diff --git a/fileManager/FileManager.js b/fileManager/FileManager.js index 3a85f9e..e8fb935 100644 --- a/fileManager/FileManager.js +++ b/fileManager/FileManager.js @@ -20,14 +20,14 @@ class FileManager { } async run() { - const read = readline.promises.createInterface({ + const consoleInputReader = readline.promises.createInterface({ input: process.stdin, output: process.stdout }); while (true) { console.info(`Current directory: ${this.#currentDirectory}`); - const input = await read.question(''); + const input = await consoleInputReader.question(''); const splittedInput = input.split(' '); const command = splittedInput.at(0); const restPartOfInput = splittedInput.slice(1).join(' '); @@ -37,6 +37,7 @@ class FileManager { this.#up(); break; } + case 'cd': { const enteredPath = PathService.extractFilePathFromString(restPartOfInput, 1); if (!enteredPath.parseStatusSuccess) { @@ -47,10 +48,12 @@ class FileManager { await this.#cd(enteredPath.paths[0]); break; } + case 'ls': { await this.#ls(); break; } + case 'cat': { const enteredPath = PathService.extractFilePathFromString(restPartOfInput, 1); if (!enteredPath.parseStatusSuccess) { @@ -61,6 +64,7 @@ class FileManager { await this.#cat(enteredPath.paths[0]); break; } + case 'add': { const enteredFilename = PathService.extractFilePathFromString(restPartOfInput, 1); if (!enteredFilename.parseStatusSuccess) { @@ -70,6 +74,7 @@ class FileManager { await this.#add(enteredFilename.paths[0]); break; } + case 'rn': { const enteredFilename = PathService.extractFilePathFromString(restPartOfInput, 2); if (!enteredFilename.parseStatusSuccess) { @@ -79,6 +84,7 @@ class FileManager { await this.#rn(...enteredFilename.paths); break; } + case 'cp': { const enteredFilename = PathService.extractFilePathFromString(restPartOfInput, 2); if (!enteredFilename.parseStatusSuccess) { @@ -88,6 +94,7 @@ class FileManager { await this.#cp(...enteredFilename.paths); break; } + case 'mv': { const enteredFilename = PathService.extractFilePathFromString(restPartOfInput, 2); if (!enteredFilename.parseStatusSuccess) { @@ -97,6 +104,7 @@ class FileManager { await this.#mv(...enteredFilename.paths); break; } + case 'rm': { const enteredFilename = PathService.extractFilePathFromString(restPartOfInput, 1); if (!enteredFilename.parseStatusSuccess) { @@ -106,25 +114,30 @@ class FileManager { await this.#rm(enteredFilename.paths[0]); break; } + case 'os': { const argument = splittedInput.at(1); this.#os(argument); break; } + case 'hash': { const filePath = PathService.extractFilePathFromString(splittedInput.slice(1)); await this.#hashFile(filePath); break; } + case 'compress': { const enteredFilename = PathService.extractFilePathFromString(restPartOfInput, 1); if (!enteredFilename.parseStatusSuccess) { console.warn(FileManager.#invalid2PathsMessage); break; } + await this.#compress(...enteredFilename.paths); break; } + case 'decompress': { const enteredFilename = PathService.extractFilePathFromString(restPartOfInput, 1); if (!enteredFilename.parseStatusSuccess) { @@ -134,14 +147,17 @@ class FileManager { await this.#decompress(...enteredFilename.paths); break; } + case '.exit': { - read.close(); + consoleInputReader.close(); return; } + case '': { console.warn('Empty command'); break; } + default: { console.warn(`Unknown command "${command}"`); break; @@ -182,20 +198,19 @@ class FileManager { const fullPath = PathService.toAbsolute(this.#currentDirectory, entity); const stats = await StatsService.stats(fullPath); - try { - if (stats.isDirectory()) { - return [entity, 'directory']; - } else if (stats.isFile()) { - return [entity, 'file']; - } else if (stats.isSymbolicLink()) { - return [entity, 'symbolic link']; - } - else { - return [entity, 'other']; - } - } catch (error) { - return [entity, 'unknown']; - } + try { + if (stats.isDirectory()) { + return [entity, 'directory']; + } else if (stats.isFile()) { + return [entity, 'file']; + } else if (stats.isSymbolicLink()) { + return [entity, 'symbolic link']; + } else { + return [entity, 'other']; + } + } catch (error) { + return [entity, 'unknown']; + } })); console.table(table); diff --git a/fileManager/services/PathService.js b/fileManager/services/PathService.js index 2c32df6..1908b07 100644 --- a/fileManager/services/PathService.js +++ b/fileManager/services/PathService.js @@ -48,7 +48,7 @@ export class PathService { return invalidPathResponse; } - return { parseStatusSuccess: true, paths}; + return {parseStatusSuccess: true, paths}; } // one or all paths are in quotes, for example: cp "./file.txt" file2.txt @@ -56,7 +56,7 @@ export class PathService { return invalidPathResponse; } - return {parseStatusSuccess: true, paths: splittedByQuotes} + return {parseStatusSuccess: true, paths: splittedByQuotes}; } static #splitPathsByDelimiter(input, delimiter) { diff --git a/fileManager/services/StreamsService.js b/fileManager/services/StreamsService.js index 5384fb0..4d8dec6 100644 --- a/fileManager/services/StreamsService.js +++ b/fileManager/services/StreamsService.js @@ -4,7 +4,7 @@ export class StreamsService { static async readFile(absolutePath) { return await new Promise((resolve, reject) => { let response = ''; - const readStream = fs.createReadStream(absolutePath, { encoding: 'utf-8' }); + const readStream = fs.createReadStream(absolutePath, {encoding: 'utf-8'}); readStream.on('data', chunk => { response += chunk; }); diff --git a/index.js b/index.js index 5c3fc40..d2da6c8 100644 --- a/index.js +++ b/index.js @@ -1,8 +1,7 @@ import FileManager from './fileManager/FileManager.js'; const main = async () => { - // TODO - const username = process.argv[2]; + const username = process.argv[2].split('=').at(1); console.info(`Welcome to the File Manager, ${username}!`); diff --git a/package.json b/package.json index 39d77ce..c128f32 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "main": "index.js", "scripts": { "start": "node ./index.js", + "start:with-default-user": "node ./index.js --username=Mikita", "test:path-service": "node fileManager/tests/PathService.test.js" }, "keywords": [], From 37ae75372efc5aa410dbf6cf3244c78249fa4b85 Mon Sep 17 00:00:00 2001 From: Nikita Date: Sun, 4 Feb 2024 18:24:27 +0300 Subject: [PATCH 21/24] fix: cp, mv; docs: readme --- {fileManager => FileManager}/FileManager.js | 44 +++++++++++------- .../services/CompressService.js | 0 .../services/PathService.js | 4 ++ .../services/StatsService.js | 0 .../services/StreamsService.js | 0 FileManager/tests/PathService.test.js | 34 ++++++++++++++ .../tests/test.data.js | 30 +++--------- Readme.md | 8 ++-- demo/2-args-paths.PNG | Bin 0 -> 48923 bytes demo/single-argument-path-2.PNG | Bin 0 -> 44112 bytes demo/single-argument-path.PNG | Bin 0 -> 53120 bytes index.js | 14 ++++-- utils.js | 16 +++++++ 13 files changed, 102 insertions(+), 48 deletions(-) rename {fileManager => FileManager}/FileManager.js (89%) rename {fileManager => FileManager}/services/CompressService.js (100%) rename {fileManager => FileManager}/services/PathService.js (97%) rename {fileManager => FileManager}/services/StatsService.js (100%) rename {fileManager => FileManager}/services/StreamsService.js (100%) create mode 100644 FileManager/tests/PathService.test.js rename fileManager/tests/PathService.test.js => FileManager/tests/test.data.js (64%) create mode 100644 demo/2-args-paths.PNG create mode 100644 demo/single-argument-path-2.PNG create mode 100644 demo/single-argument-path.PNG create mode 100644 utils.js diff --git a/fileManager/FileManager.js b/FileManager/FileManager.js similarity index 89% rename from fileManager/FileManager.js rename to FileManager/FileManager.js index e8fb935..15bd81a 100644 --- a/fileManager/FileManager.js +++ b/FileManager/FileManager.js @@ -1,13 +1,16 @@ import os from 'os'; import readline from 'readline'; +import path from 'path'; import fs from 'fs'; +import stream from 'stream'; import {StatsService} from './services/StatsService.js'; import {StreamsService} from './services/StreamsService.js'; import {PathService} from './services/PathService.js'; import {CompressService} from './services/CompressService.js'; -class FileManager { + +export class FileManager { static #invalidSinglePathMessage = 'Invalid path argument'; static #unknownAttributeMessage = 'Unknown attribute'; static #invalid2PathsMessage = 'Invalid paths argument. 2 paths must be provided. If file-path contains spaces, it should be wrapped with "quoues"'; @@ -16,7 +19,6 @@ class FileManager { constructor() { this.#currentDirectory = os.homedir(); - console.info(`Current directory: ${this.#currentDirectory}`); } async run() { @@ -108,9 +110,10 @@ class FileManager { case 'rm': { const enteredFilename = PathService.extractFilePathFromString(restPartOfInput, 1); if (!enteredFilename.parseStatusSuccess) { - console.warn(FileManager.#invalid2PathsMessage); + console.warn(FileManager.#invalidSinglePathMessage); break; } + await this.#rm(enteredFilename.paths[0]); break; } @@ -264,23 +267,34 @@ class FileManager { async #cp(source, destination) { const absoluteSource = PathService.toAbsolute(this.#currentDirectory, source); - const absoluteDestination = PathService.toAbsolute(this.#currentDirectory, destination); + const copiedFileName = PathService.getFullFilename(absoluteSource); + const absoluteDestination = path.resolve(PathService.toAbsolute(this.#currentDirectory, destination), copiedFileName); - return await new Promise(resolve => { - fs.cp(absoluteSource, absoluteDestination, {recursive: true}, error => { - if (error) { - console.warn(error.message); - resolve(false); - } + const isSourceAFile = (await StatsService.stats(absoluteSource))?.isFile(); - resolve(true); - }); - }); + if (!isSourceAFile) { + console.warn('cp command works only with file types and existing source-paths'); + return false; + } + + try { + const sourceStream = StreamsService.getReadStream(absoluteSource); + const destinationStream = StreamsService.getWriteStream(absoluteDestination, {flags: 'wx+'}); + + await stream.promises.pipeline(sourceStream, destinationStream); + } catch { + console.warn('Unable to copy. Some error occurred. Maybe some path is invalid or target file already exists in destination folder'); + return false; + } + + return true; } async #rm(path) { + const absolutePath = PathService.toAbsolute(this.#currentDirectory, path); + await new Promise(resolve => { - fs.rm(path, error => { + fs.rm(absolutePath, error => { if (error) { console.warn(error.message); } @@ -358,5 +372,3 @@ class FileManager { await CompressService.decompressWithBrotli(sourceStream, destinationStream); } } - -export default new FileManager(); diff --git a/fileManager/services/CompressService.js b/FileManager/services/CompressService.js similarity index 100% rename from fileManager/services/CompressService.js rename to FileManager/services/CompressService.js diff --git a/fileManager/services/PathService.js b/FileManager/services/PathService.js similarity index 97% rename from fileManager/services/PathService.js rename to FileManager/services/PathService.js index 1908b07..afbb113 100644 --- a/fileManager/services/PathService.js +++ b/FileManager/services/PathService.js @@ -62,4 +62,8 @@ export class PathService { static #splitPathsByDelimiter(input, delimiter) { return input.split(delimiter).filter(path => path.trim() !== '').map(item => item.trim()); } + + static getFullFilename(filepath) { + return path.basename(filepath); + } } diff --git a/fileManager/services/StatsService.js b/FileManager/services/StatsService.js similarity index 100% rename from fileManager/services/StatsService.js rename to FileManager/services/StatsService.js diff --git a/fileManager/services/StreamsService.js b/FileManager/services/StreamsService.js similarity index 100% rename from fileManager/services/StreamsService.js rename to FileManager/services/StreamsService.js diff --git a/FileManager/tests/PathService.test.js b/FileManager/tests/PathService.test.js new file mode 100644 index 0000000..5cca7d2 --- /dev/null +++ b/FileManager/tests/PathService.test.js @@ -0,0 +1,34 @@ +import {PathService} from '../services/PathService.js'; +import {testCasesForFullFileName, testCasesForPathExtracting} from './test.data.js'; + +const testExtractingPathsFromString = () => { + testCasesForPathExtracting.forEach((testCase, index) => { + console.log(`[Paths extracting] Test case #${index}`); + + const input = testCase[0]; + const expectedResult = testCase[1]; + const receivedResult = PathService.extractFilePathFromString(...input); + + console.assert(JSON.stringify(expectedResult) === JSON.stringify(receivedResult)); + }); +}; + + +const testGettingFullFilename = () => { + testCasesForFullFileName.forEach((testCase, index) => { + console.log(`[Getting full filename] Test case #${index}`); + + const input = testCase[0]; + const expectedResult = testCase[1]; + const receivedResult = PathService.getFullFilename(input); + + console.assert(receivedResult === expectedResult); + }); +}; + +const runTests = () => { + testExtractingPathsFromString(); + testGettingFullFilename(); +}; + +runTests(); diff --git a/fileManager/tests/PathService.test.js b/FileManager/tests/test.data.js similarity index 64% rename from fileManager/tests/PathService.test.js rename to FileManager/tests/test.data.js index 5765323..93aa9a9 100644 --- a/fileManager/tests/PathService.test.js +++ b/FileManager/tests/test.data.js @@ -1,13 +1,4 @@ -import {PathService} from '../services/PathService.js'; - -/** - PathService::extractFilePathFromString() receives a string and expected number of paths inside it - If path contains spaces, it must be wrapped with quotes, for example: cp "./path-to-file/file name with space.txt". - But if we are expecting only one file in function, for example cat new file.txt -- then we can not use quotes. - So quotes in required as wrapper for path/filename only if >= 2 entities are expected as arguments. - So acceptable combinations for entering paths: cp "folder 1" "folder 2", cp folder-no-space "folder 2", cp "folder space" folder-no-space, cp folder-no-space1 no-space2 - */ -const testCases = [ +export const testCasesForPathExtracting = [ [['"path-in-quotes" path-without-quotes', 2], {parseStatusSuccess: true, paths: ['path-in-quotes', 'path-without-quotes']}], [['"./long path with space/path-in-quotes.txt" path-without-quotes', 2], {parseStatusSuccess: true, paths: ['./long path with space/path-in-quotes.txt', 'path-without-quotes']}], [['"./long path with space/path-in-quotes.txt" path-without-quotes', 1], {parseStatusSuccess: false, paths: []}], @@ -23,17 +14,8 @@ const testCases = [ [['file with long name/file.txt "./another folder 2/file"', 2], {parseStatusSuccess: true, paths: ['file with long name/file.txt', './another folder 2/file']}], // strange case, but actually it does not break anything :) ]; -const test = () => { - testCases.forEach((testCase, index) => { - console.log(`Test case #${index}`); - - const input = testCase[0]; - const expectedResult = testCase[1]; - const receivedResult = PathService.extractFilePathFromString(...input); - - console.assert(JSON.stringify(expectedResult) === JSON.stringify(receivedResult)); - }); -}; - - -test(); +export const testCasesForFullFileName = [ + ['../folder 1/filename.txt', 'filename.txt'], + ['filename.txt', 'filename.txt'], + ['./filename.bat.txt', 'filename.bat.txt'], +]; diff --git a/Readme.md b/Readme.md index 5471011..cb8eef0 100644 --- a/Readme.md +++ b/Readme.md @@ -3,11 +3,11 @@ ___ ### _Manual:_ To run this console application, use command `npm run start -- --username=YOUR_NAME`. Or just use `npm run start:with-default-user`. #### _About paths:_ -1. If command requires only one path argument, for example `cd PATH`, `add PATH` etc., then you can optionally wrap path with quotes. -For example, `cd "./files"`, `cd ./files`, `cd ./folder with space/files`, `cd "./folder with space/files"`; +1. If command requires only one path argument, for example `cd PATH`, `add PATH` etc., then you can __optionally wrap__ path with quotes. But not necessary. +For example, `cd "./files"`, `cd ./files`, `cd ./folder with space/files`, `cd "./folder with space/files"`; ![Demo](./demo/single-argument-path.PNG) ![Demo](./demo/single-argument-path-2.PNG) 2. If command requires 2+ paths and one of them contains space — then you _must__ wrap this path with quotes. For example: -`cp ./folder1/file1.txt "../folder with space/folder2"`, `cp "../folder with space/folder2" ./folder1` or also wrap all: `cp "../folder with space/folder2" "./folder1"`. -3. If my parser considers you inserting invalid number of paths for command or inserting them in not valid way — it will abort operation and print warning about this. +`cp ./folder1/file1.txt "../folder with space/folder2"`, `cp "../folder with space/folder2" ./folder1` or also wrap all: `cp "../folder with space/folder2" "./folder1"`; ![Demo](./demo/2-args-paths.PNG) __NOTE: cd REQUIRES ONLY 1 PATH, THAT'S WHY IT CAN BE WRITTEN BOTH WITH WRAPPING QUOTES AND WITHOUT THEM__ +3. If my parser considers you inserting invalid number of paths for command or inserting them in not valid way — it will abort operation and print warning about this; 4. _You can see parser's expected logic in `./FileManager/tests/PathService.test.js`_ ___ diff --git a/demo/2-args-paths.PNG b/demo/2-args-paths.PNG new file mode 100644 index 0000000000000000000000000000000000000000..d1437604e70bf8caa21cd4d566aaf26393c8fa4e GIT binary patch literal 48923 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 EA5qk`h5!Hn literal 0 HcmV?d00001 diff --git a/demo/single-argument-path-2.PNG b/demo/single-argument-path-2.PNG new file mode 100644 index 0000000000000000000000000000000000000000..9f8cba22822a50bbff7089c3606d331f9665758a GIT binary patch literal 44112 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 G-u^F!!s~qi literal 0 HcmV?d00001 diff --git a/demo/single-argument-path.PNG b/demo/single-argument-path.PNG new file mode 100644 index 0000000000000000000000000000000000000000..8b7ba5baa85f1798b1d522b5b4efb77ed8e29bfa GIT binary patch literal 53120 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 DQps22 literal 0 HcmV?d00001 diff --git a/index.js b/index.js index d2da6c8..a548e16 100644 --- a/index.js +++ b/index.js @@ -1,15 +1,21 @@ -import FileManager from './fileManager/FileManager.js'; +import {FileManager} from './fileManager/FileManager.js'; +import {getUsernameFromArgs} from './utils.js'; const main = async () => { - const username = process.argv[2].split('=').at(1); + const username = getUsernameFromArgs(process.argv); - console.info(`Welcome to the File Manager, ${username}!`); + if (username === undefined) { + console.warn('Username argument not provided'); + } else { + console.info(`Welcome to the File Manager, ${username}!`); + } process.addListener('exit', () => { console.info(`Thank you for using File Manager, ${username}, goodbye!`); }); - await FileManager.run(); + const fileManager = new FileManager(); + await fileManager.run(); }; diff --git a/utils.js b/utils.js new file mode 100644 index 0000000..dbcfb49 --- /dev/null +++ b/utils.js @@ -0,0 +1,16 @@ +/** + Looks for --username={name} pattern in args +*/ +export const getUsernameFromArgs = processArgv => { + const usernameArgKey = '--username'; + + for (let index = 2; index < processArgv.length; ++index) { + const splitted = processArgv.at(index).split('='); + + if (splitted.at(0) === usernameArgKey) { + return splitted.at(1); + } + } + + return undefined; +}; From 910df647e2b6d619164b47e474add234b1e56353 Mon Sep 17 00:00:00 2001 From: Nikita Date: Sun, 4 Feb 2024 18:30:42 +0300 Subject: [PATCH 22/24] fix: hash --- FileManager/FileManager.js | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/FileManager/FileManager.js b/FileManager/FileManager.js index 15bd81a..5a95b52 100644 --- a/FileManager/FileManager.js +++ b/FileManager/FileManager.js @@ -125,8 +125,13 @@ export class FileManager { } case 'hash': { - const filePath = PathService.extractFilePathFromString(splittedInput.slice(1)); - await this.#hashFile(filePath); + const enteredFilename = PathService.extractFilePathFromString(restPartOfInput, 1); + if (!enteredFilename.parseStatusSuccess) { + console.warn(FileManager.#invalidSinglePathMessage); + break; + } + + await this.#hashFile(enteredFilename.paths.at(0)); break; } @@ -338,6 +343,10 @@ export class FileManager { console.info(os.arch()); break; } + case undefined: { + console.info('os command requires an attribute'); + break; + } default: { console.info(FileManager.#unknownAttributeMessage); break; @@ -347,9 +356,14 @@ export class FileManager { async #hashFile(filePath) { const absolutePath = PathService.toAbsolute(this.#currentDirectory, filePath); - const fileData = await StreamsService.readFile(absolutePath); - const hashedValue = CompressService.hash(fileData); - console.info(hashedValue); + + try { + const fileData = await StreamsService.readFile(absolutePath); + const hashedValue = CompressService.hash(fileData); + console.info(hashedValue); + } catch { + console.warn('Provided path is not a file or not valid or unable to hash such contents'); + } } async #compress(source, destination) { From 41273f07f546809e83a60ae804c7b89414be2158 Mon Sep 17 00:00:00 2001 From: Nikita Date: Sun, 4 Feb 2024 19:26:25 +0300 Subject: [PATCH 23/24] fix: compression --- FileManager/FileManager.js | 132 +++++++++++++++++------- FileManager/services/CompressService.js | 30 ++++-- FileManager/services/StreamsService.js | 16 ++- 3 files changed, 128 insertions(+), 50 deletions(-) diff --git a/FileManager/FileManager.js b/FileManager/FileManager.js index 5a95b52..1d7a425 100644 --- a/FileManager/FileManager.js +++ b/FileManager/FileManager.js @@ -9,7 +9,6 @@ import {PathService} from './services/PathService.js'; import {CompressService} from './services/CompressService.js'; - export class FileManager { static #invalidSinglePathMessage = 'Invalid path argument'; static #unknownAttributeMessage = 'Unknown attribute'; @@ -41,13 +40,13 @@ export class FileManager { } case 'cd': { - const enteredPath = PathService.extractFilePathFromString(restPartOfInput, 1); - if (!enteredPath.parseStatusSuccess) { + const parsed = PathService.extractFilePathFromString(restPartOfInput, 1); + if (!parsed.parseStatusSuccess) { console.warn(FileManager.#invalidSinglePathMessage); break; } - await this.#cd(enteredPath.paths[0]); + await this.#cd(parsed.paths[0]); break; } @@ -57,64 +56,64 @@ export class FileManager { } case 'cat': { - const enteredPath = PathService.extractFilePathFromString(restPartOfInput, 1); - if (!enteredPath.parseStatusSuccess) { + const parsed = PathService.extractFilePathFromString(restPartOfInput, 1); + if (!parsed.parseStatusSuccess) { console.warn(FileManager.#invalidSinglePathMessage); break; } - await this.#cat(enteredPath.paths[0]); + await this.#cat(parsed.paths[0]); break; } case 'add': { - const enteredFilename = PathService.extractFilePathFromString(restPartOfInput, 1); - if (!enteredFilename.parseStatusSuccess) { + const parsed = PathService.extractFilePathFromString(restPartOfInput, 1); + if (!parsed.parseStatusSuccess) { console.warn(FileManager.#invalidSinglePathMessage); break; } - await this.#add(enteredFilename.paths[0]); + await this.#add(parsed.paths[0]); break; } case 'rn': { - const enteredFilename = PathService.extractFilePathFromString(restPartOfInput, 2); - if (!enteredFilename.parseStatusSuccess) { + const parsed = PathService.extractFilePathFromString(restPartOfInput, 2); + if (!parsed.parseStatusSuccess) { console.warn(FileManager.#invalid2PathsMessage); break; } - await this.#rn(...enteredFilename.paths); + await this.#rn(...parsed.paths); break; } case 'cp': { - const enteredFilename = PathService.extractFilePathFromString(restPartOfInput, 2); - if (!enteredFilename.parseStatusSuccess) { + const parsed = PathService.extractFilePathFromString(restPartOfInput, 2); + if (!parsed.parseStatusSuccess) { console.warn(FileManager.#invalid2PathsMessage); break; } - await this.#cp(...enteredFilename.paths); + await this.#cp(...parsed.paths); break; } case 'mv': { - const enteredFilename = PathService.extractFilePathFromString(restPartOfInput, 2); - if (!enteredFilename.parseStatusSuccess) { + const parsed = PathService.extractFilePathFromString(restPartOfInput, 2); + if (!parsed.parseStatusSuccess) { console.warn(FileManager.#invalid2PathsMessage); break; } - await this.#mv(...enteredFilename.paths); + await this.#mv(...parsed.paths); break; } case 'rm': { - const enteredFilename = PathService.extractFilePathFromString(restPartOfInput, 1); - if (!enteredFilename.parseStatusSuccess) { + const parsed = PathService.extractFilePathFromString(restPartOfInput, 1); + if (!parsed.parseStatusSuccess) { console.warn(FileManager.#invalidSinglePathMessage); break; } - await this.#rm(enteredFilename.paths[0]); + await this.#rm(parsed.paths[0]); break; } @@ -125,29 +124,29 @@ export class FileManager { } case 'hash': { - const enteredFilename = PathService.extractFilePathFromString(restPartOfInput, 1); - if (!enteredFilename.parseStatusSuccess) { + const parsed = PathService.extractFilePathFromString(restPartOfInput, 1); + if (!parsed.parseStatusSuccess) { console.warn(FileManager.#invalidSinglePathMessage); break; } - await this.#hashFile(enteredFilename.paths.at(0)); + await this.#hashFile(parsed.paths.at(0)); break; } case 'compress': { - const enteredFilename = PathService.extractFilePathFromString(restPartOfInput, 1); - if (!enteredFilename.parseStatusSuccess) { + const parsed = PathService.extractFilePathFromString(restPartOfInput, 2); + if (!parsed.parseStatusSuccess) { console.warn(FileManager.#invalid2PathsMessage); break; } - await this.#compress(...enteredFilename.paths); + await this.#compress(...parsed.paths); break; } case 'decompress': { - const enteredFilename = PathService.extractFilePathFromString(restPartOfInput, 1); + const enteredFilename = PathService.extractFilePathFromString(restPartOfInput, 2); if (!enteredFilename.parseStatusSuccess) { console.warn(FileManager.#invalid2PathsMessage); break; @@ -368,21 +367,80 @@ export class FileManager { async #compress(source, destination) { const absoluteSource = PathService.toAbsolute(this.#currentDirectory, source); - const absoluteDestination = PathService.toAbsolute(this.#currentDirectory, destination); + const absoluteDestinationFolder = PathService.toAbsolute(this.#currentDirectory, destination); + + const destinationFolderExists = await new Promise(resolve => fs.exists(absoluteDestinationFolder, resolve)); + + if (!destinationFolderExists) { + console.warn('Destination folder does not exist: ' + absoluteDestinationFolder); + return; + } + + const absoluteDestination = path.resolve( + absoluteDestinationFolder, + PathService.getFullFilename(source) + CompressService.compressExtension + ); + + const destinationExists = await new Promise(resolve => fs.exists(absoluteDestination, resolve)); + + if (destinationExists) { + console.warn('Destination file already exists: ' + absoluteDestination); + return; + } + + try { + await fs.promises.access(absoluteSource); - const sourceStream = StreamsService.getReadStream(absoluteSource); - const destinationStream = StreamsService.getWriteStream(absoluteDestination); + const sourceStream = StreamsService.getReadStream(absoluteSource); + const destinationStream = StreamsService.getWriteStream(absoluteDestination, {flags: 'wx+'}); - await CompressService.compressWithBrotli(sourceStream, destinationStream); + await CompressService.compressWithBrotli(sourceStream, destinationStream); + } catch (error) { + console.warn('Unable to compress. Paths may be incorrect, or destination folder already contains compressed file with same name'); + console.warn(error.message); + } } async #decompress(source, destination) { const absoluteSource = PathService.toAbsolute(this.#currentDirectory, source); - const absoluteDestination = PathService.toAbsolute(this.#currentDirectory, destination); + const absoluteDestinationFolder = PathService.toAbsolute(this.#currentDirectory, destination); + + const destinationFolderExists = await new Promise(resolve => fs.exists(absoluteDestinationFolder, resolve)); + + if (!destinationFolderExists) { + console.warn('Destination folder does not exist: ' + absoluteDestinationFolder); + return; + } + + let absoluteDestination = path.resolve( + absoluteDestinationFolder, + PathService.getFullFilename(source) + ); + + if (absoluteDestination.endsWith(CompressService.compressExtension)) { + absoluteDestination = absoluteDestination.slice(0, -1 * CompressService.compressExtension.length); + } else { + console.warn(`Can not decompress files without ${CompressService.compressExtension} extension`); + return; + } + + const destinationExists = await new Promise(resolve => fs.exists(absoluteDestination, resolve)); - const sourceStream = StreamsService.getReadStream(absoluteSource); - const destinationStream = StreamsService.getWriteStream(absoluteDestination); + if (destinationExists) { + console.warn('Destination file already exists: ' + absoluteDestination); + return; + } - await CompressService.decompressWithBrotli(sourceStream, destinationStream); + try { + await fs.promises.access(absoluteSource); + + const sourceStream = StreamsService.getReadStream(absoluteSource); + const destinationStream = StreamsService.getWriteStream(absoluteDestination, {flags: 'wx+'}); + + await CompressService.decompressWithBrotli(sourceStream, destinationStream); + } catch (error) { + console.warn('Unable to decompress. Paths may be incorrect, or destination folder already contains compressed file with same name'); + console.warn(error.message); + } } } diff --git a/FileManager/services/CompressService.js b/FileManager/services/CompressService.js index 08fb9e7..26e71f5 100644 --- a/FileManager/services/CompressService.js +++ b/FileManager/services/CompressService.js @@ -3,6 +3,8 @@ import zlib from 'zlib'; export class CompressService { + static compressExtension = '.br'; + static hash(data) { return crypto.createHash('md5').update(data).digest('hex'); } @@ -10,22 +12,28 @@ export class CompressService { static async compressWithBrotli(sourceStream, destinationStream) { const brotli = zlib.createBrotliCompress(); - await new Promise(resolve => { - const stream = sourceStream.pipe(brotli).pipe(destinationStream); - stream.on('finish', () => { - resolve(); + try { + await new Promise(resolve => { + const stream = sourceStream.pipe(brotli).pipe(destinationStream); + stream.on('finish', resolve); }); - }); + } catch (error) { + throw error; + } } static async decompressWithBrotli(sourceStream, destinationStream) { const brotli = zlib.createBrotliDecompress(); - await new Promise(resolve => { - const stream = sourceStream.pipe(brotli).pipe(destinationStream); - stream.on('finish', () => { - resolve(); - }); - }); + try { + await new Promise(resolve => { + const stream = sourceStream.pipe(brotli).pipe(destinationStream); + stream.on('finish', () => { + resolve(); + }); + }); + } catch (error) { + throw error; + } } } diff --git a/FileManager/services/StreamsService.js b/FileManager/services/StreamsService.js index 4d8dec6..6a0a955 100644 --- a/FileManager/services/StreamsService.js +++ b/FileManager/services/StreamsService.js @@ -18,10 +18,22 @@ export class StreamsService { } static getReadStream(source, options = {}) { - return fs.createReadStream(source, options); + try { + return fs.createReadStream(source, options).on('error', error => { + throw error; + }); + } catch (error) { + throw error; + } } static getWriteStream(destination, options = {}) { - return fs.createWriteStream(destination, options); + try { + return fs.createWriteStream(destination, options).on('error', error => { + throw error; + }); + } catch (error) { + throw error; + } } } From d32fb9f3bac10cd624ae0a06a6155dd0f0196db3 Mon Sep 17 00:00:00 2001 From: Nikita Date: Mon, 5 Feb 2024 17:38:18 +0300 Subject: [PATCH 24/24] fix: add output of cpu speed in ghz --- FileManager/FileManager.js | 5 ++++- FileManager/services/Converter.js | 5 +++++ 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 FileManager/services/Converter.js diff --git a/FileManager/FileManager.js b/FileManager/FileManager.js index 1d7a425..25dada2 100644 --- a/FileManager/FileManager.js +++ b/FileManager/FileManager.js @@ -7,6 +7,7 @@ import {StatsService} from './services/StatsService.js'; import {StreamsService} from './services/StreamsService.js'; import {PathService} from './services/PathService.js'; import {CompressService} from './services/CompressService.js'; +import {Converter} from './services/Converter.js'; export class FileManager { @@ -325,7 +326,9 @@ export class FileManager { break; } case '--cpus': { - const table = os.cpus().map(cpu => [cpu.model, cpu.speed]); + const table = os.cpus().map(cpu => { + return [cpu.model, `${Converter.MHzToGHz(cpu.speed)} GHz`]; + }); console.info(`CPUs count: ${table.length}`); console.table(table); break; diff --git a/FileManager/services/Converter.js b/FileManager/services/Converter.js new file mode 100644 index 0000000..0c6a245 --- /dev/null +++ b/FileManager/services/Converter.js @@ -0,0 +1,5 @@ +export class Converter { + static MHzToGHz(mhz) { + return mhz / 1000; + } +}