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
diff --git a/FileManager/FileManager.js b/FileManager/FileManager.js
new file mode 100644
index 0000000..25dada2
--- /dev/null
+++ b/FileManager/FileManager.js
@@ -0,0 +1,449 @@
+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';
+import {Converter} from './services/Converter.js';
+
+
+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"';
+
+ #currentDirectory;
+
+ constructor() {
+ this.#currentDirectory = os.homedir();
+ }
+
+ async run() {
+ const consoleInputReader = readline.promises.createInterface({
+ input: process.stdin, output: process.stdout
+ });
+
+ while (true) {
+ console.info(`Current directory: ${this.#currentDirectory}`);
+
+ const input = await consoleInputReader.question('');
+ const splittedInput = input.split(' ');
+ const command = splittedInput.at(0);
+ const restPartOfInput = splittedInput.slice(1).join(' ');
+
+ switch (command) {
+ case 'up': {
+ this.#up();
+ break;
+ }
+
+ case 'cd': {
+ const parsed = PathService.extractFilePathFromString(restPartOfInput, 1);
+ if (!parsed.parseStatusSuccess) {
+ console.warn(FileManager.#invalidSinglePathMessage);
+ break;
+ }
+
+ await this.#cd(parsed.paths[0]);
+ break;
+ }
+
+ case 'ls': {
+ await this.#ls();
+ break;
+ }
+
+ case 'cat': {
+ const parsed = PathService.extractFilePathFromString(restPartOfInput, 1);
+ if (!parsed.parseStatusSuccess) {
+ console.warn(FileManager.#invalidSinglePathMessage);
+ break;
+ }
+
+ await this.#cat(parsed.paths[0]);
+ break;
+ }
+
+ case 'add': {
+ const parsed = PathService.extractFilePathFromString(restPartOfInput, 1);
+ if (!parsed.parseStatusSuccess) {
+ console.warn(FileManager.#invalidSinglePathMessage);
+ break;
+ }
+ await this.#add(parsed.paths[0]);
+ break;
+ }
+
+ case 'rn': {
+ const parsed = PathService.extractFilePathFromString(restPartOfInput, 2);
+ if (!parsed.parseStatusSuccess) {
+ console.warn(FileManager.#invalid2PathsMessage);
+ break;
+ }
+ await this.#rn(...parsed.paths);
+ break;
+ }
+
+ case 'cp': {
+ const parsed = PathService.extractFilePathFromString(restPartOfInput, 2);
+ if (!parsed.parseStatusSuccess) {
+ console.warn(FileManager.#invalid2PathsMessage);
+ break;
+ }
+ await this.#cp(...parsed.paths);
+ break;
+ }
+
+ case 'mv': {
+ const parsed = PathService.extractFilePathFromString(restPartOfInput, 2);
+ if (!parsed.parseStatusSuccess) {
+ console.warn(FileManager.#invalid2PathsMessage);
+ break;
+ }
+ await this.#mv(...parsed.paths);
+ break;
+ }
+
+ case 'rm': {
+ const parsed = PathService.extractFilePathFromString(restPartOfInput, 1);
+ if (!parsed.parseStatusSuccess) {
+ console.warn(FileManager.#invalidSinglePathMessage);
+ break;
+ }
+
+ await this.#rm(parsed.paths[0]);
+ break;
+ }
+
+ case 'os': {
+ const argument = splittedInput.at(1);
+ this.#os(argument);
+ break;
+ }
+
+ case 'hash': {
+ const parsed = PathService.extractFilePathFromString(restPartOfInput, 1);
+ if (!parsed.parseStatusSuccess) {
+ console.warn(FileManager.#invalidSinglePathMessage);
+ break;
+ }
+
+ await this.#hashFile(parsed.paths.at(0));
+ break;
+ }
+
+ case 'compress': {
+ const parsed = PathService.extractFilePathFromString(restPartOfInput, 2);
+ if (!parsed.parseStatusSuccess) {
+ console.warn(FileManager.#invalid2PathsMessage);
+ break;
+ }
+
+ await this.#compress(...parsed.paths);
+ break;
+ }
+
+ case 'decompress': {
+ const enteredFilename = PathService.extractFilePathFromString(restPartOfInput, 2);
+ if (!enteredFilename.parseStatusSuccess) {
+ console.warn(FileManager.#invalid2PathsMessage);
+ break;
+ }
+ await this.#decompress(...enteredFilename.paths);
+ break;
+ }
+
+ case '.exit': {
+ consoleInputReader.close();
+ return;
+ }
+
+ case '': {
+ console.warn('Empty command');
+ break;
+ }
+
+ default: {
+ console.warn(`Unknown command "${command}"`);
+ break;
+ }
+ }
+ }
+ }
+
+ #up() {
+ this.#currentDirectory = PathService.toAbsolute(this.#currentDirectory, '../');
+ }
+
+ 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.warn('Can\'t move into non-directory');
+ return;
+ }
+
+ this.#currentDirectory = absolutePath;
+ }
+
+ async #ls() {
+ const list = await new Promise(resolve => {
+ fs.readdir(this.#currentDirectory, (error, files) => resolve(files));
+ });
+
+ const table = await Promise.all(list.map(async entity => {
+ 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'];
+ }
+ }));
+
+ console.table(table);
+ }
+
+ async #cat(filePath) {
+ const absolutePath = PathService.toAbsolute(this.#currentDirectory, filePath);
+ try {
+ const data = await StreamsService.readFile(absolutePath);
+ console.info(data);
+ } catch {
+ console.warn('Unable to read contents by passed path');
+ }
+ }
+
+ async #add(filename) {
+ const absolutePath = PathService.toAbsolute(this.#currentDirectory, filename);
+
+ const doesAlreadyExist = await StatsService.doesPathExist(absolutePath);
+ 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.info(err.message);
+ resolve();
+ }
+
+ fs.close(descriptor, () => {
+ resolve();
+ });
+ });
+ });
+ }
+
+ async #rn(source, destination) {
+ const [sourceAbsolute, destinationAbsolute] = [PathService.toAbsolute(this.#currentDirectory, source), PathService.toAbsolute(this.#currentDirectory, destination)];
+
+ await new Promise(resolve => {
+ fs.rename(sourceAbsolute, destinationAbsolute, error => {
+ if (error) {
+ console.log(error.message);
+ }
+ resolve();
+ });
+ });
+ }
+
+ async #cp(source, destination) {
+ const absoluteSource = PathService.toAbsolute(this.#currentDirectory, source);
+ const copiedFileName = PathService.getFullFilename(absoluteSource);
+ const absoluteDestination = path.resolve(PathService.toAbsolute(this.#currentDirectory, destination), copiedFileName);
+
+ const isSourceAFile = (await StatsService.stats(absoluteSource))?.isFile();
+
+ 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(absolutePath, error => {
+ if (error) {
+ console.warn(error.message);
+ }
+
+ 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');
+ }
+ }
+
+ #os(argument) {
+ switch (argument) {
+ case '--EOL': {
+ console.info(os.EOL);
+ break;
+ }
+ case '--cpus': {
+ const table = os.cpus().map(cpu => {
+ return [cpu.model, `${Converter.MHzToGHz(cpu.speed)} GHz`];
+ });
+ 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;
+ }
+ case undefined: {
+ console.info('os command requires an attribute');
+ break;
+ }
+ default: {
+ console.info(FileManager.#unknownAttributeMessage);
+ break;
+ }
+ }
+ }
+
+ async #hashFile(filePath) {
+ const absolutePath = PathService.toAbsolute(this.#currentDirectory, filePath);
+
+ 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) {
+ const absoluteSource = PathService.toAbsolute(this.#currentDirectory, source);
+ 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, {flags: 'wx+'});
+
+ 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 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));
+
+ 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, {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
new file mode 100644
index 0000000..26e71f5
--- /dev/null
+++ b/FileManager/services/CompressService.js
@@ -0,0 +1,39 @@
+import crypto from 'crypto';
+import zlib from 'zlib';
+
+
+export class CompressService {
+ static compressExtension = '.br';
+
+ static hash(data) {
+ return crypto.createHash('md5').update(data).digest('hex');
+ }
+
+ static async compressWithBrotli(sourceStream, destinationStream) {
+ const brotli = zlib.createBrotliCompress();
+
+ 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();
+
+ 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/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;
+ }
+}
diff --git a/FileManager/services/PathService.js b/FileManager/services/PathService.js
new file mode 100644
index 0000000..afbb113
--- /dev/null
+++ b/FileManager/services/PathService.js
@@ -0,0 +1,69 @@
+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);
+ }
+
+ /**
+ 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 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 invalidPathResponse;
+ }
+
+ 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 invalidPathResponse;
+ }
+
+ return {parseStatusSuccess: true, paths};
+ }
+
+ // one or all paths are in quotes, for example: cp "./file.txt" file2.txt
+ if (splittedByQuotes.length !== expectedPathsCount) {
+ return invalidPathResponse;
+ }
+
+ return {parseStatusSuccess: true, paths: splittedByQuotes};
+ }
+
+ 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
new file mode 100644
index 0000000..836d37c
--- /dev/null
+++ b/FileManager/services/StatsService.js
@@ -0,0 +1,17 @@
+import fs from 'fs';
+
+export class StatsService {
+ static async stats(absolutePath) {
+ return await new Promise(resolve => {
+ fs.lstat(absolutePath, (error, stats) => {
+ resolve(stats);
+ });
+ });
+ }
+
+ static async doesPathExist(absolutePath) {
+ return await new Promise(resolve => {
+ fs.exists(absolutePath, resolve);
+ });
+ }
+}
diff --git a/FileManager/services/StreamsService.js b/FileManager/services/StreamsService.js
new file mode 100644
index 0000000..6a0a955
--- /dev/null
+++ b/FileManager/services/StreamsService.js
@@ -0,0 +1,39 @@
+import fs from 'fs';
+
+export class StreamsService {
+ static async readFile(absolutePath) {
+ 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);
+ });
+ });
+ }
+
+ static getReadStream(source, options = {}) {
+ try {
+ return fs.createReadStream(source, options).on('error', error => {
+ throw error;
+ });
+ } catch (error) {
+ throw error;
+ }
+ }
+
+ static getWriteStream(destination, options = {}) {
+ try {
+ return fs.createWriteStream(destination, options).on('error', error => {
+ throw error;
+ });
+ } catch (error) {
+ throw error;
+ }
+ }
+}
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/test.data.js b/FileManager/tests/test.data.js
new file mode 100644
index 0000000..93aa9a9
--- /dev/null
+++ b/FileManager/tests/test.data.js
@@ -0,0 +1,21 @@
+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: []}],
+ [[' "./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']}],
+ [['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 :)
+];
+
+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
new file mode 100644
index 0000000..cb8eef0
--- /dev/null
+++ b/Readme.md
@@ -0,0 +1,22 @@
+## _Simple File Manager_
+___
+### _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. But not necessary.
+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"`;  __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`_
+
+___
+### _Technologies stack:_
+- _Node.js_
+- _JavaScript_
+
+___
+
+###### Copyright © 2024
+###### Created by Slutski Mikita
+###### Inspired by Rolling Scopes and RS NodeJS 2024 Q1
diff --git a/demo/2-args-paths.PNG b/demo/2-args-paths.PNG
new file mode 100644
index 0000000..d143760
Binary files /dev/null and b/demo/2-args-paths.PNG differ
diff --git a/demo/single-argument-path-2.PNG b/demo/single-argument-path-2.PNG
new file mode 100644
index 0000000..9f8cba2
Binary files /dev/null and b/demo/single-argument-path-2.PNG differ
diff --git a/demo/single-argument-path.PNG b/demo/single-argument-path.PNG
new file mode 100644
index 0000000..8b7ba5b
Binary files /dev/null and b/demo/single-argument-path.PNG differ
diff --git a/index.js b/index.js
index 8a5f682..a548e16 100644
--- a/index.js
+++ b/index.js
@@ -1,9 +1,22 @@
+import {FileManager} from './fileManager/FileManager.js';
+import {getUsernameFromArgs} from './utils.js';
+
const main = async () => {
- const username = process.argv[3];
- console.log(`Welcome to the File Manager, ${username}!`);
+ const username = getUsernameFromArgs(process.argv);
-};
+ 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!`);
+ });
+
+ const fileManager = new FileManager();
+ await fileManager.run();
+};
-main();
+await main();
diff --git a/package.json b/package.json
index 6992092..c128f32 100644
--- a/package.json
+++ b/package.json
@@ -1,13 +1,18 @@
{
"name": "file-manager",
- "version": "1.0.0",
- "description": "File manager for Rolling Scopes",
+ "version": "0.1.0",
+ "description": "File manager for Rolling Scopes Node.js 2024Q1",
"type": "module",
"main": "index.js",
"scripts": {
- "start": "node ./index.js"
+ "start": "node ./index.js",
+ "start:with-default-user": "node ./index.js --username=Mikita",
+ "test:path-service": "node fileManager/tests/PathService.test.js"
},
"keywords": [],
- "author": "Slutski Mikita (@user-of-github)",
+ "author": {
+ "name": "Slutski Mikita",
+ "url": "https://github.com/user-of-github"
+ },
"license": "ISC"
}
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;
+};