From e1b1dfadf0f8c12e5a6439cc7d850131a1d54016 Mon Sep 17 00:00:00 2001 From: Bespalov Tymofii Date: Tue, 20 Jan 2026 16:40:21 +0100 Subject: [PATCH 1/6] part 1 --- .github/workflows/test.yml-template | 23 ++++++ package-lock.json | 35 ++++----- package.json | 2 +- src/createServer.js | 107 +++++++++++++++++++++++++++- src/index.html | 0 5 files changed, 148 insertions(+), 19 deletions(-) create mode 100644 .github/workflows/test.yml-template create mode 100644 src/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..3ddfa6c 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", @@ -63,6 +63,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.9.tgz", "integrity": "sha512-5e3FI4Q3M3Pbr21+5xJwCv6ZT6KmGkI0vw3Tozy5ODAQFTIWe37iT8Cr7Ice2Ntb+M3iSKCEWMB1MBgKrW3whg==", "dev": true, + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.24.7", @@ -1487,10 +1488,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", @@ -1556,7 +1558,6 @@ "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-5.1.1.tgz", "integrity": "sha512-rh3G3wDO8J9wSjfI436JUKzHIxq8NaiL0tVeB2aXmG6p/9859aUOAjA9pmSPNGGZxfwmaJ9ozOJImuNVJdpvbA==", "dev": true, - "peer": true, "engines": { "node": ">= 18" } @@ -1585,7 +1586,6 @@ "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.1.tgz", "integrity": "sha512-JYjh5rMOwXMJyUpj028cu0Gbp7qe/ihxfJMLc8VZBMMqSwLgOxDI1911gV4Enl1QSavAQNJcwmwBF9M0VvLh6Q==", "dev": true, - "peer": true, "dependencies": { "@octokit/types": "^13.0.0", "universal-user-agent": "^7.0.2" @@ -1599,7 +1599,6 @@ "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-8.1.1.tgz", "integrity": "sha512-ukiRmuHTi6ebQx/HFRCXKbDlOh/7xEV6QUXaE7MJEKGNAncGI/STSbOkl12qVXZrfZdpXctx5O9X1AIaebiDBg==", "dev": true, - "peer": true, "dependencies": { "@octokit/request": "^9.0.0", "@octokit/types": "^13.0.0", @@ -1613,8 +1612,7 @@ "version": "22.2.0", "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz", "integrity": "sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==", - "dev": true, - "peer": true + "dev": true }, "node_modules/@octokit/plugin-paginate-rest": { "version": "2.21.3", @@ -1676,7 +1674,6 @@ "resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.1.3.tgz", "integrity": "sha512-V+TFhu5fdF3K58rs1pGUJIDH5RZLbZm5BI+MNF+6o/ssFNT4vWlCh/tVpF3NxGtP15HUxTTMUbsG5llAuU2CZA==", "dev": true, - "peer": true, "dependencies": { "@octokit/endpoint": "^10.0.0", "@octokit/request-error": "^6.0.1", @@ -1692,7 +1689,6 @@ "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.4.tgz", "integrity": "sha512-VpAhIUxwhWZQImo/dWAN/NpPqqojR6PSLgLYAituLM6U+ddx9hCioFGwBr5Mi+oi5CLeJkcAs3gJ0PYYzU6wUg==", "dev": true, - "peer": true, "dependencies": { "@octokit/types": "^13.0.0" }, @@ -1880,7 +1876,6 @@ "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.5.0.tgz", "integrity": "sha512-HdqWTf5Z3qwDVlzCrP8UJquMwunpDiMPt5er+QjGzL4hqr/vBVY/MauQgS1xWxCDT1oMx1EULyqxncdCY/NVSQ==", "dev": true, - "peer": true, "dependencies": { "@octokit/openapi-types": "^22.2.0" } @@ -2223,6 +2218,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", "dev": true, + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2667,8 +2663,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-3.0.2.tgz", "integrity": "sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==", - "dev": true, - "peer": true + "dev": true }, "node_modules/brace-expansion": { "version": "1.1.11", @@ -2711,6 +2706,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001640", "electron-to-chromium": "^1.4.820", @@ -3455,6 +3451,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", "dev": true, + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -3510,6 +3507,7 @@ "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", "dev": true, + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -3600,6 +3598,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz", "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==", "dev": true, + "peer": true, "dependencies": { "array-includes": "^3.1.7", "array.prototype.findlastindex": "^1.2.3", @@ -3677,6 +3676,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz", "integrity": "sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g==", "dev": true, + "peer": true, "dependencies": { "eslint-plugin-es": "^3.0.0", "eslint-utils": "^2.0.0", @@ -3727,6 +3727,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-4.3.1.tgz", "integrity": "sha512-bY2sGqyptzFBDLh/GMbAxfdJC+b0f23ME63FOE4+Jao0oZ3E1LEwFtWJX/1pGMJLiTtrSSern2CRM/g+dfc0eQ==", "dev": true, + "peer": true, "engines": { "node": ">=6" } @@ -3750,6 +3751,7 @@ "url": "https://feross.org/support" } ], + "peer": true, "peerDependencies": { "eslint": ">=5.0.0" } @@ -5175,6 +5177,7 @@ "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, + "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -7528,6 +7531,7 @@ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", "dev": true, + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -8465,8 +8469,7 @@ "version": "7.0.2", "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.2.tgz", "integrity": "sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q==", - "dev": true, - "peer": true + "dev": true }, "node_modules/universalify": { "version": "2.0.1", 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/src/createServer.js b/src/createServer.js index 1cf1dda..0b95e55 100644 --- a/src/createServer.js +++ b/src/createServer.js @@ -1,10 +1,113 @@ 'use strict'; +import http from 'http'; +import fs from 'fs'; + +const endpoints = { + home: '/', + compress: '/compress', +}; + +const compMethods = { + gzip: 'gzip', + deflate: 'deflate', + brotli: 'br', +}; + +const index = ` + + + + + + Compression app + + +
+ + + + + +
+ + +`; + +const processor = { + [endpoints.home]: (res) => { + res.setHeader('Content-Type', 'text/html'); + res.end(index); + }, + [endpoints.compress]: (req, res) => { + const fileStream = fs.createReadStream(req); + + fileStream.on('data', (chunk) => { + res.write(chunk); + }); + }, +}; function createServer() { - /* Write your code here */ - // Return instance of http.Server class + return http.createServer((req, res) => { + const validated = validate(req); + + if (!validated.ok) { + res.statusCode = validated.statusCode; + res.end(validated.message); + } + + processor[validated.pathname](req); + }); } module.exports = { createServer, }; + +const getValidationError = (statusCode, message) => { + return { + ok: false, + statusCode: statusCode, + message: message, + }; +}; + +const validate = (req) => { + const url = new URL(req.url, `http://${req.headers.host}`); + + if (!Object.values(endpoints).includes(url.pathname)) { + return getValidationError(404, `Endpoint doesn't exist: ${url.pathname}`); + } + + if (url.pathname === endpoints.compress) { + if (req.method !== 'POST') { + return getValidationError( + 400, + `Bad request: unsupported method ${req.method}`, + ); + } + + if (!req.file || !req.compressionType) { + return getValidationError(400, `Invalid form`); + } + + if (!Object.keys(compMethods).includes(req.compressionType)) { + return getValidationError(400, `Unsupported compression method`); + } + } + + return { + ok: true, + pathname: url.pathname, + }; +}; diff --git a/src/index.html b/src/index.html new file mode 100644 index 0000000..e69de29 From f7a7021910692138780316d580370902a357fb64 Mon Sep 17 00:00:00 2001 From: Bespalov Tymofii Date: Wed, 21 Jan 2026 11:36:46 +0100 Subject: [PATCH 2/6] test --- package-lock.json | 22 ++++++ package.json | 3 + src/constants.js | 66 ++++++++++++++++++ src/createServer.js | 164 +++++++++++++++++++------------------------- src/index.html | 0 src/validate.js | 51 ++++++++++++++ 6 files changed, 212 insertions(+), 94 deletions(-) create mode 100644 src/constants.js delete mode 100644 src/index.html create mode 100644 src/validate.js diff --git a/package-lock.json b/package-lock.json index 3ddfa6c..1b7f8ad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,9 @@ "version": "1.0.0", "hasInstallScript": true, "license": "GPL-3.0", + "dependencies": { + "busboy": "^1.6.0" + }, "devDependencies": { "@faker-js/faker": "^8.4.1", "@mate-academy/eslint-config": "latest", @@ -2735,6 +2738,17 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, "node_modules/call-bind": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", @@ -8044,6 +8058,14 @@ "node": ">=8" } }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", diff --git a/package.json b/package.json index 8e6392d..9f3cce9 100644 --- a/package.json +++ b/package.json @@ -30,5 +30,8 @@ }, "mateAcademy": { "projectType": "javascript" + }, + "dependencies": { + "busboy": "^1.6.0" } } diff --git a/src/constants.js b/src/constants.js new file mode 100644 index 0000000..7104c14 --- /dev/null +++ b/src/constants.js @@ -0,0 +1,66 @@ +'use strict'; + +const endpoints = { + home: '/', + compress: '/compress', +}; + +const compMethods = { + gzip: 'gzip', + deflate: 'deflate', + brotli: 'br', +}; + +const htmlNames = { + comp: 'compressionType', + file: 'file', +}; + +const index = ` + + + + + + Compression app + + + + + +
+ + + +
+ + + + + +`; + +module.exports = { + endpoints, + compMethods, + htmlNames, + index, +}; diff --git a/src/createServer.js b/src/createServer.js index 0b95e55..2f673f0 100644 --- a/src/createServer.js +++ b/src/createServer.js @@ -1,113 +1,89 @@ 'use strict'; -import http from 'http'; -import fs from 'fs'; -const endpoints = { - home: '/', - compress: '/compress', -}; - -const compMethods = { - gzip: 'gzip', - deflate: 'deflate', - brotli: 'br', -}; +const http = require('http'); +const zlib = require('zlib'); +const { validate } = require('./validate'); +const { endpoints, index, compMethods } = require('./constants'); +const Busboy = require('busboy'); -const index = ` - - - - - - Compression app - - -
- - - - - -
- - -`; - -const processor = { - [endpoints.home]: (res) => { - res.setHeader('Content-Type', 'text/html'); - res.end(index); - }, - [endpoints.compress]: (req, res) => { - const fileStream = fs.createReadStream(req); +function handleCompress(req, res, comp) { + const bb = Busboy({ headers: req.headers }); - fileStream.on('data', (chunk) => { - res.write(chunk); - }); - }, -}; + const compressors = { + [compMethods.gzip]: zlib.createGzip, + [compMethods.deflate]: zlib.createDeflate, + [compMethods.brotli]: zlib.createBrotliCompress, + }; -function createServer() { - return http.createServer((req, res) => { - const validated = validate(req); + const compressor = compressors[comp]?.(); - if (!validated.ok) { - res.statusCode = validated.statusCode; - res.end(validated.message); - } + if (!compressor) { + res.statusCode = 400; - processor[validated.pathname](req); - }); -} + return res.end('Unsupported compression method'); + } -module.exports = { - createServer, -}; + let fileHandled = false; -const getValidationError = (statusCode, message) => { - return { - ok: false, - statusCode: statusCode, - message: message, + const err = () => { + res.statusCode = 500; + res.end('Server Error'); }; -}; -const validate = (req) => { - const url = new URL(req.url, `http://${req.headers.host}`); + bb.on('file', (name, file, { filename }) => { + fileHandled = true; - if (!Object.values(endpoints).includes(url.pathname)) { - return getValidationError(404, `Endpoint doesn't exist: ${url.pathname}`); - } + const finName = `${filename}.${comp}`; - if (url.pathname === endpoints.compress) { - if (req.method !== 'POST') { - return getValidationError( - 400, - `Bad request: unsupported method ${req.method}`, - ); - } + res.statusCode = 200; + res.setHeader('Content-Type', 'application/octet-stream'); + + res.setHeader('Content-Disposition', `attachment; filename="${finName}"`); - if (!req.file || !req.compressionType) { - return getValidationError(400, `Invalid form`); + file + .on('error', err) + .pipe(compressor) + .on('error', err) + .pipe(res) + .on('error', err); + }); + + bb.on('finish', () => { + if (!fileHandled) { + res.statusCode = 400; + res.end('No file provided'); } + }); + + bb.on('close', () => bb.destroy()); + + req.pipe(bb); +} - if (!Object.keys(compMethods).includes(req.compressionType)) { - return getValidationError(400, `Unsupported compression method`); +const createProcessor = (req, res, params = null) => ({ + [endpoints.home]: () => { + res.statusCode = 200; + res.setHeader('Content-Type', 'text/html'); + res.end(index); + }, + [endpoints.compress]: () => handleCompress(req, res, params), +}); + +function createServer() { + return http.createServer((req, res) => { + const val = validate(req); + + if (!val.ok) { + res.statusCode = val.statusCode; + res.end(val.message); + + return; } - } - return { - ok: true, - pathname: url.pathname, - }; + createProcessor(req, res, val.compression)[val.pathname](); + }); +} + +module.exports = { + createServer, }; diff --git a/src/index.html b/src/index.html deleted file mode 100644 index e69de29..0000000 diff --git a/src/validate.js b/src/validate.js new file mode 100644 index 0000000..51d56e9 --- /dev/null +++ b/src/validate.js @@ -0,0 +1,51 @@ +'use strict'; + +const { endpoints, htmlNames, compMethods } = require('./constants'); + +const getValidationError = (statusCode, message) => { + return { + ok: false, + statusCode: statusCode, + message: message, + }; +}; + +const validate = (req) => { + const url = new URL(req.url, `http://${req.headers.host}`); + + if (!Object.values(endpoints).includes(url.pathname)) { + return getValidationError(404, `Endpoint doesn't exist: ${url.pathname}`); + } + + if (url.pathname === endpoints.compress) { + if (req.method !== 'POST') { + return getValidationError( + 400, + `Bad request: unsupported method ${req.method}`, + ); + } + + const compression = url.searchParams.get(htmlNames.comp); + + if (!compression) { + return getValidationError(400, `Expected compression method`); + } + + if (!Object.values(compMethods).includes(compression)) { + return getValidationError(400, `Unsupported compression method`); + } + + return { + ok: true, + pathname: url.pathname, + compression: compression, + }; + } + + return { + ok: true, + pathname: url.pathname, + }; +}; + +module.exports = { validate }; From ffe5c8be33b4dc865efbaf81f8e03e9e55d359e6 Mon Sep 17 00:00:00 2001 From: Bespalov Tymofii Date: Wed, 21 Jan 2026 11:41:21 +0100 Subject: [PATCH 3/6] processing --- src/constants.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/constants.js b/src/constants.js index 7104c14..df23527 100644 --- a/src/constants.js +++ b/src/constants.js @@ -48,10 +48,10 @@ const index = ` - From 389dbc88841b02c072f3ae6e2e478121a8a43735 Mon Sep 17 00:00:00 2001 From: Bespalov Tymofii Date: Wed, 21 Jan 2026 13:03:13 +0100 Subject: [PATCH 4/6] done --- src/constants.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/constants.js b/src/constants.js index df23527..110b4b2 100644 --- a/src/constants.js +++ b/src/constants.js @@ -26,6 +26,12 @@ const index = ` +
+ -