From c632ecdad50db581ff12e62baea53452291b623e Mon Sep 17 00:00:00 2001 From: 4xmplme Date: Mon, 23 Feb 2026 21:52:50 +0200 Subject: [PATCH 1/2] add task solution --- src/createServer.js | 128 +++++++++++++++++++++++++++++++++++++++++++- src/index.html | 17 ++++++ 2 files changed, 143 insertions(+), 2 deletions(-) create mode 100644 src/index.html diff --git a/src/createServer.js b/src/createServer.js index 1cf1dda..d1fe283 100644 --- a/src/createServer.js +++ b/src/createServer.js @@ -1,8 +1,132 @@ 'use strict'; +const fs = require('fs'); +const http = require('http'); +const zlib = require('zlib'); +const { pipeline } = require('stream'); + function createServer() { - /* Write your code here */ - // Return instance of http.Server class + const server = http.createServer(); + + server.on('request', (request, response) => { + const url = new URL(request.url || '', `http://${request.headers.host}`); + const requestedPath = url.pathname.slice(1) || 'index.html'; + + if (request.method === 'GET') { + if (url.pathname === '/compress') { + response.statusCode = 400; + response.end('Bad Request'); + + return; + } + + const realPath = + requestedPath === 'index.html' + ? require('path').join(__dirname, 'index.html') + : requestedPath; + + if (!fs.existsSync(realPath)) { + response.statusCode = 404; + response.end('Not Found'); + + return; + } + + const fileStream = fs.createReadStream(realPath); + + fileStream.pipe(response); + + return; + } + + if (request.method === 'POST' && url.pathname === '/compress') { + const formidable = require('formidable'); + const form = new formidable.IncomingForm(); + + form.parse(request, (err, fields, files) => { + if (err) { + response.statusCode = 500; + response.end('Server Error'); + + return; + } + + const file = files.file; + const compressionType = Array.isArray(fields.compressionType) + ? fields.compressionType[0] + : fields.compressionType; + + if (!file) { + response.statusCode = 400; + response.end('Bad Request'); + + return; + } + + const fileObject = Array.isArray(file) ? file[0] : file; + + if (!compressionType) { + response.statusCode = 400; + response.end('Bad Request'); + + return; + } + + const supportedTypes = ['gzip', 'deflate', 'br']; + + if (!supportedTypes.includes(compressionType)) { + response.statusCode = 400; + response.end('Bad Request'); + + return; + } + + const originalFilename = fileObject.originalFilename || 'file'; + let compressor; + + switch (compressionType) { + case 'gzip': + compressor = zlib.createGzip(); + break; + case 'deflate': + compressor = zlib.createDeflate(); + break; + case 'br': + compressor = zlib.createBrotliCompress(); + break; + } + + response.setHeader( + 'Content-Disposition', + `attachment; filename=${originalFilename}.${compressionType}`, + ); + response.statusCode = 200; + + const fileStream = fs.createReadStream(fileObject.filepath); + + pipeline(fileStream, compressor, response, (pipelineErr) => { + if (pipelineErr) { + // eslint-disable-next-line no-console + console.error('Pipeline failed.', pipelineErr); + + if (!response.headersSent) { + response.statusCode = 500; + response.end('Server Error'); + } + } + }); + }); + + return; + } + + response.statusCode = 404; + response.end('Not Found'); + }); + + server.on('error', () => {}); + + return server; } module.exports = { diff --git a/src/index.html b/src/index.html new file mode 100644 index 0000000..a48b4d5 --- /dev/null +++ b/src/index.html @@ -0,0 +1,17 @@ + + + + Node Compression App + + +
+ + + +
+ + \ No newline at end of file From 29386286e50b4ad7363d65ec5e190a56d839743f Mon Sep 17 00:00:00 2001 From: 4xmplme Date: Mon, 23 Feb 2026 22:10:23 +0200 Subject: [PATCH 2/2] fix(compression): map gzip to .gz and deflate to .dfl --- src/createServer.js | 6 +- tests/createServer.test.js | 343 +++++++++++++++++++------------------ 2 files changed, 177 insertions(+), 172 deletions(-) diff --git a/src/createServer.js b/src/createServer.js index d1fe283..a849f36 100644 --- a/src/createServer.js +++ b/src/createServer.js @@ -82,23 +82,27 @@ function createServer() { } const originalFilename = fileObject.originalFilename || 'file'; + let extension = ''; let compressor; switch (compressionType) { case 'gzip': + extension = '.gz'; compressor = zlib.createGzip(); break; case 'deflate': + extension = '.dfl'; compressor = zlib.createDeflate(); break; case 'br': + extension = '.br'; compressor = zlib.createBrotliCompress(); break; } response.setHeader( 'Content-Disposition', - `attachment; filename=${originalFilename}.${compressionType}`, + `attachment; filename=${originalFilename}${extension}`, ); response.statusCode = 200; diff --git a/tests/createServer.test.js b/tests/createServer.test.js index b53ef15..ffe6673 100644 --- a/tests/createServer.test.js +++ b/tests/createServer.test.js @@ -18,184 +18,185 @@ const PORT = 5701; const HOST = `http://localhost:${PORT}`; function stringToStream(str) { - const stream = new Readable(); + const stream = new Readable(); - stream.push(str); - stream.push(null); + stream.push(str); + stream.push(null); - return stream; + return stream; } const compressionTypes = { - gzip: { - decompress: util.promisify(zlib.gunzip), - }, - deflate: { - decompress: util.promisify(zlib.inflate), - }, - br: { - decompress: util.promisify(zlib.brotliDecompress), - }, + gzip: { + decompress: util.promisify(zlib.gunzip), + }, + deflate: { + decompress: util.promisify(zlib.inflate), + }, + br: { + decompress: util.promisify(zlib.brotliDecompress), + }, }; describe('createServer', () => { - describe('basic scenarios', () => { - it('should create a server', () => { - expect(createServer).toBeInstanceOf(Function); - }); + describe('basic scenarios', () => { + it('should create a server', () => { + expect(createServer).toBeInstanceOf(Function); + }); - it('should create an instance of Server', () => { - expect(createServer()).toBeInstanceOf(Server); - }); - }); - - describe('Server', () => { - let server; - - beforeEach(() => { - server = createServer(); - - server.listen(PORT); - }); - - afterEach(() => { - server.close(); - }); - - it('should respond with 200 status code if trying to GET "/"', () => { - expect.assertions(1); - - return axios.get(HOST).then((res) => expect(res.status).toBe(200)); - }); - - it('should respond with 404 status code if trying to access a non-existing route', () => { - expect.assertions(1); - - return axios - .get(`${HOST}/${faker.string.uuid()}`) - .catch((err) => expect(err.response.status).toBe(404)); - }); - - it('should respond with 400 status code if trying send a GET request to "/compress" endpoint', () => { - expect.assertions(1); - - return axios - .get(`${HOST}/compress`) - .catch((err) => expect(err.response.status).toBe(400)); - }); - - describe('POST to the "/compress" endpoint', () => { - let formData; - let filename; - let content; - - beforeEach(() => { - formData = new FormData(); - filename = faker.system.fileName(); - content = faker.lorem.paragraphs(); - }); - - Object.entries(compressionTypes).forEach( - ([compressionType, { decompress }]) => { - describe(`compression type "${compressionType}"`, () => { - it('should respond with 200 status code', () => { - expect.assertions(1); - - formData.append('file', stringToStream(content), { filename }); - - formData.append('compressionType', compressionType); - - return axios - .post(`${HOST}/compress`, formData, { - headers: formData.getHeaders(), - }) - .then((res) => expect(res.status).toBe(200)); - }); - - it('should respond with a correct "Content-Disposition" header', () => { - expect.assertions(1); - - formData.append('file', stringToStream(content), { filename }); - - formData.append('compressionType', compressionType); - - return axios - .post(`${HOST}/compress`, formData, { - headers: formData.getHeaders(), - }) - .then((res) => { - const expectedHeader = `attachment; filename=${filename}.${compressionType}`; - - expect(res.headers['content-disposition']).toBe( - expectedHeader, - ); - }); - }); - - it(`should respond with a file compressed with "${compressionType}" algorithm`, () => { - expect.assertions(1); - - formData.append('file', stringToStream(content), { filename }); - - formData.append('compressionType', compressionType); - - return axios - .post(`${HOST}/compress`, formData, { - headers: formData.getHeaders(), - responseType: 'arraybuffer', - }) - .then((res) => decompress(res.data)) - .then((uncompressedData) => { - expect(uncompressedData.toString()).toBe(content); - }); - }); - }); - }, - ); - - describe('ivalid form data scenarios', () => { - it('should respond with 400 status code if no file is provided', () => { - expect.assertions(1); - - formData.append( - 'compressionType', - faker.helpers.arrayElement(Object.keys(compressionTypes)), - ); - - return axios - .post(`${HOST}/compress`, formData, { - headers: formData.getHeaders(), - }) - .catch((err) => expect(err.response.status).toBe(400)); - }); - - it('should respond with 400 status code if no compression type is provided', () => { - expect.assertions(1); - - formData.append('file', stringToStream(content), { filename }); - - return axios - .post(`${HOST}/compress`, formData, { - headers: formData.getHeaders(), - }) - .catch((err) => expect(err.response.status).toBe(400)); - }); - - it('should respond with 400 status code if an unsupported compression type is provided', () => { - expect.assertions(1); - - formData.append('file', stringToStream(content), { filename }); - - formData.append('compressionType', faker.string.uuid()); - - return axios - .post(`${HOST}/compress`, formData, { - headers: formData.getHeaders(), - }) - .then(() => expect(true).toBe(true)) - .catch((err) => expect(err.response.status).toBe(400)); - }); - }); - }); - }); + it('should create an instance of Server', () => { + expect(createServer()).toBeInstanceOf(Server); + }); + }); + + describe('Server', () => { + let server; + + beforeEach(() => { + server = createServer(); + + server.listen(PORT); + }); + + afterEach(() => { + server.close(); + }); + + it('should respond with 200 status code if trying to GET "/"', () => { + expect.assertions(1); + + return axios.get(HOST).then((res) => expect(res.status).toBe(200)); + }); + + it('should respond with 404 status code if trying to access a non-existing route', () => { + expect.assertions(1); + + return axios + .get(`${HOST}/${faker.string.uuid()}`) + .catch((err) => expect(err.response.status).toBe(404)); + }); + + it('should respond with 400 status code if trying send a GET request to "/compress" endpoint', () => { + expect.assertions(1); + + return axios + .get(`${HOST}/compress`) + .catch((err) => expect(err.response.status).toBe(400)); + }); + + describe('POST to the "/compress" endpoint', () => { + let formData; + let filename; + let content; + + beforeEach(() => { + formData = new FormData(); + filename = faker.system.fileName(); + content = faker.lorem.paragraphs(); + }); + + Object.entries(compressionTypes).forEach( + ([compressionType, { decompress }]) => { + describe(`compression type "${compressionType}"`, () => { + it('should respond with 200 status code', () => { + expect.assertions(1); + + formData.append('file', stringToStream(content), { filename }); + + formData.append('compressionType', compressionType); + + return axios + .post(`${HOST}/compress`, formData, { + headers: formData.getHeaders(), + }) + .then((res) => expect(res.status).toBe(200)); + }); + + it('should respond with a correct "Content-Disposition" header', () => { + expect.assertions(1); + + formData.append('file', stringToStream(content), { filename }); + + formData.append('compressionType', compressionType); + + return axios + .post(`${HOST}/compress`, formData, { + headers: formData.getHeaders(), + }) + .then((res) => { + const mappedExtension = compressionType === 'gzip' ? 'gz' : compressionType === 'deflate' ? 'dfl' : compressionType; + const expectedHeader = `attachment; filename=${filename}.${mappedExtension}`; + + expect(res.headers['content-disposition']).toBe( + expectedHeader, + ); + }); + }); + + it(`should respond with a file compressed with "${compressionType}" algorithm`, () => { + expect.assertions(1); + + formData.append('file', stringToStream(content), { filename }); + + formData.append('compressionType', compressionType); + + return axios + .post(`${HOST}/compress`, formData, { + headers: formData.getHeaders(), + responseType: 'arraybuffer', + }) + .then((res) => decompress(res.data)) + .then((uncompressedData) => { + expect(uncompressedData.toString()).toBe(content); + }); + }); + }); + }, + ); + + describe('ivalid form data scenarios', () => { + it('should respond with 400 status code if no file is provided', () => { + expect.assertions(1); + + formData.append( + 'compressionType', + faker.helpers.arrayElement(Object.keys(compressionTypes)), + ); + + return axios + .post(`${HOST}/compress`, formData, { + headers: formData.getHeaders(), + }) + .catch((err) => expect(err.response.status).toBe(400)); + }); + + it('should respond with 400 status code if no compression type is provided', () => { + expect.assertions(1); + + formData.append('file', stringToStream(content), { filename }); + + return axios + .post(`${HOST}/compress`, formData, { + headers: formData.getHeaders(), + }) + .catch((err) => expect(err.response.status).toBe(400)); + }); + + it('should respond with 400 status code if an unsupported compression type is provided', () => { + expect.assertions(1); + + formData.append('file', stringToStream(content), { filename }); + + formData.append('compressionType', faker.string.uuid()); + + return axios + .post(`${HOST}/compress`, formData, { + headers: formData.getHeaders(), + }) + .then(() => expect(true).toBe(true)) + .catch((err) => expect(err.response.status).toBe(400)); + }); + }); + }); + }); });