whistle.mockbubu 1.0.0-dev.4 → 2.0.0-beta.0
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/.gitignore +4 -0
- package/CHANGELOG_GROUP_FEATURE.md +468 -0
- package/CHANGELOG_P0_FIXES.md +412 -0
- package/CHANGELOG_P1_OPTIMIZATIONS.md +292 -0
- package/CLAUDE.md +469 -0
- package/GROUP_FEATURE_DESIGN.md +520 -0
- package/README.md +106 -0
- package/lib/archive-utils.js +332 -0
- package/lib/const.js +19 -0
- package/lib/group-manager.js +660 -0
- package/lib/migration-v3.js +321 -0
- package/lib/server.js +333 -60
- package/lib/storage-adapter.js +518 -0
- package/lib/storage-v3.js +1368 -0
- package/lib/uiServer/index.js +76 -5
- package/lib/uiServer/router/group-router.js +218 -0
- package/lib/uiServer/router/index.js +1074 -51
- package/lib/uiServer/router/version-router.js +208 -63
- package/lib/uiServer/util.js +74 -16
- package/lib/uiServer/validator.js +105 -0
- package/lib/utils.js +107 -171
- package/package.json +1 -1
- package/public/js/app.js +5216 -1379
- package/public/js/app.js.map +1 -1
- package/public/js/chunk-vendors.js +14179 -8217
- package/public/js/chunk-vendors.js.map +1 -1
- package/rules.txt +1 -1
- package//346/212/200/346/234/257/346/226/271/346/241/210.md +452 -0
- package//346/265/213/350/257/225/346/215/225/350/216/267/345/212/237/350/203/275/346/255/245/351/252/244.md +145 -0
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
const {
|
|
2
2
|
updateFile,
|
|
3
|
-
removeFile,
|
|
4
|
-
writeFile,
|
|
5
3
|
readFile,
|
|
6
|
-
|
|
7
|
-
getProperty,
|
|
4
|
+
readSession,
|
|
8
5
|
getApiListUpdated,
|
|
9
6
|
setApiListUpdated,
|
|
10
7
|
} = require('../../utils')
|
|
@@ -14,23 +11,46 @@ const {
|
|
|
14
11
|
handleFilterList,
|
|
15
12
|
getFullDataList,
|
|
16
13
|
} = require('../util')
|
|
14
|
+
const {
|
|
15
|
+
validateFilename,
|
|
16
|
+
validateMockData,
|
|
17
|
+
validateBoolean,
|
|
18
|
+
validateRuleValue,
|
|
19
|
+
validate,
|
|
20
|
+
} = require('../validator')
|
|
17
21
|
const versionRouter = require('./version-router')
|
|
22
|
+
const groupRouter = require('./group-router')
|
|
23
|
+
const {
|
|
24
|
+
createTarGz,
|
|
25
|
+
extractTarGz,
|
|
26
|
+
parseMultipartFile,
|
|
27
|
+
sanitizeFilename,
|
|
28
|
+
} = require('../../archive-utils')
|
|
29
|
+
const fs = require('fs').promises
|
|
30
|
+
const path = require('path')
|
|
31
|
+
const os = require('os')
|
|
18
32
|
|
|
19
33
|
module.exports = (router) => {
|
|
20
34
|
// 文件版本路由
|
|
21
35
|
versionRouter(router)
|
|
36
|
+
// 组管理路由
|
|
37
|
+
groupRouter(router)
|
|
22
38
|
|
|
23
39
|
// 获取列表数据接口
|
|
24
40
|
router.post('/cgi-bin/mockbubu/api-list', async (ctx) => {
|
|
25
41
|
try {
|
|
26
|
-
const {
|
|
42
|
+
const { storageAdapter, groupManager } = ctx
|
|
27
43
|
|
|
44
|
+
// 获取当前激活的组ID
|
|
45
|
+
const currentGroupId = await groupManager.getCurrentGroupId()
|
|
46
|
+
|
|
47
|
+
// 从当前组读取数据
|
|
28
48
|
const filteredList = handleFilterList(
|
|
29
|
-
getFullDataList(
|
|
49
|
+
await getFullDataList(storageAdapter, groupManager, currentGroupId),
|
|
30
50
|
ctx.request.body,
|
|
31
51
|
)
|
|
32
52
|
|
|
33
|
-
setApiListUpdated(
|
|
53
|
+
await setApiListUpdated(storageAdapter, false)
|
|
34
54
|
ctx.body = createSuccessResponse(filteredList || [])
|
|
35
55
|
} catch (error) {
|
|
36
56
|
ctx.body = createErrorResponse(error.message)
|
|
@@ -38,10 +58,10 @@ module.exports = (router) => {
|
|
|
38
58
|
})
|
|
39
59
|
|
|
40
60
|
// 检测接口是否更新
|
|
41
|
-
router.get('/cgi-bin/mockbubu/check-api-list', (ctx) => {
|
|
61
|
+
router.get('/cgi-bin/mockbubu/check-api-list', async (ctx) => {
|
|
42
62
|
try {
|
|
43
|
-
const {
|
|
44
|
-
const updated = getApiListUpdated(
|
|
63
|
+
const { storageAdapter } = ctx
|
|
64
|
+
const updated = await getApiListUpdated(storageAdapter)
|
|
45
65
|
ctx.body = createSuccessResponse(updated)
|
|
46
66
|
} catch (error) {
|
|
47
67
|
ctx.body = createErrorResponse(error.message)
|
|
@@ -49,17 +69,23 @@ module.exports = (router) => {
|
|
|
49
69
|
})
|
|
50
70
|
|
|
51
71
|
// 更新文件详情接口
|
|
52
|
-
router.post('/cgi-bin/mockbubu/update-api-data', (
|
|
72
|
+
router.post('/cgi-bin/mockbubu/update-api-data', validate({
|
|
73
|
+
name: validateFilename,
|
|
74
|
+
data: validateMockData,
|
|
75
|
+
}), async (ctx) => {
|
|
53
76
|
try {
|
|
54
|
-
const {
|
|
77
|
+
const { storageAdapter } = ctx
|
|
78
|
+
const { groupManager } = ctx
|
|
55
79
|
const { name, data } = ctx.request.body
|
|
56
80
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
81
|
+
const currentGroupId = await groupManager.getCurrentGroupId()
|
|
82
|
+
|
|
83
|
+
updateFile(storageAdapter, name, data, currentGroupId)
|
|
84
|
+
const file = await readFile(storageAdapter, name, currentGroupId)
|
|
85
|
+
const config = await groupManager.getGroupFileConfig(currentGroupId, name)
|
|
60
86
|
|
|
61
87
|
ctx.body = createSuccessResponse({
|
|
62
|
-
...
|
|
88
|
+
...config,
|
|
63
89
|
name,
|
|
64
90
|
data: file,
|
|
65
91
|
})
|
|
@@ -69,18 +95,25 @@ module.exports = (router) => {
|
|
|
69
95
|
})
|
|
70
96
|
|
|
71
97
|
// 获取文件详情接口
|
|
72
|
-
router.post('/cgi-bin/mockbubu/get-api-data', (
|
|
98
|
+
router.post('/cgi-bin/mockbubu/get-api-data', validate({
|
|
99
|
+
name: validateFilename,
|
|
100
|
+
}), async (ctx) => {
|
|
73
101
|
try {
|
|
74
|
-
const {
|
|
102
|
+
const { storageAdapter } = ctx
|
|
103
|
+
const { groupManager } = ctx
|
|
75
104
|
const { name } = ctx.request.body
|
|
76
105
|
|
|
77
|
-
const
|
|
78
|
-
|
|
106
|
+
const currentGroupId = await groupManager.getCurrentGroupId()
|
|
107
|
+
|
|
108
|
+
const file = await readFile(storageAdapter, name, currentGroupId)
|
|
109
|
+
const session = await readSession(storageAdapter, name, currentGroupId)
|
|
110
|
+
const config = await groupManager.getGroupFileConfig(currentGroupId, name)
|
|
79
111
|
|
|
80
112
|
ctx.body = createSuccessResponse({
|
|
81
|
-
...
|
|
113
|
+
...config,
|
|
82
114
|
name,
|
|
83
115
|
data: file,
|
|
116
|
+
session, // 包含完整的 req 和 res 数据
|
|
84
117
|
})
|
|
85
118
|
} catch (error) {
|
|
86
119
|
ctx.body = createErrorResponse(error.message)
|
|
@@ -88,17 +121,50 @@ module.exports = (router) => {
|
|
|
88
121
|
})
|
|
89
122
|
|
|
90
123
|
// 新增mock接口
|
|
91
|
-
router.post('/cgi-bin/mockbubu/create-api-data', (
|
|
124
|
+
router.post('/cgi-bin/mockbubu/create-api-data', validate({
|
|
125
|
+
name: validateFilename,
|
|
126
|
+
content: validateMockData,
|
|
127
|
+
ruleValue: validateRuleValue,
|
|
128
|
+
}), async (ctx) => {
|
|
92
129
|
try {
|
|
93
|
-
const {
|
|
130
|
+
const { storageAdapter } = ctx
|
|
131
|
+
const { groupManager } = ctx
|
|
94
132
|
const { name, content, ruleValue } = ctx.request.body
|
|
95
133
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
134
|
+
// 获取当前组ID
|
|
135
|
+
const currentGroupId = await groupManager.getCurrentGroupId()
|
|
136
|
+
|
|
137
|
+
// 完全隔离架构:检查当前组的文件是否存在
|
|
138
|
+
const filePath = `${currentGroupId}/${name}`
|
|
139
|
+
const fileExists = !!await storageAdapter.readFile(filePath)
|
|
140
|
+
|
|
141
|
+
// 创建文件到当前组目录
|
|
142
|
+
if (!fileExists) {
|
|
143
|
+
const session = {
|
|
144
|
+
req: {
|
|
145
|
+
method: 'GET',
|
|
146
|
+
url: name,
|
|
147
|
+
headers: {},
|
|
148
|
+
},
|
|
149
|
+
res: {
|
|
150
|
+
statusCode: 200,
|
|
151
|
+
headers: { 'content-type': 'application/json' },
|
|
152
|
+
body: content, // content 是 JSON 字符串
|
|
153
|
+
},
|
|
154
|
+
}
|
|
155
|
+
await storageAdapter.writeFile(filePath, JSON.stringify(session))
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// 在当前组配置中添加记录
|
|
159
|
+
const groupConfig = await groupManager.getGroupFileConfig(currentGroupId, name)
|
|
160
|
+
groupConfig.ruleValue = ruleValue
|
|
161
|
+
groupConfig.mock = true
|
|
162
|
+
groupConfig.mockTime = Date.now()
|
|
163
|
+
groupConfig.url = name
|
|
164
|
+
groupConfig.method = 'GET'
|
|
165
|
+
groupConfig.date = Date.now()
|
|
166
|
+
groupConfig.status = 200
|
|
167
|
+
await groupManager.setGroupFileConfig(currentGroupId, name, groupConfig)
|
|
102
168
|
|
|
103
169
|
ctx.body = createSuccessResponse(null, '创建成功')
|
|
104
170
|
} catch (error) {
|
|
@@ -107,12 +173,24 @@ module.exports = (router) => {
|
|
|
107
173
|
})
|
|
108
174
|
|
|
109
175
|
// 修改接口mock开关
|
|
110
|
-
router.post('/cgi-bin/mockbubu/update-api-mock', (
|
|
176
|
+
router.post('/cgi-bin/mockbubu/update-api-mock', validate({
|
|
177
|
+
name: validateFilename,
|
|
178
|
+
mock: (val) => validateBoolean(val, 'mock'),
|
|
179
|
+
}), async (ctx) => {
|
|
111
180
|
try {
|
|
112
|
-
const {
|
|
181
|
+
const { groupManager } = ctx
|
|
113
182
|
const { name, mock } = ctx.request.body
|
|
114
183
|
|
|
115
|
-
|
|
184
|
+
// 获取当前组ID
|
|
185
|
+
const currentGroupId = await groupManager.getCurrentGroupId()
|
|
186
|
+
// 获取当前组配置
|
|
187
|
+
const groupConfig = await groupManager.getGroupFileConfig(currentGroupId, name)
|
|
188
|
+
// 更新 mock 状态
|
|
189
|
+
groupConfig.mock = mock
|
|
190
|
+
groupConfig.mockTime = Date.now()
|
|
191
|
+
// 写回组配置
|
|
192
|
+
await groupManager.setGroupFileConfig(currentGroupId, name, groupConfig)
|
|
193
|
+
|
|
116
194
|
ctx.body = createSuccessResponse(null, '更新成功')
|
|
117
195
|
} catch (error) {
|
|
118
196
|
ctx.body = createErrorResponse(error.message)
|
|
@@ -120,50 +198,995 @@ module.exports = (router) => {
|
|
|
120
198
|
})
|
|
121
199
|
|
|
122
200
|
// 修改接口lock开关
|
|
123
|
-
router.post('/cgi-bin/mockbubu/update-api-lock', (
|
|
201
|
+
router.post('/cgi-bin/mockbubu/update-api-lock', validate({
|
|
202
|
+
name: validateFilename,
|
|
203
|
+
locked: (val) => validateBoolean(val, 'locked'),
|
|
204
|
+
}), async (ctx) => {
|
|
124
205
|
try {
|
|
125
|
-
const {
|
|
206
|
+
const { groupManager } = ctx
|
|
126
207
|
const { name, locked } = ctx.request.body
|
|
127
208
|
|
|
128
|
-
|
|
209
|
+
// 获取当前组ID
|
|
210
|
+
const currentGroupId = await groupManager.getCurrentGroupId()
|
|
211
|
+
// 获取当前组配置
|
|
212
|
+
const groupConfig = await groupManager.getGroupFileConfig(currentGroupId, name)
|
|
213
|
+
// 更新 locked 状态
|
|
214
|
+
groupConfig.locked = locked
|
|
215
|
+
// 写回组配置
|
|
216
|
+
await groupManager.setGroupFileConfig(currentGroupId, name, groupConfig)
|
|
217
|
+
|
|
129
218
|
ctx.body = createSuccessResponse(null, '更新成功')
|
|
130
219
|
} catch (error) {
|
|
131
220
|
ctx.body = createErrorResponse(error.message)
|
|
132
221
|
}
|
|
133
222
|
})
|
|
134
223
|
|
|
135
|
-
//
|
|
136
|
-
router.post('/cgi-bin/mockbubu/delete-api', (
|
|
224
|
+
// 删除接口数据(智能删除)
|
|
225
|
+
router.post('/cgi-bin/mockbubu/delete-api', validate({
|
|
226
|
+
name: validateFilename,
|
|
227
|
+
}), async (ctx) => {
|
|
137
228
|
try {
|
|
138
|
-
const {
|
|
139
|
-
const {
|
|
229
|
+
const { storageAdapter } = ctx
|
|
230
|
+
const { groupManager } = ctx
|
|
231
|
+
const { name, groupId } = ctx.request.body
|
|
232
|
+
|
|
233
|
+
// 优先使用传入的 groupId,否则使用当前组ID
|
|
234
|
+
const targetGroupId = groupId || await groupManager.getCurrentGroupId()
|
|
235
|
+
|
|
236
|
+
// [mockbubu] 单文件删除日志 (delete-api)
|
|
237
|
+
console.log('[mockbubu] delete-api 开始', new Date().toLocaleString('zh-CN', { hour12: false }))
|
|
238
|
+
console.log('[mockbubu] 待删除文件:', name)
|
|
239
|
+
console.log('[mockbubu] 目标组ID:', targetGroupId)
|
|
240
|
+
|
|
241
|
+
// ✅ 检查文件是否被锁定
|
|
242
|
+
const fileConfig = await groupManager.getGroupFileConfig(targetGroupId, name)
|
|
243
|
+
if (fileConfig && fileConfig.locked) {
|
|
244
|
+
console.log('[mockbubu] delete-api 失败: 文件已锁定', name)
|
|
245
|
+
ctx.body = createErrorResponse('文件已锁定,无法删除')
|
|
246
|
+
return
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// 完全隔离架构:直接删除物理文件 + 组配置
|
|
250
|
+
// 注意:storageAdapter.removeFile 是 async 方法,必须 await
|
|
251
|
+
await storageAdapter.removeFile(`${targetGroupId}/${name}`)
|
|
252
|
+
await groupManager.removeGroupFileConfig(targetGroupId, name)
|
|
253
|
+
|
|
254
|
+
console.log('[mockbubu] delete-api 删除成功:', name)
|
|
140
255
|
|
|
141
|
-
removeFile(localStorage, name)
|
|
142
256
|
ctx.body = createSuccessResponse(null, '删除成功')
|
|
143
257
|
} catch (error) {
|
|
144
258
|
ctx.body = createErrorResponse(error.message)
|
|
145
259
|
}
|
|
146
260
|
})
|
|
147
261
|
|
|
148
|
-
//
|
|
149
|
-
router.post('/cgi-bin/mockbubu/batch-delete-api', (ctx) => {
|
|
262
|
+
// 批量删除接口(按范围)
|
|
263
|
+
router.post('/cgi-bin/mockbubu/batch-delete-api', async (ctx) => {
|
|
150
264
|
try {
|
|
151
|
-
const {
|
|
265
|
+
const { storageAdapter } = ctx
|
|
266
|
+
const { groupManager } = ctx
|
|
267
|
+
|
|
268
|
+
// 获取当前组ID
|
|
269
|
+
const currentGroupId = await groupManager.getCurrentGroupId()
|
|
270
|
+
|
|
271
|
+
const filteredList = handleFilterList(
|
|
272
|
+
await getFullDataList(storageAdapter, groupManager, currentGroupId),
|
|
273
|
+
{
|
|
274
|
+
...ctx.request.body,
|
|
275
|
+
locked: 'unlocked', // 锁定的文件不可批量删除
|
|
276
|
+
},
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
if (filteredList.length === 0) {
|
|
280
|
+
ctx.body = createErrorResponse('没有符合条件的文件', 400)
|
|
281
|
+
return
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// 完全隔离架构:直接批量删除
|
|
285
|
+
const result = {
|
|
286
|
+
total: filteredList.length,
|
|
287
|
+
success: 0,
|
|
288
|
+
failed: 0,
|
|
289
|
+
errors: [],
|
|
290
|
+
}
|
|
152
291
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
292
|
+
// [mockbubu] 批量删除日志
|
|
293
|
+
console.log('[mockbubu] 批量删除开始', new Date().toLocaleString('zh-CN', { hour12: false }))
|
|
294
|
+
console.log('[mockbubu] 待删除文件数量:', filteredList.length)
|
|
295
|
+
console.log('[mockbubu] 待删除文件列表:', filteredList.map(f => f.name))
|
|
296
|
+
|
|
297
|
+
for (const item of filteredList) {
|
|
298
|
+
try {
|
|
299
|
+
console.log('[mockbubu] 正在删除:', item.name)
|
|
300
|
+
// 直接删除物理文件 + 组配置
|
|
301
|
+
// 注意:storageAdapter.removeFile 是 async 方法,必须 await
|
|
302
|
+
await storageAdapter.removeFile(`${currentGroupId}/${item.name}`)
|
|
303
|
+
await groupManager.removeGroupFileConfig(currentGroupId, item.name)
|
|
304
|
+
result.success++
|
|
305
|
+
console.log('[mockbubu] 删除成功:', item.name)
|
|
306
|
+
} catch (error) {
|
|
307
|
+
result.failed++
|
|
308
|
+
result.errors.push({
|
|
309
|
+
name: item.name,
|
|
310
|
+
error: error.message,
|
|
311
|
+
})
|
|
312
|
+
console.error(`[mockbubu] 删除文件 ${item.name} 失败:`, error.message, error.stack)
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
console.log('[mockbubu] 批量删除结束', new Date().toLocaleString('zh-CN', { hour12: false }), {
|
|
317
|
+
total: result.total,
|
|
318
|
+
success: result.success,
|
|
319
|
+
failed: result.failed,
|
|
156
320
|
})
|
|
157
|
-
|
|
158
|
-
|
|
321
|
+
|
|
322
|
+
if (result.failed > 0) {
|
|
323
|
+
ctx.body = createSuccessResponse(
|
|
324
|
+
result,
|
|
325
|
+
`删除完成: 成功 ${result.success}/${result.total},失败 ${result.failed}`,
|
|
326
|
+
)
|
|
327
|
+
} else {
|
|
328
|
+
ctx.body = createSuccessResponse(
|
|
329
|
+
{ total: result.total, success: result.success },
|
|
330
|
+
`成功删除 ${result.success} 个文件`,
|
|
331
|
+
)
|
|
332
|
+
}
|
|
333
|
+
} catch (error) {
|
|
334
|
+
ctx.body = createErrorResponse(error.message)
|
|
335
|
+
}
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
// 批量删除接口(按名称数组) - 智能删除
|
|
340
|
+
router.post('/cgi-bin/mockbubu/batch-delete-by-names', validate({
|
|
341
|
+
names: (val) => {
|
|
342
|
+
if (!Array.isArray(val)) {
|
|
343
|
+
throw new Error('names 必须是数组')
|
|
344
|
+
}
|
|
345
|
+
if (val.length === 0) {
|
|
346
|
+
throw new Error('names 不能为空')
|
|
347
|
+
}
|
|
348
|
+
val.forEach(name => validateFilename(name))
|
|
349
|
+
return null
|
|
350
|
+
},
|
|
351
|
+
}), async (ctx) => {
|
|
352
|
+
try {
|
|
353
|
+
const { storageAdapter } = ctx
|
|
354
|
+
const { groupManager } = ctx
|
|
355
|
+
const { names } = ctx.request.body
|
|
356
|
+
|
|
357
|
+
// 获取当前组ID
|
|
358
|
+
const currentGroupId = await groupManager.getCurrentGroupId()
|
|
359
|
+
|
|
360
|
+
const result = {
|
|
361
|
+
total: names.length,
|
|
362
|
+
success: 0,
|
|
363
|
+
failed: 0,
|
|
364
|
+
locked: 0,
|
|
365
|
+
errors: [],
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// [mockbubu] 批量删除日志 (batch-delete-by-names)
|
|
369
|
+
console.log('[mockbubu] batch-delete-by-names 开始', new Date().toLocaleString('zh-CN', { hour12: false }))
|
|
370
|
+
console.log('[mockbubu] 待删除文件数量:', names.length)
|
|
371
|
+
console.log('[mockbubu] 待删除文件列表:', names)
|
|
372
|
+
|
|
373
|
+
for (const name of names) {
|
|
159
374
|
try {
|
|
160
|
-
|
|
375
|
+
console.log('[mockbubu] 正在处理:', name)
|
|
376
|
+
// 从当前组配置检查文件是否锁定
|
|
377
|
+
const groupConfig = await groupManager.getGroupFileConfig(currentGroupId, name)
|
|
378
|
+
if (groupConfig.locked) {
|
|
379
|
+
console.log('[mockbubu] 文件已锁定,跳过:', name)
|
|
380
|
+
result.locked++
|
|
381
|
+
result.errors.push({ name, error: '文件已锁定' })
|
|
382
|
+
continue
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// 完全隔离架构:直接删除物理文件 + 组配置
|
|
386
|
+
// 注意:storageAdapter.removeFile 是 async 方法,必须 await
|
|
387
|
+
await storageAdapter.removeFile(`${currentGroupId}/${name}`)
|
|
388
|
+
await groupManager.removeGroupFileConfig(currentGroupId, name)
|
|
389
|
+
|
|
390
|
+
result.success++
|
|
391
|
+
console.log('[mockbubu] 删除成功:', name)
|
|
161
392
|
} catch (error) {
|
|
162
|
-
|
|
393
|
+
result.failed++
|
|
394
|
+
result.errors.push({ name, error: error.message })
|
|
395
|
+
console.error(`[mockbubu] 删除文件 ${name} 失败:`, error.message, error.stack)
|
|
163
396
|
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
console.log('[mockbubu] batch-delete-by-names 结束', new Date().toLocaleString('zh-CN', { hour12: false }), {
|
|
400
|
+
total: result.total,
|
|
401
|
+
success: result.success,
|
|
402
|
+
failed: result.failed,
|
|
403
|
+
locked: result.locked,
|
|
164
404
|
})
|
|
165
405
|
|
|
166
|
-
|
|
406
|
+
// 返回结果
|
|
407
|
+
if (result.locked > 0 && result.success === 0) {
|
|
408
|
+
ctx.body = createErrorResponse('所有文件都已锁定,无法删除', 403)
|
|
409
|
+
return
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (result.failed > 0 || result.locked > 0) {
|
|
413
|
+
const messages = []
|
|
414
|
+
if (result.success > 0) {
|
|
415
|
+
messages.push(`成功 ${result.success}`)
|
|
416
|
+
}
|
|
417
|
+
if (result.locked > 0) {
|
|
418
|
+
messages.push(`锁定 ${result.locked}`)
|
|
419
|
+
}
|
|
420
|
+
if (result.failed > 0) {
|
|
421
|
+
messages.push(`失败 ${result.failed}`)
|
|
422
|
+
}
|
|
423
|
+
ctx.body = createSuccessResponse(
|
|
424
|
+
result,
|
|
425
|
+
`删除完成: ${messages.join(',')}`,
|
|
426
|
+
)
|
|
427
|
+
return
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
ctx.body = createSuccessResponse(
|
|
431
|
+
{ total: result.total, success: result.success },
|
|
432
|
+
`成功删除 ${result.success} 个文件`,
|
|
433
|
+
)
|
|
434
|
+
} catch (error) {
|
|
435
|
+
ctx.body = createErrorResponse(error.message)
|
|
436
|
+
}
|
|
437
|
+
})
|
|
438
|
+
|
|
439
|
+
// 批量更新 mock 状态
|
|
440
|
+
router.post('/cgi-bin/mockbubu/batch-update-mock', validate({
|
|
441
|
+
names: (val) => {
|
|
442
|
+
if (!Array.isArray(val)) {
|
|
443
|
+
throw new Error('names 必须是数组')
|
|
444
|
+
}
|
|
445
|
+
if (val.length === 0) {
|
|
446
|
+
throw new Error('names 不能为空')
|
|
447
|
+
}
|
|
448
|
+
val.forEach(name => validateFilename(name))
|
|
449
|
+
return null
|
|
450
|
+
},
|
|
451
|
+
mock: (val) => validateBoolean(val, 'mock'),
|
|
452
|
+
}), async (ctx) => {
|
|
453
|
+
try {
|
|
454
|
+
const { groupManager } = ctx
|
|
455
|
+
const { names, mock } = ctx.request.body
|
|
456
|
+
|
|
457
|
+
// 获取当前组ID
|
|
458
|
+
const currentGroupId = await groupManager.getCurrentGroupId()
|
|
459
|
+
const errors = []
|
|
460
|
+
const mockTime = Date.now()
|
|
461
|
+
|
|
462
|
+
for (const name of names) {
|
|
463
|
+
try {
|
|
464
|
+
// 获取当前组配置
|
|
465
|
+
const groupConfig = await groupManager.getGroupFileConfig(currentGroupId, name)
|
|
466
|
+
// 更新 mock 状态
|
|
467
|
+
groupConfig.mock = mock
|
|
468
|
+
groupConfig.mockTime = mockTime
|
|
469
|
+
// 写回组配置
|
|
470
|
+
await groupManager.setGroupFileConfig(currentGroupId, name, groupConfig)
|
|
471
|
+
} catch (error) {
|
|
472
|
+
errors.push({ name, error: error.message })
|
|
473
|
+
if (process.env.NODE_ENV === 'development') {
|
|
474
|
+
console.error(`[mockbubu] 更新文件 ${name} 的 mock 状态失败:`, error.message)
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
if (errors.length > 0) {
|
|
480
|
+
ctx.body = createSuccessResponse({ errors }, `更新完成,${errors.length} 个文件更新失败`)
|
|
481
|
+
return
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const action = mock ? '开启' : '关闭'
|
|
485
|
+
ctx.body = createSuccessResponse(null, `成功${action} ${names.length} 个文件的 Mock 状态`)
|
|
486
|
+
} catch (error) {
|
|
487
|
+
ctx.body = createErrorResponse(error.message)
|
|
488
|
+
}
|
|
489
|
+
})
|
|
490
|
+
|
|
491
|
+
// ==================== 压缩包导出/导入功能 ====================
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* 导出当前组为压缩包(仅导出含版本的文件)
|
|
495
|
+
* POST /cgi-bin/mockbubu/export-group-archive
|
|
496
|
+
*/
|
|
497
|
+
router.post('/cgi-bin/mockbubu/export-group-archive', async (ctx) => {
|
|
498
|
+
console.log('[mockbubu 2025/11/19 16:30:00] 开始导出压缩包')
|
|
499
|
+
|
|
500
|
+
try {
|
|
501
|
+
const { groupManager, storageAdapter } = ctx
|
|
502
|
+
const currentGroupId = await groupManager.getCurrentGroupId()
|
|
503
|
+
const currentGroup = await groupManager.getCurrentGroup()
|
|
504
|
+
const v3Storage = storageAdapter.v3Storage
|
|
505
|
+
|
|
506
|
+
console.log(`[mockbubu 2025/11/19 16:30:00] 当前组: ${currentGroup.name} (${currentGroupId})`)
|
|
507
|
+
|
|
508
|
+
// 1. 读取索引
|
|
509
|
+
const index = await v3Storage.getIndex(currentGroupId)
|
|
510
|
+
console.log(`[mockbubu 2025/11/19 16:30:00] 索引文件总数: ${index.files.length}`)
|
|
511
|
+
|
|
512
|
+
// 2. 筛选含版本的文件
|
|
513
|
+
const filesWithVersions = []
|
|
514
|
+
for (const fileEntry of index.files) {
|
|
515
|
+
const versions = await v3Storage.listVersions(currentGroupId, fileEntry.id)
|
|
516
|
+
if (versions.length > 0) {
|
|
517
|
+
filesWithVersions.push({
|
|
518
|
+
...fileEntry,
|
|
519
|
+
versionCount: versions.length,
|
|
520
|
+
})
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
console.log(`[mockbubu 2025/11/19 16:30:00] 含版本的文件数: ${filesWithVersions.length}`)
|
|
525
|
+
|
|
526
|
+
if (filesWithVersions.length === 0) {
|
|
527
|
+
ctx.body = createErrorResponse('当前组没有包含版本的文件,无法导出')
|
|
528
|
+
return
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// 3. 创建临时目录
|
|
532
|
+
const tmpDir = path.join(os.tmpdir(), `mockbubu-export-${Date.now()}-${Math.random().toString(36).slice(2)}`)
|
|
533
|
+
const exportDir = path.join(tmpDir, 'export')
|
|
534
|
+
const filesDir = path.join(exportDir, 'files')
|
|
535
|
+
await fs.mkdir(filesDir, { recursive: true })
|
|
536
|
+
|
|
537
|
+
console.log(`[mockbubu 2025/11/19 16:30:00] 临时目录: ${tmpDir}`)
|
|
538
|
+
|
|
539
|
+
try {
|
|
540
|
+
// 4. 复制文件
|
|
541
|
+
for (const fileEntry of filesWithVersions) {
|
|
542
|
+
const sourceFileDir = path.join(v3Storage.groupsDir, currentGroupId, 'files', fileEntry.id)
|
|
543
|
+
const targetFileDir = path.join(filesDir, fileEntry.id)
|
|
544
|
+
|
|
545
|
+
// 递归复制文件目录(包括 file.json 和 versions/)
|
|
546
|
+
await v3Storage._copyDirectory(sourceFileDir, targetFileDir)
|
|
547
|
+
console.log(`[mockbubu 2025/11/19 16:30:00] 已复制文件: ${fileEntry.url} (${fileEntry.id})`)
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// 5. 创建导出元数据
|
|
551
|
+
const exportMeta = {
|
|
552
|
+
exportTime: new Date().toISOString(),
|
|
553
|
+
exportVersion: '3.0.0',
|
|
554
|
+
groupName: currentGroup.name,
|
|
555
|
+
groupDescription: currentGroup.description || '',
|
|
556
|
+
fileCount: filesWithVersions.length,
|
|
557
|
+
files: filesWithVersions.map(f => ({
|
|
558
|
+
id: f.id,
|
|
559
|
+
url: f.url,
|
|
560
|
+
method: f.method,
|
|
561
|
+
versionCount: f.versionCount,
|
|
562
|
+
})),
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
await fs.writeFile(
|
|
566
|
+
path.join(exportDir, 'export-meta.json'),
|
|
567
|
+
JSON.stringify(exportMeta, null, 2),
|
|
568
|
+
'utf8',
|
|
569
|
+
)
|
|
570
|
+
|
|
571
|
+
console.log('[mockbubu 2025/11/19 16:30:00] 元数据已写入')
|
|
572
|
+
|
|
573
|
+
// 6. 创建压缩包
|
|
574
|
+
const archiveName = sanitizeFilename(`${currentGroup.name}-${Date.now()}.tar.gz`)
|
|
575
|
+
const archivePath = path.join(tmpDir, archiveName)
|
|
576
|
+
|
|
577
|
+
await createTarGz(exportDir, archivePath)
|
|
578
|
+
console.log(`[mockbubu 2025/11/19 16:30:00] 压缩包已创建: ${archivePath}`)
|
|
579
|
+
|
|
580
|
+
// 7. 读取压缩包内容
|
|
581
|
+
const archiveBuffer = await fs.readFile(archivePath)
|
|
582
|
+
console.log(`[mockbubu 2025/11/19 16:30:00] 压缩包大小: ${(archiveBuffer.length / 1024).toFixed(2)} KB`)
|
|
583
|
+
|
|
584
|
+
// 8. 返回压缩包
|
|
585
|
+
ctx.set('Content-Type', 'application/gzip')
|
|
586
|
+
ctx.set('Content-Disposition', `attachment; filename="${encodeURIComponent(archiveName)}"`)
|
|
587
|
+
ctx.body = archiveBuffer
|
|
588
|
+
|
|
589
|
+
// 9. 清理临时目录(异步执行,不阻塞响应)
|
|
590
|
+
fs.rm(tmpDir, { recursive: true, force: true }).catch(err => {
|
|
591
|
+
console.error(`[mockbubu 2025/11/19 16:30:00] 清理临时目录失败: ${err.message}`)
|
|
592
|
+
})
|
|
593
|
+
|
|
594
|
+
console.log('[mockbubu 2025/11/19 16:30:00] 导出完成')
|
|
595
|
+
} catch (err) {
|
|
596
|
+
// 出错时清理临时目录
|
|
597
|
+
await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {})
|
|
598
|
+
throw err
|
|
599
|
+
}
|
|
600
|
+
} catch (error) {
|
|
601
|
+
console.error('[mockbubu 2025/11/19 16:30:00] 导出失败:', error.message)
|
|
602
|
+
ctx.set('Content-Type', 'application/json')
|
|
603
|
+
ctx.body = createErrorResponse(`导出失败: ${error.message}`)
|
|
604
|
+
}
|
|
605
|
+
})
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* 导入压缩包到新组
|
|
609
|
+
* POST /cgi-bin/mockbubu/import-group-archive
|
|
610
|
+
* Content-Type: multipart/form-data
|
|
611
|
+
*/
|
|
612
|
+
router.post('/cgi-bin/mockbubu/import-group-archive', async (ctx) => {
|
|
613
|
+
console.log('[mockbubu 2025/11/19 16:30:00] 开始导入压缩包')
|
|
614
|
+
|
|
615
|
+
try {
|
|
616
|
+
const { storageAdapter } = ctx
|
|
617
|
+
const v3Storage = storageAdapter.v3Storage
|
|
618
|
+
|
|
619
|
+
// 1. 解析上传的文件
|
|
620
|
+
const file = await parseMultipartFile(ctx)
|
|
621
|
+
if (!file || !file.buffer) {
|
|
622
|
+
ctx.body = createErrorResponse('未检测到上传文件')
|
|
623
|
+
return
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
console.log(`[mockbubu 2025/11/19 16:30:00] 上传文件: ${file.filename}, 大小: ${(file.buffer.length / 1024).toFixed(2)} KB`)
|
|
627
|
+
|
|
628
|
+
// 2. 创建临时目录
|
|
629
|
+
const tmpDir = path.join(os.tmpdir(), `mockbubu-import-${Date.now()}-${Math.random().toString(36).slice(2)}`)
|
|
630
|
+
const extractDir = path.join(tmpDir, 'extract')
|
|
631
|
+
await fs.mkdir(extractDir, { recursive: true })
|
|
632
|
+
|
|
633
|
+
console.log(`[mockbubu 2025/11/19 16:30:00] 临时目录: ${tmpDir}`)
|
|
634
|
+
|
|
635
|
+
try {
|
|
636
|
+
// 3. 保存上传的压缩包
|
|
637
|
+
const uploadPath = path.join(tmpDir, 'upload.tar.gz')
|
|
638
|
+
await fs.writeFile(uploadPath, file.buffer)
|
|
639
|
+
|
|
640
|
+
// 4. 解压
|
|
641
|
+
await extractTarGz(uploadPath, extractDir)
|
|
642
|
+
console.log('[mockbubu 2025/11/19 16:30:00] 解压完成')
|
|
643
|
+
|
|
644
|
+
// 5. 读取导出元数据
|
|
645
|
+
const exportMetaPath = path.join(extractDir, 'export-meta.json')
|
|
646
|
+
const exportMetaContent = await fs.readFile(exportMetaPath, 'utf8')
|
|
647
|
+
const exportMeta = JSON.parse(exportMetaContent)
|
|
648
|
+
|
|
649
|
+
console.log(`[mockbubu 2025/11/19 16:30:00] 导出元数据: 组名=${exportMeta.groupName}, 文件数=${exportMeta.fileCount}`)
|
|
650
|
+
|
|
651
|
+
// 6. 生成新组名(处理冲突)
|
|
652
|
+
let newGroupName = exportMeta.groupName
|
|
653
|
+
let suffix = 1
|
|
654
|
+
const allGroups = await v3Storage.listGroups()
|
|
655
|
+
const existingNames = allGroups.map(g => g.name)
|
|
656
|
+
|
|
657
|
+
while (existingNames.includes(newGroupName)) {
|
|
658
|
+
newGroupName = `${exportMeta.groupName} (${suffix})`
|
|
659
|
+
suffix++
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
console.log(`[mockbubu 2025/11/19 16:30:00] 新组名: ${newGroupName}`)
|
|
663
|
+
|
|
664
|
+
// 7. 生成新组ID(确保唯一)
|
|
665
|
+
let newGroupId = newGroupName.toLowerCase().replace(/[^a-z0-9]/g, '-')
|
|
666
|
+
const existingIds = allGroups.map(g => g.id)
|
|
667
|
+
|
|
668
|
+
if (existingIds.includes(newGroupId)) {
|
|
669
|
+
newGroupId = `${newGroupId}-${Date.now()}`
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
console.log(`[mockbubu 2025/11/19 16:30:00] 新组ID: ${newGroupId}`)
|
|
673
|
+
|
|
674
|
+
// 8. 创建新组
|
|
675
|
+
await v3Storage.createGroup({
|
|
676
|
+
id: newGroupId,
|
|
677
|
+
name: newGroupName,
|
|
678
|
+
description: exportMeta.groupDescription || `从【${exportMeta.groupName}】导入`,
|
|
679
|
+
})
|
|
680
|
+
|
|
681
|
+
console.log('[mockbubu 2025/11/19 16:30:00] 新组已创建')
|
|
682
|
+
|
|
683
|
+
// 9. 复制文件到新组
|
|
684
|
+
const sourceFilesDir = path.join(extractDir, 'files')
|
|
685
|
+
const targetFilesDir = path.join(v3Storage.groupsDir, newGroupId, 'files')
|
|
686
|
+
await fs.mkdir(targetFilesDir, { recursive: true })
|
|
687
|
+
|
|
688
|
+
const importedFiles = []
|
|
689
|
+
for (const fileMeta of exportMeta.files) {
|
|
690
|
+
const sourceFileDir = path.join(sourceFilesDir, fileMeta.id)
|
|
691
|
+
const targetFileDir = path.join(targetFilesDir, fileMeta.id)
|
|
692
|
+
|
|
693
|
+
// 检查源文件是否存在
|
|
694
|
+
try {
|
|
695
|
+
await fs.access(sourceFileDir)
|
|
696
|
+
await v3Storage._copyDirectory(sourceFileDir, targetFileDir)
|
|
697
|
+
|
|
698
|
+
importedFiles.push(fileMeta)
|
|
699
|
+
console.log(`[mockbubu 2025/11/19 16:30:00] 已导入文件: ${fileMeta.url} (${fileMeta.id})`)
|
|
700
|
+
} catch (err) {
|
|
701
|
+
console.error(`[mockbubu 2025/11/19 16:30:00] 导入文件失败: ${fileMeta.id}`, err.message)
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// 10. 重建索引
|
|
706
|
+
await v3Storage.rebuildIndex(newGroupId)
|
|
707
|
+
console.log('[mockbubu 2025/11/19 16:30:00] 索引已重建')
|
|
708
|
+
|
|
709
|
+
// 11. 清理临时目录
|
|
710
|
+
await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {})
|
|
711
|
+
|
|
712
|
+
console.log('[mockbubu 2025/11/19 16:30:00] 导入完成')
|
|
713
|
+
|
|
714
|
+
ctx.body = createSuccessResponse(
|
|
715
|
+
{
|
|
716
|
+
groupId: newGroupId,
|
|
717
|
+
groupName: newGroupName,
|
|
718
|
+
fileCount: importedFiles.length,
|
|
719
|
+
},
|
|
720
|
+
`成功导入到新组【${newGroupName}】,共 ${importedFiles.length} 个文件`,
|
|
721
|
+
)
|
|
722
|
+
} catch (err) {
|
|
723
|
+
// 出错时清理临时目录
|
|
724
|
+
await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {})
|
|
725
|
+
throw err
|
|
726
|
+
}
|
|
727
|
+
} catch (error) {
|
|
728
|
+
console.error('[mockbubu 2025/11/19 16:30:00] 导入失败:', error.message)
|
|
729
|
+
ctx.body = createErrorResponse(`导入失败: ${error.message}`)
|
|
730
|
+
}
|
|
731
|
+
})
|
|
732
|
+
|
|
733
|
+
// ==================== 存储管理功能 ====================
|
|
734
|
+
|
|
735
|
+
// 获取存储统计信息
|
|
736
|
+
router.get('/cgi-bin/mockbubu/storage-stats', async (ctx) => {
|
|
737
|
+
try {
|
|
738
|
+
const { storageAdapter } = ctx
|
|
739
|
+
const { groupManager } = ctx
|
|
740
|
+
|
|
741
|
+
// 获取所有物理文件
|
|
742
|
+
const allFiles = storageAdapter.getFileList()
|
|
743
|
+
const groups = groupManager.getGroups()
|
|
744
|
+
|
|
745
|
+
// 初始化统计数据
|
|
746
|
+
let totalSize = 0
|
|
747
|
+
let originalDataSize = 0
|
|
748
|
+
let versionDataSize = 0
|
|
749
|
+
|
|
750
|
+
const stats = {
|
|
751
|
+
mockEnabled: 0,
|
|
752
|
+
mockDisabled: 0,
|
|
753
|
+
locked: 0,
|
|
754
|
+
orphanFiles: 0,
|
|
755
|
+
unusedFiles: 0,
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
const groupsFileCount = {}
|
|
759
|
+
for (const group of groups) {
|
|
760
|
+
groupsFileCount[group.id] = 0
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// 遍历所有文件统计
|
|
764
|
+
for (const item of allFiles) {
|
|
765
|
+
const filename = item.name
|
|
766
|
+
let hasConfig = false
|
|
767
|
+
|
|
768
|
+
// 读取文件大小
|
|
769
|
+
const file = await storageAdapter.readFile(filename)
|
|
770
|
+
if (file) {
|
|
771
|
+
const fileSize = Buffer.byteLength(file, 'utf8')
|
|
772
|
+
totalSize += fileSize
|
|
773
|
+
originalDataSize += fileSize
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// 检查各组配置
|
|
777
|
+
for (const group of groups) {
|
|
778
|
+
// 使用 hasGroupConfig 直接检查配置是否存在
|
|
779
|
+
const hasActualConfig = await groupManager.hasGroupConfig(group.id, filename)
|
|
780
|
+
|
|
781
|
+
if (hasActualConfig) {
|
|
782
|
+
hasConfig = true
|
|
783
|
+
groupsFileCount[group.id]++
|
|
784
|
+
|
|
785
|
+
// 获取配置详情用于统计
|
|
786
|
+
const config = await groupManager.getGroupFileConfig(group.id, filename)
|
|
787
|
+
|
|
788
|
+
// 统计 mock 状态
|
|
789
|
+
if (config.mock) {
|
|
790
|
+
stats.mockEnabled++
|
|
791
|
+
} else {
|
|
792
|
+
stats.mockDisabled++
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// 统计锁定状态
|
|
796
|
+
if (config.locked) {
|
|
797
|
+
stats.locked++
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// 统计版本数据大小
|
|
801
|
+
const versions = await groupManager.getGroupVersions(group.id, filename)
|
|
802
|
+
for (const version of versions) {
|
|
803
|
+
if (version.content) {
|
|
804
|
+
const versionSize = Buffer.byteLength(JSON.stringify(version.content), 'utf8')
|
|
805
|
+
versionDataSize += versionSize
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// 统计孤儿文件
|
|
812
|
+
if (!hasConfig) {
|
|
813
|
+
stats.orphanFiles++
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// 统计未使用文件
|
|
817
|
+
if (hasConfig) {
|
|
818
|
+
let allMockDisabled = true
|
|
819
|
+
let allLocked = false
|
|
820
|
+
let hasCustomVersions = false
|
|
821
|
+
|
|
822
|
+
for (const group of groups) {
|
|
823
|
+
if (await groupManager.hasGroupConfig(group.id, filename)) {
|
|
824
|
+
const config = await groupManager.getGroupFileConfig(group.id, filename)
|
|
825
|
+
|
|
826
|
+
if (config.mock) {
|
|
827
|
+
allMockDisabled = false
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
if (config.locked) {
|
|
831
|
+
allLocked = true
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
const versions = await groupManager.getGroupVersions(group.id, filename)
|
|
835
|
+
const customVersions = versions.filter(v => v.filename !== 'source')
|
|
836
|
+
if (customVersions.length > 0) {
|
|
837
|
+
hasCustomVersions = true
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// 未使用文件:所有组未开启mock + 所有组未锁定 + 所有组无自定义版本
|
|
843
|
+
if (allMockDisabled && !allLocked && !hasCustomVersions) {
|
|
844
|
+
stats.unusedFiles++
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// 构造响应数据
|
|
850
|
+
const groupsData = groups.map(group => ({
|
|
851
|
+
id: group.id,
|
|
852
|
+
name: group.name,
|
|
853
|
+
fileCount: groupsFileCount[group.id],
|
|
854
|
+
}))
|
|
855
|
+
|
|
856
|
+
ctx.body = createSuccessResponse({
|
|
857
|
+
totalFiles: allFiles.length,
|
|
858
|
+
totalSize,
|
|
859
|
+
originalDataSize,
|
|
860
|
+
versionDataSize,
|
|
861
|
+
groups: groupsData,
|
|
862
|
+
stats,
|
|
863
|
+
})
|
|
864
|
+
} catch (error) {
|
|
865
|
+
ctx.body = createErrorResponse(error.message)
|
|
866
|
+
}
|
|
867
|
+
})
|
|
868
|
+
|
|
869
|
+
// 预览孤儿文件
|
|
870
|
+
router.get('/cgi-bin/mockbubu/preview-orphan-files', async (ctx) => {
|
|
871
|
+
try {
|
|
872
|
+
const { storageAdapter } = ctx
|
|
873
|
+
const { groupManager } = ctx
|
|
874
|
+
|
|
875
|
+
const allFiles = storageAdapter.getFileList()
|
|
876
|
+
const orphanFiles = []
|
|
877
|
+
let totalSize = 0
|
|
878
|
+
|
|
879
|
+
for (const item of allFiles) {
|
|
880
|
+
try {
|
|
881
|
+
// 解码文件名: {index}.{encodedName} -> groupId/filename
|
|
882
|
+
const decodedName = decodeURIComponent(item.name)
|
|
883
|
+
|
|
884
|
+
// 提取 groupId 和纯文件名
|
|
885
|
+
const slashIndex = decodedName.indexOf('/')
|
|
886
|
+
if (slashIndex === -1) {
|
|
887
|
+
// 文件名格式不正确,跳过
|
|
888
|
+
continue
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
const groupId = decodedName.substring(0, slashIndex)
|
|
892
|
+
const pureFilename = decodedName.substring(slashIndex + 1)
|
|
893
|
+
|
|
894
|
+
// 检查该组是否有配置
|
|
895
|
+
const hasConfig = await groupManager.hasGroupConfig(groupId, pureFilename)
|
|
896
|
+
|
|
897
|
+
// 是孤儿文件:物理文件存在但没有组配置
|
|
898
|
+
if (!hasConfig) {
|
|
899
|
+
const file = await storageAdapter.readFile(item.name)
|
|
900
|
+
const fileSize = file ? Buffer.byteLength(file, 'utf8') : 0
|
|
901
|
+
|
|
902
|
+
orphanFiles.push({
|
|
903
|
+
name: item.name, // 原始编码文件名
|
|
904
|
+
pureFilename, // 纯文件名
|
|
905
|
+
groupId, // 所属组
|
|
906
|
+
url: pureFilename, // URL 就是纯文件名
|
|
907
|
+
size: fileSize,
|
|
908
|
+
})
|
|
909
|
+
|
|
910
|
+
totalSize += fileSize
|
|
911
|
+
}
|
|
912
|
+
} catch (err) {
|
|
913
|
+
console.warn(`[mockbubu] 处理文件 ${item.name} 失败:`, err.message)
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
ctx.body = createSuccessResponse({
|
|
918
|
+
files: orphanFiles,
|
|
919
|
+
totalSize,
|
|
920
|
+
count: orphanFiles.length,
|
|
921
|
+
})
|
|
922
|
+
} catch (error) {
|
|
923
|
+
ctx.body = createErrorResponse(error.message)
|
|
924
|
+
}
|
|
925
|
+
})
|
|
926
|
+
|
|
927
|
+
// 清理孤儿文件
|
|
928
|
+
router.post('/cgi-bin/mockbubu/cleanup-orphan-files', async (ctx) => {
|
|
929
|
+
try {
|
|
930
|
+
const { storageAdapter } = ctx
|
|
931
|
+
const { groupManager } = ctx
|
|
932
|
+
|
|
933
|
+
const allFiles = storageAdapter.getFileList()
|
|
934
|
+
let deleted = 0
|
|
935
|
+
let freedSpace = 0
|
|
936
|
+
|
|
937
|
+
for (const item of allFiles) {
|
|
938
|
+
try {
|
|
939
|
+
// 解码文件名: {index}.{encodedName} -> groupId/filename
|
|
940
|
+
const decodedName = decodeURIComponent(item.name)
|
|
941
|
+
|
|
942
|
+
// 提取 groupId 和纯文件名
|
|
943
|
+
const slashIndex = decodedName.indexOf('/')
|
|
944
|
+
if (slashIndex === -1) {
|
|
945
|
+
// 文件名格式不正确,跳过
|
|
946
|
+
continue
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
const groupId = decodedName.substring(0, slashIndex)
|
|
950
|
+
const pureFilename = decodedName.substring(slashIndex + 1)
|
|
951
|
+
|
|
952
|
+
// 检查该组是否有配置
|
|
953
|
+
const hasConfig = await groupManager.hasGroupConfig(groupId, pureFilename)
|
|
954
|
+
|
|
955
|
+
// 删除孤儿文件:物理文件存在但没有组配置
|
|
956
|
+
if (!hasConfig) {
|
|
957
|
+
const file = await storageAdapter.readFile(item.name)
|
|
958
|
+
const fileSize = file ? Buffer.byteLength(file, 'utf8') : 0
|
|
959
|
+
|
|
960
|
+
// 删除物理文件(使用原始编码文件名)
|
|
961
|
+
await storageAdapter.removeFile(item.name)
|
|
962
|
+
|
|
963
|
+
deleted++
|
|
964
|
+
freedSpace += fileSize
|
|
965
|
+
}
|
|
966
|
+
} catch (err) {
|
|
967
|
+
console.warn(`[mockbubu] 处理文件 ${item.name} 失败:`, err.message)
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
const freedSpaceMB = (freedSpace / (1024 * 1024)).toFixed(2)
|
|
972
|
+
ctx.body = createSuccessResponse({
|
|
973
|
+
deleted,
|
|
974
|
+
freedSpace,
|
|
975
|
+
}, `成功清理 ${deleted} 个孤儿文件,释放 ${freedSpaceMB} MB 存储空间`)
|
|
976
|
+
} catch (error) {
|
|
977
|
+
ctx.body = createErrorResponse(error.message)
|
|
978
|
+
}
|
|
979
|
+
})
|
|
980
|
+
|
|
981
|
+
// 预览未使用文件
|
|
982
|
+
router.get('/cgi-bin/mockbubu/preview-unused-files', async (ctx) => {
|
|
983
|
+
try {
|
|
984
|
+
const { storageAdapter } = ctx
|
|
985
|
+
const { groupManager } = ctx
|
|
986
|
+
|
|
987
|
+
const allFiles = storageAdapter.getFileList()
|
|
988
|
+
const unusedFiles = []
|
|
989
|
+
let totalSize = 0
|
|
990
|
+
|
|
991
|
+
for (const item of allFiles) {
|
|
992
|
+
try {
|
|
993
|
+
// 解码文件名: {index}.{encodedName} -> groupId/filename
|
|
994
|
+
const decodedName = decodeURIComponent(item.name)
|
|
995
|
+
|
|
996
|
+
// 提取 groupId 和纯文件名
|
|
997
|
+
const slashIndex = decodedName.indexOf('/')
|
|
998
|
+
if (slashIndex === -1) {
|
|
999
|
+
continue
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
const groupId = decodedName.substring(0, slashIndex)
|
|
1003
|
+
const pureFilename = decodedName.substring(slashIndex + 1)
|
|
1004
|
+
|
|
1005
|
+
// 检查该文件所属的组是否有配置
|
|
1006
|
+
if (!await groupManager.hasGroupConfig(groupId, pureFilename)) {
|
|
1007
|
+
// 没有配置,是孤儿文件,不是未使用文件
|
|
1008
|
+
continue
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
const config = await groupManager.getGroupFileConfig(groupId, pureFilename)
|
|
1012
|
+
|
|
1013
|
+
// 检查是否为未使用文件:未开启mock + 未锁定 + 无自定义版本
|
|
1014
|
+
const isMockDisabled = !config.mock
|
|
1015
|
+
const isNotLocked = !config.locked
|
|
1016
|
+
const versions = await groupManager.getGroupVersions(groupId, pureFilename)
|
|
1017
|
+
const customVersions = versions.filter(v => v.filename !== 'source')
|
|
1018
|
+
const hasNoCustomVersions = customVersions.length === 0
|
|
1019
|
+
|
|
1020
|
+
if (isMockDisabled && isNotLocked && hasNoCustomVersions) {
|
|
1021
|
+
const file = await storageAdapter.readFile(item.name)
|
|
1022
|
+
const fileSize = file ? Buffer.byteLength(file, 'utf8') : 0
|
|
1023
|
+
|
|
1024
|
+
unusedFiles.push({
|
|
1025
|
+
name: item.name, // 原始编码文件名
|
|
1026
|
+
pureFilename, // 纯文件名
|
|
1027
|
+
groupId, // 所属组
|
|
1028
|
+
url: pureFilename,
|
|
1029
|
+
size: fileSize,
|
|
1030
|
+
mock: config.mock,
|
|
1031
|
+
locked: config.locked,
|
|
1032
|
+
})
|
|
1033
|
+
|
|
1034
|
+
totalSize += fileSize
|
|
1035
|
+
}
|
|
1036
|
+
} catch (err) {
|
|
1037
|
+
console.warn(`[mockbubu] 处理文件 ${item.name} 失败:`, err.message)
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
ctx.body = createSuccessResponse({
|
|
1042
|
+
files: unusedFiles,
|
|
1043
|
+
totalSize,
|
|
1044
|
+
count: unusedFiles.length,
|
|
1045
|
+
})
|
|
1046
|
+
} catch (error) {
|
|
1047
|
+
ctx.body = createErrorResponse(error.message)
|
|
1048
|
+
}
|
|
1049
|
+
})
|
|
1050
|
+
|
|
1051
|
+
// 清理未使用文件
|
|
1052
|
+
router.post('/cgi-bin/mockbubu/cleanup-unused-files', validate({
|
|
1053
|
+
confirmation: (val) => {
|
|
1054
|
+
if (val !== 'DELETE') {
|
|
1055
|
+
throw new Error('必须输入 DELETE 确认删除操作')
|
|
1056
|
+
}
|
|
1057
|
+
return null
|
|
1058
|
+
},
|
|
1059
|
+
}), async (ctx) => {
|
|
1060
|
+
try {
|
|
1061
|
+
const { storageAdapter } = ctx
|
|
1062
|
+
const { groupManager } = ctx
|
|
1063
|
+
|
|
1064
|
+
const allFiles = storageAdapter.getFileList()
|
|
1065
|
+
let deleted = 0
|
|
1066
|
+
let freedSpace = 0
|
|
1067
|
+
|
|
1068
|
+
for (const item of allFiles) {
|
|
1069
|
+
try {
|
|
1070
|
+
// 解码文件名: {index}.{encodedName} -> groupId/filename
|
|
1071
|
+
const decodedName = decodeURIComponent(item.name)
|
|
1072
|
+
|
|
1073
|
+
// 提取 groupId 和纯文件名
|
|
1074
|
+
const slashIndex = decodedName.indexOf('/')
|
|
1075
|
+
if (slashIndex === -1) {
|
|
1076
|
+
continue
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
const groupId = decodedName.substring(0, slashIndex)
|
|
1080
|
+
const pureFilename = decodedName.substring(slashIndex + 1)
|
|
1081
|
+
|
|
1082
|
+
// 检查该文件所属的组是否有配置
|
|
1083
|
+
if (!await groupManager.hasGroupConfig(groupId, pureFilename)) {
|
|
1084
|
+
// 没有配置,是孤儿文件,不是未使用文件
|
|
1085
|
+
continue
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
const config = await groupManager.getGroupFileConfig(groupId, pureFilename)
|
|
1089
|
+
|
|
1090
|
+
// 检查是否为未使用文件:未开启mock + 未锁定 + 无自定义版本
|
|
1091
|
+
const isMockDisabled = !config.mock
|
|
1092
|
+
const isNotLocked = !config.locked
|
|
1093
|
+
const versions = await groupManager.getGroupVersions(groupId, pureFilename)
|
|
1094
|
+
const customVersions = versions.filter(v => v.filename !== 'source')
|
|
1095
|
+
const hasNoCustomVersions = customVersions.length === 0
|
|
1096
|
+
|
|
1097
|
+
if (isMockDisabled && isNotLocked && hasNoCustomVersions) {
|
|
1098
|
+
const file = await storageAdapter.readFile(item.name)
|
|
1099
|
+
const fileSize = file ? Buffer.byteLength(file, 'utf8') : 0
|
|
1100
|
+
|
|
1101
|
+
// 删除物理文件(使用原始编码文件名)
|
|
1102
|
+
await storageAdapter.removeFile(item.name)
|
|
1103
|
+
|
|
1104
|
+
// 删除该组的配置(Layer 3)
|
|
1105
|
+
await groupManager.removeGroupFileConfig(groupId, pureFilename)
|
|
1106
|
+
|
|
1107
|
+
deleted++
|
|
1108
|
+
freedSpace += fileSize
|
|
1109
|
+
}
|
|
1110
|
+
} catch (err) {
|
|
1111
|
+
console.warn(`[mockbubu] 处理文件 ${item.name} 失败:`, err.message)
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
const freedSpaceMB = (freedSpace / (1024 * 1024)).toFixed(2)
|
|
1116
|
+
ctx.body = createSuccessResponse({
|
|
1117
|
+
deleted,
|
|
1118
|
+
freedSpace,
|
|
1119
|
+
}, `成功清理 ${deleted} 个未使用文件,释放 ${freedSpaceMB} MB 存储空间`)
|
|
1120
|
+
} catch (error) {
|
|
1121
|
+
ctx.body = createErrorResponse(error.message)
|
|
1122
|
+
}
|
|
1123
|
+
})
|
|
1124
|
+
|
|
1125
|
+
// 内存监控接口
|
|
1126
|
+
router.get('/cgi-bin/mockbubu/memory-stats', async (ctx) => {
|
|
1127
|
+
try {
|
|
1128
|
+
const v8 = require('v8')
|
|
1129
|
+
const memoryUsage = process.memoryUsage()
|
|
1130
|
+
const heapStats = v8.getHeapStatistics()
|
|
1131
|
+
const heapSpaces = v8.getHeapSpaceStatistics()
|
|
1132
|
+
|
|
1133
|
+
// 格式化字节为 MB
|
|
1134
|
+
const formatMB = (bytes) => (bytes / 1024 / 1024).toFixed(2)
|
|
1135
|
+
|
|
1136
|
+
// New Space 和 Old Space
|
|
1137
|
+
const newSpace = heapSpaces.find(space => space.space_name === 'new_space')
|
|
1138
|
+
const oldSpace = heapSpaces.find(space => space.space_name === 'old_space')
|
|
1139
|
+
|
|
1140
|
+
// 计算使用率
|
|
1141
|
+
const heapUsagePercent = ((heapStats.used_heap_size / heapStats.heap_size_limit) * 100).toFixed(2)
|
|
1142
|
+
|
|
1143
|
+
ctx.body = createSuccessResponse({
|
|
1144
|
+
// 总体堆内存
|
|
1145
|
+
heap: {
|
|
1146
|
+
used: formatMB(heapStats.used_heap_size),
|
|
1147
|
+
total: formatMB(heapStats.total_heap_size),
|
|
1148
|
+
limit: formatMB(heapStats.heap_size_limit),
|
|
1149
|
+
usagePercent: heapUsagePercent,
|
|
1150
|
+
},
|
|
1151
|
+
// New Space (Young Generation)
|
|
1152
|
+
newSpace: newSpace
|
|
1153
|
+
? {
|
|
1154
|
+
size: formatMB(newSpace.space_size),
|
|
1155
|
+
used: formatMB(newSpace.space_used_size),
|
|
1156
|
+
usagePercent: ((newSpace.space_used_size / newSpace.space_size) * 100).toFixed(2),
|
|
1157
|
+
available: formatMB(newSpace.space_available_size),
|
|
1158
|
+
physical: formatMB(newSpace.physical_space_size),
|
|
1159
|
+
}
|
|
1160
|
+
: null,
|
|
1161
|
+
// Old Space (Old Generation)
|
|
1162
|
+
oldSpace: oldSpace
|
|
1163
|
+
? {
|
|
1164
|
+
size: formatMB(oldSpace.space_size),
|
|
1165
|
+
used: formatMB(oldSpace.space_used_size),
|
|
1166
|
+
usagePercent: ((oldSpace.space_used_size / oldSpace.space_size) * 100).toFixed(2),
|
|
1167
|
+
available: formatMB(oldSpace.space_available_size),
|
|
1168
|
+
physical: formatMB(oldSpace.physical_space_size),
|
|
1169
|
+
}
|
|
1170
|
+
: null,
|
|
1171
|
+
// 进程总内存
|
|
1172
|
+
process: {
|
|
1173
|
+
rss: formatMB(memoryUsage.rss),
|
|
1174
|
+
heapUsed: formatMB(memoryUsage.heapUsed),
|
|
1175
|
+
heapTotal: formatMB(memoryUsage.heapTotal),
|
|
1176
|
+
external: formatMB(memoryUsage.external),
|
|
1177
|
+
arrayBuffers: formatMB(memoryUsage.arrayBuffers),
|
|
1178
|
+
},
|
|
1179
|
+
// GC 统计
|
|
1180
|
+
gc: {
|
|
1181
|
+
mallocedMemory: formatMB(heapStats.malloced_memory),
|
|
1182
|
+
peakMalloced: formatMB(heapStats.peak_malloced_memory),
|
|
1183
|
+
nativeContexts: heapStats.number_of_native_contexts,
|
|
1184
|
+
detachedContexts: heapStats.number_of_detached_contexts,
|
|
1185
|
+
},
|
|
1186
|
+
// 时间戳
|
|
1187
|
+
timestamp: new Date().toISOString(),
|
|
1188
|
+
pid: process.pid,
|
|
1189
|
+
}, '内存统计数据获取成功')
|
|
167
1190
|
} catch (error) {
|
|
168
1191
|
ctx.body = createErrorResponse(error.message)
|
|
169
1192
|
}
|