-
Notifications
You must be signed in to change notification settings - Fork 275
Solution #198
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Solution #198
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,159 @@ | ||
| 'use strict'; | ||
|
|
||
| const fs = require('node:fs'); | ||
| const http = require('node:http'); | ||
| const path = require('node:path'); | ||
| const { pipeline } = require('node:stream'); | ||
| const zlib = require('node:zlib'); | ||
| const { IncomingForm } = require('formidable'); | ||
|
|
||
| const SUPPORTED_COMPRESSION_TYPES = ['gz', 'dfl', 'br']; | ||
| const CONTENT_TYPES = { | ||
| html: 'text/html', | ||
| css: 'text/css', | ||
| js: 'application/javascript', | ||
| plain: 'text/plain', | ||
| }; | ||
|
|
||
| function encodeFilename(filename) { | ||
| const cleaned = filename | ||
| .replace(/[\r\n\t]/g, '') | ||
| .replace(/["\\]/g, '') | ||
| .trim(); | ||
|
|
||
| const hasNonASCII = [...cleaned].some((char) => char.charCodeAt(0) > 127); | ||
| const hasSpaces = cleaned.includes(' '); | ||
|
|
||
| if (hasNonASCII || hasSpaces) { | ||
| const encoded = encodeURIComponent(cleaned); | ||
|
|
||
| return `filename*=UTF-8''${encoded}`; | ||
| } | ||
|
|
||
| return `filename=${cleaned}`; | ||
| } | ||
|
|
||
| function createCompressionStream(type) { | ||
| const streams = { | ||
| gz: () => zlib.createGzip(), | ||
| dfl: () => zlib.createDeflate(), | ||
| br: () => zlib.createBrotliCompress(), | ||
| }; | ||
|
Comment on lines
+37
to
+41
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To align with the task requirements, the keys in this |
||
|
|
||
| return streams[type](); | ||
| } | ||
|
|
||
| function serveStaticFile(res, filepath, contentType) { | ||
| const fileStream = fs.createReadStream(path.join(__dirname, filepath)); | ||
|
|
||
| res.statusCode = 200; | ||
| res.setHeader('Content-Type', contentType); | ||
|
|
||
| pipeline(fileStream, res, (err) => { | ||
| if (err && !res.headersSent) { | ||
| sendError(res, 500, 'Internal Server Error'); | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| function sendError(res, statusCode, message) { | ||
| res.statusCode = statusCode; | ||
| res.setHeader('Content-Type', CONTENT_TYPES.plain); | ||
| res.end(message); | ||
| } | ||
|
|
||
| function handleCompressRequest(req, res) { | ||
| if (req.method !== 'POST') { | ||
| sendError(res, 400, 'Bad Request'); | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| const form = new IncomingForm(); | ||
| let file = null; | ||
| let compressionType = null; | ||
| let filename = null; | ||
|
|
||
| form.on('file', (name, fileData) => { | ||
| if (name === 'file') { | ||
| file = fileData; | ||
| filename = fileData.originalFilename || 'file.txt'; | ||
| } | ||
| }); | ||
|
|
||
| form.on('field', (name, value) => { | ||
| if (name === 'compressionType') { | ||
| compressionType = value; | ||
| } | ||
| }); | ||
|
|
||
| form.on('end', () => { | ||
| if ( | ||
| !file || | ||
| !compressionType || | ||
| !SUPPORTED_COMPRESSION_TYPES.includes(compressionType) | ||
| ) { | ||
| sendError(res, 400, 'Bad Request'); | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| const compressStream = createCompressionStream(compressionType); | ||
| const encodedFilename = encodeFilename(`${filename}.${compressionType}`); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The file extension for the compressed file doesn't match the requirements for all compression types. The task specifies
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The tests expect tests/createServer.test.js: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The file extensions for This line currently uses the compression type directly, which results in incorrect extensions like
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The tests expect tests/createServer.test.js: They are failing if extensions There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Appending |
||
| const fileStream = fs.createReadStream(file.filepath); | ||
| const tempFilePath = file.filepath; | ||
|
|
||
| res.statusCode = 200; | ||
| res.setHeader('Content-Disposition', `attachment; ${encodedFilename}`); | ||
|
|
||
| pipeline(fileStream, compressStream, res, (err) => { | ||
| if (err && !res.headersSent) { | ||
| sendError(res, 500, 'Internal Server Error'); | ||
| } | ||
|
|
||
| fs.unlink(tempFilePath, () => {}); | ||
| }); | ||
| }); | ||
|
|
||
| form.on('error', () => { | ||
| sendError(res, 400, 'Bad Request'); | ||
| }); | ||
|
|
||
| form.parse(req); | ||
| } | ||
|
|
||
| function createServer() { | ||
| /* Write your code here */ | ||
| // Return instance of http.Server class | ||
| const server = new http.Server(); | ||
|
|
||
| server.on('request', (req, res) => { | ||
| if (req.url === '/') { | ||
| serveStaticFile(res, 'index.html', CONTENT_TYPES.html); | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| if (req.url === '/styles.css') { | ||
| serveStaticFile(res, 'styles.css', CONTENT_TYPES.css); | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| if (req.url === '/script.js') { | ||
| serveStaticFile(res, 'script.js', CONTENT_TYPES.js); | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| if (req.url === '/compress') { | ||
| handleCompressRequest(req, res); | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| sendError(res, 404, 'Not Found'); | ||
| }); | ||
|
|
||
| return server; | ||
| } | ||
|
|
||
| module.exports = { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| <!DOCTYPE html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="UTF-8"> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||
| <title>File Compression Tool</title> | ||
| <link rel="stylesheet" href="/styles.css"> | ||
| </head> | ||
| <body> | ||
| <div class="container"> | ||
| <h1 class="title">File Compression Tool</h1> | ||
|
|
||
| <form action="/compress" enctype="multipart/form-data" method="post" id="compressForm"> | ||
| <div class="form-group"> | ||
| <label for="file" class="label">Select File</label> | ||
| <input type="file" name="file" id="file" class="file-input" required> | ||
| <div class="file-info" id="fileInfo"></div> | ||
| </div> | ||
|
|
||
| <div class="form-group"> | ||
| <label for="compressionType" class="label">Compression Type</label> | ||
| <select name="compressionType" id="compressionType" class="select" required> | ||
| <option value="gz">Gzip</option> | ||
| <option value="dfl">Deflate</option> | ||
| <option value="br">Brotli</option> | ||
| </select> | ||
| </div> | ||
|
|
||
| <button type="submit" id="submitBtn" class="submit-btn">Compress File</button> | ||
| </form> | ||
| </div> | ||
|
|
||
| <script src="/script.js"></script> | ||
| </body> | ||
| </html> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| const fileInput = document.getElementById('file'); | ||
| const fileInfo = document.getElementById('fileInfo'); | ||
| const submitBtn = document.getElementById('submitBtn'); | ||
| const form = document.getElementById('compressForm'); | ||
|
|
||
| function formatFileSize(bytes) { | ||
| return `${(bytes / 1024).toFixed(2)} KB`; | ||
| } | ||
|
|
||
| function extractFilename(contentDisposition) { | ||
| if (!contentDisposition) { | ||
| return 'compressed-file'; | ||
| } | ||
|
|
||
| const rfc5987Match = contentDisposition.match(/filename\*=UTF-8''([^;]+)/); | ||
|
|
||
| if (rfc5987Match && rfc5987Match[1]) { | ||
| return decodeURIComponent(rfc5987Match[1]); | ||
| } | ||
|
|
||
| const filenameMatch = contentDisposition.match( | ||
| /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/, | ||
| ); | ||
|
|
||
| if (filenameMatch && filenameMatch[1]) { | ||
| return filenameMatch[1].replace(/['"]/g, ''); | ||
| } | ||
|
|
||
| return 'compressed-file'; | ||
| } | ||
|
|
||
| function downloadFile(blob, filename) { | ||
| const url = window.URL.createObjectURL(blob); | ||
| const link = document.createElement('a'); | ||
|
|
||
| link.href = url; | ||
| link.download = filename; | ||
| document.body.appendChild(link); | ||
| link.click(); | ||
| window.URL.revokeObjectURL(url); | ||
| document.body.removeChild(link); | ||
| } | ||
|
|
||
| function resetButton(originalText) { | ||
| submitBtn.disabled = false; | ||
| submitBtn.textContent = originalText; | ||
| } | ||
|
|
||
| function clearFileInfo() { | ||
| fileInfo.textContent = ''; | ||
| fileInfo.classList.remove('show'); | ||
| } | ||
|
|
||
| fileInput.addEventListener('change', (e) => { | ||
| const file = e.target.files[0]; | ||
|
|
||
| if (file) { | ||
| fileInfo.textContent = `Selected: ${file.name} (${formatFileSize(file.size)})`; | ||
| fileInfo.classList.add('show'); | ||
| } else { | ||
| clearFileInfo(); | ||
| } | ||
| }); | ||
|
|
||
| form.addEventListener('submit', async (e) => { | ||
| e.preventDefault(); | ||
|
|
||
| const formData = new FormData(form); | ||
| const originalText = submitBtn.textContent; | ||
|
|
||
| submitBtn.disabled = true; | ||
| submitBtn.textContent = 'Compressing...'; | ||
|
|
||
| try { | ||
| const response = await fetch('/compress', { | ||
| method: 'POST', | ||
| body: formData, | ||
| }); | ||
|
|
||
| if (!response.ok) { | ||
| throw new Error(`Server error: ${response.status}`); | ||
| } | ||
|
|
||
| const blob = await response.blob(); | ||
| const contentDisposition = response.headers.get('content-disposition'); | ||
| const filename = extractFilename(contentDisposition); | ||
|
|
||
| downloadFile(blob, filename); | ||
| resetButton(originalText); | ||
| form.reset(); | ||
| clearFileInfo(); | ||
| } catch (error) { | ||
| window.alert(`Error: ${error.message}`); | ||
| resetButton(originalText); | ||
| } | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The task requirements state that the form options for compression type should be
gzip,deflate, andbr. This array should be updated to match those required values, like['gzip', 'deflate', 'br'].