Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 153 additions & 2 deletions src/createServer.js
Original file line number Diff line number Diff line change
@@ -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'];

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The task requirements state that the form options for compression type should be gzip, deflate, and br. This array should be updated to match those required values, like ['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 = {
gz: () => zlib.createGzip(),
dfl: () => zlib.createDeflate(),
br: () => zlib.createBrotliCompress(),
};
Comment on lines +37 to +41

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To align with the task requirements, the keys in this streams object should be gzip, deflate, and br. The server should expect these values from the form and map them to the correct zlib stream.


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}`);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The file extension for the compressed file doesn't match the requirements for all compression types. The task specifies .gz for gzip and .dfl for deflate, but this implementation will generate .gzip and .deflate respectively. You might need a way to map the compressionType to the correct extension.

Copy link
Author

@hma-3 hma-3 Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tests expect .gzip and .deflate, not .gz and .dfl.

tests/createServer.test.js:

const compressionTypes = {
  gzip: {
    decompress: util.promisify(zlib.gunzip),
  },
  deflate: {
    decompress: util.promisify(zlib.inflate),
  },
  br: {
    decompress: util.promisify(zlib.brotliDecompress),
  },
};

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The file extensions for gzip and deflate are still not matching the requirements. According to the task description, the extensions should be .gz for gzip and .dfl for deflate.

This line currently uses the compression type directly, which results in incorrect extensions like .gzip. You'll need to create a mapping from the compression type to the correct file extension.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tests expect .gzip and .deflate, not .gz and .dfl.

tests/createServer.test.js:

const compressionTypes = {
  gzip: {
    decompress: util.promisify(zlib.gunzip),
  },
  deflate: {
    decompress: util.promisify(zlib.inflate),
  },
  br: {
    decompress: util.promisify(zlib.brotliDecompress),
  },
};

They are failing if extensions .gz or .dfl are used:

createServer › Server › POST to the "/compress" endpoint › compression type "gzip" › should respond with a correct "Content-Disposition" header

    expect(received).toBe(expected) // Object.is equality

    Expected: "attachment; filename=extraneous.pptx.gzip"
    Received: "attachment; filename=extraneous.pptx.gz"

      127 |                   const expectedHeader = `attachment; filename=${filename}.${compressionType}`;
      128 |
    > 129 |                   expect(res.headers['content-disposition']).toBe(
          |                                                              ^
      130 |                     expectedHeader,
      131 |                   );
      132 |                 });

      at toBe (tests/createServer.test.js:129:62)

  ● createServer › Server › POST to the "/compress" endpoint › compression type "deflate" › should respond with a correct "Content-Disposition" header

    expect(received).toBe(expected) // Object.is equality

    Expected: "attachment; filename=colonise_whoever_sleepy.jpe.deflate"
    Received: "attachment; filename=colonise_whoever_sleepy.jpe.dfl"

      127 |                   const expectedHeader = `attachment; filename=${filename}.${compressionType}`;
      128 |
    > 129 |                   expect(res.headers['content-disposition']).toBe(
          |                                                              ^
      130 |                     expectedHeader,
      131 |                   );
      132 |                 });

      at toBe (tests/createServer.test.js:129:62)
      ```

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Appending compressionType directly will cause an issue once you update the form values as required. For example, if compressionType is "gzip", the filename will become filename.gzip instead of the required filename.gz. You need to create a mapping to convert the compression type (gzip, deflate) to the correct file extension (.gz, .dfl).

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 = {
Expand Down
35 changes: 35 additions & 0 deletions src/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>File Compression Tool</title>
<link rel="stylesheet" href="/styles.css">
</head>
<body>
<div class="container">
<h1 class="title">File Compression Tool</h1>

<form action="/compress" enctype="multipart/form-data" method="post" id="compressForm">
<div class="form-group">
<label for="file" class="label">Select File</label>
<input type="file" name="file" id="file" class="file-input" required>
<div class="file-info" id="fileInfo"></div>
</div>

<div class="form-group">
<label for="compressionType" class="label">Compression Type</label>
<select name="compressionType" id="compressionType" class="select" required>
<option value="gz">Gzip</option>
<option value="dfl">Deflate</option>
<option value="br">Brotli</option>
</select>
</div>

<button type="submit" id="submitBtn" class="submit-btn">Compress File</button>
</form>
</div>

<script src="/script.js"></script>
</body>
</html>
96 changes: 96 additions & 0 deletions src/script.js
Original file line number Diff line number Diff line change
@@ -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);
}
});
Loading