UNPKG

titbit-toolkit

Version:

titbit框架的工具集,包括跨域、静态资源处理,权限过滤,请求计时,cookie,session,jwt等大量中间件

399 lines (298 loc) 9.37 kB
'use strict'; //const fs = require('fs'); const zlib = require('node:zlib') const fs = require('node:fs') const fsp = fs.promises let _typemap = { '.css' : 'text/css; charset=utf-8', '.js' : 'text/javascript; charset=utf-8', '.txt' : 'text/plain; charset=utf-8', '.json' : 'application/json; charset=utf-8', '.lrc' : 'text/plain; charset=utf-8', '.md' : 'text/plain; charset=utf-8', '.html' : 'text/html; charset=utf-8', '.xml' : 'text/xml; charset=utf-8', '.svg' : 'image/svg+xml', '.jpg' : 'image/jpeg', '.jpeg' : 'image/jpeg', '.png' : 'image/png', '.gif' : 'image/gif', '.ico' : 'image/x-icon', '.webp' : 'image/webp', '.tif' : 'image/tiff', '.tiff' : 'image/tiff', '.avif' : 'image/avif', '.apng' : 'image/apng', '.mp3' : 'audio/mpeg', '.flac' : 'audio/flac', '.wav' : 'audio/x-wav', '.mp4' : 'video/mp4', '.webm' : 'video/webm', '.otf' : 'font/otf', '.ttf' : 'font/ttf', '.wtf' : 'font/wtf', '.woff' : 'font/woff', '.ttc' : 'font/ttc', '.woff2' : 'font/woff2', '.pdf': 'application/pdf', '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', } /** * 处理静态资源的请求,需要把中间件挂载到一个分组下,否则会影响全局,如果一个只做静态分发的服务则可以全局启用。 */ class Resource { constructor(options = {}) { this.cache = new Map() this.staticPath = '' //最大缓存,单位为字节,0表示不限制。 this.maxCacheSize = 128_000_000 this.size = 0 //控制释放缓存的概率,1到100 this.prob = 6 //失败缓存统计,当失败缓存计数达到一个阈值,则会清空缓存。 this.cacheFailed = 0 this.failedLimit = 50 this.compress = true this.cacheControl = null this.routePath = '/static/*' this.prepath = '' this.routeGroup = `__static_${parseInt(Math.random()*10000)}_` this.decodePath = false this.maxFileSize = 10_000_000 if (typeof options !== 'object') { options = {} } for (let k in options) { switch(k) { case 'staticPath': this.staticPath = options[k] break case 'maxCacheSize': case 'maxFileSize': if (typeof options[k] === 'number') { this[k] = options[k] } break case 'failedLimit': if (options[k] > 0) { this.failedLimit = options[k] } break case 'compress': this.compress = options[k] break case 'cacheControl': this.cacheControl = options[k] break case 'routePath': if (typeof options[k] === 'string') { this.routePath = options[k] } break case 'routeGroup': if (typeof options[k] === 'string') { this.routeGroup = options[k] } break case 'decodePath': this.decodePath = options[k] break case 'prepath': this.prepath = options[k] if (this.prepath.length > 0 && this.prepath[0] !== '/') { this.prepath = `/${this.prepath}` } break case 'prob': if (typeof options[k] === 'number' && options[k] >= 1 && options[k] <= 100) { this.prob = options[k] } break } } if (this.maxCacheSize < 10_000_000) { this.maxCacheSize = 10_000_000 } if (this.maxFileSize < 10000 || this.maxFileSize > 500_000_000) { this.maxFileSize = 10_000_000 } if (this.staticPath.length > 1 && this.staticPath[ this.staticPath.length-1 ] === '/') { this.staticPath = this.staticPath.substring(0, this.staticPath.length-1) } this.ctypeMap = _typemap for (let k in this.ctypeMap) { this.ctypeMap[ k.toUpperCase() ] = _typemap[k] } } addType(tobj) { let lower_name, up_name; for (let k in tobj) { lower_name = k.toLowerCase() up_name = k.toUpperCase() this.ctypeMap[lower_name] = tobj[k] this.ctypeMap[up_name] = tobj[k] } } extName(filename) { let extind = filename.length - 1 let extstart = filename.length - 5 while (extind > 0 && extind >= extstart) { if (filename[extind] === '.') break extind -= 1 } return filename.substring(extind) } filetype(extname) { if (this.ctypeMap[extname] !== undefined) { return this.ctypeMap[extname] } return 'application/octet-stream' } removeGroupCache(filepre) { let keys = this.cache.keys() for (let k of keys) { if (k.indexOf(filepre) === 0) this.cache.delete(k) } } removeNameCache(name) { let keys = this.cache.keys() for (let k of keys) { if (k.lastIndexOf(name) >= 0) this.cache.delete(k) } } removeCache(filepath) { if (this.cache.has(filepath)) { this.cache.delete(filepath) } } clearCache() { this.size = 0 this.cacheFailed = 0 this.cache.clear() } async pipeData(pathfile, ctx, filesize) { let stm = fs.createReadStream(pathfile) let dataBuffer = [] let total = 0 if (ctx.major === 2) { ctx.sendHeader() } return new Promise((rv, rj) => { stm.on('data', data => { if (filesize <= this.maxFileSize) { total += data.length dataBuffer.push(data) } }) stm.on('error', err => { dataBuffer = null !stm.destroyed && stm.destroy() rj(err) }) stm.on('end', () => { if (dataBuffer && dataBuffer.length > 0) { let retData = Buffer.concat(dataBuffer, total) dataBuffer = null rv(retData) } else { rv(null) } }) stm.pipe(ctx.reply) }) } mid() { let self = this return async (c, next) => { let rpath = c.param.starPath || c.path if (rpath[0] !== '/') { rpath = `/${rpath}` } let real_path = rpath if (self.decodePath) { try { real_path = decodeURIComponent(rpath) } catch (err) { real_path = rpath } } let pathfile = `${self.staticPath}${self.prepath}${real_path}` if (self.cache.has(real_path)) { let r = self.cache.get(real_path) c.setHeader('content-type', r.type) c.setHeader('content-length', r.data.length) if (r.gzip) { c.setHeader('content-encoding', 'gzip') } if (self.cacheControl) { c.setHeader('cache-control', self.cacheControl) } return c.send(r.data) } let data = null let extname = this.extName(pathfile) let ctype = self.filetype(extname) let zipdata = null try { if (ctype.indexOf('text/') === 0 || extname === '.json' || ctype.indexOf('font/') === 0) { data = await fsp.readFile(pathfile, {encoding: 'utf8'}) //若文件很小,压缩后的数据很可能要比源文件还大,所以对超过1k的文件进行压缩,否则不进行压缩。 if (data && data.length > 1024) { zipdata = await new Promise((rv, rj) => { zlib.gzip(data, (err, d) => { if (err) { rj(err) } else { rv(d) } }) }).catch(err => { zipdata = null }) } c.setHeader('content-type', ctype) .setHeader('content-length', zipdata ? zipdata.length : data.length) if (zipdata) { c.setHeader('content-encoding', 'gzip') } self.cacheControl && c.setHeader('cache-control', self.cacheControl); c.sendHeader().send(zipdata || data) } else { let fst = await fsp.stat(pathfile) c.setHeader('content-type', ctype).setHeader('content-length', fst.size); self.cacheControl && c.setHeader('cache-control', self.cacheControl); data = await this.pipeData(pathfile, c, fst.size) //说明数据太大,放弃了缓存 if (!data) return; } if (self.cacheFailed >= self.failedLimit) { //以{self.prob}%概率决定是否释放缓存。 if (parseInt(Math.random() * 100) < self.prob) { self.clearCache() } } else if (self.maxCacheSize > 0 && self.size >= self.maxCacheSize) { if (self.cacheFailed < 1000_0000) self.cacheFailed++ } else { self.cache.set(real_path, { data : zipdata || data, type : ctype, gzip : zipdata ? true : false, }) self.size += zipdata ? zipdata.length : data.length } } catch (err) { c.status(404).send('read file failed') } } } init(app, group = null) { app.get(this.routePath, async c => {}, {group: group || this.routeGroup}) app.use(this.mid(), {group : group || this.routeGroup}) } } module.exports = Resource