From b30e5962160abf57daaaa2af2f4f08e436ca4867 Mon Sep 17 00:00:00 2001 From: valerij Date: Mon, 6 Jul 2026 17:04:31 +0300 Subject: [PATCH] feat: implement file compression server --- .github/workflows/test.yml-template | 23 ++++ package-lock.json | 9 +- package.json | 2 +- src/createServer.js | 156 +++++++++++++++++++++++++++- 4 files changed, 180 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..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..06dcc25 100644 --- a/src/createServer.js +++ b/src/createServer.js @@ -1,10 +1,156 @@ 'use strict'; +const http = require('http'); +const zlib = require('zlib'); +const { Readable } = require('stream'); // Додаємо модуль для створення потоку + +const htmlForm = ` + + + +
+ + + +
+ + +`; + function createServer() { - /* Write your code here */ - // Return instance of http.Server class + return http.createServer((req, res) => { + const { method, url } = req; + + // 1. Головна сторінка + if (url === '/' && method === 'GET') { + res.writeHead(200, { 'Content-Type': 'text/html' }); + res.end(htmlForm); + + return; + } + + // 2. Обробка стиснення + if (url === '/compress') { + // Вимога: 400 status code for GET request to /compress + if (method === 'GET') { + res.statusCode = 400; + res.end('Bad Request'); + + return; + } + + if (method === 'POST') { + const contentType = req.headers['content-type'] || ''; + + // Вимога: 400 status code if the form is invalid + if (!contentType.includes('multipart/form-data')) { + res.statusCode = 400; + res.end('Invalid form'); + + return; + } + + const boundaryStr = contentType.split('boundary=')[1]; + + if (!boundaryStr) { + res.statusCode = 400; + res.end('Invalid form'); + + return; + } + + const chunks = []; + + req.on('data', (chunk) => chunks.push(chunk)); + + req.on('end', () => { + const buffer = Buffer.concat(chunks); + + const bodyStr = buffer.toString('latin1'); + const parts = bodyStr.split(boundaryStr); + + let compressionType = ''; + let fileName = ''; + let fileContentBuffer = null; + + // Розбираємо частини форми + for (const part of parts) { + if (part.includes('name="compressionType"')) { + const val = part.split('\r\n\r\n')[1]; + + if (val) { + compressionType = val.split('\r\n')[0].trim(); + } + } else if (part.includes('name="file"')) { + const nameMatch = part.match(/filename="(.+?)"/); + + if (nameMatch) { + fileName = nameMatch[1]; + } + + const headerEnd = part.indexOf('\r\n\r\n'); + + if (headerEnd !== -1) { + // Вирізаємо чистий вміст файлу + const contentStr = part.substring( + headerEnd + 4, + part.lastIndexOf('\r\n'), + ); + + fileContentBuffer = Buffer.from(contentStr, 'latin1'); + } + } + } + + if (!fileName || !fileContentBuffer || !compressionType) { + res.statusCode = 400; + res.end('Invalid form data'); + + return; + } + + // Налаштування Zlib та розширень файлу + let transformStream; + let ext = ''; + + if (compressionType === 'gzip') { + transformStream = zlib.createGzip(); + ext = '.gz'; + } else if (compressionType === 'deflate') { + transformStream = zlib.createDeflate(); + ext = '.dfl'; + } else if (compressionType === 'br') { + transformStream = zlib.createBrotliCompress(); + ext = '.br'; + } else { + // Вимога: 400 status code for unsupported compression type + res.statusCode = 400; + res.end('Unsupported compression type'); + + return; + } + + // Встановлюємо заголовки для завантаження файлу + res.writeHead(200, { + 'Content-Disposition': `attachment; filename=${fileName}${ext}`, + 'Content-Type': 'application/octet-stream', + }); + + Readable.from(fileContentBuffer).pipe(transformStream).pipe(res); + }); + + return; + } + } + + // 3. Вимога: 404 status code for non-existent endpoint + res.statusCode = 404; + res.end('Not Found'); + }); } -module.exports = { - createServer, -}; +module.exports = { createServer };