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/index.html b/index.html
new file mode 100644
index 0000000..e39c001
--- /dev/null
+++ b/index.html
@@ -0,0 +1,56 @@
+
+
+
+
+
+ Compression App
+
+
+
+ Compression App
+
+
+
+
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..a008965 100644
--- a/src/createServer.js
+++ b/src/createServer.js
@@ -1,8 +1,173 @@
'use strict';
+const zlib = require('node:zlib');
+const http = require('node:http');
+const { Transform } = require('node:stream');
+
+class MultipartStream extends Transform {
+ constructor(boundaryString) {
+ super();
+ this.buffer = Buffer.alloc(0);
+ this.isHeaderParsed = false;
+ this.boundaryEnd = Buffer.from(`\r\n--${boundaryString}`);
+ }
+
+ _transform(chunk, encoding, callback) {
+ this.buffer = Buffer.concat([this.buffer, chunk]);
+
+ if (!this.isHeaderParsed) {
+ const fileIdx = this.buffer.indexOf('filename="');
+
+ if (fileIdx !== -1) {
+ const headerEnd = this.buffer.indexOf('\r\n\r\n', fileIdx);
+
+ if (headerEnd !== -1) {
+ const startIdx = headerEnd + 4;
+ const str = this.buffer.toString('utf8');
+
+ const cMatch = str.match(
+ /name="compressionType"\r\n\r\n(gzip|deflate|br)/,
+ );
+ const fMatch = str.match(/filename="([^"]+)"/);
+
+ if (cMatch && fMatch) {
+ this.isHeaderParsed = true;
+
+ this.emit('fileReady', {
+ compressionType: cMatch[1],
+ filename: fMatch[1],
+ });
+ this.buffer = this.buffer.subarray(startIdx);
+ }
+ }
+ }
+
+ if (!this.isHeaderParsed && this.buffer.length > 1024 * 1024) {
+ this.emit('error', new Error('Invalid form data or headers too large'));
+
+ return callback();
+ }
+ }
+
+ // Шаг 2: Стримим чистый файл
+ if (this.isHeaderParsed) {
+ const endIdx = this.buffer.indexOf(this.boundaryEnd);
+
+ if (endIdx !== -1) {
+ this.push(this.buffer.subarray(0, endIdx));
+ this.buffer = Buffer.alloc(0);
+ this.push(null);
+ } else {
+ if (this.buffer.length > this.boundaryEnd.length) {
+ const safeLen = this.buffer.length - this.boundaryEnd.length;
+
+ this.push(this.buffer.subarray(0, safeLen));
+ this.buffer = this.buffer.subarray(safeLen);
+ }
+ }
+ }
+
+ callback();
+ }
+
+ _flush(callback) {
+ if (!this.isHeaderParsed) {
+ this.emit('error', new Error('Missing file or compression type'));
+ }
+ callback();
+ }
+}
+
function createServer() {
- /* Write your code here */
- // Return instance of http.Server class
+ const server = http.createServer();
+
+ server.on('request', (req, res) => {
+ const url = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
+ const reqPath = url.pathname;
+
+ if (reqPath === '/' && req.method === 'GET') {
+ res.statusCode = 200;
+
+ return res.end();
+ }
+
+ if (reqPath !== '/compress') {
+ res.statusCode = 404;
+
+ return res.end('Not Found');
+ }
+
+ if (req.method !== 'POST') {
+ res.statusCode = 400;
+
+ return res.end('Bad Request');
+ }
+
+ // Достаем уникальную границу (boundary) из заголовков запроса
+ const boundaryMatch = req.headers['content-type']?.match(/boundary=(.*)/);
+
+ if (!boundaryMatch) {
+ res.statusCode = 400;
+
+ return res.end('Bad Request: Not a multipart form');
+ }
+
+ const boundaryStr = boundaryMatch[1].replace(/"/g, '');
+
+ const formParser = new MultipartStream(boundaryStr);
+
+ formParser.on('fileReady', ({ compressionType, filename }) => {
+ let resFile = null;
+
+ switch (compressionType) {
+ case 'gzip':
+ resFile = zlib.createGzip();
+ break;
+ case 'deflate':
+ resFile = zlib.createDeflate();
+ break;
+ case 'br':
+ resFile = zlib.createBrotliCompress();
+ break;
+ }
+
+ res.statusCode = 200;
+
+ res.setHeader(
+ 'Content-Disposition',
+ `attachment; filename=${filename}.${compressionType}`,
+ );
+
+ resFile.on('error', () => {
+ if (!res.headersSent) {
+ res.statusCode = 500;
+ }
+ res.end('Compression Error');
+ });
+
+ // Соединяем трубы: Парсер -> Архиватор -> Клиент
+ formParser.pipe(resFile).pipe(res);
+ });
+
+ formParser.on('error', (err) => {
+ if (!res.headersSent) {
+ res.statusCode = 400;
+ res.end(`Bad Request: ${err.message}`);
+ }
+ });
+
+ // Начинаем заливать данные из запроса в наш парсер
+ req.pipe(formParser);
+
+ req.on('error', () => {
+ if (!res.headersSent) {
+ res.statusCode = 500;
+ res.end('Request Error');
+ }
+ });
+ });
+
+ return server;
}
module.exports = {