From aeecfa63e395008a78d4df054ba61869968484d7 Mon Sep 17 00:00:00 2001 From: Mariia Hula Date: Wed, 7 Jan 2026 16:10:03 +0200 Subject: [PATCH 1/3] Solution --- src/createServer.js | 148 +++++++++++++++++++++++++++++++++++++++++++- src/index.html | 35 +++++++++++ src/script.js | 96 ++++++++++++++++++++++++++++ src/styles.css | 135 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 412 insertions(+), 2 deletions(-) create mode 100644 src/index.html create mode 100644 src/script.js create mode 100644 src/styles.css diff --git a/src/createServer.js b/src/createServer.js index 1cf1dda..fc6b1e3 100644 --- a/src/createServer.js +++ b/src/createServer.js @@ -1,8 +1,152 @@ 'use strict'; +const fs = require('node:fs'); +const http = require('node:http'); +const path = require('node:path'); +const { pipeline } = require('node:stream'); +const zlib = require('node:zlib'); +const { IncomingForm } = require('formidable'); + +const SUPPORTED_COMPRESSION_TYPES = ['gzip', 'deflate', 'br']; +const CONTENT_TYPES = { + html: 'text/html', + css: 'text/css', + js: 'application/javascript', + plain: 'text/plain', +}; + +function encodeFilename(filename) { + const cleaned = filename + .replace(/[\r\n\t]/g, '') + .replace(/["\\]/g, '') + .trim(); + + const hasNonASCII = [...cleaned].some((char) => char.charCodeAt(0) > 127); + const hasSpaces = cleaned.includes(' '); + + if (hasNonASCII || hasSpaces) { + const encoded = encodeURIComponent(cleaned); + + return `filename*=UTF-8''${encoded}`; + } + + return `filename=${cleaned}`; +} + +function createCompressionStream(type) { + const streams = { + gzip: () => zlib.createGzip(), + deflate: () => zlib.createDeflate(), + br: () => zlib.createBrotliCompress(), + }; + + return streams[type](); +} + +function serveStaticFile(res, filepath, contentType) { + res.statusCode = 200; + res.setHeader('Content-Type', contentType); + res.end(fs.readFileSync(path.join(__dirname, filepath))); +} + +function sendError(res, statusCode, message) { + res.statusCode = statusCode; + res.setHeader('Content-Type', CONTENT_TYPES.plain); + res.end(message); +} + +function handleCompressRequest(req, res) { + if (req.method !== 'POST') { + sendError(res, 400, 'Bad Request'); + + return; + } + + const form = new IncomingForm(); + let file = null; + let compressionType = null; + let filename = null; + + form.on('file', (name, fileData) => { + if (name === 'file') { + file = fileData; + filename = fileData.originalFilename || 'file.txt'; + } + }); + + form.on('field', (name, value) => { + if (name === 'compressionType') { + compressionType = value; + } + }); + + form.on('end', () => { + if ( + !file || + !compressionType || + !SUPPORTED_COMPRESSION_TYPES.includes(compressionType) + ) { + sendError(res, 400, 'Bad Request'); + + return; + } + + const compressStream = createCompressionStream(compressionType); + const encodedFilename = encodeFilename(`${filename}.${compressionType}`); + const fileStream = fs.createReadStream(file.filepath); + const tempFilePath = file.filepath; + + res.statusCode = 200; + res.setHeader('Content-Disposition', `attachment; ${encodedFilename}`); + + pipeline(fileStream, compressStream, res, (err) => { + if (err && !res.headersSent) { + sendError(res, 500, 'Internal Server Error'); + } + + fs.unlink(tempFilePath, () => {}); + }); + }); + + form.on('error', () => { + sendError(res, 400, 'Bad Request'); + }); + + form.parse(req); +} + function createServer() { - /* Write your code here */ - // Return instance of http.Server class + const server = new http.Server(); + + server.on('request', (req, res) => { + if (req.url === '/') { + serveStaticFile(res, 'index.html', CONTENT_TYPES.html); + + return; + } + + if (req.url === '/styles.css') { + serveStaticFile(res, 'styles.css', CONTENT_TYPES.css); + + return; + } + + if (req.url === '/script.js') { + serveStaticFile(res, 'script.js', CONTENT_TYPES.js); + + return; + } + + if (req.url === '/compress') { + handleCompressRequest(req, res); + + return; + } + + sendError(res, 404, 'Not Found'); + }); + + return server; } module.exports = { diff --git a/src/index.html b/src/index.html new file mode 100644 index 0000000..8bd83dd --- /dev/null +++ b/src/index.html @@ -0,0 +1,35 @@ + + + + + + File Compression Tool + + + +
+

File Compression Tool

+ +
+
+ + +
+
+ +
+ + +
+ + +
+
+ + + + diff --git a/src/script.js b/src/script.js new file mode 100644 index 0000000..0e12a4d --- /dev/null +++ b/src/script.js @@ -0,0 +1,96 @@ +const fileInput = document.getElementById('file'); +const fileInfo = document.getElementById('fileInfo'); +const submitBtn = document.getElementById('submitBtn'); +const form = document.getElementById('compressForm'); + +function formatFileSize(bytes) { + return `${(bytes / 1024).toFixed(2)} KB`; +} + +function extractFilename(contentDisposition) { + if (!contentDisposition) { + return 'compressed-file'; + } + + const rfc5987Match = contentDisposition.match(/filename\*=UTF-8''([^;]+)/); + + if (rfc5987Match && rfc5987Match[1]) { + return decodeURIComponent(rfc5987Match[1]); + } + + const filenameMatch = contentDisposition.match( + /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/, + ); + + if (filenameMatch && filenameMatch[1]) { + return filenameMatch[1].replace(/['"]/g, ''); + } + + return 'compressed-file'; +} + +function downloadFile(blob, filename) { + const url = window.URL.createObjectURL(blob); + const link = document.createElement('a'); + + link.href = url; + link.download = filename; + document.body.appendChild(link); + link.click(); + window.URL.revokeObjectURL(url); + document.body.removeChild(link); +} + +function resetButton(originalText) { + submitBtn.disabled = false; + submitBtn.textContent = originalText; +} + +function clearFileInfo() { + fileInfo.textContent = ''; + fileInfo.classList.remove('show'); +} + +fileInput.addEventListener('change', (e) => { + const file = e.target.files[0]; + + if (file) { + fileInfo.textContent = `Selected: ${file.name} (${formatFileSize(file.size)})`; + fileInfo.classList.add('show'); + } else { + clearFileInfo(); + } +}); + +form.addEventListener('submit', async (e) => { + e.preventDefault(); + + const formData = new FormData(form); + const originalText = submitBtn.textContent; + + submitBtn.disabled = true; + submitBtn.textContent = 'Compressing...'; + + try { + const response = await fetch('/compress', { + method: 'POST', + body: formData, + }); + + if (!response.ok) { + throw new Error(`Server error: ${response.status}`); + } + + const blob = await response.blob(); + const contentDisposition = response.headers.get('content-disposition'); + const filename = extractFilename(contentDisposition); + + downloadFile(blob, filename); + resetButton(originalText); + form.reset(); + clearFileInfo(); + } catch (error) { + window.alert(`Error: ${error.message}`); + resetButton(originalText); + } +}); diff --git a/src/styles.css b/src/styles.css new file mode 100644 index 0000000..c0a6eb5 --- /dev/null +++ b/src/styles.css @@ -0,0 +1,135 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + display: flex; + align-items: center; + justify-content: center; + + padding: 20px; + min-height: 100vh; + + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, + Ubuntu, Cantarell, sans-serif; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); +} + +.container { + padding: 40px; + + width: 100%; + max-width: 500px; + + background: white; + border-radius: 12px; + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2); +} + +.title { + margin-bottom: 30px; + + font-size: 28px; + text-align: center; + color: #333; +} + +.form-group { + margin-bottom: 20px; +} + +.label { + display: block; + margin-bottom: 8px; + + font-size: 14px; + font-weight: 500; + color: #555; +} + +.file-input { + width: 100%; + padding: 12px; + + font-size: 14px; + + border: 2px dashed #ddd; + border-radius: 8px; + background: #f9f9f9; + cursor: pointer; + transition: all 0.3s ease; +} + +.file-input:hover { + border-color: #667eea; + background: #f0f0ff; +} + +.file-info { + display: none; + margin-top: 10px; + padding: 10px; + + font-size: 13px; + color: #555; + + background: #f0f0ff; + border-radius: 6px; +} + +.file-info.show { + display: block; +} + +.select { + padding: 12px; + width: 100%; + + font-size: 14px; + + border: 2px solid #ddd; + border-radius: 8px; + background: white; + cursor: pointer; + transition: border-color 0.3s ease; +} + +.select:focus { + outline: none; + border-color: #667eea; +} + +.submit-btn { + width: 100%; + padding: 14px; + margin-top: 10px; + + font-size: 16px; + font-weight: 600; + color: white; + + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border: none; + border-radius: 8px; + cursor: pointer; + transition: + transform 0.2s ease, + box-shadow 0.2s ease; +} + +.submit-btn:hover { + transform: translateY(-2px); + box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4); +} + +.submit-btn:active { + transform: translateY(0); +} + +.submit-btn:disabled { + opacity: 0.6; + cursor: not-allowed; + transform: none; +} From b317f703a8543a33cf8e9fdafc366790a205d65c Mon Sep 17 00:00:00 2001 From: Mariia Hula Date: Wed, 7 Jan 2026 17:06:29 +0200 Subject: [PATCH 2/3] use streams to read static files --- src/createServer.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/createServer.js b/src/createServer.js index fc6b1e3..0449d7e 100644 --- a/src/createServer.js +++ b/src/createServer.js @@ -44,9 +44,16 @@ function createCompressionStream(type) { } function serveStaticFile(res, filepath, contentType) { + const fileStream = fs.createReadStream(path.join(__dirname, filepath)); + res.statusCode = 200; res.setHeader('Content-Type', contentType); - res.end(fs.readFileSync(path.join(__dirname, filepath))); + + pipeline(fileStream, res, (err) => { + if (err && !res.headersSent) { + sendError(res, 500, 'Internal Server Error'); + } + }); } function sendError(res, statusCode, message) { From 0eeb96e7f6a47a35e791bcb5e978a74e97789e59 Mon Sep 17 00:00:00 2001 From: Mariia Hula Date: Wed, 7 Jan 2026 17:16:33 +0200 Subject: [PATCH 3/3] change compression type extension --- src/createServer.js | 6 +++--- src/index.html | 4 ++-- tests/createServer.test.js | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/createServer.js b/src/createServer.js index 0449d7e..da9c2ff 100644 --- a/src/createServer.js +++ b/src/createServer.js @@ -7,7 +7,7 @@ const { pipeline } = require('node:stream'); const zlib = require('node:zlib'); const { IncomingForm } = require('formidable'); -const SUPPORTED_COMPRESSION_TYPES = ['gzip', 'deflate', 'br']; +const SUPPORTED_COMPRESSION_TYPES = ['gz', 'dfl', 'br']; const CONTENT_TYPES = { html: 'text/html', css: 'text/css', @@ -35,8 +35,8 @@ function encodeFilename(filename) { function createCompressionStream(type) { const streams = { - gzip: () => zlib.createGzip(), - deflate: () => zlib.createDeflate(), + gz: () => zlib.createGzip(), + dfl: () => zlib.createDeflate(), br: () => zlib.createBrotliCompress(), }; diff --git a/src/index.html b/src/index.html index 8bd83dd..f6af7dd 100644 --- a/src/index.html +++ b/src/index.html @@ -20,8 +20,8 @@

File Compression Tool

diff --git a/tests/createServer.test.js b/tests/createServer.test.js index b53ef15..4773075 100644 --- a/tests/createServer.test.js +++ b/tests/createServer.test.js @@ -27,10 +27,10 @@ function stringToStream(str) { } const compressionTypes = { - gzip: { + gz: { decompress: util.promisify(zlib.gunzip), }, - deflate: { + dfl: { decompress: util.promisify(zlib.inflate), }, br: {