most-box 0.0.1 → 0.0.2

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/src/index.js CHANGED
@@ -1,6 +1,11 @@
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'
@@ -15,7 +20,7 @@ import path from 'node:path'
15
20
  import { calculateCid, parseMostLink } from './core/cid.js'
16
21
  import { sanitizeFilename, validateAndSanitizePath, validateFileSize, checkDirectoryWritable, formatFileSize } from './utils/security.js'
17
22
  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'
23
+ 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 } from './config.js'
19
24
 
20
25
  export class MostBoxEngine extends EventEmitter {
21
26
  #store = null
@@ -25,22 +30,23 @@ export class MostBoxEngine extends EventEmitter {
25
30
  #trashFiles = []
26
31
  #initialized = false
27
32
  #options = null
28
- #activeDownloads = new Map() // taskId -> { aborted, readStream, writeStream }
33
+ #activeDownloads = new Map()
34
+ #drivePromises = new Map()
29
35
 
30
36
  /**
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)
37
+ * 创建新的 MostBoxEngine 实例
38
+ * @param {object} options - 配置选项
39
+ * @param {string} options.dataPath - 存储 P2P 数据的路径(必填)
40
+ * @param {string} [options.downloadPath] - 默认下载路径(可选,默认为 dataPath/downloads
41
+ * @param {number} [options.maxFileSize] - 最大文件大小(字节)(默认:100GB
36
42
  */
37
43
  constructor(options) {
38
44
  super()
39
-
45
+
40
46
  if (!options || !options.dataPath) {
41
47
  throw new Error('dataPath is required')
42
48
  }
43
-
49
+
44
50
  this.#options = {
45
51
  dataPath: options.dataPath,
46
52
  downloadPath: options.downloadPath || path.join(options.dataPath, 'downloads'),
@@ -49,7 +55,7 @@ export class MostBoxEngine extends EventEmitter {
49
55
  }
50
56
 
51
57
  /**
52
- * Initialize the engine - must be called before other methods
58
+ * 初始化引擎 必须在调用其他方法之前调用
53
59
  */
54
60
  async start() {
55
61
  if (this.#initialized) {
@@ -57,19 +63,17 @@ export class MostBoxEngine extends EventEmitter {
57
63
  }
58
64
 
59
65
  const { dataPath } = this.#options
60
-
66
+
61
67
  console.log(`[MostBox] Initializing engine...`)
62
68
  console.log(`[MostBox] Storage path: ${dataPath}`)
63
-
64
- // Create storage directory if not exists
69
+
65
70
  if (!fs.existsSync(dataPath)) {
66
71
  fs.mkdirSync(dataPath, { recursive: true })
67
72
  }
68
73
 
69
- // Initialize Corestore with global shared seed
70
74
  const GLOBAL_SHARED_SEED = b4a.alloc(32).fill(GLOBAL_SHARED_SEED_STRING)
71
75
  this.#store = new Corestore(dataPath, { primaryKey: GLOBAL_SHARED_SEED, unsafe: true })
72
-
76
+
73
77
  try {
74
78
  await this.#store.ready()
75
79
  console.log(`[MostBox] Corestore ready`)
@@ -90,27 +94,17 @@ export class MostBoxEngine extends EventEmitter {
90
94
  }
91
95
  }
92
96
 
93
- // Initialize Hyperswarm with NAT traversal enabled
94
97
  console.log(`[MostBox] Initializing Hyperswarm...`)
95
98
  this.#swarm = new Hyperswarm({
96
- // Connection settings for better stability
97
- maxPeers: 64,
98
- // DHT bootstrap nodes (same as Keet.io/HyperDHT)
99
+ maxPeers: MAX_PEERS,
99
100
  bootstrap: SWARM_BOOTSTRAP,
100
- // Enable NAT traversal (hole punching)
101
- // firewall function: allow all connections (default behavior)
102
101
  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
102
+ connectionKeepAlive: SWARM_KEEP_ALIVE_INTERVAL,
103
+ randomPunchInterval: SWARM_RANDOM_PUNCH_INTERVAL,
108
104
  handshakeTimeout: CONNECTION_TIMEOUT
109
105
  })
110
106
 
111
- // Handle swarm-level errors
112
107
  this.#swarm.on('error', (err) => {
113
- // Silently handle SSL/network errors - they're non-critical for DHT discovery
114
108
  if (err.code === 'SSL_ERROR' || err.message?.includes('handshake') || err.message?.includes('ECONNRESET')) {
115
109
  console.warn('[MostBox] Network warning (non-critical):', err.message)
116
110
  return
@@ -119,10 +113,8 @@ export class MostBoxEngine extends EventEmitter {
119
113
  this.emit('error', err)
120
114
  })
121
115
 
122
- // Replicate store on new connections
123
116
  this.#swarm.on('connection', (conn, info) => {
124
117
  console.log(`[MostBox] New peer connection established`)
125
- // Handle connection errors gracefully
126
118
  conn.on('error', (err) => {
127
119
  if (err.code === 'SSL_ERROR' || err.message?.includes('handshake')) {
128
120
  console.warn('[MostBox] Connection warning:', err.message)
@@ -135,42 +127,42 @@ export class MostBoxEngine extends EventEmitter {
135
127
  this.emit('connection', conn)
136
128
  })
137
129
 
138
- // Load published files metadata
139
130
  this.#publishedFiles = this.#loadPublishedMetadata()
140
131
  console.log(`[MostBox] Loaded ${this.#publishedFiles.length} published files`)
141
-
142
- // Load trash files metadata
132
+
143
133
  this.#trashFiles = this.#loadTrashMetadata()
144
134
  console.log(`[MostBox] Loaded ${this.#trashFiles.length} trash files`)
145
-
135
+
146
136
  this.#initialized = true
147
137
  console.log(`[MostBox] Engine initialized successfully`)
148
138
  this.emit('ready')
149
-
139
+
150
140
  return this
151
141
  }
152
142
 
153
143
  /**
154
- * Stop the engine and cleanup resources
144
+ * 停止引擎并清理资源
155
145
  */
156
146
  async stop() {
157
147
  if (!this.#initialized) {
158
148
  return
159
149
  }
160
150
 
161
- // Close all drives
162
- for (const drive of this.#drives.values()) {
163
- await drive.close()
151
+ for (const task of this.#activeDownloads.values()) {
152
+ task.aborted = true
153
+ if (task.readStream) task.readStream.destroy()
154
+ if (task.writeStream) task.writeStream.destroy()
164
155
  }
156
+ this.#activeDownloads.clear()
157
+
158
+ await Promise.allSettled([...this.#drives.values()].map(d => d.close()))
165
159
  this.#drives.clear()
166
160
 
167
- // Destroy swarm
168
161
  if (this.#swarm) {
169
162
  await this.#swarm.destroy()
170
163
  this.#swarm = null
171
164
  }
172
165
 
173
- // Close store
174
166
  if (this.#store) {
175
167
  await this.#store.close()
176
168
  this.#store = null
@@ -181,8 +173,8 @@ export class MostBoxEngine extends EventEmitter {
181
173
  }
182
174
 
183
175
  /**
184
- * Get the node's public key
185
- * @returns {string} Node ID as hex string
176
+ * 获取节点的公钥
177
+ * @returns {string} 节点 ID(十六进制字符串)
186
178
  */
187
179
  getNodeId() {
188
180
  this.#ensureInitialized()
@@ -190,7 +182,7 @@ export class MostBoxEngine extends EventEmitter {
190
182
  }
191
183
 
192
184
  /**
193
- * Get current network status
185
+ * 获取当前网络状态
194
186
  * @returns {{ peers: number, status: string }}
195
187
  */
196
188
  getNetworkStatus() {
@@ -203,9 +195,10 @@ export class MostBoxEngine extends EventEmitter {
203
195
  }
204
196
 
205
197
  /**
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)
198
+ * 将内容发布到 P2P 网络
199
+ * Hyperdrive 中存储 key '/' + cid,metadata 中存储 displayName(用户看到的路径)
200
+ * @param {string|Buffer} content - 文件路径(字符串)或内容(Buffer
201
+ * @param {string} [fileName] - 文件名(Buffer 输入时必填)
209
202
  * @returns {Promise<{ cid: string, link: string, fileName: string }>}
210
203
  */
211
204
  async publishFile(content, fileName) {
@@ -246,47 +239,61 @@ export class MostBoxEngine extends EventEmitter {
246
239
  this.emit('publish:progress', { stage: 'calculating-cid', file: safeFileName })
247
240
 
248
241
  const { cid: rootCid } = await calculateCid(content)
249
- const hashHex = b4a.toString(rootCid.multihash.digest, 'hex')
250
242
  const cidString = rootCid.toString()
251
243
 
244
+ // 检查相同内容是否已存在
245
+ const existingIndex = this.#publishedFiles.findIndex(f => f.cid === cidString)
246
+ if (existingIndex !== -1) {
247
+ const existing = this.#publishedFiles[existingIndex]
248
+ return {
249
+ cid: cidString,
250
+ link: `most://${cidString}`,
251
+ fileName: existing.fileName,
252
+ alreadyExists: true
253
+ }
254
+ }
255
+
256
+ // 获取或创建该 CID 对应的 drive
257
+ const hashHex = b4a.toString(rootCid.multihash.digest, 'hex')
252
258
  const name = `drive-${hashHex}`
253
259
  let drive = this.#drives.get(name)
254
-
260
+
255
261
  if (!drive) {
256
- drive = new Hyperdrive(this.#store.namespace(name))
257
- await drive.ready()
258
- this.#drives.set(name, drive)
259
-
262
+ drive = await this.#getOrCreateDrive(name, { server: true, client: false })
260
263
  const discovery = this.#swarm.join(drive.discoveryKey, { server: true, client: false })
261
264
  await discovery.flushed()
262
265
  }
263
266
 
264
267
  this.emit('publish:progress', { stage: 'uploading', file: safeFileName })
265
268
 
266
- const ws = drive.createWriteStream(safeFileName)
269
+ // Hyperdrive 中用 CID 作为 key 存储(解耦目录结构)
270
+ const driveKey = '/' + cidString
271
+
272
+ const ws = drive.createWriteStream(driveKey)
267
273
 
268
274
  if (Buffer.isBuffer(content)) {
269
- // Stream buffer in chunks to avoid exceeding Hyperdrive block size limit
270
- const CHUNK_SIZE = 64 * 1024 // 64KB chunks
275
+ const CHUNK_SIZE = 64 * 1024
271
276
  let offset = 0
272
-
273
277
  const waitForDrain = () => new Promise(resolve => ws.once('drain', resolve))
274
278
 
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()
279
+ try {
280
+ while (offset < content.length) {
281
+ const chunk = content.slice(offset, offset + CHUNK_SIZE)
282
+ const canContinue = ws.write(chunk)
283
+ offset += chunk.length
284
+ if (!canContinue && offset < content.length) {
285
+ await waitForDrain()
286
+ }
282
287
  }
288
+ ws.end()
289
+ await new Promise((resolve, reject) => {
290
+ ws.on('finish', resolve)
291
+ ws.on('error', reject)
292
+ })
293
+ } catch (err) {
294
+ ws.destroy()
295
+ throw err
283
296
  }
284
-
285
- ws.end()
286
- await new Promise((resolve, reject) => {
287
- ws.on('finish', resolve)
288
- ws.on('error', reject)
289
- })
290
297
  } else {
291
298
  const rs = fs.createReadStream(cleanPath)
292
299
  await new Promise((resolve, reject) => {
@@ -297,24 +304,14 @@ export class MostBoxEngine extends EventEmitter {
297
304
  })
298
305
  }
299
306
 
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
- }
307
+ // 存储 displayName(用户看到的文件夹路径),不存储 drivePath
308
+ this.#publishedFiles.push({
309
+ fileName: safeFileName,
310
+ cid: cidString,
311
+ driveName: name,
312
+ publishedAt: new Date().toISOString(),
313
+ starred: false
314
+ })
318
315
  this.#savePublishedMetadata()
319
316
 
320
317
  const result = {
@@ -328,25 +325,22 @@ export class MostBoxEngine extends EventEmitter {
328
325
  }
329
326
 
330
327
  /**
331
- * Download a file from the P2P network
332
- * @param {string} link - most:// link
333
- * @param {string} [taskId] - Task ID for cancellation
328
+ * P2P 网络下载文件
329
+ * @param {string} link - most:// 链接
330
+ * @param {string} [taskId] - 用于取消的任务 ID
334
331
  * @returns {Promise<{ taskId: string, fileName: string, savedPath: string, alreadyExists?: boolean }>}
335
332
  */
336
333
  async downloadFile(link, taskId = null) {
337
334
  this.#ensureInitialized()
338
335
 
339
- // Generate taskId if not provided
340
336
  taskId = taskId || `dl_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
341
337
 
342
338
  console.log(`[MostBox] Starting download for link: ${link} (taskId: ${taskId})`)
343
339
 
344
- // Register in active downloads
345
340
  const taskState = { aborted: false, readStream: null, writeStream: null }
346
341
  this.#activeDownloads.set(taskId, taskState)
347
342
 
348
343
  try {
349
- // Parse link
350
344
  const parsed = parseMostLink(link)
351
345
  if (parsed.error) {
352
346
  throw new ValidationError(parsed.error)
@@ -354,7 +348,6 @@ export class MostBoxEngine extends EventEmitter {
354
348
  const cidString = parsed.cid
355
349
  console.log(`[MostBox] Parsed CID: ${cidString}`)
356
350
 
357
- // Check if file already exists in published files
358
351
  const existingFile = this.#publishedFiles.find(f => f.cid === cidString)
359
352
  if (existingFile) {
360
353
  console.log(`[MostBox] File already exists: ${existingFile.fileName}`)
@@ -365,50 +358,40 @@ export class MostBoxEngine extends EventEmitter {
365
358
  }
366
359
  }
367
360
 
368
- // Parse CID
369
361
  const parsedCid = CID.parse(cidString)
370
- const hashBytes = parsedCid.multihash.digest
371
- const hashHex = b4a.toString(hashBytes, 'hex')
362
+ const hashHex = b4a.toString(parsedCid.multihash.digest, 'hex')
372
363
 
373
- // Check cancellation
374
364
  if (taskState.aborted) throw new Error('Download cancelled')
375
365
 
376
- // Get/Create drive
377
366
  const name = `drive-${hashHex}`
378
367
  let drive = this.#drives.get(name)
379
-
368
+
380
369
  if (!drive) {
381
370
  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
-
371
+ drive = await this.#getOrCreateDrive(name, { server: true, client: true })
372
+
386
373
  this.emit('download:status', { taskId, status: 'connecting' })
387
-
374
+
388
375
  console.log(`[MostBox] Joining swarm for drive discovery...`)
389
- // Join as both server and client to allow self-downloads
390
376
  await this.#swarm.join(drive.discoveryKey, { server: true, client: true }).flushed()
391
377
  console.log(`[MostBox] Swarm join flushed`)
392
378
  } else {
393
379
  console.log(`[MostBox] Using existing drive: ${name}`)
394
380
  }
395
381
 
396
- // Check cancellation
397
382
  if (taskState.aborted) throw new Error('Download cancelled')
398
383
 
399
384
  this.emit('download:status', { taskId, status: 'finding-peers' })
400
385
 
401
- // Wait for peers and data to sync
402
- console.log(`[MostBox] Waiting for drive content (timeout: ${DOWNLOAD_TIMEOUT/1000}s)...`)
386
+ console.log(`[MostBox] Waiting for drive content (timeout: ${DOWNLOAD_TIMEOUT / 1000}s)...`)
403
387
  const entries = await this.#waitForDriveContent(drive, DOWNLOAD_TIMEOUT, taskId, taskState)
404
388
 
405
389
  if (entries.length === 0) {
406
390
  console.log(`[MostBox] No entries found after timeout`)
407
-
408
- // 提供更详细的错误信息
391
+
409
392
  const peerCount = this.#swarm.connections.size
410
393
  let errorMessage = 'No files found in drive. '
411
-
394
+
412
395
  if (peerCount === 0) {
413
396
  errorMessage += 'Could not connect to any peers. This may be due to:\n'
414
397
  errorMessage += '1. Network firewall blocking P2P connections\n'
@@ -421,29 +404,27 @@ export class MostBoxEngine extends EventEmitter {
421
404
  errorMessage += '2. File may have been removed by publisher\n'
422
405
  errorMessage += '3. File link may be invalid or corrupted'
423
406
  }
424
-
407
+
425
408
  throw new PeerNotFoundError(errorMessage)
426
409
  }
427
410
 
428
- // Check cancellation
429
411
  if (taskState.aborted) throw new Error('Download cancelled')
430
412
 
431
413
  console.log(`[MostBox] Found ${entries.length} entries, starting download...`)
432
414
 
433
- // Save to storage directory (not Downloads folder)
434
415
  const targetDir = this.#options.dataPath
435
416
 
436
- // Check storage directory
437
417
  const writableCheck = await checkDirectoryWritable(targetDir)
438
418
  if (!writableCheck.writable) {
439
419
  throw new PermissionError(writableCheck.error)
440
420
  }
441
421
 
442
- // Download files
422
+ // 下载文件
443
423
  for (const entry of entries) {
444
- const sanitizedFileName = sanitizeFilename(entry.key.replace(/^[\/\\]/, ''))
445
-
446
- // Get file size
424
+ const cleanKey = entry.key.replace(/^[\/\\]/, '')
425
+ // 用原始文件名作为 displayName
426
+ const sanitizedFileName = sanitizeFilename(cleanKey)
427
+
447
428
  let totalBytes = 0
448
429
  try {
449
430
  const stat = await drive.entry(entry.key)
@@ -451,56 +432,53 @@ export class MostBoxEngine extends EventEmitter {
451
432
  totalBytes = stat.value.blob.byteLength || 0
452
433
  }
453
434
  } catch {
454
- // Ignore
435
+ // 忽略
455
436
  }
456
437
 
457
438
  const savePath = path.join(targetDir, sanitizedFileName)
458
-
459
- this.emit('download:status', {
439
+
440
+ this.emit('download:status', {
460
441
  taskId,
461
- status: 'downloading',
462
- file: sanitizedFileName,
463
- size: totalBytes ? formatFileSize(totalBytes) : null
442
+ status: 'downloading',
443
+ file: sanitizedFileName,
444
+ size: totalBytes ? formatFileSize(totalBytes) : null
464
445
  })
465
446
 
466
- // Download with progress
467
447
  const rs = drive.createReadStream(entry.key)
468
448
  const ws = fs.createWriteStream(savePath)
469
-
449
+
470
450
  taskState.readStream = rs
471
451
  taskState.writeStream = ws
472
452
 
473
453
  let loadedBytes = 0
474
454
  let lastProgressUpdate = 0
475
-
455
+
476
456
  await new Promise((resolve, reject) => {
477
457
  rs.on('data', (chunk) => {
478
- // Check cancellation
479
458
  if (taskState.aborted) {
480
459
  rs.destroy()
481
460
  ws.destroy()
461
+ fs.unlink(savePath, () => { })
482
462
  reject(new Error('Download cancelled'))
483
463
  return
484
464
  }
485
465
  loadedBytes += chunk.length
486
466
  const now = Date.now()
487
- if (totalBytes > 0 && now - lastProgressUpdate > 500) {
467
+ if (totalBytes > 0 && now - lastProgressUpdate > PROGRESS_THROTTLE) {
488
468
  lastProgressUpdate = now
489
469
  const percent = Math.round((loadedBytes / totalBytes) * 100)
490
470
  this.emit('download:progress', { taskId, loaded: loadedBytes, total: totalBytes, percent })
491
471
  }
492
472
  })
493
-
473
+
494
474
  rs.pipe(ws)
495
475
  ws.on('finish', resolve)
496
476
  ws.on('error', reject)
497
477
  rs.on('error', reject)
498
478
  })
499
479
 
500
- // Check cancellation before verification
501
480
  if (taskState.aborted) throw new Error('Download cancelled')
502
481
 
503
- // Verify integrity
504
482
  this.emit('download:status', { taskId, status: 'verifying' })
505
483
 
506
484
  const { cid: downloadedCid } = await calculateCid(savePath)
@@ -512,13 +490,24 @@ export class MostBoxEngine extends EventEmitter {
512
490
  throw new IntegrityError(`File content CID mismatch. File may be corrupted or tampered.`)
513
491
  }
514
492
 
493
+ // Write file content to Hyperdrive so it can be served for preview
494
+ const driveKey = '/' + cidString
495
+ const readStream = fs.createReadStream(savePath)
496
+ const writeStream = drive.createWriteStream(driveKey)
497
+ await new Promise((resolve, reject) => {
498
+ readStream.pipe(writeStream)
499
+ writeStream.on('finish', resolve)
500
+ writeStream.on('error', reject)
501
+ readStream.on('error', reject)
502
+ })
503
+
515
504
  const result = {
516
505
  taskId,
517
506
  fileName: sanitizedFileName,
518
507
  savedPath: savePath
519
508
  }
520
509
 
521
- // 将下载的文件添加到已发布文件列表
510
+ // 将下载的文件添加到已发布文件列表(displayName 用原始文件名)
522
511
  const existingIndex = this.#publishedFiles.findIndex(f => f.cid === cidString)
523
512
  if (existingIndex !== -1) {
524
513
  const existing = this.#publishedFiles[existingIndex]
@@ -530,6 +519,7 @@ export class MostBoxEngine extends EventEmitter {
530
519
  this.#publishedFiles.push({
531
520
  fileName: sanitizedFileName,
532
521
  cid: cidString,
522
+ driveName: name,
533
523
  publishedAt: new Date().toISOString(),
534
524
  starred: false
535
525
  })
@@ -545,19 +535,19 @@ export class MostBoxEngine extends EventEmitter {
545
535
  }
546
536
 
547
537
  /**
548
- * List all published files
549
- * @param {object} [options] - Filter options
550
- * @param {boolean} [options.starred] - Filter by starred status
538
+ * 列出所有已发布文件
539
+ * @param {object} [options] - 筛选选项
540
+ * @param {boolean} [options.starred] - 按收藏状态筛选
551
541
  * @returns {Array<{ fileName: string, cid: string, link: string, publishedAt: string, starred: boolean }>}
552
542
  */
553
543
  listPublishedFiles(options = {}) {
554
544
  this.#ensureInitialized()
555
545
  let files = this.#publishedFiles
556
-
546
+
557
547
  if (options.starred === true) {
558
548
  files = files.filter(f => f.starred === true)
559
549
  }
560
-
550
+
561
551
  return files.map(f => ({
562
552
  fileName: f.fileName,
563
553
  cid: f.cid,
@@ -566,11 +556,11 @@ export class MostBoxEngine extends EventEmitter {
566
556
  starred: f.starred || false
567
557
  }))
568
558
  }
569
-
559
+
570
560
  /**
571
- * Toggle starred status of a file
572
- * @param {string} cid - CID of the file
573
- * @returns {object} Updated file info
561
+ * 切换文件的收藏状态
562
+ * @param {string} cid - 文件的 CID
563
+ * @returns {object} 更新后的文件信息
574
564
  */
575
565
  toggleStarred(cid) {
576
566
  this.#ensureInitialized()
@@ -587,36 +577,35 @@ export class MostBoxEngine extends EventEmitter {
587
577
  }
588
578
 
589
579
  /**
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
580
+ * 删除已发布文件 移至回收站而非永久删除
581
+ * @param {string} cid - 要删除文件的 CID
582
+ * @returns {Promise<Array>} 更新后的已发布文件列表
593
583
  */
594
584
  async deletePublishedFile(cid) {
595
585
  this.#ensureInitialized()
596
586
  const index = this.#publishedFiles.findIndex(f => f.cid === cid)
597
587
  if (index !== -1) {
598
588
  const fileRecord = this.#publishedFiles[index]
599
-
600
- // Move to trash instead of permanent deletion
589
+
601
590
  this.#trashFiles.push({
602
591
  fileName: fileRecord.fileName,
603
592
  cid: fileRecord.cid,
593
+ driveName: fileRecord.driveName,
604
594
  publishedAt: fileRecord.publishedAt,
605
595
  starred: fileRecord.starred || false,
606
596
  deletedAt: new Date().toISOString()
607
597
  })
608
598
  this.#saveTrashMetadata()
609
-
610
- // Remove from published files
599
+
611
600
  this.#publishedFiles.splice(index, 1)
612
601
  this.#savePublishedMetadata()
613
602
  }
614
603
  return this.listPublishedFiles()
615
604
  }
616
-
605
+
617
606
  /**
618
- * List all files in trash
619
- * @returns {Array} Trash files
607
+ * 列出回收站中的所有文件
608
+ * @returns {Array} 回收站文件
620
609
  */
621
610
  listTrashFiles() {
622
611
  this.#ensureInitialized()
@@ -629,11 +618,11 @@ export class MostBoxEngine extends EventEmitter {
629
618
  deletedAt: f.deletedAt
630
619
  }))
631
620
  }
632
-
621
+
633
622
  /**
634
- * Restore a file from trash
635
- * @param {string} cid - CID of the file to restore
636
- * @returns {Array} Updated list of published files
623
+ * 从回收站恢复文件
624
+ * @param {string} cid - 要恢复文件的 CID
625
+ * @returns {Array} 更新后的已发布文件列表
637
626
  */
638
627
  restoreTrashFile(cid) {
639
628
  this.#ensureInitialized()
@@ -641,120 +630,105 @@ export class MostBoxEngine extends EventEmitter {
641
630
  if (index === -1) {
642
631
  throw new Error('File not found in trash')
643
632
  }
644
-
633
+
645
634
  const fileRecord = this.#trashFiles[index]
646
-
647
- // Restore to published files
635
+
636
+ const parsedCid = CID.parse(fileRecord.cid)
637
+ const hashHex = b4a.toString(parsedCid.multihash.digest, 'hex')
638
+ const driveName = `drive-${hashHex}`
639
+
648
640
  this.#publishedFiles.push({
649
641
  fileName: fileRecord.fileName,
650
642
  cid: fileRecord.cid,
643
+ driveName,
651
644
  publishedAt: fileRecord.publishedAt,
652
645
  starred: fileRecord.starred || false
653
646
  })
654
647
  this.#savePublishedMetadata()
655
-
656
- // Remove from trash
648
+
657
649
  this.#trashFiles.splice(index, 1)
658
650
  this.#saveTrashMetadata()
659
-
651
+
660
652
  return this.listPublishedFiles()
661
653
  }
662
-
654
+
663
655
  /**
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
656
+ * 永久删除回收站中的文件
657
+ * @param {string} cid - 要永久删除文件的 CID
658
+ * @returns {Promise<Array>} 更新后的回收站列表
667
659
  */
668
660
  async permanentDeleteTrashFile(cid) {
669
661
  this.#ensureInitialized()
670
662
  const index = this.#trashFiles.findIndex(f => f.cid === cid)
671
663
  if (index !== -1) {
672
664
  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
665
+ const driveName = fileRecord.driveName
666
+
680
667
  const drive = this.#drives.get(driveName)
681
668
  if (drive) {
682
669
  try {
683
- await drive.del(fileRecord.fileName)
670
+ await drive.del('/' + fileRecord.cid)
684
671
  } catch (err) {
685
- // File may not exist in drive, continue with cleanup
672
+ // 文件可能不存在于驱动器中
686
673
  }
687
-
688
- // Leave swarm for this drive
674
+
689
675
  await this.#swarm.leave(drive.discoveryKey)
690
-
691
- // Close and remove drive
692
676
  await drive.close()
693
677
  this.#drives.delete(driveName)
694
678
  }
695
-
696
- // Remove from trash
679
+
697
680
  this.#trashFiles.splice(index, 1)
698
681
  this.#saveTrashMetadata()
699
682
  }
700
683
  return this.listTrashFiles()
701
684
  }
702
-
685
+
703
686
  /**
704
- * Empty the trash - permanently delete all trash files
705
- * @returns {Promise<Array>} Empty trash list
687
+ * 清空回收站 永久删除所有回收站文件
688
+ * @returns {Promise<Array>} 清空后的回收站列表
706
689
  */
707
690
  async emptyTrash() {
708
691
  this.#ensureInitialized()
709
-
692
+
710
693
  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
694
+ const driveName = fileRecord.driveName
695
+
717
696
  const drive = this.#drives.get(driveName)
718
697
  if (drive) {
719
698
  try {
720
- await drive.del(fileRecord.fileName)
699
+ await drive.del('/' + fileRecord.cid)
721
700
  } catch (err) {
722
- // File may not exist in drive, continue with cleanup
701
+ // 文件可能不存在
723
702
  }
724
-
725
- // Leave swarm for this drive
726
- this.#swarm.leave(drive.discoveryKey)
727
-
728
- // Close and remove drive
703
+
704
+ await this.#swarm.leave(drive.discoveryKey)
729
705
  await drive.close()
730
706
  this.#drives.delete(driveName)
731
707
  }
732
708
  }
733
-
734
- // Clear trash
709
+
735
710
  this.#trashFiles = []
736
711
  this.#saveTrashMetadata()
737
-
712
+
738
713
  return []
739
714
  }
740
-
715
+
741
716
  /**
742
- * Get storage statistics
717
+ * 获取存储统计信息
743
718
  * @returns {Promise<{ total: number, used: number, free: number, fileCount: number, trashCount: number }>}
744
719
  */
745
720
  async getStorageStats() {
746
721
  this.#ensureInitialized()
747
-
722
+
748
723
  let totalSize = 0
749
724
  let freeSize = 0
750
725
  const { dataPath } = this.#options
751
-
726
+
752
727
  try {
753
728
  const stats = fs.statfsSync(dataPath)
754
729
  totalSize = stats.bsize * stats.blocks
755
730
  freeSize = stats.bsize * stats.bfree
756
731
  } catch (err) {
757
- // Fallback if statfs is not available
758
732
  try {
759
733
  const stats = fs.statSync(dataPath)
760
734
  totalSize = 0
@@ -764,8 +738,7 @@ export class MostBoxEngine extends EventEmitter {
764
738
  freeSize = 0
765
739
  }
766
740
  }
767
-
768
- // Calculate used space by files
741
+
769
742
  let usedSize = 0
770
743
  const calculateDirSize = (dirPath) => {
771
744
  try {
@@ -781,17 +754,17 @@ export class MostBoxEngine extends EventEmitter {
781
754
  const stat = fs.statSync(fullPath)
782
755
  usedSize += stat.size
783
756
  } catch {
784
- // Skip files we can't access
757
+ // 跳过无法访问的文件
785
758
  }
786
759
  }
787
760
  }
788
761
  } catch {
789
- // Skip directories we can't access
762
+ // 跳过无法访问的目录
790
763
  }
791
764
  }
792
-
765
+
793
766
  calculateDirSize(dataPath)
794
-
767
+
795
768
  return {
796
769
  total: totalSize,
797
770
  used: usedSize,
@@ -802,10 +775,11 @@ export class MostBoxEngine extends EventEmitter {
802
775
  }
803
776
 
804
777
  /**
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
778
+ * 移动/重命名已发布文件
779
+ * 只更新 metadata 中的 displayName,不修改 Hyperdrive
780
+ * @param {string} cid - 要移动文件的 CID
781
+ * @param {string} newFileName - 新文件路径
782
+ * @returns {object} 更新后的文件信息
809
783
  */
810
784
  moveFile(cid, newFileName) {
811
785
  this.#ensureInitialized()
@@ -825,20 +799,22 @@ export class MostBoxEngine extends EventEmitter {
825
799
  }
826
800
 
827
801
  /**
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
802
+ * 重命名文件夹(重命名文件夹内的所有文件 displayName)
803
+ * 只更新 metadata,不修改 Hyperdrive
804
+ * @param {string} oldPath - 当前文件夹路径
805
+ * @param {string} newPath - 新文件夹路径
806
+ * @returns {object} 更新后的文件信息
832
807
  */
833
808
  renameFolder(oldPath, newPath) {
834
809
  this.#ensureInitialized()
835
810
  const prefix = oldPath + '/'
836
811
  const updatedFiles = []
837
-
812
+
838
813
  for (const file of this.#publishedFiles) {
839
814
  if (file.fileName.startsWith(prefix)) {
840
- const newFileName = newPath + file.fileName.substring(prefix.length)
841
- file.fileName = sanitizeFilename(newFileName)
815
+ const remainder = file.fileName.substring(prefix.length)
816
+ const newFileName = sanitizeFilename(remainder ? newPath + '/' + remainder : newPath)
817
+ file.fileName = newFileName
842
818
  file.publishedAt = new Date().toISOString()
843
819
  updatedFiles.push({
844
820
  cid: file.cid,
@@ -847,19 +823,19 @@ export class MostBoxEngine extends EventEmitter {
847
823
  })
848
824
  }
849
825
  }
850
-
826
+
851
827
  if (updatedFiles.length > 0) {
852
828
  this.#savePublishedMetadata()
853
829
  }
854
-
830
+
855
831
  return { files: updatedFiles }
856
832
  }
857
833
 
858
834
  /**
859
- * Cancel an active download
860
- * @param {string} taskId - The task ID of the download to cancel
835
+ * 取消正在进行的下载
836
+ * @param {string} taskId - 要取消下载的任务 ID
861
837
  */
862
- cancelDownload(taskId) {
838
+ cancelDownload(taskId) {
863
839
  const task = this.#activeDownloads.get(taskId)
864
840
  if (task) {
865
841
  task.aborted = true
@@ -872,7 +848,130 @@ export class MostBoxEngine extends EventEmitter {
872
848
  return this.#publishedFiles
873
849
  }
874
850
 
875
- // --- Private methods ---
851
+ /**
852
+ * 读取已发布文件的内容(用于预览)
853
+ * Hyperdrive 中用 CID 作为 key 存储
854
+ * @param {string} cid - 文件的 CID
855
+ * @param {number} [offset=0] - 读取起始位置
856
+ * @param {number} [limit=10000] - 最大读取字节数
857
+ */
858
+ async readFileContent(cid, offset = 0, limit = DEFAULT_READ_LIMIT) {
859
+ this.#ensureInitialized()
860
+
861
+ const fileRecord = this.#publishedFiles.find(f => f.cid === cid)
862
+ if (!fileRecord) {
863
+ throw new Error('File not found')
864
+ }
865
+
866
+ const drive = await this.#getDriveForFile(fileRecord)
867
+
868
+ // Hyperdrive 中 key 为 '/' + cid
869
+ const driveKey = '/' + cid
870
+ const entry = await drive.entry(driveKey, { wait: true, timeout: DRIVE_ENTRY_TIMEOUT })
871
+ if (!entry || !entry.value) {
872
+ throw new Error('File content not available')
873
+ }
874
+
875
+ const chunks = []
876
+ const stream = drive.createReadStream(driveKey, { start: offset, end: offset + limit - 1 })
877
+
878
+ const timeoutPromise = new Promise((_, reject) => {
879
+ setTimeout(() => reject(new Error('Stream read timeout')), STREAM_READ_TIMEOUT)
880
+ })
881
+
882
+ const readPromise = (async () => {
883
+ for await (const chunk of stream) {
884
+ chunks.push(chunk)
885
+ }
886
+ })()
887
+
888
+ await Promise.race([readPromise, timeoutPromise])
889
+
890
+ const content = Buffer.concat(chunks).toString('utf8')
891
+ const hasMore = chunks.length > 0 && chunks[chunks.length - 1].length === limit
892
+
893
+ return { content, hasMore }
894
+ }
895
+
896
+ /**
897
+ * 读取已发布文件的原始内容(用于预览/下载)
898
+ * Hyperdrive 中用 CID 作为 key 存储
899
+ * @param {string} cid - 文件的 CID
900
+ * @param {object} [options] - 选项
901
+ * @param {number} [options.offset=0] - 读取起始位置
902
+ * @param {number} [options.limit] - 最大读取字节数,不指定则读取到末尾
903
+ * @param {number} [options.timeout=10000] - 流读取超时(毫秒)
904
+ * @returns {Promise<{buffer: Buffer, fileName: string, totalSize: number}>}
905
+ */
906
+ async readFileRaw(cid, options = {}) {
907
+ this.#ensureInitialized()
908
+
909
+ const fileRecord = this.#publishedFiles.find(f => f.cid === cid)
910
+ if (!fileRecord) {
911
+ throw new Error('File not found')
912
+ }
913
+
914
+ const drive = await this.#getDriveForFile(fileRecord)
915
+
916
+ const driveKey = '/' + cid
917
+ const entry = await drive.entry(driveKey, { wait: true, timeout: DRIVE_ENTRY_TIMEOUT })
918
+ if (!entry || !entry.value || !entry.value.blob) {
919
+ throw new Error('File content not available')
920
+ }
921
+
922
+ const totalSize = entry.value.blob.byteLength || 0
923
+
924
+ const { offset = 0, limit, timeout = STREAM_READ_TIMEOUT } = options
925
+ const effectiveLimit = (limit === undefined || limit === null)
926
+ ? totalSize - offset
927
+ : Math.min(limit, totalSize - offset)
928
+
929
+ if (effectiveLimit <= 0) {
930
+ return { buffer: Buffer.alloc(0), fileName: fileRecord.fileName, totalSize }
931
+ }
932
+
933
+ const chunks = []
934
+ const stream = drive.createReadStream(driveKey, {
935
+ start: offset,
936
+ end: offset + effectiveLimit - 1
937
+ })
938
+
939
+ const timeoutPromise = new Promise((_, reject) => {
940
+ setTimeout(() => reject(new Error('Stream read timeout')), timeout)
941
+ })
942
+
943
+ const readPromise = (async () => {
944
+ try {
945
+ for await (const chunk of stream) {
946
+ chunks.push(chunk)
947
+ }
948
+ } catch (err) {
949
+ if (err.message !== 'Stream read timeout') {
950
+ throw err
951
+ }
952
+ }
953
+ })()
954
+
955
+ await Promise.race([readPromise, timeoutPromise])
956
+ await readPromise.catch(() => { })
957
+
958
+ const buffer = Buffer.concat(chunks)
959
+ return { buffer, fileName: fileRecord.fileName, totalSize }
960
+ }
961
+
962
+ /**
963
+ * 获取文件对应的 drive,如果不存在则创建并同步
964
+ */
965
+ async #getDriveForFile(fileRecord) {
966
+ let drive = this.#drives.get(fileRecord.driveName)
967
+ if (!drive) {
968
+ drive = await this.#getOrCreateDrive(fileRecord.driveName, { server: true, client: true })
969
+ }
970
+ await this.#syncDrive(drive)
971
+ return drive
972
+ }
973
+
974
+ // --- 私有方法 ---
876
975
 
877
976
  #ensureInitialized() {
878
977
  if (!this.#initialized) {
@@ -880,21 +979,61 @@ export class MostBoxEngine extends EventEmitter {
880
979
  }
881
980
  }
882
981
 
982
+ async #getOrCreateDrive(name, options = { server: true, client: false }) {
983
+ if (this.#drives.has(name)) return this.#drives.get(name)
984
+ if (this.#drivePromises.has(name)) return this.#drivePromises.get(name)
985
+
986
+ const promise = (async () => {
987
+ const drive = new Hyperdrive(this.#store.namespace(name))
988
+ await drive.ready()
989
+ this.#drives.set(name, drive)
990
+ return drive
991
+ })()
992
+
993
+ this.#drivePromises.set(name, promise)
994
+
995
+ try {
996
+ const drive = await promise
997
+ return drive
998
+ } finally {
999
+ this.#drivePromises.delete(name)
1000
+ }
1001
+ }
1002
+
1003
+ async #syncDrive(drive, timeout = DRIVE_SYNC_TIMEOUT) {
1004
+ const done = drive.findingPeers()
1005
+ this.#swarm.join(drive.discoveryKey, { server: true, client: true }).flushed().then(done, done)
1006
+ try {
1007
+ const updated = await Promise.race([
1008
+ drive.update(),
1009
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Sync timeout')), timeout))
1010
+ ])
1011
+ return updated
1012
+ } catch {
1013
+ return false
1014
+ }
1015
+ }
1016
+
883
1017
  #getMetadataPath() {
884
1018
  return path.join(this.#options.dataPath, 'published-files.json')
885
1019
  }
886
-
1020
+
887
1021
  #getTrashMetadataPath() {
888
1022
  return path.join(this.#options.dataPath, 'trash-files.json')
889
1023
  }
890
1024
 
1025
+ #atomicWrite(filePath, data) {
1026
+ const tmpPath = filePath + '.tmp'
1027
+ fs.writeFileSync(tmpPath, data, 'utf-8')
1028
+ fs.renameSync(tmpPath, filePath)
1029
+ }
1030
+
891
1031
  #loadPublishedMetadata() {
892
1032
  try {
893
1033
  const metadataPath = this.#getMetadataPath()
894
1034
  if (fs.existsSync(metadataPath)) {
895
1035
  const data = fs.readFileSync(metadataPath, 'utf-8')
896
1036
  const parsed = JSON.parse(data)
897
- // Ensure starred field exists for older data
898
1037
  return parsed.map(f => ({ ...f, starred: f.starred || false }))
899
1038
  }
900
1039
  } catch (err) {
@@ -906,12 +1045,12 @@ export class MostBoxEngine extends EventEmitter {
906
1045
  #savePublishedMetadata() {
907
1046
  try {
908
1047
  const metadataPath = this.#getMetadataPath()
909
- fs.writeFileSync(metadataPath, JSON.stringify(this.#publishedFiles, null, 2), 'utf-8')
1048
+ this.#atomicWrite(metadataPath, JSON.stringify(this.#publishedFiles, null, 2))
910
1049
  } catch (err) {
911
1050
  console.error('Failed to save published metadata:', err.message)
912
1051
  }
913
1052
  }
914
-
1053
+
915
1054
  #loadTrashMetadata() {
916
1055
  try {
917
1056
  const metadataPath = this.#getTrashMetadataPath()
@@ -924,32 +1063,31 @@ export class MostBoxEngine extends EventEmitter {
924
1063
  }
925
1064
  return []
926
1065
  }
927
-
1066
+
928
1067
  #saveTrashMetadata() {
929
1068
  try {
930
1069
  const metadataPath = this.#getTrashMetadataPath()
931
- fs.writeFileSync(metadataPath, JSON.stringify(this.#trashFiles, null, 2), 'utf-8')
1070
+ this.#atomicWrite(metadataPath, JSON.stringify(this.#trashFiles, null, 2))
932
1071
  } catch (err) {
933
1072
  console.error('Failed to save trash metadata:', err.message)
934
1073
  }
935
1074
  }
936
1075
 
937
1076
  /**
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
1077
+ * 等待驱动器内容从对等节点或本地可用
1078
+ * @param {Hyperdrive} drive - 要检查的驱动器
1079
+ * @param {number} timeout - 最大等待时间(毫秒)
1080
+ * @param {string} [taskId] - 用于取消的任务 ID
1081
+ * @param {object} [taskState] - 任务状态对象
1082
+ * @returns {Promise<Array>} - 条目列表
944
1083
  */
945
1084
  async #waitForDriveContent(drive, timeout, taskId = null, taskState = null) {
946
1085
  const startTime = Date.now()
947
- const checkInterval = 1000 // Check every second
1086
+ const checkInterval = DOWNLOAD_POLL_INTERVAL
948
1087
  let lastPeerCount = 0
949
1088
  let lastStatus = ''
950
1089
  let bootstrapNodesChecked = false
951
1090
 
952
- // First, check if content is already available locally (for self-published files)
953
1091
  const localEntries = []
954
1092
  try {
955
1093
  for await (const entry of drive.list()) {
@@ -961,36 +1099,32 @@ export class MostBoxEngine extends EventEmitter {
961
1099
  return localEntries
962
1100
  }
963
1101
  } catch (err) {
964
- // Continue to peer discovery
1102
+ // 继续进行节点发现
965
1103
  }
966
1104
 
967
1105
  while (Date.now() - startTime < timeout) {
968
- // Check cancellation
969
1106
  if (taskState && taskState.aborted) {
970
1107
  throw new Error('Download cancelled')
971
1108
  }
972
1109
 
973
1110
  const currentTime = Date.now()
974
1111
  const elapsed = Math.round((currentTime - startTime) / 1000)
975
-
976
- // Check if we have peers
1112
+
977
1113
  const currentPeerCount = this.#swarm.connections.size
978
1114
  const hasPeers = currentPeerCount > 0
979
1115
 
980
- // Log peer count changes
981
1116
  if (currentPeerCount !== lastPeerCount) {
982
1117
  console.log(`[MostBox] Peer count changed: ${lastPeerCount} -> ${currentPeerCount} (elapsed: ${elapsed}s)`)
983
1118
  lastPeerCount = currentPeerCount
984
1119
  }
985
1120
 
986
- // Try to list entries (works for both local and synced data)
987
1121
  const entries = []
988
1122
  try {
989
1123
  for await (const entry of drive.list()) {
990
1124
  entries.push(entry)
991
1125
  }
992
1126
  } catch (err) {
993
- // Drive might not be ready yet
1127
+ // 驱动器可能尚未就绪
994
1128
  }
995
1129
 
996
1130
  if (entries.length > 0) {
@@ -999,7 +1133,6 @@ export class MostBoxEngine extends EventEmitter {
999
1133
  return entries
1000
1134
  }
1001
1135
 
1002
- // Update status based on peer connection
1003
1136
  if (hasPeers) {
1004
1137
  const newStatus = 'syncing'
1005
1138
  if (lastStatus !== newStatus) {
@@ -1012,12 +1145,10 @@ export class MostBoxEngine extends EventEmitter {
1012
1145
  this.emit('download:status', { taskId, status: newStatus })
1013
1146
  lastStatus = newStatus
1014
1147
  }
1015
-
1016
- // Log progress every 30 seconds
1148
+
1017
1149
  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)
1150
+ console.log(`[MostBox] Still waiting for peers... (${elapsed}s elapsed, timeout: ${timeout / 1000}s)`)
1151
+
1021
1152
  if (!bootstrapNodesChecked && elapsed >= 60) {
1022
1153
  bootstrapNodesChecked = true
1023
1154
  console.log(`[MostBox] No peers found after 60s. This may indicate:`)
@@ -1029,13 +1160,11 @@ export class MostBoxEngine extends EventEmitter {
1029
1160
  }
1030
1161
  }
1031
1162
 
1032
- // Wait before next check
1033
1163
  await new Promise(resolve => setTimeout(resolve, checkInterval))
1034
1164
  }
1035
1165
 
1036
- console.log(`[MostBox] Timeout reached after ${timeout/1000}s, making final attempt...`)
1166
+ console.log(`[MostBox] Timeout reached after ${timeout / 1000}s, making final attempt...`)
1037
1167
 
1038
- // Final attempt - return whatever we have (might be empty)
1039
1168
  const entries = []
1040
1169
  try {
1041
1170
  for await (const entry of drive.list()) {
@@ -1044,30 +1173,29 @@ export class MostBoxEngine extends EventEmitter {
1044
1173
  } catch (err) {
1045
1174
  console.log(`[MostBox] Final attempt failed: ${err.message}`)
1046
1175
  }
1047
-
1176
+
1048
1177
  console.log(`[MostBox] Final entry count: ${entries.length}`)
1049
-
1050
- // Provide detailed error information
1178
+
1051
1179
  if (entries.length === 0) {
1052
1180
  const peerCount = this.#swarm.connections.size
1053
1181
  console.log(`[MostBox] Diagnostic information:`)
1054
1182
  console.log(`[MostBox] - Peer count: ${peerCount}`)
1055
1183
  console.log(`[MostBox] - Bootstrap nodes: ${SWARM_BOOTSTRAP.length}`)
1056
- console.log(`[MostBox] - Timeout: ${timeout/1000}s`)
1057
-
1184
+ console.log(`[MostBox] - Timeout: ${timeout / 1000}s`)
1185
+
1058
1186
  if (peerCount === 0) {
1059
1187
  console.log(`[MostBox] Suggestion: Check network connectivity and firewall settings`)
1060
1188
  } else {
1061
1189
  console.log(`[MostBox] Suggestion: Publisher may be offline or file may have been removed`)
1062
1190
  }
1063
1191
  }
1064
-
1192
+
1065
1193
  return entries
1066
1194
  }
1067
1195
  }
1068
1196
 
1069
- // Re-export utilities
1197
+ // 重新导出工具函数
1070
1198
  export * from './config.js'
1071
1199
  export * from './core/cid.js'
1072
1200
  export * from './utils/errors.js'
1073
- export * from './utils/security.js'
1201
+ export * from './utils/security.js'