titbit-toolkit
Version:
titbit框架的工具集,包括跨域、静态资源处理,权限过滤,请求计时,cookie,session,jwt等大量中间件
399 lines (298 loc) • 9.37 kB
JavaScript
'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