130 lines
3.4 KiB
JavaScript
130 lines
3.4 KiB
JavaScript
'use strict';
|
|
|
|
const fs = require('fs');
|
|
const http = require('http');
|
|
const mime = require('mime');
|
|
const readline = require('readline');
|
|
const util = require('util');
|
|
|
|
const readdir = util.promisify(fs.readdir);
|
|
const stat = util.promisify(fs.stat);
|
|
|
|
const send_error = (res, code, message) => {
|
|
res.writeHead(code, { 'content-type': 'text/html; charset=utf-8' });
|
|
res.end(`<!doctype html>
|
|
<html>
|
|
<head>
|
|
<title>${code}: ${message}</title>
|
|
</head>
|
|
<body>
|
|
<h1>${code}: ${message}</h1>
|
|
</body>
|
|
</html>`);
|
|
};
|
|
|
|
const prefixes = ' KMGT';
|
|
const humanize_size = size => {
|
|
let t = 0;
|
|
while (size >= 1024) {
|
|
size /= 1024;
|
|
t++;
|
|
}
|
|
return t ? `${size.toFixed(1)} ${prefixes[t]}B` : `${size} B`;
|
|
};
|
|
|
|
if (process.argv.length < 3 || process.argv.length > 4) {
|
|
console.error('Arguments: <port> [host]');
|
|
process.exit(1);
|
|
}
|
|
|
|
const port = +process.argv[2];
|
|
const host = process.argv[3] || 'localhost';
|
|
|
|
http
|
|
.createServer(async (req, res) => {
|
|
const path = '.' + decodeURIComponent(req.url);
|
|
if (!path.startsWith('./') || path.includes('/..') || path.includes('\\')) {
|
|
send_error(res, 403, 'Forbidden');
|
|
return;
|
|
}
|
|
let stats;
|
|
try {
|
|
stats = await stat(path);
|
|
} catch (e) {
|
|
send_error(res, 404, 'Not Found');
|
|
return;
|
|
}
|
|
if (stats.isFile()) {
|
|
let type = mime.getType(path) || 'application/octet-stream';
|
|
if (
|
|
type.startsWith('text/') ||
|
|
type === 'application/javascript' ||
|
|
type === 'application/json'
|
|
) {
|
|
type += '; charset=utf-8';
|
|
}
|
|
let range = /^bytes=(\d+)-(\d*)$/.exec(req.headers.range);
|
|
if (range) {
|
|
const start = +range[1];
|
|
const end = range[2] ? +range[2] : stats.size - 1;
|
|
range = start <= end && end < stats.size ? { start, end } : null;
|
|
}
|
|
if (range) {
|
|
res.writeHead(206, {
|
|
'content-type': type,
|
|
'content-range': `bytes ${range.start}-${range.end}/${stats.size}`,
|
|
'content-length': range.end - range.start + 1,
|
|
});
|
|
} else {
|
|
res.writeHead(200, {
|
|
'content-type': type,
|
|
'content-length': stats.size,
|
|
});
|
|
}
|
|
fs.createReadStream(path, range)
|
|
.on('error', () => send_error(res, 500, 'Internal Server Error'))
|
|
.pipe(res);
|
|
return;
|
|
}
|
|
if (stats.isDirectory()) {
|
|
if (!req.url.endsWith('/')) {
|
|
res.writeHead(302, { location: req.url + '/' });
|
|
res.end();
|
|
return;
|
|
}
|
|
const dir = (await Promise.all((await readdir(path)).map(async file => [
|
|
file,
|
|
await stat(path + file),
|
|
file.replace(/\d+/g, _ => '1'.repeat(_.length - 1) + '0' + _),
|
|
]))).sort((a, b) => +a[1].isFile() - +b[1].isFile() || a[2].localeCompare(b[2]));
|
|
if (path !== './') {
|
|
dir.unshift(['..', { isFile: () => false, isDirectory: () => true }]);
|
|
}
|
|
res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
|
|
res.end(`<!doctype html>
|
|
<html>
|
|
<head>
|
|
<title>${req.headers.host + decodeURIComponent(req.url)}</title>
|
|
</head>
|
|
<body>
|
|
<h1>${req.headers.host + decodeURIComponent(req.url)}</h1>
|
|
<ul>${dir.map(([file, stats]) => `
|
|
<li>
|
|
<a href="${encodeURIComponent(file)}${stats.isDirectory() ? '/' : ''}">
|
|
${file.replace(/&/g, '&')}
|
|
</a>${stats.isFile() ? ` (${humanize_size(stats.size)})` : ''}
|
|
</li>`).join('')}
|
|
</ul>
|
|
</body>
|
|
</html>`);
|
|
return;
|
|
}
|
|
send_error(res, 500, 'Internal Server Error');
|
|
})
|
|
.listen(port, host);
|
|
|
|
console.log(`Serving on http://${host}:${port} - Press any key to stop`);
|
|
readline.emitKeypressEvents(process.stdin);
|
|
process.stdin.setRawMode(true);
|
|
process.stdin.on('keypress', () => process.exit(0));
|