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.
- package/README.md +38 -0
- package/index.js +3 -3
- package/lib/config/const.js +138 -0
- package/lib/config/rule-collector.js +81 -0
- package/lib/constants.js +62 -0
- package/lib/core/memory-buffer/index.js +207 -0
- package/lib/core/memory-buffer/shared-instance.js +15 -0
- package/lib/core/plugin-mode-manager.js +74 -0
- package/lib/core/resRulesServer.js +14 -0
- package/lib/core/rulesServer.js +31 -0
- package/lib/core/server-entry/capture-handler.js +191 -0
- package/lib/core/server-entry/request-interceptor.js +82 -0
- package/lib/core/server-entry/response-handler.js +147 -0
- package/lib/core/server-entry/server.js +230 -0
- package/lib/storage/group-manager.js +627 -0
- package/lib/storage/storage-adapter.js +712 -0
- package/lib/storage/storage-v3.js +1418 -0
- package/lib/uiServer/index.js +61 -24
- package/lib/uiServer/router/export/import-export-router.js +459 -0
- package/lib/uiServer/router/files/api-list-router.js +150 -0
- package/lib/uiServer/router/files/batch-operations-router.js +185 -0
- package/lib/uiServer/router/files/file-config-router.js +118 -0
- package/lib/uiServer/router/files/file-crud-router.js +212 -0
- package/lib/uiServer/router/files/file-save-router.js +146 -0
- package/lib/uiServer/router/files/version-router.js +260 -0
- package/lib/uiServer/router/global/plugin-control.js +135 -0
- package/lib/uiServer/router/global/system-info-router.js +386 -0
- package/lib/uiServer/router/{group-router.js → groups/group-router.js} +21 -20
- package/lib/uiServer/router/index.js +38 -1521
- package/lib/uiServer/utils/router-helpers.js +100 -0
- package/lib/uiServer/utils/util.js +172 -0
- package/lib/uiServer/{validator.js → utils/validator.js} +11 -6
- package/lib/utils/archive-utils.js +788 -0
- package/lib/utils/error-handler.js +173 -0
- package/lib/utils/logger.js +79 -0
- package/lib/utils/path-utils.js +147 -0
- package/lib/utils/performance.js +265 -0
- package/lib/utils/utils.js +541 -0
- package/package.json +2 -2
- package/public/js/app.js +3707 -1922
- package/public/js/app.js.map +1 -1
- package/public/js/chunk-vendors.js +5098 -3965
- package/public/js/chunk-vendors.js.map +1 -1
- package/rules.txt +1 -1
- package/CHANGELOG_GROUP_FEATURE.md +0 -468
- package/CHANGELOG_P0_FIXES.md +0 -412
- package/CHANGELOG_P1_OPTIMIZATIONS.md +0 -292
- package/CLAUDE.md +0 -436
- package/GROUP_FEATURE_DESIGN.md +0 -520
- package/lib/const.js +0 -47
- package/lib/group-manager.js +0 -491
- package/lib/resRulesServer.js +0 -9
- package/lib/server.js +0 -249
- package/lib/uiServer/router/version-router.js +0 -205
- package/lib/uiServer/util.js +0 -153
- 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
|