whistle.mockbubu 2.0.0 → 2.1.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.
Files changed (56) hide show
  1. package/README.md +38 -0
  2. package/index.js +3 -3
  3. package/lib/config/const.js +138 -0
  4. package/lib/config/rule-collector.js +81 -0
  5. package/lib/constants.js +62 -0
  6. package/lib/core/memory-buffer/index.js +207 -0
  7. package/lib/core/memory-buffer/shared-instance.js +15 -0
  8. package/lib/core/plugin-mode-manager.js +74 -0
  9. package/lib/core/resRulesServer.js +14 -0
  10. package/lib/core/rulesServer.js +31 -0
  11. package/lib/core/server-entry/capture-handler.js +191 -0
  12. package/lib/core/server-entry/request-interceptor.js +82 -0
  13. package/lib/core/server-entry/response-handler.js +147 -0
  14. package/lib/core/server-entry/server.js +230 -0
  15. package/lib/storage/group-manager.js +627 -0
  16. package/lib/storage/storage-adapter.js +712 -0
  17. package/lib/storage/storage-v3.js +1418 -0
  18. package/lib/uiServer/index.js +61 -24
  19. package/lib/uiServer/router/export/import-export-router.js +459 -0
  20. package/lib/uiServer/router/files/api-list-router.js +150 -0
  21. package/lib/uiServer/router/files/batch-operations-router.js +185 -0
  22. package/lib/uiServer/router/files/file-config-router.js +118 -0
  23. package/lib/uiServer/router/files/file-crud-router.js +212 -0
  24. package/lib/uiServer/router/files/file-save-router.js +146 -0
  25. package/lib/uiServer/router/files/version-router.js +260 -0
  26. package/lib/uiServer/router/global/plugin-control.js +135 -0
  27. package/lib/uiServer/router/global/system-info-router.js +386 -0
  28. package/lib/uiServer/router/{group-router.js → groups/group-router.js} +21 -20
  29. package/lib/uiServer/router/index.js +38 -1521
  30. package/lib/uiServer/utils/router-helpers.js +100 -0
  31. package/lib/uiServer/utils/util.js +172 -0
  32. package/lib/uiServer/{validator.js → utils/validator.js} +11 -6
  33. package/lib/utils/archive-utils.js +788 -0
  34. package/lib/utils/error-handler.js +173 -0
  35. package/lib/utils/logger.js +79 -0
  36. package/lib/utils/path-utils.js +147 -0
  37. package/lib/utils/performance.js +265 -0
  38. package/lib/utils/utils.js +541 -0
  39. package/package.json +2 -2
  40. package/public/js/app.js +3707 -1922
  41. package/public/js/app.js.map +1 -1
  42. package/public/js/chunk-vendors.js +5098 -3965
  43. package/public/js/chunk-vendors.js.map +1 -1
  44. package/rules.txt +1 -1
  45. package/CHANGELOG_GROUP_FEATURE.md +0 -468
  46. package/CHANGELOG_P0_FIXES.md +0 -412
  47. package/CHANGELOG_P1_OPTIMIZATIONS.md +0 -292
  48. package/CLAUDE.md +0 -436
  49. package/GROUP_FEATURE_DESIGN.md +0 -520
  50. package/lib/const.js +0 -47
  51. package/lib/group-manager.js +0 -491
  52. package/lib/resRulesServer.js +0 -9
  53. package/lib/server.js +0 -249
  54. package/lib/uiServer/router/version-router.js +0 -205
  55. package/lib/uiServer/util.js +0 -153
  56. package/lib/utils.js +0 -409
@@ -0,0 +1,1418 @@
1
+ /**
2
+ * FileSystemStorage V3 - 基于文件系统的存储层
3
+ *
4
+ * 架构设计:
5
+ * - 每个组一个独立目录
6
+ * - 索引文件(index.json)用于快速列表查询
7
+ * - 文件数据按需加载,避免内存溢出
8
+ * - 支持流式导出/导入
9
+ *
10
+ * 目录结构:
11
+ * groups/
12
+ * ├── {groupId}/
13
+ * │ ├── group.json # 组元数据
14
+ * │ ├── index.json # 文件索引(轻量级)
15
+ * │ └── files/
16
+ * │ └── {fileId}/
17
+ * │ ├── file.json # 文件完整数据
18
+ * │ └── versions/ # 版本目录
19
+ * │ ├── v1.json
20
+ * │ └── v2.json
21
+ */
22
+
23
+ const fs = require('fs').promises
24
+ const path = require('path')
25
+
26
+ class FileSystemStorage {
27
+ constructor(baseDir) {
28
+ this.baseDir = baseDir
29
+ this.groupsDir = path.join(baseDir, 'groups')
30
+ this.indexCache = new Map() // 索引缓存
31
+ this.maxCacheAge = 3000 // 缓存3秒(降低延迟,提高实时性)
32
+ this.indexLocks = new Map() // 索引写入锁(内存锁,同进程有效)
33
+ }
34
+
35
+ /**
36
+ * 初始化存储目录
37
+ */
38
+ async init() {
39
+ await fs.mkdir(this.groupsDir, { recursive: true })
40
+
41
+ // 确保默认组存在
42
+ const defaultGroupDir = path.join(this.groupsDir, 'default')
43
+ try {
44
+ await fs.access(defaultGroupDir)
45
+ } catch {
46
+ await this.createGroup({
47
+ id: 'default',
48
+ name: '默认组',
49
+ description: '系统默认分组',
50
+ isDefault: true,
51
+ })
52
+ }
53
+
54
+ // 清理老旧临时目录(启动时执行)
55
+ this._cleanupOldTempDirs().catch((err) => {
56
+ console.error('[storage-v3] 清理临时目录失败:', err.message)
57
+ })
58
+ }
59
+
60
+ /**
61
+ * 清理7天前的临时目录
62
+ * @private
63
+ */
64
+ async _cleanupOldTempDirs() {
65
+ const tmpDir = path.join(this.baseDir, '.tmp')
66
+
67
+ try {
68
+ await fs.access(tmpDir)
69
+ } catch {
70
+ // .tmp 目录不存在,无需清理
71
+ return
72
+ }
73
+
74
+ try {
75
+ const entries = await fs.readdir(tmpDir, { withFileTypes: true })
76
+ const now = Date.now()
77
+ const SEVEN_DAYS = 7 * 24 * 60 * 60 * 1000
78
+ let cleanedCount = 0
79
+
80
+ for (const entry of entries) {
81
+ if (entry.isDirectory()) {
82
+ const dirPath = path.join(tmpDir, entry.name)
83
+ try {
84
+ const stat = await fs.stat(dirPath)
85
+ const age = now - stat.mtimeMs
86
+
87
+ // 删除7天前的临时目录
88
+ if (age > SEVEN_DAYS) {
89
+ await fs.rm(dirPath, { recursive: true, force: true })
90
+ cleanedCount++
91
+ console.log(`[storage-v3] ✓ 已清理老旧临时目录: ${entry.name} (${Math.floor(age / (24 * 60 * 60 * 1000))}天前)`)
92
+ }
93
+ } catch (err) {
94
+ console.error(`[storage-v3] 清理临时目录失败: ${entry.name}`, err.message)
95
+ }
96
+ }
97
+ }
98
+
99
+ if (cleanedCount > 0) {
100
+ console.log(`[storage-v3] ✓ 临时目录清理完成,共清理 ${cleanedCount} 个`)
101
+ }
102
+ } catch (err) {
103
+ console.error('[storage-v3] 扫描临时目录失败:', err.message)
104
+ }
105
+ }
106
+
107
+ /**
108
+ * 生成文件ID(随机生成,确保唯一性)
109
+ * ⚠️ 重要设计原则:
110
+ * - fileId 是文件系统的唯一标识符,用于删除/编辑/查找文件
111
+ * - 每次创建新文件时生成新的 fileId(即使URL相同)
112
+ * - 重复检测通过文件名(URL)而不是 fileId
113
+ * - 删除后重新捕获同一URL会生成新的 fileId
114
+ *
115
+ * @returns {string} 12位随机ID
116
+ */
117
+ generateFileId() {
118
+ // 使用时间戳 + 随机数确保唯一性
119
+ const timestamp = Date.now().toString(36)
120
+ const random = Math.random().toString(36).substring(2, 10)
121
+ return `${timestamp}${random}`.substring(0, 12)
122
+ }
123
+
124
+ // ========================================
125
+ // 组管理
126
+ // ========================================
127
+
128
+ /**
129
+ * 列出所有组
130
+ */
131
+ async listGroups() {
132
+ const entries = await fs.readdir(this.groupsDir, { withFileTypes: true })
133
+ const groups = []
134
+
135
+ for (const entry of entries) {
136
+ if (entry.isDirectory()) {
137
+ try {
138
+ const groupData = await this.getGroup(entry.name)
139
+ groups.push(groupData)
140
+ } catch (err) {
141
+ console.error(`[storage-v3] 读取组失败: ${entry.name}`, err.message)
142
+ }
143
+ }
144
+ }
145
+
146
+ // 排序:默认组在前,其他组按创建时间升序(旧组在前,新组在后)
147
+ return groups.sort((a, b) => {
148
+ // 默认组始终排在第一位
149
+ if (a.isDefault) return -1
150
+ if (b.isDefault) return 1
151
+ // 其他组按创建时间升序排序
152
+ return (a.createTime || 0) - (b.createTime || 0)
153
+ })
154
+ }
155
+
156
+ /**
157
+ * 获取组信息
158
+ * 如果组文件不存在,自动创建默认组元数据
159
+ */
160
+ async getGroup(groupId) {
161
+ const groupFile = path.join(this.groupsDir, groupId, 'group.json')
162
+
163
+ try {
164
+ const content = await fs.readFile(groupFile, 'utf8')
165
+ return JSON.parse(content)
166
+ } catch (err) {
167
+ if (err.code === 'ENOENT') {
168
+ // 文件不存在,自动创建默认组元数据
169
+ console.log(`[storage-v3] 组元数据不存在,自动创建: ${groupId}`)
170
+
171
+ const defaultMeta = {
172
+ id: groupId,
173
+ name: groupId === 'default' ? '默认组' : groupId,
174
+ description: '',
175
+ isDefault: groupId === 'default',
176
+ totalSize: 0,
177
+ createTime: Date.now(),
178
+ updateTime: Date.now(),
179
+ }
180
+
181
+ // 确保目录存在
182
+ const groupDir = path.join(this.groupsDir, groupId)
183
+ await fs.mkdir(groupDir, { recursive: true })
184
+
185
+ // 写入组元数据
186
+ await this._writeJSON(groupFile, defaultMeta)
187
+
188
+ return defaultMeta
189
+ }
190
+
191
+ // 其他错误继续抛出
192
+ console.error(`[storage-v3] 读取组元数据失败: ${groupId}`, err)
193
+ throw err
194
+ }
195
+ }
196
+
197
+ /**
198
+ * 创建组
199
+ */
200
+ async createGroup(groupData) {
201
+ const { id, name, description = '', isDefault = false } = groupData
202
+
203
+ const groupDir = path.join(this.groupsDir, id)
204
+ const filesDir = path.join(groupDir, 'files')
205
+
206
+ // 创建目录结构
207
+ await fs.mkdir(filesDir, { recursive: true })
208
+
209
+ // 写入组元数据
210
+ const group = {
211
+ id,
212
+ name,
213
+ description,
214
+ isDefault,
215
+ totalSize: 0,
216
+ createTime: Date.now(),
217
+ updateTime: Date.now(),
218
+ }
219
+
220
+ await this._writeJSON(path.join(groupDir, 'group.json'), group)
221
+
222
+ // 初始化索引文件
223
+ const index = {
224
+ version: '3.0.0',
225
+ updateTime: Date.now(),
226
+ files: [],
227
+ }
228
+
229
+ await this._writeJSON(path.join(groupDir, 'index.json'), index)
230
+
231
+ // 更新组元数据文件(_meta.json)
232
+ try {
233
+ const meta = await this.getGroupsMeta()
234
+ if (!meta.groups.find(g => g.id === id)) {
235
+ meta.groups.push(group)
236
+ await this.setGroupsMeta(meta)
237
+ }
238
+ } catch (err) {
239
+ console.error(`[storage-v3 ${new Date().toLocaleString('zh-CN', { hour12: false })}] 更新 _meta.json 失败:`, err.message)
240
+ }
241
+
242
+ return group
243
+ }
244
+
245
+ /**
246
+ * 更新组信息
247
+ */
248
+ async updateGroup(groupId, updates) {
249
+ const group = await this.getGroup(groupId)
250
+ const updated = {
251
+ ...group,
252
+ ...updates,
253
+ updateTime: Date.now(),
254
+ }
255
+
256
+ // 1. 更新 group.json
257
+ const groupFile = path.join(this.groupsDir, groupId, 'group.json')
258
+ await this._writeJSON(groupFile, updated)
259
+
260
+ // 2. 同步更新 _meta.json 中的组信息
261
+ const meta = await this.getGroupsMeta()
262
+ const groupIndex = meta.groups.findIndex(g => g.id === groupId)
263
+ if (groupIndex !== -1) {
264
+ meta.groups[groupIndex] = {
265
+ ...meta.groups[groupIndex],
266
+ ...updates,
267
+ updateTime: updated.updateTime,
268
+ }
269
+ await this.setGroupsMeta(meta)
270
+ }
271
+
272
+ return updated
273
+ }
274
+
275
+ /**
276
+ * 删除组(删除整个目录)
277
+ */
278
+ async deleteGroup(groupId) {
279
+ if (groupId === 'default') {
280
+ throw new Error('不能删除默认组')
281
+ }
282
+
283
+ const groupDir = path.join(this.groupsDir, groupId)
284
+ await fs.rm(groupDir, { recursive: true, force: true })
285
+
286
+ // 清除缓存
287
+ this.indexCache.delete(groupId)
288
+
289
+ // 同时更新组元数据,移除已删除的组
290
+ const meta = await this.getGroupsMeta()
291
+ meta.groups = meta.groups.filter(g => g.id !== groupId)
292
+ await this.setGroupsMeta(meta)
293
+ }
294
+
295
+ /**
296
+ * 获取组元数据(所有组列表 + 当前组ID)
297
+ * 从独立的 _meta.json 读取
298
+ */
299
+ async getGroupsMeta() {
300
+ const metaFile = path.join(this.groupsDir, '_meta.json')
301
+
302
+ try {
303
+ const content = await fs.readFile(metaFile, 'utf-8')
304
+ const meta = JSON.parse(content)
305
+
306
+ // 确保 config 字段存在(兼容旧数据)
307
+ if (!meta.config) {
308
+ meta.config = { mode: 'default' }
309
+ }
310
+
311
+ return meta
312
+ } catch (err) {
313
+ // 如果文件不存在,返回默认值
314
+ if (err.code === 'ENOENT') {
315
+ console.log(`[storage-v3 ${new Date().toLocaleString('zh-CN', { hour12: false })}] _meta.json 不存在,创建默认组元数据`)
316
+
317
+ const defaultMeta = {
318
+ groups: [{
319
+ id: 'default',
320
+ name: '默认组',
321
+ description: '系统默认分组',
322
+ createTime: Date.now(),
323
+ updateTime: Date.now(),
324
+ isDefault: true,
325
+ }],
326
+ currentGroupId: 'default',
327
+ config: {
328
+ mode: 'default',
329
+ },
330
+ }
331
+
332
+ await this.setGroupsMeta(defaultMeta)
333
+ return defaultMeta
334
+ }
335
+ throw err
336
+ }
337
+ }
338
+
339
+ /**
340
+ * 设置组元数据
341
+ * 写入独立的 _meta.json
342
+ */
343
+ async setGroupsMeta(meta) {
344
+ const metaFile = path.join(this.groupsDir, '_meta.json')
345
+ await this._writeJSON(metaFile, {
346
+ ...meta,
347
+ version: '3.0',
348
+ updateTime: Date.now(),
349
+ })
350
+ }
351
+
352
+ /**
353
+ * 复制组的所有文件(V3架构专用)
354
+ * @param {string} sourceGroupId - 源组ID
355
+ * @param {string} targetGroupId - 目标组ID
356
+ */
357
+ async copyGroup(sourceGroupId, targetGroupId) {
358
+ console.log(`[storage-v3 2025/11/19 14:10:00] 开始复制组: ${sourceGroupId} -> ${targetGroupId}`)
359
+
360
+ // 1. 读取源组的索引
361
+ const sourceIndex = await this.getIndex(sourceGroupId)
362
+ console.log(`[storage-v3 2025/11/19 14:10:00] 源组文件数: ${sourceIndex.files.length}`)
363
+
364
+ // 2. 确保目标组目录存在
365
+ const targetGroupDir = path.join(this.groupsDir, targetGroupId)
366
+ const targetFilesDir = path.join(targetGroupDir, 'files')
367
+ await fs.mkdir(targetFilesDir, { recursive: true })
368
+
369
+ // 3. 复制所有文件
370
+ const copiedFiles = []
371
+ for (const fileEntry of sourceIndex.files) {
372
+ try {
373
+ const sourceFileDir = path.join(this.groupsDir, sourceGroupId, 'files', fileEntry.id)
374
+ const targetFileDir = path.join(targetFilesDir, fileEntry.id)
375
+
376
+ // 递归复制文件目录(包括file.json和versions/)
377
+ await this._copyDirectory(sourceFileDir, targetFileDir)
378
+
379
+ // 将文件条目添加到目标索引
380
+ copiedFiles.push({
381
+ ...fileEntry,
382
+ // 保持原有时间戳
383
+ })
384
+
385
+ console.log(`[storage-v3 2025/11/19 14:10:00] 已复制文件: ${fileEntry.url} (${fileEntry.id})`)
386
+ } catch (err) {
387
+ console.error(`[storage-v3 2025/11/19 14:10:00] 复制文件失败: ${fileEntry.id}`, err.message)
388
+ }
389
+ }
390
+
391
+ // 4. 创建目标组索引
392
+ const targetIndex = {
393
+ version: '3.0.0',
394
+ files: copiedFiles,
395
+ updateTime: Date.now(),
396
+ }
397
+
398
+ await this.updateIndex(targetGroupId, targetIndex)
399
+ console.log(`[storage-v3 2025/11/19 14:10:00] 复制完成,共 ${copiedFiles.length} 个文件`)
400
+
401
+ return copiedFiles.length
402
+ }
403
+
404
+ /**
405
+ * 递归复制目录
406
+ * @private
407
+ */
408
+ async _copyDirectory(source, target) {
409
+ await fs.mkdir(target, { recursive: true })
410
+
411
+ const entries = await fs.readdir(source, { withFileTypes: true })
412
+
413
+ for (const entry of entries) {
414
+ const sourcePath = path.join(source, entry.name)
415
+ const targetPath = path.join(target, entry.name)
416
+
417
+ if (entry.isDirectory()) {
418
+ await this._copyDirectory(sourcePath, targetPath)
419
+ } else {
420
+ await fs.copyFile(sourcePath, targetPath)
421
+ }
422
+ }
423
+ }
424
+
425
+ // ========================================
426
+ // 索引管理
427
+ // ========================================
428
+
429
+ /**
430
+ * 获取索引(带缓存)
431
+ * 如果索引文件不存在,自动创建空索引
432
+ */
433
+ async getIndex(groupId) {
434
+ // 检查缓存
435
+ const cached = this.indexCache.get(groupId)
436
+ if (cached && Date.now() - cached.time < this.maxCacheAge) {
437
+ return cached.data
438
+ }
439
+
440
+ // 读取索引文件
441
+ const indexFile = path.join(this.groupsDir, groupId, 'index.json')
442
+
443
+ try {
444
+ const content = await fs.readFile(indexFile, 'utf8')
445
+ const index = JSON.parse(content)
446
+
447
+ // 更新缓存
448
+ this.indexCache.set(groupId, { data: index, time: Date.now() })
449
+
450
+ return index
451
+ } catch (err) {
452
+ if (err.code === 'ENOENT') {
453
+ // 文件不存在,自动创建空索引
454
+ console.log(`[storage-v3] 索引文件不存在,自动创建空索引: ${groupId}`)
455
+
456
+ const emptyIndex = {
457
+ version: '3.0.0',
458
+ files: [],
459
+ updateTime: Date.now(),
460
+ }
461
+
462
+ // 确保目录存在
463
+ const groupDir = path.join(this.groupsDir, groupId)
464
+ await fs.mkdir(groupDir, { recursive: true })
465
+
466
+ // 写入空索引
467
+ await this._writeJSON(indexFile, emptyIndex)
468
+
469
+ // 更新缓存
470
+ this.indexCache.set(groupId, { data: emptyIndex, time: Date.now() })
471
+
472
+ return emptyIndex
473
+ }
474
+
475
+ // 其他错误继续抛出
476
+ console.error(`[storage-v3] 读取索引失败: ${groupId}`, err)
477
+ throw err
478
+ }
479
+ }
480
+
481
+ /**
482
+ * 更新索引
483
+ */
484
+ async updateIndex(groupId, indexData) {
485
+ const indexFile = path.join(this.groupsDir, groupId, 'index.json')
486
+ const updated = {
487
+ ...indexData,
488
+ updateTime: Date.now(),
489
+ }
490
+
491
+ await this._writeJSON(indexFile, updated)
492
+
493
+ // 清除缓存
494
+ this.indexCache.delete(groupId)
495
+ }
496
+
497
+ /**
498
+ * 使索引失效(强制下次重新读取)
499
+ */
500
+ invalidateIndex(groupId) {
501
+ this.indexCache.delete(groupId)
502
+ }
503
+
504
+ /**
505
+ * 重建索引(扫描 files 目录)
506
+ */
507
+ async rebuildIndex(groupId) {
508
+ console.log(`[storage-v3] 开始重建组 ${groupId} 的索引...`)
509
+
510
+ const filesDir = path.join(this.groupsDir, groupId, 'files')
511
+
512
+ try {
513
+ const fileDirs = await fs.readdir(filesDir)
514
+ const index = {
515
+ version: '3.0.0',
516
+ files: [],
517
+ updateTime: Date.now(),
518
+ }
519
+
520
+ for (const fileId of fileDirs) {
521
+ if (fileId.includes('.backup')) continue // 跳过备份文件
522
+
523
+ const fileJsonPath = path.join(filesDir, fileId, 'file.json')
524
+
525
+ try {
526
+ const fileData = await fs.readFile(fileJsonPath, 'utf8')
527
+ const file = JSON.parse(fileData)
528
+
529
+ // 添加到索引
530
+ index.files.push({
531
+ id: file.id,
532
+ url: file.url,
533
+ method: file.method || 'GET',
534
+ status: file.status || 200,
535
+ size: this._calculateSize(file),
536
+ mock: file.config?.mock || false,
537
+ locked: file.config?.locked || false,
538
+ mockVersion: file.config?.mockVersion || 'source',
539
+ versionCount: 0, // TODO: 扫描 versions 目录
540
+ createTime: file.createTime,
541
+ updateTime: file.updateTime,
542
+ })
543
+
544
+ console.log(`[storage-v3] ✓ 添加文件: ${file.url}`)
545
+ } catch (err) {
546
+ console.warn(`[storage-v3] ✗ 跳过损坏的文件: ${fileId}`, err.message)
547
+ }
548
+ }
549
+
550
+ // 写入索引
551
+ await this.updateIndex(groupId, index)
552
+ console.log(`[storage-v3] 索引重建完成,共 ${index.files.length} 个文件`)
553
+
554
+ return index
555
+ } catch (err) {
556
+ console.error('[storage-v3] 重建索引失败:', err.message)
557
+ throw err
558
+ }
559
+ }
560
+
561
+ // ========================================
562
+ // 文件操作
563
+ // ========================================
564
+
565
+ /**
566
+ * 列出文件(从索引)
567
+ * @param {string} groupId - 组ID
568
+ * @param {object} options - 选项
569
+ * @param {number} options.offset - 偏移量
570
+ * @param {number} options.limit - 限制数量
571
+ * @param {object} options.filter - 过滤条件
572
+ */
573
+ async listFiles(groupId, options = {}) {
574
+ const { offset = 0, limit, filter = {} } = options
575
+
576
+ const index = await this.getIndex(groupId)
577
+ let files = index.files
578
+
579
+ // 应用过滤器
580
+ if (Object.keys(filter).length > 0) {
581
+ files = files.filter(file => {
582
+ return Object.entries(filter).every(([key, value]) => file[key] === value)
583
+ })
584
+ }
585
+
586
+ // 应用分页
587
+ if (limit) {
588
+ files = files.slice(offset, offset + limit)
589
+ } else if (offset > 0) {
590
+ files = files.slice(offset)
591
+ }
592
+
593
+ return files
594
+ }
595
+
596
+ /**
597
+ * 获取文件完整数据
598
+ * 如果文件损坏,返回 null 而不是抛出错误
599
+ */
600
+ async getFile(groupId, fileId) {
601
+ const fileJsonPath = path.join(this.groupsDir, groupId, 'files', fileId, 'file.json')
602
+
603
+ try {
604
+ const content = await fs.readFile(fileJsonPath, 'utf8')
605
+ return JSON.parse(content)
606
+ } catch (err) {
607
+ if (err.code === 'ENOENT') {
608
+ // 文件不存在
609
+ return null
610
+ }
611
+
612
+ // JSON 解析错误 - 文件损坏
613
+ if (err instanceof SyntaxError) {
614
+ console.error(`[storage-v3] 文件JSON损坏,跳过: ${groupId}/${fileId}`, err.message)
615
+ return null
616
+ }
617
+
618
+ // 其他错误继续抛出
619
+ throw err
620
+ }
621
+ }
622
+
623
+ /**
624
+ * 创建文件
625
+ */
626
+ async createFile(groupId, fileId, fileData) {
627
+ // 验证数据
628
+ const validation = this._validateJSON(fileData)
629
+ if (!validation.valid) {
630
+ const error = new Error(`Cannot store invalid JSON data: ${validation.error}`)
631
+ console.error(`[storage-v3] 创建文件失败: ${groupId}/${fileId}`, error.message)
632
+ throw error
633
+ }
634
+
635
+ const fileDir = path.join(this.groupsDir, groupId, 'files', fileId)
636
+ const versionsDir = path.join(fileDir, 'versions')
637
+
638
+ // 创建目录
639
+ await fs.mkdir(versionsDir, { recursive: true })
640
+
641
+ // 确保 config 字段存在
642
+ const config = fileData.config || {
643
+ mock: false,
644
+ locked: false,
645
+ mockVersion: 'source',
646
+ mockTime: null,
647
+ }
648
+
649
+ // 写入文件数据(只保留 config 字段,不保留根级别的配置字段)
650
+ const file = {
651
+ id: fileId,
652
+ url: fileData.url,
653
+ method: fileData.method || 'GET',
654
+ status: fileData.status || 200,
655
+ date: fileData.date || Date.now(),
656
+ rule: fileData.rule || '',
657
+ ruleValue: fileData.ruleValue || '',
658
+ pattern: fileData.pattern || '',
659
+ config,
660
+ session: fileData.session || null, // ✅ 保存完整 session 数据
661
+ createTime: Date.now(),
662
+ updateTime: Date.now(),
663
+ }
664
+
665
+ await this._writeJSON(path.join(fileDir, 'file.json'), file)
666
+
667
+ // 解析 URL 增强字段
668
+ const urlFields = this._parseUrlFields(fileData.url)
669
+
670
+ // 更新索引(包含增强字段)
671
+ await this._addToIndex(groupId, {
672
+ id: fileId,
673
+ url: fileData.url,
674
+ method: fileData.method || 'GET',
675
+ status: fileData.status || 200,
676
+ size: this._calculateSize(file),
677
+ mock: config.mock,
678
+ locked: config.locked,
679
+ mockVersion: config.mockVersion,
680
+ versionCount: 0,
681
+ createTime: file.createTime,
682
+ updateTime: file.updateTime,
683
+ // ✨ Phase 3.1: 索引增强字段
684
+ domain: urlFields.domain,
685
+ pathname: urlFields.pathname,
686
+ urlHash: urlFields.urlHash,
687
+ })
688
+
689
+ // 更新组统计
690
+ await this._updateGroupStats(groupId)
691
+
692
+ return file
693
+ }
694
+
695
+ /**
696
+ * 更新文件
697
+ * 如果原文件损坏或不存在,将 updates 作为新文件创建
698
+ */
699
+ async updateFile(groupId, fileId, updates) {
700
+ const file = await this.getFile(groupId, fileId)
701
+
702
+ // 🔧 FIX: 过滤掉 updates 中值为 undefined 的字段
703
+ // 这样可以支持删除字段的操作
704
+ const filteredUpdates = {}
705
+ Object.keys(updates).forEach(key => {
706
+ if (updates[key] !== undefined) {
707
+ filteredUpdates[key] = updates[key]
708
+ }
709
+ })
710
+
711
+ let updated
712
+ if (!file) {
713
+ // 原文件不存在或损坏,使用 updates 作为新文件
714
+ console.log(`[storage-v3] 原文件不存在或损坏,创建新文件: ${groupId}/${fileId}`)
715
+
716
+ // 确保 config 字段存在
717
+ const config = filteredUpdates.config || {
718
+ mock: false,
719
+ locked: false,
720
+ mockVersion: 'source',
721
+ mockTime: null,
722
+ }
723
+
724
+ updated = {
725
+ id: fileId,
726
+ ...filteredUpdates,
727
+ config,
728
+ createTime: Date.now(),
729
+ updateTime: Date.now(),
730
+ }
731
+ } else {
732
+ // 正常更新:深度合并 config 字段
733
+ const mergedConfig = {
734
+ ...file.config,
735
+ ...filteredUpdates.config,
736
+ }
737
+
738
+ updated = {
739
+ ...file,
740
+ ...filteredUpdates,
741
+ config: mergedConfig,
742
+ updateTime: Date.now(),
743
+ }
744
+
745
+ // 🔧 FIX: 从合并结果中删除 updates 里标记为 undefined 的字段
746
+ Object.keys(updates).forEach(key => {
747
+ if (updates[key] === undefined) {
748
+ delete updated[key]
749
+ }
750
+ })
751
+
752
+ // 清理根级别的重复配置字段(如果存在)
753
+ delete updated.mock
754
+ delete updated.locked
755
+ delete updated.mockVersion
756
+ delete updated.mockTime
757
+ }
758
+
759
+ // 验证更新后的数据
760
+ const validation = this._validateJSON(updated)
761
+ if (!validation.valid) {
762
+ const error = new Error(`Cannot store invalid JSON data: ${validation.error}`)
763
+ console.error(`[storage-v3] 更新文件失败: ${groupId}/${fileId}`, error.message)
764
+ throw error
765
+ }
766
+
767
+ const fileJsonPath = path.join(this.groupsDir, groupId, 'files', fileId, 'file.json')
768
+ await this._writeJSON(fileJsonPath, updated)
769
+
770
+ // 解析 URL 增强字段(如果 URL 发生变化)
771
+ const urlFields = updated.url ? this._parseUrlFields(updated.url) : {}
772
+
773
+ // 更新索引(包含增强字段)
774
+ await this._updateInIndex(groupId, fileId, {
775
+ url: updated.url,
776
+ method: updated.method,
777
+ status: updated.status,
778
+ size: this._calculateSize(updated),
779
+ mock: updated.config?.mock,
780
+ locked: updated.config?.locked,
781
+ mockVersion: updated.config?.mockVersion,
782
+ updateTime: updated.updateTime,
783
+ // ✨ Phase 3.1: 索引增强字段
784
+ ...(updated.url && {
785
+ domain: urlFields.domain,
786
+ pathname: urlFields.pathname,
787
+ urlHash: urlFields.urlHash,
788
+ }),
789
+ })
790
+
791
+ return updated
792
+ }
793
+
794
+ /**
795
+ * 删除文件
796
+ */
797
+ async deleteFile(groupId, fileId) {
798
+ const logFile = '/tmp/mockbubu-delete.log'
799
+ const log = (msg) => {
800
+ const fs = require('fs')
801
+ fs.appendFileSync(logFile, `${new Date().toISOString()} ${msg}\n`)
802
+ }
803
+
804
+ log('[storage-v3] deleteFile 开始')
805
+ log(`[storage-v3] groupId: ${groupId}`)
806
+ log(`[storage-v3] fileId: ${fileId}`)
807
+
808
+ const fileDir = path.join(this.groupsDir, groupId, 'files', fileId)
809
+ log(`[storage-v3] 要删除的目录: ${fileDir}`)
810
+
811
+ try {
812
+ await fs.rm(fileDir, { recursive: true, force: true })
813
+ log('[storage-v3] 物理文件删除成功')
814
+ } catch (err) {
815
+ log(`[storage-v3] 物理文件删除失败: ${err.message}`)
816
+ throw err
817
+ }
818
+
819
+ // 从索引中移除
820
+ try {
821
+ await this._removeFromIndex(groupId, fileId)
822
+ log('[storage-v3] 已从索引中移除')
823
+ } catch (err) {
824
+ log(`[storage-v3] 从索引移除失败: ${err.message}`)
825
+ throw err
826
+ }
827
+
828
+ // 更新组统计
829
+ try {
830
+ await this._updateGroupStats(groupId)
831
+ log('[storage-v3] 组统计已更新')
832
+ } catch (err) {
833
+ log(`[storage-v3] 更新组统计失败: ${err.message}`)
834
+ throw err
835
+ }
836
+ }
837
+
838
+ // ========================================
839
+ // Phase 3.2: 批量操作支持
840
+ // ========================================
841
+
842
+ /**
843
+ * 批量删除文件
844
+ * @param {string} groupId - 组ID
845
+ * @param {Array<string>} fileIds - 文件ID数组
846
+ * @returns {Promise<{success: number, failed: Array}>} 删除结果统计
847
+ */
848
+ async batchDeleteFiles(groupId, fileIds) {
849
+ console.log(`[storage-v3] 🗑️ 批量删除开始: ${fileIds.length} 个文件`)
850
+
851
+ const results = {
852
+ success: 0,
853
+ failed: [],
854
+ }
855
+
856
+ // 并发删除文件(但保持索引操作的串行性)
857
+ await Promise.all(
858
+ fileIds.map(async fileId => {
859
+ try {
860
+ const fileDir = path.join(this.groupsDir, groupId, 'files', fileId)
861
+ await fs.rm(fileDir, { recursive: true, force: true })
862
+ results.success++
863
+ } catch (err) {
864
+ console.error(`[storage-v3] 删除文件失败: ${groupId}/${fileId}`, err.message)
865
+ results.failed.push({ fileId, error: err.message })
866
+ }
867
+ }),
868
+ )
869
+
870
+ // 批量从索引中移除(一次性更新索引)
871
+ try {
872
+ const release = await this._acquireIndexLock(groupId)
873
+ try {
874
+ this.invalidateIndex(groupId)
875
+ const index = await this.getIndex(groupId)
876
+
877
+ // 过滤掉已删除的文件
878
+ const initialCount = index.files.length
879
+ index.files = index.files.filter(f => !fileIds.includes(f.id))
880
+ const removedCount = initialCount - index.files.length
881
+
882
+ await this.updateIndex(groupId, index)
883
+ console.log(`[storage-v3] ✅ 从索引中移除 ${removedCount} 个文件`)
884
+ } finally {
885
+ this._releaseIndexLock(groupId, release)
886
+ }
887
+ } catch (err) {
888
+ console.error('[storage-v3] 批量更新索引失败:', err.message)
889
+ }
890
+
891
+ // 更新组统计
892
+ await this._updateGroupStats(groupId)
893
+
894
+ console.log(`[storage-v3] ✅ 批量删除完成: 成功 ${results.success}, 失败 ${results.failed.length}`)
895
+ return results
896
+ }
897
+
898
+ /**
899
+ * 批量更新文件(用于批量修改 config 等字段)
900
+ * @param {string} groupId - 组ID
901
+ * @param {Array<{fileId: string, updates: Object}>} updates - 更新数组
902
+ * @returns {Promise<{success: number, failed: Array}>} 更新结果统计
903
+ */
904
+ async batchUpdateFiles(groupId, updates) {
905
+ console.log(`[storage-v3] 📝 批量更新开始: ${updates.length} 个文件`)
906
+
907
+ const results = {
908
+ success: 0,
909
+ failed: [],
910
+ }
911
+
912
+ // 并发更新文件内容
913
+ await Promise.all(
914
+ updates.map(async ({ fileId, updates: fileUpdates }) => {
915
+ try {
916
+ // 读取现有文件
917
+ const file = await this.getFile(groupId, fileId)
918
+ if (!file) {
919
+ throw new Error('文件不存在')
920
+ }
921
+
922
+ // 合并更新
923
+ const updated = {
924
+ ...file,
925
+ ...fileUpdates,
926
+ config: {
927
+ ...file.config,
928
+ ...fileUpdates.config,
929
+ },
930
+ updateTime: Date.now(),
931
+ }
932
+
933
+ // 写入文件
934
+ const fileJsonPath = path.join(this.groupsDir, groupId, 'files', fileId, 'file.json')
935
+ await this._writeJSON(fileJsonPath, updated)
936
+
937
+ results.success++
938
+ } catch (err) {
939
+ console.error(`[storage-v3] 更新文件失败: ${groupId}/${fileId}`, err.message)
940
+ results.failed.push({ fileId, error: err.message })
941
+ }
942
+ }),
943
+ )
944
+
945
+ // 批量更新索引(一次性)
946
+ try {
947
+ const release = await this._acquireIndexLock(groupId)
948
+ try {
949
+ this.invalidateIndex(groupId)
950
+ const index = await this.getIndex(groupId)
951
+
952
+ // 更新索引中的相关字段
953
+ updates.forEach(({ fileId, updates: fileUpdates }) => {
954
+ const fileIndex = index.files.findIndex(f => f.id === fileId)
955
+ if (fileIndex !== -1) {
956
+ index.files[fileIndex] = {
957
+ ...index.files[fileIndex],
958
+ ...fileUpdates,
959
+ updateTime: Date.now(),
960
+ }
961
+ }
962
+ })
963
+
964
+ await this.updateIndex(groupId, index)
965
+ console.log('[storage-v3] ✅ 批量更新索引完成')
966
+ } finally {
967
+ this._releaseIndexLock(groupId, release)
968
+ }
969
+ } catch (err) {
970
+ console.error('[storage-v3] 批量更新索引失败:', err.message)
971
+ }
972
+
973
+ console.log(`[storage-v3] ✅ 批量更新完成: 成功 ${results.success}, 失败 ${results.failed.length}`)
974
+ return results
975
+ }
976
+
977
+ /**
978
+ * 文件是否存在
979
+ */
980
+ async fileExists(groupId, fileId) {
981
+ const fileJsonPath = path.join(this.groupsDir, groupId, 'files', fileId, 'file.json')
982
+ try {
983
+ await fs.access(fileJsonPath)
984
+ return true
985
+ } catch {
986
+ return false
987
+ }
988
+ }
989
+
990
+ // ========================================
991
+ // 版本管理
992
+ // ========================================
993
+
994
+ /**
995
+ * 列出版本
996
+ */
997
+ async listVersions(groupId, fileId) {
998
+ const versionsDir = path.join(this.groupsDir, groupId, 'files', fileId, 'versions')
999
+
1000
+ try {
1001
+ const entries = await fs.readdir(versionsDir)
1002
+ const versions = []
1003
+
1004
+ for (const entry of entries) {
1005
+ if (entry.endsWith('.json')) {
1006
+ const versionId = entry.replace('.json', '')
1007
+ const versionData = await this.getVersionById(groupId, fileId, versionId)
1008
+ if (versionData) {
1009
+ versions.push(versionData)
1010
+ }
1011
+ }
1012
+ }
1013
+
1014
+ // 按创建时间降序排序(新版本在前)
1015
+ return versions.sort((a, b) => (b.createTime || 0) - (a.createTime || 0))
1016
+ } catch (err) {
1017
+ if (err.code === 'ENOENT') {
1018
+ return []
1019
+ }
1020
+ throw err
1021
+ }
1022
+ }
1023
+
1024
+ /**
1025
+ * 通过版本ID获取版本
1026
+ */
1027
+ async getVersionById(groupId, fileId, versionId) {
1028
+ const versionFile = path.join(this.groupsDir, groupId, 'files', fileId, 'versions', `${versionId}.json`)
1029
+ try {
1030
+ const content = await fs.readFile(versionFile, 'utf8')
1031
+ return JSON.parse(content)
1032
+ } catch (err) {
1033
+ if (err.code === 'ENOENT') {
1034
+ return null
1035
+ }
1036
+ throw err
1037
+ }
1038
+ }
1039
+
1040
+ /**
1041
+ * 通过版本名称获取版本
1042
+ */
1043
+ async getVersionByName(groupId, fileId, versionName) {
1044
+ const versions = await this.listVersions(groupId, fileId)
1045
+ return versions.find(v => v.name === versionName)
1046
+ }
1047
+
1048
+ /**
1049
+ * 创建版本
1050
+ * @param {string} versionName - 用户可见的版本名称
1051
+ */
1052
+ async createVersion(groupId, fileId, versionName, versionData) {
1053
+ // 验证版本数据
1054
+ const validation = this._validateJSON(versionData)
1055
+ if (!validation.valid) {
1056
+ const error = new Error(`Cannot store invalid JSON data: ${validation.error}`)
1057
+ console.error(`[storage-v3] 创建版本失败: ${groupId}/${fileId}/${versionName}`, error.message)
1058
+ throw error
1059
+ }
1060
+
1061
+ // 检查版本名是否重复
1062
+ const existingVersion = await this.getVersionByName(groupId, fileId, versionName)
1063
+ if (existingVersion) {
1064
+ throw new Error(`版本名称已存在: ${versionName}`)
1065
+ }
1066
+
1067
+ const versionsDir = path.join(this.groupsDir, groupId, 'files', fileId, 'versions')
1068
+ await fs.mkdir(versionsDir, { recursive: true })
1069
+
1070
+ // 生成唯一的版本ID
1071
+ const versionId = this.generateFileId()
1072
+
1073
+ const version = {
1074
+ id: versionId,
1075
+ name: versionName,
1076
+ ...versionData,
1077
+ createTime: Date.now(),
1078
+ updateTime: Date.now(),
1079
+ }
1080
+
1081
+ const versionFile = path.join(versionsDir, `${versionId}.json`)
1082
+ await this._writeJSON(versionFile, version)
1083
+
1084
+ // 更新索引中的版本计数
1085
+ const versions = await this.listVersions(groupId, fileId)
1086
+ await this._updateInIndex(groupId, fileId, {
1087
+ versionCount: versions.length,
1088
+ })
1089
+
1090
+ return version
1091
+ }
1092
+
1093
+ /**
1094
+ * 更新版本(通过版本ID)
1095
+ */
1096
+ async updateVersion(groupId, fileId, versionId, updates) {
1097
+ const version = await this.getVersionById(groupId, fileId, versionId)
1098
+ if (!version) {
1099
+ throw new Error(`版本不存在: ${versionId}`)
1100
+ }
1101
+
1102
+ // 如果要更新name,检查是否重复
1103
+ if (updates.name && updates.name !== version.name) {
1104
+ const existingVersion = await this.getVersionByName(groupId, fileId, updates.name)
1105
+ if (existingVersion && existingVersion.id !== versionId) {
1106
+ throw new Error(`版本名称已存在: ${updates.name}`)
1107
+ }
1108
+ }
1109
+
1110
+ const updatedVersion = {
1111
+ ...version,
1112
+ ...updates,
1113
+ id: versionId, // 确保ID不被修改
1114
+ updateTime: Date.now(),
1115
+ }
1116
+
1117
+ const versionFile = path.join(this.groupsDir, groupId, 'files', fileId, 'versions', `${versionId}.json`)
1118
+ await this._writeJSON(versionFile, updatedVersion)
1119
+
1120
+ return updatedVersion
1121
+ }
1122
+
1123
+ /**
1124
+ * 删除版本(通过版本ID)
1125
+ */
1126
+ async deleteVersion(groupId, fileId, versionId) {
1127
+ const versionFile = path.join(this.groupsDir, groupId, 'files', fileId, 'versions', `${versionId}.json`)
1128
+ await fs.unlink(versionFile)
1129
+
1130
+ // 更新索引中的版本计数
1131
+ const versions = await this.listVersions(groupId, fileId)
1132
+ await this._updateInIndex(groupId, fileId, {
1133
+ versionCount: versions.length,
1134
+ })
1135
+ }
1136
+
1137
+ // ========================================
1138
+ // 内部辅助方法
1139
+ // ========================================
1140
+
1141
+ /**
1142
+ * 获取当前组ID
1143
+ * 从 _meta.json 读取
1144
+ */
1145
+ async getCurrentGroupId() {
1146
+ const meta = await this.getGroupsMeta()
1147
+ return meta.currentGroupId || 'default'
1148
+ }
1149
+
1150
+ /**
1151
+ * 设置当前组ID
1152
+ * 写入 _meta.json
1153
+ */
1154
+ async setCurrentGroupId(groupId) {
1155
+ const meta = await this.getGroupsMeta()
1156
+ meta.currentGroupId = groupId
1157
+ await this.setGroupsMeta(meta)
1158
+ }
1159
+
1160
+ /**
1161
+ * 验证数据是否可以安全序列化为 JSON
1162
+ * @param {*} data - 要验证的数据
1163
+ * @returns {Object} { valid: boolean, error?: string }
1164
+ */
1165
+ _validateJSON(data) {
1166
+ try {
1167
+ const json = JSON.stringify(data)
1168
+ JSON.parse(json)
1169
+ return { valid: true }
1170
+ } catch (err) {
1171
+ return {
1172
+ valid: false,
1173
+ error: `Invalid JSON: ${err.message}`,
1174
+ }
1175
+ }
1176
+ }
1177
+
1178
+ /**
1179
+ * 写入JSON文件(原子操作)
1180
+ * 使用临时文件+原子重命名模式,确保并发安全
1181
+ */
1182
+ async _writeJSON(filePath, data) {
1183
+ // 确保目标目录存在
1184
+ const dir = path.dirname(filePath)
1185
+ await fs.mkdir(dir, { recursive: true })
1186
+
1187
+ // 生成唯一的临时文件名(时间戳 + 随机数)
1188
+ const tmpPath = `${filePath}.tmp.${Date.now()}.${Math.random().toString(36).slice(2)}`
1189
+
1190
+ try {
1191
+ // 写入临时文件
1192
+ const content = JSON.stringify(data, null, 2)
1193
+ await fs.writeFile(tmpPath, content, 'utf8')
1194
+
1195
+ // 原子重命名
1196
+ await fs.rename(tmpPath, filePath)
1197
+ } catch (err) {
1198
+ // 清理临时文件
1199
+ try {
1200
+ await fs.unlink(tmpPath)
1201
+ } catch (unlinkErr) {
1202
+ // 忽略清理错误
1203
+ }
1204
+ throw err
1205
+ }
1206
+ }
1207
+
1208
+ /**
1209
+ * 获取索引锁(基于 Promise 的互斥锁)
1210
+ */
1211
+ async _acquireIndexLock(groupId) {
1212
+ while (this.indexLocks.has(groupId)) {
1213
+ await this.indexLocks.get(groupId)
1214
+ }
1215
+ let resolve
1216
+ const promise = new Promise(_resolve => { resolve = _resolve })
1217
+ this.indexLocks.set(groupId, promise)
1218
+ return resolve
1219
+ }
1220
+
1221
+ /**
1222
+ * 释放索引锁
1223
+ */
1224
+ _releaseIndexLock(groupId, resolve) {
1225
+ this.indexLocks.delete(groupId)
1226
+ resolve()
1227
+ }
1228
+
1229
+ /**
1230
+ * 添加到索引(带去重检查和并发保护)
1231
+ */
1232
+ async _addToIndex(groupId, fileEntry) {
1233
+ const release = await this._acquireIndexLock(groupId)
1234
+ try {
1235
+ // ⚠️ 强制清除缓存,确保读取最新数据
1236
+ this.invalidateIndex(groupId)
1237
+ const index = await this.getIndex(groupId)
1238
+
1239
+ // 检查文件是否已存在索引中(防止并发重复添加)
1240
+ const exists = index.files.some(f => f.id === fileEntry.id)
1241
+ if (exists) {
1242
+ console.log(`[storage-v3] 文件已在索引中,跳过添加: ${groupId}/${fileEntry.id}`)
1243
+ return
1244
+ }
1245
+
1246
+ index.files.push(fileEntry)
1247
+ await this.updateIndex(groupId, index)
1248
+ } finally {
1249
+ this._releaseIndexLock(groupId, release)
1250
+ }
1251
+ }
1252
+
1253
+ /**
1254
+ * 更新索引中的文件(带并发保护)
1255
+ * 如果文件不在索引中,自动添加到索引
1256
+ */
1257
+ async _updateInIndex(groupId, fileId, updates) {
1258
+ const release = await this._acquireIndexLock(groupId)
1259
+ try {
1260
+ // 强制清除缓存,确保读取最新数据
1261
+ this.invalidateIndex(groupId)
1262
+ const index = await this.getIndex(groupId)
1263
+ const fileIndex = index.files.findIndex(f => f.id === fileId)
1264
+
1265
+ if (fileIndex !== -1) {
1266
+ // 文件存在于索引,更新
1267
+ index.files[fileIndex] = {
1268
+ ...index.files[fileIndex],
1269
+ ...updates,
1270
+ updateTime: Date.now(),
1271
+ }
1272
+ await this.updateIndex(groupId, index)
1273
+ } else {
1274
+ // 文件不在索引中,添加到索引
1275
+ console.log(`[storage-v3] 文件不在索引中,自动添加: ${groupId}/${fileId}`)
1276
+ // 注意:这里已经持有锁,直接操作索引而不是递归调用 _addToIndex
1277
+ const exists = index.files.some(f => f.id === fileId)
1278
+ if (!exists) {
1279
+ index.files.push({
1280
+ id: fileId,
1281
+ ...updates,
1282
+ createTime: updates.createTime || Date.now(),
1283
+ updateTime: updates.updateTime || Date.now(),
1284
+ })
1285
+ await this.updateIndex(groupId, index)
1286
+ }
1287
+ }
1288
+ } finally {
1289
+ this._releaseIndexLock(groupId, release)
1290
+ }
1291
+ }
1292
+
1293
+ /**
1294
+ * 从索引中移除(带并发保护)
1295
+ */
1296
+ async _removeFromIndex(groupId, fileId) {
1297
+ const release = await this._acquireIndexLock(groupId)
1298
+ try {
1299
+ this.invalidateIndex(groupId)
1300
+ const index = await this.getIndex(groupId)
1301
+ index.files = index.files.filter(f => f.id !== fileId)
1302
+ await this.updateIndex(groupId, index)
1303
+ } finally {
1304
+ this._releaseIndexLock(groupId, release)
1305
+ }
1306
+ }
1307
+
1308
+ /**
1309
+ * 更新组统计信息
1310
+ */
1311
+ async _updateGroupStats(groupId) {
1312
+ const index = await this.getIndex(groupId)
1313
+ const totalSize = index.files.reduce((sum, file) => sum + (file.size || 0), 0)
1314
+
1315
+ await this.updateGroup(groupId, { totalSize })
1316
+ }
1317
+
1318
+ /**
1319
+ * 计算文件大小
1320
+ */
1321
+ _calculateSize(fileData) {
1322
+ return Buffer.byteLength(JSON.stringify(fileData), 'utf8')
1323
+ }
1324
+
1325
+ /**
1326
+ * 解析 URL 并提取增强字段(domain、pathname、urlHash)
1327
+ * @param {string} url - 完整的 URL
1328
+ * @returns {Object} { domain, pathname, urlHash }
1329
+ */
1330
+ _parseUrlFields(url) {
1331
+ try {
1332
+ const urlObj = new URL(url)
1333
+ const domain = urlObj.hostname || ''
1334
+ const pathname = urlObj.pathname || '/'
1335
+
1336
+ // 生成 URL 哈希(用于快速查找)
1337
+ const crypto = require('crypto')
1338
+ const urlHash = crypto.createHash('md5').update(url).digest('hex').substring(0, 8)
1339
+
1340
+ return { domain, pathname, urlHash }
1341
+ } catch (err) {
1342
+ // URL 解析失败,返回默认值
1343
+ console.warn(`[storage-v3] URL 解析失败: ${url}`, err.message)
1344
+ return {
1345
+ domain: '',
1346
+ pathname: '',
1347
+ urlHash: '',
1348
+ }
1349
+ }
1350
+ }
1351
+
1352
+ // ========================================
1353
+ // 全局配置操作(存储在 _meta.json.config)
1354
+ // ========================================
1355
+
1356
+ /**
1357
+ * 获取全局配置
1358
+ * @param {string} key - 配置键(如 'mode', 'enabled')
1359
+ * @returns {Promise<*>} 配置值
1360
+ */
1361
+ async getConfig(key) {
1362
+ try {
1363
+ const meta = await this.getGroupsMeta()
1364
+ return meta.config?.[key]
1365
+ } catch (err) {
1366
+ console.error('[storage-v3] getConfig failed:', key, err.message)
1367
+ return undefined
1368
+ }
1369
+ }
1370
+
1371
+ /**
1372
+ * 设置全局配置
1373
+ * @param {string} key - 配置键
1374
+ * @param {*} value - 配置值
1375
+ */
1376
+ async setConfig(key, value) {
1377
+ try {
1378
+ const meta = await this.getGroupsMeta()
1379
+ if (!meta.config) {
1380
+ meta.config = {}
1381
+ }
1382
+ meta.config[key] = value
1383
+ await this.setGroupsMeta(meta)
1384
+ } catch (err) {
1385
+ console.error('[storage-v3] setConfig failed:', key, err.message)
1386
+ }
1387
+ }
1388
+
1389
+ /**
1390
+ * 获取完整的配置对象
1391
+ * @returns {Promise<Object>} 配置对象
1392
+ */
1393
+ async getAllConfig() {
1394
+ try {
1395
+ const meta = await this.getGroupsMeta()
1396
+ return meta.config || {}
1397
+ } catch (err) {
1398
+ console.error('[storage-v3] getAllConfig failed:', err.message)
1399
+ return {}
1400
+ }
1401
+ }
1402
+
1403
+ /**
1404
+ * 设置完整的配置对象
1405
+ * @param {Object} config - 配置对象
1406
+ */
1407
+ async setAllConfig(config) {
1408
+ try {
1409
+ const meta = await this.getGroupsMeta()
1410
+ meta.config = config
1411
+ await this.setGroupsMeta(meta)
1412
+ } catch (err) {
1413
+ console.error('[storage-v3] setAllConfig failed:', err.message)
1414
+ }
1415
+ }
1416
+ }
1417
+
1418
+ module.exports = FileSystemStorage