Skip to content

File Manager #1

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 27 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
24fabd9
feat: add package.json
izy-code Oct 13, 2024
ff7a8de
feat: add colored output utils
izy-code Oct 13, 2024
71d87ce
feat: add username related utils
izy-code Oct 13, 2024
bc6c8ff
feat: add readline interface
izy-code Oct 13, 2024
23c61f8
refactor: change colors keys names
izy-code Oct 13, 2024
69381a6
feat: add directory path utils
izy-code Oct 13, 2024
293e6a5
feat: add command input utils
izy-code Oct 13, 2024
cebe1dd
feat: add input handler
izy-code Oct 13, 2024
9b127c0
feat: add readline prompt and new line handler
izy-code Oct 13, 2024
7adc7cc
feat: add up command
izy-code Oct 13, 2024
b459609
fix: del unneeded imports
izy-code Oct 13, 2024
5fdc637
fix: change array index in getArgByNumber
izy-code Oct 13, 2024
a93c2fc
feat: add cd command
izy-code Oct 13, 2024
4339091
feat: add ls command
izy-code Oct 13, 2024
3fe13c6
feat: add cat command
izy-code Oct 14, 2024
e62d894
fix: use events instead of pipeline in cat
izy-code Oct 14, 2024
f27a202
feat: implement add command
izy-code Oct 14, 2024
37080ec
feat: add rn command
izy-code Oct 14, 2024
d307094
feat: add cp command
izy-code Oct 14, 2024
79613cb
feat: add rm command
izy-code Oct 14, 2024
a22e689
feat: add mv command
izy-code Oct 14, 2024
8cc57ca
feat: add os command
izy-code Oct 14, 2024
4b81f11
feat: add hash command
izy-code Oct 14, 2024
74bce0c
feat: add archive commands
izy-code Oct 14, 2024
04e9703
fix: use only double quotes as path with spaces delimiter
izy-code Oct 14, 2024
44c6a94
refactor: add readlineClose function
izy-code Oct 14, 2024
ce595c3
fix: add source path condition to cp and brotli
izy-code Oct 14, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "node-file-manager",
"version": "1.0.0",
"description": "RS School Node.js course task 2: File manager",
"main": "src/index.js",
"type": "module",
"scripts": {
"start": "node src/index.js"
},
"author": "Izy",
"license": "ISC"
}
25 changes: 25 additions & 0 deletions src/commands/archive/brotli.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@

import { resolve } from 'path';
import { createReadStream, createWriteStream } from 'fs';
import { pipeline } from 'stream/promises';
import { createBrotliCompress, createBrotliDecompress } from 'zlib';
import { getArgByNumber, isPathAccessible } from "../../utils/command-input.js";
import { getCurrentDir } from '../../utils/directory-path.js';

export const useBrotli = async (args, isCompress = true) => {
const sourceArgPath = getArgByNumber(args, 0);
const destArgPath = getArgByNumber(args, 1);

const resolvedSourcePath = resolve(getCurrentDir(), sourceArgPath);
const resolvedDestDirPath = resolve(getCurrentDir(), destArgPath);

if (!(await isPathAccessible(resolvedSourcePath))) {
throw new Error("Source path is not accessible.");
}

const readStream = createReadStream(resolvedSourcePath);
const brotliStream = isCompress ? createBrotliCompress() : createBrotliDecompress();
const writeStream = createWriteStream(resolvedDestDirPath);

await pipeline(readStream, brotliStream, writeStream);
};
12 changes: 12 additions & 0 deletions src/commands/file-system/add.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { basename, resolve } from 'path';
import { writeFile } from 'fs/promises';
import { getCurrentDir } from '../../utils/directory-path.js';
import { getArgByNumber } from "../../utils/command-input.js";

export const add = async (args) => {
const argPath = getArgByNumber(args, 0);
const fileBasename = basename(argPath);
const filePath = resolve(getCurrentDir(), fileBasename);

await writeFile(filePath, '', { flag: 'wx' });
};
18 changes: 18 additions & 0 deletions src/commands/file-system/cat.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { resolve } from 'path';
import { createReadStream } from 'fs';
import { EOL } from 'os';
import { getCurrentDir } from '../../utils/directory-path.js';
import { getArgByNumber } from "../../utils/command-input.js";

export const cat = async (args) => {
const argPath = getArgByNumber(args, 0);
const resolvedPath = resolve(getCurrentDir(), argPath);

return new Promise((resolve, reject) => {
const readStream = createReadStream(resolvedPath);

readStream.on('data', (chunk) => process.stdout.write(`${EOL}${chunk.toString()}`));
readStream.on('end', resolve);
readStream.on('error', (error) => reject(error));
});
};
25 changes: 25 additions & 0 deletions src/commands/file-system/cp.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { resolve, basename } from 'path';
import { createReadStream, createWriteStream } from 'fs';
import { pipeline } from 'stream/promises';
import { getCurrentDir } from '../../utils/directory-path.js';
import { getArgByNumber, isPathAccessible } from "../../utils/command-input.js";

export const cp = async (args) => {
const sourceArgPath = getArgByNumber(args, 0);
const destArgPath = getArgByNumber(args, 1);

const resolvedSourcePath = resolve(getCurrentDir(), sourceArgPath);
const resolvedDestDirPath = resolve(getCurrentDir(), destArgPath);

if (!(await isPathAccessible(resolvedSourcePath))) {
throw new Error("Source path is not accessible.");
}

const sourceFilename = basename(resolvedSourcePath);
const resolvedDestFilePath = resolve(resolvedDestDirPath, sourceFilename);

const sourceStream = createReadStream(resolvedSourcePath);
const destStream = createWriteStream(resolvedDestFilePath, { flags: 'wx' });

await pipeline(sourceStream, destStream);
};
7 changes: 7 additions & 0 deletions src/commands/file-system/mv.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { cp } from './cp.js';
import { rm } from './rm.js';

export const mv = async (args) => {
await cp(args);
await rm(args);
};
11 changes: 11 additions & 0 deletions src/commands/file-system/rm.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { resolve } from 'path';
import { unlink } from 'fs/promises';
import { getCurrentDir } from '../../utils/directory-path.js';
import { getArgByNumber } from "../../utils/command-input.js";

export const rm = async (args) => {
const argPath = getArgByNumber(args, 0);
const filePath = resolve(getCurrentDir(), argPath);

await unlink(filePath);
};
15 changes: 15 additions & 0 deletions src/commands/file-system/rn.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { rename} from 'fs/promises';
import { basename, resolve, dirname} from 'path';
import { getCurrentDir } from '../../utils/directory-path.js';
import { getArgByNumber } from "../../utils/command-input.js";

export const rn = async (args) => {
const oldPathArg = getArgByNumber(args, 0);
const newNameArg = getArgByNumber(args, 1);
const newFilename = basename(newNameArg);
const oldFilePath = resolve(getCurrentDir(), oldPathArg);
const fileDir = dirname(oldFilePath);
const newFilePath = resolve(fileDir, newFilename);

await rename(oldFilePath, newFilePath);
};
22 changes: 22 additions & 0 deletions src/commands/hash/hash.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { resolve } from 'path';
import { createReadStream } from 'fs';
import { createHash } from 'crypto';
import { getArgByNumber } from "../../utils/command-input.js";
import { getCurrentDir } from '../../utils/directory-path.js';

export const calculateHash = async (args) => {
const argPath = getArgByNumber(args, 0);
const resolvedPath = resolve(getCurrentDir(), argPath);

return new Promise((res, rej) => {
const readStream = createReadStream(resolvedPath);
const hash = createHash('sha256');

readStream.on('data', (chunk) => hash.update(chunk));
readStream.on('end', () => {
console.log(hash.digest('hex'));
res();
});
readStream.on('error', (error) => rej(error));
});
};
83 changes: 83 additions & 0 deletions src/commands/input-handler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { EOL } from 'os';
import { checkArgCount, COMMANDS } from "../utils/command-input.js";
import { printColoredText } from "../utils/color-output.js";
import { printCurrentDirectory } from "../utils/directory-path.js";
import { up } from "./navigation/up.js";
import { cd } from './navigation/cd.js';
import { ls } from './navigation/ls.js';
import { cat } from './file-system/cat.js';
import { add } from './file-system/add.js';
import { rn } from './file-system/rn.js';
import { cp } from './file-system/cp.js';
import { rm } from './file-system/rm.js';
import { mv } from './file-system/mv.js';
import { executeOsCommand } from './os/os.js';
import { calculateHash } from './hash/hash.js';
import { useBrotli } from './archive/brotli.js';

export const handleInput = async (line, readlineClose) => {
try {
const args = line.match(/(").*?\1|\S+/g) || [];
const command = args.shift();

checkArgCount(command, args);

switch (command) {
case undefined:
break;
case COMMANDS.EXIT.name:
readlineClose();
break;
case COMMANDS.UP.name:
up();
break;
case COMMANDS.CD.name:
cd(args);
break;
case COMMANDS.LS.name:
await ls();
break;
case COMMANDS.CAT.name:
await cat(args);
break;
case COMMANDS.ADD.name:
await add(args);
break;
case COMMANDS.RN.name:
await rn(args);
break;
case COMMANDS.CP.name:
await cp(args);
break;
case COMMANDS.MV.name:
await mv(args);
break;
case COMMANDS.RM.name:
await rm(args);
break;
case COMMANDS.OS.name:
executeOsCommand(args);
break;
case COMMANDS.HASH.name:
await calculateHash(args);
break;
case COMMANDS.COMPRESS.name:
await useBrotli(args);
break;
case COMMANDS.DECOMPRESS.name:
await useBrotli(args, false);
break;
default:
printColoredText('RED', `Invalid input. Command "${command}" not supported.`);
break;
}
} catch (err) {
if (err.message.startsWith('Invalid input.')) {
printColoredText('RED', err.message);
} else {
printColoredText('RED', `Operation failed.${EOL}${err.message}`);
}
}

printCurrentDirectory();
};
10 changes: 10 additions & 0 deletions src/commands/navigation/cd.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { isAbsolute, resolve } from 'path';
import { getArgByNumber } from "../../utils/command-input.js";

export const cd = (args) => {
const path = getArgByNumber(args, 0);
const isWinOsIncompleteRootPath = process.platform === 'win32' && !isAbsolute(path) && path.includes(':');
const improvedPath = resolve(isWinOsIncompleteRootPath ? '/' : process.cwd(), path);

process.chdir(improvedPath);
};
20 changes: 20 additions & 0 deletions src/commands/navigation/ls.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { readdir } from 'fs/promises';
import { getCurrentDir } from '../../utils/directory-path.js';

export const ls = async () => {
const dirents = await readdir(getCurrentDir(), { withFileTypes: true });

const getDirentType = (dirent) =>
dirent.isFile() ? 'file'
: dirent.isDirectory() ? 'directory' : 'other';

const tabularData = dirents
.map((dirent) => ({
Name: dirent.name,
Type: getDirentType(dirent),
}))
.filter(({ Type }) => Type !== 'other')
.sort((a, b) => a.Type.localeCompare(b.Type) || a.Name.localeCompare(b.Name));

console.table(tabularData);
};
11 changes: 11 additions & 0 deletions src/commands/navigation/up.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { resolve } from 'path';
import { getCurrentDir } from '../../utils/directory-path.js';

export const up = () => {
const currentDir = getCurrentDir();
const parentDir = resolve(currentDir, '..');

if (currentDir !== parentDir) {
process.chdir('..');
}
};
11 changes: 11 additions & 0 deletions src/commands/os/cpu-info.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { cpus } from 'os';

export const getCpuInfo = () => {
const cpuData = cpus().map((cpu) => ({
'Model': cpu.model.trim(),
'Clock rate, GHz': (cpu.speed / 1000).toFixed(2),
}));

console.log(`Overall amount of CPUs: ${cpuData.length}`);
console.table(cpuData);
};
26 changes: 26 additions & 0 deletions src/commands/os/os.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { EOL, homedir, userInfo } from 'os';
import { getColoredText } from '../../utils/color-output.js';
import { getCpuInfo } from './cpu-info.js';

export const executeOsCommand = (args) => {
switch (args[0]) {
case '--EOL':
console.log('Default system End-Of-Line:', getColoredText('GREEN', JSON.stringify(EOL)));
break;
case '--cpus':
getCpuInfo()
break;
case '--homedir':
console.log('Home directory:', getColoredText('GREEN', homedir()));
break;
case '--username':
console.log('System user name:', getColoredText('GREEN', userInfo().username));
break;
case '--architecture':
console.log('CPU architecture:', getColoredText('GREEN', process.arch));
break;
default:
throw new Error(`Invalid input. Command "os" doesn't support argument "${args[0]}"`);
break;
}
};
31 changes: 31 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { createInterface } from 'readline/promises';
import { printWelcomeMessage, printFarewellMessage } from './utils/user.js';
import { handleInput } from './commands/input-handler.js';
import { printCurrentDirectory, setStartingDir } from './utils/directory-path.js';
import { getColoredText } from './utils/color-output.js';

const init = () => {
const readline = createInterface({
input: process.stdin,
output: process.stdout,
prompt: getColoredText('BLUE', '> '),
});

const readlineClose = () => {
readline.close();
process.exit(0);
};

printWelcomeMessage();
setStartingDir();
printCurrentDirectory();
readline.prompt();

readline.on('close', printFarewellMessage);
readline.on('line', async (input) => {
await handleInput(input, readlineClose);
readline.prompt();
});
};

init();
21 changes: 21 additions & 0 deletions src/utils/color-output.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
const COLORS = {
RESET: '[0m',
BLACK: '[30m',
RED: '[31m',
GREEN: '[32m',
YELLOW: '[33m',
BLUE: '[34m',
PURPLE: '[35m',
CYAN: '[36m',
WHITE: '[37m',
};

export const getColoredText = (color, text) => {
const colorCode = COLORS[color] ?? COLORS.RESET;

return `\x1b${colorCode}${text}\x1b${COLORS.RESET}`;
};

export const printColoredText = (color, text) => {
console.log(getColoredText(color, text));
};
Loading