wok-server 0.2.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/config/index.js +17 -3
- package/dist/log/file.js +4 -11
- package/dist/mvc/config.js +23 -16
- package/dist/mvc/handler/json.js +27 -0
- package/dist/mvc/index.js +6 -295
- package/dist/mvc/render/file.js +3 -85
- package/dist/mvc/server.js +265 -0
- package/dist/mvc/static/header.js +67 -0
- package/dist/mvc/static/index.js +6 -0
- package/dist/mvc/static/mime-type.js +84 -0
- package/dist/mvc/static/server-cache-config.js +66 -0
- package/dist/mvc/static/server-cache.js +133 -0
- package/dist/mvc/static/static-handler.js +355 -0
- package/dist/mysql/manager/ops/update.js +15 -1
- package/dist/validation/index.js +12 -1
- package/dist/validation/validator/array.js +7 -11
- package/dist/validation/validator/min.js +2 -2
- package/documentation/zh-cn/config.md +31 -0
- package/documentation/zh-cn/mvc.md +48 -0
- package/documentation/zh-cn/mysql.md +72 -0
- package/documentation/zh-cn/validate.md +21 -15
- package/package.json +1 -1
- package/types/config/index.d.ts +10 -0
- package/types/mvc/config.d.ts +13 -1
- package/types/mvc/handler/json.d.ts +14 -0
- package/types/mvc/index.d.ts +3 -36
- package/types/mvc/server.d.ts +85 -0
- package/types/mvc/static/header.d.ts +27 -0
- package/types/mvc/static/index.d.ts +3 -0
- package/types/mvc/static/mime-type.d.ts +2 -0
- package/types/mvc/static/server-cache-config.d.ts +30 -0
- package/types/mvc/static/server-cache.d.ts +76 -0
- package/types/mvc/static/static-handler.d.ts +72 -0
- package/types/mysql/manager/ops/update.d.ts +7 -1
- package/types/validation/validator/array.d.ts +2 -2
- package/types/validation/validator/min.d.ts +2 -2
- package/types/validation/validator/plain-obj.d.ts +1 -1
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WokServer = void 0;
|
|
4
|
+
const fs_1 = require("fs");
|
|
5
|
+
const http_1 = require("http");
|
|
6
|
+
const https_1 = require("https");
|
|
7
|
+
const os_1 = require("os");
|
|
8
|
+
const path_1 = require("path");
|
|
9
|
+
const log_1 = require("../log");
|
|
10
|
+
const access_log_1 = require("./access-log");
|
|
11
|
+
const config_1 = require("./config");
|
|
12
|
+
const exchange_1 = require("./exchange");
|
|
13
|
+
const render_1 = require("./render");
|
|
14
|
+
const static_1 = require("./static");
|
|
15
|
+
const validation_1 = require("../validation");
|
|
16
|
+
function resolvePath(path) {
|
|
17
|
+
if ((0, path_1.isAbsolute)(path)) {
|
|
18
|
+
return path;
|
|
19
|
+
}
|
|
20
|
+
return (0, path_1.resolve)(process.cwd(), path);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* web 服务
|
|
24
|
+
*/
|
|
25
|
+
class WokServer {
|
|
26
|
+
opts;
|
|
27
|
+
config;
|
|
28
|
+
server;
|
|
29
|
+
interceptors;
|
|
30
|
+
routers;
|
|
31
|
+
defaultRouter;
|
|
32
|
+
staticHandler;
|
|
33
|
+
constructor(opts) {
|
|
34
|
+
this.opts = opts;
|
|
35
|
+
this.config = (0, config_1.getConfig)();
|
|
36
|
+
// https
|
|
37
|
+
let tls;
|
|
38
|
+
if (this.config.tlsEnable) {
|
|
39
|
+
(0, validation_1.validate)(this.config, {
|
|
40
|
+
tlsCert: [(0, validation_1.notBlank)()],
|
|
41
|
+
tlsKey: [(0, validation_1.notBlank)()]
|
|
42
|
+
});
|
|
43
|
+
tls = {
|
|
44
|
+
cert: (0, fs_1.readFileSync)(resolvePath(this.config.tlsCert)),
|
|
45
|
+
key: (0, fs_1.readFileSync)(resolvePath(this.config.tlsKey))
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
// 路由
|
|
49
|
+
this.routers = opts.routers;
|
|
50
|
+
this.defaultRouter = this.routers['*'];
|
|
51
|
+
// 拦截器
|
|
52
|
+
this.interceptors = [];
|
|
53
|
+
if (this.config.accessLog) {
|
|
54
|
+
this.interceptors.push(access_log_1.accessLogInterceptor);
|
|
55
|
+
}
|
|
56
|
+
if (opts.interceptors) {
|
|
57
|
+
this.interceptors.push(...opts.interceptors);
|
|
58
|
+
}
|
|
59
|
+
// 静态处理
|
|
60
|
+
if (opts.static) {
|
|
61
|
+
this.staticHandler = new static_1.StaticHandler(opts.static);
|
|
62
|
+
}
|
|
63
|
+
// 主服务
|
|
64
|
+
if (!tls) {
|
|
65
|
+
this.server = (0, http_1.createServer)({
|
|
66
|
+
requestTimeout: this.config.timeout
|
|
67
|
+
}, (req, res) => {
|
|
68
|
+
res.setHeader('Server', 'Wok Server');
|
|
69
|
+
res.on('error', error => {
|
|
70
|
+
// 如果响应流发生错误,只能把信息记录下来
|
|
71
|
+
(0, log_1.getLogger)().error(`Response Error:${req.url}`, error);
|
|
72
|
+
});
|
|
73
|
+
this.handleRequest(req, res).catch(error => {
|
|
74
|
+
(0, log_1.getLogger)().error(`Handle request failed:${req.url}`, error);
|
|
75
|
+
if (!res.writableEnded) {
|
|
76
|
+
// 响应 500
|
|
77
|
+
(0, render_1.renderError)(res, error.message ? error.message : 'Internal Server Error', 500);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
this.server = (0, https_1.createServer)({
|
|
84
|
+
requestTimeout: this.config.timeout,
|
|
85
|
+
key: tls.key,
|
|
86
|
+
cert: tls.cert
|
|
87
|
+
}, (req, res) => {
|
|
88
|
+
res.setHeader('Server', 'Wok Server');
|
|
89
|
+
res.on('error', error => {
|
|
90
|
+
// 如果响应流发生错误,只能把信息记录下来
|
|
91
|
+
(0, log_1.getLogger)().error(`Response Error:${req.url}`, error);
|
|
92
|
+
});
|
|
93
|
+
this.handleRequest(req, res).catch(error => {
|
|
94
|
+
(0, log_1.getLogger)().error(`Handle request failed:${req.url}`, error);
|
|
95
|
+
if (!res.writableEnded) {
|
|
96
|
+
// 响应 500
|
|
97
|
+
(0, render_1.renderError)(res, error.message ? error.message : 'Internal Server Error', 500);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
this.server.on('timeout', (socket) => {
|
|
103
|
+
socket.end('HTTP/1.1 408 Timeout\ncontent-type: application/json; charset=utf-8\n\n{"message":"Request timeout"}');
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* 处理请求,完成拦截器和路由的流程.
|
|
108
|
+
*
|
|
109
|
+
* @param req
|
|
110
|
+
* @param res
|
|
111
|
+
*/
|
|
112
|
+
async handleRequest(req, res) {
|
|
113
|
+
req.socket.remoteAddress;
|
|
114
|
+
const { method } = req;
|
|
115
|
+
// cros 支持
|
|
116
|
+
res.setHeader('Access-Control-Allow-Origin', this.config.corsAllowOrigin);
|
|
117
|
+
res.setHeader('Access-Control-Allow-Headers', this.config.corsAllowHeaders);
|
|
118
|
+
res.setHeader('Access-Control-Allow-Methods', this.config.corsAllowMethods);
|
|
119
|
+
if (method === 'OPTIONS') {
|
|
120
|
+
res.statusCode = 200;
|
|
121
|
+
res.end();
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const exchange = new exchange_1.ServerExchange(req, res);
|
|
125
|
+
// 顺序执行拦截器
|
|
126
|
+
await this.handleInterceptor(0, exchange, req, res);
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* 处理拦截器.
|
|
130
|
+
* @param idx 当前要执行的拦截器下标
|
|
131
|
+
* @param exchange 传输对象
|
|
132
|
+
* @param req
|
|
133
|
+
* @param res
|
|
134
|
+
*/
|
|
135
|
+
async handleInterceptor(idx, exchange, req, res) {
|
|
136
|
+
const interceptor = this.interceptors[idx];
|
|
137
|
+
// 到最后一个了,那么执行路由处理
|
|
138
|
+
if (!interceptor) {
|
|
139
|
+
await this.handleRouter(exchange);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
await interceptor(exchange, () => this.handleInterceptor(idx + 1, exchange, req, res));
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* 处理路由.
|
|
146
|
+
* @param exchange
|
|
147
|
+
* @param routers
|
|
148
|
+
* @param staticSettings
|
|
149
|
+
* @returns
|
|
150
|
+
*/
|
|
151
|
+
async handleRouter(exchange) {
|
|
152
|
+
const url = exchange.request.url;
|
|
153
|
+
if (url === undefined) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
// 判定路由
|
|
157
|
+
const idx = url.indexOf('?');
|
|
158
|
+
let path = idx === -1 ? url : url.substring(0, idx);
|
|
159
|
+
const router = this.routers[path];
|
|
160
|
+
if (!router) {
|
|
161
|
+
// 路由找不不到,尝试静态文件
|
|
162
|
+
if (this.staticHandler) {
|
|
163
|
+
const method = (exchange.request.method || '').toLowerCase();
|
|
164
|
+
if (method === 'head') {
|
|
165
|
+
if (await this.staticHandler.handleHead(path, exchange.response)) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (method === 'get') {
|
|
170
|
+
if (await this.staticHandler.handleGet(exchange.request, exchange.response, path)) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
this.respond404(exchange, path);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
// 执行路由
|
|
179
|
+
await router(exchange);
|
|
180
|
+
// 在路由顺利处理的情况下,如果 res 没有 end ,就表示响应没有完成
|
|
181
|
+
// 也就是说路由没有做响应处理,或处理没有完成就结束了,给予错误提示
|
|
182
|
+
if (!exchange.response.writableEnded) {
|
|
183
|
+
throw new Error(`RouterHandler unresponsive, url: ${url}`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* 404响应
|
|
188
|
+
*
|
|
189
|
+
* @param exchange
|
|
190
|
+
* @param path
|
|
191
|
+
*/
|
|
192
|
+
async respond404(exchange, path) {
|
|
193
|
+
if (this.defaultRouter) {
|
|
194
|
+
await this.defaultRouter(exchange);
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
exchange.respondErrMsg(`${path} not found`, 404);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* 启动
|
|
202
|
+
*/
|
|
203
|
+
async start() {
|
|
204
|
+
if (this.opts.preHandler) {
|
|
205
|
+
await this.opts.preHandler(this.server);
|
|
206
|
+
}
|
|
207
|
+
await new Promise((resolve, reject) => {
|
|
208
|
+
this.server.on('error', e => {
|
|
209
|
+
if (e.code === 'EADDRINUSE') {
|
|
210
|
+
reject(`Port ${this.config.port} is already in use.`);
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
reject(e);
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
this.server.listen(this.config.port, resolve);
|
|
217
|
+
});
|
|
218
|
+
console.log('App running at: ');
|
|
219
|
+
let portOmitted = (this.server instanceof https_1.Server && this.config.port === 443) ||
|
|
220
|
+
(this.server instanceof http_1.Server && this.config.port === 80);
|
|
221
|
+
this.getIpv4List().forEach(ip => {
|
|
222
|
+
if (portOmitted) {
|
|
223
|
+
console.log(`http://${ip}`);
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
console.log(`http://${ip}:${this.config.port}`);
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* 获取 ipv4 地址列表
|
|
232
|
+
* @returns
|
|
233
|
+
*/
|
|
234
|
+
getIpv4List() {
|
|
235
|
+
const ifs = (0, os_1.networkInterfaces)();
|
|
236
|
+
const res = [];
|
|
237
|
+
for (const name in ifs) {
|
|
238
|
+
const list = ifs[name];
|
|
239
|
+
if (!list) {
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
res.push(...list.filter(info => info.family === 'IPv4').map(info => info.address));
|
|
243
|
+
}
|
|
244
|
+
return res;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* 停止
|
|
248
|
+
*/
|
|
249
|
+
async stop() {
|
|
250
|
+
if (!this.server.listening) {
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
await new Promise((res, rej) => {
|
|
254
|
+
this.server.close(err => {
|
|
255
|
+
if (err) {
|
|
256
|
+
rej(err);
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
res();
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
exports.WokServer = WokServer;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseHeaders = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* 解析 range 消息头,如果解析失败,返回 null
|
|
6
|
+
*/
|
|
7
|
+
function parseRange(rangeHeader) {
|
|
8
|
+
// 解析,range 示例:bytes=200-1000, 2000-6576, 19000-
|
|
9
|
+
// 多段的情况,暂时不做支持,非常麻烦,段数多还可能会有效率问题
|
|
10
|
+
// 如果不是字节范围不是以字节为单位,暂时也不做支持
|
|
11
|
+
const ranges = rangeHeader.split(',');
|
|
12
|
+
if (!ranges.length) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
let range = ranges[0].trim();
|
|
16
|
+
if (!range.startsWith('bytes=')) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
range = range.substring(6);
|
|
20
|
+
const strs = range.split('-');
|
|
21
|
+
let start = strs[0] ? parseInt(strs[0], 10) : NaN;
|
|
22
|
+
let end = strs[1] ? parseInt(strs[1], 10) : undefined;
|
|
23
|
+
return { start, end };
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* 解析消息头,返回静态文件处理需要的信息
|
|
27
|
+
* @param headers
|
|
28
|
+
*/
|
|
29
|
+
function parseHeaders(headers) {
|
|
30
|
+
// 是否支持 gzip
|
|
31
|
+
const acceptEncoding = headers['accept-encoding'];
|
|
32
|
+
let acceptGzip = false;
|
|
33
|
+
if (acceptEncoding) {
|
|
34
|
+
// Accept-Encoding: br;q=1.0, gzip;q=0.8, *;q=0.1
|
|
35
|
+
const acceptEncodings = acceptEncoding
|
|
36
|
+
.trim()
|
|
37
|
+
.split(',')
|
|
38
|
+
.map(item => item.trim())
|
|
39
|
+
.map(item => item.split(';')[0]);
|
|
40
|
+
if (acceptEncodings.includes('gzip') || acceptEncodings.includes('*')) {
|
|
41
|
+
acceptGzip = true;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
let res = {
|
|
45
|
+
gzip: acceptGzip
|
|
46
|
+
};
|
|
47
|
+
// 支持 If-Modified-Since
|
|
48
|
+
// 由于只是简单的文件映射,没有 etag,不能支持 If-None-Match
|
|
49
|
+
const ifModifiedSince = headers['if-modified-since'];
|
|
50
|
+
if (ifModifiedSince) {
|
|
51
|
+
const modifiedSince = new Date(ifModifiedSince);
|
|
52
|
+
if (modifiedSince instanceof Date && !isNaN(modifiedSince.getTime())) {
|
|
53
|
+
res.ifModifiedSince = modifiedSince;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// 支持 Range
|
|
57
|
+
// https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Range
|
|
58
|
+
const rangeHeader = headers['range'];
|
|
59
|
+
if (rangeHeader) {
|
|
60
|
+
const rangeRes = parseRange(rangeHeader);
|
|
61
|
+
if (rangeRes) {
|
|
62
|
+
res.range = rangeRes;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return res;
|
|
66
|
+
}
|
|
67
|
+
exports.parseHeaders = parseHeaders;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const tslib_1 = require("tslib");
|
|
4
|
+
tslib_1.__exportStar(require("./mime-type"), exports);
|
|
5
|
+
tslib_1.__exportStar(require("./header"), exports);
|
|
6
|
+
tslib_1.__exportStar(require("./static-handler"), exports);
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.decideContentType = exports.MIME_TYPES = void 0;
|
|
4
|
+
exports.MIME_TYPES = {
|
|
5
|
+
aac: 'audio/aac',
|
|
6
|
+
abw: 'application/x-abiword',
|
|
7
|
+
arc: 'application/x-freearc',
|
|
8
|
+
avi: 'video/x-msvideo',
|
|
9
|
+
azw: 'application/vnd.amazon.ebook',
|
|
10
|
+
bin: 'application/octet-stream',
|
|
11
|
+
bmp: 'image/bmp',
|
|
12
|
+
bz: 'application/x-bzip',
|
|
13
|
+
bz2: 'application/x-bzip2',
|
|
14
|
+
csh: 'application/x-csh',
|
|
15
|
+
css: 'text/css',
|
|
16
|
+
csv: 'text/csv',
|
|
17
|
+
doc: 'application/msword',
|
|
18
|
+
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
19
|
+
eot: 'application/vnd.ms-fontobject',
|
|
20
|
+
epub: 'application/epub+zip',
|
|
21
|
+
gif: 'image/gif',
|
|
22
|
+
htm: 'text/html',
|
|
23
|
+
html: 'text/html',
|
|
24
|
+
ico: 'image/vnd.microsoft.icon',
|
|
25
|
+
ics: 'text/calendar',
|
|
26
|
+
jar: 'application/java-archive',
|
|
27
|
+
jpeg: 'image/jpeg',
|
|
28
|
+
jpg: 'image/jpeg',
|
|
29
|
+
js: 'text/javascript',
|
|
30
|
+
json: 'application/json',
|
|
31
|
+
jsonld: 'application/ld+json',
|
|
32
|
+
mid: 'audio/midi',
|
|
33
|
+
midi: 'audio/midi',
|
|
34
|
+
mjs: 'text/javascript',
|
|
35
|
+
mp3: 'audio/mpeg',
|
|
36
|
+
mpeg: 'video/mpeg',
|
|
37
|
+
mpkg: 'application/vnd.apple.installer+xml',
|
|
38
|
+
odp: 'application/vnd.oasis.opendocument.presentation',
|
|
39
|
+
ods: 'application/vnd.oasis.opendocument.spreadsheet',
|
|
40
|
+
odt: 'application/vnd.oasis.opendocument.text',
|
|
41
|
+
oga: 'audio/ogg',
|
|
42
|
+
ogv: 'video/ogg',
|
|
43
|
+
ogx: 'application/ogg',
|
|
44
|
+
otf: 'font/otf',
|
|
45
|
+
png: 'image/png',
|
|
46
|
+
pdf: 'application/pdf',
|
|
47
|
+
ppt: 'application/vnd.ms-powerpoint',
|
|
48
|
+
pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
49
|
+
rar: 'application/x-rar-compressed',
|
|
50
|
+
rtf: 'application/rtf',
|
|
51
|
+
sh: 'application/x-sh',
|
|
52
|
+
svg: 'image/svg+xml',
|
|
53
|
+
swf: 'application/x-shockwave-flash',
|
|
54
|
+
tar: 'application/x-tar',
|
|
55
|
+
tif: 'image/tiff',
|
|
56
|
+
tiff: 'image/tiff',
|
|
57
|
+
ttf: 'font/ttf',
|
|
58
|
+
txt: 'text/plain',
|
|
59
|
+
vsd: 'application/vnd.visio',
|
|
60
|
+
wav: 'audio/wav',
|
|
61
|
+
weba: 'audio/webm',
|
|
62
|
+
webm: 'video/webm',
|
|
63
|
+
webp: 'image/webp',
|
|
64
|
+
woff: 'font/woff',
|
|
65
|
+
woff2: 'font/woff2',
|
|
66
|
+
xhtml: 'application/xhtml+xml',
|
|
67
|
+
xls: 'application/vnd.ms-excel',
|
|
68
|
+
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
69
|
+
xml: 'application/xml',
|
|
70
|
+
xul: 'application/vnd.mozilla.xul+xml',
|
|
71
|
+
zip: 'application/zip',
|
|
72
|
+
'3gp': 'video/3gpp',
|
|
73
|
+
'3g2': 'video/3gpp2',
|
|
74
|
+
'7z': 'application/x-7z-compressed'
|
|
75
|
+
};
|
|
76
|
+
function decideContentType(fileName) {
|
|
77
|
+
const idx = fileName.lastIndexOf('.');
|
|
78
|
+
if (idx === -1) {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
const suffix = fileName.substring(idx + 1);
|
|
82
|
+
return exports.MIME_TYPES[suffix];
|
|
83
|
+
}
|
|
84
|
+
exports.decideContentType = decideContentType;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getConfig = exports.parseSize = void 0;
|
|
4
|
+
const config_1 = require("../../config");
|
|
5
|
+
const validation_1 = require("../../validation");
|
|
6
|
+
function parseSize(size) {
|
|
7
|
+
if (typeof size === 'number') {
|
|
8
|
+
return size;
|
|
9
|
+
}
|
|
10
|
+
const match = size.match(/^(\d+)([kmg]?)$/i);
|
|
11
|
+
if (!match) {
|
|
12
|
+
throw new Error(`Invalid size: ${size}`);
|
|
13
|
+
}
|
|
14
|
+
const [, num, unit] = match;
|
|
15
|
+
let sizeNum = parseInt(num, 10);
|
|
16
|
+
switch (unit.toLowerCase()) {
|
|
17
|
+
case 'k':
|
|
18
|
+
sizeNum *= 1024;
|
|
19
|
+
break;
|
|
20
|
+
case 'm':
|
|
21
|
+
sizeNum *= 1024 * 1024;
|
|
22
|
+
break;
|
|
23
|
+
case 'g':
|
|
24
|
+
sizeNum *= 1024 * 1024 * 1024;
|
|
25
|
+
break;
|
|
26
|
+
default:
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
return sizeNum;
|
|
30
|
+
}
|
|
31
|
+
exports.parseSize = parseSize;
|
|
32
|
+
function validateSize(max) {
|
|
33
|
+
const maxNum = parseSize(max);
|
|
34
|
+
const validator = 'size';
|
|
35
|
+
return val => {
|
|
36
|
+
if (val === null || val === undefined) {
|
|
37
|
+
return { ok: true };
|
|
38
|
+
}
|
|
39
|
+
const sizeNum = typeof val === 'number' ? val : parseSize(val);
|
|
40
|
+
if (sizeNum < 1) {
|
|
41
|
+
return { ok: false, validator, message: `size must greater than 1` };
|
|
42
|
+
}
|
|
43
|
+
if (sizeNum > maxNum) {
|
|
44
|
+
return { ok: false, validator, message: `size must less than ${max}` };
|
|
45
|
+
}
|
|
46
|
+
return { ok: true };
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* 获取缓存的配置信息
|
|
51
|
+
* @returns
|
|
52
|
+
*/
|
|
53
|
+
function getConfig() {
|
|
54
|
+
return (0, config_1.generateConfig)({
|
|
55
|
+
enable: false,
|
|
56
|
+
maxAge: 600,
|
|
57
|
+
maxSize: '100m',
|
|
58
|
+
maxFileSize: '10m'
|
|
59
|
+
}, 'SERVER_STATIC_CACHE', {
|
|
60
|
+
enable: [(0, validation_1.notNull)()],
|
|
61
|
+
maxAge: [(0, validation_1.notNull)(), (0, validation_1.min)(1), (0, validation_1.max)(31536000)],
|
|
62
|
+
maxSize: [(0, validation_1.notNull)(), validateSize('1024g')],
|
|
63
|
+
maxFileSize: [(0, validation_1.notNull)(), validateSize('1g')]
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
exports.getConfig = getConfig;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ServerCache = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* 文件的服务器缓存
|
|
6
|
+
*/
|
|
7
|
+
class ServerCache {
|
|
8
|
+
opts;
|
|
9
|
+
/**
|
|
10
|
+
* 当前的缓存内容大小,单位字节
|
|
11
|
+
*/
|
|
12
|
+
size = 0;
|
|
13
|
+
/**
|
|
14
|
+
* 缓存内容
|
|
15
|
+
*/
|
|
16
|
+
cacheMap = new Map();
|
|
17
|
+
/**
|
|
18
|
+
* promise 表,作用是处理异步并发问题,如果有相同的 key 同时请求,保证异步的 provider 只执行一次
|
|
19
|
+
*/
|
|
20
|
+
promiseMap = new Map();
|
|
21
|
+
constructor(opts) {
|
|
22
|
+
this.opts = opts;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* 获取缓存
|
|
26
|
+
*/
|
|
27
|
+
get(key) {
|
|
28
|
+
const data = this.cacheMap.get(key);
|
|
29
|
+
if (data) {
|
|
30
|
+
if (data.expireAt < Date.now()) {
|
|
31
|
+
this.cacheMap.delete(key);
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
return data;
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* 设置缓存内容,如果缓存内容超过最大缓存大小,则触发清除缓存操作
|
|
40
|
+
* @param key
|
|
41
|
+
* @param value
|
|
42
|
+
* @returns
|
|
43
|
+
*/
|
|
44
|
+
set(key, value) {
|
|
45
|
+
const expireAt = Date.now() + this.opts.maxAge * 1000;
|
|
46
|
+
const content = value;
|
|
47
|
+
content.expireAt = expireAt;
|
|
48
|
+
this.cacheMap.set(key, content);
|
|
49
|
+
this.size += content.buffer.length;
|
|
50
|
+
if (this.size > this.opts.maxSize) {
|
|
51
|
+
setTimeout(() => this.clean(), 0);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* 删除缓存
|
|
56
|
+
*/
|
|
57
|
+
remove(key) {
|
|
58
|
+
const data = this.cacheMap.get(key);
|
|
59
|
+
if (data) {
|
|
60
|
+
this.cacheMap.delete(key);
|
|
61
|
+
this.size -= data.buffer.length;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* 如果缓存不存在,则计算缓存内容并放入缓存
|
|
66
|
+
* @param key
|
|
67
|
+
* @param provider 计算函数,返回值将放入缓存,如果返回 null 则表示未能计算出缓存内容,不进行缓存
|
|
68
|
+
*/
|
|
69
|
+
async computeIfAbsent(key, provider) {
|
|
70
|
+
const data = this.get(key);
|
|
71
|
+
if (data) {
|
|
72
|
+
return data;
|
|
73
|
+
}
|
|
74
|
+
// 如果已经在处理中,则直接返回 promise
|
|
75
|
+
const ep = this.promiseMap.get(key);
|
|
76
|
+
if (ep) {
|
|
77
|
+
return ep;
|
|
78
|
+
}
|
|
79
|
+
const promise = Promise.resolve().then(async () => {
|
|
80
|
+
try {
|
|
81
|
+
const res = await provider();
|
|
82
|
+
if (res) {
|
|
83
|
+
this.set(key, res);
|
|
84
|
+
}
|
|
85
|
+
return res;
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
this.promiseMap.delete(key);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
this.promiseMap.set(key, promise);
|
|
92
|
+
return promise;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* 清理无用的缓存内容
|
|
96
|
+
*/
|
|
97
|
+
clean() {
|
|
98
|
+
// 先清理掉过期的
|
|
99
|
+
const keys = Array.from(this.cacheMap.keys());
|
|
100
|
+
for (const key of keys) {
|
|
101
|
+
const data = this.cacheMap.get(key);
|
|
102
|
+
if (data) {
|
|
103
|
+
if (data.expireAt < Date.now()) {
|
|
104
|
+
this.cacheMap.delete(key);
|
|
105
|
+
this.size -= data.buffer.length;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (this.size < this.opts.maxSize) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const keys2 = Array.from(this.cacheMap.keys());
|
|
113
|
+
// 再逐个清理,直到空间不会超出最大缓存大小
|
|
114
|
+
for (const key of keys2) {
|
|
115
|
+
if (this.size < this.opts.maxSize * 0.8) {
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
const data = this.cacheMap.get(key);
|
|
119
|
+
if (data) {
|
|
120
|
+
this.cacheMap.delete(key);
|
|
121
|
+
this.size -= data.buffer.length;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* 清除缓存内容
|
|
127
|
+
*/
|
|
128
|
+
clear() {
|
|
129
|
+
this.cacheMap.clear();
|
|
130
|
+
this.size = 0;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
exports.ServerCache = ServerCache;
|