lupine.api 1.1.58 → 1.1.60

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.
Files changed (137) hide show
  1. package/README.md +3 -3
  2. package/admin/admin-about.tsx +12 -16
  3. package/admin/admin-config.tsx +47 -44
  4. package/admin/admin-css.tsx +3 -3
  5. package/admin/admin-db.tsx +75 -75
  6. package/admin/admin-frame-helper.tsx +364 -364
  7. package/admin/admin-frame.tsx +164 -164
  8. package/admin/admin-index.tsx +65 -65
  9. package/admin/admin-login.tsx +111 -111
  10. package/admin/admin-menu-edit.tsx +637 -637
  11. package/admin/admin-menu-list.tsx +87 -87
  12. package/admin/admin-page-edit.tsx +564 -564
  13. package/admin/admin-page-list.tsx +83 -83
  14. package/admin/admin-performance.tsx +28 -28
  15. package/admin/admin-release.tsx +427 -426
  16. package/admin/admin-resources.tsx +382 -382
  17. package/admin/admin-shell.tsx +89 -89
  18. package/admin/admin-table-data.tsx +146 -146
  19. package/admin/admin-table-list.tsx +230 -230
  20. package/admin/admin-test-animations.tsx +395 -395
  21. package/admin/admin-test-component.tsx +823 -808
  22. package/admin/admin-test-edit.tsx +319 -319
  23. package/admin/admin-test-themes.tsx +56 -56
  24. package/admin/admin-tokens.tsx +338 -338
  25. package/admin/design/admin-design.tsx +174 -174
  26. package/admin/design/block-grid.tsx +36 -36
  27. package/admin/design/block-grid1.tsx +21 -21
  28. package/admin/design/block-paragraph.tsx +19 -19
  29. package/admin/design/block-title.tsx +19 -19
  30. package/admin/design/design-block-box.tsx +140 -140
  31. package/admin/design/drag-data.tsx +24 -24
  32. package/admin/index.ts +9 -9
  33. package/admin/package.json +15 -15
  34. package/admin/tsconfig.json +127 -127
  35. package/dev/copy-folder.js +32 -32
  36. package/dev/cp-index-html.js +69 -69
  37. package/dev/file-utils.js +12 -12
  38. package/dev/index.js +18 -19
  39. package/dev/package.json +12 -12
  40. package/dev/plugin-ifelse.js +168 -168
  41. package/dev/plugin-ifelse.test.js +37 -37
  42. package/dev/run-cmd.js +14 -14
  43. package/dev/send-request.js +12 -12
  44. package/package.json +55 -55
  45. package/src/admin-api/admin-api-helper.ts +210 -205
  46. package/src/admin-api/admin-api.ts +65 -65
  47. package/src/admin-api/admin-auth.ts +152 -146
  48. package/src/admin-api/admin-config.ts +94 -84
  49. package/src/admin-api/admin-csv.ts +94 -94
  50. package/src/admin-api/admin-db.ts +269 -269
  51. package/src/admin-api/admin-menu.ts +135 -135
  52. package/src/admin-api/admin-page.ts +135 -135
  53. package/src/admin-api/admin-performance.ts +128 -128
  54. package/src/admin-api/admin-release.ts +706 -700
  55. package/src/admin-api/admin-resources.ts +318 -318
  56. package/src/admin-api/admin-token-helper.ts +82 -79
  57. package/src/admin-api/admin-tokens.ts +90 -90
  58. package/src/admin-api/index.ts +2 -2
  59. package/src/admin-api/web-config-api.ts +19 -19
  60. package/src/api/api-cache.ts +103 -103
  61. package/src/api/api-helper.ts +44 -44
  62. package/src/api/api-module.ts +67 -60
  63. package/src/api/api-router.ts +177 -177
  64. package/src/api/api-shared-storage.ts +64 -64
  65. package/src/api/async-storage.ts +5 -5
  66. package/src/api/debug-service.ts +56 -56
  67. package/src/api/encode-html.ts +27 -27
  68. package/src/api/handle-status.ts +75 -75
  69. package/src/api/index.ts +15 -16
  70. package/src/api/mini-web-socket.ts +270 -270
  71. package/src/api/server-content-type.ts +82 -82
  72. package/src/api/server-render.ts +235 -215
  73. package/src/api/shell-service.ts +74 -74
  74. package/src/api/simple-storage.ts +80 -80
  75. package/src/api/static-server.ts +128 -125
  76. package/src/api/to-client-delivery.ts +26 -26
  77. package/src/app/app-cache.ts +55 -55
  78. package/src/app/app-helper.ts +62 -62
  79. package/src/app/app-message.ts +109 -109
  80. package/src/app/app-shared-storage.ts +363 -363
  81. package/src/app/app-start.ts +136 -136
  82. package/src/app/cleanup-exit.ts +16 -16
  83. package/src/app/host-to-path.ts +38 -38
  84. package/src/app/index.ts +11 -11
  85. package/src/app/process-dev-requests.ts +130 -130
  86. package/src/app/web-listener.ts +294 -294
  87. package/src/app/web-processor.ts +47 -42
  88. package/src/app/web-server.ts +100 -100
  89. package/src/common-js/web-env.js +104 -104
  90. package/src/index.ts +7 -7
  91. package/src/lang/api-lang-en.ts +26 -26
  92. package/src/lang/api-lang-zh-cn.ts +27 -27
  93. package/src/lang/index.ts +2 -2
  94. package/src/lang/lang-helper.ts +76 -76
  95. package/src/lang/lang-props.ts +6 -6
  96. package/src/lib/db/db-helper.ts +23 -23
  97. package/src/lib/db/db-mysql.ts +249 -250
  98. package/src/lib/db/db-sqlite.ts +101 -101
  99. package/src/lib/db/db.spec.ts +28 -28
  100. package/src/lib/db/db.ts +325 -325
  101. package/src/lib/db/index.ts +5 -5
  102. package/src/lib/index.ts +3 -3
  103. package/src/lib/logger.spec.ts +214 -214
  104. package/src/lib/logger.ts +281 -281
  105. package/src/lib/runtime-require.ts +37 -37
  106. package/src/lib/utils/cookie-util.ts +34 -34
  107. package/src/lib/utils/crypto.ts +58 -58
  108. package/src/lib/utils/date-utils.ts +317 -317
  109. package/src/lib/utils/deep-merge.ts +37 -37
  110. package/src/lib/utils/delay.ts +12 -12
  111. package/src/lib/utils/file-setting.ts +55 -55
  112. package/src/lib/utils/format-bytes.ts +11 -11
  113. package/src/lib/utils/fs-utils.ts +158 -158
  114. package/src/lib/utils/get-env.ts +27 -27
  115. package/src/lib/utils/index.ts +12 -12
  116. package/src/lib/utils/is-type.ts +48 -48
  117. package/src/lib/utils/load-env.ts +14 -14
  118. package/src/lib/utils/pad.ts +6 -6
  119. package/src/models/api-base.ts +5 -5
  120. package/src/models/api-module-props.ts +10 -11
  121. package/src/models/api-router-props.ts +26 -26
  122. package/src/models/app-cache-props.ts +33 -33
  123. package/src/models/app-data-props.ts +10 -10
  124. package/src/models/app-helper-props.ts +6 -6
  125. package/src/models/app-shared-storage-props.ts +38 -38
  126. package/src/models/app-start-props.ts +18 -18
  127. package/src/models/async-storage-props.ts +13 -13
  128. package/src/models/db-config.ts +30 -30
  129. package/src/models/host-to-path-props.ts +12 -12
  130. package/src/models/index.ts +16 -16
  131. package/src/models/json-object.ts +8 -8
  132. package/src/models/locals-props.ts +36 -36
  133. package/src/models/logger-props.ts +84 -84
  134. package/src/models/simple-storage-props.ts +13 -14
  135. package/src/models/to-client-delivery-props.ts +6 -6
  136. package/tsconfig.json +115 -115
  137. package/dev/plugin-gen-versions.js +0 -20
@@ -1,700 +1,706 @@
1
- import { ServerResponse } from 'http';
2
- import {
3
- IApiBase,
4
- Logger,
5
- apiCache,
6
- ServerRequest,
7
- ApiRouter,
8
- ApiHelper,
9
- langHelper,
10
- FsUtils,
11
- adminApiHelper,
12
- processRefreshCache,
13
- apiStorage,
14
- processRestartApp,
15
- processShell,
16
- } from 'lupine.api';
17
- import path from 'path';
18
- import { needDevAdminSession } from './admin-auth';
19
- import { adminTokenHelper } from './admin-token-helper';
20
-
21
- const releaseProgress = 'admin-release-progress';
22
- export class AdminRelease implements IApiBase {
23
- private logger = new Logger('release-api');
24
- protected router = new ApiRouter();
25
-
26
- constructor() {
27
- this.mountDashboard();
28
- }
29
-
30
- public getRouter(): ApiRouter {
31
- return this.router;
32
- }
33
-
34
- protected mountDashboard() {
35
- // called by FE
36
- this.router.use('/check', needDevAdminSession, this.check.bind(this));
37
- this.router.use('/update', needDevAdminSession, this.callUpdate.bind(this));
38
- this.router.use('/view-log', needDevAdminSession, this.viewLog.bind(this));
39
- // called online or by clients
40
- this.router.use('/refresh-cache', needDevAdminSession, this.refreshCache.bind(this));
41
- this.router.use('/restart-app', needDevAdminSession, this.restartApp.bind(this));
42
-
43
- this.router.use('/shell', needDevAdminSession, this.shell.bind(this));
44
-
45
- // ...ByClient will verify credentials from post, so it doesn't need AdminSession
46
- this.router.use('/byClientCheck', this.byClientCheck.bind(this));
47
- this.router.use('/byClientUpdate', this.byClientUpdate.bind(this));
48
- this.router.use('/byClientRefreshCache', this.byClientRefreshCache.bind(this));
49
- this.router.use('/byClientRestartApp', this.byClientRestartApp.bind(this));
50
- this.router.use('/byClientViewLog', this.byClientViewLog.bind(this));
51
-
52
- this.router.use('/byClientShell', this.byClientShell.bind(this));
53
- }
54
-
55
- async viewLog(req: ServerRequest, res: ServerResponse) {
56
- const jsonData = req.locals.json();
57
- const data = await this.chkData(jsonData, req, res, true);
58
- if (!data) return true;
59
-
60
- let targetUrl = data.targetUrl as string;
61
- if (targetUrl.endsWith('/')) {
62
- targetUrl = targetUrl.slice(0, -1);
63
- }
64
- const remoteData = await fetch(targetUrl + '/api/admin/release/byClientViewLog', {
65
- method: 'POST',
66
- body: JSON.stringify(data),
67
- });
68
- // (remoteData.body as any).pipe(res);
69
- const data2 = await remoteData.text();
70
- // res.setHeader('Content-Disposition', 'attachment; filename="log.txt"');
71
- res.writeHead(200, { 'Content-Type': 'application/octet-stream' });
72
- res.write(data2);
73
- res.end();
74
- return true;
75
- }
76
-
77
- async byClientViewLog(req: ServerRequest, res: ServerResponse) {
78
- const jsonData = req.locals.json();
79
- const data = await this.chkData(jsonData, req, res, true);
80
- if (!data) return true;
81
-
82
- const appData = apiCache.getAppData();
83
- const logFile = path.join(appData.apiPath, '../../log', data.logName);
84
- if (!(await FsUtils.pathExist(logFile))) {
85
- const response = {
86
- status: 'error',
87
- message: 'Log file not found.',
88
- };
89
- ApiHelper.sendJson(req, res, response);
90
- return true;
91
- }
92
- ApiHelper.sendFile(req, res, logFile);
93
- return true;
94
- }
95
-
96
- async refreshCache(req: ServerRequest, res: ServerResponse) {
97
- // check whether it's from online admin
98
- const json = await adminApiHelper.getDevAdminFromCookie(req, res, false);
99
- const jsonData = req.locals.json();
100
- if (json && jsonData && !Array.isArray(jsonData) && jsonData.isLocal) {
101
- await processRefreshCache(req);
102
- const response = {
103
- status: 'ok',
104
- message: 'Cache refreshed successfully.',
105
- };
106
- ApiHelper.sendJson(req, res, response);
107
- return true;
108
- }
109
-
110
- const data = await this.chkData(jsonData, req, res, true);
111
- if (!data) return true;
112
-
113
- let targetUrl = data.targetUrl as string;
114
- if (targetUrl.endsWith('/')) {
115
- targetUrl = targetUrl.slice(0, -1);
116
- }
117
- const remoteData = await fetch(targetUrl + '/api/admin/release/byClientRefreshCache', {
118
- method: 'POST',
119
- body: JSON.stringify(data),
120
- });
121
- const resultText = await remoteData.text();
122
- let remoteResult: any;
123
- try {
124
- remoteResult = JSON.parse(resultText);
125
- } catch (e: any) {
126
- remoteResult = { status: 'error', message: resultText };
127
- }
128
- const response = {
129
- status: 'ok',
130
- message: 'check.',
131
- ...remoteResult,
132
- };
133
- ApiHelper.sendJson(req, res, response);
134
- return true;
135
- }
136
-
137
- async restartApp(req: ServerRequest, res: ServerResponse) {
138
- // check whether it's from online admin
139
- const json = await adminApiHelper.getDevAdminFromCookie(req, res, false);
140
- const jsonData = req.locals.json();
141
- if (json && jsonData && !Array.isArray(jsonData) && jsonData.isLocal) {
142
- await processRestartApp(req);
143
- const response = {
144
- status: 'ok',
145
- message: 'Restart app successfully.',
146
- };
147
- ApiHelper.sendJson(req, res, response);
148
- return true;
149
- }
150
-
151
- const data = await this.chkData(jsonData, req, res, true);
152
- if (!data) return true;
153
-
154
- let targetUrl = data.targetUrl as string;
155
- if (targetUrl.endsWith('/')) {
156
- targetUrl = targetUrl.slice(0, -1);
157
- }
158
- const remoteData = await fetch(targetUrl + '/api/admin/release/byClientRestartApp', {
159
- method: 'POST',
160
- body: JSON.stringify(data),
161
- });
162
- const resultText = await remoteData.text();
163
- let remoteResult: any;
164
- try {
165
- remoteResult = JSON.parse(resultText);
166
- } catch (e: any) {
167
- remoteResult = { status: 'error', message: resultText };
168
- }
169
- const response = {
170
- status: 'ok',
171
- message: 'check.',
172
- ...remoteResult,
173
- };
174
- ApiHelper.sendJson(req, res, response);
175
- return true;
176
- }
177
-
178
- async shell(req: ServerRequest, res: ServerResponse) {
179
- // check whether it's from online admin
180
- const json = await adminApiHelper.getDevAdminFromCookie(req, res, false);
181
- const jsonData = req.locals.json();
182
- if (json && jsonData && !Array.isArray(jsonData) && jsonData.isLocal) {
183
- const result = await processShell(req);
184
- const response = {
185
- status: 'ok',
186
- message: 'Shell executed successfully.',
187
- result,
188
- };
189
- ApiHelper.sendJson(req, res, response);
190
- return true;
191
- }
192
-
193
- const data = await this.chkData(jsonData, req, res, true);
194
- if (!data) return true;
195
-
196
- let targetUrl = data.targetUrl as string;
197
- if (targetUrl.endsWith('/')) {
198
- targetUrl = targetUrl.slice(0, -1);
199
- }
200
- const remoteData = await fetch(targetUrl + '/api/admin/release/byClientShell', {
201
- method: 'POST',
202
- body: JSON.stringify(data),
203
- });
204
- const resultText = await remoteData.text();
205
- let remoteResult: any;
206
- try {
207
- remoteResult = JSON.parse(resultText);
208
- } catch (e: any) {
209
- remoteResult = { status: 'error', message: resultText };
210
- }
211
- const response = {
212
- status: 'ok',
213
- message: 'check.',
214
- ...remoteResult,
215
- };
216
- ApiHelper.sendJson(req, res, response);
217
- return true;
218
- }
219
-
220
- public async chkData(data: any, req: ServerRequest, res: ServerResponse, chkCredential: boolean) {
221
- // add access token
222
- if (!data || Array.isArray(data) || typeof data !== 'object' || !data.accessToken || !data.targetUrl) {
223
- const response = {
224
- status: 'error',
225
- message: 'Wrong data [missing parameters].', //langHelper.getLang('shared:wrong_data'),
226
- };
227
- ApiHelper.sendJson(req, res, response);
228
- return false;
229
- }
230
- if (chkCredential) {
231
- if (await adminTokenHelper.validateToken(data.accessToken)) {
232
- return data;
233
- } else if (
234
- process.env['DEV_ADMIN_PASS'] !== '' &&
235
- (data.accessToken === `${process.env['DEV_ADMIN_USER']}@${process.env['DEV_ADMIN_PASS']}` ||
236
- data.accessToken === `${process.env['DEV_ADMIN_USER']}:${process.env['DEV_ADMIN_PASS']}`)
237
- ) {
238
- return data;
239
- } else {
240
- const response = {
241
- status: 'error',
242
- message: 'Wrong data [wrong token].', //langHelper.getLang('shared:wrong_data'),
243
- };
244
- ApiHelper.sendJson(req, res, response);
245
- return false;
246
- }
247
- }
248
- return data;
249
- }
250
-
251
- // this is called by the FE, then call byClientCheck to get remote server's information
252
- async check(req: ServerRequest, res: ServerResponse) {
253
- const jsonData = req.locals.json();
254
- const data = await this.chkData(jsonData, req, res, false);
255
- if (!data) return true;
256
-
257
- // From app list is from local
258
- const appData = apiCache.getAppData();
259
- const folders = await FsUtils.getDirAndFiles(path.join(appData.apiPath, '..'));
260
- const apps = folders.filter((app: string) => app.endsWith('_web')).map((app: string) => app.replace('_web', ''));
261
-
262
- let targetUrl = data.targetUrl as string;
263
- if (targetUrl.endsWith('/')) {
264
- targetUrl = targetUrl.slice(0, -1);
265
- }
266
- const remoteData = await fetch(targetUrl + '/api/admin/release/byClientCheck', {
267
- method: 'POST',
268
- body: JSON.stringify(data),
269
- });
270
- const resultText = await remoteData.text();
271
- let remoteResult: any;
272
- try {
273
- remoteResult = JSON.parse(resultText);
274
- } catch (e: any) {
275
- remoteResult = { status: 'error', message: resultText };
276
- }
277
-
278
- // local dirs under _web
279
- const webSub: string[] = [];
280
- for (let j = 0; j < apps.length; j++) {
281
- const e = apps[j];
282
- const p0 = path.join(appData.apiPath, '..');
283
- const subFolders = await FsUtils.getDirsFullpath(path.join(p0, e + '_web'), 5);
284
- webSub.push(
285
- ...subFolders
286
- .filter((i) => i.isDirectory())
287
- .map((i) => path.join(i.parentPath.substring(p0.length + 1), i.name).replace(/\\/g, '/'))
288
- );
289
- }
290
- // const webSub = webSubFolders.filter(i => i.isDirectory()).map(i => path.join(i.parentPath.substring(appData.webPath.length + 1), i.name).replace(/\\/g, '/')).sort();
291
-
292
- const response = {
293
- releaseProgress: await apiStorage.get(releaseProgress),
294
- status: 'ok',
295
- message: 'check.',
296
- appsFrom: apps,
297
- ...remoteResult,
298
- webSub: webSub, // webSubFolders.filter((folder) => folder.isDirectory()).map((folder) => folder.name),
299
- };
300
- ApiHelper.sendJson(req, res, response);
301
- return true;
302
- }
303
-
304
- async getFileList(parentPath: string, subFolders: string[]) {
305
- const subFoldersWithTime = [];
306
- for (let j = 0; j < subFolders.length; j++) {
307
- const subFolder = subFolders[j];
308
- const fileInfo = await FsUtils.fileInfo(path.join(parentPath, subFolder));
309
- subFoldersWithTime.push({
310
- name: subFolder,
311
- time: new Date(fileInfo!.mtime).toLocaleString(),
312
- size: fileInfo?.size,
313
- dir: fileInfo?.isDir,
314
- });
315
- }
316
- return subFoldersWithTime;
317
- }
318
-
319
- // called by clients
320
- async byClientCheck(req: ServerRequest, res: ServerResponse) {
321
- const jsonData = req.locals.json();
322
- const data = await this.chkData(jsonData, req, res, true);
323
- if (!data) return true;
324
-
325
- const appData = apiCache.getAppData();
326
- const folders = await FsUtils.getDirAndFiles(path.join(appData.apiPath, '..'));
327
- const apps = folders.filter((app: string) => app.endsWith('_web')).map((app: string) => app.replace('_web', ''));
328
-
329
- const foldersWithTime = [];
330
- for (let i = 0; i < folders.length; i++) {
331
- const folder = folders[i];
332
- const subFolders = await FsUtils.getDirAndFiles(path.join(appData.apiPath, '..', folder));
333
- const subFoldersWithTime = await this.getFileList(path.join(appData.apiPath, '..', folder), subFolders);
334
- const fileInfo = await FsUtils.fileInfo(path.join(appData.apiPath, '..', folder));
335
- foldersWithTime.push({
336
- name: folder,
337
- time: new Date(fileInfo!.mtime).toLocaleString(),
338
- items: subFoldersWithTime,
339
- dir: fileInfo?.isDir,
340
- });
341
- }
342
-
343
- const logFolders = await FsUtils.getDirAndFiles(path.join(appData.apiPath, '../../log'));
344
- const logFoldersWithTime = await this.getFileList(path.join(appData.apiPath, '../../log'), logFolders);
345
- const response = {
346
- status: 'ok',
347
- message: 'Remote server information called from a client.',
348
- appData: appData as any,
349
- apps,
350
- folders,
351
- foldersWithTime,
352
- logs: logFoldersWithTime,
353
- };
354
- ApiHelper.sendJson(req, res, response);
355
- return true;
356
- }
357
-
358
- async callUpdate(req: ServerRequest, res: ServerResponse) {
359
- // when remote server is slow, then local update call may be timeout.
360
- // so we set a flag to prevent multiple update calls
361
- apiStorage.set(releaseProgress, 'update started: ' + new Date().toLocaleString());
362
- let result = true;
363
- try {
364
- result = await this.update(req, res);
365
- } catch (e: any) {
366
- const response = {
367
- status: 'error',
368
- message: e.message,
369
- };
370
- ApiHelper.sendJson(req, res, response);
371
- }
372
- apiStorage.set(releaseProgress, undefined);
373
- return result;
374
- }
375
-
376
- async update(req: ServerRequest, res: ServerResponse) {
377
- const jsonData = req.locals.json();
378
- const data = await this.chkData(jsonData, req, res, false);
379
- if (!data) return true;
380
-
381
- if (!data.chkServer && !data.chkApi && !data.chkWeb && !data.chkEnv) {
382
- const response = {
383
- status: 'error',
384
- message: langHelper.getLang('shared:wrong_data'),
385
- };
386
- ApiHelper.sendJson(req, res, response);
387
- return true;
388
- }
389
-
390
- const appData = apiCache.getAppData();
391
- let targetUrl = data.targetUrl as string;
392
- if (targetUrl.endsWith('/')) {
393
- targetUrl = targetUrl.slice(0, -1);
394
- }
395
- if (data.chkEnv) {
396
- const result = await this.updateSendFile(data, '.env');
397
- if (!result || result.status !== 'ok') {
398
- ApiHelper.sendJson(req, res, result);
399
- return true;
400
- }
401
- const result2 = await this.updateSendFile(data, '.env.development');
402
- if (!result2 || result2.status !== 'ok') {
403
- ApiHelper.sendJson(req, res, result2);
404
- return true;
405
- }
406
- const result3 = await this.updateSendFile(data, '.env.production');
407
- if (!result3 || result3.status !== 'ok') {
408
- ApiHelper.sendJson(req, res, result3);
409
- return true;
410
- }
411
- }
412
- if (data.chkWeb) {
413
- const result = await this.updateSendFile(data, 'web');
414
- if (!result || result.status !== 'ok') {
415
- ApiHelper.sendJson(req, res, result);
416
- return true;
417
- }
418
- // if (data.webSub) {
419
- // const result2 = await this.updateSendFile(data, 'web-sub');
420
- // if (!result2 || result2.status !== 'ok') {
421
- // ApiHelper.sendJson(req, res, result2);
422
- // return true;
423
- // }
424
- // }
425
-
426
- if (data.webSubs && data.webSubs.length > 0) {
427
- const subTop = path.join(appData.apiPath, '..', data.fromList + '_web/');
428
- for (let i = 0; i < data.webSubs.length; i++) {
429
- if (!data.webSubs[i].startsWith(data.fromList + '_web/')) {
430
- const response = {
431
- status: 'error',
432
- message: `Error: ${data.webSubs[i]} is not under ${data.fromList}`,
433
- };
434
- ApiHelper.sendJson(req, res, response);
435
- return true;
436
- }
437
- const subFolders = await FsUtils.getDirsFullpath(path.join(appData.apiPath, '..', data.webSubs[i]));
438
- const subFiles = subFolders
439
- .filter((e) => e.isFile())
440
- .map((e) => path.join(e.parentPath.substring(subTop.length), e.name).replace(/\\/g, '/'))
441
- .sort();
442
- for (let j = 0; j < subFiles.length; j++) {
443
- if (subFiles[j].endsWith('.js.map')) {
444
- continue;
445
- }
446
- data.webSub = subFiles[j];
447
- this.logger.info(`update, webSubs: ${data.webSubs[i]}, subFiles: ${subFiles[j]})`);
448
- const result2 = await this.updateSendFile(data, 'web-sub');
449
- if (!result2 || result2.status !== 'ok') {
450
- ApiHelper.sendJson(req, res, result2);
451
- return true;
452
- }
453
- }
454
- }
455
- }
456
- }
457
- if (data.chkApi) {
458
- const result = await this.updateSendFile(data, 'api');
459
- if (!result || result.status !== 'ok') {
460
- ApiHelper.sendJson(req, res, result);
461
- return true;
462
- }
463
- }
464
- // update server at the last
465
- if (data.chkServer) {
466
- const result = await this.updateSendFile(data, 'server');
467
- if (!result || result.status !== 'ok') {
468
- ApiHelper.sendJson(req, res, result);
469
- return true;
470
- }
471
- const result2 = await this.updateSendFile(data, 'app-loader');
472
- if (!result2 || result2.status !== 'ok') {
473
- ApiHelper.sendJson(req, res, result2);
474
- return true;
475
- }
476
- }
477
-
478
- const response = {
479
- status: 'ok',
480
- message: 'updated',
481
- };
482
- ApiHelper.sendJson(req, res, response);
483
- this.logger.info(`updated, successful`);
484
- return true;
485
- }
486
-
487
- async updateSendFile(data: any, chkOption: string) {
488
- let targetUrl = data.targetUrl;
489
- if (targetUrl.endsWith('/')) {
490
- targetUrl = targetUrl.slice(0, -1);
491
- }
492
- const fromList = data.fromList;
493
- const appData = apiCache.getAppData();
494
- let sendFile = '';
495
- if (chkOption === 'server') {
496
- sendFile = path.join(appData.apiPath, '..', 'server', 'index.js');
497
- } else if (chkOption === 'app-loader') {
498
- sendFile = path.join(appData.apiPath, '..', 'server', 'app-loader.js');
499
- } else if (chkOption === 'api') {
500
- sendFile = path.join(appData.apiPath, '..', fromList + '_api', 'index.js');
501
- } else if (chkOption === 'web') {
502
- sendFile = path.join(appData.apiPath, '..', fromList + '_web', 'index.js');
503
- } else if (chkOption === 'web-sub' && data.webSub) {
504
- // sendFile = path.join(appData.apiPath, '..', fromList + '_web', data.webSub, 'index.js');
505
- sendFile = path.join(appData.apiPath, '..', fromList + '_web', data.webSub);
506
- } else if (chkOption.startsWith('.env')) {
507
- sendFile = path.join(appData.apiPath, '../../..', chkOption);
508
- }
509
- if (!(await FsUtils.pathExist(sendFile))) {
510
- this.logger.error(`updateSendFile, not found: ${sendFile}`);
511
- return { status: 'error', message: 'Client file not found: ' + sendFile };
512
- }
513
- apiStorage.set(releaseProgress, 'updateSendFile: ' + sendFile);
514
- const fileContent = (await FsUtils.readFile(sendFile))!;
515
- // const compressedContent = await new Promise<Buffer>((resolve, reject) => {
516
- // zlib.gzip(fileContent, (err, buffer) => {
517
- // if (err) {
518
- // reject(err);
519
- // } else {
520
- // resolve(buffer);
521
- // }
522
- // });
523
- // })
524
- const chunkSize = 1024 * 500;
525
- let cnt = 0;
526
- this.logger.info(`updateSendFile, sendFile: ${sendFile}, len: ${fileContent.length}`);
527
- for (let i = 0; i < fileContent.length; i += chunkSize) {
528
- const chunk = fileContent.slice(i, i + chunkSize);
529
- if (!chunk) break;
530
-
531
- const postData = {
532
- method: 'POST',
533
- body: JSON.stringify({ ...data, chkOption, index: cnt, size: fileContent.length }) + '\n\n' + chunk,
534
- };
535
- this.logger.info(
536
- `updateSendFile, index: ${cnt}, sending: ${chunk.length} (${i + chunk.length} / ${fileContent.length
537
- }), f: ${sendFile}`
538
- );
539
- apiStorage.set(
540
- releaseProgress,
541
- `updateSendFile, index: ${cnt}, sending: ${chunk.length} (${i + chunk.length} / ${fileContent.length
542
- }), f: ${sendFile}`
543
- );
544
- i > 0 && (await new Promise((resolve) => setTimeout(resolve, 1000)));
545
- const remoteData = await fetch(targetUrl + '/api/admin/release/byClientUpdate', postData);
546
- const resultText = await remoteData.text();
547
- this.logger.info(`updateSendFile, index: ${cnt}, resultText: ${resultText}`);
548
- let remoteResult: any;
549
- try {
550
- remoteResult = JSON.parse(resultText);
551
- } catch (e: any) {
552
- remoteResult = { status: 'error', message: resultText };
553
- }
554
- if (!remoteResult || remoteResult.status !== 'ok') {
555
- return remoteResult;
556
- }
557
- cnt++;
558
- }
559
-
560
- const remoteResult = { status: 'ok', message: 'updated' };
561
- return remoteResult;
562
- }
563
-
564
- // called by clients
565
- async byClientUpdate(req: ServerRequest, res: ServerResponse) {
566
- const body = req.locals.body as Buffer;
567
- let jsonData = {};
568
- let fileContent = null;
569
- try {
570
- const index = body.indexOf('\n\n');
571
- if (index !== -1) {
572
- jsonData = JSON.parse(body.subarray(0, index).toString());
573
- fileContent = body.subarray(index + 2);
574
- }
575
- const data = await this.chkData(jsonData, req, res, true);
576
- if (!data) return true;
577
-
578
- const toList = data.toList as string;
579
- const chkOption = data.chkOption as string;
580
- if (
581
- !chkOption ||
582
- !toList ||
583
- (chkOption !== 'server' &&
584
- chkOption !== 'app-loader' &&
585
- chkOption !== 'api' &&
586
- chkOption !== 'web' &&
587
- chkOption !== 'web-sub' &&
588
- !chkOption.startsWith('.env'))
589
- ) {
590
- const response = {
591
- status: 'error',
592
- message: 'Wrong data.',
593
- };
594
- ApiHelper.sendJson(req, res, response);
595
- return true;
596
- }
597
-
598
- const appData = apiCache.getAppData();
599
- let saveFile = '';
600
- if (chkOption === 'server') {
601
- saveFile = path.join(appData.apiPath, '..', 'server', 'index.js');
602
- } else if (chkOption === 'app-loader') {
603
- saveFile = path.join(appData.apiPath, '..', 'server', 'app-loader.js');
604
- } else if (chkOption === 'api') {
605
- saveFile = path.join(appData.apiPath, '..', toList + '_api', 'index.js');
606
- } else if (chkOption === 'web') {
607
- saveFile = path.join(appData.apiPath, '..', toList + '_web', 'index.js');
608
- } else if (chkOption === 'web-sub' && data.webSub) {
609
- const folder = path.join(appData.apiPath, '..', toList + '_web', path.basename(data.webSub));
610
- if (!(await FsUtils.pathExist(folder))) {
611
- await FsUtils.mkdir(folder);
612
- }
613
- saveFile = path.join(appData.apiPath, '..', toList + '_web', data.webSub);
614
- } else if ((chkOption as string).startsWith('.env')) {
615
- saveFile = path.join(appData.apiPath, '../../..', chkOption);
616
- }
617
- if (chkOption !== 'web-sub' && !(await FsUtils.pathExist(saveFile))) {
618
- const response = {
619
- status: 'error',
620
- message: 'Server file not found: ' + saveFile,
621
- };
622
- ApiHelper.sendJson(req, res, response);
623
- return true;
624
- }
625
- if (data.chkBackup && data.index === 0) {
626
- const bakContent = await FsUtils.readFile(saveFile);
627
- if (bakContent) {
628
- const bakFile = saveFile + '.bak-' + new Date().toISOString().replace(/:/g, '-');
629
- await FsUtils.writeFile(bakFile, bakContent);
630
- }
631
- }
632
-
633
- this.logger.info(
634
- `byClientUpdate, index: ${data.index}, saveFile: ${saveFile}, received len: ${(fileContent || '').length}`
635
- );
636
- if (data.index === 0) {
637
- await FsUtils.writeFile(saveFile, fileContent || '');
638
- } else {
639
- await FsUtils.appendFile(saveFile, fileContent || '');
640
- }
641
-
642
- const response = {
643
- status: 'ok',
644
- message: 'Remote server updated by a client.',
645
- };
646
- ApiHelper.sendJson(req, res, response);
647
- } catch (e: any) {
648
- console.log('byClientUpdate failed', e);
649
- const response = {
650
- status: 'error',
651
- message: 'byClientUpdate failed',
652
- };
653
- ApiHelper.sendJson(req, res, response);
654
- }
655
- return true;
656
- }
657
-
658
- async byClientRefreshCache(req: ServerRequest, res: ServerResponse) {
659
- const jsonData = req.locals.json();
660
- const data = await this.chkData(jsonData, req, res, true);
661
- if (!data) return true;
662
-
663
- await processRefreshCache(req);
664
- const response = {
665
- status: 'ok',
666
- message: 'Cache refreshed successfully.',
667
- };
668
- ApiHelper.sendJson(req, res, response);
669
- return true;
670
- }
671
-
672
- async byClientRestartApp(req: ServerRequest, res: ServerResponse) {
673
- const jsonData = req.locals.json();
674
- const data = await this.chkData(jsonData, req, res, true);
675
- if (!data) return true;
676
-
677
- await processRestartApp(req);
678
- const response = {
679
- status: 'ok',
680
- message: 'Restart app successfully.',
681
- };
682
- ApiHelper.sendJson(req, res, response);
683
- return true;
684
- }
685
-
686
- async byClientShell(req: ServerRequest, res: ServerResponse) {
687
- const jsonData = req.locals.json();
688
- const data = await this.chkData(jsonData, req, res, true);
689
- if (!data) return true;
690
-
691
- const result = await processShell(req);
692
- const response = {
693
- status: 'ok',
694
- message: 'Shell executed successfully.',
695
- result,
696
- };
697
- ApiHelper.sendJson(req, res, response);
698
- return true;
699
- }
700
- }
1
+ import { ServerResponse } from 'http';
2
+ import {
3
+ IApiBase,
4
+ Logger,
5
+ apiCache,
6
+ ServerRequest,
7
+ ApiRouter,
8
+ ApiHelper,
9
+ langHelper,
10
+ FsUtils,
11
+ adminApiHelper,
12
+ processRefreshCache,
13
+ apiStorage,
14
+ processRestartApp,
15
+ processShell,
16
+ } from 'lupine.api';
17
+ import path from 'path';
18
+ import { needDevAdminSession } from './admin-auth';
19
+ import { adminTokenHelper } from './admin-token-helper';
20
+
21
+ const releaseProgress = 'admin-release-progress';
22
+ export class AdminRelease implements IApiBase {
23
+ private logger = new Logger('release-api');
24
+ protected router = new ApiRouter();
25
+
26
+ constructor() {
27
+ this.mountDashboard();
28
+ }
29
+
30
+ public getRouter(): ApiRouter {
31
+ return this.router;
32
+ }
33
+
34
+ protected mountDashboard() {
35
+ // called by FE
36
+ this.router.use('/check', needDevAdminSession, this.check.bind(this));
37
+ this.router.use('/update', needDevAdminSession, this.callUpdate.bind(this));
38
+ this.router.use('/view-log', needDevAdminSession, this.viewLog.bind(this));
39
+ // called online or by clients
40
+ this.router.use('/refresh-cache', needDevAdminSession, this.refreshCache.bind(this));
41
+ this.router.use('/restart-app', needDevAdminSession, this.restartApp.bind(this));
42
+
43
+ this.router.use('/shell', needDevAdminSession, this.shell.bind(this));
44
+
45
+ // ...ByClient will verify credentials from post, so it doesn't need AdminSession
46
+ this.router.use('/byClientCheck', this.byClientCheck.bind(this));
47
+ this.router.use('/byClientUpdate', this.byClientUpdate.bind(this));
48
+ this.router.use('/byClientRefreshCache', this.byClientRefreshCache.bind(this));
49
+ this.router.use('/byClientRestartApp', this.byClientRestartApp.bind(this));
50
+ this.router.use('/byClientViewLog', this.byClientViewLog.bind(this));
51
+
52
+ this.router.use('/byClientShell', this.byClientShell.bind(this));
53
+ }
54
+
55
+ async viewLog(req: ServerRequest, res: ServerResponse) {
56
+ const jsonData = req.locals.json();
57
+ const data = await this.chkData(jsonData, req, res, true);
58
+ if (!data) return true;
59
+
60
+ let targetUrl = data.targetUrl as string;
61
+ if (targetUrl.endsWith('/')) {
62
+ targetUrl = targetUrl.slice(0, -1);
63
+ }
64
+ const remoteData = await fetch(targetUrl + '/api/admin/release/byClientViewLog', {
65
+ method: 'POST',
66
+ body: JSON.stringify(data),
67
+ });
68
+ // (remoteData.body as any).pipe(res);
69
+ const data2 = await remoteData.text();
70
+ // res.setHeader('Content-Disposition', 'attachment; filename="log.txt"');
71
+ res.writeHead(200, { 'Content-Type': 'application/octet-stream' });
72
+ res.write(data2);
73
+ res.end();
74
+ return true;
75
+ }
76
+
77
+ async byClientViewLog(req: ServerRequest, res: ServerResponse) {
78
+ const jsonData = req.locals.json();
79
+ const data = await this.chkData(jsonData, req, res, true);
80
+ if (!data) return true;
81
+
82
+ const appData = apiCache.getAppData();
83
+ const logFile = path.join(appData.apiPath, '../../log', data.logName);
84
+ if (!(await FsUtils.pathExist(logFile))) {
85
+ const response = {
86
+ status: 'error',
87
+ message: 'Log file not found.',
88
+ };
89
+ ApiHelper.sendJson(req, res, response);
90
+ return true;
91
+ }
92
+ ApiHelper.sendFile(req, res, logFile);
93
+ return true;
94
+ }
95
+
96
+ async refreshCache(req: ServerRequest, res: ServerResponse) {
97
+ // check whether it's from online admin
98
+ const json = await adminApiHelper.getDevAdminFromCookie(req, res, false);
99
+ const jsonData = req.locals.json();
100
+ if (json && jsonData && !Array.isArray(jsonData) && jsonData.isLocal) {
101
+ await processRefreshCache(req);
102
+ const response = {
103
+ status: 'ok',
104
+ message: 'Cache refreshed successfully.',
105
+ };
106
+ ApiHelper.sendJson(req, res, response);
107
+ return true;
108
+ }
109
+
110
+ const data = await this.chkData(jsonData, req, res, true);
111
+ if (!data) return true;
112
+
113
+ let targetUrl = data.targetUrl as string;
114
+ if (targetUrl.endsWith('/')) {
115
+ targetUrl = targetUrl.slice(0, -1);
116
+ }
117
+ const remoteData = await fetch(targetUrl + '/api/admin/release/byClientRefreshCache', {
118
+ method: 'POST',
119
+ body: JSON.stringify(data),
120
+ });
121
+ const resultText = await remoteData.text();
122
+ let remoteResult: any;
123
+ try {
124
+ remoteResult = JSON.parse(resultText);
125
+ } catch (e: any) {
126
+ remoteResult = { status: 'error', message: resultText };
127
+ }
128
+ const response = {
129
+ status: 'ok',
130
+ message: 'check.',
131
+ ...remoteResult,
132
+ };
133
+ ApiHelper.sendJson(req, res, response);
134
+ return true;
135
+ }
136
+
137
+ async restartApp(req: ServerRequest, res: ServerResponse) {
138
+ // check whether it's from online admin
139
+ const json = await adminApiHelper.getDevAdminFromCookie(req, res, false);
140
+ const jsonData = req.locals.json();
141
+ if (json && jsonData && !Array.isArray(jsonData) && jsonData.isLocal) {
142
+ await processRestartApp();
143
+ const response = {
144
+ status: 'ok',
145
+ message: 'Restart app successfully.',
146
+ };
147
+ ApiHelper.sendJson(req, res, response);
148
+ return true;
149
+ }
150
+
151
+ const data = await this.chkData(jsonData, req, res, true);
152
+ if (!data) return true;
153
+
154
+ let targetUrl = data.targetUrl as string;
155
+ if (targetUrl.endsWith('/')) {
156
+ targetUrl = targetUrl.slice(0, -1);
157
+ }
158
+ const remoteData = await fetch(targetUrl + '/api/admin/release/byClientRestartApp', {
159
+ method: 'POST',
160
+ body: JSON.stringify(data),
161
+ });
162
+ const resultText = await remoteData.text();
163
+ let remoteResult: any;
164
+ try {
165
+ remoteResult = JSON.parse(resultText);
166
+ } catch (e: any) {
167
+ remoteResult = { status: 'error', message: resultText };
168
+ }
169
+ const response = {
170
+ status: 'ok',
171
+ message: 'check.',
172
+ ...remoteResult,
173
+ };
174
+ ApiHelper.sendJson(req, res, response);
175
+ return true;
176
+ }
177
+
178
+ async shell(req: ServerRequest, res: ServerResponse) {
179
+ // check whether it's from online admin
180
+ const json = await adminApiHelper.getDevAdminFromCookie(req, res, false);
181
+ const jsonData = req.locals.json();
182
+ if (json && jsonData && !Array.isArray(jsonData) && jsonData.isLocal) {
183
+ const result = await processShell(req);
184
+ const response = {
185
+ status: 'ok',
186
+ message: 'Shell executed successfully.',
187
+ result,
188
+ };
189
+ ApiHelper.sendJson(req, res, response);
190
+ return true;
191
+ }
192
+
193
+ const data = await this.chkData(jsonData, req, res, true);
194
+ if (!data) return true;
195
+
196
+ let targetUrl = data.targetUrl as string;
197
+ if (targetUrl.endsWith('/')) {
198
+ targetUrl = targetUrl.slice(0, -1);
199
+ }
200
+ const remoteData = await fetch(targetUrl + '/api/admin/release/byClientShell', {
201
+ method: 'POST',
202
+ body: JSON.stringify(data),
203
+ });
204
+ const resultText = await remoteData.text();
205
+ let remoteResult: any;
206
+ try {
207
+ remoteResult = JSON.parse(resultText);
208
+ } catch (e: any) {
209
+ remoteResult = { status: 'error', message: resultText };
210
+ }
211
+ const response = {
212
+ status: 'ok',
213
+ message: 'check.',
214
+ ...remoteResult,
215
+ };
216
+ ApiHelper.sendJson(req, res, response);
217
+ return true;
218
+ }
219
+
220
+ public async chkData(data: any, req: ServerRequest, res: ServerResponse, chkCredential: boolean) {
221
+ // add access token
222
+ if (!data || Array.isArray(data) || typeof data !== 'object' || !data.accessToken || !data.targetUrl) {
223
+ const response = {
224
+ status: 'error',
225
+ message: 'Wrong data [missing parameters].', //langHelper.getLang('shared:wrong_data'),
226
+ };
227
+ ApiHelper.sendJson(req, res, response);
228
+ return false;
229
+ }
230
+ if (chkCredential) {
231
+ if (await adminTokenHelper.validateToken(data.accessToken)) {
232
+ return data;
233
+ } else if (
234
+ process.env['DEV_ADMIN_PASS'] !== '' &&
235
+ (data.accessToken === `${process.env['DEV_ADMIN_USER']}@${process.env['DEV_ADMIN_PASS']}` ||
236
+ data.accessToken === `${process.env['DEV_ADMIN_USER']}:${process.env['DEV_ADMIN_PASS']}`)
237
+ ) {
238
+ return data;
239
+ } else {
240
+ const response = {
241
+ status: 'error',
242
+ message: 'Wrong data [wrong token].', //langHelper.getLang('shared:wrong_data'),
243
+ };
244
+ ApiHelper.sendJson(req, res, response);
245
+ return false;
246
+ }
247
+ }
248
+ return data;
249
+ }
250
+
251
+ // this is called by the FE, then call byClientCheck to get remote server's information
252
+ async check(req: ServerRequest, res: ServerResponse) {
253
+ const jsonData = req.locals.json();
254
+ const data = await this.chkData(jsonData, req, res, false);
255
+ if (!data) return true;
256
+
257
+ // From app list is from local
258
+ const appData = apiCache.getAppData();
259
+ const folders = await FsUtils.getDirAndFiles(path.join(appData.apiPath, '..'));
260
+ const apps = folders.filter((app: string) => app.endsWith('_web')).map((app: string) => app.replace('_web', ''));
261
+
262
+ let targetUrl = data.targetUrl as string;
263
+ if (targetUrl.endsWith('/')) {
264
+ targetUrl = targetUrl.slice(0, -1);
265
+ }
266
+ const remoteData = await fetch(targetUrl + '/api/admin/release/byClientCheck', {
267
+ method: 'POST',
268
+ body: JSON.stringify(data),
269
+ });
270
+ const resultText = await remoteData.text();
271
+ let remoteResult: any;
272
+ try {
273
+ remoteResult = JSON.parse(resultText);
274
+ } catch (e: any) {
275
+ remoteResult = { status: 'error', message: resultText };
276
+ }
277
+
278
+ // local dirs under _web
279
+ const webSub: string[] = [];
280
+ for (let j = 0; j < apps.length; j++) {
281
+ const app = apps[j];
282
+ const appRoot = path.join(appData.apiPath, '..');
283
+ const subFolders = await FsUtils.getDirentFullpath(path.join(appRoot, app + '_web'), 5);
284
+ webSub.push(app + '_web/');
285
+ webSub.push(
286
+ ...subFolders
287
+ .filter((i) => i.isDirectory())
288
+ .map((i) => path.join(i.parentPath.substring(appRoot.length + 1), i.name).replace(/\\/g, '/'))
289
+ );
290
+ }
291
+ // const webSub = webSubFolders.filter(i => i.isDirectory()).map(i => path.join(i.parentPath.substring(appData.webPath.length + 1), i.name).replace(/\\/g, '/')).sort();
292
+
293
+ const response = {
294
+ releaseProgress: await apiStorage.get(releaseProgress),
295
+ status: 'ok',
296
+ message: 'check.',
297
+ appsFrom: apps,
298
+ ...remoteResult,
299
+ webSub: webSub, // webSubFolders.filter((folder) => folder.isDirectory()).map((folder) => folder.name),
300
+ };
301
+ ApiHelper.sendJson(req, res, response);
302
+ return true;
303
+ }
304
+
305
+ async getFileList(parentPath: string, subFolders: string[]) {
306
+ const subFoldersWithTime = [];
307
+ for (let j = 0; j < subFolders.length; j++) {
308
+ const subFolder = subFolders[j];
309
+ const fileInfo = await FsUtils.fileInfo(path.join(parentPath, subFolder));
310
+ subFoldersWithTime.push({
311
+ name: subFolder,
312
+ time: new Date(fileInfo!.mtime).toLocaleString(),
313
+ size: fileInfo?.size,
314
+ dir: fileInfo?.isDir,
315
+ });
316
+ }
317
+ return subFoldersWithTime;
318
+ }
319
+
320
+ // called by clients
321
+ async byClientCheck(req: ServerRequest, res: ServerResponse) {
322
+ const jsonData = req.locals.json();
323
+ const data = await this.chkData(jsonData, req, res, true);
324
+ if (!data) return true;
325
+
326
+ const appData = apiCache.getAppData();
327
+ const folders = await FsUtils.getDirAndFiles(path.join(appData.apiPath, '..'));
328
+ const apps = folders.filter((app: string) => app.endsWith('_web')).map((app: string) => app.replace('_web', ''));
329
+
330
+ const foldersWithTime = [];
331
+ for (let i = 0; i < folders.length; i++) {
332
+ const folder = folders[i];
333
+ const subFolders = await FsUtils.getDirAndFiles(path.join(appData.apiPath, '..', folder));
334
+ const subFoldersWithTime = await this.getFileList(path.join(appData.apiPath, '..', folder), subFolders);
335
+ const fileInfo = await FsUtils.fileInfo(path.join(appData.apiPath, '..', folder));
336
+ foldersWithTime.push({
337
+ name: folder,
338
+ time: new Date(fileInfo!.mtime).toLocaleString(),
339
+ items: subFoldersWithTime,
340
+ dir: fileInfo?.isDir,
341
+ });
342
+ }
343
+
344
+ const logFolders = await FsUtils.getDirAndFiles(path.join(appData.apiPath, '../../log'));
345
+ const logFoldersWithTime = await this.getFileList(path.join(appData.apiPath, '../../log'), logFolders);
346
+ const response = {
347
+ status: 'ok',
348
+ message: 'Remote server information called from a client.',
349
+ appData: appData as any,
350
+ apps,
351
+ folders,
352
+ foldersWithTime,
353
+ logs: logFoldersWithTime,
354
+ };
355
+ ApiHelper.sendJson(req, res, response);
356
+ return true;
357
+ }
358
+
359
+ async callUpdate(req: ServerRequest, res: ServerResponse) {
360
+ // when remote server is slow, then local update call may be timeout.
361
+ // so we set a flag to prevent multiple update calls
362
+ apiStorage.set(releaseProgress, 'update started: ' + new Date().toLocaleString());
363
+ let result = true;
364
+ try {
365
+ result = await this.update(req, res);
366
+ } catch (e: any) {
367
+ const response = {
368
+ status: 'error',
369
+ message: e.message,
370
+ };
371
+ ApiHelper.sendJson(req, res, response);
372
+ }
373
+ apiStorage.set(releaseProgress, undefined);
374
+ return result;
375
+ }
376
+
377
+ async update(req: ServerRequest, res: ServerResponse) {
378
+ const jsonData = req.locals.json();
379
+ const data = await this.chkData(jsonData, req, res, false);
380
+ if (!data) return true;
381
+
382
+ if (!data.chkServer && !data.chkApi && !data.chkWeb && !data.chkEnv) {
383
+ const response = {
384
+ status: 'error',
385
+ message: langHelper.getLang('shared:wrong_data'),
386
+ };
387
+ ApiHelper.sendJson(req, res, response);
388
+ return true;
389
+ }
390
+
391
+ const appData = apiCache.getAppData();
392
+ let targetUrl = data.targetUrl as string;
393
+ if (targetUrl.endsWith('/')) {
394
+ targetUrl = targetUrl.slice(0, -1);
395
+ }
396
+ if (data.chkEnv) {
397
+ const result = await this.updateSendFile(data, '.env');
398
+ if (!result || result.status !== 'ok') {
399
+ ApiHelper.sendJson(req, res, result);
400
+ return true;
401
+ }
402
+ const result2 = await this.updateSendFile(data, '.env.development');
403
+ if (!result2 || result2.status !== 'ok') {
404
+ ApiHelper.sendJson(req, res, result2);
405
+ return true;
406
+ }
407
+ const result3 = await this.updateSendFile(data, '.env.production');
408
+ if (!result3 || result3.status !== 'ok') {
409
+ ApiHelper.sendJson(req, res, result3);
410
+ return true;
411
+ }
412
+ }
413
+ // if (data.chkWeb) {
414
+ // const result = await this.updateSendFile(data, 'web');
415
+ // if (!result || result.status !== 'ok') {
416
+ // ApiHelper.sendJson(req, res, result);
417
+ // return true;
418
+ // }
419
+ // if (data.webSub) {
420
+ // const result2 = await this.updateSendFile(data, 'web-sub');
421
+ // if (!result2 || result2.status !== 'ok') {
422
+ // ApiHelper.sendJson(req, res, result2);
423
+ // return true;
424
+ // }
425
+ // }
426
+
427
+ if (data.webSubs && data.webSubs.length > 0) {
428
+ const subTop = path.join(appData.apiPath, '..', data.fromList + '_web/');
429
+ for (let i = 0; i < data.webSubs.length; i++) {
430
+ if (!data.webSubs[i].startsWith(data.fromList + '_web/')) {
431
+ const response = {
432
+ status: 'error',
433
+ message: `Error: ${data.webSubs[i]} is not under ${data.fromList}`,
434
+ };
435
+ ApiHelper.sendJson(req, res, response);
436
+ return true;
437
+ }
438
+ const subFolders = await FsUtils.getDirentFullpath(path.join(appData.apiPath, '..', data.webSubs[i]));
439
+ const subFiles = subFolders
440
+ .filter((e) => e.isFile())
441
+ .map((e) => path.join(e.parentPath.substring(subTop.length), e.name).replace(/\\/g, '/'))
442
+ .sort();
443
+ for (let j = 0; j < subFiles.length; j++) {
444
+ if (subFiles[j].endsWith('.js.map') || subFiles[j].endsWith('.css.map')) {
445
+ continue;
446
+ }
447
+ data.webSub = subFiles[j];
448
+ this.logger.info(`update, webSubs: ${data.webSubs[i]}, subFiles: ${subFiles[j]})`);
449
+ const result2 = await this.updateSendFile(data, 'web-sub');
450
+ if (!result2 || result2.status !== 'ok') {
451
+ ApiHelper.sendJson(req, res, result2);
452
+ return true;
453
+ }
454
+ }
455
+ }
456
+ }
457
+ // }
458
+ if (data.chkApi) {
459
+ const result = await this.updateSendFile(data, 'api');
460
+ if (!result || result.status !== 'ok') {
461
+ ApiHelper.sendJson(req, res, result);
462
+ return true;
463
+ }
464
+ }
465
+ // update server at the last
466
+ if (data.chkServer) {
467
+ const result = await this.updateSendFile(data, 'server');
468
+ if (!result || result.status !== 'ok') {
469
+ ApiHelper.sendJson(req, res, result);
470
+ return true;
471
+ }
472
+ const result2 = await this.updateSendFile(data, 'app-loader');
473
+ if (!result2 || result2.status !== 'ok') {
474
+ ApiHelper.sendJson(req, res, result2);
475
+ return true;
476
+ }
477
+ }
478
+
479
+ const response = {
480
+ status: 'ok',
481
+ message: 'updated',
482
+ };
483
+ ApiHelper.sendJson(req, res, response);
484
+ this.logger.info(`updated, successful`);
485
+ return true;
486
+ }
487
+
488
+ async updateSendFile(data: any, chkOption: string) {
489
+ let targetUrl = data.targetUrl;
490
+ if (targetUrl.endsWith('/')) {
491
+ targetUrl = targetUrl.slice(0, -1);
492
+ }
493
+ const fromList = data.fromList;
494
+ const appData = apiCache.getAppData();
495
+ let sendFile = '';
496
+ if (chkOption === 'server') {
497
+ sendFile = path.join(appData.apiPath, '..', 'server', 'index.js');
498
+ } else if (chkOption === 'app-loader') {
499
+ sendFile = path.join(appData.apiPath, '..', 'server', 'app-loader.js');
500
+ } else if (chkOption === 'api') {
501
+ sendFile = path.join(appData.apiPath, '..', fromList + '_api', 'index.js');
502
+ // } else if (chkOption === 'web') {
503
+ // sendFile = path.join(appData.apiPath, '..', fromList + '_web', 'index.js');
504
+ } else if (chkOption === 'web-sub' && data.webSub) {
505
+ // sendFile = path.join(appData.apiPath, '..', fromList + '_web', data.webSub, 'index.js');
506
+ sendFile = path.join(appData.apiPath, '..', fromList + '_web', data.webSub);
507
+ } else if (chkOption.startsWith('.env')) {
508
+ sendFile = path.join(appData.apiPath, '../../..', chkOption);
509
+ }
510
+ if (!(await FsUtils.pathExist(sendFile))) {
511
+ this.logger.error(`updateSendFile, not found: ${sendFile}`);
512
+ return { status: 'error', message: 'Client file not found: ' + sendFile };
513
+ }
514
+ apiStorage.set(releaseProgress, 'updateSendFile: ' + sendFile);
515
+ const fileContent = (await FsUtils.readFile(sendFile))!;
516
+ // const compressedContent = await new Promise<Buffer>((resolve, reject) => {
517
+ // zlib.gzip(fileContent, (err, buffer) => {
518
+ // if (err) {
519
+ // reject(err);
520
+ // } else {
521
+ // resolve(buffer);
522
+ // }
523
+ // });
524
+ // })
525
+ const chunkSize = 1024 * 500;
526
+ let cnt = 0;
527
+ this.logger.info(`updateSendFile, sendFile: ${sendFile}, len: ${fileContent.length}`);
528
+ for (let i = 0; i < fileContent.length; i += chunkSize) {
529
+ const chunk = fileContent.slice(i, i + chunkSize);
530
+ if (!chunk) break;
531
+
532
+ const postData = {
533
+ method: 'POST',
534
+ body: JSON.stringify({ ...data, chkOption, index: cnt, size: fileContent.length }) + '\n\n' + chunk,
535
+ };
536
+ this.logger.info(
537
+ `updateSendFile, index: ${cnt}, sending: ${chunk.length} (${i + chunk.length} / ${
538
+ fileContent.length
539
+ }), f: ${sendFile}`
540
+ );
541
+ apiStorage.set(
542
+ releaseProgress,
543
+ `updateSendFile, index: ${cnt}, sending: ${chunk.length} (${i + chunk.length} / ${
544
+ fileContent.length
545
+ }), f: ${sendFile}`
546
+ );
547
+ i > 0 && (await new Promise((resolve) => setTimeout(resolve, 1000)));
548
+ const remoteData = await fetch(targetUrl + '/api/admin/release/byClientUpdate', postData);
549
+ const resultText = await remoteData.text();
550
+ this.logger.info(`updateSendFile, index: ${cnt}, resultText: ${resultText}`);
551
+ let remoteResult: any;
552
+ try {
553
+ remoteResult = JSON.parse(resultText);
554
+ } catch (e: any) {
555
+ remoteResult = { status: 'error', message: resultText };
556
+ }
557
+ if (!remoteResult || remoteResult.status !== 'ok') {
558
+ return remoteResult;
559
+ }
560
+ cnt++;
561
+ }
562
+
563
+ const remoteResult = { status: 'ok', message: 'updated' };
564
+ return remoteResult;
565
+ }
566
+
567
+ // called by clients
568
+ async byClientUpdate(req: ServerRequest, res: ServerResponse) {
569
+ const body = req.locals.body as Buffer;
570
+ let jsonData = {};
571
+ let fileContent = null;
572
+ try {
573
+ const index = body.indexOf('\n\n');
574
+ if (index !== -1) {
575
+ jsonData = JSON.parse(body.subarray(0, index).toString());
576
+ fileContent = body.subarray(index + 2);
577
+ }
578
+ const data = await this.chkData(jsonData, req, res, true);
579
+ if (!data) return true;
580
+
581
+ const toList = data.toList as string;
582
+ const chkOption = data.chkOption as string;
583
+ if (
584
+ !chkOption ||
585
+ !toList ||
586
+ (chkOption !== 'server' &&
587
+ chkOption !== 'app-loader' &&
588
+ chkOption !== 'api' &&
589
+ // chkOption !== 'web' &&
590
+ chkOption !== 'web-sub' &&
591
+ !chkOption.startsWith('.env'))
592
+ ) {
593
+ const response = {
594
+ status: 'error',
595
+ message: 'Wrong data.',
596
+ };
597
+ ApiHelper.sendJson(req, res, response);
598
+ return true;
599
+ }
600
+
601
+ const appData = apiCache.getAppData();
602
+ let saveFile = '';
603
+ if (chkOption === 'server') {
604
+ saveFile = path.join(appData.apiPath, '..', 'server', 'index.js');
605
+ } else if (chkOption === 'app-loader') {
606
+ saveFile = path.join(appData.apiPath, '..', 'server', 'app-loader.js');
607
+ } else if (chkOption === 'api') {
608
+ saveFile = path.join(appData.apiPath, '..', toList + '_api', 'index.js');
609
+ // } else if (chkOption === 'web') {
610
+ // saveFile = path.join(appData.apiPath, '..', toList + '_web', 'index.js');
611
+ } else if (chkOption === 'web-sub' && data.webSub) {
612
+ const baseName = path.basename(data.webSub);
613
+ if (baseName !== data.webSub) {
614
+ const folder = path.join(appData.apiPath, '..', toList + '_web', baseName);
615
+ if (!(await FsUtils.pathExist(folder))) {
616
+ await FsUtils.mkdir(folder);
617
+ }
618
+ }
619
+ saveFile = path.join(appData.apiPath, '..', toList + '_web', data.webSub);
620
+ } else if ((chkOption as string).startsWith('.env')) {
621
+ saveFile = path.join(appData.apiPath, '../../..', chkOption);
622
+ }
623
+ if (chkOption !== 'web-sub' && !(await FsUtils.pathExist(saveFile))) {
624
+ const response = {
625
+ status: 'error',
626
+ message: 'Server file not found: ' + saveFile,
627
+ };
628
+ ApiHelper.sendJson(req, res, response);
629
+ return true;
630
+ }
631
+ if (data.chkBackup && data.index === 0) {
632
+ const bakContent = await FsUtils.readFile(saveFile);
633
+ if (bakContent) {
634
+ const bakFile = saveFile + '.bak-' + new Date().toISOString().replace(/:/g, '-');
635
+ await FsUtils.writeFile(bakFile, bakContent);
636
+ }
637
+ }
638
+
639
+ this.logger.info(
640
+ `byClientUpdate, index: ${data.index}, saveFile: ${saveFile}, received len: ${(fileContent || '').length}`
641
+ );
642
+ if (data.index === 0) {
643
+ await FsUtils.writeFile(saveFile, fileContent || '');
644
+ } else {
645
+ await FsUtils.appendFile(saveFile, fileContent || '');
646
+ }
647
+
648
+ const response = {
649
+ status: 'ok',
650
+ message: 'Remote server updated by a client.',
651
+ };
652
+ ApiHelper.sendJson(req, res, response);
653
+ } catch (e: any) {
654
+ console.log('byClientUpdate failed', e);
655
+ const response = {
656
+ status: 'error',
657
+ message: 'byClientUpdate failed',
658
+ };
659
+ ApiHelper.sendJson(req, res, response);
660
+ }
661
+ return true;
662
+ }
663
+
664
+ async byClientRefreshCache(req: ServerRequest, res: ServerResponse) {
665
+ const jsonData = req.locals.json();
666
+ const data = await this.chkData(jsonData, req, res, true);
667
+ if (!data) return true;
668
+
669
+ await processRefreshCache(req);
670
+ const response = {
671
+ status: 'ok',
672
+ message: 'Cache refreshed successfully.',
673
+ };
674
+ ApiHelper.sendJson(req, res, response);
675
+ return true;
676
+ }
677
+
678
+ async byClientRestartApp(req: ServerRequest, res: ServerResponse) {
679
+ const jsonData = req.locals.json();
680
+ const data = await this.chkData(jsonData, req, res, true);
681
+ if (!data) return true;
682
+
683
+ await processRestartApp();
684
+ const response = {
685
+ status: 'ok',
686
+ message: 'Restart app successfully.',
687
+ };
688
+ ApiHelper.sendJson(req, res, response);
689
+ return true;
690
+ }
691
+
692
+ async byClientShell(req: ServerRequest, res: ServerResponse) {
693
+ const jsonData = req.locals.json();
694
+ const data = await this.chkData(jsonData, req, res, true);
695
+ if (!data) return true;
696
+
697
+ const result = await processShell(req);
698
+ const response = {
699
+ status: 'ok',
700
+ message: 'Shell executed successfully.',
701
+ result,
702
+ };
703
+ ApiHelper.sendJson(req, res, response);
704
+ return true;
705
+ }
706
+ }