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..1b7f8ad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,10 +9,13 @@ "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", - "@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 +66,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 +1491,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 +1561,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 +1589,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 +1602,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 +1615,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 +1677,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 +1692,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 +1879,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 +2221,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 +2666,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 +2709,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001640", "electron-to-chromium": "^1.4.820", @@ -2739,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", @@ -3455,6 +3465,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 +3521,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 +3612,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 +3690,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 +3741,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 +3765,7 @@ "url": "https://feross.org/support" } ], + "peer": true, "peerDependencies": { "eslint": ">=5.0.0" } @@ -5175,6 +5191,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 +7545,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" }, @@ -8040,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", @@ -8465,8 +8491,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..9f3cce9 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", @@ -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..110b4b2 --- /dev/null +++ b/src/constants.js @@ -0,0 +1,67 @@ +'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 1cf1dda..53f3d6b 100644 --- a/src/createServer.js +++ b/src/createServer.js @@ -1,8 +1,93 @@ 'use strict'; +const http = require('http'); +const zlib = require('zlib'); +const { validate } = require('./validate'); +const { endpoints, index, compMethods } = require('./constants'); +const Busboy = require('busboy'); + +function handleCompress(req, res, comp) { + const bb = Busboy({ headers: req.headers }); + + const compressors = { + [compMethods.gzip]: zlib.createGzip, + [compMethods.deflate]: zlib.createDeflate, + [compMethods.brotli]: zlib.createBrotliCompress, + }; + + const compressor = compressors[comp]?.(); + + if (!compressor) { + res.statusCode = 400; + + return res.end('Unsupported compression method'); + } + + let fileHandled = false; + + const err = () => { + res.statusCode = 500; + res.end('Server Error'); + }; + + bb.on('file', (name, file, { filename }) => { + fileHandled = true; + + const ext = { + [compMethods.gzip]: 'gz', + [compMethods.deflate]: 'dfl', + [compMethods.brotli]: 'br', + }; + + const finName = `${filename}.${ext[comp]}`; + + res.statusCode = 200; + res.setHeader('Content-Type', 'application/octet-stream'); + + res.setHeader('Content-Disposition', `attachment; filename="${finName}"`); + + 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); +} + +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() { - /* Write your code here */ - // Return instance of http.Server class + return http.createServer((req, res) => { + const val = validate(req); + + if (!val.ok) { + res.statusCode = val.statusCode; + res.end(val.message); + + return; + } + + createProcessor(req, res, val.compression)[val.pathname](); + }); } module.exports = { 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 };