diff --git a/src/createServer.js b/src/createServer.js
index 1cf1dda..da9c2ff 100644
--- a/src/createServer.js
+++ b/src/createServer.js
@@ -1,8 +1,159 @@
'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 = ['gz', 'dfl', '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 = {
+ gz: () => zlib.createGzip(),
+ dfl: () => zlib.createDeflate(),
+ br: () => zlib.createBrotliCompress(),
+ };
+
+ return streams[type]();
+}
+
+function serveStaticFile(res, filepath, contentType) {
+ const fileStream = fs.createReadStream(path.join(__dirname, filepath));
+
+ res.statusCode = 200;
+ res.setHeader('Content-Type', contentType);
+
+ pipeline(fileStream, res, (err) => {
+ if (err && !res.headersSent) {
+ sendError(res, 500, 'Internal Server Error');
+ }
+ });
+}
+
+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..f6af7dd
--- /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;
+}
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: {