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..5551314 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,16 +9,19 @@ "version": "1.0.0", "hasInstallScript": true, "license": "GPL-3.0", + "dependencies": { + "mime-types": "3.0.1" + }, "devDependencies": { "@faker-js/faker": "^8.4.1", "@mate-academy/eslint-config": "latest", - "@mate-academy/scripts": "^1.8.6", + "@mate-academy/scripts": "2.1.2", "axios": "^1.7.2", "eslint": "^8.57.0", "eslint-plugin-jest": "^28.6.0", "eslint-plugin-node": "^11.1.0", "form-data": "^4.0.0", - "formidable": "^3.5.1", + "formidable": "3.5.4", "jest": "^29.7.0", "prettier": "^3.3.2" } @@ -1487,10 +1490,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.2", + "resolved": "https://registry.npmjs.org/@mate-academy/scripts/-/scripts-2.1.2.tgz", + "integrity": "sha512-gUXFdqqOfYzF9R3RSx2pCa5GLdOkxB9bFbF+dpUpzucdgGAANqOGdqpmNnMj+e3xA9YHraUWq3xo9cwe5vD9pQ==", "dev": true, + "license": "MIT", "dependencies": { "@octokit/rest": "^17.11.2", "@types/get-port": "^4.2.0", @@ -1516,6 +1520,19 @@ "eslint-scope": "5.1.1" } }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1885,6 +1902,16 @@ "@octokit/openapi-types": "^22.2.0" } }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", + "integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, "node_modules/@pkgr/core": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", @@ -4269,16 +4296,43 @@ "node": ">= 6" } }, + "node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/formidable": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.1.tgz", - "integrity": "sha512-WJWKelbRHN41m5dumb0/k8TeAx7Id/y3a+Z7QfhxP/htI9Js5zYaEDtG8uMgG0vM0lOlqnmjE99/kfpOYi/0Og==", + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz", + "integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==", "dev": true, + "license": "MIT", "dependencies": { + "@paralleldrive/cuid2": "^2.2.2", "dezalgo": "^1.0.4", - "hexoid": "^1.0.0", "once": "^1.4.0" }, + "engines": { + "node": ">=14.0.0" + }, "funding": { "url": "https://ko-fi.com/tunnckoCore/commissions" } @@ -4624,15 +4678,6 @@ "node": ">= 0.4" } }, - "node_modules/hexoid": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz", - "integrity": "sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -6969,21 +7014,21 @@ } }, "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", "dependencies": { - "mime-db": "1.52.0" + "mime-db": "^1.54.0" }, "engines": { "node": ">= 0.6" diff --git a/package.json b/package.json index 1d03d64..9682a73 100644 --- a/package.json +++ b/package.json @@ -18,17 +18,20 @@ "devDependencies": { "@faker-js/faker": "^8.4.1", "@mate-academy/eslint-config": "latest", - "@mate-academy/scripts": "^1.8.6", + "@mate-academy/scripts": "2.1.2", "axios": "^1.7.2", "eslint": "^8.57.0", "eslint-plugin-jest": "^28.6.0", "eslint-plugin-node": "^11.1.0", "form-data": "^4.0.0", - "formidable": "^3.5.1", + "formidable": "3.5.4", "jest": "^29.7.0", "prettier": "^3.3.2" }, "mateAcademy": { "projectType": "javascript" + }, + "dependencies": { + "mime-types": "3.0.1" } } diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..e353607 Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..24bb9fc --- /dev/null +++ b/public/index.html @@ -0,0 +1,32 @@ + + + + + + Compression App + + + +
+

Welcome to Free Compression Server

+ +
+ + + +
+
+ + diff --git a/public/style.css b/public/style.css new file mode 100644 index 0000000..4e69b8c --- /dev/null +++ b/public/style.css @@ -0,0 +1,24 @@ +body { + padding: 0; + margin: 0; +} + +.main-header { + font-size: 36px; + line-height: 140%; + margin-bottom: 40px; +} + +.main-container { + height: 100vh; + width: 100vw; + padding: 2em; + + color: white; + box-sizing: border-box; + background-color: tan; + + font-family: Arial, Helvetica, sans-serif; + font-size: 16px; + line-height: 100%; +} diff --git a/public/uploads/prize.bin b/public/uploads/prize.bin new file mode 100644 index 0000000..da33a00 --- /dev/null +++ b/public/uploads/prize.bin @@ -0,0 +1,3 @@ +Audentia conitor vulgaris excepturi vehemens adicio aliquid supellex auctor tenetur. Causa solutio ex deficio demulceo solium tamisium. Amplus cupressus tremo demoror aestus. +Deputo denuo caste spoliatio. Cedo congregatio cohors verumtamen admoneo unde bellicus delego. Cohors triduana accusator curo damnatio doloribus. +Suggero cedo creptio quae. Repudiandae vergo absque cupiditas. Deripio celo tantillus. \ No newline at end of file diff --git a/src/createServer.js b/src/createServer.js index 1cf1dda..2fd82f3 100644 --- a/src/createServer.js +++ b/src/createServer.js @@ -1,8 +1,256 @@ 'use strict'; +const http = require('http'); +const path = require('path'); +const fs = require('fs'); +const zlib = require('zlib'); +const { formidable } = require('formidable'); +const { pipeline } = require('stream'); +const mime = require('mime-types'); + function createServer() { - /* Write your code here */ - // Return instance of http.Server class + const server = http.createServer(); + const PUBLIC_PATH = path.join(__dirname, '..', 'public'); + + server.on('request', (req, res) => { + // index.html and style.css responses + if (req.url === '/') { + res.setHeader('Content-type', 'text/html'); + + const INDEX_PATH = path.resolve(PUBLIC_PATH, 'index.html'); + + if (!fs.existsSync(INDEX_PATH)) { + res.statusCode = 404; + res.end('Not Found Page'); + + return; + } + + const file = fs.createReadStream(INDEX_PATH); + + file.pipe(res); + + res.on('close', () => { + file.destroy(); + }); + + return; + } + + if (req.url === '/favicon.ico') { + const favicon = fs.createReadStream(path.resolve('public/favicon.ico')); + + res.statusCode = 200; + favicon.pipe(res); + + return; + } + + if (req.url === '/style.css') { + res.setHeader('Content-type', 'text/css'); + + const STYLES_PATH = path.resolve(PUBLIC_PATH, 'style.css'); + + if (!fs.existsSync(STYLES_PATH)) { + res.end(''); + + return; + } + + const cssFile = fs.createReadStream(STYLES_PATH); + + cssFile.pipe(res); + + res.on('close', () => { + cssFile.destroy(); + }); + + return; + } + + // Compression Logic + if (req.url === '/compress') { + // check, if method === POST, otherwise 400 + if (req.method !== 'POST') { + res.statusCode = 400; + res.end('Wrong request method!'); + + return; + } + + // let's create a Form object from Formidable lib, + // which will help us to deal with Form data + + const form = formidable({ + multiples: false, + uploadDir: path.resolve('public/uploads'), + }); + + // use mime-types from NPM to detect content-type of archive + let mimeType = ''; + + form.parse(req, (err, fields, files) => { + if (err) { + res.statusCode = 400; + res.end(err); + + return; + } + // let's fetch compressionType & file obiviosly + + const compressionType = fields.compressionType[0]; + + const uploadedFile = files.file[0]; + + // double sanity for non-valid input + if (!compressionType || !uploadedFile) { + res.statusCode = 400; + res.end('Invalid input!'); + + return; + } + + // let's rename uploaded file to it's original filename + + const correctUploadedFilePath = path.resolve( + 'public/uploads', + uploadedFile.originalFilename, + ); + + fs.rename(uploadedFile.filepath, correctUploadedFilePath, (_err) => { + if (_err) { + res.statusCode = 500; + res.end('Error while renaming uploaded file!'); + + return; + } + + const readStream = fs.createReadStream(correctUploadedFilePath); + + // if everything is OK, let's pipe uploaded file to chosen compression + switch (compressionType) { + case 'gzip': { + const gzip = zlib.createGzip(); + + // let's get rid from whitespaces(if present) in original filename + // because they can provoke an error: + // ERR_RESPONSE_HEADERS_MULTIPLE_CONTENT_DISPOSITION + + const zippedFilePath = + path.basename(correctUploadedFilePath) + '.gz'; + + mimeType = mime.contentType('gzip'); + res.statusCode = 200; + + res.setHeader( + 'Content-Disposition', + `attachment; filename=${zippedFilePath}`, + ); + res.setHeader('Content-Type', mimeType); + res.setHeader('Content-Encoding', 'gzip'); + + res.on('close', () => { + readStream.destroy(); + }); + + pipeline(readStream, gzip, res, (error) => { + res.statusCode = 500; + res.end(`Error while piping: ${error}`); + }); + + // and if everything is OK, let's finalize + // our Response with all needed headers + + return; + } + + case 'deflate': { + const deflate = zlib.createDeflate(); + + // let's get rid from whitespaces(if present) in original filename + // because they can provoke an error: + // ERR_RESPONSE_HEADERS_MULTIPLE_CONTENT_DISPOSITION + + const zippedFilePath = + path.basename(correctUploadedFilePath) + '.dfl'; + + mimeType = mime.contentType('deflate'); + + res.statusCode = 200; + + res.setHeader( + 'Content-Disposition', + `attachment; filename=${zippedFilePath}`, + ); + res.setHeader('Content-Type', mimeType); + res.setHeader('Content-Encoding', 'deflate'); + + res.on('close', () => { + readStream.destroy(); + }); + + pipeline(readStream, deflate, res, (error) => { + res.statusCode = 500; + res.end(`Error while piping: ${error}`); + }); + + // and if everything is OK, let's finalize + // our Response with all needed headers + + return; + } + + case 'br': { + const deflate = zlib.createBrotliCompress(); + + // let's get rid from whitespaces(if present) in original filename + // because they can provoke an error: + // ERR_RESPONSE_HEADERS_MULTIPLE_CONTENT_DISPOSITION + + const zippedFilePath = + path.basename(correctUploadedFilePath) + '.br'; + + mimeType = mime.contentType('br'); + + // and if everything is OK, let's finalize + // our Response with all needed headers + res.statusCode = 200; + + res.setHeader( + 'Content-Disposition', + `attachment; filename=${zippedFilePath}`, + ); + res.setHeader('Content-Type', mimeType); + res.setHeader('Content-Encoding', 'br'); + + res.on('close', () => { + readStream.destroy(); + }); + + pipeline(readStream, deflate, res, (error) => { + res.statusCode = 500; + res.end(`Error while piping: ${error}`); + }); + + return; + } + + default: { + res.statusCode = 400; + res.end('Wrong type of compression sended!'); + } + } + }); + }); + + return; + } + + res.statusCode = 404; + res.end('Not found'); + }); + + return server; } module.exports = {