#!/usr/bin/env node
// vim: set ts=2 sw=2 et tw=80:
const http = require('http');
const url = require('url');
const fs = require('fs');
const formidable = require('formidable');
const routes = Object.create(null);
function noSubPaths(req, path) {
const p = url.parse(req.url);
if (p.pathname != path && p.pathname != path) {
error(res, 404, `Only "${path}" path is accessible under this prefix`);
return true;
} else {
return false;
}
}
function error(res, code, text = '') {
res.writeHead(code, { 'Content-Type': 'text/html' });
res.end(`
Error ${code}
Error ${code}
${text}
SA3 - HW4
`);
}
function fileData(reqUrl, prefix, options = {}) {
const uri = decodeURIComponent(
!options.isPath ? url.parse(reqUrl).pathname.substring(prefix.length) : reqUrl
).replace(/\/+$/, '');
const file = __dirname + '/NodeStaticFiles' + uri;
const name = file.substring(file.lastIndexOf('/') + 1);
const ext = !options.noExt ? name.substring(name.indexOf('.') + 1) : null;
return {
uri: !uri ? '/' : uri,
file: file,
name: name,
ext: ext
};
}
routes['explore'] = (req, res) => {
if (req.method != 'GET') {
error(res, 405, 'Use this URL with only GET requests');
return;
}
const { uri, file, name } = fileData(req.url, '/explore', { noExt: true });
fs.readdir(file, { withFileTypes: true }, (err, dir) => {
if (err) {
error(res, 404, 'Directory not found');
return;
}
const list = [{
name: '.',
path: '/explore' + (uri == '/' ? '' : uri) + '/'
}];
if (uri != '/') {
const parentUri = uri.substring(0, uri.length - name.length - 1);
list.push({ name: '..', path: '/explore' + parentUri });
}
for (const e of dir) {
list.push({
name: e.name,
dir: !e.isFile(),
path: (e.isFile() ? '/file' : '/explore') + uri +
(uri == '/' ? '' : '/') + e.name
});
}
res.writeHead(200, { 'Content-Type': 'text/html' });
res.write(`
${uri} [DIR]
${uri} [DIR]
`);
});
};
routes['file'] = (req, res) => {
const FILE_TYPES = {
html: 'text/html',
css: 'text/css',
txt: 'text/plain',
mp4: 'video/mp4',
ogv: 'video/ogg',
gif: 'image/gif',
jpg: 'image/jpeg',
jpeg: 'image/jpeg',
png: 'image/png',
mp3: 'audio/mpeg',
js: 'application/javascript',
json: 'application/json',
pdf: 'application/pdf',
zip: 'application/zip'
};
if (req.method != 'GET') {
error(res, 405, 'Use this URL with only GET requests');
return;
}
const { file, name, ext } = fileData(req.url, '/file');
fs.readFile(file, (err, data) => {
if (err) {
error(res, 404, 'File not found: ' + JSON.stringify(err));
return;
}
res.setHeader('Content-Disposition', 'attachment; filename="' + name + '"');
res.writeHead(200, { 'Content-Type': ext in FILE_TYPES ? FILE_TYPES[ext] :
'application/octet-stream' });
res.end(data);
});
}
routes['upload'] = (req, res) => {
if (req.method != 'GET' && req.method != 'POST') {
error(res, 405, 'Use this URL with only GET or POST requests');
return;
}
if (req.method == 'POST') {
const form = new formidable.IncomingForm();
form.uploadDir = __dirname + '/NodeStaticFiles';
form.keepExtensions = true;
form.parse(req);
form.on('fileBegin', (name, file) => {
file.path = file.path.substring(0, file.path.lastIndexOf('/') + 1);
file.path += file.name;
});
form.on('end', () => {
res.writeHead(302, { 'Location': '/explore' });
res.end();
});
return;
}
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(`
Upload
Upload
`);
};
function wordMap(string) {
const words = string.trim().split(/[^\w]+/);
const wordDict = {};
for (const word of words) {
if (word.trim() == '') {
continue;
}
if (!wordDict[word]) {
wordDict[word] = 1;
} else {
wordDict[word]++;
}
}
return wordDict;
}
function getFileStats(req, res, path, callback) {
const p = url.parse(req.url, true);
const reqFile = p && p.query && '/' + p.query['file'];
if (!reqFile) {
error(res, 400, 'Search parameter "file" is required');
return;
}
const { file, name, ext } = fileData(reqFile, path, { isPath: true });
if (ext != 'txt' && ext != 'html') {
error(res, 400, `Only txt and html files can be processed by ${path}`);
return;
}
fs.readFile(file, 'utf8', (err, data) => {
if (err) {
error(res, 404, 'File not found: ' + JSON.stringify(err));
return;
}
const dict = wordMap(data);
callback({ reqFile: reqFile, dict: dict });
});
}
routes['stats'] = (req, res) => {
if (noSubPaths(req, '/stats')) {
return;
}
getFileStats(req, res, '/stats', ({ reqFile, dict }) => {
res.writeHead(200, { 'Content-Type': 'text/html' });
res.write(`
Stats for ${reqFile}
Word frequency count for "${reqFile}"
Word | Freq. |
`);
for (const word in dict) {
res.write(`
${word} |
${dict[word]} |
`);
}
res.end(`
`);
});
};
routes['cloud'] = (req, res) => {
if (noSubPaths(req, '/cloud')) {
return;
}
getFileStats(req, res, '/cloud', ({ reqFile, dict }) => {
if (req.headers.accept && req.headers.accept == 'application/json') {
res.writeHead(200, {'Content-Type': 'application/json' });
res.end(JSON.stringify(dict, null, 2));
} else {
res.writeHead(200, { 'Content-Type': 'text/html' });
res.write(`
Cloud for ${reqFile}
Tag cloud for "${reqFile}"
`);
const max = Math.max(...Object.values(dict));
const min = Math.min(...Object.values(dict));
for (const word in dict) {
const size = 0.75 + ((dict[word] - min) / max) * 4 + 'rem';
res.write(` ${word} `);
}
res.end(`
`);
}
});
};
// Main server handler
function onRequest(req, res) {
const pathname = url.parse(req.url).pathname;
const uri = pathname.split('/', 3)[1];
if (typeof routes[uri] === 'function') {
routes[uri](req, res);
} else {
error(res, 404, 'Path not found');
}
}
http.createServer(onRequest).listen(3000);
console.log('Server started at localhost:3000');