mm_statics 1.9.0 → 1.9.2

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/lib/manager.js ADDED
@@ -0,0 +1,285 @@
1
+ const Manager = require("mm_machine").Manager;
2
+ const Static = require("./static");
3
+ const { URLSearchParams } = require('url');
4
+
5
+ /**
6
+ * 静态文件管理类
7
+ * @augments {Manager}
8
+ * @class
9
+ */
10
+ class StaticManager extends Manager {
11
+ /**
12
+ * 配置参数
13
+ * @type {object}
14
+ */
15
+ static config = {
16
+ /**
17
+ * 标题
18
+ * @type {string}
19
+ */
20
+ title: "静态文件管理",
21
+ /**
22
+ * 描述
23
+ * @type {string}
24
+ */
25
+ description: "这是静态文件管理器",
26
+ /**
27
+ * 运行根路径(驱动计算物理根目录的回退值,为空时取 process.cwd())
28
+ * @type {string}
29
+ */
30
+ run_path: "",
31
+ /**
32
+ * 最大深度
33
+ * @type {number}
34
+ */
35
+ max_depth: 6,
36
+ /**
37
+ * 资源清单文件名(update 扫描目录时使用)
38
+ * @type {string}
39
+ */
40
+ filename: "static.json"
41
+ };
42
+
43
+ /**
44
+ * 构造函数
45
+ * @param {object} config 配置参数
46
+ * @param {object} parent 父级模块
47
+ */
48
+ constructor(config, parent) {
49
+ super({ ...StaticManager.config, ...config }, parent);
50
+ }
51
+
52
+ /**
53
+ * 处理请求
54
+ * @param {object} ctx Http请求上下文
55
+ * @param {object} next 跳过函数
56
+ */
57
+ async _handleRequest(ctx, next) {
58
+ var md = ctx.method || (ctx.params && ctx.params.method) || '';
59
+ var path = ctx.path || (ctx.params && ctx.params.path) || '';
60
+ if (md === "GET" || md === "HEAD") {
61
+ await this._handleGetRequest(ctx, next, path);
62
+ } else {
63
+ await next();
64
+ }
65
+ }
66
+
67
+ /**
68
+ * 处理GET请求
69
+ * @param {object} ctx Http请求上下文
70
+ * @param {object} next 跳过函数
71
+ */
72
+ async run(ctx, next) {
73
+ var done;
74
+ var infos = this.configs;
75
+ var len = infos.length;
76
+ let path = ctx.path || (ctx.params && ctx.params.path) || '';
77
+ if (path.indexOf(".") !== -1) {
78
+ done = await this._tryServeFile(ctx, infos, len, async () => {});
79
+ if (!done) {
80
+ await next();
81
+ }
82
+ } else {
83
+ await next();
84
+ await this._handleDirReq(ctx, infos, len, path);
85
+ }
86
+ }
87
+
88
+ /**
89
+ * 尝试提供文件服务
90
+ * @param {object} ctx Http请求上下文
91
+ * @param {Array} infos 驱动信息列表
92
+ * @param {number} len 列表长度
93
+ * @param {object} next 跳过函数
94
+ * @returns {boolean} 是否成功
95
+ */
96
+ async _tryServeFile(ctx, infos, len, next) {
97
+ for (var i = 0; i < len; i++) {
98
+ var info = infos[i];
99
+ let is_running = await this.isRunning(info.name);
100
+ if (is_running) {
101
+ let mod = this.get(info.name);
102
+ if (mod) {
103
+ await this._initDrive(mod);
104
+ var done = await mod.run(ctx, next);
105
+ if (done) {
106
+ return true;
107
+ }
108
+ }
109
+ }
110
+ }
111
+ return false;
112
+ }
113
+
114
+ /**
115
+ * 处理目录请求
116
+ * @param {object} ctx Http请求上下文
117
+ * @param {Array} infos 驱动信息列表
118
+ * @param {number} len 列表长度
119
+ * @param {string} path 请求路径
120
+ */
121
+ async _handleDirReq(ctx, infos, len, path) {
122
+ var q = this._getQueryString(ctx);
123
+ for (var i = 0; i < len; i++) {
124
+ var info = infos[i];
125
+ let is_running = await this.isRunning(info.name);
126
+ if (is_running) {
127
+ let mod = this.get(info.name);
128
+ if (mod) {
129
+ var p = mod.config.path;
130
+ if (path === p) {
131
+ this._handleExactMatch(ctx, q, p);
132
+ break;
133
+ } else {
134
+ await this._initDrive(mod);
135
+ var done = await mod.run(ctx, () => {});
136
+ if (done) {
137
+ break;
138
+ }
139
+ }
140
+ }
141
+ }
142
+ }
143
+ }
144
+
145
+ /**
146
+ * 获取查询字符串
147
+ * 兼容 Koa 的 ctx.request.querystring 与 mm_web_server 的 ctx.params.query
148
+ * @param {object} ctx Http请求上下文
149
+ * @returns {string} 查询字符串
150
+ */
151
+ _getQueryString(ctx) {
152
+ if (ctx.request && ctx.request.querystring !== undefined) {
153
+ return ctx.request.querystring;
154
+ }
155
+ var query = ctx.params && ctx.params.query;
156
+ if (query && typeof query === 'object') {
157
+ return new URLSearchParams(query).toString();
158
+ }
159
+ return '';
160
+ }
161
+
162
+ /**
163
+ * 处理精确匹配
164
+ * @param {object} ctx Http请求上下文
165
+ * @param {string} q 查询字符串
166
+ * @param {string} p 路径
167
+ */
168
+ _handleExactMatch(ctx, q, p) {
169
+ // mm_web_server 原生 ctx 无 status 字段,未设置时视为 404 以兼容旧逻辑
170
+ var status = ctx.status === undefined ? 404 : ctx.status;
171
+ if (status === 404 && typeof ctx.redirect === 'function') {
172
+ // 驱动路径可能已带尾部斜杠,避免拼接出双斜杠
173
+ var target = p.endsWith('/') ? p : p + '/';
174
+ if (q) {
175
+ ctx.redirect(target + "?" + q);
176
+ } else {
177
+ ctx.redirect(target);
178
+ }
179
+ }
180
+ }
181
+ }
182
+
183
+ /**
184
+ * 静态文件驱动类
185
+ * @type {Drive}
186
+ */
187
+ StaticManager.prototype.Drive = Static;
188
+
189
+ /**
190
+ * 注册一个静态资源驱动(方案B:手动注册,不扫描目录)
191
+ * @param {object} config 驱动配置(必须包含 name)
192
+ * @returns {object} 驱动实例
193
+ */
194
+ StaticManager.prototype.add = function (config) {
195
+ var drive = new this.Drive({ run_path: this.config.run_path, ...config }, this);
196
+ this.set(drive.config.name, drive);
197
+ return drive;
198
+ };
199
+
200
+ /**
201
+ * 注销一个静态资源驱动(先停止再删除注册表项)
202
+ * @param {string} name 驱动名称
203
+ * @returns {Promise<boolean>} 是否删除成功
204
+ */
205
+ StaticManager.prototype.del = async function (name) {
206
+ var drive = this.get(name);
207
+ if (drive) {
208
+ await drive.stop();
209
+ }
210
+ return await Manager.prototype.del.call(this, name);
211
+ };
212
+
213
+ /**
214
+ * 重启一个静态资源驱动(stop 后重新 init/start)
215
+ * 注意:生命周期状态机不支持 stopped 后再次 start,失败时需 del 后重新 add 实现手动重启
216
+ * @param {string} name 驱动名称
217
+ * @returns {Promise<boolean>} 是否执行成功
218
+ */
219
+ StaticManager.prototype.reload = async function (name) {
220
+ var drive = this.get(name);
221
+ if (!drive) {
222
+ this.log("warn", "驱动不存在,无法重启", name);
223
+ return false;
224
+ }
225
+ try {
226
+ await drive.stop();
227
+ await this._initDrive(drive);
228
+ await drive.start();
229
+ return true;
230
+ } catch (err) {
231
+ this.log("warn", "驱动重启失败(生命周期不支持停止后重启,请 del 后重新 add)", name, err);
232
+ return false;
233
+ }
234
+ };
235
+
236
+ /**
237
+ * 获取所有驱动配置列表(方案B:来自注册表,替代旧基类 getInfos)
238
+ * @returns {Array} 驱动配置数组
239
+ * @private
240
+ */
241
+ StaticManager.prototype._getInfos = function () {
242
+ return this.values().map((d) => d.config);
243
+ };
244
+
245
+ /**
246
+ * 初始化驱动实例(重复调用安全由驱动自身保证)
247
+ * @param {object} drive 驱动实例
248
+ * @returns {Promise<void>} 初始化完成
249
+ * @private
250
+ */
251
+ StaticManager.prototype._initDrive = async function (drive) {
252
+ await drive.init();
253
+ };
254
+
255
+ /**
256
+ * 初始化桥接(生命周期汇聚点:init() 与 do('init') 均调用,触发 onInit)
257
+ * @returns {Promise<void>} 初始化完成
258
+ * @private
259
+ */
260
+ StaticManager.prototype._init = async function () {
261
+ await this.onInit();
262
+ };
263
+
264
+ /**
265
+ * 启动桥接(生命周期汇聚点,触发 onStart)
266
+ * @returns {Promise<void>} 启动完成
267
+ * @private
268
+ */
269
+ StaticManager.prototype._start = async function () {
270
+ await this.onStart();
271
+ };
272
+
273
+ /**
274
+ * 停止桥接(生命周期汇聚点,触发 onStop)
275
+ * @returns {Promise<void>} 停止完成
276
+ * @private
277
+ */
278
+ StaticManager.prototype._stop = async function () {
279
+ await this.onStop();
280
+ };
281
+
282
+ /**
283
+ * @module 导出静态文件管理器类
284
+ */
285
+ module.exports = StaticManager;
package/lib/static.js ADDED
@@ -0,0 +1,237 @@
1
+ const path = require('path');
2
+ const Item = require('mm_machine').Mod;
3
+ const { string } = require('mm_helper');
4
+
5
+ /**
6
+ * 静态文件驱动类
7
+ * @augments {Item}
8
+ * @class
9
+ */
10
+ class Static extends Item {
11
+ static config = {
12
+ // 应用名
13
+ 'app': 'server',
14
+ // 插件名
15
+ 'plugin': 'sys',
16
+ // 物理根目录
17
+ 'root': '',
18
+ // 路由根路径
19
+ 'path': '',
20
+ // 首页
21
+ 'index': 'index.html',
22
+ // 重定向
23
+ 'redirect': true,
24
+ // 缓存保留10分钟
25
+ 'max_age': 60 * 10,
26
+ // 是否保持不变
27
+ 'immutable': true,
28
+ // 是否启用brotli压缩
29
+ 'brotli': true,
30
+ // 是否启用gizp压缩
31
+ 'gzip': true,
32
+ // 允许访问的拓展名
33
+ 'extensions': null,
34
+ // 是否将ES6转换AMD
35
+ 'convert_amd': true,
36
+ // 主程序文件 - 默认为空
37
+ 'main': '',
38
+ // 状态 0未启用,1启用
39
+ 'state': 1,
40
+ // 驱动目录(方案B:手动配置,替代目录扫描)
41
+ 'dir': '',
42
+ // 运行根路径(计算物理根目录的回退值,为空时取父级配置或 process.cwd())
43
+ 'run_path': ''
44
+ };
45
+ /**
46
+ * 构造函数
47
+ * @param {object} config 配置参数
48
+ * @param {object} parent 父对象
49
+ * @class
50
+ */
51
+ constructor(config, parent) {
52
+ super({ ...Static.config, ...config });
53
+ // 父级管理器引用(用于默认配置回退与插件查找)
54
+ this.parent = parent || null;
55
+ }
56
+ }
57
+
58
+ /**
59
+ * 发送文件函数(复用 Static 实例的原生文件发送,不依赖 koa-send)
60
+ * @param {object} ctx Http请求上下文
61
+ * @param {string} file_path 文件路径
62
+ * @param {object} config 发送配置(保留参数,兼容旧调用)
63
+ * @returns {Promise<string>} 发送成功返回物理文件路径
64
+ */
65
+ Static.prototype.send = async function (ctx, file_path, config) {
66
+ if (!this.static) {
67
+ this._preset();
68
+ }
69
+ return await this.static._sendFile(ctx, file_path);
70
+ };
71
+
72
+ /**
73
+ * 生命周期初始化钩子:规范化配置并创建静态文件处理器
74
+ * @returns {Promise<void>} 初始化完成
75
+ */
76
+ Static.prototype.onInit = async function () {
77
+ this._preset();
78
+ };
79
+
80
+ /**
81
+ * 更新配置对象前
82
+ */
83
+ Static.prototype._preset = function () {
84
+ var cg = this.config;
85
+ if (cg.maxAge) {
86
+ cg.max_age = cg.maxAge * 1000;
87
+ delete cg.maxAge;
88
+ }
89
+ if (!cg.app) {
90
+ cg.app = this.getAppName(cg.dir);
91
+ }
92
+ if (!cg.plugin) {
93
+ cg.plugin = this.getPluginName(cg.dir);
94
+ }
95
+ if (!cg.path) {
96
+ let app = cg.app || this.getAppName(cg.dir);
97
+ let plugin = cg.plugin || this.getPluginName(cg.dir);
98
+ if (plugin) {
99
+ cg.path = `/${app}/${plugin}/`;
100
+ }
101
+ else {
102
+ cg.path = `/${app}/`;
103
+ }
104
+ }
105
+ else if (!cg.path.endsWith('/')) {
106
+ cg.path += '/';
107
+ }
108
+ if (!cg.root) {
109
+ cg.root = this._getRoot(cg.dir);
110
+ }
111
+ const Static = require('./static_handle');
112
+ this.static = new Static(cg);
113
+ };
114
+
115
+ /**
116
+ * 获取应用名
117
+ * @param {string} file 文件路径
118
+ * @returns {string} 应用名
119
+ */
120
+ Static.prototype.getAppName = function (file) {
121
+ if (!file || typeof file !== 'string') {
122
+ return '';
123
+ }
124
+ return string.between(file, 'app/', '/') || '';
125
+ };
126
+
127
+ /**
128
+ * 获取插件名
129
+ * @param {string} file 文件路径
130
+ * @returns {string} 插件名
131
+ */
132
+ Static.prototype.getPluginName = function (file) {
133
+ if (!file || typeof file !== 'string') {
134
+ return '';
135
+ }
136
+ return string.between(file, 'plugin/', '/') || '';
137
+ };
138
+
139
+ /**
140
+ * 获取物理根目录
141
+ * @param {string} file 文件路径
142
+ * @returns {string} 物理根目录
143
+ */
144
+ Static.prototype._getRoot = function (file) {
145
+ if (!file || typeof file !== 'string') {
146
+ return '';
147
+ }
148
+ let f = file.replace(/\\/g, '/');
149
+ let dir = path.dirname(f).replace(/\\/g, '/').toLowerCase();
150
+ let run_path = (this.config.run_path || (this.parent && this.parent.config.run_path) || process.cwd()).replace(/\\/g, '/').toLowerCase();
151
+ return dir.replace(run_path, '.');
152
+ };
153
+
154
+ /**
155
+ * 执行前
156
+ * @param {object} ctx http请求上下文
157
+ * @param {object} path 文件路径
158
+ */
159
+ Static.prototype.before = async function (ctx, path) { };
160
+
161
+ /**
162
+ * 执行
163
+ * 兼容两种调用方式:
164
+ * - 中间件方式:main(ctx, next),文件路径取自 ctx.path / ctx.params.path
165
+ * - 显式路径方式:main(ctx, file_path),直接发送指定文件
166
+ * @param {object} ctx http请求上下文
167
+ * @param {object|Function} path 文件路径或下一个中间件
168
+ * @returns {Promise<string>} 发送成功返回物理文件路径,未命中返回空字符串
169
+ */
170
+ Static.prototype.main = async function (ctx, path) {
171
+ if (typeof path === 'function') {
172
+ return await this.run(ctx, path);
173
+ }
174
+ return await this.send(ctx, path, this.config);
175
+ };
176
+
177
+ /**
178
+ * 执行后
179
+ * @param {object} ctx http请求上下文
180
+ * @param {object} path 文件路径
181
+ */
182
+ Static.prototype.after = async function (ctx, path) { };
183
+
184
+ /**
185
+ * 执行静态文件
186
+ * @param {object} ctx Http请求上下文
187
+ * @param {object} next 跳过当前函数
188
+ * @returns {string} 执行成功返回文件路径
189
+ */
190
+ Static.prototype.run = async function (ctx, next) {
191
+ if (!this.static) {
192
+ this._preset();
193
+ }
194
+ return await this.static.main(ctx, next);
195
+ };
196
+
197
+ /**
198
+ * 删除静态文件缓存
199
+ * @param {string} url 请求路径(含查询参数),如 /src/test.js
200
+ * @returns {Promise<void>} 删除完成
201
+ */
202
+ Static.prototype._delCache = async function (url) {
203
+ if (!this.static) {
204
+ this._preset();
205
+ }
206
+ await this.static._delCache(url);
207
+ };
208
+
209
+ /**
210
+ * 获取名称
211
+ * @param {string} file 文件路径
212
+ * @returns {string} 名称
213
+ */
214
+ Static.prototype._getName = function (file) {
215
+ return path.basename(path.dirname(path.dirname(file)));
216
+ };
217
+
218
+ /**
219
+ * 获取模型
220
+ * @param {string} type 模型类型
221
+ * @returns {object} 返回获取到的模型
222
+ */
223
+ Static.prototype.getModel = function (type) {
224
+ let model = { ...this.config };
225
+ let dir = this.config.dir || '';
226
+ let l = path.sep;
227
+ let app_name = string.between(dir, 'app' + l, l);
228
+ let plugin_name = string.between(dir, 'plugin' + l, l);
229
+ let name = path.basename(dir);
230
+ model.app = app_name;
231
+ model.plugin = plugin_name;
232
+ model.name = model.name || (plugin_name ? plugin_name + '_' + name : app_name + '_' + name);
233
+ model.path = model.path || (plugin_name ? `/${app_name}/${plugin_name}/` : `/${app_name}/`);
234
+ return model;
235
+ };
236
+
237
+ module.exports = Static;