From 73fd12d06f554ba2a0a13406ea73102ecd15b7ed Mon Sep 17 00:00:00 2001 From: Adelina Moroaca Date: Sat, 7 Mar 2026 22:42:10 +0200 Subject: [PATCH 1/2] chore: update dependencies --- package-lock.json | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 package-lock.json diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..755c365b --- /dev/null +++ b/package-lock.json @@ -0,0 +1,17 @@ +{ + "name": "node-nodejs-fundamentals", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "node-nodejs-fundamentals", + "version": "1.0.0", + "license": "ISC", + "engines": { + "node": ">=24.10.0", + "npm": ">=10.9.2" + } + } + } +} From 70cd5826f9171a5cc385d77d02d99f9186ea78af Mon Sep 17 00:00:00 2001 From: Adelina Moroaca Date: Sun, 8 Mar 2026 17:24:39 +0200 Subject: [PATCH 2/2] Added progress bar solution --- source.txt.save | 1 + src/cli/interactive.js | 44 +++++++++++++++-- src/cli/progress.js | 41 ++++++++++++++-- src/cp/execCommand.js | 27 ++++++++--- src/fs/findByExt.js | 49 +++++++++++++++++-- src/fs/merge.js | 71 +++++++++++++++++++++++++-- src/fs/restore.js | 56 ++++++++++++++++++++-- src/fs/snapshot.js | 73 +++++++++++++++++++++++++--- src/hash/verify.js | 55 +++++++++++++++++++-- src/modules/dynamic.js | 59 ++++++++++++++++++++--- src/streams/filter.js | 37 ++++++++++++--- src/streams/lineNumberer.js | 31 ++++++++++-- src/streams/split.js | 67 ++++++++++++++++++++++++-- src/wt/main.js | 82 ++++++++++++++++++++++++++++---- src/wt/worker.js | 18 ++++--- src/zip/compressDir.js | 95 ++++++++++++++++++++++++++++++++++--- src/zip/decompressDir.js | 74 +++++++++++++++++++++++++++-- 17 files changed, 792 insertions(+), 88 deletions(-) create mode 100644 source.txt.save diff --git a/source.txt.save b/source.txt.save new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/source.txt.save @@ -0,0 +1 @@ + diff --git a/src/cli/interactive.js b/src/cli/interactive.js index d0e3e0d9..1dee8c88 100644 --- a/src/cli/interactive.js +++ b/src/cli/interactive.js @@ -1,8 +1,42 @@ +import readline from 'readline'; + const interactive = () => { - // Write your code here - // Use readline module for interactive CLI - // Support commands: uptime, cwd, date, exit - // Handle Ctrl+C and unknown commands + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + prompt: '> ', + }); + + const onExit = () => { + console.log('Goodbye!'); + process.exit(0); + }; + + rl.prompt(); + + rl.on('line', (line) => { + const input = line.trim(); + switch (input) { + case 'uptime': + console.log(`Uptime: ${process.uptime().toFixed(2)}s`); + break; + case 'cwd': + console.log(process.cwd()); + break; + case 'date': + console.log(new Date().toISOString()); + break; + case 'exit': + onExit(); + return; + default: + console.log('Unknown command'); + } + rl.prompt(); + }); + + rl.on('SIGINT', onExit); + rl.on('close', onExit); }; -interactive(); +interactive(); \ No newline at end of file diff --git a/src/cli/progress.js b/src/cli/progress.js index 3e060763..0aa687ac 100644 --- a/src/cli/progress.js +++ b/src/cli/progress.js @@ -1,8 +1,39 @@ +function getArg(name, defaultValue) { + const idx = process.argv.indexOf(`--${name}`); + if (idx !== -1 && process.argv[idx + 1]) { + return process.argv[idx + 1]; + } + return defaultValue; +} + +const totalDuration = Number(getArg('duration', 5000)); +const interval = Number(getArg('interval', 100)); +const barLength = Number(getArg('length', 30)); +const color = getArg('color', null); + const progress = () => { - // Write your code here - // Simulate progress bar from 0% to 100% over ~5 seconds - // Update in place using \r every 100ms - // Format: [████████████████████ ] 67% + let elapsed = 0; + + const timer = setInterval(() => { + elapsed += interval; + let percent = Math.min((elapsed / totalDuration) * 100, 100); + let filledLength = Math.round((percent / 100) * barLength); + let bar = '█'.repeat(filledLength) + ' '.repeat(barLength - filledLength); + + let output = `[${bar}] ${Math.round(percent)}%`; + if (color) { + const r = parseInt(color.slice(1, 3), 16); + const g = parseInt(color.slice(3, 5), 16); + const b = parseInt(color.slice(5, 7), 16); + output = `\x1b[38;2;${r};${g};${b}m${output}\x1b[0m`; + } + + process.stdout.write(`\r${output}`); + if (percent >= 100) { + clearInterval(timer); + process.stdout.write('\nDone!\n'); + } + }, interval); }; -progress(); +progress(); \ No newline at end of file diff --git a/src/cp/execCommand.js b/src/cp/execCommand.js index 34a89c8d..ee522f33 100644 --- a/src/cp/execCommand.js +++ b/src/cp/execCommand.js @@ -1,10 +1,23 @@ +import { spawn } from 'child_process'; + const execCommand = () => { - // Write your code here - // Take command from CLI argument - // Spawn child process - // Pipe child stdout/stderr to parent stdout/stderr - // Pass environment variables - // Exit with same code as child + const commandStr = process.argv[2]; + if (!commandStr) { + console.error('No command provided'); + process.exit(1); + } + + const [command, ...args] = commandStr.split(' '); + + const child = spawn(command, args, { + stdio: ['inherit', 'inherit', 'inherit'], + env: process.env, + shell: true, + }); + + child.on('exit', (code) => { + process.exit(code); + }); }; -execCommand(); +execCommand(); \ No newline at end of file diff --git a/src/fs/findByExt.js b/src/fs/findByExt.js index 24f06cb8..209887c5 100644 --- a/src/fs/findByExt.js +++ b/src/fs/findByExt.js @@ -1,7 +1,50 @@ +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + const findByExt = async () => { - // Write your code here - // Recursively find all files with specific extension - // Parse --ext CLI argument (default: .txt) + const extArgIndex = process.argv.indexOf('--ext'); + let ext = '.txt'; + if (extArgIndex !== -1 && process.argv[extArgIndex + 1]) { + let rawExt = process.argv[extArgIndex + 1]; + ext = rawExt.startsWith('.') ? rawExt : `.${rawExt}`; + } + + const workspace = path.join(__dirname, '../../workspace'); + + const result = []; + async function search(dir) { + let entries; + try { + entries = await fs.readdir(dir, { withFileTypes: true }); + } catch (e) { + throw new Error('FS operation failed'); + } + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + const relPath = path.relative(workspace, fullPath); + if (entry.isDirectory()) { + await search(fullPath); + } else if (entry.isFile() && path.extname(entry.name) === ext) { + result.push(relPath); + } + } + } + + try { + await fs.access(workspace); + } catch (e) { + throw new Error('FS operation failed'); + } + + await search(workspace); + + result.sort(); + for (const file of result) { + console.log(file); + } }; await findByExt(); diff --git a/src/fs/merge.js b/src/fs/merge.js index cb8e0d8f..f46c2c18 100644 --- a/src/fs/merge.js +++ b/src/fs/merge.js @@ -1,8 +1,71 @@ +import path from 'path'; +import fs from 'fs/promises'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + const merge = async () => { - // Write your code here - // Default: read all .txt files from workspace/parts in alphabetical order - // Optional: support --files filename1,filename2,... to merge specific files in provided order - // Concatenate content and write to workspace/merged.txt + const workspace = path.join(__dirname, '../../workspace'); + const partsDir = path.join(workspace, 'parts'); + const mergedFile = path.join(workspace, 'merged.txt'); + + const filesArgIndex = process.argv.indexOf('--files'); + let filesToMerge = null; + if (filesArgIndex !== -1 && process.argv[filesArgIndex + 1]) { + filesToMerge = process.argv[filesArgIndex + 1].split(',').map(f => f.trim()); + } + + try { + await fs.access(partsDir); + } catch (e) { + console.error(e); + throw new Error('FS operation failed'); + } + + let files; + if (filesToMerge) { + files = filesToMerge; + for (const file of files) { + const filePath = path.join(partsDir, file); + try { + await fs.access(filePath); + } catch (e) { + throw new Error('FS operation failed'); + } + } + } else { + let entries; + try { + entries = await fs.readdir(partsDir, { withFileTypes: true }); + } catch (e) { + throw new Error('FS operation failed'); + } + files = entries + .filter(entry => entry.isFile() && entry.name.endsWith('.txt')) + .map(entry => entry.name) + .sort(); + if (files.length === 0) { + throw new Error('FS operation failed'); + } + } + + let mergedContent = ''; + for (const file of files) { + const filePath = path.join(partsDir, file); + try { + const content = await fs.readFile(filePath, 'utf8'); + mergedContent += content; + } catch (e) { + throw new Error('FS operation failed'); + } + } + + try { + await fs.writeFile(mergedFile, mergedContent, 'utf8'); + } catch (e) { + throw new Error('FS operation failed'); + } }; await merge(); diff --git a/src/fs/restore.js b/src/fs/restore.js index 96ae1ffb..2fa97ae4 100644 --- a/src/fs/restore.js +++ b/src/fs/restore.js @@ -1,8 +1,54 @@ +import path from 'path'; +import fs from 'fs/promises'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + const restore = async () => { - // Write your code here - // Read snapshot.json - // Treat snapshot.rootPath as metadata only - // Recreate directory/file structure in workspace_restored + const baseDir = path.join(__dirname, '../../'); + const snapshotPath = path.join(baseDir, 'snapshot.json'); + const restoredDir = path.join(baseDir, 'workspace_restored'); + + try { + await fs.access(snapshotPath); + } catch { + throw new Error('FS operation failed'); + } + + try { + await fs.access(restoredDir); + throw new Error('FS operation failed'); + } catch { + // If error, directory does not exist, continue + } + + let snapshot; + try { + const data = await fs.readFile(snapshotPath, 'utf8'); + snapshot = JSON.parse(data); + } catch { + throw new Error('FS operation failed'); + } + + for (const entry of snapshot.entries) { + const entryPath = path.join(restoredDir, entry.path); + if (entry.type === 'directory') { + try { + await fs.mkdir(entryPath, { recursive: true }); + } catch { + throw new Error('FS operation failed'); + } + } else if (entry.type === 'file') { + await fs.mkdir(path.dirname(entryPath), { recursive: true }); + try { + const content = Buffer.from(entry.content, 'base64'); + await fs.writeFile(entryPath, content); + } catch { + throw new Error('FS operation failed'); + } + } + } }; -await restore(); +await restore(); \ No newline at end of file diff --git a/src/fs/snapshot.js b/src/fs/snapshot.js index 050103d3..14fbf023 100644 --- a/src/fs/snapshot.js +++ b/src/fs/snapshot.js @@ -1,9 +1,70 @@ +import path from 'path'; +import fs from 'fs/promises'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + const snapshot = async () => { - // Write your code here - // Recursively scan workspace directory - // Write snapshot.json with: - // - rootPath: absolute path to workspace - // - entries: flat array of relative paths and metadata + const baseDir = path.join(__dirname, '../../'); + const workspace = path.join(baseDir, 'workspace'); + const snapshotPath = path.join(baseDir, 'snapshot.json'); + + try { + await fs.access(workspace); + } catch { + throw new Error('FS operation failed'); + } + + const entries = []; + + async function scan(dir) { + let dirents; + try { + dirents = await fs.readdir(dir, { withFileTypes: true }); + } catch { + throw new Error('FS operation failed'); + } + for (const dirent of dirents) { + const fullPath = path.join(dir, dirent.name); + const relPath = path.relative(workspace, fullPath); + if (dirent.isDirectory()) { + entries.push({ + path: relPath, + type: 'directory' + }); + await scan(fullPath); + } else if (dirent.isFile()) { + let content, size; + try { + const fileBuffer = await fs.readFile(fullPath); + content = fileBuffer.toString('base64'); + size = fileBuffer.length; + } catch { + throw new Error('FS operation failed'); + } + entries.push({ + path: relPath, + type: 'file', + size, + content + }); + } + } + } + + await scan(workspace); + + const snapshotObj = { + rootPath: path.resolve(workspace), + entries + }; + + try { + await fs.writeFile(snapshotPath, JSON.stringify(snapshotObj, null, 2), 'utf8'); + } catch { + throw new Error('FS operation failed'); + } }; -await snapshot(); +await snapshot(); \ No newline at end of file diff --git a/src/hash/verify.js b/src/hash/verify.js index 7f1e8961..464d6b5d 100644 --- a/src/hash/verify.js +++ b/src/hash/verify.js @@ -1,8 +1,53 @@ +import fs from 'fs'; +import fsp from 'fs/promises'; +import path from 'path'; +import crypto from 'crypto'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + const verify = async () => { - // Write your code here - // Read checksums.json - // Calculate SHA256 hash using Streams API - // Print result: filename — OK/FAIL + const baseDir = path.join(__dirname, '../../'); + const checksumsPath = path.join(baseDir, 'checksums.json'); + + try { + await fsp.access(checksumsPath); + } catch { + throw new Error('FS operation failed'); + } + + let checksums; + try { + const data = await fsp.readFile(checksumsPath, 'utf8'); + checksums = JSON.parse(data); + } catch { + throw new Error('FS operation failed'); + } + + const fileNames = Object.keys(checksums); + for (const fileName of fileNames) { + const filePath = path.join(baseDir, fileName); + let hash; + try { + await fsp.access(filePath); + hash = await new Promise((resolve, reject) => { + const stream = fs.createReadStream(filePath); + const sha256 = crypto.createHash('sha256'); + stream.on('error', reject); + sha256.on('error', reject); + stream.on('data', chunk => sha256.update(chunk)); + stream.on('end', () => resolve(sha256.digest('hex'))); + }); + } catch { + hash = null; + } + if (hash && hash === checksums[fileName]) { + console.log(`${fileName} — OK`); + } else { + console.log(`${fileName} — FAIL`); + } + } }; -await verify(); +await verify(); \ No newline at end of file diff --git a/src/modules/dynamic.js b/src/modules/dynamic.js index 008ca387..9631cd12 100644 --- a/src/modules/dynamic.js +++ b/src/modules/dynamic.js @@ -1,9 +1,54 @@ -const dynamic = async () => { - // Write your code here - // Accept plugin name as CLI argument - // Dynamically import plugin from plugins/ directory - // Call run() function and print result - // Handle missing plugin case +import fs from 'fs'; +import fsp from 'fs/promises'; +import path from 'path'; +import crypto from 'crypto'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const verify = async () => { + const baseDir = path.join(__dirname, '../../'); + const checksumsPath = path.join(baseDir, 'checksums.json'); + + + try { + await fsp.access(checksumsPath); + } catch { + throw new Error('FS operation failed'); + } + + let checksums; + try { + const data = await fsp.readFile(checksumsPath, 'utf8'); + checksums = JSON.parse(data); + } catch { + throw new Error('FS operation failed'); + } + + const fileNames = Object.keys(checksums); + for (const fileName of fileNames) { + const filePath = path.join(baseDir, fileName); + let hash; + try { + await fsp.access(filePath); + hash = await new Promise((resolve, reject) => { + const stream = fs.createReadStream(filePath); + const sha256 = crypto.createHash('sha256'); + stream.on('error', reject); + sha256.on('error', reject); + stream.on('data', chunk => sha256.update(chunk)); + stream.on('end', () => resolve(sha256.digest('hex'))); + }); + } catch { + hash = null; + } + if (hash && hash === checksums[fileName]) { + console.log(`${fileName} — OK`); + } else { + console.log(`${fileName} — FAIL`); + } + } }; -await dynamic(); +await verify(); \ No newline at end of file diff --git a/src/streams/filter.js b/src/streams/filter.js index 3868ab46..dd79b621 100644 --- a/src/streams/filter.js +++ b/src/streams/filter.js @@ -1,9 +1,34 @@ +import { Transform } from 'stream'; + +const patternIndex = process.argv.indexOf('--pattern'); +let pattern = ''; +if (patternIndex !== -1 && process.argv[patternIndex + 1]) { + pattern = process.argv[patternIndex + 1]; +} + const filter = () => { - // Write your code here - // Read from process.stdin - // Filter lines by --pattern CLI argument - // Use Transform Stream - // Write to process.stdout + let leftover = ''; + const filterStream = new Transform({ + transform(chunk, encoding, callback) { + const data = leftover + chunk.toString(); + const lines = data.split('\n'); + leftover = lines.pop(); + for (const line of lines) { + if (line.includes(pattern)) { + this.push(line + '\n'); + } + } + callback(); + }, + flush(callback) { + if (leftover && leftover.includes(pattern)) { + this.push(leftover + '\n'); + } + callback(); + } + }); + + process.stdin.pipe(filterStream).pipe(process.stdout); }; -filter(); +filter(); \ No newline at end of file diff --git a/src/streams/lineNumberer.js b/src/streams/lineNumberer.js index 579d662e..65c4070e 100644 --- a/src/streams/lineNumberer.js +++ b/src/streams/lineNumberer.js @@ -1,8 +1,29 @@ +import { Transform } from 'stream'; + const lineNumberer = () => { - // Write your code here - // Read from process.stdin - // Use Transform Stream to prepend line numbers - // Write to process.stdout + let leftover = ''; + let lineNumber = 1; + + const numberer = new Transform({ + transform(chunk, encoding, callback) { + const data = leftover + chunk.toString(); + const lines = data.split('\n'); + leftover = lines.pop(); + for (const line of lines) { + this.push(`${lineNumber} | ${line}\n`); + lineNumber++; + } + callback(); + }, + flush(callback) { + if (leftover) { + this.push(`${lineNumber} | ${leftover}\n`); + } + callback(); + } + }); + + process.stdin.pipe(numberer).pipe(process.stdout); }; -lineNumberer(); +lineNumberer(); \ No newline at end of file diff --git a/src/streams/split.js b/src/streams/split.js index f8f814fa..bb3d2abd 100644 --- a/src/streams/split.js +++ b/src/streams/split.js @@ -1,8 +1,65 @@ +import fs from 'fs'; +import fsp from 'fs/promises'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const linesArgIndex = process.argv.indexOf('--lines'); +let maxLines = 10; +if (linesArgIndex !== -1 && process.argv[linesArgIndex + 1]) { + const parsed = parseInt(process.argv[linesArgIndex + 1], 10); + if (!isNaN(parsed) && parsed > 0) { + maxLines = parsed; + } +} + const split = async () => { - // Write your code here - // Read source.txt using Readable Stream - // Split into chunk_1.txt, chunk_2.txt, etc. - // Each chunk max N lines (--lines CLI argument, default: 10) + const baseDir = path.join(__dirname, '../../'); + const sourcePath = path.join(baseDir, 'source.txt'); + + try { + await fsp.access(sourcePath); + } catch { + throw new Error('FS operation failed'); + } + + let chunkIndex = 1; + let lineBuffer = []; + let leftover = ''; + + const readStream = fs.createReadStream(sourcePath, { encoding: 'utf8' }); + + readStream.on('data', chunk => { + const data = leftover + chunk; + const lines = data.split('\n'); + leftover = lines.pop(); + + for (const line of lines) { + lineBuffer.push(line); + if (lineBuffer.length === maxLines) { + const chunkFile = path.join(baseDir, `chunk_${chunkIndex}.txt`); + fs.writeFileSync(chunkFile, lineBuffer.join('\n') + '\n', 'utf8'); + chunkIndex++; + lineBuffer = []; + } + } + }); + + readStream.on('end', () => { + if (leftover) { + lineBuffer.push(leftover); + } + if (lineBuffer.length > 0) { + const chunkFile = path.join(baseDir, `chunk_${chunkIndex}.txt`); + fs.writeFileSync(chunkFile, lineBuffer.join('\n') + '\n', 'utf8'); + } + }); + + readStream.on('error', () => { + throw new Error('FS operation failed'); + }); }; -await split(); +await split(); \ No newline at end of file diff --git a/src/wt/main.js b/src/wt/main.js index d7d21f0c..b10347de 100644 --- a/src/wt/main.js +++ b/src/wt/main.js @@ -1,11 +1,77 @@ +import path from 'path'; +import { fileURLToPath } from 'url'; +import os from 'os'; +import { Worker } from 'worker_threads'; +import fsp from 'fs/promises'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + + const main = async () => { - // Write your code here - // Read data.json containing array of numbers - // Split into N chunks (N = CPU cores) - // Create N workers, send one chunk to each - // Collect sorted chunks - // Merge using k-way merge algorithm - // Log final sorted array + const baseDir = path.join(__dirname, '../../'); + const dataPath = path.join(baseDir, 'data.json'); + + let numbers; + try { + const data = await fsp.readFile(dataPath, 'utf8'); + numbers = JSON.parse(data); + if (!Array.isArray(numbers)) throw new Error(); + } catch { + throw new Error('FS operation failed'); + } + + const numCores = os.cpus().length; + const chunkSize = Math.ceil(numbers.length / numCores); + const chunks = []; + for (let i = 0; i < numCores; i++) { + chunks.push(numbers.slice(i * chunkSize, (i + 1) * chunkSize)); + } + + const workerPath = path.join(__dirname, 'worker.js'); + const promises = chunks.map(chunk => + new Promise((resolve, reject) => { + const worker = new Worker(workerPath, { workerData: chunk }); + worker.on('message', resolve); + worker.on('error', reject); + worker.on('exit', code => { + if (code !== 0) reject(new Error(`Worker stopped with exit code ${code}`)); + }); + }) + ); + + let sortedChunks; + try { + sortedChunks = await Promise.all(promises); + } catch { + throw new Error('Worker thread failed'); + } + + function kWayMerge(arrays) { + const result = []; + const indices = new Array(arrays.length).fill(0); + + while (true) { + let minValue = null; + let minIndex = -1; + for (let i = 0; i < arrays.length; i++) { + if (indices[i] < arrays[i].length) { + if (minValue === null || arrays[i][indices[i]] < minValue) { + minValue = arrays[i][indices[i]]; + minIndex = i; + } + } + } + if (minIndex === -1) break; + result.push(minValue); + indices[minIndex]++; + } + return result; + } + + const finalSorted = kWayMerge(sortedChunks); + + console.log(finalSorted); }; -await main(); +await main(); \ No newline at end of file diff --git a/src/wt/worker.js b/src/wt/worker.js index 15f42fc8..b7028e13 100644 --- a/src/wt/worker.js +++ b/src/wt/worker.js @@ -1,9 +1,15 @@ -import { parentPort } from 'worker_threads'; +import { parentPort, workerData } from 'worker_threads'; -// Receive array from main thread -// Sort in ascending order -// Send back to main thread +if (workerData) { + const sorted = Array.isArray(workerData) + ? [...workerData].sort((a, b) => a - b) + : []; + parentPort.postMessage(sorted); +} parentPort.on('message', (data) => { - // Write your code here -}); + if (Array.isArray(data)) { + const sorted = [...data].sort((a, b) => a - b); + parentPort.postMessage(sorted); + } +}); \ No newline at end of file diff --git a/src/zip/compressDir.js b/src/zip/compressDir.js index 3a3c5089..31a11a66 100644 --- a/src/zip/compressDir.js +++ b/src/zip/compressDir.js @@ -1,9 +1,92 @@ +import fs from 'fs'; +import fsp from 'fs/promises'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import { pipeline } from 'stream'; +import zlib from 'zlib'; +import { promisify } from 'util'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const pipe = promisify(pipeline); + const compressDir = async () => { - // Write your code here - // Read all files from workspace/toCompress/ - // Compress entire directory structure into archive.br - // Save to workspace/compressed/ - // Use Streams API + const baseDir = path.join(__dirname, '../../'); + const srcDir = path.join(baseDir, 'workspace/toCompress'); + const destDir = path.join(baseDir, 'workspace/compressed'); + const archivePath = path.join(destDir, 'archive.br'); + + try { + await fsp.access(srcDir); + } catch { + throw new Error('FS operation failed'); + } + + try { + await fsp.mkdir(destDir, { recursive: true }); + } catch { + throw new Error('FS operation failed'); + } + + async function collectEntries(dir, rel = '') { + const entries = []; + const dirents = await fsp.readdir(dir, { withFileTypes: true }); + for (const dirent of dirents) { + const fullPath = path.join(dir, dirent.name); + const relPath = path.join(rel, dirent.name); + if (dirent.isDirectory()) { + entries.push({ type: 'directory', path: relPath }); + entries.push(...await collectEntries(fullPath, relPath)); + } else if (dirent.isFile()) { + entries.push({ type: 'file', path: relPath, fullPath }); + } + } + return entries; + } + + let entries; + try { + entries = await collectEntries(srcDir); + } catch { + throw new Error('FS operation failed'); + } + + const archiveObj = []; + for (const entry of entries) { + if (entry.type === 'directory') { + archiveObj.push({ type: 'directory', path: entry.path }); + } else if (entry.type === 'file') { + const content = await fsp.readFile(entry.fullPath); + archiveObj.push({ + type: 'file', + path: entry.path, + content: content.toString('base64') + }); + } + } + + const jsonStream = fs.createReadStream( + await (async () => { + const tmpPath = path.join(destDir, 'tmp-archive.json'); + await fsp.writeFile(tmpPath, JSON.stringify(archiveObj), 'utf8'); + return tmpPath; + })() + ); + const brotli = zlib.createBrotliCompress(); + const outStream = fs.createWriteStream(archivePath); + + try { + await pipe(jsonStream, brotli, outStream); + } catch { + throw new Error('FS operation failed'); + } + + try { + await fsp.unlink(path.join(destDir, 'tmp-archive.json')); + } catch { + // ignore + } }; -await compressDir(); +await compressDir(); \ No newline at end of file diff --git a/src/zip/decompressDir.js b/src/zip/decompressDir.js index d6e770f6..16d129bb 100644 --- a/src/zip/decompressDir.js +++ b/src/zip/decompressDir.js @@ -1,8 +1,72 @@ +import fs from 'fs'; +import fsp from 'fs/promises'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import zlib from 'zlib'; +import { pipeline, Writable } from 'stream'; +import { promisify } from 'util'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const pipe = promisify(pipeline); + const decompressDir = async () => { - // Write your code here - // Read archive.br from workspace/compressed/ - // Decompress and extract to workspace/decompressed/ - // Use Streams API + const baseDir = path.join(__dirname, '../../'); + const compressedDir = path.join(baseDir, 'workspace/compressed'); + const archivePath = path.join(compressedDir, 'archive.br'); + const destDir = path.join(baseDir, 'workspace/decompressed'); + + try { + await fsp.access(compressedDir); + await fsp.access(archivePath); + } catch { + throw new Error('FS operation failed'); + } + + try { + await fsp.mkdir(destDir, { recursive: true }); + } catch { + throw new Error('FS operation failed'); + } + + let archiveObj; + try { + const chunks = []; + await pipe( + fs.createReadStream(archivePath), + zlib.createBrotliDecompress(), + new Writable({ + write(chunk, encoding, callback) { + chunks.push(chunk); + callback(); + } + }) + ); + const jsonStr = Buffer.concat(chunks).toString('utf8'); + archiveObj = JSON.parse(jsonStr); + } catch { + throw new Error('FS operation failed'); + } + + for (const entry of archiveObj) { + const entryPath = path.join(destDir, entry.path); + if (entry.type === 'directory') { + try { + await fsp.mkdir(entryPath, { recursive: true }); + } catch { + throw new Error('FS operation failed'); + } + } else if (entry.type === 'file') { + await fsp.mkdir(path.dirname(entryPath), { recursive: true }); + try { + const content = Buffer.from(entry.content, 'base64'); + await fsp.writeFile(entryPath, content); + } catch { + throw new Error('FS operation failed'); + } + } + } }; -await decompressDir(); +await decompressDir(); \ No newline at end of file