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
131 changes: 129 additions & 2 deletions src/createServer.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,135 @@
'use strict';

const fs = require('fs');
const path = require('path');
const mime = require('mime-types');
const formidable = require('formidable');
const zlib = require('zlib');
const { Server } = require('node:http');

const filesRoute = {
'/index.html': '/index.html',
'/style.css': '/style.css',
'/script.js': '/script.js',
};
const compressionMap = new Map([
['gzip', () => zlib.createGzip()],
['deflate', () => zlib.createDeflate()],
['br', () => zlib.createBrotliCompress()],
]);

const extensionMap = {
gzip: 'gz',
deflate: 'dfl',
br: 'br',
};
Comment on lines +21 to +25
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

According to the task description, the file extensions for gzip and deflate should be .gz and .dfl, respectively. The current implementation uses .gzip and .deflate.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Then why do the tests expect a filename that includes the full compression format?

● 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=ick_that.tar.gzip"
Received: "attachment; filename=ick_that.tar.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=dehumanize.dump.deflate"
Received: "attachment; filename=dehumanize.dump.dfl"

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

Comment on lines +21 to +25
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The extensions for gzip and deflate are not quite right. According to the task description, they should be .gz and .dfl respectively. Please update this map to use the correct extensions.


const handleStaticFiles = (req, res) => {
const url = new URL(req.url || '', `http://${req.headers.host}`);
const requestPath =
url.pathname === '/' ? 'index.html' : url.pathname.slice(1);
const realPath = path.join(__dirname, 'public', requestPath);

if (fs.existsSync(realPath)) {
const mimeType = mime.contentType(path.extname(realPath)) || 'text/plain';

res.writeHead(200, { 'Content-Type': mimeType });

fs.createReadStream(realPath)
.on('error', (err) => {
if (err) {
res.statusCode = 500;
res.end('Internal Server Error');
}
})
.pipe(res);
} else {
res.statusCode = 404;
res.end('Not found');
}
};

const handleFileCompress = (req, res) => {
const form = new formidable.IncomingForm();

form.parse(req, (err, fields, files) => {
if (err) {
res.writeHead(400, { 'Content-Type': 'text/plain' });
res.end('Bad request: Form data is invalid');

return;
}

const compression = Array.isArray(fields.compressionType)
? fields.compressionType[0]
: fields.compressionType;

const file = Array.isArray(files.file) ? files.file[0] : files.file;

if (!file || !compression || compression === '') {
res.writeHead(400, { 'Content-Type': 'text/plain' });
res.end('Bad request: File or compression missing');

return;
}

if (!compressionMap.has(compression)) {
res.writeHead(400, { 'Content-Type': 'text/plain' });
res.end('Bad request: Unsupported compression type');

return;
}

const filePath = file.filepath || file.path;
const fileName = file.originalFilename || file.name;

const compressStream = compressionMap.get(compression)();
const ext = extensionMap[compression];

res.writeHead(200, {
'Content-Type': 'application/octet-stream',
'Content-Disposition': `attachment; filename=${fileName}.${ext}`,
});

fs.createReadStream(filePath)
.on('error', () => {
if (!res.headersSent) {
res.writeHead(500);
res.end('Internal Server Error');
}
})
.pipe(compressStream)
.pipe(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 || 'localhost'}`);
const pathname = url.pathname;

if (filesRoute[pathname] || pathname === '/') {
return handleStaticFiles(req, res);
}

if (req.method === 'POST' && pathname === '/compress') {
return handleFileCompress(req, res);
}

if (req.method === 'GET' && pathname === '/compress') {
res.writeHead(400, { 'Content-Type': 'text/plain' });
res.end('');

return;
}

res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end('Not found');
});

return server;
}

module.exports = {
Expand Down
56 changes: 56 additions & 0 deletions src/public/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Form</title>
<link rel="stylesheet" href="/style.css">
</head>
<body>
<h1 class="title">Compression App</h1>

<form class="app"
action="/compress"
method="POST"
enctype="multipart/form-data"
>
<div class="app__top">Form</div>
<div class="app__content">
<div class="file__section">
<span class="file__title">Select a file:</span>
<label class="file__wrapper">
<input
class="file__input"
type="file"
name="file"
/>
<span class="file__status">File is not selected</span>
<span class="file__browse">Browse...</span>
</label>
</div>

<div class="type__section">
<span class="type__title">Choose a compression:</span>
<select
class="type__select"
name="compressionType"
id="select"
>
<option value=""></option>
<option value="gzip">Gzip (.gz)</option>
<option value="deflate">Deflate (.dfl)</option>
<option value="br">Brotli (.br)</option>
</select>
</div>

<button
class="submit__button"
type="submit"
>
Submit
</button>
</div>
</form>
<script src="./script.js"></script>
</body>
</html>
8 changes: 8 additions & 0 deletions src/public/script.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const input = document.querySelector('.file__input');
const fileStatus = document.querySelector('.file__status');

input.addEventListener('change', () => {
if (input.files.length > 0) {
fileStatus.textContent = input.files[0].name;
}
});
128 changes: 128 additions & 0 deletions src/public/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
body {
box-sizing: border-box;
padding-top: 30px;
color: #747474;
}

.title {
margin: 0;
padding-bottom: 25px;
text-align: center;
font-family: 'Montserrat', Tahoma, Geneva, Verdana, sans-serif;
font-size: 25px;
color: black;
}

.app {
height: auto;
width: 700px;
margin: 0 auto;
border: 1px solid #dedbde;
border-radius: 6px;
}

.app__top {
display: flex;
width: auto;
height: 40px;
padding-left: 20px;
font-size: 20px;
align-items: center;
color: #34aced;
background-color: #f2f2f2;
border-radius: 6px;
}

.app__content {
display: flex;
flex-direction: column;
gap: 25px;
margin-block: 20px;
padding: 15px;
}

.file__section {
display: flex;
flex-direction: column;
gap: 8px;
}

.file__title {
font-size: 17px;
}

.file__wrapper {
display: flex;
justify-content: space-between;
align-items: center;
width: auto;
height: 35px;
border: 1px solid #dcdcdc;
border-radius: 3px;
transition: all .3s ease;
}

.file__wrapper:hover {
cursor: pointer;
border-color: #d0cfcf;
}

.file__input {
display: none;
}

.file__status {
padding-left: 25px;
}

.file__browse {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
width: 80px;
background-color: #f5f5f5;
border-left: 1px solid #dcdcdc;
}

.type__section {
display: flex;
flex-direction: column;
gap: 8px;
}

.type__title {
font-size: 17px;
}

.type__select {
width: 100px;
height: 30px;
border: 1px solid #dcdcdc;
border-radius: 3px;
transition: all .3s ease;
}

.type__select:hover {
cursor: pointer;
border-color: #d0cfcf;
}

.submit__button {
margin-top: 10px;
height: 35px;
width: 150px;
font-family: inherit;
font-size: 19px;
letter-spacing: 0.7px;
color: inherit;
background-color: #f5f5f5;
border: 1px solid #dcdcdc;
border-radius: 3px;
transition: all .3s ease;
}

.submit__button:hover {
cursor: pointer;
border-color: #d0cfcf;
}
Loading