diff --git a/Luban/FileServer/.gitignore b/Luban/FileServer/.gitignore new file mode 100644 index 00000000..a52d3c3c --- /dev/null +++ b/Luban/FileServer/.gitignore @@ -0,0 +1,8 @@ +node_modules +*.swap +.idea +.DS_Store +*.log +.vscode +*-lock.json +AssetsRoot \ No newline at end of file diff --git a/Luban/FileServer/FileSys.js b/Luban/FileServer/FileSys.js deleted file mode 100644 index c48b7354..00000000 --- a/Luban/FileServer/FileSys.js +++ /dev/null @@ -1,98 +0,0 @@ -//----------------------------------------------------------------------- -// Copyright (c) TEngine. All rights reserved. -// Author: TangXiao -// Date: 2022/5/14 16:29:13 -//----------------------------------------------------------------------- -const http = require("http"); -const url = require("url"); -const fs = require("fs"); -const path = require("path"); -const mime = { - css: "text/css", - gif: "image/gif", - html: "text/html", - ico: "image/x-icon", - jpeg: "image/jpeg", - jpg: "image/jpeg", - js: "text/javascript", - json: "application/json", - pdf: "application/pdf", - png: "image/png", - svg: "image/svg+xml", - swf: "application/x-shockwave-flash", - tiff: "image/tiff", - txt: "text/plain", - wav: "audio/x-wav", - wma: "audio/x-ms-wma", - wmv: "video/x-ms-wmv", - xml: "text/xml", -}; -const port = 8088; - -const httpServer = http.createServer((request, response) => { - const requestUrl = request.url; - let pathName = url.parse(requestUrl).pathname; - - // 对路径解码,防止中文乱码 - pathName = decodeURI(pathName); - - // 绝对路径 - const filePath = path.resolve(__dirname + pathName); - - // 扩展名 - let ext = path.extname(pathName); - ext = ext ? ext.slice(1) : "unknown"; - - // 未知的类型一律用"text/plain"类型 - const contentType = mime[ext] || "text/plain"; - - // fs.stat()方法用于判断给定的路径是否存在 - fs.stat(filePath, (err, stats) => { - // 路径不存在,则返回404 - if (err) { - response.writeHead(404, { "content-type": "text/html" }); - response.end("

404 Not Found

"); - } - // 如果是文件 - if (!err && stats.isFile()) { - response.writeHead(200, { "content-type": contentType }); - // 建立流对象,读文件 - const stream = fs.createReadStream(filePath); - // 错误处理 - stream.on("error", function() { - response.writeHead(500, { "content-type": contentType }); - - response.end("

500 Server Error

"); - }); - // 读取文件 - stream.pipe(response); - //response.end(); // 这个地方有坑,加了会关闭对话,看不到内容了 - } - // 如果是路径 - if (!err && stats.isDirectory()) { - let html = " "; - // 读取该路径下文件 - fs.readdir(filePath, (err, files) => { - if (err) { - response.writeHead(500, { "content-type": contentType }); - response.end("

路径读取失败!

"); - } else { - for (const file of files) { - if (file === "index.html") { - response.writeHead(200, { "content-type": "text/html" }); - response.end(file); - break; - } - html += `
${file}
`; - } - response.writeHead(200, { "content-type": "text/html" }); - response.end(html); - } - }); - } - }); -}); - -httpServer.listen(port, function() { - console.log(`File Service: ${port}`); -}); \ No newline at end of file diff --git a/Luban/FileServer/README.md b/Luban/FileServer/README.md new file mode 100644 index 00000000..4f955671 --- /dev/null +++ b/Luban/FileServer/README.md @@ -0,0 +1,42 @@ +## 使用node搭建静态资源服务器 + +### 安装 + +```bash +npm install yumu-static-server -g +``` + +### 使用 + +```bash +server # 会在当前目录下启动一个静态资源服务器,默认端口为8080 + +server -p[port] 3000 # 会在当前目录下启动一个静态资源服务器,端口为3000 + +server -i[index] index.html # 设置文件夹在默认加载的文件 + +server -c[charset] UTF-8 # 设置文件默认加载的字符编码 + +server -cors # 开启文件跨域 + +server -h[https] # 开启https服务 + +server --openindex # 是否打开默认页面 + +server --no-openbrowser # 关闭自动打开浏览器 +``` + +### 基本功能 + +1. 启动静态资源服务器 +2. 端口可配置 +3. 字符编码可配置 +4. 文件夹下默认加载文件可配置 +5. 是否跨域可配置 +6. 开启https服务 + +### TODO + +- [x] 引入handlerbars编译模板 +- [x] 支持文件是否跨域 +- [x] 支持https服务 diff --git a/Luban/FileServer/bin/app.js b/Luban/FileServer/bin/app.js new file mode 100644 index 00000000..bc2a45d5 --- /dev/null +++ b/Luban/FileServer/bin/app.js @@ -0,0 +1,26 @@ +const StaticServer = require('../src/static-server'); + +const options = require('yargs') + .option('p', { alias: 'port', describe: '设置服务启动的端口号', type: 'number' }) + .option('i', { alias: 'index', describe: '设置默认打开的主页', type: 'string' }) + .option('c', { alias: 'charset', describe: '设置文件的默认字符集', type: 'string' }) + .option('o', { alias: 'openindex', describe: '是否打开默认页面', type: 'boolean' }) + .option('h', { alias: 'https', describe: '是否启用https服务', type: 'boolean' }) + .option('cors', { describe: '是否开启文件跨域', type: 'boolean' }) + .option('openbrowser', { describe: '是否默认打开浏览器', type: 'boolean' }) + + // 默认参数 + .default('openbrowser', true) + // .default('https', true) + .default('port', 8080) + .default('index', 'index.html') + .default('openindex', 'index.html') + .default('charset', 'UTF-8') + + .help() + .alias('?', 'help') + + .argv; + +const server = new StaticServer(options); +server.start(); \ No newline at end of file diff --git a/Luban/FileServer/index.js b/Luban/FileServer/index.js new file mode 100644 index 00000000..f113fe8b --- /dev/null +++ b/Luban/FileServer/index.js @@ -0,0 +1,3 @@ +#!/usr/bin/env node + +module.exports = require('./bin/app.js'); diff --git a/Luban/FileServer/instal.bat b/Luban/FileServer/instal.bat new file mode 100644 index 00000000..ad36db1c --- /dev/null +++ b/Luban/FileServer/instal.bat @@ -0,0 +1 @@ +npm install yumu-static-server -g \ No newline at end of file diff --git a/Luban/FileServer/package.json b/Luban/FileServer/package.json new file mode 100644 index 00000000..73cdb9f0 --- /dev/null +++ b/Luban/FileServer/package.json @@ -0,0 +1,30 @@ +{ + "name": "static-server", + "version": "0.0.1", + "description": "使用node搭建静态资源服务器", + "main": "index.js", + "scripts": { + "dev": "supervisor bin/app.js", + "start": "npm run dev" + }, + "bin": { + "server": "index.js" + }, + "author": "Alex", + "license": "ISC", + "keywords": [ + "static-server", + "server" + ], + "dependencies": { + "chalk": "^2.3.2", + "handlebars": "^4.0.11", + "mime": "^2.2.0", + "open": "^7.1.0", + "pem": "^1.12.5", + "yargs": "^6.6.0" + }, + "devDependencies": { + "supervisor": "^0.12.0" + } +} \ No newline at end of file diff --git a/Luban/FileServer/src/mime.js b/Luban/FileServer/src/mime.js new file mode 100644 index 00000000..967897d5 --- /dev/null +++ b/Luban/FileServer/src/mime.js @@ -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 +}; diff --git a/Luban/FileServer/src/static-server.js b/Luban/FileServer/src/static-server.js new file mode 100644 index 00000000..ca2c5113 --- /dev/null +++ b/Luban/FileServer/src/static-server.js @@ -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 ${location}`, + 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; \ No newline at end of file diff --git a/Luban/FileServer/src/templates/404.js b/Luban/FileServer/src/templates/404.js new file mode 100644 index 00000000..62d60971 --- /dev/null +++ b/Luban/FileServer/src/templates/404.js @@ -0,0 +1,44 @@ +module.exports = ` + + + + + + + node静态服务器 + + + +
+ not found +
+

抱歉,你访问的路径不存在

+

您要找的页面没有找到,请返回首页继续浏览

+
+
+ + +` \ No newline at end of file diff --git a/Luban/FileServer/src/templates/default.js b/Luban/FileServer/src/templates/default.js new file mode 100644 index 00000000..4f389f6b --- /dev/null +++ b/Luban/FileServer/src/templates/default.js @@ -0,0 +1,93 @@ +module.exports = ` + + + + + + + node静态服务器 + + + +
+

当前目录:{{requestPath}}

+ {{#if showFileList}} + + {{else}} + {{htmlStr}} + {{/if}} +
+ + + +`; \ No newline at end of file diff --git a/Luban/FileServer/src/templates/images/404.png b/Luban/FileServer/src/templates/images/404.png new file mode 100644 index 00000000..93cd1bb8 Binary files /dev/null and b/Luban/FileServer/src/templates/images/404.png differ diff --git a/Luban/FileServer/src/templates/index.js b/Luban/FileServer/src/templates/index.js new file mode 100644 index 00000000..4b51b700 --- /dev/null +++ b/Luban/FileServer/src/templates/index.js @@ -0,0 +1,7 @@ +const page_dafault = require('./default'); +const page_404 = require('./404'); + +module.exports = { + page_dafault, + page_404 +}; \ No newline at end of file diff --git a/Luban/FileServer/start.bat b/Luban/FileServer/start.bat index babde2c4..13cd1fa7 100644 --- a/Luban/FileServer/start.bat +++ b/Luban/FileServer/start.bat @@ -1 +1 @@ -node FileSys.js \ No newline at end of file +server \ No newline at end of file diff --git a/Luban/FileServer/start.sh b/Luban/FileServer/start.sh deleted file mode 100644 index babde2c4..00000000 --- a/Luban/FileServer/start.sh +++ /dev/null @@ -1 +0,0 @@ -node FileSys.js \ No newline at end of file