From 3f39cc2b52d72118d51ea060d3c7c255dcfef23a Mon Sep 17 00:00:00 2001 From: vilich Date: Mon, 5 Jan 2026 19:24:46 +0100 Subject: [PATCH 1/4] add --- src/createServer.js | 148 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 146 insertions(+), 2 deletions(-) diff --git a/src/createServer.js b/src/createServer.js index 1cf1dda..03a0da0 100644 --- a/src/createServer.js +++ b/src/createServer.js @@ -1,8 +1,152 @@ +/* eslint-disable no-console */ +/* eslint-disable no-useless-return */ 'use strict'; +const { IncomingForm } = require('formidable'); +const { Server } = require('http'); +const fs = require('fs'); +const zlib = require('zlib'); + +const ALLOWED_ENDPOINTS = { + Compress: { + route: '/compress', + allowedMethods: ['POST'], + }, + Base: { + route: '/', + alloweMethods: ['GET'], + }, +}; + +const ALLOWED_COMPRESSION_ALGS = { + gzip: 'gzip', + deflate: 'deflate', + br: 'br', +}; + +function processResponse(options) { + const { res, statusCode, contentType, message } = options; + + res.statusCode = statusCode; + res.setHeader('Content-Type', contentType); + res.end(message); + + return res; +} + function createServer() { - /* Write your code here */ - // Return instance of http.Server class + const server = new Server(); + + server.on('request', (req, res) => { + const url = new URL(req.url, `http://${req.headers.host}`); + const pathname = url.pathname; + + if ( + pathname === ALLOWED_ENDPOINTS.Base.route && + ALLOWED_ENDPOINTS.Base.alloweMethods.includes(req.method) + ) { + return processResponse({ + res, + statusCode: 200, + contentType: 'text/plain', + message: 'Server is healthy', + }); + } + + if (pathname !== ALLOWED_ENDPOINTS.Compress.route) { + return processResponse({ + res, + statusCode: 404, + contentType: 'text/plain', + message: 'Endpoint does not exist', + }); + } + + if ( + pathname === ALLOWED_ENDPOINTS.Compress.route && + !ALLOWED_ENDPOINTS.Compress.allowedMethods.includes(req.method) + ) { + return processResponse({ + res, + statusCode: 400, + contentType: 'text/plain', + message: `Endpoint does not support ${req.method}`, + }); + } + + const form = new IncomingForm({}); + + form.parse(req, (err, fields, files) => { + if (err || !fields.compressionType || !files.file) { + return processResponse({ + res, + statusCode: 400, + contentType: 'text/plain', + message: 'Invalid form data', + }); + } + + const compressionAlg = fields.compressionType?.[0]; + const file = files.file?.[0]; + + const fileName = file.originalFilename; + const filePath = file.filepath; + const mimeType = file.mimetype; + + const fileStream = fs.createReadStream(filePath); + + let compressionStream; + + switch (compressionAlg) { + case ALLOWED_COMPRESSION_ALGS.gzip: { + compressionStream = zlib.createGzip(); + break; + } + + case ALLOWED_COMPRESSION_ALGS.deflate: { + compressionStream = zlib.createDeflate(); + break; + } + + case ALLOWED_COMPRESSION_ALGS.br: { + compressionStream = zlib.createBrotliCompress(); + break; + } + + default: { + return processResponse({ + res, + statusCode: 400, + contentType: 'text/plain', + message: 'Unsupported compression type', + }); + } + } + + const fileNameCompressed = `${fileName}.${compressionAlg}`; + + res.statusCode = 200; + + res.setHeader('Content-Type', mimeType); + + res.setHeader( + 'Content-Disposition', + `attachment; filename=${fileNameCompressed}`, + ); + + fileStream + .on('error', () => { + console.error('Server error'); + }) + .pipe(compressionStream) + .on('error', () => { + console.error('Compress failed'); + }) + .pipe(res); + }); + }); + + return server; } module.exports = { From c63dbdea91f85076596af32ac28974eb48e78d02 Mon Sep 17 00:00:00 2001 From: vilich Date: Mon, 5 Jan 2026 19:33:03 +0100 Subject: [PATCH 2/4] add2 --- src/createServer.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/createServer.js b/src/createServer.js index 03a0da0..c7722c3 100644 --- a/src/createServer.js +++ b/src/createServer.js @@ -14,7 +14,7 @@ const ALLOWED_ENDPOINTS = { }, Base: { route: '/', - alloweMethods: ['GET'], + allowedMethods: ['GET'], }, }; @@ -43,7 +43,7 @@ function createServer() { if ( pathname === ALLOWED_ENDPOINTS.Base.route && - ALLOWED_ENDPOINTS.Base.alloweMethods.includes(req.method) + ALLOWED_ENDPOINTS.Base.allowedMethods.includes(req.method) ) { return processResponse({ res, @@ -123,7 +123,7 @@ function createServer() { } } - const fileNameCompressed = `${fileName}.${compressionAlg}`; + const fileNameCompressed = `${fileName}.${extension}`; res.statusCode = 200; From 74d7db0b8fbbded88f2f220e132ffd20178a4750 Mon Sep 17 00:00:00 2001 From: vilich Date: Mon, 5 Jan 2026 19:35:02 +0100 Subject: [PATCH 3/4] add3 --- src/createServer.js | 350 ++++++++++++++++++++++++++++---------------- src/index.html | 27 ++++ 2 files changed, 251 insertions(+), 126 deletions(-) create mode 100644 src/index.html diff --git a/src/createServer.js b/src/createServer.js index c7722c3..7b4395f 100644 --- a/src/createServer.js +++ b/src/createServer.js @@ -1,152 +1,250 @@ -/* eslint-disable no-console */ -/* eslint-disable no-useless-return */ 'use strict'; -const { IncomingForm } = require('formidable'); -const { Server } = require('http'); -const fs = require('fs'); +const http = require('http'); const zlib = require('zlib'); +const { Readable } = require('stream'); -const ALLOWED_ENDPOINTS = { - Compress: { - route: '/compress', - allowedMethods: ['POST'], - }, - Base: { - route: '/', - allowedMethods: ['GET'], - }, +const compressors = { + gzip: zlib.createGzip, + deflate: zlib.createDeflate, + br: zlib.createBrotliCompress, }; -const ALLOWED_COMPRESSION_ALGS = { - gzip: 'gzip', - deflate: 'deflate', - br: 'br', +const extensions = { + gzip: '.gz', + deflate: '.dfl', + br: '.br', }; -function processResponse(options) { - const { res, statusCode, contentType, message } = options; +function getBoundary(contentType = '') { + const match = contentType.match(/boundary=(?:"([^"]+)"|([^;]+))/i); - res.statusCode = statusCode; - res.setHeader('Content-Type', contentType); - res.end(message); + return match ? match[1] || match[2] : null; +} + +async function readRequestBody(req) { + const chunks = []; + + for await (const chunk of req) { + chunks.push(chunk); + } - return res; + return Buffer.concat(chunks); +} + +function parsePartHeaders(headerText) { + const headers = {}; + + headerText.split('\r\n').forEach((line) => { + const idx = line.indexOf(':'); + + if (idx === -1) { + return; + } + + const key = line.slice(0, idx).trim().toLowerCase(); + const value = line.slice(idx + 1).trim(); + + headers[key] = value; + }); + + return headers; +} + +function parseContentDisposition(value = '') { + // example: + // form-data; name="file"; filename="test.txt" + const nameMatch = value.match(/name="([^"]+)"/i); + const filenameMatch = value.match(/filename="([^"]*)"/i); + + return { + name: nameMatch ? nameMatch[1] : null, + filename: filenameMatch ? filenameMatch[1] : null, + }; +} + +function trimCrlf(buf) { + if (buf.length >= 2 && buf.slice(-2).toString() === '\r\n') { + return buf.slice(0, -2); + } + + return buf; +} + +function parseMultipart(bodyBuffer, boundary) { + const boundaryToken = Buffer.from(`--${boundary}`); + const result = { + fields: {}, + file: null, + }; + + let pos = 0; + + // Must start with boundary + const first = bodyBuffer.indexOf(boundaryToken, pos); + + if (first !== 0) { + return null; + } + + pos = first; + + while (pos < bodyBuffer.length) { + // Find next boundary + const boundaryStart = bodyBuffer.indexOf(boundaryToken, pos); + + if (boundaryStart === -1) { + break; + } + + let partStart = boundaryStart + boundaryToken.length; + + // Check for final boundary: "--" + const isFinal = + bodyBuffer.slice(partStart, partStart + 2).toString() === '--'; + + if (isFinal) { + break; + } + + // Skip leading CRLF after boundary + if (bodyBuffer.slice(partStart, partStart + 2).toString() === '\r\n') { + partStart += 2; + } + + const headersEnd = bodyBuffer.indexOf(Buffer.from('\r\n\r\n'), partStart); + + if (headersEnd === -1) { + return null; + } + + const headerText = bodyBuffer.slice(partStart, headersEnd).toString('utf8'); + const headers = parsePartHeaders(headerText); + + const cd = parseContentDisposition(headers['content-disposition']); + + if (!cd.name) { + return null; + } + + const contentStart = headersEnd + 4; + + // The content ends right before the next boundary + const nextBoundary = bodyBuffer.indexOf(boundaryToken, contentStart); + + if (nextBoundary === -1) { + return null; + } + + const rawContent = bodyBuffer.slice(contentStart, nextBoundary); + const content = trimCrlf(rawContent); + + if (cd.filename !== null) { + // file part + result.file = { + fieldName: cd.name, + filename: cd.filename, + buffer: content, + }; + } else { + // normal field + result.fields[cd.name] = content.toString('utf8').trim(); + } + + pos = nextBoundary; + } + + return result; } function createServer() { - const server = new Server(); + return http.createServer(async (req, res) => { + // Serve HTML page (optional for tests, but useful and required by mentor) + if (req.method === 'GET' && req.url === '/') { + res.writeHead(200); + res.end(); - server.on('request', (req, res) => { - const url = new URL(req.url, `http://${req.headers.host}`); - const pathname = url.pathname; + return; + } - if ( - pathname === ALLOWED_ENDPOINTS.Base.route && - ALLOWED_ENDPOINTS.Base.allowedMethods.includes(req.method) - ) { - return processResponse({ - res, - statusCode: 200, - contentType: 'text/plain', - message: 'Server is healthy', - }); + if (req.url !== '/compress') { + res.writeHead(404); + res.end(); + + return; } - if (pathname !== ALLOWED_ENDPOINTS.Compress.route) { - return processResponse({ - res, - statusCode: 404, - contentType: 'text/plain', - message: 'Endpoint does not exist', - }); + if (req.method !== 'POST') { + res.writeHead(400); + res.end(); + + return; + } + + const contentType = req.headers['content-type'] || ''; + const boundary = getBoundary(contentType); + + if (!boundary) { + res.writeHead(400); + res.end(); + + return; } + let body; + + try { + body = await readRequestBody(req); + } catch (e) { + res.writeHead(400); + res.end(); + + return; + } + + const parsed = parseMultipart(body, boundary); + + if (!parsed) { + res.writeHead(400); + res.end(); + + return; + } + + const compressionType = parsed.fields.compressionType; + const file = parsed.file; + + // invalid form if ( - pathname === ALLOWED_ENDPOINTS.Compress.route && - !ALLOWED_ENDPOINTS.Compress.allowedMethods.includes(req.method) + !compressionType || + !file || + file.fieldName !== 'file' || + !file.filename ) { - return processResponse({ - res, - statusCode: 400, - contentType: 'text/plain', - message: `Endpoint does not support ${req.method}`, - }); - } - - const form = new IncomingForm({}); - - form.parse(req, (err, fields, files) => { - if (err || !fields.compressionType || !files.file) { - return processResponse({ - res, - statusCode: 400, - contentType: 'text/plain', - message: 'Invalid form data', - }); - } - - const compressionAlg = fields.compressionType?.[0]; - const file = files.file?.[0]; - - const fileName = file.originalFilename; - const filePath = file.filepath; - const mimeType = file.mimetype; - - const fileStream = fs.createReadStream(filePath); - - let compressionStream; - - switch (compressionAlg) { - case ALLOWED_COMPRESSION_ALGS.gzip: { - compressionStream = zlib.createGzip(); - break; - } - - case ALLOWED_COMPRESSION_ALGS.deflate: { - compressionStream = zlib.createDeflate(); - break; - } - - case ALLOWED_COMPRESSION_ALGS.br: { - compressionStream = zlib.createBrotliCompress(); - break; - } - - default: { - return processResponse({ - res, - statusCode: 400, - contentType: 'text/plain', - message: 'Unsupported compression type', - }); - } - } - - const fileNameCompressed = `${fileName}.${extension}`; - - res.statusCode = 200; - - res.setHeader('Content-Type', mimeType); - - res.setHeader( - 'Content-Disposition', - `attachment; filename=${fileNameCompressed}`, - ); - - fileStream - .on('error', () => { - console.error('Server error'); - }) - .pipe(compressionStream) - .on('error', () => { - console.error('Compress failed'); - }) - .pipe(res); + res.writeHead(400); + res.end(); + + return; + } + + // unsupported type + if (!compressors[compressionType]) { + res.writeHead(400); + res.end(); + + return; + } + + // stream compression + const compressor = compressors[compressionType](); + const outName = `${file.filename}${extensions[compressionType]}`; + + res.writeHead(200, { + 'Content-Disposition': `attachment; filename=${outName}`, }); - }); - return server; + Readable.from(file.buffer).pipe(compressor).pipe(res); + }); } module.exports = { diff --git a/src/index.html b/src/index.html new file mode 100644 index 0000000..bd46688 --- /dev/null +++ b/src/index.html @@ -0,0 +1,27 @@ + + + + + + Document + + +
+
+ +
+
+ +
+ +
+ + From bf63849f303296b325b57a95481a4efd3f4921b4 Mon Sep 17 00:00:00 2001 From: vilich Date: Mon, 5 Jan 2026 19:39:10 +0100 Subject: [PATCH 4/4] add4 --- src/createServer.js | 370 ++++++++++++++++---------------------------- src/index.html | 47 +++--- 2 files changed, 152 insertions(+), 265 deletions(-) diff --git a/src/createServer.js b/src/createServer.js index 7b4395f..eb3f3d8 100644 --- a/src/createServer.js +++ b/src/createServer.js @@ -1,249 +1,139 @@ +/* eslint-disable no-console */ +/* eslint-disable curly */ 'use strict'; -const http = require('http'); -const zlib = require('zlib'); -const { Readable } = require('stream'); - -const compressors = { - gzip: zlib.createGzip, - deflate: zlib.createDeflate, - br: zlib.createBrotliCompress, -}; - -const extensions = { - gzip: '.gz', - deflate: '.dfl', - br: '.br', -}; - -function getBoundary(contentType = '') { - const match = contentType.match(/boundary=(?:"([^"]+)"|([^;]+))/i); - - return match ? match[1] || match[2] : null; -} - -async function readRequestBody(req) { - const chunks = []; - - for await (const chunk of req) { - chunks.push(chunk); - } - - return Buffer.concat(chunks); -} - -function parsePartHeaders(headerText) { - const headers = {}; - - headerText.split('\r\n').forEach((line) => { - const idx = line.indexOf(':'); - - if (idx === -1) { - return; - } - - const key = line.slice(0, idx).trim().toLowerCase(); - const value = line.slice(idx + 1).trim(); - - headers[key] = value; - }); - - return headers; -} - -function parseContentDisposition(value = '') { - // example: - // form-data; name="file"; filename="test.txt" - const nameMatch = value.match(/name="([^"]+)"/i); - const filenameMatch = value.match(/filename="([^"]*)"/i); - - return { - name: nameMatch ? nameMatch[1] : null, - filename: filenameMatch ? filenameMatch[1] : null, - }; -} - -function trimCrlf(buf) { - if (buf.length >= 2 && buf.slice(-2).toString() === '\r\n') { - return buf.slice(0, -2); - } - - return buf; -} - -function parseMultipart(bodyBuffer, boundary) { - const boundaryToken = Buffer.from(`--${boundary}`); - const result = { - fields: {}, - file: null, - }; - - let pos = 0; - - // Must start with boundary - const first = bodyBuffer.indexOf(boundaryToken, pos); - - if (first !== 0) { - return null; - } - - pos = first; - - while (pos < bodyBuffer.length) { - // Find next boundary - const boundaryStart = bodyBuffer.indexOf(boundaryToken, pos); - - if (boundaryStart === -1) { - break; - } - - let partStart = boundaryStart + boundaryToken.length; - - // Check for final boundary: "--" - const isFinal = - bodyBuffer.slice(partStart, partStart + 2).toString() === '--'; - - if (isFinal) { - break; - } - - // Skip leading CRLF after boundary - if (bodyBuffer.slice(partStart, partStart + 2).toString() === '\r\n') { - partStart += 2; - } - - const headersEnd = bodyBuffer.indexOf(Buffer.from('\r\n\r\n'), partStart); - - if (headersEnd === -1) { - return null; - } - - const headerText = bodyBuffer.slice(partStart, headersEnd).toString('utf8'); - const headers = parsePartHeaders(headerText); - - const cd = parseContentDisposition(headers['content-disposition']); - - if (!cd.name) { - return null; - } - - const contentStart = headersEnd + 4; - - // The content ends right before the next boundary - const nextBoundary = bodyBuffer.indexOf(boundaryToken, contentStart); - - if (nextBoundary === -1) { - return null; - } - - const rawContent = bodyBuffer.slice(contentStart, nextBoundary); - const content = trimCrlf(rawContent); - - if (cd.filename !== null) { - // file part - result.file = { - fieldName: cd.name, - filename: cd.filename, - buffer: content, - }; - } else { - // normal field - result.fields[cd.name] = content.toString('utf8').trim(); - } - - pos = nextBoundary; - } - - return result; -} +const http = require('node:http'); +const fs = require('node:fs'); +const path = require('node:path'); +const pipeline = require('node:stream').pipeline; +const zlib = require('node:zlib'); +const formidable = require('formidable'); +const mime = require('mime-types'); function createServer() { return http.createServer(async (req, res) => { - // Serve HTML page (optional for tests, but useful and required by mentor) - if (req.method === 'GET' && req.url === '/') { - res.writeHead(200); - res.end(); - - return; - } - - if (req.url !== '/compress') { - res.writeHead(404); - res.end(); - - return; - } - - if (req.method !== 'POST') { - res.writeHead(400); - res.end(); - - return; - } - - const contentType = req.headers['content-type'] || ''; - const boundary = getBoundary(contentType); - - if (!boundary) { - res.writeHead(400); - res.end(); - - return; - } - - let body; - - try { - body = await readRequestBody(req); - } catch (e) { - res.writeHead(400); - res.end(); - - return; - } - - const parsed = parseMultipart(body, boundary); - - if (!parsed) { - res.writeHead(400); - res.end(); - - return; - } - - const compressionType = parsed.fields.compressionType; - const file = parsed.file; - - // invalid form - if ( - !compressionType || - !file || - file.fieldName !== 'file' || - !file.filename - ) { - res.writeHead(400); - res.end(); - - return; - } - - // unsupported type - if (!compressors[compressionType]) { - res.writeHead(400); - res.end(); - - return; + if (req.url === '/' && req.method === 'GET') { + const filePath = path.join(__dirname, 'index.html'); + + fs.readFile(filePath, (err, data) => { + if (err) { + res.writeHead(500, { 'Content-Type': 'text/plain' }); + res.end('Internal Server Error'); + } else { + res.writeHead(200, { 'Content-Type': 'text/html' }); + res.end(data); + } + }); + } else if (req.method !== 'POST' && req.url === '/compress') { + res.statusCode = 400; + res.end('Bad Request: Invalid request method.'); + } else if (req.method === 'POST' && req.url === '/compress') { + const form = new formidable.IncomingForm(); + + form.parse(req, (err, fields, files) => { + if (err) { + res.statusCode = 500; + res.end('Server Error: Failed to parse form data.'); + + return; + } + + const compressionTypeArray = fields.compressionType; + const compressionType = Array.isArray(compressionTypeArray) + ? compressionTypeArray[0] + : undefined; + + const uploadedFileArray = files.file; + const uploadedFile = Array.isArray(uploadedFileArray) + ? uploadedFileArray[0] + : undefined; + + if (!uploadedFile || !uploadedFile.filepath || !compressionType) { + res.statusCode = 400; + res.end('Bad Request: File or compression type missing or invalid.'); + + return; + } + + let compressionStream; + let fileExtension; + + switch (compressionType) { + case 'gzip': + compressionStream = zlib.createGzip(); + fileExtension = '.gzip'; + break; + case 'deflate': + compressionStream = zlib.createDeflate(); + fileExtension = '.deflate'; + break; + case 'br': + compressionStream = zlib.createBrotliCompress(); + fileExtension = '.br'; + + break; + default: + res.statusCode = 400; + res.end('Bad Request: Unsupported compression type.'); + + fs.unlink(uploadedFile.filepath, (unlinkErr) => { + if (unlinkErr) + console.error('Error deleting temp file:', unlinkErr); + }); + + return; + } + + const fileReadStream = fs.createReadStream(uploadedFile.filepath); + + const originalFileName = + uploadedFile.originalFilename || 'uploaded_file'; + const baseName = path.basename( + originalFileName, + path.extname(originalFileName), + ); + + const newFileName = `${baseName}${path.extname(originalFileName)}${fileExtension}`; + const originalMimeType = + uploadedFile.mimetype || + mime.contentType(path.extname(originalFileName)) || + 'application/octet-stream'; + + res.statusCode = 200; + res.setHeader('Content-Type', originalMimeType); + + res.setHeader( + 'Content-Disposition', + `attachment; filename=${newFileName}`, + ); + + pipeline(fileReadStream, compressionStream, res, (pipelineErr) => { + if (pipelineErr) { + console.error('Pipeline failed during compression:', pipelineErr); + + if (!res.headersSent) { + res.statusCode = 500; + res.end('Server Error during file compression.'); + } else { + res.end(); + } + } + + fs.unlink(uploadedFile.filepath, (unlinkErr) => { + if (unlinkErr) + console.error('Error deleting temporary file:', unlinkErr); + }); + + res.on('close', () => { + fileReadStream.destroy(); + compressionStream.destroy(); + }); + }); + }); + } else { + res.writeHead(404, { 'Content-Type': 'text/plain' }); + res.end('Not Found'); } - - // stream compression - const compressor = compressors[compressionType](); - const outName = `${file.filename}${extensions[compressionType]}`; - - res.writeHead(200, { - 'Content-Disposition': `attachment; filename=${outName}`, - }); - - Readable.from(file.buffer).pipe(compressor).pipe(res); }); } diff --git a/src/index.html b/src/index.html index bd46688..69af3fc 100644 --- a/src/index.html +++ b/src/index.html @@ -1,27 +1,24 @@ - + - - - - Document - - -
-
- -
-
- -
- -
- + + + + Compression Application + + +

File Compression

+
+ +

+ + +

+ + +
+