@maiyunnet/kebab 2.0.3 → 2.0.4

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/core.ts DELETED
@@ -1,880 +0,0 @@
1
- /**
2
- * Project: Kebab, User: JianSuoQiYue
3
- * Date: 2019-5-3 23:54
4
- * Last: 2020-4-11 22:34:58, 2022-10-2 14:13:06, 2022-12-28 20:33:24, 2023-12-15 11:49:02, 2024-7-2 15:23:35, 2025-6-13 19:45:53
5
- */
6
- import * as cp from 'child_process';
7
- import * as http from 'http';
8
- import * as http2 from 'http2';
9
- import * as stream from 'stream';
10
- import * as lTime from '~/lib/time';
11
- import * as lFs from '~/lib/fs';
12
- import * as lText from '~/lib/text';
13
- import * as lNet from '~/lib/net';
14
- import * as lCrypto from '~/lib/crypto';
15
- import * as lResponse from '~/lib/net/response';
16
- import * as sCtr from '~/sys/ctr';
17
- import * as kebab from '~/index';
18
- import * as types from '~/types';
19
-
20
- /** --- 全局参数 --- */
21
- export const globalConfig: types.IConfig & {
22
- 'httpPort': number;
23
- 'httpsPort': number;
24
- 'rpcPort': number;
25
- 'rpcSecret': string;
26
- 'debug': boolean;
27
- 'max': number;
28
- } = {} as types.Json;
29
-
30
- /** --- Cookie 设置的选项 --- */
31
- export interface ICookieOptions {
32
- 'ttl'?: number;
33
- 'path'?: string;
34
- 'domain'?: string;
35
- 'ssl'?: boolean;
36
- 'httponly'?: boolean;
37
- 'samesite'?: 'None' | 'Lax' | 'Strict';
38
- }
39
-
40
- /**
41
- * --- 设置 cookie ---
42
- * @param ctr ctr 实例
43
- * @param name 名
44
- * @param value 值
45
- * @param opt 选项,ttl, 默认和 undefined 为关闭浏览器失效
46
- */
47
- export function setCookie(ctr: sCtr.Ctr, name: string, value: string, opt: ICookieOptions = {}): void {
48
- const res = ctr.getPrototype('_res');
49
- if (!res) {
50
- return;
51
- }
52
-
53
- /*
54
- const expires = lTime.get(ctr, {
55
- 'data': lTime.stamp() + ttl
56
- }).toUTCString();
57
- */
58
- const maxAge = opt.ttl === undefined ? '' : `; Max-Age=${opt.ttl}`;
59
- const path = `; path=${opt.path ?? '/'}`;
60
- const domain = opt.domain ? `; domain=${opt.domain}` : '';
61
- const secure = opt.ssl ? '; secure' : '';
62
- const httpOnly = opt.httponly ? '; HttpOnly' : '';
63
- const sameSite = opt.samesite ? '; SameSite=' + opt.samesite : '';
64
- const cookies: string[] = res.getHeader('set-cookie') as string[] | undefined ?? [];
65
- // cookies.push(`${name}=${encodeURIComponent(value)}; expires=${expires}; Max-Age=${ttl}${path}${domain}${secure}${httpOnly}`);
66
- cookies.push(`${name}=${encodeURIComponent(value)}${maxAge}${path}${domain}${secure}${httpOnly}${sameSite}`);
67
- res.setHeader('set-cookie', cookies);
68
- }
69
-
70
- /**
71
- * --- 生成基础的范围随机数 ---
72
- * @param min >= 最小值
73
- * @param max <= 最大值
74
- * @param prec 保留几位小数
75
- */
76
- export function rand(min: number, max: number, prec: number = 0): number {
77
- if (prec < 0) {
78
- prec = 0;
79
- }
80
- const p = Math.pow(10, prec);
81
- min = min * p;
82
- max = max * p;
83
- return Math.round(Math.random() * (max - min) + min) / p;
84
- }
85
-
86
- // --- 随机 ---
87
- export const RANDOM_N = '0123456789';
88
- export const RANDOM_U = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
89
- export const RANDOM_L = 'abcdefghijklmnopqrstuvwxyz';
90
-
91
- export const RANDOM_UN = RANDOM_U + RANDOM_N;
92
- export const RANDOM_LN = RANDOM_L + RANDOM_N;
93
- export const RANDOM_LU = RANDOM_L + RANDOM_U;
94
- export const RANDOM_LUN = RANDOM_L + RANDOM_U + RANDOM_N;
95
- export const RANDOM_V = 'ACEFGHJKLMNPRSTWXY34567';
96
- export const RANDOM_LUNS = RANDOM_LUN + '()`~!@#$%^&*-+=_|{}[]:;"<>,.?/]"';
97
-
98
- /**
99
- * --- 生成随机字符串 ---
100
- * @param length 长度
101
- * @param source 采样值
102
- * @param block 排除的字符
103
- */
104
- export function random(length: number = 8, source: string = RANDOM_LN, block: string = ''): string {
105
- // --- 剔除 block 字符 ---
106
- let len = block.length;
107
- if (len > 0) {
108
- for (let i = 0; i < len; ++i) {
109
- source = source.replace(block[i], '');
110
- }
111
- }
112
- len = source.length;
113
- if (len === 0) {
114
- return '';
115
- }
116
- let temp = '';
117
- for (let i = 0; i < length; ++i) {
118
- temp += source[rand(0, len - 1)];
119
- }
120
- return temp;
121
- }
122
-
123
- export const CONVERT62_CHAR = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
124
-
125
- /**
126
- * --- 将 10 进制转换为 62 进制 ---
127
- * @param n 10 进制数字最大 9223372036854775807n
128
- */
129
- export function convert62(n: bigint | number | string): string {
130
- if (typeof n !== 'bigint') {
131
- n = BigInt(n);
132
- }
133
- let res = '';
134
- while (n > 0) {
135
- res = CONVERT62_CHAR[Number(n % 62n)] + res;
136
- n = n / 62n;
137
- }
138
- return res;
139
- }
140
-
141
- /**
142
- * --- 将 62 进制转换为 10 进制 ---
143
- * @param n 62 进制数字最大 aZl8N0y58M7
144
- */
145
- export function unconvert62(n: string): bigint {
146
- let res = 0n;
147
- const nl = n.length;
148
- for (let i = 1; i <= nl; ++i) {
149
- res += BigInt(CONVERT62_CHAR.indexOf(n[i - 1])) * (62n ** BigInt(nl - i));
150
- }
151
- return res;
152
- }
153
-
154
- /**
155
- * --- 去除 html 的空白符、换行以及注释 ---
156
- * @param text 要纯净的字符串
157
- */
158
- export function purify(text: string): string {
159
- text = '>' + text + '<';
160
- const keepScripts: string[] = [];
161
- const keepPres: string[] = [];
162
- let nums: number = -1;
163
- let nump: number = -1;
164
- text = text.replace(/<!--([\s\S]*?)-->/g, '').replace(/<script[\s\S]+?<\/script>/g, function(t: string): string {
165
- keepScripts.push(t);
166
- return '[SCRIPT]';
167
- }).replace(/<pre[\s\S]+?<\/pre>/g, function(t: string): string {
168
- keepPres.push(t);
169
- return '[PRE]';
170
- }).replace(/>([\s\S]*?)</g, function(t: string, t1: string): string {
171
- return '>' + t1.replace(/\t|\r\n| {2}/g, '').replace(/\n|\r/g, '') + '<';
172
- }).replace(/\[SCRIPT\]/g, function(): string {
173
- ++nums;
174
- return keepScripts[nums];
175
- }).replace(/\[PRE\]/g, function(): string {
176
- ++nump;
177
- return keepPres[nump];
178
- });
179
- return text.slice(1, -1);
180
- }
181
-
182
- /**
183
- * --- 判断一个对象是否符合示例组,返回空字符串代表校验通过,返回:应该的类型:位置:传入的类型 ---
184
- * @param val 对象
185
- * @param type 示例组
186
- * @param tree 当前树,无需传入
187
- */
188
- export function checkType(val: any, type: any, tree: string = 'root'): string {
189
- /** --- 要校验的对象 --- */
190
- const vtype = typeof val;
191
- if (Array.isArray(type)) {
192
- // --- 数组的话 ---
193
- if (!Array.isArray(val)) {
194
- return 'array:' + tree + ':' + vtype;
195
- }
196
- for (let i = 0; i < val.length; ++i) {
197
- const res = checkType(val[i], type[0], tree + '.' + i.toString());
198
- if (res) {
199
- return res;
200
- }
201
- }
202
- return '';
203
- }
204
- /** --- 要符合的类型 --- */
205
- const ttype = typeof type;
206
- if (type instanceof RegExp) {
207
- // --- 正则 ---
208
- if (vtype !== 'string') {
209
- return 'regexp:' + tree + ':' + vtype;
210
- }
211
- return type.test(val) ? '' : 'regexp:' + tree + ':' + vtype;
212
- }
213
- if (ttype === 'string') {
214
- if (vtype !== 'string' && val !== undefined && val !== null) {
215
- return 'string:' + tree + ':' + vtype;
216
- }
217
- // --- 是字符串、undefined、null ---
218
- if (type) {
219
- return val ? '' : 'require:' + tree + ':' + vtype;
220
- }
221
- return '';
222
- }
223
- if (val === undefined || val === null) {
224
- return ttype + ':' + tree + ':' + (val === undefined ? 'undefined' : 'null');
225
- }
226
- if (ttype === 'object') {
227
- if (vtype !== 'object') {
228
- return 'object:' + tree + ':' + vtype;
229
- }
230
- if (Array.isArray(val)) {
231
- return 'object:' + tree + ':array';
232
- }
233
- if (!Object.keys(type).length) {
234
- return '';
235
- }
236
- // --- 先判断每个值是否相等 ---
237
- for (const key in type) {
238
- const res = checkType(val[key], type[key], tree + '.' + key);
239
- if (res) {
240
- return res;
241
- }
242
- }
243
- // --- 再判断是否传了类型限定中没有的值 ---
244
- for (const key in val) {
245
- if (type[key] !== undefined) {
246
- continue;
247
- }
248
- return `undefined:${tree}.${key}:${typeof val[key]}`;
249
- }
250
- return '';
251
- }
252
- return vtype === ttype ? '' : ttype + ':' + tree + ':' + vtype;
253
- }
254
-
255
- /**
256
- * --- 获取 MUID ---
257
- * @param ctr Ctr 对象
258
- * @param opt len: 8 - 32, 默认 8; bin: 是否含有大小写, 默认 true; key: 多样性混合, 默认空; insert: 插入指定字符, 最好不超过 2 字符,默认空,num: 是否含有数字,默认 true
259
- */
260
- export function muid(ctr: sCtr.Ctr, opt: {
261
- 'len'?: number;
262
- 'bin'?: boolean;
263
- 'key'?: string;
264
- 'insert'?: string;
265
- 'num'?: boolean;
266
- } = {}): string {
267
- const len = opt.len ?? 8;
268
- const bin = opt.bin ?? true;
269
- const key = opt.key ?? '';
270
- const insert = opt.insert ?? '';
271
- const ilen = insert.length;
272
- const num = opt.num ?? true;
273
-
274
- const headers = ctr.getPrototype('_headers');
275
- const req = ctr.getPrototype('_req');
276
- const char = lCrypto.hashHmac('sha1', (headers['user-agent'] ?? '') +
277
- (headers['referer'] ?? '') +
278
- (headers['accept-language'] ?? '') +
279
- (req.socket.remoteAddress ?? '') +
280
- ((headers['x-forwarded-for'] as string | undefined) ?? '') +
281
- ((headers['cf-connecting-ip'] as string | undefined) ?? '') + 'muid' + key + rand(0, 1000000000).toString(), 'muid');
282
- if (!char) {
283
- return '';
284
- }
285
-
286
- // --- 生成随机数 ---
287
- const over = random(len - 1 - ilen, bin ? (num ? RANDOM_LUN : RANDOM_LU) : (num ? RANDOM_LN : RANDOM_L)) + char[20];
288
- return over[0] + insert + over.slice(1);
289
- }
290
-
291
- /**
292
- * --- 获取 IP(非安全 IP)---
293
- * @param ctr
294
- */
295
- export function ip(
296
- ctr: sCtr.Ctr | http.IncomingHttpHeaders,
297
- req?: http2.Http2ServerRequest | http.IncomingMessage
298
- ): string {
299
- const headers: http.IncomingHttpHeaders = ctr instanceof sCtr.Ctr ? ctr.getPrototype('_headers') : ctr;
300
- if (typeof headers['cf-connecting-ip'] === 'string') {
301
- return headers['cf-connecting-ip'];
302
- }
303
- else if (typeof headers['x-forwarded-for'] === 'string') {
304
- return headers['x-forwarded-for'];
305
- }
306
- else {
307
- if (!req) {
308
- if (ctr instanceof sCtr.Ctr) {
309
- req = ctr.getPrototype('_req');
310
- }
311
- else {
312
- return '';
313
- }
314
- }
315
- return req.socket.remoteAddress ?? '';
316
- }
317
- }
318
-
319
- export const REAL_IP_X = 'x-forwarded-for';
320
- export const REAL_IP_CF = 'cf-connecting-ip';
321
-
322
- /**
323
- * --- 获取直连 IP(安全 IP) ---
324
- * @param ctr
325
- * @param name 输入安全的 header
326
- */
327
- export function realIP(ctr: sCtr.Ctr, name: string = ''): string {
328
- const headers: http.IncomingHttpHeaders = ctr.getPrototype('_headers');
329
- if (name !== '') {
330
- const value = headers[name];
331
- if (typeof value === 'string') {
332
- return value;
333
- }
334
- }
335
- const req: http2.Http2ServerRequest | http.IncomingMessage = ctr.getPrototype('_req');
336
- return req.socket.remoteAddress ?? '';
337
- }
338
-
339
- // --- 以下 Mutton 没有 ---
340
-
341
- /**
342
- * --- 间隔一段时间 ---
343
- * @param ms 间隔毫秒
344
- */
345
- export function sleep(ms: number): Promise<void> {
346
- return new Promise(function(resolve) {
347
- setTimeout(function() {
348
- resolve();
349
- }, ms);
350
- });
351
- }
352
-
353
- /**
354
- * --- 将对象进行升序排列 ---
355
- * @param o 要重排的对象
356
- */
357
- export function objectSort<T extends Record<string, any>>(o: T): T {
358
- const ordered: types.Json = {};
359
- const list = Object.keys(o).sort();
360
- for (const key of list) {
361
- if ((typeof o[key] === 'object') && (!Array.isArray(o[key]))) {
362
- ordered[key] = objectSort(o[key]);
363
- }
364
- else {
365
- ordered[key] = o[key];
366
- }
367
- }
368
- return ordered;
369
- }
370
-
371
- /**
372
- * --- 将对象的所有属性清除包括键,不会破坏引用关系,对象变量依然保证是引用状态 ---
373
- * @param obj 要清除的对象
374
- * @patam deep 也将子项都清空,如果子项有独立引用的话也要清空的话则要设置为 true
375
- */
376
- export function emptyObject(obj: Record<string, types.Json>, deep: boolean = false): void {
377
- const keys = Object.keys(obj);
378
- for (const key of keys) {
379
- if (deep) {
380
- const value = obj[key];
381
- if (typeof value === 'object') {
382
- emptyObject(value);
383
- }
384
- }
385
- delete obj[key];
386
- }
387
- }
388
-
389
- /**
390
- * --- 调用前自行创建 passThrough,并且调用 pipe 绑定到应该绑定的对象,然后再调用本函数 ---
391
- * @param passThrough passThrough 对象
392
- * @param data 数组
393
- * @param end 是否关闭写入,默认是,关闭后 passThrough 不能被写入,但仍然可读
394
- */
395
- export async function passThroughAppend(
396
- passThrough: stream.PassThrough,
397
- data: Array<stream.Readable | lResponse.Response | string | Buffer>,
398
- end: boolean = true
399
- ): Promise<void> {
400
- for (const item of data) {
401
- if (item instanceof stream.Readable || item instanceof lResponse.Response) {
402
- const stm = item instanceof stream.Readable ? item : item.getStream();
403
- // --- 读取流、Net 库 Response 对象 ---
404
- stm.pipe(passThrough, {
405
- 'end': false
406
- });
407
- await new Promise<void>((resolve) => {
408
- stm.on('end', () => {
409
- resolve();
410
- });
411
- });
412
- }
413
- else {
414
- // --- 字符串、Buffer ---
415
- await new Promise<void>((resolve) => {
416
- passThrough.write(item, () => {
417
- resolve();
418
- });
419
- });
420
- }
421
- }
422
- if (end) {
423
- passThrough.end();
424
- }
425
- }
426
-
427
- /**
428
- * --- 执行命令行 ---
429
- * @param command 命令字符串
430
- */
431
- export function exec(command: string): Promise<string | false> {
432
- return new Promise(function(resolve) {
433
- cp.exec(command, function(err, stdout) {
434
- if (err) {
435
- resolve(false);
436
- return;
437
- }
438
- resolve(stdout);
439
- });
440
- });
441
- }
442
-
443
- /**
444
- * --- 向主进程(或局域网同代码机子)发送广播将进行 reload 操作,等待回传 ---
445
- * --- 主要作用除代码热更新以外的其他情况 ---
446
- */
447
- export async function sendReload(hosts?: string[]): Promise<string[]> {
448
- if (!hosts) {
449
- // --- 本地模式 ---
450
- // eslint-disable-next-line no-console
451
- console.log('[ Child] Sending reload request...');
452
- process.send!({
453
- 'action': 'reload'
454
- });
455
- return [];
456
- }
457
- // --- 局域网模式 ---
458
- const time = lTime.stamp();
459
- /** --- 返回成功的 host --- */
460
- const rtn: string[] = [];
461
- for (const host of hosts) {
462
- const res = await lNet.get('http://' + host + ':' + globalConfig.rpcPort.toString() + '/' + lCrypto.aesEncrypt(lText.stringifyJson({
463
- 'action': 'reload',
464
- 'time': time
465
- }), globalConfig.rpcSecret), {
466
- 'timeout': 2
467
- });
468
- const content = await res.getContent();
469
- if (!content) {
470
- continue;
471
- }
472
- const str = content.toString();
473
- if (str === 'Done') {
474
- rtn.push(host);
475
- }
476
- }
477
- return rtn;
478
- }
479
-
480
- /**
481
- * --- 向主进程(或局域网同代码机子)发送广播将进行 restart 操作,停止监听并启动新进程,老进程在连接全部断开后自行销毁 ---
482
- * --- 主要用作不间断的代码热更新 ---
483
- */
484
- export async function sendRestart(hosts?: string[]): Promise<string[]> {
485
- if (!hosts) {
486
- // --- 本地模式 ---
487
- // eslint-disable-next-line no-console
488
- console.log('[ Child] Sending restart request...');
489
- process.send!({
490
- 'action': 'restart'
491
- });
492
- return [];
493
- }
494
- // --- 局域网模式 ---
495
- const time = lTime.stamp();
496
- /** --- 返回成功的 host --- */
497
- const rtn: string[] = [];
498
- for (const host of hosts) {
499
- const res = await lNet.get('http://' + host + ':' + globalConfig.rpcPort.toString() + '/' + lCrypto.aesEncrypt(lText.stringifyJson({
500
- 'action': 'restart',
501
- 'time': time
502
- }), globalConfig.rpcSecret), {
503
- 'timeout': 2
504
- });
505
- const content = await res.getContent();
506
- if (!content) {
507
- continue;
508
- }
509
- const str = content.toString();
510
- if (str === 'Done') {
511
- rtn.push(host);
512
- }
513
- }
514
- return rtn;
515
- }
516
-
517
- /** --- 跨进程全局变量 --- */
518
- export const global: Record<string, any> = {};
519
-
520
- /**
521
- * --- 设置跨线程的全局变量 ---
522
- * @param key 变量名
523
- * @param data 变量值
524
- * @param hosts 局域网列表
525
- */
526
- export async function setGlobal(key: string, data: types.Json, hosts?: string[]): Promise<string[]> {
527
- if (!hosts) {
528
- // --- 本地模式 ---
529
- process.send!({
530
- 'action': 'global',
531
- 'key': key,
532
- 'data': data
533
- });
534
- return [];
535
- }
536
- // --- 局域网模式 ---
537
- const time = lTime.stamp();
538
- /** --- 返回成功的 host --- */
539
- const rtn: string[] = [];
540
- for (const host of hosts) {
541
- const res = await lNet.get('http://' + host + ':' + globalConfig.rpcPort.toString() + '/' + lCrypto.aesEncrypt(lText.stringifyJson({
542
- 'action': 'global',
543
- 'time': time
544
- }), globalConfig.rpcSecret), {
545
- 'timeout': 2
546
- });
547
- const content = await res.getContent();
548
- if (!content) {
549
- continue;
550
- }
551
- const str = content.toString();
552
- if (str === 'Done') {
553
- rtn.push(host);
554
- }
555
- }
556
- return rtn;
557
- }
558
-
559
- /**
560
- * --- 移除某个跨线程全局变量 ---
561
- * @param key 变量名
562
- * @param hosts 局域网列表
563
- */
564
- export async function removeGlobal(key: string, hosts?: string[]): Promise<string[]> {
565
- return setGlobal(key, null, hosts);
566
- }
567
-
568
- /**
569
- * --- 上传并覆盖代码文件,config.json、kebab.json、.js.map、.ts, .gitignore 不会被覆盖和新建 ---
570
- * @param sourcePath zip 文件
571
- * @param path 要覆盖到的路径,无所谓是否 / 开头 / 结尾,是对方 kebab 的根据路开始算起
572
- * @param hosts 局域网多机部署,不设置默认本机部署
573
- * @param config 是否自动更新 config 的 set.staticVer 为最新,默认更新
574
- */
575
- export async function updateCode(
576
- sourcePath: string, path: string, hosts?: string[], config: boolean = true
577
- ): Promise<Record<string, {
578
- 'result': boolean;
579
- 'return': string;
580
- }>> {
581
- hosts ??= ['127.0.0.1'];
582
- /** --- 返回成功的 host --- */
583
- const rtn: Record<string, {
584
- 'result': boolean;
585
- 'return': string;
586
- }> = {};
587
- for (const host of hosts) {
588
- const fd = lNet.getFormData();
589
- if (!await fd.putFile('file', sourcePath)) {
590
- continue;
591
- }
592
- fd.putString('path', path);
593
- fd.putString('config', config ? '1' : '0');
594
- const res = await lNet.post('http://' + host + ':' + globalConfig.rpcPort.toString() + '/' + lCrypto.aesEncrypt(lText.stringifyJson({
595
- 'action': 'code',
596
- 'time': lTime.stamp()
597
- }), globalConfig.rpcSecret), fd, {
598
- 'timeout': 4
599
- });
600
- const content = await res.getContent();
601
- if (!content) {
602
- rtn[host] = {
603
- 'result': false,
604
- 'return': 'Network error'
605
- };
606
- continue;
607
- }
608
- const str = content.toString();
609
- rtn[host] = {
610
- 'result': str === 'Done' ? true : false,
611
- 'return': str
612
- };
613
- }
614
- return rtn;
615
- }
616
-
617
- /** --- log 设置的选项 --- */
618
- export interface ILogOptions {
619
- 'path': string;
620
- 'urlFull': string;
621
- 'hostname': string;
622
- 'req': http2.Http2ServerRequest | http.IncomingMessage | null;
623
- 'get': Record<string, types.Json>;
624
- 'cookie': Record<string, string>;
625
- 'headers': http.IncomingHttpHeaders;
626
- }
627
-
628
- /**
629
- * --- 写入文件日志 ---
630
- * @param msg 自定义内容
631
- * @param fend 文件名追加
632
- * @param opt 选项
633
- */
634
- export async function log(opt: sCtr.Ctr | ILogOptions, msg: string, fend: string = ''): Promise<void> {
635
- let req: http2.Http2ServerRequest | http.IncomingMessage | null;
636
- let headers: http.IncomingHttpHeaders;
637
- let get: Record<string, types.Json>;
638
- let cookie: Record<string, string>;
639
- let wpath: string;
640
- let urlFull: string;
641
- let hostname: string;
642
- if (opt instanceof sCtr.Ctr) {
643
- req = opt.getPrototype('_req');
644
- headers = opt.getPrototype('_headers');
645
- get = opt.getPrototype('_get');
646
- cookie = opt.getPrototype('_cookie');
647
- const config = opt.getPrototype('_config');
648
- wpath = config.const.path;
649
- urlFull = config.const.urlFull;
650
- hostname = config.const.hostname;
651
- }
652
- else {
653
- req = opt.req;
654
- headers = opt.headers;
655
- get = opt.get;
656
- cookie = opt.cookie;
657
- wpath = opt.path;
658
- urlFull = opt.urlFull;
659
- hostname = opt.hostname;
660
- }
661
- if (hostname === '') {
662
- hostname = 'system';
663
- }
664
-
665
- const realIp = req?.socket.remoteAddress ?? '';
666
- const clientIp = req ? ip(headers, req) : '';
667
-
668
- const [y, m, d, h] = lTime.format(null, 'Y-m-d-H').split('-');
669
- let path = kebab.LOG_CWD + hostname + fend + '/' + y + '/' + m + '/' + d + '/';
670
- const rtn = await lFs.mkdir(path, 0o777);
671
- if (!rtn) {
672
- return;
673
- }
674
- path += h + '.csv';
675
- if (!await lFs.isFile(path)) {
676
- if (!await lFs.putContent(path, 'TIME,UNIX,URL,COOKIE,USER_AGENT,REALIP,CLIENTIP,MESSAGE\n', {
677
- 'encoding': 'utf8',
678
- 'mode': 0o777
679
- })) {
680
- return;
681
- }
682
- }
683
- await lFs.putContent(path, '"' +
684
- lTime.format(null, 'H:i:s') + '","' +
685
- lTime.stamp().toString() + '","' +
686
- urlFull + wpath + (Object.keys(get).length ? '?' + lText.queryStringify(get).replace(/"/g, '""') : '') + '","' +
687
- lText.queryStringify(cookie).replace(/"/g, '""') + '","' +
688
- (headers['user-agent']?.replace(/"/g, '""') ?? 'No HTTP_USER_AGENT') + '","' +
689
- realIp.replace(/"/g, '""') + '","' +
690
- clientIp.replace(/"/g, '""') + '",' +
691
- JSON.stringify(msg.replace(/"/g, '""')) + '\n', {
692
- 'encoding': 'utf8',
693
- 'mode': 0o777,
694
- 'flag': 'a'
695
- });
696
- }
697
-
698
- /**
699
- * --- 获取日志内容为一个数组 ---
700
- * @param opt 参数
701
- */
702
- export async function getLog(opt: {
703
- /** --- 如 127.0.0.1 --- */
704
- 'host': string;
705
- /** --- 如 2024/08/01/22 --- */
706
- 'path': string;
707
- /** --- 如 -error --- */
708
- 'fend'?: string;
709
- /** --- 仅显示被搜索到的行 --- */
710
- 'search'?: string;
711
- /** --- 跳过条数 --- */
712
- 'offset'?: number;
713
- /** --- 最大限制,默认 100 --- */
714
- 'limit'?: number;
715
- }): Promise<string[][] | null | false> {
716
- const path = kebab.LOG_CWD + opt.host + (opt.fend ?? '') + '/' + opt.path + '.csv';
717
- if (!await lFs.isFile(path)) {
718
- return null;
719
- }
720
- /** --- 剩余 limit --- */
721
- let limit = opt.limit ?? 100;
722
- /** --- 剩余 offset --- */
723
- let offset = opt.offset ?? 0;
724
- return new Promise<string[][] | null | false>((resolve) => {
725
- const list: string[][] = [];
726
- /** --- 当前行号 --- */
727
- let line = 0;
728
- /** --- 当前行数据 --- */
729
- let packet = '';
730
- lFs.createReadStream(path, {
731
- 'encoding': 'utf8'
732
- }).on('data', (buf) => {
733
- if (typeof buf !== 'string') {
734
- return;
735
- }
736
- while (true) {
737
- // --- 分包 ---
738
- const index = buf.indexOf('\n');
739
- if (index === -1) {
740
- // --- 本次包还没有结束 ---
741
- packet += buf;
742
- break;
743
- }
744
- // --- 本次行结束了 ---
745
- if (limit === 0) {
746
- break;
747
- }
748
- packet += buf.slice(0, index);
749
- buf = buf.slice(index + 1);
750
- ++line;
751
- // --- 先执行下本次完成的 ---
752
- if (line > 1) {
753
- if (offset === 0) {
754
- if (!opt.search || packet.includes(opt.search)) {
755
- const result: string[] = [];
756
- let currentField = '';
757
- let inQuotes = false;
758
- for (let i = 0; i < packet.length; ++i) {
759
- const char = packet[i];
760
- if (char === '"') {
761
- if (inQuotes && packet[i + 1] === '"') {
762
- currentField += '"';
763
- ++i;
764
- }
765
- else {
766
- inQuotes = !inQuotes;
767
- }
768
- }
769
- else if (char === ',' && !inQuotes) {
770
- result.push(currentField);
771
- currentField = '';
772
- }
773
- else {
774
- currentField += char;
775
- }
776
- }
777
- result.push(currentField);
778
- list.push(result);
779
- --limit;
780
- }
781
- }
782
- else {
783
- --offset;
784
- }
785
- }
786
- // --- 处理结束 ---
787
- packet = '';
788
- // --- 看看还有没有后面的粘连包 ---
789
- if (!buf.length) {
790
- // --- 没粘连包 ---
791
- break;
792
- }
793
- // --- 有粘连包 ---
794
- }
795
- }).on('end', () => {
796
- resolve(list);
797
- }).on('error', () => {
798
- resolve(false);
799
- });
800
- });
801
- }
802
-
803
- /**
804
- * --- 完整的克隆一份数组/对象,Kebab: yes, Mutton: no ---
805
- * @param obj 要克隆的对象
806
- */
807
- export function clone(obj: Record<string, any> | any[]): any[] | any {
808
- let newObj: any = {};
809
- if (obj instanceof Array) {
810
- newObj = [];
811
- for (let i = 0; i < obj.length; ++i) {
812
- if (obj[i] instanceof Date) {
813
- newObj[i] = new Date(obj[i].getTime());
814
- }
815
- else if (obj[i] instanceof FormData) {
816
- const fd = new FormData();
817
- for (const item of obj[i]) {
818
- fd.append(item[0], item[1]);
819
- }
820
- newObj[i] = fd;
821
- }
822
- else if (obj[i] === null) {
823
- newObj[i] = null;
824
- }
825
- else if (typeof obj[i] === 'object') {
826
- newObj[i] = clone(obj[i]);
827
- }
828
- else {
829
- newObj[i] = obj[i];
830
- }
831
- }
832
- }
833
- else {
834
- for (const key in obj) {
835
- if (obj[key] instanceof Date) {
836
- newObj[key] = new Date(obj[key].getTime());
837
- }
838
- else if (obj[key] instanceof FormData) {
839
- const fd = new FormData();
840
- for (const item of obj[key]) {
841
- fd.append(item[0], item[1]);
842
- }
843
- newObj[key] = fd;
844
- }
845
- else if (obj[key] === null) {
846
- newObj[key] = null;
847
- }
848
- else if (typeof obj[key] === 'object') {
849
- newObj[key] = clone(obj[key]);
850
- }
851
- else {
852
- newObj[key] = obj[key];
853
- }
854
- }
855
- }
856
- return newObj;
857
- }
858
-
859
- /**
860
- * --- 打印调试信息,线上环境不会打印 ---
861
- * @param message 参数
862
- * @param optionalParams 参数
863
- */
864
- export function debug(message?: any, ...optionalParams: any[]): void {
865
- if (!globalConfig.debug) {
866
- return;
867
- }
868
- // eslint-disable-next-line no-console
869
- console.debug(message, ...optionalParams);
870
- }
871
-
872
- /**
873
- * --- 向控制台直接显示内容,一般情况下禁止使用 ---
874
- * @param message 参数
875
- * @param optionalParams 参数
876
- */
877
- export function display(message?: any, ...optionalParams: any[]): void {
878
- // eslint-disable-next-line no-console
879
- console.log(message, ...optionalParams);
880
- }