From ee3697adbe6bf12ea49287ba44a2320aec9f78af Mon Sep 17 00:00:00 2001 From: Artem Skichko Date: Tue, 12 May 2026 21:02:02 +0200 Subject: [PATCH 1/4] Solution --- .github/workflows/test.yml-template | 23 +++ package-lock.json | 31 +++- package.json | 5 +- src/createServer.js | 213 +++++++++++++++++++++++++++- 4 files changed, 262 insertions(+), 10 deletions(-) create mode 100644 .github/workflows/test.yml-template 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..7fe4887 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", @@ -1487,10 +1490,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", @@ -2739,6 +2743,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", @@ -8040,6 +8055,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 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/createServer.js b/src/createServer.js index 1cf1dda..1864da8 100644 --- a/src/createServer.js +++ b/src/createServer.js @@ -1,10 +1,213 @@ +/* eslint-disable no-console */ 'use strict'; -function createServer() { - /* Write your code here */ - // Return instance of http.Server class +const http = require('http'); +const zlib = require('zlib'); + +function parseMultipart(req, callback) { + const contentType = req.headers['content-type'] || ''; + const boundaryMatch = contentType.match(/boundary=(.+)$/); + + if (!boundaryMatch) { + return callback(new Error('No boundary'), null); + } + + const boundary = boundaryMatch[1]; + const chunks = []; + + req.on('data', (chunk) => chunks.push(chunk)); + + req.on('end', () => { + const buffer = Buffer.concat(chunks); + const result = { fields: {}, file: null }; + + // Розбиваємо на частини по boundary + const delimiter = Buffer.from(`\r\n--${boundary}`); + const parts = splitBuffer(buffer, delimiter); + + for (const part of parts) { + // Заголовки та тіло частини розділені \r\n\r\n + const headerEnd = indexOfSequence(part, Buffer.from('\r\n\r\n')); + + if (headerEnd === -1) { + continue; + } + + const headerStr = part.slice(0, headerEnd).toString(); + // Тіло — все після \r\n\r\n + const body = part.slice(headerEnd + 4); + + // Ім'я поля з Content-Disposition + const nameMatch = headerStr.match(/name="([^"]+)"/); + + if (!nameMatch) { + continue; + } + + const fieldName = nameMatch[1]; + const filenameMatch = headerStr.match(/filename="([^"]+)"/); + + if (filenameMatch) { + // Це файлове поле + result.file = { + filename: filenameMatch[1], + data: body, + }; + } else { + // Звичайне текстове поле + result.fields[fieldName] = body.toString(); + } + } + + callback(null, result); + }); + + req.on('error', (err) => callback(err, null)); +} + +// Розбиває Buffer на масив Buffer-ів по розділювачу +function splitBuffer(buf, delimiter) { + const parts = []; + let start = 0; + let pos = 0; + + while (pos <= buf.length - delimiter.length) { + if (buf.slice(pos, pos + delimiter.length).equals(delimiter)) { + parts.push(buf.slice(start, pos)); + pos += delimiter.length; + start = pos; + } else { + pos++; + } + } + + parts.push(buf.slice(start)); + + return parts; } -module.exports = { - createServer, +// Шукає позицію послідовності байт у Buffer +function indexOfSequence(buf, seq) { + for (let i = 0; i <= buf.length - seq.length; i++) { + if (buf.slice(i, i + seq.length).equals(seq)) { + return i; + } + } + + return -1; +} + +const COMPRESSION_MAP = { + gzip: { create: () => zlib.createGzip(), ext: 'gz' }, + deflate: { create: () => zlib.createDeflate(), ext: 'dfl' }, + br: { create: () => zlib.createBrotliCompress(), ext: 'br' }, }; + +function createServer() { + const server = http.createServer((req, res) => { + const { method, url } = req; + + // GET / → повертаємо HTML форму + if (method === 'GET' && url === '/') { + res.writeHead(200, { 'Content-Type': 'text/html' }); + + res.end(` + + + +
+ + + +
+ + + `); + + return; + } + + // GET /compress → 400 + if (method === 'GET' && url === '/compress') { + res.writeHead(400); + res.end(); + + return; + } + + // Будь-який запит на невідомий шлях → 404 + if (url !== '/compress') { + res.writeHead(404); + res.end(); + + return; + } + + // POST /compress → основна логіка + if (method === 'POST' && url === '/compress') { + parseMultipart(req, (err, parsed) => { + if (err || !parsed) { + res.writeHead(400); + + return res.end(); + } + + const { file, fields } = parsed; + const compressionType = (fields.compressionType || '').trim(); + + // Крок 4: Валідація — файл і тип стиснення обов'язкові + if (!file) { + res.writeHead(400); + + return res.end(); + } + + if (!compressionType) { + res.writeHead(400); + + return res.end(); + } + + if (!COMPRESSION_MAP[compressionType]) { + res.writeHead(400); + + return res.end(); + } + + // Крок 5: Формуємо ім'я вихідного файлу + const { create, ext } = COMPRESSION_MAP[compressionType]; + const outputFilename = `${file.filename}.${ext}`; + + res.writeHead(200, { + 'Content-Disposition': `attachment; filename=${outputFilename}`, + }); + + // Крок 6: Стискаємо через Stream + // file.data — це Buffer; перетворюємо на Readable stream + const { Readable } = require('stream'); + const readable = new Readable(); + + readable.push(file.data); + readable.push(null); + + const compressor = create(); + + readable.pipe(compressor).pipe(res); + }); + + return; + } + + // Fallback + res.writeHead(404); + res.end(); + }); + + return server; +} + +module.exports = { createServer }; From 6bf5c8db3586ba7132423f359e1c4df29170f385 Mon Sep 17 00:00:00 2001 From: Artem Skichko <118510164+Artemida1609@users.noreply.github.com> Date: Tue, 12 May 2026 21:21:19 +0200 Subject: [PATCH 2/4] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/createServer.js | 82 +++++++++++++++++++++------------------------ 1 file changed, 39 insertions(+), 43 deletions(-) diff --git a/src/createServer.js b/src/createServer.js index 1864da8..c898456 100644 --- a/src/createServer.js +++ b/src/createServer.js @@ -3,66 +3,62 @@ const http = require('http'); const zlib = require('zlib'); +const Busboy = require('busboy'); function parseMultipart(req, callback) { const contentType = req.headers['content-type'] || ''; - const boundaryMatch = contentType.match(/boundary=(.+)$/); - if (!boundaryMatch) { + if (!contentType.toLowerCase().startsWith('multipart/form-data')) { return callback(new Error('No boundary'), null); } - const boundary = boundaryMatch[1]; - const chunks = []; + const result = { fields: {}, file: null }; + let callbackCalled = false; - req.on('data', (chunk) => chunks.push(chunk)); - - req.on('end', () => { - const buffer = Buffer.concat(chunks); - const result = { fields: {}, file: null }; - - // Розбиваємо на частини по boundary - const delimiter = Buffer.from(`\r\n--${boundary}`); - const parts = splitBuffer(buffer, delimiter); - - for (const part of parts) { - // Заголовки та тіло частини розділені \r\n\r\n - const headerEnd = indexOfSequence(part, Buffer.from('\r\n\r\n')); - - if (headerEnd === -1) { - continue; - } + const done = (err, value) => { + if (callbackCalled) { + return; + } - const headerStr = part.slice(0, headerEnd).toString(); - // Тіло — все після \r\n\r\n - const body = part.slice(headerEnd + 4); + callbackCalled = true; + callback(err, value); + }; - // Ім'я поля з Content-Disposition - const nameMatch = headerStr.match(/name="([^"]+)"/); + let busboy; - if (!nameMatch) { - continue; - } + try { + busboy = Busboy({ headers: req.headers }); + } catch (err) { + return done(err, null); + } - const fieldName = nameMatch[1]; - const filenameMatch = headerStr.match(/filename="([^"]+)"/); + busboy.on('field', (fieldName, value) => { + result.fields[fieldName] = value; + }); - if (filenameMatch) { - // Це файлове поле - result.file = { - filename: filenameMatch[1], - data: body, + busboy.on('file', (fieldName, file, info) => { + const fileInfo = typeof info === 'object' && info !== null + ? info + : { + filename: arguments[2], + encoding: arguments[3], + mimeType: arguments[4], }; - } else { - // Звичайне текстове поле - result.fields[fieldName] = body.toString(); - } - } - callback(null, result); + result.file = { + fieldName, + filename: fileInfo.filename, + encoding: fileInfo.encoding, + mimeType: fileInfo.mimeType, + stream: file, + }; }); - req.on('error', (err) => callback(err, null)); + busboy.on('error', (err) => done(err, null)); + req.on('error', (err) => done(err, null)); + busboy.on('finish', () => done(null, result)); + + req.pipe(busboy); } // Розбиває Buffer на масив Buffer-ів по розділювачу From 5ab655dc67793d459ffb44bb8cae69203dd6c783 Mon Sep 17 00:00:00 2001 From: Artem Skichko Date: Tue, 12 May 2026 21:51:54 +0200 Subject: [PATCH 3/4] fix --- .github/workflows/test.yml-template | 23 ----------------------- package-lock.json | 3 --- 2 files changed, 26 deletions(-) delete mode 100644 .github/workflows/test.yml-template diff --git a/.github/workflows/test.yml-template b/.github/workflows/test.yml-template deleted file mode 100644 index bb13dfc..0000000 --- a/.github/workflows/test.yml-template +++ /dev/null @@ -1,23 +0,0 @@ -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 7fe4887..46b74df 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,9 +9,6 @@ "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", From ecf1e543483e9c458be919c2cd0f18d3acde67d7 Mon Sep 17 00:00:00 2001 From: Artem Skichko Date: Tue, 12 May 2026 21:55:40 +0200 Subject: [PATCH 4/4] fix --- src/createServer.js | 82 ++++++++++++++++++++++++--------------------- 1 file changed, 43 insertions(+), 39 deletions(-) diff --git a/src/createServer.js b/src/createServer.js index c898456..1864da8 100644 --- a/src/createServer.js +++ b/src/createServer.js @@ -3,62 +3,66 @@ const http = require('http'); const zlib = require('zlib'); -const Busboy = require('busboy'); function parseMultipart(req, callback) { const contentType = req.headers['content-type'] || ''; + const boundaryMatch = contentType.match(/boundary=(.+)$/); - if (!contentType.toLowerCase().startsWith('multipart/form-data')) { + if (!boundaryMatch) { return callback(new Error('No boundary'), null); } - const result = { fields: {}, file: null }; - let callbackCalled = false; + const boundary = boundaryMatch[1]; + const chunks = []; - const done = (err, value) => { - if (callbackCalled) { - return; - } + req.on('data', (chunk) => chunks.push(chunk)); - callbackCalled = true; - callback(err, value); - }; + req.on('end', () => { + const buffer = Buffer.concat(chunks); + const result = { fields: {}, file: null }; - let busboy; + // Розбиваємо на частини по boundary + const delimiter = Buffer.from(`\r\n--${boundary}`); + const parts = splitBuffer(buffer, delimiter); - try { - busboy = Busboy({ headers: req.headers }); - } catch (err) { - return done(err, null); - } + for (const part of parts) { + // Заголовки та тіло частини розділені \r\n\r\n + const headerEnd = indexOfSequence(part, Buffer.from('\r\n\r\n')); - busboy.on('field', (fieldName, value) => { - result.fields[fieldName] = value; - }); + if (headerEnd === -1) { + continue; + } + + const headerStr = part.slice(0, headerEnd).toString(); + // Тіло — все після \r\n\r\n + const body = part.slice(headerEnd + 4); - busboy.on('file', (fieldName, file, info) => { - const fileInfo = typeof info === 'object' && info !== null - ? info - : { - filename: arguments[2], - encoding: arguments[3], - mimeType: arguments[4], + // Ім'я поля з Content-Disposition + const nameMatch = headerStr.match(/name="([^"]+)"/); + + if (!nameMatch) { + continue; + } + + const fieldName = nameMatch[1]; + const filenameMatch = headerStr.match(/filename="([^"]+)"/); + + if (filenameMatch) { + // Це файлове поле + result.file = { + filename: filenameMatch[1], + data: body, }; + } else { + // Звичайне текстове поле + result.fields[fieldName] = body.toString(); + } + } - result.file = { - fieldName, - filename: fileInfo.filename, - encoding: fileInfo.encoding, - mimeType: fileInfo.mimeType, - stream: file, - }; + callback(null, result); }); - busboy.on('error', (err) => done(err, null)); - req.on('error', (err) => done(err, null)); - busboy.on('finish', () => done(null, result)); - - req.pipe(busboy); + req.on('error', (err) => callback(err, null)); } // Розбиває Buffer на масив Buffer-ів по розділювачу