Zqdn/Tools/FileServer/src/static-server.js

334 lines
9.9 KiB
JavaScript
Raw Normal View History

2025-04-18 19:18:15 +08:00
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;