[+] FileServer

[+] FileServer
This commit is contained in:
ALEXTANG
2023-04-25 15:12:28 +08:00
parent 4db58f507d
commit 1c0a570d74
15 changed files with 601 additions and 100 deletions

View File

@@ -0,0 +1,12 @@
const path = require('path');
const mime = require('mime');
const lookup = (pathName) => {
let ext = path.extname(pathName);
ext = ext.split('.').pop();
return mime.getType(ext) || mime.getType('txt');
}
module.exports = {
lookup
};

View File

@@ -0,0 +1,334 @@
const http = require('http');
const https = require('https');
const path = require('path');
const fs = require('fs');
const url = require('url');
const zlib = require('zlib');
const chalk = require('chalk');
const os = require('os');
const open = require("open");
const Handlebars = require('handlebars');
const pem = require('pem');
const mime = require('./mime');
const Template = require('./templates');
const _defaultTemplate = Handlebars.compile(Template.page_dafault);
const _404TempLate = Handlebars.compile(Template.page_404);
const hasTrailingSlash = url => url[url.length - 1] === '/';
const ifaces = os.networkInterfaces();
class StaticServer {
constructor(options) {
this.port = options.port;
this.indexPage = options.index;
this.openIndexPage = options.openindex;
this.openBrowser = options.openbrowser;
this.charset = options.charset;
this.cors = options.cors;
this.protocal = options.https ? 'https' : 'http';
this.zipMatch = '^\\.(css|js|html)$';
}
/**
* 响应错误
*
* @param {*} err
* @param {*} res
* @returns
* @memberof StaticServer
*/
respondError(err, res) {
res.writeHead(500);
return res.end(err);
}
/**
* 响应404
*
* @param {*} req
* @param {*} res
* @memberof StaticServer
*/
respondNotFound(req, res) {
res.writeHead(404, {
'Content-Type': 'text/html'
});
const html = _404TempLate();
res.end(html);
}
respond(pathName, req, res) {
fs.stat(pathName, (err, stat) => {
if (err) return respondError(err, res);
this.responseFile(stat, pathName, req, res);
});
}
/**
* 判断是否需要解压
*
* @param {*} pathName
* @returns
* @memberof StaticServer
*/
shouldCompress(pathName) {
return path.extname(pathName).match(this.zipMatch);
}
/**
* 解压文件
*
* @param {*} readStream
* @param {*} req
* @param {*} res
* @returns
* @memberof StaticServer
*/
compressHandler(readStream, req, res) {
const acceptEncoding = req.headers['accept-encoding'];
if (!acceptEncoding || !acceptEncoding.match(/\b(gzip|deflate)\b/)) {
return readStream;
} else if (acceptEncoding.match(/\bgzip\b/)) {
res.setHeader('Content-Encoding', 'gzip');
return readStream.pipe(zlib.createGzip());
}
}
/**
* 响应文件路径
*
* @param {*} stat
* @param {*} pathName
* @param {*} req
* @param {*} res
* @memberof StaticServer
*/
responseFile(stat, pathName, req, res) {
// 设置响应头
res.setHeader('Content-Type', `${mime.lookup(pathName)}; charset=${this.charset}`);
res.setHeader('Accept-Ranges', 'bytes');
// 添加跨域
if (this.cors) res.setHeader('Access-Control-Allow-Origin', '*');
let readStream;
readStream = fs.createReadStream(pathName);
if (this.shouldCompress(pathName)) { // 判断是否需要解压
readStream = this.compressHandler(readStream, req, res);
}
readStream.pipe(res);
}
/**
* 响应重定向
*
* @param {*} req
* @param {*} res
* @memberof StaticServer
*/
respondRedirect(req, res) {
const location = req.url + '/';
res.writeHead(301, {
'Location': location,
'Content-Type': 'text/html'
});
const html = _defaultTemplate({
htmlStr: `Redirecting to <a href='${location}'>${location}</a>`,
showFileList: false
})
res.end(html);
}
/**
* 响应文件夹路径
*
* @param {*} pathName
* @param {*} req
* @param {*} res
* @memberof StaticServer
*/
respondDirectory(pathName, req, res) {
const indexPagePath = path.join(pathName, this.indexPage);
// 如果文件夹下存在index.html则默认打开
if (this.openIndexPage && fs.existsSync(indexPagePath)) {
this.respond(indexPagePath, req, res);
} else {
fs.readdir(pathName, (err, files) => {
if (err) {
respondError(err, res);
}
const requestPath = url.parse(req.url).pathname;
const fileList = [];
files.forEach(fileName => {
let itemLink = path.join(requestPath, fileName);
let isDirectory = false;
const stat = fs.statSync(path.join(pathName, fileName));
if (stat && stat.isDirectory()) {
itemLink = path.join(itemLink, '/');
isDirectory = true;
}
fileList.push({
link: itemLink,
name: fileName,
isDirectory
});
});
// 排序,目录在前,文件在后
fileList.sort((prev, next) => {
if (prev.isDirectory && !next.isDirectory) {
return -1;
}
return 1;
});
res.writeHead(200, {
'Content-Type': 'text/html'
});
const html = _defaultTemplate({
requestPath,
fileList,
showFileList: true
})
res.end(html);
});
}
}
/**
* 路由处理
*
* @param {*} pathName
* @param {*} req
* @param {*} res
* @memberof StaticServer
*/
routeHandler(pathName, req, res) {
const realPathName = pathName.split('?')[0];
fs.stat(realPathName, (err, stat) => {
this.logGetInfo(err, pathName);
if (!err) {
const requestedPath = url.parse(req.url).pathname;
// 检查url
// 如果末尾有'/',且是文件夹,则读取文件夹
// 如果是文件夹,但末尾没'/',则重定向至'xxx/'
// 如果是文件,则判断是否是压缩文件,是则解压,不是则读取文件
if (hasTrailingSlash(requestedPath) && stat.isDirectory()) {
this.respondDirectory(realPathName, req, res);
} else if (stat.isDirectory()) {
this.respondRedirect(req, res);
} else {
this.respond(realPathName, req, res);
}
} else {
this.respondNotFound(req, res);
}
});
}
/**
* 打印ip地址
*
* @memberof StaticServer
*/
logUsingPort() {
const me = this;
console.log(`${chalk.yellow(`Starting up your server\nAvailable on:`)}`);
Object.keys(ifaces).forEach(function (dev) {
ifaces[dev].forEach(function (details) {
if (details.family === 'IPv4') {
console.log(` ${me.protocal}://${details.address}:${chalk.green(me.port)}`);
}
});
});
console.log(`${chalk.cyan(Array(50).fill('-').join(''))}`);
}
/**
* 打印占用端口
*
* @param {*} oldPort
* @param {*} port
* @memberof StaticServer
*/
logUsedPort(oldPort, port) {
const me = this;
console.log(`${chalk.red(`The port ${oldPort} is being used, change to port `)}${chalk.green(me.port)} `);
}
/**
* 打印https证书友好提示
*
* @memberof StaticServer
*/
logHttpsTrusted() {
console.log(chalk.green('Currently is using HTTPS certificate (Manually trust it if necessary)'));
}
/**
* 打印路由路径输出
*
* @param {*} isError
* @param {*} pathName
* @memberof StaticServer
*/
logGetInfo(isError, pathName) {
if (isError) {
console.log(chalk.red(`404 ${pathName}`));
} else {
console.log(chalk.cyan(`200 ${pathName}`));
}
}
startServer(keys) {
const me = this;
let isPostBeUsed = false;
const oldPort = me.port;
const protocal = me.protocal === 'https' ? https : http;
const options = me.protocal === 'https' ? { key: keys.serviceKey, cert: keys.certificate } : null;
const callback = (req, res) => {
const pathName = path.join(process.cwd(), path.normalize(decodeURI(req.url)));
me.routeHandler(pathName, req, res);
};
const params = [callback];
if (me.protocal === 'https') params.unshift(options);
const server = protocal.createServer(...params).listen(me.port);
server.on('listening', function () { // 执行这块代码说明端口未被占用
if (isPostBeUsed) {
me.logUsedPort(oldPort, me.port);
}
me.logUsingPort();
if (me.openBrowser) {
open(`${me.protocal}://127.0.0.1:${me.port}`);
}
});
server.on('error', function (err) {
if (err.code === 'EADDRINUSE') { // 端口已经被使用
isPostBeUsed = true;
me.port = parseInt(me.port) + 1;
server.listen(me.port);
} else {
console.log(err);
}
})
}
start() {
const me = this;
if (this.protocal === 'https') {
pem.createCertificate({ days: 1, selfSigned: true }, function (err, keys) {
if (err) {
throw err
}
me.logHttpsTrusted();
me.startServer(keys);
})
} else {
me.startServer();
}
}
}
module.exports = StaticServer;

View File

@@ -0,0 +1,44 @@
module.exports = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<meta http-equiv="X-UA-Compatible" content="ie=edge"/>
<title>node静态服务器</title>
<style>
.not-found-content {
display: flex;
justify-content: center;
min-height: 500px;
align-items: center;
}
.not-found-content .img-notfound {
margin-right: 50px;
}
.not-found-content h3 {
color: #333;
font-size: 24px;
margin: 20px 0;
font-weight: 400;
line-height: 24px;
}
.not-found-content p {
color: #666;
font-size: 16px;
line-height: 20px;
margin-bottom: 7px;
}
</style>
</head>
<body>
<div class="not-found-content">
<img src="https://img.alicdn.com/tfs/TB1txw7bNrI8KJjy0FpXXb5hVXa-260-260.png" class="img-notfound" alt="not found">
<div class="prompt">
<h3>抱歉,你访问的路径不存在</h3>
<p>您要找的页面没有找到,请返回<a class="link-font" href="/">首页</a>继续浏览</p>
</div>
</div>
</body>
</html>
`

View File

@@ -0,0 +1,93 @@
module.exports = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>node静态服务器</title>
<style>
html, body, ul, li, p{
padding: 0;
margin: 0;
}
html, body {
padding: 0;
margin: 0;
width: 100%;
height: 100%;
}
.app {
padding: 20px 50px 0;
min-height: calc(100% - 70px);
overflow: hidden;
color: #333;
}
.directory li {
list-style: circle;
margin-left: 20px;
}
.directory li p {
line-height: 1.7;
margin: 14px 0;
}
.directory li p a{
color: #333;
font-weight: 400;
text-decoration: none;
}
.directory li p span{
color: #3dcccc;
}
.directory li p a:hover {
color: red;
}
.footer {
text-align: center;
height: 50px;
font-size: 12px;
}
.footer span {
display: block;
line-height: 24px;
}
.footer .bold {
font-weight: 600;
}
.footer a {
color: #333;
}
.footer a:hover {
color: red;
}
</style>
</head>
<body>
<div class="app">
<h3>当前目录:<span>{{requestPath}}</span></h3>
{{#if showFileList}}
<ul class="directory">
{{#each fileList}}
<li>
<p>
{{#if isDirectory }}
<span>「目录」</span>
{{else}}
<span>「文件」</span>
{{/if}}
<a href='{{link}}'>{{name}}</a>
</p>
</li>
{{/each}}
</ul>
{{else}}
{{htmlStr}}
{{/if}}
</div>
<div class="footer">
<span>Github地址: <a target="_blank" href="https://github.com/WisestCoder/static-server">https://github.com/WisestCoder/static-server</a></span>
<span class="bold">By <a target="_blank" href="https://github.com/WisestCoder">WisestCoder</a></span>
</div>
</body>
</html>
`;

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,7 @@
const page_dafault = require('./default');
const page_404 = require('./404');
module.exports = {
page_dafault,
page_404
};