From 679a81d3224220a5142bde1a5d979216fa03c84f Mon Sep 17 00:00:00 2001 From: Vasyl Pryimak Date: Sun, 22 Feb 2026 23:02:31 +0200 Subject: [PATCH 1/4] first commit --- .github/workflows/test.yml-template | 23 ++++++ package-lock.json | 9 ++- package.json | 2 +- src/createServer.js | 109 +++++++++++++++++++++++++++- src/index.html | 36 +++++++++ 5 files changed, 172 insertions(+), 7 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..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/src/createServer.js b/src/createServer.js index 1cf1dda..abf2948 100644 --- a/src/createServer.js +++ b/src/createServer.js @@ -1,8 +1,113 @@ 'use strict'; +/* eslint-disable no-console */ + +const http = require('http'); +const { readFile } = require('fs'); +const path = require('path'); +const zlib = require('zlib'); + +const compressTypes = { + gzip: { + stream: zlib.createGzip, + extention: '.gz', + }, + deflate: { + stream: zlib.createDeflate, + extention: '.dfl', + }, + br: { + stream: zlib.createBrotliCompress, + extention: '.br', + }, +}; + function createServer() { - /* Write your code here */ - // Return instance of http.Server class + const server = http.createServer((req, res) => { + if (req.url === '/') { + res.setHeader('Content-Type', 'text/html'); + + readFile(path.resolve(__dirname, 'index.html'), 'utf8', (err, data) => { + if (err) { + console.error(err); + + return; + } + + return res.end(data); + }); + } else if (req.url === '/compress') { + if (req.method === 'POST') { + let startedStream = false; + let fileName = ''; + let compressToType = ''; + let compressor; + + req.on('data', (chunk) => { + if (!startedStream) { + const chunkStr = chunk.toString(); + const startIdx = chunkStr.lastIndexOf('\r\n\r\n'); + const headers = chunkStr.slice(0, startIdx); + + const compressionMatch = headers.match( + /name="compressionType"\r\n\r\n([^\r\n]+)/, + ); + const fileNameMatch = headers.match(/filename="([^"]+)"/); + + if (compressionMatch) { + compressToType = compressionMatch[1]; + } + + if (fileNameMatch) { + fileName = fileNameMatch[1]; + } + + if ( + !compressToType || + !compressTypes.hasOwnProperty(compressToType) || + !fileName + ) { + res.statusCode = 400; + res.end('Provide file and valid compression type!'); + + return; + } + + const fileChunk = chunk.slice(startIdx + 4); + + startedStream = true; + + res.writeHead(200, { + 'Content-Type': 'application/octet-stream', + 'Content-Disposition': `attachment; filename=${fileName}${compressTypes[compressToType].extention}`, + }); + + compressor = compressTypes[compressToType].stream(); + compressor.pipe(res); + compressor.write(fileChunk); + } else { + compressor.write(chunk); + } + }); + + req.on('end', () => { + if (compressor) { + compressor.end(); + } + }); + } else { + res.statusCode = 400; + + return res.end(); + } + } else { + res.statusCode = 404; + + return res.end(); + } + }); + + return server; } module.exports = { diff --git a/src/index.html b/src/index.html new file mode 100644 index 0000000..4ee0d26 --- /dev/null +++ b/src/index.html @@ -0,0 +1,36 @@ + + + + + + Compression-App + + +

Hello world!

+
+
+
+ + +
+ +
+ + +
+
+ + +
+ + From 6c9cb7e0ba2814c37bbbfb80b1aac0ca3f8eab58 Mon Sep 17 00:00:00 2001 From: Vasyl Pryimak Date: Tue, 24 Feb 2026 12:13:10 +0200 Subject: [PATCH 2/4] first commit --- src/createServer.js | 192 ++++++++++++++++++++++---------------------- src/index.html | 10 +-- 2 files changed, 100 insertions(+), 102 deletions(-) diff --git a/src/createServer.js b/src/createServer.js index abf2948..73515ee 100644 --- a/src/createServer.js +++ b/src/createServer.js @@ -1,115 +1,113 @@ 'use strict'; -/* eslint-disable no-console */ - const http = require('http'); -const { readFile } = require('fs'); -const path = require('path'); const zlib = require('zlib'); -const compressTypes = { - gzip: { - stream: zlib.createGzip, - extention: '.gz', - }, - deflate: { - stream: zlib.createDeflate, - extention: '.dfl', - }, - br: { - stream: zlib.createBrotliCompress, - extention: '.br', - }, -}; - function createServer() { - const server = http.createServer((req, res) => { - if (req.url === '/') { - res.setHeader('Content-Type', 'text/html'); + return http.createServer((req, res) => { + if (req.method === 'GET' && req.url === '/') { + res.statusCode = 200; + res.end('OK'); - readFile(path.resolve(__dirname, 'index.html'), 'utf8', (err, data) => { - if (err) { - console.error(err); + return; + } - return; - } + if (req.url !== '/compress') { + res.statusCode = 404; + res.end(); - return res.end(data); - }); - } else if (req.url === '/compress') { - if (req.method === 'POST') { - let startedStream = false; - let fileName = ''; - let compressToType = ''; - let compressor; - - req.on('data', (chunk) => { - if (!startedStream) { - const chunkStr = chunk.toString(); - const startIdx = chunkStr.lastIndexOf('\r\n\r\n'); - const headers = chunkStr.slice(0, startIdx); - - const compressionMatch = headers.match( - /name="compressionType"\r\n\r\n([^\r\n]+)/, - ); - const fileNameMatch = headers.match(/filename="([^"]+)"/); - - if (compressionMatch) { - compressToType = compressionMatch[1]; - } - - if (fileNameMatch) { - fileName = fileNameMatch[1]; - } - - if ( - !compressToType || - !compressTypes.hasOwnProperty(compressToType) || - !fileName - ) { - res.statusCode = 400; - res.end('Provide file and valid compression type!'); - - return; - } - - const fileChunk = chunk.slice(startIdx + 4); - - startedStream = true; - - res.writeHead(200, { - 'Content-Type': 'application/octet-stream', - 'Content-Disposition': `attachment; filename=${fileName}${compressTypes[compressToType].extention}`, - }); - - compressor = compressTypes[compressToType].stream(); - compressor.pipe(res); - compressor.write(fileChunk); - } else { - compressor.write(chunk); - } - }); + return; + } + + if (req.method !== 'POST') { + res.statusCode = 400; + res.end(); + + return; + } + + const contentType = req.headers['content-type'] || ''; + + if (!contentType.includes('multipart/form-data')) { + res.statusCode = 400; + res.end(); + + return; + } + + const boundary = '--' + contentType.split('boundary=')[1]; + + const chunks = []; + + req.on('data', (chunk) => chunks.push(chunk)); + + req.on('end', () => { + const body = Buffer.concat(chunks).toString('binary'); + + const parts = body.split(boundary).filter((p) => p.trim()); + + let fileBuffer = null; + let filename = null; + let compressionType = null; - req.on('end', () => { - if (compressor) { - compressor.end(); + for (const part of parts) { + if (part.includes('name="file"')) { + const headerEnd = part.indexOf('\r\n\r\n'); + + const headers = part.slice(0, headerEnd); + const content = part.slice(headerEnd + 4, part.lastIndexOf('\r\n')); + + const match = headers.match(/filename="(.+?)"/); + + if (match) { + filename = match[1]; } - }); - } else { + + fileBuffer = Buffer.from(content, 'binary'); + } + + if (part.includes('name="compressionType"')) { + const headerEnd = part.indexOf('\r\n\r\n'); + + const value = part + .slice(headerEnd + 4) + .replace(/\r\n$/, '') + .trim(); + + compressionType = value; + } + } + + const compressors = { + gzip: zlib.gzipSync, + deflate: zlib.deflateSync, + br: zlib.brotliCompressSync, + }; + + if (!fileBuffer || !compressionType || !compressors[compressionType]) { res.statusCode = 400; + res.end(); - return res.end(); + return; } - } else { - res.statusCode = 404; - return res.end(); - } - }); + const compressed = compressors[compressionType](fileBuffer); - return server; + res.statusCode = 200; + + res.setHeader( + 'Content-Disposition', + `attachment; filename=${filename}.${compressionType}`, + ); + + res.end(compressed); + }); + + req.on('error', () => { + res.statusCode = 500; + res.end(); + }); + }); } -module.exports = { - createServer, -}; +module.exports = { createServer }; diff --git a/src/index.html b/src/index.html index 4ee0d26..0c368e0 100644 --- a/src/index.html +++ b/src/index.html @@ -15,6 +15,11 @@

Hello world!

style="display: flex; flex-direction: column; gap: 10px" >
+
+ + +
+
- -
- - -
From 70fe7273154e6546eb8a0d9da05cfcdd50a5e000 Mon Sep 17 00:00:00 2001 From: Vasyl Pryimak Date: Tue, 24 Feb 2026 12:20:24 +0200 Subject: [PATCH 3/4] second commit --- package-lock.json | 22 +++++++ package.json | 3 + src/createServer.js | 145 ++++++++++++++++++++++---------------------- src/index.html | 36 ----------- 4 files changed, 98 insertions(+), 108 deletions(-) delete mode 100644 src/index.html diff --git a/package-lock.json b/package-lock.json index 3650660..7fe4887 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", @@ -2740,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", @@ -8041,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 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/createServer.js b/src/createServer.js index 73515ee..e10b124 100644 --- a/src/createServer.js +++ b/src/createServer.js @@ -1,112 +1,113 @@ 'use strict'; const http = require('http'); +const Busboy = require('busboy'); const zlib = require('zlib'); +const compressionMap = { + gzip: { + stream: () => zlib.createGzip(), + ext: 'gz', + }, + deflate: { + stream: () => zlib.createDeflate(), + ext: 'dfl', + }, + br: { + stream: () => zlib.createBrotliCompress(), + ext: 'br', + }, +}; + function createServer() { return http.createServer((req, res) => { if (req.method === 'GET' && req.url === '/') { - res.statusCode = 200; + res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end('OK'); return; } - if (req.url !== '/compress') { - res.statusCode = 404; - res.end(); - - return; - } - - if (req.method !== 'POST') { - res.statusCode = 400; + if (req.method !== 'POST' || req.url !== '/compress') { + res.writeHead(req.method === 'GET' ? 404 : 400); res.end(); return; } - const contentType = req.headers['content-type'] || ''; - - if (!contentType.includes('multipart/form-data')) { - res.statusCode = 400; - res.end(); - - return; - } + const busboy = Busboy({ headers: req.headers }); - const boundary = '--' + contentType.split('boundary=')[1]; + let fileStream = null; + let filename = null; + let compressionType = null; + let compressionStream = null; + let responded = false; - const chunks = []; - - req.on('data', (chunk) => chunks.push(chunk)); - - req.on('end', () => { - const body = Buffer.concat(chunks).toString('binary'); + function tryStartPipe() { + if (!fileStream || !compressionType || responded) { + return; + } - const parts = body.split(boundary).filter((p) => p.trim()); + const config = compressionMap[compressionType]; - let fileBuffer = null; - let filename = null; - let compressionType = null; + if (!config) { + responded = true; + res.writeHead(400); + res.end(); - for (const part of parts) { - if (part.includes('name="file"')) { - const headerEnd = part.indexOf('\r\n\r\n'); + return; + } - const headers = part.slice(0, headerEnd); - const content = part.slice(headerEnd + 4, part.lastIndexOf('\r\n')); + compressionStream = config.stream(); - const match = headers.match(/filename="(.+?)"/); + res.setHeader( + 'Content-Disposition', + `attachment; filename=${filename}.${config.ext}`, + ); - if (match) { - filename = match[1]; + fileStream + .pipe(compressionStream) + .pipe(res) + .on('error', () => { + if (!responded) { + responded = true; + res.writeHead(500); + res.end(); } + }); + } - fileBuffer = Buffer.from(content, 'binary'); - } - - if (part.includes('name="compressionType"')) { - const headerEnd = part.indexOf('\r\n\r\n'); - - const value = part - .slice(headerEnd + 4) - .replace(/\r\n$/, '') - .trim(); - - compressionType = value; - } - } - - const compressors = { - gzip: zlib.gzipSync, - deflate: zlib.deflateSync, - br: zlib.brotliCompressSync, - }; - - if (!fileBuffer || !compressionType || !compressors[compressionType]) { - res.statusCode = 400; - res.end(); + busboy.on('file', (name, stream, info) => { + if (name !== 'file') { + stream.resume(); return; } - const compressed = compressors[compressionType](fileBuffer); + filename = info.filename; + fileStream = stream; - res.statusCode = 200; - - res.setHeader( - 'Content-Disposition', - `attachment; filename=${filename}.${compressionType}`, - ); + tryStartPipe(); + }); - res.end(compressed); + busboy.on('field', (name, value) => { + if (name === 'compressionType') { + compressionType = value; + tryStartPipe(); + } }); - req.on('error', () => { - res.statusCode = 500; - res.end(); + busboy.on('finish', () => { + if (!fileStream || !compressionType) { + if (!responded) { + responded = true; + res.writeHead(400); + res.end(); + } + } }); + + req.pipe(busboy); }); } diff --git a/src/index.html b/src/index.html deleted file mode 100644 index 0c368e0..0000000 --- a/src/index.html +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - Compression-App - - -

Hello world!

-
-
-
- - -
- -
- - -
-
- - -
- - From 0ac92778e1e167dc69585481e059fae5f5d65085 Mon Sep 17 00:00:00 2001 From: Vasyl Pryimak Date: Tue, 24 Feb 2026 16:54:55 +0200 Subject: [PATCH 4/4] third commit --- src/createServer.js | 145 ++++++++++++++++++++++---------------------- 1 file changed, 72 insertions(+), 73 deletions(-) diff --git a/src/createServer.js b/src/createServer.js index e10b124..73515ee 100644 --- a/src/createServer.js +++ b/src/createServer.js @@ -1,113 +1,112 @@ 'use strict'; const http = require('http'); -const Busboy = require('busboy'); const zlib = require('zlib'); -const compressionMap = { - gzip: { - stream: () => zlib.createGzip(), - ext: 'gz', - }, - deflate: { - stream: () => zlib.createDeflate(), - ext: 'dfl', - }, - br: { - stream: () => zlib.createBrotliCompress(), - ext: 'br', - }, -}; - function createServer() { return http.createServer((req, res) => { if (req.method === 'GET' && req.url === '/') { - res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.statusCode = 200; res.end('OK'); return; } - if (req.method !== 'POST' || req.url !== '/compress') { - res.writeHead(req.method === 'GET' ? 404 : 400); + if (req.url !== '/compress') { + res.statusCode = 404; res.end(); return; } - const busboy = Busboy({ headers: req.headers }); + if (req.method !== 'POST') { + res.statusCode = 400; + res.end(); - let fileStream = null; - let filename = null; - let compressionType = null; - let compressionStream = null; - let responded = false; + return; + } - function tryStartPipe() { - if (!fileStream || !compressionType || responded) { - return; - } + const contentType = req.headers['content-type'] || ''; - const config = compressionMap[compressionType]; + if (!contentType.includes('multipart/form-data')) { + res.statusCode = 400; + res.end(); - if (!config) { - responded = true; - res.writeHead(400); - res.end(); + return; + } - return; - } + const boundary = '--' + contentType.split('boundary=')[1]; - compressionStream = config.stream(); + const chunks = []; - res.setHeader( - 'Content-Disposition', - `attachment; filename=${filename}.${config.ext}`, - ); + req.on('data', (chunk) => chunks.push(chunk)); + + req.on('end', () => { + const body = Buffer.concat(chunks).toString('binary'); + + const parts = body.split(boundary).filter((p) => p.trim()); + + let fileBuffer = null; + let filename = null; + let compressionType = null; + + for (const part of parts) { + if (part.includes('name="file"')) { + const headerEnd = part.indexOf('\r\n\r\n'); + + const headers = part.slice(0, headerEnd); + const content = part.slice(headerEnd + 4, part.lastIndexOf('\r\n')); + + const match = headers.match(/filename="(.+?)"/); - fileStream - .pipe(compressionStream) - .pipe(res) - .on('error', () => { - if (!responded) { - responded = true; - res.writeHead(500); - res.end(); + if (match) { + filename = match[1]; } - }); - } - busboy.on('file', (name, stream, info) => { - if (name !== 'file') { - stream.resume(); + fileBuffer = Buffer.from(content, 'binary'); + } - return; + if (part.includes('name="compressionType"')) { + const headerEnd = part.indexOf('\r\n\r\n'); + + const value = part + .slice(headerEnd + 4) + .replace(/\r\n$/, '') + .trim(); + + compressionType = value; + } } - filename = info.filename; - fileStream = stream; + const compressors = { + gzip: zlib.gzipSync, + deflate: zlib.deflateSync, + br: zlib.brotliCompressSync, + }; - tryStartPipe(); - }); + if (!fileBuffer || !compressionType || !compressors[compressionType]) { + res.statusCode = 400; + res.end(); - busboy.on('field', (name, value) => { - if (name === 'compressionType') { - compressionType = value; - tryStartPipe(); + return; } - }); - busboy.on('finish', () => { - if (!fileStream || !compressionType) { - if (!responded) { - responded = true; - res.writeHead(400); - res.end(); - } - } + const compressed = compressors[compressionType](fileBuffer); + + res.statusCode = 200; + + res.setHeader( + 'Content-Disposition', + `attachment; filename=${filename}.${compressionType}`, + ); + + res.end(compressed); }); - req.pipe(busboy); + req.on('error', () => { + res.statusCode = 500; + res.end(); + }); }); }