diff --git a/src/createServer.js b/src/createServer.js index 1cf1dda..a849f36 100644 --- a/src/createServer.js +++ b/src/createServer.js @@ -1,8 +1,136 @@ '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 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}${extension}`, + ); + 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 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)); + }); + }); + }); + }); });