plugin-cluster-manager 1.1.7 → 1.1.11

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 (108) hide show
  1. package/client.js +1 -0
  2. package/dist/client/AclCacheManager.d.ts +2 -0
  3. package/dist/client/CacheMonitor.d.ts +2 -0
  4. package/dist/client/ClusterManagerLayout.d.ts +2 -0
  5. package/dist/client/ClusterNodes.d.ts +2 -0
  6. package/dist/client/ContainerOrchestrator.d.ts +2 -0
  7. package/dist/client/Doctor.d.ts +2 -0
  8. package/dist/client/EventQueueMonitor.d.ts +2 -0
  9. package/dist/client/LockMonitor.d.ts +2 -0
  10. package/dist/client/NginxCacheManager.d.ts +2 -0
  11. package/dist/client/PackageInstaller.d.ts +2 -0
  12. package/dist/client/PluginOperations.d.ts +2 -0
  13. package/dist/client/RedisMonitor.d.ts +2 -0
  14. package/dist/client/TaskManager.d.ts +2 -0
  15. package/dist/client/WorkflowExecutions.d.ts +2 -0
  16. package/dist/client/index.d.ts +5 -0
  17. package/dist/client/index.js +1 -1
  18. package/dist/client/utils/clientSafeCache.d.ts +3 -0
  19. package/dist/client/utils/requestDedupInterceptor.d.ts +2 -0
  20. package/dist/client/utils.d.ts +12 -0
  21. package/dist/externalVersion.js +5 -5
  22. package/dist/index.d.ts +2 -0
  23. package/dist/locale/en-US.json +97 -1
  24. package/dist/locale/vi-VN.json +98 -1
  25. package/dist/locale/zh-CN.json +98 -1
  26. package/dist/server/actions/acl-cache.d.ts +53 -0
  27. package/dist/server/actions/acl-cache.js +1 -1
  28. package/dist/server/actions/cache-monitor.d.ts +33 -0
  29. package/dist/server/actions/cache-monitor.js +301 -0
  30. package/dist/server/actions/cluster-nodes.d.ts +64 -0
  31. package/dist/server/actions/cluster-nodes.js +394 -10
  32. package/dist/server/actions/doctor.d.ts +82 -0
  33. package/dist/server/actions/doctor.js +1250 -0
  34. package/dist/server/actions/event-queue-monitor.d.ts +13 -0
  35. package/dist/server/actions/lock-monitor.d.ts +19 -0
  36. package/dist/server/actions/orchestrator.d.ts +58 -0
  37. package/dist/server/actions/package-manager.d.ts +6 -0
  38. package/dist/server/actions/plugin-operations.d.ts +6 -0
  39. package/dist/server/actions/redis-monitor.d.ts +12 -0
  40. package/dist/server/actions/tasks.d.ts +7 -0
  41. package/dist/server/actions/workflow-executions.d.ts +7 -0
  42. package/dist/server/adapters/redis-lock-adapter.d.ts +15 -0
  43. package/dist/server/adapters/redis-node-registry.d.ts +12 -0
  44. package/dist/server/adapters/redis-pubsub-adapter.d.ts +16 -0
  45. package/dist/server/collections/app.d.ts +8 -0
  46. package/dist/server/collections/cluster-manager-acl-cache.d.ts +22 -0
  47. package/dist/server/collections/cluster-manager-cache-mgr.d.ts +22 -0
  48. package/dist/server/collections/cluster-manager-cluster.d.ts +22 -0
  49. package/dist/server/collections/cluster-manager-doctor-runs.d.ts +3 -0
  50. package/dist/server/collections/cluster-manager-doctor-runs.js +52 -0
  51. package/dist/server/collections/cluster-manager-doctor.d.ts +18 -0
  52. package/dist/server/collections/cluster-manager-doctor.js +44 -0
  53. package/dist/server/collections/cluster-manager-lock.d.ts +22 -0
  54. package/dist/server/collections/cluster-manager-plugins.d.ts +18 -0
  55. package/dist/server/collections/cluster-manager-queue.d.ts +22 -0
  56. package/dist/server/collections/cluster-manager-redis.d.ts +22 -0
  57. package/dist/server/collections/cluster-manager-workflow.d.ts +22 -0
  58. package/dist/server/collections/cluster-manager.d.ts +22 -0
  59. package/dist/server/collections/orchestrator-settings.d.ts +59 -0
  60. package/dist/server/collections/orchestrator-stacks.d.ts +102 -0
  61. package/dist/server/collections/worker-orchestrator.d.ts +22 -0
  62. package/dist/server/collections/worker-packages-configs.d.ts +3 -0
  63. package/dist/server/collections/worker-packages.d.ts +22 -0
  64. package/dist/server/hooks/cacheInvalidationHooks.d.ts +1 -0
  65. package/dist/server/hooks/cacheInvalidationHooks.js +81 -0
  66. package/dist/server/index.d.ts +1 -0
  67. package/dist/server/middlewares/listMetaCacheMiddleware.d.ts +2 -0
  68. package/dist/server/middlewares/listMetaCacheMiddleware.js +79 -0
  69. package/dist/server/orchestrator/PackageManager.d.ts +39 -0
  70. package/dist/server/orchestrator/PackageManager.js +83 -27
  71. package/dist/server/orchestrator/docker-adapter.d.ts +41 -0
  72. package/dist/server/orchestrator/index.d.ts +4 -0
  73. package/dist/server/orchestrator/k8s-adapter.d.ts +50 -0
  74. package/dist/server/orchestrator/leader-election.d.ts +48 -0
  75. package/dist/server/orchestrator/types.d.ts +84 -0
  76. package/dist/server/plugin.d.ts +26 -0
  77. package/dist/server/plugin.js +70 -8
  78. package/dist/server/utils/node.d.ts +6 -0
  79. package/dist/server/utils/redis.d.ts +29 -0
  80. package/dist/server/utils/versionManager.d.ts +10 -0
  81. package/dist/server/utils/versionManager.js +91 -0
  82. package/dist/shared/packages.d.ts +23 -0
  83. package/package.json +41 -41
  84. package/server.js +1 -0
  85. package/src/client/CacheMonitor.tsx +166 -179
  86. package/src/client/ClusterManagerLayout.tsx +48 -42
  87. package/src/client/ClusterNodes.tsx +691 -418
  88. package/src/client/Doctor.tsx +559 -0
  89. package/src/client/NginxCacheManager.tsx +415 -0
  90. package/src/client/PluginOperations.tsx +234 -234
  91. package/src/client/index.tsx +22 -14
  92. package/src/client/utils/clientSafeCache.ts +41 -0
  93. package/src/client/utils/requestDedupInterceptor.ts +213 -0
  94. package/src/locale/en-US.json +97 -1
  95. package/src/locale/vi-VN.json +98 -1
  96. package/src/locale/zh-CN.json +98 -1
  97. package/src/server/__tests__/doctor.test.ts +53 -0
  98. package/src/server/actions/acl-cache.ts +272 -272
  99. package/src/server/actions/cache-monitor.ts +453 -116
  100. package/src/server/actions/cluster-nodes.ts +882 -378
  101. package/src/server/actions/doctor.ts +1540 -0
  102. package/src/server/collections/cluster-manager-doctor-runs.ts +23 -0
  103. package/src/server/collections/cluster-manager-doctor.ts +19 -0
  104. package/src/server/hooks/cacheInvalidationHooks.ts +58 -0
  105. package/src/server/middlewares/listMetaCacheMiddleware.ts +55 -0
  106. package/src/server/orchestrator/PackageManager.ts +19 -15
  107. package/src/server/plugin.ts +353 -263
  108. package/src/server/utils/versionManager.ts +69 -0
@@ -1,116 +1,453 @@
1
- import { Context } from '@nocobase/actions';
2
- import { getRedis } from '../utils/redis';
3
-
4
-
5
- export const cacheMonitorActions = {
6
- /**
7
- * GET /clusterManagerCacheMgr:stores
8
- * List all registered cache stores and their config
9
- */
10
- async stores(ctx: Context, next: () => Promise<void>) {
11
- const cm = ctx.app.cacheManager;
12
- if (!cm) {
13
- ctx.throw(503, 'Cache manager is not available');
14
- }
15
-
16
- const stores: any[] = [];
17
-
18
- // storeTypes is a Map of registered store type configs
19
- const storeTypes = (cm as any).storeTypes as Map<string, any>;
20
- if (storeTypes) {
21
- for (const [name, config] of storeTypes.entries()) {
22
- const storeType = config.store === 'memory' ? 'memory' : 'redis';
23
- stores.push({
24
- name,
25
- type: storeType,
26
- isDefault: name === cm.defaultStore,
27
- });
28
- }
29
- }
30
-
31
- ctx.body = { data: stores, meta: { count: stores.length, defaultStore: cm.defaultStore } };
32
- await next();
33
- },
34
-
35
- /**
36
- * GET /clusterManagerCacheMgr:caches
37
- * List all created named caches
38
- */
39
- async caches(ctx: Context, next: () => Promise<void>) {
40
- const cm = ctx.app.cacheManager;
41
- if (!cm) {
42
- ctx.throw(503, 'Cache manager is not available');
43
- }
44
-
45
- const caches: any[] = [];
46
- const cacheMap = (cm as any).caches as Map<string, any>;
47
- if (cacheMap) {
48
- for (const [name, cache] of cacheMap.entries()) {
49
- caches.push({
50
- name,
51
- prefix: (cache as any).prefix || null,
52
- });
53
- }
54
- }
55
-
56
- ctx.body = { data: caches, meta: { count: caches.length } };
57
- await next();
58
- },
59
-
60
- /**
61
- * GET /clusterManagerCacheMgr:redisMemory
62
- * Get Redis memory usage for cache keys
63
- */
64
- async redisMemory(ctx: Context, next: () => Promise<void>) {
65
- const redis = getRedis(ctx);
66
- if (!redis) {
67
- ctx.body = { available: false };
68
- await next();
69
- return;
70
- }
71
-
72
- try {
73
- const info = await redis.sendCommand(['INFO', 'memory']);
74
- const lines = String(info).split(/\r?\n/);
75
- const memInfo: Record<string, string> = {};
76
- for (const line of lines) {
77
- const idx = line.indexOf(':');
78
- if (idx > 0) {
79
- memInfo[line.slice(0, idx)] = line.slice(idx + 1).trim();
80
- }
81
- }
82
-
83
- // Count keys by prefix pattern
84
- const dbSize = await redis.sendCommand(['DBSIZE']);
85
-
86
- ctx.body = {
87
- available: true,
88
- usedMemory: memInfo.used_memory_human,
89
- usedMemoryBytes: Number(memInfo.used_memory || 0),
90
- totalKeys: Number(dbSize) || 0,
91
- };
92
- } catch (e: any) {
93
- ctx.body = { available: false, error: e.message };
94
- }
95
- await next();
96
- },
97
-
98
- /**
99
- * POST /clusterManagerCacheMgr:flushAll
100
- * Flush all caches via CacheManager
101
- */
102
- async flushAll(ctx: Context, next: () => Promise<void>) {
103
- const cm = ctx.app.cacheManager;
104
- if (!cm) {
105
- ctx.throw(503, 'Cache manager is not available');
106
- }
107
-
108
- const user = ctx.state?.currentUser?.nickname || ctx.state?.currentUser?.id || 'unknown';
109
- ctx.app.logger.warn(`[cluster-manager] Flushing all caches by user ${user}`);
110
-
111
- await cm.flushAll();
112
-
113
- ctx.body = { success: true };
114
- await next();
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
+ };