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.
- package/README.md +3 -3
- package/admin/admin-about.tsx +12 -16
- package/admin/admin-config.tsx +47 -44
- package/admin/admin-css.tsx +3 -3
- package/admin/admin-db.tsx +75 -75
- package/admin/admin-frame-helper.tsx +364 -364
- package/admin/admin-frame.tsx +164 -164
- package/admin/admin-index.tsx +65 -65
- package/admin/admin-login.tsx +111 -111
- package/admin/admin-menu-edit.tsx +637 -637
- package/admin/admin-menu-list.tsx +87 -87
- package/admin/admin-page-edit.tsx +564 -564
- package/admin/admin-page-list.tsx +83 -83
- package/admin/admin-performance.tsx +28 -28
- package/admin/admin-release.tsx +427 -426
- package/admin/admin-resources.tsx +382 -382
- package/admin/admin-shell.tsx +89 -89
- package/admin/admin-table-data.tsx +146 -146
- package/admin/admin-table-list.tsx +230 -230
- package/admin/admin-test-animations.tsx +395 -395
- package/admin/admin-test-component.tsx +823 -808
- package/admin/admin-test-edit.tsx +319 -319
- package/admin/admin-test-themes.tsx +56 -56
- package/admin/admin-tokens.tsx +338 -338
- package/admin/design/admin-design.tsx +174 -174
- package/admin/design/block-grid.tsx +36 -36
- package/admin/design/block-grid1.tsx +21 -21
- package/admin/design/block-paragraph.tsx +19 -19
- package/admin/design/block-title.tsx +19 -19
- package/admin/design/design-block-box.tsx +140 -140
- package/admin/design/drag-data.tsx +24 -24
- package/admin/index.ts +9 -9
- package/admin/package.json +15 -15
- package/admin/tsconfig.json +127 -127
- package/dev/copy-folder.js +32 -32
- package/dev/cp-index-html.js +69 -69
- package/dev/file-utils.js +12 -12
- package/dev/index.js +18 -19
- package/dev/package.json +12 -12
- package/dev/plugin-ifelse.js +168 -168
- package/dev/plugin-ifelse.test.js +37 -37
- package/dev/run-cmd.js +14 -14
- package/dev/send-request.js +12 -12
- package/package.json +55 -55
- package/src/admin-api/admin-api-helper.ts +210 -205
- package/src/admin-api/admin-api.ts +65 -65
- package/src/admin-api/admin-auth.ts +152 -146
- package/src/admin-api/admin-config.ts +94 -84
- package/src/admin-api/admin-csv.ts +94 -94
- package/src/admin-api/admin-db.ts +269 -269
- package/src/admin-api/admin-menu.ts +135 -135
- package/src/admin-api/admin-page.ts +135 -135
- package/src/admin-api/admin-performance.ts +128 -128
- package/src/admin-api/admin-release.ts +706 -700
- package/src/admin-api/admin-resources.ts +318 -318
- package/src/admin-api/admin-token-helper.ts +82 -79
- package/src/admin-api/admin-tokens.ts +90 -90
- package/src/admin-api/index.ts +2 -2
- package/src/admin-api/web-config-api.ts +19 -19
- package/src/api/api-cache.ts +103 -103
- package/src/api/api-helper.ts +44 -44
- package/src/api/api-module.ts +67 -60
- package/src/api/api-router.ts +177 -177
- package/src/api/api-shared-storage.ts +64 -64
- package/src/api/async-storage.ts +5 -5
- package/src/api/debug-service.ts +56 -56
- package/src/api/encode-html.ts +27 -27
- package/src/api/handle-status.ts +75 -75
- package/src/api/index.ts +15 -16
- package/src/api/mini-web-socket.ts +270 -270
- package/src/api/server-content-type.ts +82 -82
- package/src/api/server-render.ts +235 -215
- package/src/api/shell-service.ts +74 -74
- package/src/api/simple-storage.ts +80 -80
- package/src/api/static-server.ts +128 -125
- package/src/api/to-client-delivery.ts +26 -26
- package/src/app/app-cache.ts +55 -55
- package/src/app/app-helper.ts +62 -62
- package/src/app/app-message.ts +109 -109
- package/src/app/app-shared-storage.ts +363 -363
- package/src/app/app-start.ts +136 -136
- package/src/app/cleanup-exit.ts +16 -16
- package/src/app/host-to-path.ts +38 -38
- package/src/app/index.ts +11 -11
- package/src/app/process-dev-requests.ts +130 -130
- package/src/app/web-listener.ts +294 -294
- package/src/app/web-processor.ts +47 -42
- package/src/app/web-server.ts +100 -100
- package/src/common-js/web-env.js +104 -104
- package/src/index.ts +7 -7
- package/src/lang/api-lang-en.ts +26 -26
- package/src/lang/api-lang-zh-cn.ts +27 -27
- package/src/lang/index.ts +2 -2
- package/src/lang/lang-helper.ts +76 -76
- package/src/lang/lang-props.ts +6 -6
- package/src/lib/db/db-helper.ts +23 -23
- package/src/lib/db/db-mysql.ts +249 -250
- package/src/lib/db/db-sqlite.ts +101 -101
- package/src/lib/db/db.spec.ts +28 -28
- package/src/lib/db/db.ts +325 -325
- package/src/lib/db/index.ts +5 -5
- package/src/lib/index.ts +3 -3
- package/src/lib/logger.spec.ts +214 -214
- package/src/lib/logger.ts +281 -281
- package/src/lib/runtime-require.ts +37 -37
- package/src/lib/utils/cookie-util.ts +34 -34
- package/src/lib/utils/crypto.ts +58 -58
- package/src/lib/utils/date-utils.ts +317 -317
- package/src/lib/utils/deep-merge.ts +37 -37
- package/src/lib/utils/delay.ts +12 -12
- package/src/lib/utils/file-setting.ts +55 -55
- package/src/lib/utils/format-bytes.ts +11 -11
- package/src/lib/utils/fs-utils.ts +158 -158
- package/src/lib/utils/get-env.ts +27 -27
- package/src/lib/utils/index.ts +12 -12
- package/src/lib/utils/is-type.ts +48 -48
- package/src/lib/utils/load-env.ts +14 -14
- package/src/lib/utils/pad.ts +6 -6
- package/src/models/api-base.ts +5 -5
- package/src/models/api-module-props.ts +10 -11
- package/src/models/api-router-props.ts +26 -26
- package/src/models/app-cache-props.ts +33 -33
- package/src/models/app-data-props.ts +10 -10
- package/src/models/app-helper-props.ts +6 -6
- package/src/models/app-shared-storage-props.ts +38 -38
- package/src/models/app-start-props.ts +18 -18
- package/src/models/async-storage-props.ts +13 -13
- package/src/models/db-config.ts +30 -30
- package/src/models/host-to-path-props.ts +12 -12
- package/src/models/index.ts +16 -16
- package/src/models/json-object.ts +8 -8
- package/src/models/locals-props.ts +36 -36
- package/src/models/logger-props.ts +84 -84
- package/src/models/simple-storage-props.ts +13 -14
- package/src/models/to-client-delivery-props.ts +6 -6
- package/tsconfig.json +115 -115
- 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(
|
|
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
|
|
282
|
-
const
|
|
283
|
-
const subFolders = await FsUtils.
|
|
284
|
-
webSub.push(
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
.
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
const
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
const
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
const
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
const
|
|
333
|
-
const
|
|
334
|
-
const
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
const
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
//
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
const
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
const
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
sendFile = path.join(appData.apiPath, '..', fromList + '_web', data.webSub);
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
//
|
|
517
|
-
//
|
|
518
|
-
//
|
|
519
|
-
//
|
|
520
|
-
//
|
|
521
|
-
//
|
|
522
|
-
//
|
|
523
|
-
//
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
const
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
chkOption !== '
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
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
|
+
}
|