alan
/
sfs
1
Fork 0
sfs/sfs.js

138 lines
3.7 KiB
JavaScript
Executable File

#!/usr/bin/env node
'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 (req.method !== 'GET' && req.method !== 'HEAD') {
send_error(res, 405, 'Method Not Allowed');
return;
}
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,
});
}
if (req.method === 'GET') {
fs.createReadStream(path, range)
.on('error', () => send_error(res, 500, 'Internal Server Error'))
.pipe(res);
} else {
res.end();
}
} else 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(req.method === 'GET' ? `<!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, '&amp;')}
</a>${stats.isFile() ? ` (${humanize_size(stats.size)})` : ''}
</li>`).join('')}
</ul>
</body>
</html>` : undefined);
} else {
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));