326 lines
8 KiB
JavaScript
Executable file
326 lines
8 KiB
JavaScript
Executable file
#!/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(`<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset='utf-8'>
|
|
<title>Error ${code}</title>
|
|
</head>
|
|
<body style="text-align: center">
|
|
<h1>Error ${code}</h1>
|
|
<p>${text}</p>
|
|
<hr>
|
|
<p>SA3 - HW4</p>
|
|
</body>
|
|
</html>`);
|
|
}
|
|
|
|
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(`<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>${uri} [DIR]</title>
|
|
</head>
|
|
<body>
|
|
<h1>${uri} [DIR]</h1>
|
|
<ul>`);
|
|
for (const { name, dir, path } of list) {
|
|
res.write(`<li>
|
|
<a href="${path}">${name}${dir ? ' [DIR]' : ''}</a>
|
|
</li>`);
|
|
}
|
|
res.end(`</ul>
|
|
</body>
|
|
</html>`);
|
|
});
|
|
};
|
|
|
|
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(`<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>Upload</title>
|
|
</head>
|
|
<body>
|
|
<h1>Upload</h1>
|
|
<form id="upload-form" enctype="multipart/form-data" method="post">
|
|
<label for="file">File:</label>
|
|
<input type="file" name="file"><br>
|
|
<input type="submit" name="submit" value="Submit">
|
|
</form>
|
|
</body>
|
|
</html>`);
|
|
};
|
|
|
|
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(`<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>Stats for ${reqFile}</title>
|
|
<style>
|
|
table { border-collapse: collapse; }
|
|
td, th { border: 1px solid black; }
|
|
th { font-weight: bold; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>Word frequency count for "${reqFile}"</h1>
|
|
<table id="frequency-tbl">
|
|
<thead>
|
|
<tr><th>Word</th><th>Freq.</th></tr>
|
|
</thead>
|
|
<tbody>`);
|
|
|
|
for (const word in dict) {
|
|
res.write(`<tr>
|
|
<td class="word">${word}</td>
|
|
<td class="frequency">${dict[word]}</td>
|
|
</tr>`);
|
|
}
|
|
|
|
res.end(`</tbody>
|
|
</table>
|
|
</body>
|
|
</html>`);
|
|
});
|
|
};
|
|
|
|
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(`<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>Cloud for ${reqFile}</title>
|
|
<style>
|
|
table { border-collapse: collapse; }
|
|
td, th { border: 1px solid black; }
|
|
th { font-weight: bold; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>Tag cloud for "${reqFile}"</h1>
|
|
<div class="cloud">`);
|
|
|
|
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(`<span style="font-size: ${size}"> ${word} </span>`);
|
|
}
|
|
|
|
res.end(`</div>
|
|
</body>
|
|
</html>`);
|
|
}
|
|
});
|
|
};
|
|
|
|
// 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');
|