mm_statics 1.9.0 → 1.9.1

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