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
23 changes: 23 additions & 0 deletions .github/workflows/test.yml-template
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: Test

on:
pull_request:
branches: [ master ]

jobs:
build:

runs-on: ubuntu-latest

strategy:
matrix:
node-version: [20.x]

steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- run: npm install
- run: npm test
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"devDependencies": {
"@faker-js/faker": "^8.4.1",
"@mate-academy/eslint-config": "latest",
"@mate-academy/scripts": "^1.8.6",
"@mate-academy/scripts": "^2.1.3",
"axios": "^1.7.2",
"eslint": "^8.57.0",
"eslint-plugin-jest": "^28.6.0",
Expand Down
110 changes: 105 additions & 5 deletions src/createServer.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,110 @@
/* eslint-disable no-console */
'use strict';

const http = require('http');
const zlib = require('zlib');
const { pipeline, Readable } = require('stream');

function createServer() {
/* Write your code here */
// Return instance of http.Server class
return http.createServer((req, res) => {
const { method, url } = req;

if (url === '/' && method === 'GET') {
res.writeHead(200, { 'Content-type': 'text/html' });

return res.end(`
<form action="/compress" method="POST" enctype="multipart/form-data">
<input type="file" name="file" required>
<select name="compressionType">
<option value="gzip">gzip</option>
<option value="deflate">deflate</option>
<option value="br">br</option>
</select>
<button type="submit">Compress</button>
</form>
`);
}

if (url !== '/compress') {
res.statusCode = 404;

return res.end('The endpoint does not exist');
}

if (method !== 'POST') {
res.statusCode = 400;

return res.end('Only POST requests are allowed');
}

let bodyBuffer = Buffer.alloc(0);
let isStreamingStarted = false;

req.on('data', (chunk) => {

Choose a reason for hiding this comment

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

This implementation has a few issues. First, by buffering the entire request with Buffer.concat, it defeats the purpose of using streams for potentially large files, risking out-of-memory errors. Second, the isStreamingStarted check on the next line will cause your server to ignore all subsequent data chunks after the file headers are found, leading to truncated and corrupted files. The file's content needs to be piped from the request stream to the compression stream as it arrives.

if (isStreamingStarted) {
return;
}

bodyBuffer = Buffer.concat([bodyBuffer, chunk]);

const bodyStr = bodyBuffer.toString('binary');
const filePartHeaderMatch = bodyStr.match(
/filename="(.+?)"\r\nContent-Type: .+?\r\n\r\n/,
);
const typeMatch = bodyStr.match(
/name="compressionType"\r\n\r\n(.+?)\r\n/,
);

if (filePartHeaderMatch && typeMatch) {
isStreamingStarted = true;

const filename = filePartHeaderMatch[1];
const compressionType = typeMatch[1].trim();

const validTypes = {
gzip: { create: zlib.createGzip, ext: 'gz' },
deflate: { create: zlib.createDeflate, ext: 'dfl' },
br: { create: zlib.createBrotliCompress, ext: 'br' },
};

const config = validTypes[compressionType];

if (!config) {
res.statusCode = 400;

return res.end('Unsupported compression type');
}

// Prepare response
res.writeHead(200, {
'Content-Type': 'application/octet-stream',
'Content-Disposition': `attachment; filename="${filename}.${compressionType}"`,

Choose a reason for hiding this comment

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

The task requires using the correct file extension for the compressed file (e.g., .gz for gzip). This line is using the full compression type name (gzip) instead of the extension. You have the correct extension available in config.ext which you defined on lines 65-67.

});

const headerEndIndex =
bodyStr.indexOf(filePartHeaderMatch[0]) +
filePartHeaderMatch[0].length;
const initialFileData = bodyBuffer.slice(headerEndIndex);

const boundary = bodyStr.split('\r\n')[0];
const footerIndex = initialFileData
.toString('binary')
.indexOf(boundary);

const actualFileSource = Readable.from(
footerIndex !== -1
? initialFileData.slice(0, footerIndex - 2)
: initialFileData,
);

pipeline(actualFileSource, config.create(), res, (err) => {
if (err) {
console.error(err);
}
});
}
});

Choose a reason for hiding this comment

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

The server needs to handle cases where the form submission is invalid (e.g., no file is uploaded) and respond with a 400 status code as required. Currently, if the filePartHeaderMatch and typeMatch are never found, the request will hang until it times out. Consider adding a req.on('end', ...) event listener to check if streaming has started. If not, it means the form was invalid and you should send the 400 error.

});
}

module.exports = {
createServer,
};
module.exports = { createServer };
Loading