most-box 0.0.1 → 0.0.4

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 (71) hide show
  1. package/README.md +182 -73
  2. package/out/404/index.html +15 -0
  3. package/out/404.html +15 -0
  4. package/out/__next.__PAGE__.txt +9 -0
  5. package/out/__next._full.txt +18 -0
  6. package/out/__next._head.txt +5 -0
  7. package/out/__next._index.txt +6 -0
  8. package/out/__next._tree.txt +2 -0
  9. package/out/_next/static/0h4f4QFk_KC9FlSRfQACk/_buildManifest.js +11 -0
  10. package/out/_next/static/0h4f4QFk_KC9FlSRfQACk/_clientMiddlewareManifest.js +1 -0
  11. package/out/_next/static/0h4f4QFk_KC9FlSRfQACk/_ssgManifest.js +1 -0
  12. package/out/_next/static/chunks/00l-yd3t8dvwz.js +5 -0
  13. package/out/_next/static/chunks/03k8t3tgym~8~.js +1 -0
  14. package/out/_next/static/chunks/03~yq9q893hmn.js +1 -0
  15. package/out/_next/static/chunks/09vfh8lfuacc0.css +1 -0
  16. package/out/_next/static/chunks/0bogtdbh.dcu1.js +1 -0
  17. package/out/_next/static/chunks/0dbhjjzl8qfwv.js +1 -0
  18. package/out/_next/static/chunks/0f73psqhr8dre.css +1 -0
  19. package/out/_next/static/chunks/0fbi7z4_.4j1j.js +1 -0
  20. package/out/_next/static/chunks/0ht900cau6_ur.js +31 -0
  21. package/out/_next/static/chunks/0ohm.ia-4ec60.js +1 -0
  22. package/out/_next/static/chunks/0u5ydb-f0.vxl.js +1 -0
  23. package/out/_next/static/chunks/14t2m1on-s5v~.js +1 -0
  24. package/out/_next/static/chunks/turbopack-076ce9exut_h3.js +1 -0
  25. package/out/_not-found/__next._full.txt +16 -0
  26. package/out/_not-found/__next._head.txt +5 -0
  27. package/out/_not-found/__next._index.txt +6 -0
  28. package/out/_not-found/__next._not-found/__PAGE__.txt +5 -0
  29. package/out/_not-found/__next._not-found.txt +5 -0
  30. package/out/_not-found/__next._tree.txt +2 -0
  31. package/out/_not-found/index.html +15 -0
  32. package/out/_not-found/index.txt +16 -0
  33. package/out/app.css +1535 -0
  34. package/out/bundle.js +107 -0
  35. package/out/bundle.js.map +7 -0
  36. package/out/chat/__next._full.txt +19 -0
  37. package/out/chat/__next._head.txt +5 -0
  38. package/out/chat/__next._index.txt +6 -0
  39. package/out/chat/__next._tree.txt +3 -0
  40. package/out/chat/__next.chat/__PAGE__.txt +9 -0
  41. package/out/chat/__next.chat.txt +5 -0
  42. package/out/chat/index.html +15 -0
  43. package/out/chat/index.txt +19 -0
  44. package/out/chat-page.js +112 -0
  45. package/out/chat.css +378 -0
  46. package/out/favicon.ico +0 -0
  47. package/out/index.html +15 -0
  48. package/out/index.js +148 -0
  49. package/out/index.txt +18 -0
  50. package/package.json +16 -7
  51. package/public/app.css +1535 -0
  52. package/public/bundle.js +10 -14
  53. package/public/bundle.js.map +4 -4
  54. package/public/chat-page.js +112 -0
  55. package/public/chat.css +378 -0
  56. package/public/index.js +148 -0
  57. package/server.js +464 -199
  58. package/src/config.js +36 -8
  59. package/src/core/cid.js +28 -19
  60. package/src/index.js +872 -276
  61. package/src/utils/api.js +6 -0
  62. package/src/utils/security.js +27 -24
  63. package/build.mjs +0 -40
  64. package/public/app.jsx +0 -1335
  65. package/public/icons/apple-touch-icon.png +0 -0
  66. package/public/icons/mask-icon.svg +0 -3
  67. package/public/icons/most.png +0 -0
  68. package/public/icons/pwa-192x192.png +0 -0
  69. package/public/icons/pwa-512x512.png +0 -0
  70. package/public/index.html +0 -15
  71. package/public/index.jsx +0 -5
package/src/index.js CHANGED
@@ -1,13 +1,20 @@
1
1
  /**
2
- * MostBoxEngine - Core P2P Engine
3
- * Platform-agnostic engine for P2P file sharing using Hyperswarm/Hyperdrive
2
+ * MostBoxEngine - 核心 P2P 引擎
3
+ * 基于 Hyperswarm/Hyperdrive 的跨平台 P2P 文件共享引擎
4
+ *
5
+ * 架构设计:
6
+ * - Hyperdrive: 只负责存储文件内容,key 使用 CID(解耦存储与目录结构)
7
+ * - published-files.json: 维护文件元数据和显示路径(用户看到的文件夹结构)
8
+ * - 移动/重命名只需更新 JSON,零成本,不修改 Hyperdrive
4
9
  */
5
10
 
6
11
  import EventEmitter from 'eventemitter3'
7
12
  import Hyperswarm from 'hyperswarm'
8
13
  import Corestore from 'corestore'
9
14
  import Hyperdrive from 'hyperdrive'
15
+ import Hypercore from 'hypercore'
10
16
  import b4a from 'b4a'
17
+ import crypto from 'node:crypto'
11
18
  import { CID } from 'multiformats/cid'
12
19
  import fs from 'node:fs'
13
20
  import path from 'node:path'
@@ -15,7 +22,7 @@ import path from 'node:path'
15
22
  import { calculateCid, parseMostLink } from './core/cid.js'
16
23
  import { sanitizeFilename, validateAndSanitizePath, validateFileSize, checkDirectoryWritable, formatFileSize } from './utils/security.js'
17
24
  import { ValidationError, PathSecurityError, FileSizeError, PeerNotFoundError, IntegrityError, PermissionError, EngineNotInitializedError } from './utils/errors.js'
18
- import { GLOBAL_SHARED_SEED_STRING, MAX_FILE_SIZE, CONNECTION_TIMEOUT, DOWNLOAD_TIMEOUT, SWARM_BOOTSTRAP } from './config.js'
25
+ import { GLOBAL_SHARED_SEED_STRING, MAX_FILE_SIZE, CONNECTION_TIMEOUT, DOWNLOAD_TIMEOUT, SWARM_BOOTSTRAP, MAX_PEERS, SWARM_KEEP_ALIVE_INTERVAL, SWARM_RANDOM_PUNCH_INTERVAL, DRIVE_ENTRY_TIMEOUT, DRIVE_SYNC_TIMEOUT, STREAM_READ_TIMEOUT, DOWNLOAD_POLL_INTERVAL, PROGRESS_THROTTLE, DEFAULT_READ_LIMIT, CHANNEL_NAME_MIN_LENGTH, CHANNEL_NAME_MAX_LENGTH, CHANNEL_NAME_REGEX, CHANNEL_NAME_PREFIX, CHANNEL_TOPIC_STRING, CHANNEL_MESSAGE_LIMIT, MAX_MESSAGE_LENGTH } from './config.js'
19
26
 
20
27
  export class MostBoxEngine extends EventEmitter {
21
28
  #store = null
@@ -25,22 +32,28 @@ export class MostBoxEngine extends EventEmitter {
25
32
  #trashFiles = []
26
33
  #initialized = false
27
34
  #options = null
28
- #activeDownloads = new Map() // taskId -> { aborted, readStream, writeStream }
35
+ #activeDownloads = new Map()
36
+ #drivePromises = new Map()
37
+
38
+ #channels = []
39
+ #channelCores = new Map()
40
+ #channelDiscoveries = new Map()
41
+ #channelPeers = new Map()
29
42
 
30
43
  /**
31
- * Create a new MostBoxEngine instance
32
- * @param {object} options - Configuration options
33
- * @param {string} options.dataPath - Path to store P2P data (required)
34
- * @param {string} [options.downloadPath] - Default download path (optional, defaults to dataPath/downloads)
35
- * @param {number} [options.maxFileSize] - Maximum file size in bytes (default: 100GB)
44
+ * 创建新的 MostBoxEngine 实例
45
+ * @param {object} options - 配置选项
46
+ * @param {string} options.dataPath - 存储 P2P 数据的路径(必填)
47
+ * @param {string} [options.downloadPath] - 默认下载路径(可选,默认为 dataPath/downloads
48
+ * @param {number} [options.maxFileSize] - 最大文件大小(字节)(默认:100GB
36
49
  */
37
50
  constructor(options) {
38
51
  super()
39
-
52
+
40
53
  if (!options || !options.dataPath) {
41
54
  throw new Error('dataPath is required')
42
55
  }
43
-
56
+
44
57
  this.#options = {
45
58
  dataPath: options.dataPath,
46
59
  downloadPath: options.downloadPath || path.join(options.dataPath, 'downloads'),
@@ -49,7 +62,7 @@ export class MostBoxEngine extends EventEmitter {
49
62
  }
50
63
 
51
64
  /**
52
- * Initialize the engine - must be called before other methods
65
+ * 初始化引擎 必须在调用其他方法之前调用
53
66
  */
54
67
  async start() {
55
68
  if (this.#initialized) {
@@ -57,19 +70,17 @@ export class MostBoxEngine extends EventEmitter {
57
70
  }
58
71
 
59
72
  const { dataPath } = this.#options
60
-
73
+
61
74
  console.log(`[MostBox] Initializing engine...`)
62
75
  console.log(`[MostBox] Storage path: ${dataPath}`)
63
-
64
- // Create storage directory if not exists
76
+
65
77
  if (!fs.existsSync(dataPath)) {
66
78
  fs.mkdirSync(dataPath, { recursive: true })
67
79
  }
68
80
 
69
- // Initialize Corestore with global shared seed
70
81
  const GLOBAL_SHARED_SEED = b4a.alloc(32).fill(GLOBAL_SHARED_SEED_STRING)
71
82
  this.#store = new Corestore(dataPath, { primaryKey: GLOBAL_SHARED_SEED, unsafe: true })
72
-
83
+
73
84
  try {
74
85
  await this.#store.ready()
75
86
  console.log(`[MostBox] Corestore ready`)
@@ -90,27 +101,17 @@ export class MostBoxEngine extends EventEmitter {
90
101
  }
91
102
  }
92
103
 
93
- // Initialize Hyperswarm with NAT traversal enabled
94
104
  console.log(`[MostBox] Initializing Hyperswarm...`)
95
105
  this.#swarm = new Hyperswarm({
96
- // Connection settings for better stability
97
- maxPeers: 64,
98
- // DHT bootstrap nodes (same as Keet.io/HyperDHT)
106
+ maxPeers: MAX_PEERS,
99
107
  bootstrap: SWARM_BOOTSTRAP,
100
- // Enable NAT traversal (hole punching)
101
- // firewall function: allow all connections (default behavior)
102
108
  firewall: () => false,
103
- // Connection keep-alive timeout (5 seconds)
104
- connectionKeepAlive: 5000,
105
- // Random punch interval for NAT traversal (20 seconds)
106
- randomPunchInterval: 20000,
107
- // Increase timeouts for unstable networks
109
+ connectionKeepAlive: SWARM_KEEP_ALIVE_INTERVAL,
110
+ randomPunchInterval: SWARM_RANDOM_PUNCH_INTERVAL,
108
111
  handshakeTimeout: CONNECTION_TIMEOUT
109
112
  })
110
113
 
111
- // Handle swarm-level errors
112
114
  this.#swarm.on('error', (err) => {
113
- // Silently handle SSL/network errors - they're non-critical for DHT discovery
114
115
  if (err.code === 'SSL_ERROR' || err.message?.includes('handshake') || err.message?.includes('ECONNRESET')) {
115
116
  console.warn('[MostBox] Network warning (non-critical):', err.message)
116
117
  return
@@ -119,58 +120,97 @@ export class MostBoxEngine extends EventEmitter {
119
120
  this.emit('error', err)
120
121
  })
121
122
 
122
- // Replicate store on new connections
123
123
  this.#swarm.on('connection', (conn, info) => {
124
- console.log(`[MostBox] New peer connection established`)
125
- // Handle connection errors gracefully
126
124
  conn.on('error', (err) => {
127
125
  if (err.code === 'SSL_ERROR' || err.message?.includes('handshake')) {
128
- console.warn('[MostBox] Connection warning:', err.message)
129
126
  return
130
127
  }
131
- console.error('[MostBox] Connection error:', err.message)
132
128
  })
133
129
 
134
130
  this.#store.replicate(conn)
131
+ this.#handleChannelConnection(conn).catch(() => {})
135
132
  this.emit('connection', conn)
136
133
  })
137
134
 
138
- // Load published files metadata
139
135
  this.#publishedFiles = this.#loadPublishedMetadata()
140
136
  console.log(`[MostBox] Loaded ${this.#publishedFiles.length} published files`)
141
-
142
- // Load trash files metadata
137
+
143
138
  this.#trashFiles = this.#loadTrashMetadata()
144
139
  console.log(`[MostBox] Loaded ${this.#trashFiles.length} trash files`)
145
-
140
+
141
+ this.#channels = this.#loadChannelsMetadata()
142
+ console.log(`[MostBox] Loaded ${this.#channels.length} channels`)
143
+
144
+ for (const channel of this.#channels) {
145
+ try {
146
+ const ns = this.#store.namespace(`channel-${channel.name}`)
147
+ const core = ns.get({ key: b4a.from(channel.coreKey, 'hex'), valueEncoding: 'json' })
148
+ await core.ready()
149
+ this.#channelCores.set(channel.name, core)
150
+ this.#channelPeers.set(channel.name, new Map())
151
+
152
+ let lastCoreLength = core.length
153
+ core.on('append', async () => {
154
+ if (core.length > lastCoreLength) {
155
+ for (let i = lastCoreLength; i < core.length; i++) {
156
+ try {
157
+ const entry = await core.get(i)
158
+ if (entry && entry.type === 'message') {
159
+ this.emit('channel:message', { channel: channel.name, message: entry })
160
+ }
161
+ } catch {}
162
+ }
163
+ lastCoreLength = core.length
164
+ }
165
+ })
166
+
167
+ const discoveryKey = b4a.from(channel.discoveryKey, 'hex')
168
+ const discovery = this.#swarm.join(discoveryKey, { server: true, client: true })
169
+ this.#channelDiscoveries.set(channel.name, discovery)
170
+ console.log(`[MostBox] Rejoined channel: ${channel.name}`)
171
+ } catch (err) {
172
+ console.warn(`[MostBox] Failed to rejoin channel ${channel.name}:`, err.message)
173
+ }
174
+ }
175
+
146
176
  this.#initialized = true
147
177
  console.log(`[MostBox] Engine initialized successfully`)
148
178
  this.emit('ready')
149
-
179
+
150
180
  return this
151
181
  }
152
182
 
153
183
  /**
154
- * Stop the engine and cleanup resources
184
+ * 停止引擎并清理资源
155
185
  */
156
186
  async stop() {
157
187
  if (!this.#initialized) {
158
188
  return
159
189
  }
160
190
 
161
- // Close all drives
162
- for (const drive of this.#drives.values()) {
163
- await drive.close()
191
+ for (const task of this.#activeDownloads.values()) {
192
+ task.aborted = true
193
+ if (task.readStream) task.readStream.destroy()
194
+ if (task.writeStream) task.writeStream.destroy()
164
195
  }
196
+ this.#activeDownloads.clear()
197
+
198
+ await Promise.allSettled([...this.#drives.values()].map(d => d.close()))
165
199
  this.#drives.clear()
166
200
 
167
- // Destroy swarm
201
+ for (const core of this.#channelCores.values()) {
202
+ try { await core.close() } catch {}
203
+ }
204
+ this.#channelCores.clear()
205
+ this.#channelDiscoveries.clear()
206
+ this.#channelPeers.clear()
207
+ this.#channels = []
208
+
168
209
  if (this.#swarm) {
169
210
  await this.#swarm.destroy()
170
211
  this.#swarm = null
171
212
  }
172
213
 
173
- // Close store
174
214
  if (this.#store) {
175
215
  await this.#store.close()
176
216
  this.#store = null
@@ -181,8 +221,8 @@ export class MostBoxEngine extends EventEmitter {
181
221
  }
182
222
 
183
223
  /**
184
- * Get the node's public key
185
- * @returns {string} Node ID as hex string
224
+ * 获取节点的公钥
225
+ * @returns {string} 节点 ID(十六进制字符串)
186
226
  */
187
227
  getNodeId() {
188
228
  this.#ensureInitialized()
@@ -190,7 +230,7 @@ export class MostBoxEngine extends EventEmitter {
190
230
  }
191
231
 
192
232
  /**
193
- * Get current network status
233
+ * 获取当前网络状态
194
234
  * @returns {{ peers: number, status: string }}
195
235
  */
196
236
  getNetworkStatus() {
@@ -203,9 +243,10 @@ export class MostBoxEngine extends EventEmitter {
203
243
  }
204
244
 
205
245
  /**
206
- * Publish content to the P2P network
207
- * @param {string|Buffer} content - File path (string) or content (Buffer)
208
- * @param {string} [fileName] - Name for the file (required for Buffer input)
246
+ * 将内容发布到 P2P 网络
247
+ * Hyperdrive 中存储 key '/' + cid,metadata 中存储 displayName(用户看到的路径)
248
+ * @param {string|Buffer} content - 文件路径(字符串)或内容(Buffer
249
+ * @param {string} [fileName] - 文件名(Buffer 输入时必填)
209
250
  * @returns {Promise<{ cid: string, link: string, fileName: string }>}
210
251
  */
211
252
  async publishFile(content, fileName) {
@@ -246,47 +287,61 @@ export class MostBoxEngine extends EventEmitter {
246
287
  this.emit('publish:progress', { stage: 'calculating-cid', file: safeFileName })
247
288
 
248
289
  const { cid: rootCid } = await calculateCid(content)
249
- const hashHex = b4a.toString(rootCid.multihash.digest, 'hex')
250
290
  const cidString = rootCid.toString()
251
291
 
292
+ // 检查相同内容是否已存在
293
+ const existingIndex = this.#publishedFiles.findIndex(f => f.cid === cidString)
294
+ if (existingIndex !== -1) {
295
+ const existing = this.#publishedFiles[existingIndex]
296
+ return {
297
+ cid: cidString,
298
+ link: `most://${cidString}`,
299
+ fileName: existing.fileName,
300
+ alreadyExists: true
301
+ }
302
+ }
303
+
304
+ // 获取或创建该 CID 对应的 drive
305
+ const hashHex = b4a.toString(rootCid.multihash.digest, 'hex')
252
306
  const name = `drive-${hashHex}`
253
307
  let drive = this.#drives.get(name)
254
-
308
+
255
309
  if (!drive) {
256
- drive = new Hyperdrive(this.#store.namespace(name))
257
- await drive.ready()
258
- this.#drives.set(name, drive)
259
-
310
+ drive = await this.#getOrCreateDrive(name, { server: true, client: false })
260
311
  const discovery = this.#swarm.join(drive.discoveryKey, { server: true, client: false })
261
312
  await discovery.flushed()
262
313
  }
263
314
 
264
315
  this.emit('publish:progress', { stage: 'uploading', file: safeFileName })
265
316
 
266
- const ws = drive.createWriteStream(safeFileName)
317
+ // Hyperdrive 中用 CID 作为 key 存储(解耦目录结构)
318
+ const driveKey = '/' + cidString
319
+
320
+ const ws = drive.createWriteStream(driveKey)
267
321
 
268
322
  if (Buffer.isBuffer(content)) {
269
- // Stream buffer in chunks to avoid exceeding Hyperdrive block size limit
270
- const CHUNK_SIZE = 64 * 1024 // 64KB chunks
323
+ const CHUNK_SIZE = 64 * 1024
271
324
  let offset = 0
272
-
273
325
  const waitForDrain = () => new Promise(resolve => ws.once('drain', resolve))
274
326
 
275
- while (offset < content.length) {
276
- const chunk = content.slice(offset, offset + CHUNK_SIZE)
277
- const canContinue = ws.write(chunk)
278
- offset += chunk.length
279
-
280
- if (!canContinue && offset < content.length) {
281
- await waitForDrain()
327
+ try {
328
+ while (offset < content.length) {
329
+ const chunk = content.slice(offset, offset + CHUNK_SIZE)
330
+ const canContinue = ws.write(chunk)
331
+ offset += chunk.length
332
+ if (!canContinue && offset < content.length) {
333
+ await waitForDrain()
334
+ }
282
335
  }
336
+ ws.end()
337
+ await new Promise((resolve, reject) => {
338
+ ws.on('finish', resolve)
339
+ ws.on('error', reject)
340
+ })
341
+ } catch (err) {
342
+ ws.destroy()
343
+ throw err
283
344
  }
284
-
285
- ws.end()
286
- await new Promise((resolve, reject) => {
287
- ws.on('finish', resolve)
288
- ws.on('error', reject)
289
- })
290
345
  } else {
291
346
  const rs = fs.createReadStream(cleanPath)
292
347
  await new Promise((resolve, reject) => {
@@ -297,29 +352,19 @@ export class MostBoxEngine extends EventEmitter {
297
352
  })
298
353
  }
299
354
 
300
- const existingIndex = this.#publishedFiles.findIndex(f => f.cid === cidString)
301
- if (existingIndex !== -1) {
302
- const existing = this.#publishedFiles[existingIndex]
303
- // Same content already exists - return "already exists" regardless of filename
304
- return {
305
- cid: cidString,
306
- link: `most://${cidString}`,
307
- fileName: existing.fileName,
308
- alreadyExists: true
309
- }
310
- } else {
311
- this.#publishedFiles.push({
312
- fileName: safeFileName,
313
- cid: cidString,
314
- publishedAt: new Date().toISOString(),
315
- starred: false
316
- })
317
- }
355
+ // 存储 displayName(用户看到的文件夹路径),不存储 drivePath
356
+ this.#publishedFiles.push({
357
+ fileName: safeFileName,
358
+ cid: cidString,
359
+ driveName: name,
360
+ publishedAt: new Date().toISOString(),
361
+ starred: false
362
+ })
318
363
  this.#savePublishedMetadata()
319
364
 
320
365
  const result = {
321
366
  cid: cidString,
322
- link: `most://${cidString}`,
367
+ link: `most://${cidString}?filename=${encodeURIComponent(safeFileName)}`,
323
368
  fileName: safeFileName
324
369
  }
325
370
 
@@ -328,25 +373,22 @@ export class MostBoxEngine extends EventEmitter {
328
373
  }
329
374
 
330
375
  /**
331
- * Download a file from the P2P network
332
- * @param {string} link - most:// link
333
- * @param {string} [taskId] - Task ID for cancellation
376
+ * P2P 网络下载文件
377
+ * @param {string} link - most:// 链接
378
+ * @param {string} [taskId] - 用于取消的任务 ID
334
379
  * @returns {Promise<{ taskId: string, fileName: string, savedPath: string, alreadyExists?: boolean }>}
335
380
  */
336
381
  async downloadFile(link, taskId = null) {
337
382
  this.#ensureInitialized()
338
383
 
339
- // Generate taskId if not provided
340
384
  taskId = taskId || `dl_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
341
385
 
342
386
  console.log(`[MostBox] Starting download for link: ${link} (taskId: ${taskId})`)
343
387
 
344
- // Register in active downloads
345
388
  const taskState = { aborted: false, readStream: null, writeStream: null }
346
389
  this.#activeDownloads.set(taskId, taskState)
347
390
 
348
391
  try {
349
- // Parse link
350
392
  const parsed = parseMostLink(link)
351
393
  if (parsed.error) {
352
394
  throw new ValidationError(parsed.error)
@@ -354,7 +396,6 @@ export class MostBoxEngine extends EventEmitter {
354
396
  const cidString = parsed.cid
355
397
  console.log(`[MostBox] Parsed CID: ${cidString}`)
356
398
 
357
- // Check if file already exists in published files
358
399
  const existingFile = this.#publishedFiles.find(f => f.cid === cidString)
359
400
  if (existingFile) {
360
401
  console.log(`[MostBox] File already exists: ${existingFile.fileName}`)
@@ -365,50 +406,42 @@ export class MostBoxEngine extends EventEmitter {
365
406
  }
366
407
  }
367
408
 
368
- // Parse CID
409
+ const linkFileName = parsed.fileName
410
+
369
411
  const parsedCid = CID.parse(cidString)
370
- const hashBytes = parsedCid.multihash.digest
371
- const hashHex = b4a.toString(hashBytes, 'hex')
412
+ const hashHex = b4a.toString(parsedCid.multihash.digest, 'hex')
372
413
 
373
- // Check cancellation
374
414
  if (taskState.aborted) throw new Error('Download cancelled')
375
415
 
376
- // Get/Create drive
377
416
  const name = `drive-${hashHex}`
378
417
  let drive = this.#drives.get(name)
379
-
418
+
380
419
  if (!drive) {
381
420
  console.log(`[MostBox] Creating new drive: ${name}`)
382
- drive = new Hyperdrive(this.#store.namespace(name))
383
- await drive.ready()
384
- this.#drives.set(name, drive)
385
-
421
+ drive = await this.#getOrCreateDrive(name, { server: true, client: true })
422
+
386
423
  this.emit('download:status', { taskId, status: 'connecting' })
387
-
424
+
388
425
  console.log(`[MostBox] Joining swarm for drive discovery...`)
389
- // Join as both server and client to allow self-downloads
390
426
  await this.#swarm.join(drive.discoveryKey, { server: true, client: true }).flushed()
391
427
  console.log(`[MostBox] Swarm join flushed`)
392
428
  } else {
393
429
  console.log(`[MostBox] Using existing drive: ${name}`)
394
430
  }
395
431
 
396
- // Check cancellation
397
432
  if (taskState.aborted) throw new Error('Download cancelled')
398
433
 
399
434
  this.emit('download:status', { taskId, status: 'finding-peers' })
400
435
 
401
- // Wait for peers and data to sync
402
- console.log(`[MostBox] Waiting for drive content (timeout: ${DOWNLOAD_TIMEOUT/1000}s)...`)
436
+ console.log(`[MostBox] Waiting for drive content (timeout: ${DOWNLOAD_TIMEOUT / 1000}s)...`)
403
437
  const entries = await this.#waitForDriveContent(drive, DOWNLOAD_TIMEOUT, taskId, taskState)
404
438
 
405
439
  if (entries.length === 0) {
406
440
  console.log(`[MostBox] No entries found after timeout`)
407
-
408
- // 提供更详细的错误信息
441
+
409
442
  const peerCount = this.#swarm.connections.size
410
443
  let errorMessage = 'No files found in drive. '
411
-
444
+
412
445
  if (peerCount === 0) {
413
446
  errorMessage += 'Could not connect to any peers. This may be due to:\n'
414
447
  errorMessage += '1. Network firewall blocking P2P connections\n'
@@ -421,29 +454,28 @@ export class MostBoxEngine extends EventEmitter {
421
454
  errorMessage += '2. File may have been removed by publisher\n'
422
455
  errorMessage += '3. File link may be invalid or corrupted'
423
456
  }
424
-
457
+
425
458
  throw new PeerNotFoundError(errorMessage)
426
459
  }
427
460
 
428
- // Check cancellation
429
461
  if (taskState.aborted) throw new Error('Download cancelled')
430
462
 
431
463
  console.log(`[MostBox] Found ${entries.length} entries, starting download...`)
432
464
 
433
- // Save to storage directory (not Downloads folder)
434
465
  const targetDir = this.#options.dataPath
435
466
 
436
- // Check storage directory
437
467
  const writableCheck = await checkDirectoryWritable(targetDir)
438
468
  if (!writableCheck.writable) {
439
469
  throw new PermissionError(writableCheck.error)
440
470
  }
441
471
 
442
- // Download files
472
+ // 下载文件
443
473
  for (const entry of entries) {
444
- const sanitizedFileName = sanitizeFilename(entry.key.replace(/^[\/\\]/, ''))
445
-
446
- // Get file size
474
+ const cleanKey = entry.key.replace(/^[\/\\]/, '')
475
+ const sanitizedFileName = linkFileName
476
+ ? sanitizeFilename(linkFileName)
477
+ : sanitizeFilename(cleanKey)
478
+
447
479
  let totalBytes = 0
448
480
  try {
449
481
  const stat = await drive.entry(entry.key)
@@ -451,56 +483,53 @@ export class MostBoxEngine extends EventEmitter {
451
483
  totalBytes = stat.value.blob.byteLength || 0
452
484
  }
453
485
  } catch {
454
- // Ignore
486
+ // 忽略
455
487
  }
456
488
 
457
489
  const savePath = path.join(targetDir, sanitizedFileName)
458
-
459
- this.emit('download:status', {
490
+
491
+ this.emit('download:status', {
460
492
  taskId,
461
- status: 'downloading',
462
- file: sanitizedFileName,
463
- size: totalBytes ? formatFileSize(totalBytes) : null
493
+ status: 'downloading',
494
+ file: sanitizedFileName,
495
+ size: totalBytes ? formatFileSize(totalBytes) : null
464
496
  })
465
497
 
466
- // Download with progress
467
498
  const rs = drive.createReadStream(entry.key)
468
499
  const ws = fs.createWriteStream(savePath)
469
-
500
+
470
501
  taskState.readStream = rs
471
502
  taskState.writeStream = ws
472
503
 
473
504
  let loadedBytes = 0
474
505
  let lastProgressUpdate = 0
475
-
506
+
476
507
  await new Promise((resolve, reject) => {
477
508
  rs.on('data', (chunk) => {
478
- // Check cancellation
479
509
  if (taskState.aborted) {
480
510
  rs.destroy()
481
511
  ws.destroy()
512
+ fs.unlink(savePath, () => { })
482
513
  reject(new Error('Download cancelled'))
483
514
  return
484
515
  }
485
516
  loadedBytes += chunk.length
486
517
  const now = Date.now()
487
- if (totalBytes > 0 && now - lastProgressUpdate > 500) {
518
+ if (totalBytes > 0 && now - lastProgressUpdate > PROGRESS_THROTTLE) {
488
519
  lastProgressUpdate = now
489
520
  const percent = Math.round((loadedBytes / totalBytes) * 100)
490
521
  this.emit('download:progress', { taskId, loaded: loadedBytes, total: totalBytes, percent })
491
522
  }
492
523
  })
493
-
524
+
494
525
  rs.pipe(ws)
495
526
  ws.on('finish', resolve)
496
527
  ws.on('error', reject)
497
528
  rs.on('error', reject)
498
529
  })
499
530
 
500
- // Check cancellation before verification
501
531
  if (taskState.aborted) throw new Error('Download cancelled')
502
532
 
503
- // Verify integrity
504
533
  this.emit('download:status', { taskId, status: 'verifying' })
505
534
 
506
535
  const { cid: downloadedCid } = await calculateCid(savePath)
@@ -512,13 +541,24 @@ export class MostBoxEngine extends EventEmitter {
512
541
  throw new IntegrityError(`File content CID mismatch. File may be corrupted or tampered.`)
513
542
  }
514
543
 
544
+ // Write file content to Hyperdrive so it can be served for preview
545
+ const driveKey = '/' + cidString
546
+ const readStream = fs.createReadStream(savePath)
547
+ const writeStream = drive.createWriteStream(driveKey)
548
+ await new Promise((resolve, reject) => {
549
+ readStream.pipe(writeStream)
550
+ writeStream.on('finish', resolve)
551
+ writeStream.on('error', reject)
552
+ readStream.on('error', reject)
553
+ })
554
+
515
555
  const result = {
516
556
  taskId,
517
557
  fileName: sanitizedFileName,
518
558
  savedPath: savePath
519
559
  }
520
560
 
521
- // 将下载的文件添加到已发布文件列表
561
+ // 将下载的文件添加到已发布文件列表(displayName 用原始文件名)
522
562
  const existingIndex = this.#publishedFiles.findIndex(f => f.cid === cidString)
523
563
  if (existingIndex !== -1) {
524
564
  const existing = this.#publishedFiles[existingIndex]
@@ -530,6 +570,7 @@ export class MostBoxEngine extends EventEmitter {
530
570
  this.#publishedFiles.push({
531
571
  fileName: sanitizedFileName,
532
572
  cid: cidString,
573
+ driveName: name,
533
574
  publishedAt: new Date().toISOString(),
534
575
  starred: false
535
576
  })
@@ -545,19 +586,19 @@ export class MostBoxEngine extends EventEmitter {
545
586
  }
546
587
 
547
588
  /**
548
- * List all published files
549
- * @param {object} [options] - Filter options
550
- * @param {boolean} [options.starred] - Filter by starred status
589
+ * 列出所有已发布文件
590
+ * @param {object} [options] - 筛选选项
591
+ * @param {boolean} [options.starred] - 按收藏状态筛选
551
592
  * @returns {Array<{ fileName: string, cid: string, link: string, publishedAt: string, starred: boolean }>}
552
593
  */
553
594
  listPublishedFiles(options = {}) {
554
595
  this.#ensureInitialized()
555
596
  let files = this.#publishedFiles
556
-
597
+
557
598
  if (options.starred === true) {
558
599
  files = files.filter(f => f.starred === true)
559
600
  }
560
-
601
+
561
602
  return files.map(f => ({
562
603
  fileName: f.fileName,
563
604
  cid: f.cid,
@@ -566,11 +607,11 @@ export class MostBoxEngine extends EventEmitter {
566
607
  starred: f.starred || false
567
608
  }))
568
609
  }
569
-
610
+
570
611
  /**
571
- * Toggle starred status of a file
572
- * @param {string} cid - CID of the file
573
- * @returns {object} Updated file info
612
+ * 切换文件的收藏状态
613
+ * @param {string} cid - 文件的 CID
614
+ * @returns {object} 更新后的文件信息
574
615
  */
575
616
  toggleStarred(cid) {
576
617
  this.#ensureInitialized()
@@ -587,36 +628,35 @@ export class MostBoxEngine extends EventEmitter {
587
628
  }
588
629
 
589
630
  /**
590
- * Delete a published file - moves to trash instead of permanent deletion
591
- * @param {string} cid - CID of the file to delete
592
- * @returns {Promise<Array>} Updated list of published files
631
+ * 删除已发布文件 移至回收站而非永久删除
632
+ * @param {string} cid - 要删除文件的 CID
633
+ * @returns {Promise<Array>} 更新后的已发布文件列表
593
634
  */
594
635
  async deletePublishedFile(cid) {
595
636
  this.#ensureInitialized()
596
637
  const index = this.#publishedFiles.findIndex(f => f.cid === cid)
597
638
  if (index !== -1) {
598
639
  const fileRecord = this.#publishedFiles[index]
599
-
600
- // Move to trash instead of permanent deletion
640
+
601
641
  this.#trashFiles.push({
602
642
  fileName: fileRecord.fileName,
603
643
  cid: fileRecord.cid,
644
+ driveName: fileRecord.driveName,
604
645
  publishedAt: fileRecord.publishedAt,
605
646
  starred: fileRecord.starred || false,
606
647
  deletedAt: new Date().toISOString()
607
648
  })
608
649
  this.#saveTrashMetadata()
609
-
610
- // Remove from published files
650
+
611
651
  this.#publishedFiles.splice(index, 1)
612
652
  this.#savePublishedMetadata()
613
653
  }
614
654
  return this.listPublishedFiles()
615
655
  }
616
-
656
+
617
657
  /**
618
- * List all files in trash
619
- * @returns {Array} Trash files
658
+ * 列出回收站中的所有文件
659
+ * @returns {Array} 回收站文件
620
660
  */
621
661
  listTrashFiles() {
622
662
  this.#ensureInitialized()
@@ -629,11 +669,11 @@ export class MostBoxEngine extends EventEmitter {
629
669
  deletedAt: f.deletedAt
630
670
  }))
631
671
  }
632
-
672
+
633
673
  /**
634
- * Restore a file from trash
635
- * @param {string} cid - CID of the file to restore
636
- * @returns {Array} Updated list of published files
674
+ * 从回收站恢复文件
675
+ * @param {string} cid - 要恢复文件的 CID
676
+ * @returns {Array} 更新后的已发布文件列表
637
677
  */
638
678
  restoreTrashFile(cid) {
639
679
  this.#ensureInitialized()
@@ -641,120 +681,105 @@ export class MostBoxEngine extends EventEmitter {
641
681
  if (index === -1) {
642
682
  throw new Error('File not found in trash')
643
683
  }
644
-
684
+
645
685
  const fileRecord = this.#trashFiles[index]
646
-
647
- // Restore to published files
686
+
687
+ const parsedCid = CID.parse(fileRecord.cid)
688
+ const hashHex = b4a.toString(parsedCid.multihash.digest, 'hex')
689
+ const driveName = `drive-${hashHex}`
690
+
648
691
  this.#publishedFiles.push({
649
692
  fileName: fileRecord.fileName,
650
693
  cid: fileRecord.cid,
694
+ driveName,
651
695
  publishedAt: fileRecord.publishedAt,
652
696
  starred: fileRecord.starred || false
653
697
  })
654
698
  this.#savePublishedMetadata()
655
-
656
- // Remove from trash
699
+
657
700
  this.#trashFiles.splice(index, 1)
658
701
  this.#saveTrashMetadata()
659
-
702
+
660
703
  return this.listPublishedFiles()
661
704
  }
662
-
705
+
663
706
  /**
664
- * Permanently delete a file from trash
665
- * @param {string} cid - CID of the file to permanently delete
666
- * @returns {Promise<Array>} Updated trash list
707
+ * 永久删除回收站中的文件
708
+ * @param {string} cid - 要永久删除文件的 CID
709
+ * @returns {Promise<Array>} 更新后的回收站列表
667
710
  */
668
711
  async permanentDeleteTrashFile(cid) {
669
712
  this.#ensureInitialized()
670
713
  const index = this.#trashFiles.findIndex(f => f.cid === cid)
671
714
  if (index !== -1) {
672
715
  const fileRecord = this.#trashFiles[index]
673
-
674
- // Reconstruct drive name from CID
675
- const parsedCid = CID.parse(cid)
676
- const hashHex = b4a.toString(parsedCid.multihash.digest, 'hex')
677
- const driveName = `drive-${hashHex}`
678
-
679
- // Delete file from Hyperdrive and cleanup drive
716
+ const driveName = fileRecord.driveName
717
+
680
718
  const drive = this.#drives.get(driveName)
681
719
  if (drive) {
682
720
  try {
683
- await drive.del(fileRecord.fileName)
721
+ await drive.del('/' + fileRecord.cid)
684
722
  } catch (err) {
685
- // File may not exist in drive, continue with cleanup
723
+ // 文件可能不存在于驱动器中
686
724
  }
687
-
688
- // Leave swarm for this drive
725
+
689
726
  await this.#swarm.leave(drive.discoveryKey)
690
-
691
- // Close and remove drive
692
727
  await drive.close()
693
728
  this.#drives.delete(driveName)
694
729
  }
695
-
696
- // Remove from trash
730
+
697
731
  this.#trashFiles.splice(index, 1)
698
732
  this.#saveTrashMetadata()
699
733
  }
700
734
  return this.listTrashFiles()
701
735
  }
702
-
736
+
703
737
  /**
704
- * Empty the trash - permanently delete all trash files
705
- * @returns {Promise<Array>} Empty trash list
738
+ * 清空回收站 永久删除所有回收站文件
739
+ * @returns {Promise<Array>} 清空后的回收站列表
706
740
  */
707
741
  async emptyTrash() {
708
742
  this.#ensureInitialized()
709
-
743
+
710
744
  for (const fileRecord of this.#trashFiles) {
711
- // Reconstruct drive name from CID
712
- const parsedCid = CID.parse(fileRecord.cid)
713
- const hashHex = b4a.toString(parsedCid.multihash.digest, 'hex')
714
- const driveName = `drive-${hashHex}`
715
-
716
- // Delete file from Hyperdrive and cleanup drive
745
+ const driveName = fileRecord.driveName
746
+
717
747
  const drive = this.#drives.get(driveName)
718
748
  if (drive) {
719
749
  try {
720
- await drive.del(fileRecord.fileName)
750
+ await drive.del('/' + fileRecord.cid)
721
751
  } catch (err) {
722
- // File may not exist in drive, continue with cleanup
752
+ // 文件可能不存在
723
753
  }
724
-
725
- // Leave swarm for this drive
726
- this.#swarm.leave(drive.discoveryKey)
727
-
728
- // Close and remove drive
754
+
755
+ await this.#swarm.leave(drive.discoveryKey)
729
756
  await drive.close()
730
757
  this.#drives.delete(driveName)
731
758
  }
732
759
  }
733
-
734
- // Clear trash
760
+
735
761
  this.#trashFiles = []
736
762
  this.#saveTrashMetadata()
737
-
763
+
738
764
  return []
739
765
  }
740
-
766
+
741
767
  /**
742
- * Get storage statistics
768
+ * 获取存储统计信息
743
769
  * @returns {Promise<{ total: number, used: number, free: number, fileCount: number, trashCount: number }>}
744
770
  */
745
771
  async getStorageStats() {
746
772
  this.#ensureInitialized()
747
-
773
+
748
774
  let totalSize = 0
749
775
  let freeSize = 0
750
776
  const { dataPath } = this.#options
751
-
777
+
752
778
  try {
753
779
  const stats = fs.statfsSync(dataPath)
754
780
  totalSize = stats.bsize * stats.blocks
755
781
  freeSize = stats.bsize * stats.bfree
756
782
  } catch (err) {
757
- // Fallback if statfs is not available
758
783
  try {
759
784
  const stats = fs.statSync(dataPath)
760
785
  totalSize = 0
@@ -764,8 +789,7 @@ export class MostBoxEngine extends EventEmitter {
764
789
  freeSize = 0
765
790
  }
766
791
  }
767
-
768
- // Calculate used space by files
792
+
769
793
  let usedSize = 0
770
794
  const calculateDirSize = (dirPath) => {
771
795
  try {
@@ -781,17 +805,17 @@ export class MostBoxEngine extends EventEmitter {
781
805
  const stat = fs.statSync(fullPath)
782
806
  usedSize += stat.size
783
807
  } catch {
784
- // Skip files we can't access
808
+ // 跳过无法访问的文件
785
809
  }
786
810
  }
787
811
  }
788
812
  } catch {
789
- // Skip directories we can't access
813
+ // 跳过无法访问的目录
790
814
  }
791
815
  }
792
-
816
+
793
817
  calculateDirSize(dataPath)
794
-
818
+
795
819
  return {
796
820
  total: totalSize,
797
821
  used: usedSize,
@@ -802,10 +826,11 @@ export class MostBoxEngine extends EventEmitter {
802
826
  }
803
827
 
804
828
  /**
805
- * Move/rename a published file (changes path without re-uploading)
806
- * @param {string} cid - CID of the file to move
807
- * @param {string} newFileName - New file path
808
- * @returns {object} Updated file info
829
+ * 移动/重命名已发布文件
830
+ * 只更新 metadata 中的 displayName,不修改 Hyperdrive
831
+ * @param {string} cid - 要移动文件的 CID
832
+ * @param {string} newFileName - 新文件路径
833
+ * @returns {object} 更新后的文件信息
809
834
  */
810
835
  moveFile(cid, newFileName) {
811
836
  this.#ensureInitialized()
@@ -825,20 +850,22 @@ export class MostBoxEngine extends EventEmitter {
825
850
  }
826
851
 
827
852
  /**
828
- * Rename a folder (renames all files within the folder)
829
- * @param {string} oldPath - Current folder path
830
- * @param {string} newPath - New folder path
831
- * @returns {object} Updated files info
853
+ * 重命名文件夹(重命名文件夹内的所有文件 displayName)
854
+ * 只更新 metadata,不修改 Hyperdrive
855
+ * @param {string} oldPath - 当前文件夹路径
856
+ * @param {string} newPath - 新文件夹路径
857
+ * @returns {object} 更新后的文件信息
832
858
  */
833
859
  renameFolder(oldPath, newPath) {
834
860
  this.#ensureInitialized()
835
861
  const prefix = oldPath + '/'
836
862
  const updatedFiles = []
837
-
863
+
838
864
  for (const file of this.#publishedFiles) {
839
865
  if (file.fileName.startsWith(prefix)) {
840
- const newFileName = newPath + file.fileName.substring(prefix.length)
841
- file.fileName = sanitizeFilename(newFileName)
866
+ const remainder = file.fileName.substring(prefix.length)
867
+ const newFileName = sanitizeFilename(remainder ? newPath + '/' + remainder : newPath)
868
+ file.fileName = newFileName
842
869
  file.publishedAt = new Date().toISOString()
843
870
  updatedFiles.push({
844
871
  cid: file.cid,
@@ -847,19 +874,19 @@ export class MostBoxEngine extends EventEmitter {
847
874
  })
848
875
  }
849
876
  }
850
-
877
+
851
878
  if (updatedFiles.length > 0) {
852
879
  this.#savePublishedMetadata()
853
880
  }
854
-
881
+
855
882
  return { files: updatedFiles }
856
883
  }
857
884
 
858
885
  /**
859
- * Cancel an active download
860
- * @param {string} taskId - The task ID of the download to cancel
886
+ * 取消正在进行的下载
887
+ * @param {string} taskId - 要取消下载的任务 ID
861
888
  */
862
- cancelDownload(taskId) {
889
+ cancelDownload(taskId) {
863
890
  const task = this.#activeDownloads.get(taskId)
864
891
  if (task) {
865
892
  task.aborted = true
@@ -872,7 +899,452 @@ export class MostBoxEngine extends EventEmitter {
872
899
  return this.#publishedFiles
873
900
  }
874
901
 
875
- // --- Private methods ---
902
+ /**
903
+ * 读取已发布文件的内容(用于预览)
904
+ * Hyperdrive 中用 CID 作为 key 存储
905
+ * @param {string} cid - 文件的 CID
906
+ * @param {number} [offset=0] - 读取起始位置
907
+ * @param {number} [limit=10000] - 最大读取字节数
908
+ */
909
+ async readFileContent(cid, offset = 0, limit = DEFAULT_READ_LIMIT) {
910
+ this.#ensureInitialized()
911
+
912
+ const fileRecord = this.#publishedFiles.find(f => f.cid === cid)
913
+ if (!fileRecord) {
914
+ throw new Error('File not found')
915
+ }
916
+
917
+ const drive = await this.#getDriveForFile(fileRecord)
918
+
919
+ // Hyperdrive 中 key 为 '/' + cid
920
+ const driveKey = '/' + cid
921
+ const entry = await drive.entry(driveKey, { wait: true, timeout: DRIVE_ENTRY_TIMEOUT })
922
+ if (!entry || !entry.value) {
923
+ throw new Error('File content not available')
924
+ }
925
+
926
+ const chunks = []
927
+ const stream = drive.createReadStream(driveKey, { start: offset, end: offset + limit - 1 })
928
+
929
+ const timeoutPromise = new Promise((_, reject) => {
930
+ setTimeout(() => reject(new Error('Stream read timeout')), STREAM_READ_TIMEOUT)
931
+ })
932
+
933
+ const readPromise = (async () => {
934
+ for await (const chunk of stream) {
935
+ chunks.push(chunk)
936
+ }
937
+ })()
938
+
939
+ await Promise.race([readPromise, timeoutPromise])
940
+
941
+ const content = Buffer.concat(chunks).toString('utf8')
942
+ const hasMore = chunks.length > 0 && chunks[chunks.length - 1].length === limit
943
+
944
+ return { content, hasMore }
945
+ }
946
+
947
+ /**
948
+ * 读取已发布文件的原始内容(用于预览/下载)
949
+ * Hyperdrive 中用 CID 作为 key 存储
950
+ * @param {string} cid - 文件的 CID
951
+ * @param {object} [options] - 选项
952
+ * @param {number} [options.offset=0] - 读取起始位置
953
+ * @param {number} [options.limit] - 最大读取字节数,不指定则读取到末尾
954
+ * @param {number} [options.timeout=10000] - 流读取超时(毫秒)
955
+ * @returns {Promise<{buffer: Buffer, fileName: string, totalSize: number}>}
956
+ */
957
+ async readFileRaw(cid, options = {}) {
958
+ this.#ensureInitialized()
959
+
960
+ const fileRecord = this.#publishedFiles.find(f => f.cid === cid)
961
+ if (!fileRecord) {
962
+ throw new Error('File not found')
963
+ }
964
+
965
+ const drive = await this.#getDriveForFile(fileRecord)
966
+
967
+ const driveKey = '/' + cid
968
+ const entry = await drive.entry(driveKey, { wait: true, timeout: DRIVE_ENTRY_TIMEOUT })
969
+ if (!entry || !entry.value || !entry.value.blob) {
970
+ throw new Error('File content not available')
971
+ }
972
+
973
+ const totalSize = entry.value.blob.byteLength || 0
974
+
975
+ const { offset = 0, limit, timeout = STREAM_READ_TIMEOUT } = options
976
+ const effectiveLimit = (limit === undefined || limit === null)
977
+ ? totalSize - offset
978
+ : Math.min(limit, totalSize - offset)
979
+
980
+ if (effectiveLimit <= 0) {
981
+ return { buffer: Buffer.alloc(0), fileName: fileRecord.fileName, totalSize }
982
+ }
983
+
984
+ const chunks = []
985
+ const stream = drive.createReadStream(driveKey, {
986
+ start: offset,
987
+ end: offset + effectiveLimit - 1
988
+ })
989
+
990
+ const timeoutPromise = new Promise((_, reject) => {
991
+ setTimeout(() => reject(new Error('Stream read timeout')), timeout)
992
+ })
993
+
994
+ const readPromise = (async () => {
995
+ try {
996
+ for await (const chunk of stream) {
997
+ chunks.push(chunk)
998
+ }
999
+ } catch (err) {
1000
+ if (err.message !== 'Stream read timeout') {
1001
+ throw err
1002
+ }
1003
+ }
1004
+ })()
1005
+
1006
+ await Promise.race([readPromise, timeoutPromise])
1007
+ await readPromise.catch(() => { })
1008
+
1009
+ const buffer = Buffer.concat(chunks)
1010
+ return { buffer, fileName: fileRecord.fileName, totalSize }
1011
+ }
1012
+
1013
+ /**
1014
+ * 获取文件对应的 drive,如果不存在则创建并同步
1015
+ */
1016
+ async #getDriveForFile(fileRecord) {
1017
+ let drive = this.#drives.get(fileRecord.driveName)
1018
+ if (!drive) {
1019
+ drive = await this.#getOrCreateDrive(fileRecord.driveName, { server: true, client: true })
1020
+ }
1021
+ await this.#syncDrive(drive)
1022
+ return drive
1023
+ }
1024
+
1025
+ // --- 频道管理 ---
1026
+
1027
+ /**
1028
+ * 创建或加入频道
1029
+ * @param {string} name - 频道名
1030
+ * @param {string} [type='personal'] - 频道类型
1031
+ * @returns {Promise<{ name: string, key: string }>}
1032
+ */
1033
+ async createChannel(name, type = 'personal') {
1034
+ this.#ensureInitialized()
1035
+
1036
+ if (!CHANNEL_NAME_REGEX.test(name)) {
1037
+ throw new Error('频道名只能包含字母、数字、下划线和连字符')
1038
+ }
1039
+ if (name.length < CHANNEL_NAME_MIN_LENGTH) {
1040
+ throw new Error(`频道名至少 ${CHANNEL_NAME_MIN_LENGTH} 个字符`)
1041
+ }
1042
+ if (name.length > CHANNEL_NAME_MAX_LENGTH) {
1043
+ throw new Error(`频道名最多 ${CHANNEL_NAME_MAX_LENGTH} 个字符`)
1044
+ }
1045
+
1046
+ const existing = this.#channels.find(c => c.name === name)
1047
+ if (existing) {
1048
+ return { name: existing.name, key: existing.coreKey }
1049
+ }
1050
+
1051
+ const ns = this.#store.namespace(`channel-${name}`)
1052
+ const core = ns.get({ name: 'messages', valueEncoding: 'json' })
1053
+ await core.ready()
1054
+
1055
+ const discoveryKey = this.#generateChannelDiscoveryKey(name)
1056
+ const discovery = this.#swarm.join(discoveryKey, { server: true, client: true })
1057
+ await discovery.flushed()
1058
+
1059
+ let lastCoreLength = core.length
1060
+ core.on('append', async () => {
1061
+ if (core.length > lastCoreLength) {
1062
+
1063
+ for (let i = lastCoreLength; i < core.length; i++) {
1064
+ try {
1065
+ const entry = await core.get(i)
1066
+ if (entry && entry.type === 'message') {
1067
+
1068
+ this.emit('channel:message', { channel: name, message: entry })
1069
+ }
1070
+ } catch (err) {
1071
+
1072
+ }
1073
+ }
1074
+ lastCoreLength = core.length
1075
+ }
1076
+ })
1077
+
1078
+ const channelInfo = {
1079
+ name,
1080
+ discoveryKey: b4a.toString(discoveryKey, 'hex'),
1081
+ coreKey: b4a.toString(core.key, 'hex'),
1082
+ createdAt: new Date().toISOString(),
1083
+ type
1084
+ }
1085
+
1086
+ this.#channels.push(channelInfo)
1087
+ this.#channelCores.set(name, core)
1088
+ this.#channelPeers.set(name, new Map())
1089
+ this.#saveChannelsMetadata()
1090
+
1091
+ console.log(`[MostBox] Channel created: ${name}`)
1092
+ this.emit('channel:joined', { name, key: channelInfo.coreKey })
1093
+
1094
+ return { name, key: channelInfo.coreKey }
1095
+ }
1096
+
1097
+ /**
1098
+ * 加入已有频道(通过频道名和 coreKey)
1099
+ * @param {string} name - 频道名
1100
+ * @param {string} [coreKey] - 频道的 coreKey(加入他人创建的频道时必填)
1101
+ * @returns {Promise<{ name: string, key: string }>}
1102
+ */
1103
+ async joinChannel(name, coreKey = null) {
1104
+ this.#ensureInitialized()
1105
+
1106
+ const existing = this.#channels.find(c => c.name === name)
1107
+ if (existing) {
1108
+ return { name: existing.name, key: existing.coreKey }
1109
+ }
1110
+
1111
+ if (!coreKey) {
1112
+ throw new Error('加入已有频道需要提供 coreKey')
1113
+ }
1114
+
1115
+ const ns = this.#store.namespace(`channel-${name}`)
1116
+ const core = ns.get({ key: b4a.from(coreKey, 'hex'), valueEncoding: 'json' })
1117
+ await core.ready()
1118
+
1119
+ const discoveryKey = this.#generateChannelDiscoveryKey(name)
1120
+ const discovery = this.#swarm.join(discoveryKey, { server: true, client: true })
1121
+ await discovery.flushed()
1122
+
1123
+ let lastCoreLength = core.length
1124
+ core.on('append', async () => {
1125
+ if (core.length > lastCoreLength) {
1126
+
1127
+ for (let i = lastCoreLength; i < core.length; i++) {
1128
+ try {
1129
+ const entry = await core.get(i)
1130
+ if (entry && entry.type === 'message') {
1131
+
1132
+ this.emit('channel:message', { channel: name, message: entry })
1133
+ }
1134
+ } catch (err) {
1135
+
1136
+ }
1137
+ }
1138
+ lastCoreLength = core.length
1139
+ }
1140
+ })
1141
+
1142
+ const channelInfo = {
1143
+ name,
1144
+ discoveryKey: b4a.toString(discoveryKey, 'hex'),
1145
+ coreKey,
1146
+ createdAt: new Date().toISOString(),
1147
+ type: 'group'
1148
+ }
1149
+
1150
+ this.#channels.push(channelInfo)
1151
+ this.#channelCores.set(name, core)
1152
+ this.#channelPeers.set(name, new Map())
1153
+ this.#saveChannelsMetadata()
1154
+
1155
+ console.log(`[MostBox] Joined channel: ${name}`)
1156
+ this.emit('channel:joined', { name, key: coreKey })
1157
+
1158
+ return { name, key: coreKey }
1159
+ }
1160
+
1161
+ /**
1162
+ * 离开频道
1163
+ * @param {string} name - 频道名
1164
+ * @returns {Promise<string[]>} 剩余频道列表
1165
+ */
1166
+ async leaveChannel(name) {
1167
+ this.#ensureInitialized()
1168
+
1169
+ const index = this.#channels.findIndex(c => c.name === name)
1170
+ if (index === -1) {
1171
+ throw new Error('频道不存在')
1172
+ }
1173
+
1174
+ const channel = this.#channels[index]
1175
+
1176
+ const discovery = this.#channelDiscoveries.get(name)
1177
+ if (discovery) {
1178
+ try {
1179
+ await this.#swarm.leave(b4a.from(channel.discoveryKey, 'hex'))
1180
+ } catch {}
1181
+ this.#channelDiscoveries.delete(name)
1182
+ }
1183
+
1184
+ const core = this.#channelCores.get(name)
1185
+ if (core) {
1186
+ try {
1187
+ await core.close()
1188
+ } catch {}
1189
+ this.#channelCores.delete(name)
1190
+ }
1191
+
1192
+ this.#channelPeers.delete(name)
1193
+ this.#channels.splice(index, 1)
1194
+ this.#saveChannelsMetadata()
1195
+
1196
+ console.log(`[MostBox] Left channel: ${name}`)
1197
+ this.emit('channel:left', { name })
1198
+
1199
+ return this.listChannels()
1200
+ }
1201
+
1202
+ /**
1203
+ * 列出所有频道
1204
+ * @returns {Array<{ name: string, coreKey: string, createdAt: string, type: string, peerCount: number }>}
1205
+ */
1206
+ listChannels() {
1207
+ this.#ensureInitialized()
1208
+
1209
+ return this.#channels.map(c => ({
1210
+ name: c.name,
1211
+ coreKey: c.coreKey,
1212
+ createdAt: c.createdAt,
1213
+ type: c.type,
1214
+ peerCount: (this.#channelPeers.get(c.name) || new Map()).size
1215
+ }))
1216
+ }
1217
+
1218
+ /**
1219
+ * 获取频道消息
1220
+ * @param {string} name - 频道名
1221
+ * @param {object} [options] - 选项
1222
+ * @param {number} [options.limit=100] - 消息数量
1223
+ * @param {number} [options.offset=0] - 偏移量
1224
+ * @returns {Promise<Array>}
1225
+ */
1226
+ async getChannelMessages(name, options = {}) {
1227
+ this.#ensureInitialized()
1228
+
1229
+ const { limit = CHANNEL_MESSAGE_LIMIT, offset = 0 } = options
1230
+
1231
+ const core = this.#channelCores.get(name)
1232
+ if (!core) {
1233
+ throw new Error('频道未初始化')
1234
+ }
1235
+
1236
+ const messages = []
1237
+ const total = core.length
1238
+ const start = Math.max(0, total - offset - limit)
1239
+ const end = total - offset
1240
+
1241
+ for (let i = start; i < end; i++) {
1242
+ try {
1243
+ const entry = await core.get(i)
1244
+ messages.push(entry)
1245
+ } catch {
1246
+ break
1247
+ }
1248
+ }
1249
+
1250
+ return messages
1251
+ }
1252
+
1253
+ /**
1254
+ * 发送消息到频道
1255
+ * @param {string} name - 频道名
1256
+ * @param {string} content - 消息内容
1257
+ * @param {string} [authorName] - 作者名(可选,默认使用 displayName)
1258
+ * @returns {Promise<object>}
1259
+ */
1260
+ async sendMessage(name, content, authorName = null) {
1261
+ this.#ensureInitialized()
1262
+
1263
+ const core = this.#channelCores.get(name)
1264
+ if (!core) {
1265
+ throw new Error('频道未初始化')
1266
+ }
1267
+
1268
+ if (!content || !content.trim()) {
1269
+ throw new Error('消息内容不能为空')
1270
+ }
1271
+
1272
+ const trimmed = content.trim()
1273
+ if (trimmed.length > MAX_MESSAGE_LENGTH) {
1274
+ throw new Error(`消息内容不能超过 ${MAX_MESSAGE_LENGTH} 字符`)
1275
+ }
1276
+
1277
+ const displayName = this.getNodeId().slice(0, 4)
1278
+
1279
+ const message = {
1280
+ type: 'message',
1281
+ author: this.getNodeId(),
1282
+ authorName: displayName,
1283
+ content: trimmed,
1284
+ timestamp: Date.now()
1285
+ }
1286
+
1287
+ await core.append(message)
1288
+
1289
+ this.emit('channel:message', { channel: name, message })
1290
+
1291
+ return message
1292
+ }
1293
+
1294
+ /**
1295
+ * 获取频道内在线用户
1296
+ * @param {string} name - 频道名
1297
+ * @returns {Array<{ peerId: string, authorName: string, lastSeen: number }>}
1298
+ */
1299
+ getChannelPeers(name) {
1300
+ this.#ensureInitialized()
1301
+
1302
+ const peers = this.#channelPeers.get(name)
1303
+ if (!peers) {
1304
+ throw new Error('频道未初始化')
1305
+ }
1306
+
1307
+ return [...peers.values()].map(p => ({
1308
+ peerId: p.peerId,
1309
+ authorName: p.authorName,
1310
+ lastSeen: p.lastSeen
1311
+ }))
1312
+ }
1313
+
1314
+ /**
1315
+ * 获取显示名
1316
+ * @returns {string|null}
1317
+ */
1318
+ getDisplayName() {
1319
+ try {
1320
+ const configPath = this.#getConfigPath()
1321
+ if (fs.existsSync(configPath)) {
1322
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'))
1323
+ return config.displayName || null
1324
+ }
1325
+ } catch {}
1326
+ return null
1327
+ }
1328
+
1329
+ /**
1330
+ * 设置显示名
1331
+ * @param {string} name - 显示名
1332
+ */
1333
+ setDisplayName(name) {
1334
+ try {
1335
+ const configPath = this.#getConfigPath()
1336
+ const config = fs.existsSync(configPath) ? JSON.parse(fs.readFileSync(configPath, 'utf-8')) : {}
1337
+ config.displayName = name.trim()
1338
+ const tmpPath = configPath + '.tmp'
1339
+ fs.writeFileSync(tmpPath, JSON.stringify(config, null, 2), 'utf-8')
1340
+ fs.renameSync(tmpPath, configPath)
1341
+ return true
1342
+ } catch {
1343
+ return false
1344
+ }
1345
+ }
1346
+
1347
+ // --- 私有方法 ---
876
1348
 
877
1349
  #ensureInitialized() {
878
1350
  if (!this.#initialized) {
@@ -880,21 +1352,61 @@ export class MostBoxEngine extends EventEmitter {
880
1352
  }
881
1353
  }
882
1354
 
1355
+ async #getOrCreateDrive(name, options = { server: true, client: false }) {
1356
+ if (this.#drives.has(name)) return this.#drives.get(name)
1357
+ if (this.#drivePromises.has(name)) return this.#drivePromises.get(name)
1358
+
1359
+ const promise = (async () => {
1360
+ const drive = new Hyperdrive(this.#store.namespace(name))
1361
+ await drive.ready()
1362
+ this.#drives.set(name, drive)
1363
+ return drive
1364
+ })()
1365
+
1366
+ this.#drivePromises.set(name, promise)
1367
+
1368
+ try {
1369
+ const drive = await promise
1370
+ return drive
1371
+ } finally {
1372
+ this.#drivePromises.delete(name)
1373
+ }
1374
+ }
1375
+
1376
+ async #syncDrive(drive, timeout = DRIVE_SYNC_TIMEOUT) {
1377
+ const done = drive.findingPeers()
1378
+ this.#swarm.join(drive.discoveryKey, { server: true, client: true }).flushed().then(done, done)
1379
+ try {
1380
+ const updated = await Promise.race([
1381
+ drive.update(),
1382
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Sync timeout')), timeout))
1383
+ ])
1384
+ return updated
1385
+ } catch {
1386
+ return false
1387
+ }
1388
+ }
1389
+
883
1390
  #getMetadataPath() {
884
1391
  return path.join(this.#options.dataPath, 'published-files.json')
885
1392
  }
886
-
1393
+
887
1394
  #getTrashMetadataPath() {
888
1395
  return path.join(this.#options.dataPath, 'trash-files.json')
889
1396
  }
890
1397
 
1398
+ #atomicWrite(filePath, data) {
1399
+ const tmpPath = filePath + '.tmp'
1400
+ fs.writeFileSync(tmpPath, data, 'utf-8')
1401
+ fs.renameSync(tmpPath, filePath)
1402
+ }
1403
+
891
1404
  #loadPublishedMetadata() {
892
1405
  try {
893
1406
  const metadataPath = this.#getMetadataPath()
894
1407
  if (fs.existsSync(metadataPath)) {
895
1408
  const data = fs.readFileSync(metadataPath, 'utf-8')
896
1409
  const parsed = JSON.parse(data)
897
- // Ensure starred field exists for older data
898
1410
  return parsed.map(f => ({ ...f, starred: f.starred || false }))
899
1411
  }
900
1412
  } catch (err) {
@@ -906,12 +1418,12 @@ export class MostBoxEngine extends EventEmitter {
906
1418
  #savePublishedMetadata() {
907
1419
  try {
908
1420
  const metadataPath = this.#getMetadataPath()
909
- fs.writeFileSync(metadataPath, JSON.stringify(this.#publishedFiles, null, 2), 'utf-8')
1421
+ this.#atomicWrite(metadataPath, JSON.stringify(this.#publishedFiles, null, 2))
910
1422
  } catch (err) {
911
1423
  console.error('Failed to save published metadata:', err.message)
912
1424
  }
913
1425
  }
914
-
1426
+
915
1427
  #loadTrashMetadata() {
916
1428
  try {
917
1429
  const metadataPath = this.#getTrashMetadataPath()
@@ -924,32 +1436,126 @@ export class MostBoxEngine extends EventEmitter {
924
1436
  }
925
1437
  return []
926
1438
  }
927
-
1439
+
928
1440
  #saveTrashMetadata() {
929
1441
  try {
930
1442
  const metadataPath = this.#getTrashMetadataPath()
931
- fs.writeFileSync(metadataPath, JSON.stringify(this.#trashFiles, null, 2), 'utf-8')
1443
+ this.#atomicWrite(metadataPath, JSON.stringify(this.#trashFiles, null, 2))
932
1444
  } catch (err) {
933
1445
  console.error('Failed to save trash metadata:', err.message)
934
1446
  }
935
1447
  }
936
1448
 
1449
+ #getChannelsMetadataPath() {
1450
+ return path.join(this.#options.dataPath, 'channels.json')
1451
+ }
1452
+
1453
+ #getConfigPath() {
1454
+ return path.join(this.#options.dataPath, 'channel-config.json')
1455
+ }
1456
+
1457
+ #loadChannelsMetadata() {
1458
+ try {
1459
+ const metadataPath = this.#getChannelsMetadataPath()
1460
+ if (fs.existsSync(metadataPath)) {
1461
+ const data = fs.readFileSync(metadataPath, 'utf-8')
1462
+ return JSON.parse(data)
1463
+ }
1464
+ } catch (err) {
1465
+ console.warn('Failed to load channels metadata, using empty list:', err.message)
1466
+ }
1467
+ return []
1468
+ }
1469
+
1470
+ #saveChannelsMetadata() {
1471
+ try {
1472
+ const metadataPath = this.#getChannelsMetadataPath()
1473
+ this.#atomicWrite(metadataPath, JSON.stringify(this.#channels, null, 2))
1474
+ } catch (err) {
1475
+ console.error('Failed to save channels metadata:', err.message)
1476
+ }
1477
+ }
1478
+
1479
+ #generateChannelDiscoveryKey(name) {
1480
+ const hash = crypto.createHash('sha256')
1481
+ .update(`${CHANNEL_NAME_PREFIX}${name}`)
1482
+ .digest()
1483
+ return hash
1484
+ }
1485
+
1486
+ async #handleChannelConnection(conn) {
1487
+ const stream = conn
1488
+ let connectedPeerId = null
1489
+ let connectedAuthorName = null
1490
+
1491
+ const helloMessage = JSON.stringify({
1492
+ type: 'channel-hello',
1493
+ peerId: this.getNodeId(),
1494
+ authorName: this.getNodeId().slice(0, 4),
1495
+ channels: this.#channels.map(c => c.name)
1496
+ })
1497
+
1498
+ try {
1499
+ stream.write(helloMessage)
1500
+ } catch {
1501
+ return
1502
+ }
1503
+
1504
+ stream.on('data', async (data) => {
1505
+ try {
1506
+ const msg = JSON.parse(data.toString())
1507
+ if (msg.type === 'channel-hello') {
1508
+ connectedPeerId = msg.peerId
1509
+ connectedAuthorName = msg.authorName
1510
+
1511
+ const theirChannels = new Set(msg.channels || [])
1512
+ for (const [name, peers] of this.#channelPeers) {
1513
+ if (theirChannels.has(name)) {
1514
+ peers.set(msg.peerId, {
1515
+ peerId: msg.peerId,
1516
+ authorName: msg.authorName,
1517
+ lastSeen: Date.now()
1518
+ })
1519
+ const core = this.#channelCores.get(name)
1520
+ if (core) {
1521
+ const ns = this.#store.namespace(`channel-${name}`)
1522
+ ns.replicate(conn).catch(() => {})
1523
+ }
1524
+ }
1525
+ }
1526
+ this.emit('channel:peer:online', { peerId: msg.peerId, authorName: msg.authorName })
1527
+ }
1528
+ } catch {}
1529
+ })
1530
+
1531
+ stream.on('close', () => {
1532
+ if (connectedPeerId) {
1533
+ for (const [name, peers] of this.#channelPeers) {
1534
+ if (peers.has(connectedPeerId)) {
1535
+ const peer = peers.get(connectedPeerId)
1536
+ peers.delete(connectedPeerId)
1537
+ this.emit('channel:peer:offline', { peerId: connectedPeerId, authorName: peer?.authorName })
1538
+ }
1539
+ }
1540
+ }
1541
+ })
1542
+ }
1543
+
937
1544
  /**
938
- * Wait for drive content to be available from peers or local
939
- * @param {Hyperdrive} drive - The drive to check
940
- * @param {number} timeout - Maximum wait time in ms
941
- * @param {string} [taskId] - Task ID for cancellation
942
- * @param {object} [taskState] - Task state object
943
- * @returns {Promise<Array>} - List of entries
1545
+ * 等待驱动器内容从对等节点或本地可用
1546
+ * @param {Hyperdrive} drive - 要检查的驱动器
1547
+ * @param {number} timeout - 最大等待时间(毫秒)
1548
+ * @param {string} [taskId] - 用于取消的任务 ID
1549
+ * @param {object} [taskState] - 任务状态对象
1550
+ * @returns {Promise<Array>} - 条目列表
944
1551
  */
945
1552
  async #waitForDriveContent(drive, timeout, taskId = null, taskState = null) {
946
1553
  const startTime = Date.now()
947
- const checkInterval = 1000 // Check every second
1554
+ const checkInterval = DOWNLOAD_POLL_INTERVAL
948
1555
  let lastPeerCount = 0
949
1556
  let lastStatus = ''
950
1557
  let bootstrapNodesChecked = false
951
1558
 
952
- // First, check if content is already available locally (for self-published files)
953
1559
  const localEntries = []
954
1560
  try {
955
1561
  for await (const entry of drive.list()) {
@@ -961,36 +1567,32 @@ export class MostBoxEngine extends EventEmitter {
961
1567
  return localEntries
962
1568
  }
963
1569
  } catch (err) {
964
- // Continue to peer discovery
1570
+ // 继续进行节点发现
965
1571
  }
966
1572
 
967
1573
  while (Date.now() - startTime < timeout) {
968
- // Check cancellation
969
1574
  if (taskState && taskState.aborted) {
970
1575
  throw new Error('Download cancelled')
971
1576
  }
972
1577
 
973
1578
  const currentTime = Date.now()
974
1579
  const elapsed = Math.round((currentTime - startTime) / 1000)
975
-
976
- // Check if we have peers
1580
+
977
1581
  const currentPeerCount = this.#swarm.connections.size
978
1582
  const hasPeers = currentPeerCount > 0
979
1583
 
980
- // Log peer count changes
981
1584
  if (currentPeerCount !== lastPeerCount) {
982
1585
  console.log(`[MostBox] Peer count changed: ${lastPeerCount} -> ${currentPeerCount} (elapsed: ${elapsed}s)`)
983
1586
  lastPeerCount = currentPeerCount
984
1587
  }
985
1588
 
986
- // Try to list entries (works for both local and synced data)
987
1589
  const entries = []
988
1590
  try {
989
1591
  for await (const entry of drive.list()) {
990
1592
  entries.push(entry)
991
1593
  }
992
1594
  } catch (err) {
993
- // Drive might not be ready yet
1595
+ // 驱动器可能尚未就绪
994
1596
  }
995
1597
 
996
1598
  if (entries.length > 0) {
@@ -999,7 +1601,6 @@ export class MostBoxEngine extends EventEmitter {
999
1601
  return entries
1000
1602
  }
1001
1603
 
1002
- // Update status based on peer connection
1003
1604
  if (hasPeers) {
1004
1605
  const newStatus = 'syncing'
1005
1606
  if (lastStatus !== newStatus) {
@@ -1012,12 +1613,10 @@ export class MostBoxEngine extends EventEmitter {
1012
1613
  this.emit('download:status', { taskId, status: newStatus })
1013
1614
  lastStatus = newStatus
1014
1615
  }
1015
-
1016
- // Log progress every 30 seconds
1616
+
1017
1617
  if (elapsed % 30 === 0 && elapsed > 0) {
1018
- console.log(`[MostBox] Still waiting for peers... (${elapsed}s elapsed, timeout: ${timeout/1000}s)`)
1019
-
1020
- // Check if bootstrap nodes are reachable (only once)
1618
+ console.log(`[MostBox] Still waiting for peers... (${elapsed}s elapsed, timeout: ${timeout / 1000}s)`)
1619
+
1021
1620
  if (!bootstrapNodesChecked && elapsed >= 60) {
1022
1621
  bootstrapNodesChecked = true
1023
1622
  console.log(`[MostBox] No peers found after 60s. This may indicate:`)
@@ -1029,13 +1628,11 @@ export class MostBoxEngine extends EventEmitter {
1029
1628
  }
1030
1629
  }
1031
1630
 
1032
- // Wait before next check
1033
1631
  await new Promise(resolve => setTimeout(resolve, checkInterval))
1034
1632
  }
1035
1633
 
1036
- console.log(`[MostBox] Timeout reached after ${timeout/1000}s, making final attempt...`)
1634
+ console.log(`[MostBox] Timeout reached after ${timeout / 1000}s, making final attempt...`)
1037
1635
 
1038
- // Final attempt - return whatever we have (might be empty)
1039
1636
  const entries = []
1040
1637
  try {
1041
1638
  for await (const entry of drive.list()) {
@@ -1044,30 +1641,29 @@ export class MostBoxEngine extends EventEmitter {
1044
1641
  } catch (err) {
1045
1642
  console.log(`[MostBox] Final attempt failed: ${err.message}`)
1046
1643
  }
1047
-
1644
+
1048
1645
  console.log(`[MostBox] Final entry count: ${entries.length}`)
1049
-
1050
- // Provide detailed error information
1646
+
1051
1647
  if (entries.length === 0) {
1052
1648
  const peerCount = this.#swarm.connections.size
1053
1649
  console.log(`[MostBox] Diagnostic information:`)
1054
1650
  console.log(`[MostBox] - Peer count: ${peerCount}`)
1055
1651
  console.log(`[MostBox] - Bootstrap nodes: ${SWARM_BOOTSTRAP.length}`)
1056
- console.log(`[MostBox] - Timeout: ${timeout/1000}s`)
1057
-
1652
+ console.log(`[MostBox] - Timeout: ${timeout / 1000}s`)
1653
+
1058
1654
  if (peerCount === 0) {
1059
1655
  console.log(`[MostBox] Suggestion: Check network connectivity and firewall settings`)
1060
1656
  } else {
1061
1657
  console.log(`[MostBox] Suggestion: Publisher may be offline or file may have been removed`)
1062
1658
  }
1063
1659
  }
1064
-
1660
+
1065
1661
  return entries
1066
1662
  }
1067
1663
  }
1068
1664
 
1069
- // Re-export utilities
1665
+ // 重新导出工具函数
1070
1666
  export * from './config.js'
1071
1667
  export * from './core/cid.js'
1072
1668
  export * from './utils/errors.js'
1073
- export * from './utils/security.js'
1669
+ export * from './utils/security.js'