From b7ff2c0b1b8fd73496e720d415afb9ccbf588814 Mon Sep 17 00:00:00 2001 From: dmytro_mykoliv Date: Sat, 21 Mar 2026 17:41:52 +0200 Subject: [PATCH 1/3] feat: file compression --- .github/workflows/test.yml-template | 23 ++++++ package-lock.json | 9 ++- package.json | 2 +- public/index.html | 37 +++++++++ src/createServer.js | 114 +++++++++++++++++++++++++++- 5 files changed, 178 insertions(+), 7 deletions(-) create mode 100644 .github/workflows/test.yml-template create mode 100644 public/index.html diff --git a/.github/workflows/test.yml-template b/.github/workflows/test.yml-template new file mode 100644 index 0000000..bb13dfc --- /dev/null +++ b/.github/workflows/test.yml-template @@ -0,0 +1,23 @@ +name: Test + +on: + pull_request: + branches: [ master ] + +jobs: + build: + + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [20.x] + + steps: + - uses: actions/checkout@v2 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + - run: npm install + - run: npm test diff --git a/package-lock.json b/package-lock.json index d0b3b95..3650660 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "devDependencies": { "@faker-js/faker": "^8.4.1", "@mate-academy/eslint-config": "latest", - "@mate-academy/scripts": "^1.8.6", + "@mate-academy/scripts": "^2.1.3", "axios": "^1.7.2", "eslint": "^8.57.0", "eslint-plugin-jest": "^28.6.0", @@ -1487,10 +1487,11 @@ } }, "node_modules/@mate-academy/scripts": { - "version": "1.8.6", - "resolved": "https://registry.npmjs.org/@mate-academy/scripts/-/scripts-1.8.6.tgz", - "integrity": "sha512-b4om/whj4G9emyi84ORE3FRZzCRwRIesr8tJHXa8EvJdOaAPDpzcJ8A0sFfMsWH9NUOVmOwkBtOXDu5eZZ00Ig==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@mate-academy/scripts/-/scripts-2.1.3.tgz", + "integrity": "sha512-a07wHTj/1QUK2Aac5zHad+sGw4rIvcNl5lJmJpAD7OxeSbnCdyI6RXUHwXhjF5MaVo9YHrJ0xVahyERS2IIyBQ==", "dev": true, + "license": "MIT", "dependencies": { "@octokit/rest": "^17.11.2", "@types/get-port": "^4.2.0", diff --git a/package.json b/package.json index 1d03d64..8e6392d 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "devDependencies": { "@faker-js/faker": "^8.4.1", "@mate-academy/eslint-config": "latest", - "@mate-academy/scripts": "^1.8.6", + "@mate-academy/scripts": "^2.1.3", "axios": "^1.7.2", "eslint": "^8.57.0", "eslint-plugin-jest": "^28.6.0", diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..cee6eb0 --- /dev/null +++ b/public/index.html @@ -0,0 +1,37 @@ + + + + + + + Document + + +

Compress your files!

+
+
+ + +
+ +
+ + +
+ +
+ + diff --git a/src/createServer.js b/src/createServer.js index 1cf1dda..79a876d 100644 --- a/src/createServer.js +++ b/src/createServer.js @@ -1,8 +1,118 @@ +/* eslint-disable no-console */ 'use strict'; +const { Server } = require('http'); +const fs = require('fs'); +const path = require('path'); +const mime = require('mime-types'); +const { pipeline } = require('stream'); +const zlib = require('zlib'); +const formidable = require('formidable'); + +const compressMap = new Map([ + ['gzip', { compressor: () => zlib.createGzip() }], + ['deflate', { compressor: () => zlib.createDeflate() }], + ['br', { compressor: () => zlib.createBrotliCompress() }], +]); + function createServer() { - /* Write your code here */ - // Return instance of http.Server class + const server = new Server(); + + server.on('request', (req, res) => { + try { + const url = new URL(req.url || '', `http://${req.headers.host}`); + const requestedPath = url.pathname.slice(1) || 'index.html'; + const realPath = path.join('public', requestedPath); + const mimeType = mime.contentType(path.extname(realPath)) || 'text/plain'; + const form = new formidable.IncomingForm(); + + if (requestedPath === 'compress') { + if (req.method === 'POST') { + form.parse(req, (err, fields, files) => { + if (err) { + console.log('[Error]: ', err); + res.statusCode = 400; + res.end('Error: form is invalid'); + + return; + } + + const file = Array.isArray(files.file) ? files.file[0] : files.file; + const compressionType = Array.isArray(fields.compressionType) + ? fields.compressionType[0] + : fields.compressionType; + + if ( + !file || + !compressionType || + !compressMap.has(compressionType) + ) { + res.statusCode = 400; + res.end('Error: File or compressionType is missing'); + + return; + } + + const inputPath = file.filepath; + const readStream = fs.createReadStream(inputPath); + const { compressor } = compressMap.get(compressionType); + + res.writeHead(200, { + 'Content-Type': 'application/octet-stream', + 'Content-Disposition': `attachment; filename=${file.originalFilename}.${compressionType}`, + }); + + pipeline(readStream, compressor(), res, (error) => { + fs.unlink(file.filepath, () => {}); + + if (error) { + console.log(error); + + if (!res.headersSent) { + res.statusCode = 500; + res.end('Error: Compression failed!'); + } else { + res.destroy(); + } + } + }); + + req.on('close', () => { + readStream.destroy(); + compressor.destroy(); + }); + }); + } else { + res.statusCode = 400; + res.end('Error: Wrong method!'); + } + + return; + } + + if (!fs.existsSync(realPath)) { + res.statusCode = 404; + res.end('Not Found'); + + return; + } + + const html = fs.readFileSync(realPath); + + res.statusCode = 200; + res.setHeader('Content-Type', mimeType); + res.end(html); + } catch (error) { + res.statusCode = 500; + res.end('Server Error'); + } + }); + + server.on('error', (error) => { + console.error('An error occurred:', error); + }); + + return server; } module.exports = { From e1df0a3e5c9d28af3e83e39b06c79c2e2668729f Mon Sep 17 00:00:00 2001 From: dmytro_mykoliv Date: Sat, 21 Mar 2026 19:23:07 +0200 Subject: [PATCH 2/3] fix: after review, corrected tests --- src/createServer.js | 19 ++++++------------- tests/createServer.test.js | 7 +++++-- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/src/createServer.js b/src/createServer.js index 79a876d..059fd98 100644 --- a/src/createServer.js +++ b/src/createServer.js @@ -10,9 +10,9 @@ const zlib = require('zlib'); const formidable = require('formidable'); const compressMap = new Map([ - ['gzip', { compressor: () => zlib.createGzip() }], - ['deflate', { compressor: () => zlib.createDeflate() }], - ['br', { compressor: () => zlib.createBrotliCompress() }], + ['gzip', { ext: 'gz', compressor: () => zlib.createGzip() }], + ['deflate', { ext: 'dfl', compressor: () => zlib.createDeflate() }], + ['br', { ext: 'br', compressor: () => zlib.createBrotliCompress() }], ]); function createServer() { @@ -55,11 +55,11 @@ function createServer() { const inputPath = file.filepath; const readStream = fs.createReadStream(inputPath); - const { compressor } = compressMap.get(compressionType); + const { ext, compressor } = compressMap.get(compressionType); res.writeHead(200, { 'Content-Type': 'application/octet-stream', - 'Content-Disposition': `attachment; filename=${file.originalFilename}.${compressionType}`, + 'Content-Disposition': `attachment; filename=${file.originalFilename}.${ext}`, }); pipeline(readStream, compressor(), res, (error) => { @@ -76,11 +76,6 @@ function createServer() { } } }); - - req.on('close', () => { - readStream.destroy(); - compressor.destroy(); - }); }); } else { res.statusCode = 400; @@ -97,11 +92,9 @@ function createServer() { return; } - const html = fs.readFileSync(realPath); - res.statusCode = 200; res.setHeader('Content-Type', mimeType); - res.end(html); + res.end('OK'); } catch (error) { res.statusCode = 500; res.end('Server Error'); diff --git a/tests/createServer.test.js b/tests/createServer.test.js index b53ef15..27b2977 100644 --- a/tests/createServer.test.js +++ b/tests/createServer.test.js @@ -29,12 +29,15 @@ function stringToStream(str) { const compressionTypes = { gzip: { decompress: util.promisify(zlib.gunzip), + extension: 'gz', }, deflate: { decompress: util.promisify(zlib.inflate), + extension: 'dfl', }, br: { decompress: util.promisify(zlib.brotliDecompress), + extension: 'br', }, }; @@ -96,7 +99,7 @@ describe('createServer', () => { }); Object.entries(compressionTypes).forEach( - ([compressionType, { decompress }]) => { + ([compressionType, { decompress, extension }]) => { describe(`compression type "${compressionType}"`, () => { it('should respond with 200 status code', () => { expect.assertions(1); @@ -124,7 +127,7 @@ describe('createServer', () => { headers: formData.getHeaders(), }) .then((res) => { - const expectedHeader = `attachment; filename=${filename}.${compressionType}`; + const expectedHeader = `attachment; filename=${filename}.${extension}`; expect(res.headers['content-disposition']).toBe( expectedHeader, From a1d951f22c5dfe77558a04c787d1cb7906635484 Mon Sep 17 00:00:00 2001 From: dmytro_mykoliv Date: Sat, 21 Mar 2026 19:39:44 +0200 Subject: [PATCH 3/3] fix: return response file as stream --- public/index.html | 1 - src/createServer.js | 4 +++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/public/index.html b/public/index.html index cee6eb0..9c943ca 100644 --- a/public/index.html +++ b/public/index.html @@ -28,7 +28,6 @@

Compress your files!

- diff --git a/src/createServer.js b/src/createServer.js index 059fd98..d82fb43 100644 --- a/src/createServer.js +++ b/src/createServer.js @@ -92,9 +92,11 @@ function createServer() { return; } + const dataStream = fs.createReadStream(realPath); + res.statusCode = 200; res.setHeader('Content-Type', mimeType); - res.end('OK'); + dataStream.pipe(res); } catch (error) { res.statusCode = 500; res.end('Server Error');