diff --git a/.github/workflows/test.yml-template b/.github/workflows/test.yml-template new file mode 100644 index 0000000..bb13dfc --- /dev/null +++ b/.github/workflows/test.yml-template @@ -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 diff --git a/package-lock.json b/package-lock.json index d0b3b95..3650660 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,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", @@ -1487,10 +1487,11 @@ } }, "node_modules/@mate-academy/scripts": { - "version": "1.8.6", - "resolved": "https://registry.npmjs.org/@mate-academy/scripts/-/scripts-1.8.6.tgz", - "integrity": "sha512-b4om/whj4G9emyi84ORE3FRZzCRwRIesr8tJHXa8EvJdOaAPDpzcJ8A0sFfMsWH9NUOVmOwkBtOXDu5eZZ00Ig==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@mate-academy/scripts/-/scripts-2.1.3.tgz", + "integrity": "sha512-a07wHTj/1QUK2Aac5zHad+sGw4rIvcNl5lJmJpAD7OxeSbnCdyI6RXUHwXhjF5MaVo9YHrJ0xVahyERS2IIyBQ==", "dev": true, + "license": "MIT", "dependencies": { "@octokit/rest": "^17.11.2", "@types/get-port": "^4.2.0", diff --git a/package.json b/package.json index 1d03d64..8e6392d 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..9c943ca --- /dev/null +++ b/public/index.html @@ -0,0 +1,36 @@ + + + + + + + Document + + +

Compress your files!

+
+
+ + +
+ +
+ + +
+ +
+ + diff --git a/src/createServer.js b/src/createServer.js index 1cf1dda..d82fb43 100644 --- a/src/createServer.js +++ b/src/createServer.js @@ -1,8 +1,113 @@ +/* eslint-disable no-console */ 'use strict'; +const { Server } = require('http'); +const fs = require('fs'); +const path = require('path'); +const mime = require('mime-types'); +const { pipeline } = require('stream'); +const zlib = require('zlib'); +const formidable = require('formidable'); + +const compressMap = new Map([ + ['gzip', { ext: 'gz', compressor: () => zlib.createGzip() }], + ['deflate', { ext: 'dfl', compressor: () => zlib.createDeflate() }], + ['br', { ext: 'br', compressor: () => zlib.createBrotliCompress() }], +]); + function createServer() { - /* Write your code here */ - // Return instance of http.Server class + const server = new Server(); + + server.on('request', (req, res) => { + try { + const url = new URL(req.url || '', `http://${req.headers.host}`); + const requestedPath = url.pathname.slice(1) || 'index.html'; + const realPath = path.join('public', requestedPath); + const mimeType = mime.contentType(path.extname(realPath)) || 'text/plain'; + const form = new formidable.IncomingForm(); + + if (requestedPath === 'compress') { + if (req.method === 'POST') { + form.parse(req, (err, fields, files) => { + if (err) { + console.log('[Error]: ', err); + res.statusCode = 400; + res.end('Error: form is invalid'); + + return; + } + + const file = Array.isArray(files.file) ? files.file[0] : files.file; + const compressionType = Array.isArray(fields.compressionType) + ? fields.compressionType[0] + : fields.compressionType; + + if ( + !file || + !compressionType || + !compressMap.has(compressionType) + ) { + res.statusCode = 400; + res.end('Error: File or compressionType is missing'); + + return; + } + + const inputPath = file.filepath; + const readStream = fs.createReadStream(inputPath); + const { ext, compressor } = compressMap.get(compressionType); + + res.writeHead(200, { + 'Content-Type': 'application/octet-stream', + 'Content-Disposition': `attachment; filename=${file.originalFilename}.${ext}`, + }); + + pipeline(readStream, compressor(), res, (error) => { + fs.unlink(file.filepath, () => {}); + + if (error) { + console.log(error); + + if (!res.headersSent) { + res.statusCode = 500; + res.end('Error: Compression failed!'); + } else { + res.destroy(); + } + } + }); + }); + } else { + res.statusCode = 400; + res.end('Error: Wrong method!'); + } + + return; + } + + if (!fs.existsSync(realPath)) { + res.statusCode = 404; + res.end('Not Found'); + + return; + } + + const dataStream = fs.createReadStream(realPath); + + res.statusCode = 200; + res.setHeader('Content-Type', mimeType); + dataStream.pipe(res); + } catch (error) { + res.statusCode = 500; + res.end('Server Error'); + } + }); + + server.on('error', (error) => { + console.error('An error occurred:', error); + }); + + return server; } module.exports = { diff --git a/tests/createServer.test.js b/tests/createServer.test.js index b53ef15..27b2977 100644 --- a/tests/createServer.test.js +++ b/tests/createServer.test.js @@ -29,12 +29,15 @@ function stringToStream(str) { const compressionTypes = { gzip: { decompress: util.promisify(zlib.gunzip), + extension: 'gz', }, deflate: { decompress: util.promisify(zlib.inflate), + extension: 'dfl', }, br: { decompress: util.promisify(zlib.brotliDecompress), + extension: 'br', }, }; @@ -96,7 +99,7 @@ describe('createServer', () => { }); Object.entries(compressionTypes).forEach( - ([compressionType, { decompress }]) => { + ([compressionType, { decompress, extension }]) => { describe(`compression type "${compressionType}"`, () => { it('should respond with 200 status code', () => { expect.assertions(1); @@ -124,7 +127,7 @@ describe('createServer', () => { headers: formData.getHeaders(), }) .then((res) => { - const expectedHeader = `attachment; filename=${filename}.${compressionType}`; + const expectedHeader = `attachment; filename=${filename}.${extension}`; expect(res.headers['content-disposition']).toBe( expectedHeader,