plugin-cluster-manager 1.1.10 → 1.1.13
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/client-v2.d.ts +2 -0
- package/client-v2.js +1 -0
- package/client.js +1 -0
- package/dist/client/index.js +1 -1
- package/dist/client-v2/914.5dc1105cf3ada6a6.js +10 -0
- package/dist/client-v2/index.js +10 -0
- package/dist/externalVersion.js +6 -5
- package/dist/locale/en-US.json +138 -28
- package/dist/locale/vi-VN.json +139 -28
- package/dist/locale/zh-CN.json +140 -28
- package/dist/server/actions/cache-monitor.js +301 -0
- package/dist/server/actions/cluster-nodes.js +391 -11
- package/dist/server/actions/doctor.js +1246 -0
- package/dist/server/actions/orchestrator.js +37 -0
- package/dist/server/actions/queue-mappings.js +107 -0
- package/dist/server/collections/cluster-manager-doctor-runs.js +52 -0
- package/dist/server/collections/cluster-manager-doctor.js +44 -0
- package/dist/server/collections/worker-queue-mappings.js +106 -0
- package/dist/server/hooks/cacheInvalidationHooks.js +81 -0
- package/dist/server/middlewares/listMetaCacheMiddleware.js +79 -0
- package/dist/server/orchestrator/PackageManager.js +21 -24
- package/dist/server/orchestrator/docker-adapter.js +49 -27
- package/dist/server/plugin.js +71 -16
- package/dist/server/queue-scanner.js +141 -0
- package/dist/server/utils/node.js +30 -2
- package/dist/server/utils/versionManager.js +91 -0
- package/package.json +9 -5
- package/server.js +1 -0
- package/src/client/AclCacheManager.tsx +292 -287
- package/src/client/CacheMonitor.tsx +166 -179
- package/src/client/ClusterManagerLayout.tsx +54 -42
- package/src/client/ClusterNodes.tsx +698 -418
- package/src/client/ContainerOrchestrator.tsx +184 -102
- package/src/client/Doctor.tsx +559 -0
- package/src/client/NginxCacheManager.tsx +415 -0
- package/src/client/PluginOperations.tsx +234 -234
- package/src/client/QueueAssignment.tsx +355 -0
- package/src/client/TaskManager.tsx +194 -187
- package/src/client/WorkflowExecutions.tsx +243 -238
- package/src/client/index.tsx +22 -14
- package/src/client/utils/clientSafeCache.ts +41 -0
- package/src/client/utils/requestDedupInterceptor.ts +213 -0
- package/src/client-v2/plugin.tsx +24 -0
- package/src/locale/en-US.json +138 -28
- package/src/locale/vi-VN.json +139 -28
- package/src/locale/zh-CN.json +140 -28
- package/src/server/__tests__/doctor.test.ts +53 -0
- package/src/server/actions/acl-cache.ts +272 -272
- package/src/server/actions/cache-monitor.ts +453 -116
- package/src/server/actions/cluster-nodes.ts +878 -378
- package/src/server/actions/doctor.ts +1536 -0
- package/src/server/actions/orchestrator.ts +54 -2
- package/src/server/actions/queue-mappings.ts +94 -0
- package/src/server/collections/cluster-manager-doctor-runs.ts +23 -0
- package/src/server/collections/cluster-manager-doctor.ts +19 -0
- package/src/server/collections/worker-queue-mappings.ts +85 -0
- package/src/server/hooks/cacheInvalidationHooks.ts +58 -0
- package/src/server/middlewares/listMetaCacheMiddleware.ts +55 -0
- package/src/server/orchestrator/PackageManager.ts +20 -24
- package/src/server/orchestrator/docker-adapter.ts +74 -37
- package/src/server/plugin.ts +347 -270
- package/src/server/queue-scanner.ts +154 -0
- package/src/server/utils/node.ts +48 -0
- package/src/server/utils/versionManager.ts +69 -0
- package/dist/client/AclCacheManager.d.ts +0 -2
- package/dist/client/CacheMonitor.d.ts +0 -2
- package/dist/client/ClusterManagerLayout.d.ts +0 -2
- package/dist/client/ClusterNodes.d.ts +0 -2
- package/dist/client/ContainerOrchestrator.d.ts +0 -2
- package/dist/client/EventQueueMonitor.d.ts +0 -2
- package/dist/client/LockMonitor.d.ts +0 -2
- package/dist/client/PackageInstaller.d.ts +0 -2
- package/dist/client/PluginOperations.d.ts +0 -2
- package/dist/client/RedisMonitor.d.ts +0 -2
- package/dist/client/TaskManager.d.ts +0 -2
- package/dist/client/WorkflowExecutions.d.ts +0 -2
- package/dist/client/index.d.ts +0 -5
- package/dist/client/utils.d.ts +0 -12
- package/dist/index.d.ts +0 -2
- package/dist/server/actions/acl-cache.d.ts +0 -53
- package/dist/server/actions/cache-monitor.d.ts +0 -23
- package/dist/server/actions/cluster-nodes.d.ts +0 -49
- package/dist/server/actions/event-queue-monitor.d.ts +0 -13
- package/dist/server/actions/lock-monitor.d.ts +0 -19
- package/dist/server/actions/orchestrator.d.ts +0 -58
- package/dist/server/actions/package-manager.d.ts +0 -6
- package/dist/server/actions/plugin-operations.d.ts +0 -6
- package/dist/server/actions/redis-monitor.d.ts +0 -12
- package/dist/server/actions/tasks.d.ts +0 -7
- package/dist/server/actions/workflow-executions.d.ts +0 -7
- package/dist/server/adapters/redis-lock-adapter.d.ts +0 -15
- package/dist/server/adapters/redis-node-registry.d.ts +0 -12
- package/dist/server/adapters/redis-pubsub-adapter.d.ts +0 -16
- package/dist/server/collections/app.d.ts +0 -8
- package/dist/server/collections/cluster-manager-acl-cache.d.ts +0 -22
- package/dist/server/collections/cluster-manager-cache-mgr.d.ts +0 -22
- package/dist/server/collections/cluster-manager-cluster.d.ts +0 -22
- package/dist/server/collections/cluster-manager-lock.d.ts +0 -22
- package/dist/server/collections/cluster-manager-plugins.d.ts +0 -18
- package/dist/server/collections/cluster-manager-queue.d.ts +0 -22
- package/dist/server/collections/cluster-manager-redis.d.ts +0 -22
- package/dist/server/collections/cluster-manager-workflow.d.ts +0 -22
- package/dist/server/collections/cluster-manager.d.ts +0 -22
- package/dist/server/collections/orchestrator-settings.d.ts +0 -59
- package/dist/server/collections/orchestrator-stacks.d.ts +0 -102
- package/dist/server/collections/worker-orchestrator.d.ts +0 -22
- package/dist/server/collections/worker-packages-configs.d.ts +0 -3
- package/dist/server/collections/worker-packages.d.ts +0 -22
- package/dist/server/orchestrator/PackageManager.d.ts +0 -39
- package/dist/server/orchestrator/docker-adapter.d.ts +0 -41
- package/dist/server/orchestrator/index.d.ts +0 -4
- package/dist/server/orchestrator/k8s-adapter.d.ts +0 -50
- package/dist/server/orchestrator/leader-election.d.ts +0 -48
- package/dist/server/orchestrator/types.d.ts +0 -84
- package/dist/server/plugin.d.ts +0 -26
- package/dist/server/utils/node.d.ts +0 -6
- package/dist/server/utils/redis.d.ts +0 -29
- package/dist/shared/packages.d.ts +0 -23
- /package/{dist/server/index.d.ts → src/client-v2/index.tsx} +0 -0
|
@@ -1,116 +1,453 @@
|
|
|
1
|
-
import { Context } from '@nocobase/actions';
|
|
2
|
-
import { getRedis } from '../utils/redis';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
1
|
+
import { Context } from '@nocobase/actions';
|
|
2
|
+
import { getRedis } from '../utils/redis';
|
|
3
|
+
import { promises as fsp } from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { exec } from 'child_process';
|
|
6
|
+
import http from 'http';
|
|
7
|
+
import https from 'https';
|
|
8
|
+
|
|
9
|
+
async function exists(p: string): Promise<boolean> {
|
|
10
|
+
try {
|
|
11
|
+
await fsp.access(p);
|
|
12
|
+
return true;
|
|
13
|
+
} catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function isSafePath(dirPath: string): boolean {
|
|
19
|
+
if (!dirPath) return false;
|
|
20
|
+
const resolved = path.resolve(dirPath);
|
|
21
|
+
const normalized = resolved.toLowerCase().replace(/\\/g, '/');
|
|
22
|
+
|
|
23
|
+
const restrictedPatterns = [
|
|
24
|
+
'^/$',
|
|
25
|
+
'^[a-z]:/?$',
|
|
26
|
+
'^/[^/]+$',
|
|
27
|
+
'^[a-z]:/[^/]+$',
|
|
28
|
+
'/windows',
|
|
29
|
+
'/system32',
|
|
30
|
+
'/program files',
|
|
31
|
+
'/etc',
|
|
32
|
+
'/var$',
|
|
33
|
+
'/usr$',
|
|
34
|
+
'/boot',
|
|
35
|
+
'/sys',
|
|
36
|
+
'/proc',
|
|
37
|
+
'/dev',
|
|
38
|
+
'/home$',
|
|
39
|
+
'/root$',
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
for (const pat of restrictedPatterns) {
|
|
43
|
+
const re = new RegExp(pat);
|
|
44
|
+
if (re.test(normalized)) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const parts = normalized.split('/').filter(Boolean);
|
|
50
|
+
const nonDriveParts = parts.filter((p) => !/^[a-z]:$/.test(p));
|
|
51
|
+
if (nonDriveParts.length < 2) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function findNginxConfig(): Promise<string | null> {
|
|
59
|
+
const nginxConfPath = await new Promise<string | null>((resolve) => {
|
|
60
|
+
exec('nginx -V', (err, stdout, stderr) => {
|
|
61
|
+
if (err) return resolve(null);
|
|
62
|
+
const output = stdout + stderr;
|
|
63
|
+
const match = output.match(/--conf-path=([^\s]+)/);
|
|
64
|
+
if (match && match[1]) {
|
|
65
|
+
resolve(match[1]);
|
|
66
|
+
} else {
|
|
67
|
+
resolve(null);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
if (nginxConfPath && (await exists(nginxConfPath))) {
|
|
72
|
+
return nginxConfPath;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const searchPaths = [
|
|
76
|
+
'/etc/nginx/nginx.conf',
|
|
77
|
+
'/usr/local/nginx/conf/nginx.conf',
|
|
78
|
+
'/usr/local/etc/nginx/nginx.conf',
|
|
79
|
+
'/opt/homebrew/etc/nginx/nginx.conf',
|
|
80
|
+
'C:\\nginx\\conf\\nginx.conf',
|
|
81
|
+
];
|
|
82
|
+
for (const p of searchPaths) {
|
|
83
|
+
if (await exists(p)) {
|
|
84
|
+
return p;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function readConfigRecursive(configPath: string, visited: Set<string> = new Set()): Promise<string> {
|
|
91
|
+
const resolvedPath = path.resolve(configPath);
|
|
92
|
+
if (visited.has(resolvedPath)) return '';
|
|
93
|
+
visited.add(resolvedPath);
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const content = await fsp.readFile(resolvedPath, 'utf8');
|
|
97
|
+
const lines = content.split(/\r?\n/);
|
|
98
|
+
let fullText = content + '\n';
|
|
99
|
+
|
|
100
|
+
const configDir = path.dirname(resolvedPath);
|
|
101
|
+
|
|
102
|
+
for (const line of lines) {
|
|
103
|
+
const trimmed = line.trim();
|
|
104
|
+
if (trimmed.startsWith('#')) continue;
|
|
105
|
+
|
|
106
|
+
const includeMatch = trimmed.match(/^include\s+([^\s;]+)/);
|
|
107
|
+
if (includeMatch && includeMatch[1]) {
|
|
108
|
+
let includePattern = includeMatch[1].trim().replace(/^["']|["']$/g, '');
|
|
109
|
+
|
|
110
|
+
if (!path.isAbsolute(includePattern)) {
|
|
111
|
+
includePattern = path.join(configDir, includePattern);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (includePattern.includes('*')) {
|
|
115
|
+
try {
|
|
116
|
+
const patternDir = path.dirname(includePattern);
|
|
117
|
+
const ext = path.extname(includePattern);
|
|
118
|
+
if (await exists(patternDir)) {
|
|
119
|
+
const files = await fsp.readdir(patternDir);
|
|
120
|
+
for (const file of files) {
|
|
121
|
+
if (file.endsWith(ext) || includePattern.endsWith('*')) {
|
|
122
|
+
const filePath = path.join(patternDir, file);
|
|
123
|
+
fullText += (await readConfigRecursive(filePath, visited)) + '\n';
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
} catch {
|
|
128
|
+
// Ignore include directory read failures
|
|
129
|
+
}
|
|
130
|
+
} else {
|
|
131
|
+
if (await exists(includePattern)) {
|
|
132
|
+
fullText += (await readConfigRecursive(includePattern, visited)) + '\n';
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return fullText;
|
|
138
|
+
} catch {
|
|
139
|
+
return '';
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function extractCachePaths(configText: string): string[] {
|
|
144
|
+
const paths: string[] = [];
|
|
145
|
+
const lines = configText.split(/\r?\n/);
|
|
146
|
+
for (const line of lines) {
|
|
147
|
+
const trimmed = line.trim();
|
|
148
|
+
if (trimmed.startsWith('#')) continue;
|
|
149
|
+
|
|
150
|
+
const match = trimmed.match(/^(?:proxy|fastcgi|scgi|uwsgi)_cache_path\s+([^\s;]+)/);
|
|
151
|
+
if (match && match[1]) {
|
|
152
|
+
const p = match[1].trim();
|
|
153
|
+
const cleanPath = p.replace(/^["']|["']$/g, '');
|
|
154
|
+
if (cleanPath && !paths.includes(cleanPath)) {
|
|
155
|
+
paths.push(cleanPath);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return paths;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function emptyDirectory(dirPath: string): Promise<{ success: boolean; clearedCount: number; error?: string }> {
|
|
163
|
+
if (!isSafePath(dirPath)) {
|
|
164
|
+
return { success: false, clearedCount: 0, error: 'Path is classified as unsafe or restricted' };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
if (!(await exists(dirPath))) {
|
|
169
|
+
return { success: false, clearedCount: 0, error: 'Directory does not exist' };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const stats = await fsp.stat(dirPath);
|
|
173
|
+
if (!stats.isDirectory()) {
|
|
174
|
+
return { success: false, clearedCount: 0, error: 'Path is not a directory' };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const files = await fsp.readdir(dirPath);
|
|
178
|
+
let clearedCount = 0;
|
|
179
|
+
for (const file of files) {
|
|
180
|
+
const fullPath = path.join(dirPath, file);
|
|
181
|
+
await fsp.rm(fullPath, { recursive: true, force: true });
|
|
182
|
+
clearedCount++;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return { success: true, clearedCount };
|
|
186
|
+
} catch (err: any) {
|
|
187
|
+
return { success: false, clearedCount: 0, error: err.message };
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function makePurgeRequest(
|
|
192
|
+
urlStr: string,
|
|
193
|
+
method = 'PURGE',
|
|
194
|
+
headers: Record<string, string> = {},
|
|
195
|
+
): Promise<{ success: boolean; status?: number; data?: string; error?: string }> {
|
|
196
|
+
return new Promise((resolve) => {
|
|
197
|
+
try {
|
|
198
|
+
const url = new URL(urlStr);
|
|
199
|
+
const isHttps = url.protocol === 'https:';
|
|
200
|
+
const client = isHttps ? https : http;
|
|
201
|
+
|
|
202
|
+
const reqHeaders = { ...headers };
|
|
203
|
+
|
|
204
|
+
const req = client.request(
|
|
205
|
+
urlStr,
|
|
206
|
+
{
|
|
207
|
+
method,
|
|
208
|
+
headers: reqHeaders,
|
|
209
|
+
timeout: 10000,
|
|
210
|
+
},
|
|
211
|
+
(res) => {
|
|
212
|
+
let body = '';
|
|
213
|
+
res.on('data', (chunk) => {
|
|
214
|
+
body += chunk;
|
|
215
|
+
});
|
|
216
|
+
res.on('end', () => {
|
|
217
|
+
resolve({
|
|
218
|
+
success: (res.statusCode || 0) >= 200 && (res.statusCode || 0) < 300,
|
|
219
|
+
status: res.statusCode,
|
|
220
|
+
data: body,
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
},
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
req.on('error', (err) => {
|
|
227
|
+
resolve({
|
|
228
|
+
success: false,
|
|
229
|
+
error: err.message,
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
req.on('timeout', () => {
|
|
234
|
+
req.destroy();
|
|
235
|
+
resolve({
|
|
236
|
+
success: false,
|
|
237
|
+
error: 'Request timed out after 10 seconds',
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
req.end();
|
|
242
|
+
} catch (err: any) {
|
|
243
|
+
resolve({
|
|
244
|
+
success: false,
|
|
245
|
+
error: err.message,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export const cacheMonitorActions = {
|
|
252
|
+
/**
|
|
253
|
+
* GET /clusterManagerCacheMgr:stores
|
|
254
|
+
* List all registered cache stores and their config
|
|
255
|
+
*/
|
|
256
|
+
async stores(ctx: Context, next: () => Promise<void>) {
|
|
257
|
+
const cm = ctx.app.cacheManager;
|
|
258
|
+
if (!cm) {
|
|
259
|
+
ctx.throw(503, 'Cache manager is not available');
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const stores: any[] = [];
|
|
263
|
+
|
|
264
|
+
// storeTypes is a Map of registered store type configs
|
|
265
|
+
const storeTypes = (cm as any).storeTypes as Map<string, any>;
|
|
266
|
+
if (storeTypes) {
|
|
267
|
+
for (const [name, config] of storeTypes.entries()) {
|
|
268
|
+
const storeType = config.store === 'memory' ? 'memory' : 'redis';
|
|
269
|
+
stores.push({
|
|
270
|
+
name,
|
|
271
|
+
type: storeType,
|
|
272
|
+
isDefault: name === cm.defaultStore,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
ctx.body = { data: stores, meta: { count: stores.length, defaultStore: cm.defaultStore } };
|
|
278
|
+
await next();
|
|
279
|
+
},
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* GET /clusterManagerCacheMgr:caches
|
|
283
|
+
* List all created named caches
|
|
284
|
+
*/
|
|
285
|
+
async caches(ctx: Context, next: () => Promise<void>) {
|
|
286
|
+
const cm = ctx.app.cacheManager;
|
|
287
|
+
if (!cm) {
|
|
288
|
+
ctx.throw(503, 'Cache manager is not available');
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const caches: any[] = [];
|
|
292
|
+
const cacheMap = (cm as any).caches as Map<string, any>;
|
|
293
|
+
if (cacheMap) {
|
|
294
|
+
for (const [name, cache] of cacheMap.entries()) {
|
|
295
|
+
caches.push({
|
|
296
|
+
name,
|
|
297
|
+
prefix: (cache as any).prefix || null,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
ctx.body = { data: caches, meta: { count: caches.length } };
|
|
303
|
+
await next();
|
|
304
|
+
},
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* GET /clusterManagerCacheMgr:redisMemory
|
|
308
|
+
* Get Redis memory usage for cache keys
|
|
309
|
+
*/
|
|
310
|
+
async redisMemory(ctx: Context, next: () => Promise<void>) {
|
|
311
|
+
const redis = getRedis(ctx);
|
|
312
|
+
if (!redis) {
|
|
313
|
+
ctx.body = { available: false };
|
|
314
|
+
await next();
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
try {
|
|
319
|
+
const info = await redis.sendCommand(['INFO', 'memory']);
|
|
320
|
+
const lines = String(info).split(/\r?\n/);
|
|
321
|
+
const memInfo: Record<string, string> = {};
|
|
322
|
+
for (const line of lines) {
|
|
323
|
+
const idx = line.indexOf(':');
|
|
324
|
+
if (idx > 0) {
|
|
325
|
+
memInfo[line.slice(0, idx)] = line.slice(idx + 1).trim();
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Count keys by prefix pattern
|
|
330
|
+
const dbSize = await redis.sendCommand(['DBSIZE']);
|
|
331
|
+
|
|
332
|
+
ctx.body = {
|
|
333
|
+
available: true,
|
|
334
|
+
usedMemory: memInfo.used_memory_human,
|
|
335
|
+
usedMemoryBytes: Number(memInfo.used_memory || 0),
|
|
336
|
+
totalKeys: Number(dbSize) || 0,
|
|
337
|
+
};
|
|
338
|
+
} catch (e: any) {
|
|
339
|
+
ctx.body = { available: false, error: e.message };
|
|
340
|
+
}
|
|
341
|
+
await next();
|
|
342
|
+
},
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* POST /clusterManagerCacheMgr:flushAll
|
|
346
|
+
* Flush all caches via CacheManager
|
|
347
|
+
*/
|
|
348
|
+
async flushAll(ctx: Context, next: () => Promise<void>) {
|
|
349
|
+
const cm = ctx.app.cacheManager;
|
|
350
|
+
if (!cm) {
|
|
351
|
+
ctx.throw(503, 'Cache manager is not available');
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const user = ctx.state?.currentUser?.nickname || ctx.state?.currentUser?.id || 'unknown';
|
|
355
|
+
ctx.app.logger.warn(`[cluster-manager] Flushing all caches by user ${user}`);
|
|
356
|
+
|
|
357
|
+
await cm.flushAll();
|
|
358
|
+
|
|
359
|
+
ctx.body = { success: true };
|
|
360
|
+
await next();
|
|
361
|
+
},
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* GET /clusterManagerCacheMgr:nginxCacheStatus
|
|
365
|
+
* Detect if Nginx is installed, locate conf, and auto-load cache paths
|
|
366
|
+
*/
|
|
367
|
+
async nginxCacheStatus(ctx: Context, next: () => Promise<void>) {
|
|
368
|
+
let nginxInstalled = false;
|
|
369
|
+
let mainConfigPath: string | null = null;
|
|
370
|
+
let detectedPaths: string[] = [];
|
|
371
|
+
|
|
372
|
+
try {
|
|
373
|
+
// 1. Try running "nginx -T" to get fully resolved configuration
|
|
374
|
+
const configText = await new Promise<string | null>((resolve) => {
|
|
375
|
+
exec('nginx -T', { maxBuffer: 10 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
376
|
+
if (err) {
|
|
377
|
+
resolve(null);
|
|
378
|
+
} else {
|
|
379
|
+
nginxInstalled = true;
|
|
380
|
+
resolve(stdout);
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
if (configText) {
|
|
386
|
+
detectedPaths = extractCachePaths(configText);
|
|
387
|
+
mainConfigPath = await findNginxConfig();
|
|
388
|
+
} else {
|
|
389
|
+
// Fallback: search main config and manually trace includes
|
|
390
|
+
mainConfigPath = await findNginxConfig();
|
|
391
|
+
if (mainConfigPath) {
|
|
392
|
+
nginxInstalled = true;
|
|
393
|
+
const fullConfigText = await readConfigRecursive(mainConfigPath);
|
|
394
|
+
detectedPaths = extractCachePaths(fullConfigText);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
} catch (err) {
|
|
398
|
+
// Catch any unexpected exceptions to ensure endpoint never crashes
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
ctx.body = {
|
|
402
|
+
nginxInstalled,
|
|
403
|
+
mainConfigPath,
|
|
404
|
+
detectedPaths,
|
|
405
|
+
};
|
|
406
|
+
await next();
|
|
407
|
+
},
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* POST /clusterManagerCacheMgr:clearNginxCache
|
|
411
|
+
* Clear physical cache files or send an HTTP Purge request
|
|
412
|
+
*/
|
|
413
|
+
async clearNginxCache(ctx: Context, next: () => Promise<void>) {
|
|
414
|
+
const { method = 'directory', directory, url, httpMethod = 'PURGE', headers = {} } = ctx.action.params.values || {};
|
|
415
|
+
|
|
416
|
+
if (method === 'directory') {
|
|
417
|
+
if (!directory) {
|
|
418
|
+
ctx.throw(400, 'Directory path is required for physical cache clearing');
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const result = await emptyDirectory(directory);
|
|
422
|
+
if (!result.success) {
|
|
423
|
+
ctx.throw(400, result.error || 'Failed to clear cache directory');
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
ctx.body = {
|
|
427
|
+
success: true,
|
|
428
|
+
message: `Successfully cleared physical cache directory`,
|
|
429
|
+
clearedCount: result.clearedCount,
|
|
430
|
+
};
|
|
431
|
+
} else if (method === 'purgeRequest') {
|
|
432
|
+
if (!url) {
|
|
433
|
+
ctx.throw(400, 'Purge URL is required for HTTP Purge request method');
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const result = await makePurgeRequest(url, httpMethod, headers);
|
|
437
|
+
if (!result.success) {
|
|
438
|
+
ctx.throw(400, result.error || `HTTP Purge request failed with status: ${result.status}`);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
ctx.body = {
|
|
442
|
+
success: true,
|
|
443
|
+
message: `HTTP Purge request sent successfully`,
|
|
444
|
+
status: result.status,
|
|
445
|
+
data: result.data,
|
|
446
|
+
};
|
|
447
|
+
} else {
|
|
448
|
+
ctx.throw(400, `Unknown clearing method: ${method}`);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
await next();
|
|
452
|
+
},
|
|
453
|
+
};
|