most-box 0.1.6 → 0.1.8

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 (187) hide show
  1. package/README.md +18 -13
  2. package/out/404/index.html +2 -2
  3. package/out/404.html +2 -2
  4. package/out/__next.__PAGE__.txt +6 -6
  5. package/out/__next._full.txt +15 -15
  6. package/out/__next._head.txt +3 -3
  7. package/out/__next._index.txt +7 -7
  8. package/out/__next._tree.txt +4 -4
  9. package/out/_next/static/chunks/00t5ddkkjek9v.js +1 -0
  10. package/out/_next/static/chunks/0hodisxqbtmye.js +1 -0
  11. package/out/_next/static/chunks/0jinrmgt26crz.js +1 -0
  12. package/out/_next/static/chunks/0mslhkdi-msxv.js +1 -0
  13. package/out/_next/static/chunks/1h-f81ddyfcmx.css +1 -0
  14. package/out/_next/static/chunks/1x76jmdus29em.js +1 -0
  15. package/out/_next/static/chunks/22vt64m0krmev.js +1 -0
  16. package/out/_next/static/chunks/{0vmjat-s4a3yj.css → 258tyt1xt34p4.css} +1 -1
  17. package/out/_next/static/chunks/266xuv6ts5889.js +5 -0
  18. package/out/_next/static/chunks/26hcvxjw9do9d.js +1 -0
  19. package/out/_next/static/chunks/{0nwdp-n94owg0.css → 2jnstwjg45vfp.css} +1 -1
  20. package/out/_next/static/chunks/2o-0n-9r5u1oh.js +1 -0
  21. package/out/_next/static/chunks/2pkm-6u9yq_cq.js +1 -0
  22. package/out/_next/static/chunks/31zdi2pl6em6-.css +1 -0
  23. package/out/_next/static/chunks/393isf5crmqic.js +1 -0
  24. package/out/_next/static/chunks/3e9ia0hs-pqvs.css +1 -0
  25. package/out/_next/static/chunks/3ej6im7cbakx3.js +1 -0
  26. package/out/_next/static/chunks/3ejz1mdljwwqf.css +1 -0
  27. package/out/_next/static/chunks/{0s7i7nfr9j3b1.js → 3m7_o1ihz8ay_.js} +1 -1
  28. package/out/_next/static/chunks/3qm2vbylm324o.css +1 -0
  29. package/out/_next/static/chunks/3uxltn4677hjy.js +1 -0
  30. package/out/_next/static/chunks/3w2qk10_udrd9.js +1 -0
  31. package/out/_next/static/chunks/3xl0fq2d3dle6.js +1 -0
  32. package/out/_next/static/chunks/{23kls47vn_rpg.css → 440es_b4nw-7c.css} +2 -2
  33. package/out/_next/static/chunks/{turbopack-026p0g9v9_pva.js → turbopack-3vvza9m6839dy.js} +1 -1
  34. package/out/_not-found/__next._full.txt +12 -12
  35. package/out/_not-found/__next._head.txt +3 -3
  36. package/out/_not-found/__next._index.txt +7 -7
  37. package/out/_not-found/__next._not-found.__PAGE__.txt +4 -4
  38. package/out/_not-found/__next._not-found.txt +3 -3
  39. package/out/_not-found/__next._tree.txt +2 -2
  40. package/out/_not-found/index.html +2 -2
  41. package/out/_not-found/index.txt +12 -12
  42. package/out/admin/__next._full.txt +13 -13
  43. package/out/admin/__next._head.txt +3 -3
  44. package/out/admin/__next._index.txt +7 -7
  45. package/out/admin/__next._tree.txt +2 -2
  46. package/out/admin/__next.admin.__PAGE__.txt +4 -4
  47. package/out/admin/__next.admin.txt +3 -3
  48. package/out/admin/index.html +2 -2
  49. package/out/admin/index.txt +13 -13
  50. package/out/app/__next._full.txt +13 -13
  51. package/out/app/__next._head.txt +3 -3
  52. package/out/app/__next._index.txt +7 -7
  53. package/out/app/__next._tree.txt +2 -2
  54. package/out/app/__next.app.__PAGE__.txt +4 -4
  55. package/out/app/__next.app.txt +3 -3
  56. package/out/app/index.html +2 -2
  57. package/out/app/index.txt +13 -13
  58. package/out/chat/__next._full.txt +14 -14
  59. package/out/chat/__next._head.txt +3 -3
  60. package/out/chat/__next._index.txt +7 -7
  61. package/out/chat/__next._tree.txt +3 -3
  62. package/out/chat/__next.chat.__PAGE__.txt +4 -4
  63. package/out/chat/__next.chat.txt +4 -4
  64. package/out/chat/index.html +2 -2
  65. package/out/chat/index.txt +14 -14
  66. package/out/chat/join/__next._full.txt +14 -14
  67. package/out/chat/join/__next._head.txt +3 -3
  68. package/out/chat/join/__next._index.txt +7 -7
  69. package/out/chat/join/__next._tree.txt +3 -3
  70. package/out/chat/join/__next.chat.join.__PAGE__.txt +4 -4
  71. package/out/chat/join/__next.chat.join.txt +3 -3
  72. package/out/chat/join/__next.chat.txt +4 -4
  73. package/out/chat/join/index.html +2 -2
  74. package/out/chat/join/index.txt +14 -14
  75. package/out/demo/__next._full.txt +17 -16
  76. package/out/demo/__next._head.txt +3 -3
  77. package/out/demo/__next._index.txt +7 -7
  78. package/out/demo/__next._tree.txt +6 -5
  79. package/out/demo/__next.demo.__PAGE__.txt +8 -7
  80. package/out/demo/__next.demo.txt +3 -3
  81. package/out/demo/index.html +3 -3
  82. package/out/demo/index.txt +17 -16
  83. package/out/download/__next._full.txt +37 -33
  84. package/out/download/__next._head.txt +3 -3
  85. package/out/download/__next._index.txt +7 -7
  86. package/out/download/__next._tree.txt +3 -3
  87. package/out/download/__next.download.__PAGE__.txt +6 -6
  88. package/out/download/__next.download.txt +3 -3
  89. package/out/download/index.html +2 -2
  90. package/out/download/index.txt +37 -33
  91. package/out/game/__next._full.txt +11 -11
  92. package/out/game/__next._head.txt +3 -3
  93. package/out/game/__next._index.txt +7 -7
  94. package/out/game/__next._tree.txt +2 -2
  95. package/out/game/__next.game.__PAGE__.txt +2 -2
  96. package/out/game/__next.game.txt +3 -3
  97. package/out/game/gandengyan/__next._full.txt +14 -14
  98. package/out/game/gandengyan/__next._head.txt +3 -3
  99. package/out/game/gandengyan/__next._index.txt +7 -7
  100. package/out/game/gandengyan/__next._tree.txt +3 -3
  101. package/out/game/gandengyan/__next.game.gandengyan.__PAGE__.txt +5 -5
  102. package/out/game/gandengyan/__next.game.gandengyan.txt +3 -3
  103. package/out/game/gandengyan/__next.game.txt +3 -3
  104. package/out/game/gandengyan/index.html +2 -2
  105. package/out/game/gandengyan/index.txt +14 -14
  106. package/out/game/index.html +1 -1
  107. package/out/game/index.txt +11 -11
  108. package/out/game/zhajinhua/__next._full.txt +14 -13
  109. package/out/game/zhajinhua/__next._head.txt +3 -3
  110. package/out/game/zhajinhua/__next._index.txt +7 -7
  111. package/out/game/zhajinhua/__next._tree.txt +3 -2
  112. package/out/game/zhajinhua/__next.game.txt +3 -3
  113. package/out/game/zhajinhua/__next.game.zhajinhua.__PAGE__.txt +5 -4
  114. package/out/game/zhajinhua/__next.game.zhajinhua.txt +3 -3
  115. package/out/game/zhajinhua/index.html +2 -2
  116. package/out/game/zhajinhua/index.txt +14 -13
  117. package/out/index.html +2 -2
  118. package/out/index.txt +15 -15
  119. package/out/note/__next._full.txt +13 -13
  120. package/out/note/__next._head.txt +3 -3
  121. package/out/note/__next._index.txt +7 -7
  122. package/out/note/__next._tree.txt +2 -2
  123. package/out/note/__next.note.__PAGE__.txt +4 -4
  124. package/out/note/__next.note.txt +3 -3
  125. package/out/note/index.html +2 -2
  126. package/out/note/index.txt +13 -13
  127. package/out/ping/__next._full.txt +14 -14
  128. package/out/ping/__next._head.txt +3 -3
  129. package/out/ping/__next._index.txt +7 -7
  130. package/out/ping/__next._tree.txt +3 -3
  131. package/out/ping/__next.ping.__PAGE__.txt +5 -5
  132. package/out/ping/__next.ping.txt +3 -3
  133. package/out/ping/index.html +2 -2
  134. package/out/ping/index.txt +14 -14
  135. package/out/web3/__next._full.txt +14 -14
  136. package/out/web3/__next._head.txt +3 -3
  137. package/out/web3/__next._index.txt +7 -7
  138. package/out/web3/__next._tree.txt +3 -3
  139. package/out/web3/__next.web3.__PAGE__.txt +4 -4
  140. package/out/web3/__next.web3.txt +4 -4
  141. package/out/web3/ed25519/__next._full.txt +12 -12
  142. package/out/web3/ed25519/__next._head.txt +3 -3
  143. package/out/web3/ed25519/__next._index.txt +7 -7
  144. package/out/web3/ed25519/__next._tree.txt +3 -3
  145. package/out/web3/ed25519/__next.web3.ed25519.__PAGE__.txt +2 -2
  146. package/out/web3/ed25519/__next.web3.ed25519.txt +3 -3
  147. package/out/web3/ed25519/__next.web3.txt +4 -4
  148. package/out/web3/ed25519/index.html +1 -1
  149. package/out/web3/ed25519/index.txt +12 -12
  150. package/out/web3/index.html +2 -2
  151. package/out/web3/index.txt +14 -14
  152. package/out/web3/tools/__next._full.txt +12 -12
  153. package/out/web3/tools/__next._head.txt +3 -3
  154. package/out/web3/tools/__next._index.txt +7 -7
  155. package/out/web3/tools/__next._tree.txt +3 -3
  156. package/out/web3/tools/__next.web3.tools.__PAGE__.txt +2 -2
  157. package/out/web3/tools/__next.web3.tools.txt +3 -3
  158. package/out/web3/tools/__next.web3.txt +4 -4
  159. package/out/web3/tools/index.html +1 -1
  160. package/out/web3/tools/index.txt +12 -12
  161. package/package.json +1 -1
  162. package/server/src/core/gameRoom.js +12 -0
  163. package/server/src/core/zhajinhua.js +82 -14
  164. package/server/src/games/gandengyan.js +39 -0
  165. package/server/src/http/app.js +66 -1
  166. package/server/src/http/nodeStatus.js +18 -0
  167. package/server/src/index.js +667 -165
  168. package/out/_next/static/chunks/0-ryc9icn6dql.css +0 -1
  169. package/out/_next/static/chunks/088-v8tzobg5g.js +0 -1
  170. package/out/_next/static/chunks/0g9303i8psy6z.js +0 -1
  171. package/out/_next/static/chunks/10p6-qnuk3p9x.js +0 -1
  172. package/out/_next/static/chunks/1ei8b9nw-ea8p.css +0 -1
  173. package/out/_next/static/chunks/2524ggtr3e4fp.js +0 -1
  174. package/out/_next/static/chunks/2fmkcw8ui5xp3.js +0 -5
  175. package/out/_next/static/chunks/2mcmt4yla_xle.js +0 -1
  176. package/out/_next/static/chunks/2nbju8t9-ux58.css +0 -1
  177. package/out/_next/static/chunks/2v7_4ps1l4a8o.css +0 -1
  178. package/out/_next/static/chunks/2vk2ln79gpvm4.js +0 -1
  179. package/out/_next/static/chunks/37msy2-4ucgts.js +0 -1
  180. package/out/_next/static/chunks/384w3w4qvhw61.js +0 -1
  181. package/out/_next/static/chunks/3aa-weedhv4e0.js +0 -1
  182. package/out/_next/static/chunks/3erbaukgkw8f2.js +0 -1
  183. package/out/_next/static/chunks/3fea100s764m-.js +0 -1
  184. package/out/_next/static/chunks/3qj5xz_d4cbk6.js +0 -1
  185. /package/out/_next/static/{NPIlpdzQWkiVZpgfhlypH → TXy93SnAZQ7OQ-aVUkmHa}/_buildManifest.js +0 -0
  186. /package/out/_next/static/{NPIlpdzQWkiVZpgfhlypH → TXy93SnAZQ7OQ-aVUkmHa}/_clientMiddlewareManifest.js +0 -0
  187. /package/out/_next/static/{NPIlpdzQWkiVZpgfhlypH → TXy93SnAZQ7OQ-aVUkmHa}/_ssgManifest.js +0 -0
@@ -67,12 +67,45 @@ import {
67
67
 
68
68
  const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
69
69
  const CHAT_FILE_ROOT = 'chat-file'
70
+ const TRANSIENT_CHANNEL_TYPES = new Set(['game'])
71
+ const DEFAULT_OWNER_BUCKET = '__local__'
72
+ const USER_DATA_SCHEMA_VERSION = 1
73
+ const IMPORT_CHECK_TTL_MS = 10 * 60 * 1000
74
+ const IMPORT_CHECK_MAX_FILES = 5000
70
75
 
71
76
  function normalizeOwnerAddress(address) {
72
77
  const value = String(address || '').trim()
73
78
  return /^0x[a-fA-F0-9]{40}$/.test(value) ? value.toLowerCase() : ''
74
79
  }
75
80
 
81
+ function getOwnerBucketKey(address) {
82
+ return normalizeOwnerAddress(address) || DEFAULT_OWNER_BUCKET
83
+ }
84
+
85
+ function normalizeMetadataBuckets(input) {
86
+ if (!input || typeof input !== 'object' || Array.isArray(input)) {
87
+ return {}
88
+ }
89
+ const buckets = {}
90
+ for (const [rawOwner, records] of Object.entries(input)) {
91
+ const ownerKey =
92
+ rawOwner === DEFAULT_OWNER_BUCKET
93
+ ? DEFAULT_OWNER_BUCKET
94
+ : normalizeOwnerAddress(rawOwner)
95
+ if (!ownerKey || !Array.isArray(records)) continue
96
+ buckets[ownerKey] = records.map(record => ({ ...record }))
97
+ }
98
+ return buckets
99
+ }
100
+
101
+ function cloneMetadataRecord(record, ownerAddress = '') {
102
+ return {
103
+ ...record,
104
+ ownerAddress:
105
+ ownerAddress && ownerAddress !== DEFAULT_OWNER_BUCKET ? ownerAddress : '',
106
+ }
107
+ }
108
+
76
109
  function getPathBaseName(fileName) {
77
110
  const parts = String(fileName || '').split('/').filter(Boolean)
78
111
  return parts[parts.length - 1] || 'unnamed_file'
@@ -122,12 +155,13 @@ export class MostBoxEngine extends EventEmitter {
122
155
  #store = null
123
156
  #swarm = null
124
157
  #drives = new Map()
125
- #publishedFiles = []
158
+ #publishedFiles = {}
126
159
  #holdings = []
127
- #trashFiles = []
160
+ #trashFiles = {}
128
161
  #initialized = false
129
162
  #options = null
130
163
  #activeDownloads = new Map()
164
+ #importChecks = new Map()
131
165
  #drivePromises = new Map()
132
166
  #fileDiscoveries = new Map()
133
167
  #fileMonitors = new Map()
@@ -302,7 +336,7 @@ export class MostBoxEngine extends EventEmitter {
302
336
 
303
337
  this.#publishedFiles = this.#loadPublishedMetadata()
304
338
  console.log(
305
- `[MostBox] Loaded ${this.#publishedFiles.length} published files`
339
+ `[MostBox] Loaded ${this.#countBucketRecords(this.#publishedFiles)} published files`
306
340
  )
307
341
 
308
342
  this.#holdings = this.#loadHoldingsMetadata()
@@ -317,7 +351,9 @@ export class MostBoxEngine extends EventEmitter {
317
351
  }
318
352
 
319
353
  this.#trashFiles = this.#loadTrashMetadata()
320
- console.log(`[MostBox] Loaded ${this.#trashFiles.length} trash files`)
354
+ console.log(
355
+ `[MostBox] Loaded ${this.#countBucketRecords(this.#trashFiles)} trash files`
356
+ )
321
357
 
322
358
  this.#channels = this.#loadChannelsMetadata()
323
359
  console.log(`[MostBox] Loaded ${this.#channels.length} channels`)
@@ -428,6 +464,7 @@ export class MostBoxEngine extends EventEmitter {
428
464
  this.#channelChatDiscoveries.clear()
429
465
  this.#channelPeers.clear()
430
466
  this.#channels = []
467
+ this.#importChecks.clear()
431
468
 
432
469
  if (this.#store) {
433
470
  await this.#store.close()
@@ -470,7 +507,6 @@ export class MostBoxEngine extends EventEmitter {
470
507
  * @param {string|Buffer} content - 文件路径(字符串)或内容(Buffer)
471
508
  * @param {string} [fileName] - 文件名(Buffer 输入时必填)
472
509
  * @param {object} [options] - 发布选项
473
- * @param {string|null} [options.localPath] - 持有记录中的本地路径
474
510
  * @returns {Promise<{ cid: string, link: string, fileName: string }>}
475
511
  */
476
512
  async publishFile(content, fileName, options = {}) {
@@ -525,15 +561,11 @@ export class MostBoxEngine extends EventEmitter {
525
561
  const { cid: rootCid } = await calculateCid(content)
526
562
  const cidString = rootCid.toString()
527
563
  const { driveName: name } = this.#getCidInfo(cidString)
528
- const holdingLocalPath =
529
- options.localPath === undefined ? cleanPath : options.localPath
530
-
564
+ const publishedBucket = this.#getPublishedBucket(ownerAddress, true)
531
565
  // 检查相同内容是否已存在
532
- const existingIndex = this.#publishedFiles.findIndex(
533
- f => f.cid === cidString && this.#recordMatchesOwner(f, ownerAddress)
534
- )
566
+ const existingIndex = publishedBucket.findIndex(f => f.cid === cidString)
535
567
  if (existingIndex !== -1) {
536
- const existing = this.#publishedFiles[existingIndex]
568
+ const existing = publishedBucket[existingIndex]
537
569
  await this.#joinCidTopicInternal(cidString, {
538
570
  server: true,
539
571
  client: false,
@@ -542,7 +574,6 @@ export class MostBoxEngine extends EventEmitter {
542
574
  cid: cidString,
543
575
  fileName: existing.fileName,
544
576
  size: fileSize,
545
- localPath: holdingLocalPath,
546
577
  driveName: name,
547
578
  source: 'published',
548
579
  })
@@ -613,20 +644,18 @@ export class MostBoxEngine extends EventEmitter {
613
644
  }
614
645
 
615
646
  // 存储 displayName(用户看到的文件夹路径),不存储 drivePath
616
- this.#publishedFiles.push({
647
+ publishedBucket.push({
617
648
  fileName: safeFileName,
618
649
  cid: cidString,
619
650
  driveName: name,
620
651
  publishedAt: new Date().toISOString(),
621
652
  starred: false,
622
- ownerAddress,
623
653
  })
624
654
  this.#savePublishedMetadata()
625
655
  this.#upsertHolding({
626
656
  cid: cidString,
627
657
  fileName: safeFileName,
628
658
  size: fileSize,
629
- localPath: holdingLocalPath,
630
659
  driveName: name,
631
660
  source: 'published',
632
661
  })
@@ -700,8 +729,6 @@ export class MostBoxEngine extends EventEmitter {
700
729
  size:
701
730
  existingHolding?.size ??
702
731
  (Number.isFinite(localContent.size) ? localContent.size : 0),
703
- localPath:
704
- existingHolding?.localPath || existingFile?.localPath || null,
705
732
  driveName: existingFile?.driveName || name,
706
733
  source: existingHolding?.source || 'published',
707
734
  })
@@ -959,27 +986,26 @@ export class MostBoxEngine extends EventEmitter {
959
986
  savedPath: savePath,
960
987
  }
961
988
 
962
- // 将下载的文件添加到已发布文件列表(displayName 用原始文件名)
963
- const existingIndex = this.#publishedFiles.findIndex(
964
- f => f.cid === cidString && this.#recordMatchesOwner(f, ownerAddress)
989
+ const publishedBucket = this.#getPublishedBucket(ownerAddress, true)
990
+ const existingIndex = publishedBucket.findIndex(
991
+ f => f.cid === cidString
965
992
  )
966
993
  this.#assertDisplayNameAvailable(sanitizedFileName, {
967
994
  ownerAddress,
968
995
  excludeCid: cidString,
969
996
  })
970
997
  if (existingIndex !== -1) {
971
- const existing = this.#publishedFiles[existingIndex]
998
+ const existing = publishedBucket[existingIndex]
972
999
  existing.fileName = sanitizedFileName
973
1000
  existing.driveName = name
974
1001
  existing.publishedAt = new Date().toISOString()
975
1002
  } else {
976
- this.#publishedFiles.push({
1003
+ publishedBucket.push({
977
1004
  fileName: sanitizedFileName,
978
1005
  cid: cidString,
979
1006
  driveName: name,
980
1007
  publishedAt: new Date().toISOString(),
981
1008
  starred: false,
982
- ownerAddress,
983
1009
  })
984
1010
  }
985
1011
  this.#savePublishedMetadata()
@@ -988,7 +1014,6 @@ export class MostBoxEngine extends EventEmitter {
988
1014
  cid: cidString,
989
1015
  fileName: sanitizedFileName,
990
1016
  size: savedSize,
991
- localPath: savePath,
992
1017
  driveName: name,
993
1018
  source: 'downloaded',
994
1019
  })
@@ -1118,12 +1143,8 @@ export class MostBoxEngine extends EventEmitter {
1118
1143
  */
1119
1144
  listPublishedFiles(options = {}) {
1120
1145
  this.#ensureInitialized()
1121
- let files = this.#publishedFiles
1122
1146
  const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
1123
-
1124
- if (ownerAddress) {
1125
- files = files.filter(f => this.#recordMatchesOwner(f, ownerAddress))
1126
- }
1147
+ let files = this.#getPublishedBucket(ownerAddress)
1127
1148
 
1128
1149
  if (options.starred === true) {
1129
1150
  files = files.filter(f => f.starred === true)
@@ -1135,7 +1156,7 @@ export class MostBoxEngine extends EventEmitter {
1135
1156
  link: `most://${f.cid}?filename=${encodeURIComponent(f.fileName)}`,
1136
1157
  publishedAt: f.publishedAt,
1137
1158
  starred: f.starred || false,
1138
- ownerAddress: f.ownerAddress || '',
1159
+ ownerAddress: ownerAddress || '',
1139
1160
  }))
1140
1161
  }
1141
1162
 
@@ -1147,17 +1168,16 @@ export class MostBoxEngine extends EventEmitter {
1147
1168
  toggleStarred(cid, options = {}) {
1148
1169
  this.#ensureInitialized()
1149
1170
  const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
1150
- const index = this.#publishedFiles.findIndex(
1151
- f => f.cid === cid && this.#recordMatchesOwner(f, ownerAddress)
1152
- )
1171
+ const files = this.#getPublishedBucket(ownerAddress)
1172
+ const index = files.findIndex(f => f.cid === cid)
1153
1173
  if (index === -1) {
1154
1174
  throw new Error('File not found')
1155
1175
  }
1156
- this.#publishedFiles[index].starred = !this.#publishedFiles[index].starred
1176
+ files[index].starred = !files[index].starred
1157
1177
  this.#savePublishedMetadata()
1158
1178
  return {
1159
1179
  cid,
1160
- starred: this.#publishedFiles[index].starred,
1180
+ starred: files[index].starred,
1161
1181
  }
1162
1182
  }
1163
1183
 
@@ -1169,29 +1189,28 @@ export class MostBoxEngine extends EventEmitter {
1169
1189
  async deletePublishedFile(cid, options = {}) {
1170
1190
  this.#ensureInitialized()
1171
1191
  const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
1172
- const index = this.#publishedFiles.findIndex(
1173
- f => f.cid === cid && this.#recordMatchesOwner(f, ownerAddress)
1174
- )
1192
+ const files = this.#getPublishedBucket(ownerAddress)
1193
+ const trashFiles = this.#getTrashBucket(ownerAddress, true)
1194
+ const index = files.findIndex(f => f.cid === cid)
1175
1195
  if (index !== -1) {
1176
- const fileRecord = this.#publishedFiles[index]
1196
+ const fileRecord = files[index]
1177
1197
  const holding = this.#holdings.find(item => item.cid === fileRecord.cid)
1178
1198
 
1179
- this.#trashFiles.push({
1199
+ trashFiles.push({
1180
1200
  fileName: fileRecord.fileName,
1181
1201
  cid: fileRecord.cid,
1182
1202
  driveName:
1183
1203
  fileRecord.driveName || this.#getCidInfo(fileRecord.cid).driveName,
1184
1204
  size: holding?.size ?? fileRecord.size ?? 0,
1185
- localPath: holding?.localPath || fileRecord.localPath || null,
1186
1205
  source: holding?.source || 'published',
1187
1206
  publishedAt: fileRecord.publishedAt,
1188
1207
  starred: fileRecord.starred || false,
1189
- ownerAddress: fileRecord.ownerAddress || ownerAddress,
1190
1208
  deletedAt: new Date().toISOString(),
1191
1209
  })
1192
1210
  this.#saveTrashMetadata()
1193
1211
 
1194
- this.#publishedFiles.splice(index, 1)
1212
+ files.splice(index, 1)
1213
+ this.#setPublishedBucket(ownerAddress, files)
1195
1214
  this.#savePublishedMetadata()
1196
1215
 
1197
1216
  if (!this.#hasPublishedReference(fileRecord.cid)) {
@@ -1212,16 +1231,14 @@ export class MostBoxEngine extends EventEmitter {
1212
1231
  listTrashFiles(options = {}) {
1213
1232
  this.#ensureInitialized()
1214
1233
  const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
1215
- const files = ownerAddress
1216
- ? this.#trashFiles.filter(f => this.#recordMatchesOwner(f, ownerAddress))
1217
- : this.#trashFiles
1234
+ const files = this.#getTrashBucket(ownerAddress)
1218
1235
  return files.map(f => ({
1219
1236
  fileName: f.fileName,
1220
1237
  cid: f.cid,
1221
1238
  link: `most://${f.cid}?filename=${encodeURIComponent(f.fileName)}`,
1222
1239
  publishedAt: f.publishedAt,
1223
1240
  starred: f.starred || false,
1224
- ownerAddress: f.ownerAddress || '',
1241
+ ownerAddress: ownerAddress || '',
1225
1242
  deletedAt: f.deletedAt,
1226
1243
  }))
1227
1244
  }
@@ -1234,22 +1251,23 @@ export class MostBoxEngine extends EventEmitter {
1234
1251
  async restoreTrashFile(cid, options = {}) {
1235
1252
  this.#ensureInitialized()
1236
1253
  const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
1237
- const index = this.#trashFiles.findIndex(
1238
- f => f.cid === cid && this.#recordMatchesOwner(f, ownerAddress)
1239
- )
1254
+ const trashFiles = this.#getTrashBucket(ownerAddress)
1255
+ const publishedFiles = this.#getPublishedBucket(ownerAddress, true)
1256
+ const index = trashFiles.findIndex(f => f.cid === cid)
1240
1257
  if (index === -1) {
1241
1258
  throw new Error('File not found in trash')
1242
1259
  }
1243
1260
 
1244
- const fileRecord = this.#trashFiles[index]
1261
+ const fileRecord = trashFiles[index]
1245
1262
 
1246
1263
  const { driveName } = this.#getCidInfo(fileRecord.cid)
1247
1264
 
1248
- const existingIndex = this.#publishedFiles.findIndex(
1249
- f => f.cid === fileRecord.cid && this.#recordMatchesOwner(f, ownerAddress)
1265
+ const existingIndex = publishedFiles.findIndex(
1266
+ f => f.cid === fileRecord.cid
1250
1267
  )
1251
1268
  if (existingIndex !== -1) {
1252
- this.#trashFiles.splice(index, 1)
1269
+ trashFiles.splice(index, 1)
1270
+ this.#setTrashBucket(ownerAddress, trashFiles)
1253
1271
  this.#saveTrashMetadata()
1254
1272
  return this.listPublishedFiles({ ownerAddress })
1255
1273
  }
@@ -1259,17 +1277,17 @@ export class MostBoxEngine extends EventEmitter {
1259
1277
  excludeCid: fileRecord.cid,
1260
1278
  })
1261
1279
 
1262
- this.#publishedFiles.push({
1280
+ publishedFiles.push({
1263
1281
  fileName: fileRecord.fileName,
1264
1282
  cid: fileRecord.cid,
1265
1283
  driveName,
1266
1284
  publishedAt: fileRecord.publishedAt,
1267
1285
  starred: fileRecord.starred || false,
1268
- ownerAddress: fileRecord.ownerAddress || ownerAddress,
1269
1286
  })
1270
1287
  this.#savePublishedMetadata()
1271
1288
 
1272
- this.#trashFiles.splice(index, 1)
1289
+ trashFiles.splice(index, 1)
1290
+ this.#setTrashBucket(ownerAddress, trashFiles)
1273
1291
  this.#saveTrashMetadata()
1274
1292
 
1275
1293
  await this.#joinCidTopicInternal(fileRecord.cid, {
@@ -1280,7 +1298,6 @@ export class MostBoxEngine extends EventEmitter {
1280
1298
  cid: fileRecord.cid,
1281
1299
  fileName: fileRecord.fileName,
1282
1300
  size: Number(fileRecord.size) || 0,
1283
- localPath: fileRecord.localPath || null,
1284
1301
  driveName,
1285
1302
  source: fileRecord.source || 'published',
1286
1303
  })
@@ -1296,15 +1313,15 @@ export class MostBoxEngine extends EventEmitter {
1296
1313
  async permanentDeleteTrashFile(cid, options = {}) {
1297
1314
  this.#ensureInitialized()
1298
1315
  const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
1299
- const index = this.#trashFiles.findIndex(
1300
- f => f.cid === cid && this.#recordMatchesOwner(f, ownerAddress)
1301
- )
1316
+ const trashFiles = this.#getTrashBucket(ownerAddress)
1317
+ const index = trashFiles.findIndex(f => f.cid === cid)
1302
1318
  if (index !== -1) {
1303
- const fileRecord = this.#trashFiles[index]
1319
+ const fileRecord = trashFiles[index]
1304
1320
  const driveName =
1305
1321
  fileRecord.driveName || this.#getCidInfo(fileRecord.cid).driveName
1306
1322
 
1307
- this.#trashFiles.splice(index, 1)
1323
+ trashFiles.splice(index, 1)
1324
+ this.#setTrashBucket(ownerAddress, trashFiles)
1308
1325
  this.#saveTrashMetadata()
1309
1326
 
1310
1327
  if (!this.#hasAnyUserReference(fileRecord.cid)) {
@@ -1329,18 +1346,8 @@ export class MostBoxEngine extends EventEmitter {
1329
1346
  async emptyTrash(options = {}) {
1330
1347
  this.#ensureInitialized()
1331
1348
  const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
1332
- const remainingTrash = []
1333
- const removedTrash = []
1334
-
1335
- for (const fileRecord of this.#trashFiles) {
1336
- if (ownerAddress && !this.#recordMatchesOwner(fileRecord, ownerAddress)) {
1337
- remainingTrash.push(fileRecord)
1338
- continue
1339
- }
1340
- removedTrash.push(fileRecord)
1341
- }
1342
-
1343
- this.#trashFiles = remainingTrash
1349
+ const removedTrash = [...this.#getTrashBucket(ownerAddress)]
1350
+ this.#setTrashBucket(ownerAddress, [])
1344
1351
  this.#saveTrashMetadata()
1345
1352
 
1346
1353
  for (const fileRecord of removedTrash) {
@@ -1419,15 +1426,11 @@ export class MostBoxEngine extends EventEmitter {
1419
1426
  used: usedSize,
1420
1427
  free: freeSize,
1421
1428
  fileCount: ownerAddress
1422
- ? this.#publishedFiles.filter(f =>
1423
- this.#recordMatchesOwner(f, ownerAddress)
1424
- ).length
1425
- : this.#publishedFiles.length,
1429
+ ? this.#getPublishedBucket(ownerAddress).length
1430
+ : this.#countBucketRecords(this.#publishedFiles),
1426
1431
  trashCount: ownerAddress
1427
- ? this.#trashFiles.filter(f =>
1428
- this.#recordMatchesOwner(f, ownerAddress)
1429
- ).length
1430
- : this.#trashFiles.length,
1432
+ ? this.#getTrashBucket(ownerAddress).length
1433
+ : this.#countBucketRecords(this.#trashFiles),
1431
1434
  }
1432
1435
  }
1433
1436
 
@@ -1441,9 +1444,8 @@ export class MostBoxEngine extends EventEmitter {
1441
1444
  moveFile(cid, newFileName, options = {}) {
1442
1445
  this.#ensureInitialized()
1443
1446
  const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
1444
- const index = this.#publishedFiles.findIndex(
1445
- f => f.cid === cid && this.#recordMatchesOwner(f, ownerAddress)
1446
- )
1447
+ const files = this.#getPublishedBucket(ownerAddress)
1448
+ const index = files.findIndex(f => f.cid === cid)
1447
1449
  if (index === -1) {
1448
1450
  throw new Error('File not found')
1449
1451
  }
@@ -1452,8 +1454,8 @@ export class MostBoxEngine extends EventEmitter {
1452
1454
  ownerAddress,
1453
1455
  excludeCid: cid,
1454
1456
  })
1455
- this.#publishedFiles[index].fileName = safeFileName
1456
- this.#publishedFiles[index].publishedAt = new Date().toISOString()
1457
+ files[index].fileName = safeFileName
1458
+ files[index].publishedAt = new Date().toISOString()
1457
1459
  this.#savePublishedMetadata()
1458
1460
  return {
1459
1461
  cid,
@@ -1474,12 +1476,10 @@ export class MostBoxEngine extends EventEmitter {
1474
1476
  const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
1475
1477
  const prefix = oldPath + '/'
1476
1478
  const updates = []
1479
+ const files = this.#getPublishedBucket(ownerAddress)
1477
1480
 
1478
- for (const file of this.#publishedFiles) {
1479
- if (
1480
- file.fileName.startsWith(prefix) &&
1481
- this.#recordMatchesOwner(file, ownerAddress)
1482
- ) {
1481
+ for (const file of files) {
1482
+ if (file.fileName.startsWith(prefix)) {
1483
1483
  const remainder = file.fileName.substring(prefix.length)
1484
1484
  const newFileName = sanitizeFilename(
1485
1485
  remainder ? newPath + '/' + remainder : newPath
@@ -1544,10 +1544,8 @@ export class MostBoxEngine extends EventEmitter {
1544
1544
  getPublishedFiles(options = {}) {
1545
1545
  const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
1546
1546
  return ownerAddress
1547
- ? this.#publishedFiles.filter(f =>
1548
- this.#recordMatchesOwner(f, ownerAddress)
1549
- )
1550
- : this.#publishedFiles
1547
+ ? this.#getPublishedBucket(ownerAddress)
1548
+ : this.#allPublishedRecords()
1551
1549
  }
1552
1550
 
1553
1551
  listUsers() {
@@ -1568,17 +1566,21 @@ export class MostBoxEngine extends EventEmitter {
1568
1566
  return users.get(ownerAddress)
1569
1567
  }
1570
1568
 
1571
- for (const file of this.#publishedFiles) {
1572
- const entry = ensure(file.ownerAddress)
1569
+ for (const [ownerAddress, files] of Object.entries(this.#publishedFiles)) {
1570
+ const entry = ensure(ownerAddress)
1573
1571
  if (!entry) continue
1574
- entry.fileCount += 1
1575
- entry.cids.add(file.cid)
1572
+ entry.fileCount += files.length
1573
+ for (const file of files) {
1574
+ entry.cids.add(file.cid)
1575
+ }
1576
1576
  }
1577
- for (const file of this.#trashFiles) {
1578
- const entry = ensure(file.ownerAddress)
1577
+ for (const [ownerAddress, files] of Object.entries(this.#trashFiles)) {
1578
+ const entry = ensure(ownerAddress)
1579
1579
  if (!entry) continue
1580
- entry.trashCount += 1
1581
- entry.cids.add(file.cid)
1580
+ entry.trashCount += files.length
1581
+ for (const file of files) {
1582
+ entry.cids.add(file.cid)
1583
+ }
1582
1584
  }
1583
1585
 
1584
1586
  return [...users.values()].map(user => ({
@@ -1596,58 +1598,258 @@ export class MostBoxEngine extends EventEmitter {
1596
1598
  throw new ValidationError('valid owner address is required')
1597
1599
  }
1598
1600
 
1599
- const affectedCids = new Set()
1600
- const beforeFiles = this.#publishedFiles.length
1601
- const beforeTrash = this.#trashFiles.length
1601
+ const result = await this.#clearUserDataInternal(ownerAddress)
1602
1602
 
1603
- this.#publishedFiles = this.#publishedFiles.filter(file => {
1604
- if (this.#recordMatchesOwner(file, ownerAddress)) {
1605
- affectedCids.add(file.cid)
1606
- return false
1607
- }
1608
- return true
1609
- })
1610
- this.#trashFiles = this.#trashFiles.filter(file => {
1611
- if (this.#recordMatchesOwner(file, ownerAddress)) {
1612
- affectedCids.add(file.cid)
1613
- return false
1614
- }
1615
- return true
1616
- })
1617
- this.#channels = this.#channels
1603
+ return {
1604
+ ownerAddress,
1605
+ ...result,
1606
+ }
1607
+ }
1608
+
1609
+ exportUserMetadata(ownerAddressInput) {
1610
+ this.#ensureInitialized()
1611
+ const ownerAddress = normalizeOwnerAddress(ownerAddressInput)
1612
+ if (!ownerAddress) {
1613
+ throw new ValidationError('valid owner address is required')
1614
+ }
1615
+
1616
+ const files = this.#getPublishedBucket(ownerAddress).map(file => ({
1617
+ fileName: file.fileName,
1618
+ cid: file.cid,
1619
+ driveName: file.driveName || this.#getCidInfo(file.cid).driveName,
1620
+ publishedAt: file.publishedAt,
1621
+ starred: file.starred || false,
1622
+ link: buildMostLink(file.cid, file.fileName),
1623
+ }))
1624
+ const trashFiles = this.#getTrashBucket(ownerAddress).map(file => ({
1625
+ fileName: file.fileName,
1626
+ cid: file.cid,
1627
+ driveName: file.driveName || this.#getCidInfo(file.cid).driveName,
1628
+ size: Number(file.size) || 0,
1629
+ source: file.source || 'published',
1630
+ publishedAt: file.publishedAt,
1631
+ starred: file.starred || false,
1632
+ deletedAt: file.deletedAt,
1633
+ link: buildMostLink(file.cid, file.fileName),
1634
+ }))
1635
+ const channels = this.#channels
1636
+ .filter(channel => this.#channelHasMember(channel, ownerAddress))
1618
1637
  .map(channel => ({
1619
- ...channel,
1620
- members: Array.isArray(channel.members)
1621
- ? channel.members.filter(
1622
- member => normalizeOwnerAddress(member?.address) !== ownerAddress
1623
- )
1624
- : [],
1638
+ name: channel.name,
1639
+ type: channel.type,
1640
+ coreKey: channel.coreKey,
1641
+ createdAt: channel.createdAt,
1642
+ lastMessageAt: channel.lastMessageAt || '',
1643
+ member: this.#getChannelMembers(channel).find(
1644
+ member => member.address === ownerAddress
1645
+ ),
1646
+ remark: channel.remarks?.[ownerAddress] || '',
1647
+ pinned: Boolean(channel.pinnedBy?.[ownerAddress]),
1625
1648
  }))
1626
- .filter(channel => channel.members.length > 0)
1627
1649
 
1650
+ return {
1651
+ schemaVersion: USER_DATA_SCHEMA_VERSION,
1652
+ exportedAt: new Date().toISOString(),
1653
+ ownerAddress,
1654
+ files,
1655
+ trashFiles,
1656
+ channels,
1657
+ }
1658
+ }
1659
+
1660
+ async checkUserImport(input = {}, options = {}) {
1661
+ this.#ensureInitialized()
1662
+ const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
1663
+ if (!ownerAddress) {
1664
+ throw new ValidationError('valid owner address is required')
1665
+ }
1666
+
1667
+ const normalized = this.#normalizeUserImportPackage(input)
1668
+ const failures = []
1669
+ const seenPaths = new Set()
1670
+ const checkedActiveCids = new Set()
1671
+ const importedCids = new Set([
1672
+ ...normalized.files.map(file => file.cid),
1673
+ ...normalized.trashFiles.map(file => file.cid),
1674
+ ])
1675
+ const currentCids = this.#collectUserCids(ownerAddress)
1676
+ let requiredBytes = 0
1677
+
1678
+ if (normalized.files.length > IMPORT_CHECK_MAX_FILES) {
1679
+ failures.push({
1680
+ scope: 'package',
1681
+ error: `too many files: max ${IMPORT_CHECK_MAX_FILES}`,
1682
+ })
1683
+ }
1684
+
1685
+ for (const file of normalized.files) {
1686
+ const pathKey = sanitizeFilename(file.fileName).toLowerCase()
1687
+ if (seenPaths.has(pathKey)) {
1688
+ failures.push({
1689
+ cid: file.cid,
1690
+ fileName: file.fileName,
1691
+ error: 'duplicate file path',
1692
+ })
1693
+ continue
1694
+ }
1695
+ seenPaths.add(pathKey)
1696
+
1697
+ const link = buildMostLink(file.cid, file.fileName)
1698
+ try {
1699
+ const result = await this.checkDownloadAvailability(link, {
1700
+ ownerAddress,
1701
+ timeout: options.timeout || DRIVE_ENTRY_TIMEOUT,
1702
+ })
1703
+ if (!checkedActiveCids.has(file.cid) && !result.alreadyExists) {
1704
+ requiredBytes += Number(result.size) || Number(file.size) || 0
1705
+ }
1706
+ checkedActiveCids.add(file.cid)
1707
+ } catch (err) {
1708
+ failures.push({
1709
+ cid: file.cid,
1710
+ fileName: file.fileName,
1711
+ error: err.message,
1712
+ code: err.code || 'UNAVAILABLE',
1713
+ })
1714
+ }
1715
+ }
1716
+
1717
+ let reclaimableBytes = 0
1718
+ for (const cid of currentCids) {
1719
+ if (importedCids.has(cid)) continue
1720
+ const referencedByOtherUser =
1721
+ this.#allPublishedRecords().some(
1722
+ file => file.cid === cid && file.ownerAddress !== ownerAddress
1723
+ ) ||
1724
+ this.#allTrashRecords().some(
1725
+ file => file.cid === cid && file.ownerAddress !== ownerAddress
1726
+ )
1727
+ if (referencedByOtherUser) continue
1728
+ const holding = this.#holdings.find(item => item.cid === cid)
1729
+ reclaimableBytes += Number(holding?.size) || 0
1730
+ }
1731
+ const availableBytes = Math.max(
1732
+ 0,
1733
+ this.#options.capacityBytes - this.#getUsedBytes() + reclaimableBytes
1734
+ )
1735
+ if (requiredBytes > availableBytes) {
1736
+ failures.push({
1737
+ scope: 'capacity',
1738
+ error: 'insufficient capacity',
1739
+ requiredBytes,
1740
+ availableBytes,
1741
+ })
1742
+ }
1743
+
1744
+ const ready = failures.length === 0
1745
+ const checkId = `import_${Date.now()}_${crypto.randomBytes(6).toString('hex')}`
1746
+ if (ready) {
1747
+ this.#importChecks.set(checkId, {
1748
+ ownerAddress,
1749
+ packageHash: this.#hashImportPackage(input),
1750
+ expiresAt: Date.now() + IMPORT_CHECK_TTL_MS,
1751
+ })
1752
+ }
1753
+
1754
+ return {
1755
+ ready,
1756
+ checkId: ready ? checkId : '',
1757
+ failures,
1758
+ currentFileCount: this.#getPublishedBucket(ownerAddress).length,
1759
+ currentTrashCount: this.#getTrashBucket(ownerAddress).length,
1760
+ currentCidCount: currentCids.size,
1761
+ importFileCount: normalized.files.length,
1762
+ importTrashCount: normalized.trashFiles.length,
1763
+ requiredBytes,
1764
+ availableBytes,
1765
+ }
1766
+ }
1767
+
1768
+ async importUserMetadata(input = {}, options = {}) {
1769
+ this.#ensureInitialized()
1770
+ const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
1771
+ if (!ownerAddress) {
1772
+ throw new ValidationError('valid owner address is required')
1773
+ }
1774
+
1775
+ const checkId = String(options.checkId || '').trim()
1776
+ const check = this.#importChecks.get(checkId)
1777
+ if (
1778
+ !check ||
1779
+ check.ownerAddress !== ownerAddress ||
1780
+ check.expiresAt < Date.now() ||
1781
+ check.packageHash !== this.#hashImportPackage(input)
1782
+ ) {
1783
+ throw new ValidationError('valid import check is required')
1784
+ }
1785
+ this.#importChecks.delete(checkId)
1786
+
1787
+ const normalized = this.#normalizeUserImportPackage(input)
1788
+ const previousCids = this.#collectUserCids(ownerAddress)
1789
+ const previous = {
1790
+ removedFiles: this.#getPublishedBucket(ownerAddress).length,
1791
+ removedTrashFiles: this.#getTrashBucket(ownerAddress).length,
1792
+ }
1793
+ const now = new Date().toISOString()
1794
+
1795
+ this.#removeUserFromChannels(ownerAddress)
1796
+ this.#setPublishedBucket(
1797
+ ownerAddress,
1798
+ normalized.files.map(file => ({
1799
+ fileName: file.fileName,
1800
+ cid: file.cid,
1801
+ driveName: file.driveName || this.#getCidInfo(file.cid).driveName,
1802
+ publishedAt: file.publishedAt || now,
1803
+ starred: file.starred || false,
1804
+ }))
1805
+ )
1806
+ this.#setTrashBucket(
1807
+ ownerAddress,
1808
+ normalized.trashFiles.map(file => ({
1809
+ fileName: file.fileName,
1810
+ cid: file.cid,
1811
+ driveName: file.driveName || this.#getCidInfo(file.cid).driveName,
1812
+ size: Number(file.size) || 0,
1813
+ source: file.source || 'imported',
1814
+ publishedAt: file.publishedAt || now,
1815
+ starred: file.starred || false,
1816
+ deletedAt: file.deletedAt || now,
1817
+ }))
1818
+ )
1819
+ this.#applyImportedChannels(ownerAddress, normalized.channels)
1628
1820
  this.#savePublishedMetadata()
1629
1821
  this.#saveTrashMetadata()
1630
1822
  this.#saveChannelsMetadata()
1823
+ previous.removedReplicas = await this.#cleanupUnreferencedCids(previousCids)
1631
1824
 
1632
- let removedReplicas = 0
1633
- for (const cid of affectedCids) {
1634
- if (this.#hasAnyUserReference(cid)) continue
1635
- const driveName = this.#getCidInfo(cid).driveName
1825
+ const importedFiles = []
1826
+ const failedFiles = []
1827
+ for (const file of normalized.files) {
1636
1828
  try {
1637
- const drive = await this.#getOrCreateDrive(driveName)
1638
- await drive.del('/' + cid)
1639
- } catch {}
1640
- await this.#closeDriveForSeed(driveName)
1641
- await this.#leaveCidTopic(cid)
1642
- this.#removeHolding(cid)
1643
- removedReplicas += 1
1829
+ const result = await this.pullByCid({
1830
+ link: buildMostLink(file.cid, file.fileName),
1831
+ ownerAddress,
1832
+ timeout: options.timeout,
1833
+ })
1834
+ importedFiles.push({ cid: file.cid, fileName: file.fileName, result })
1835
+ } catch (err) {
1836
+ failedFiles.push({
1837
+ cid: file.cid,
1838
+ fileName: file.fileName,
1839
+ error: err.message,
1840
+ code: err.code || 'IMPORT_PULL_FAILED',
1841
+ })
1842
+ }
1644
1843
  }
1645
1844
 
1646
1845
  return {
1846
+ success: failedFiles.length === 0,
1647
1847
  ownerAddress,
1648
- removedFiles: beforeFiles - this.#publishedFiles.length,
1649
- removedTrashFiles: beforeTrash - this.#trashFiles.length,
1650
- removedReplicas,
1848
+ replacedFiles: previous.removedFiles,
1849
+ replacedTrashFiles: previous.removedTrashFiles,
1850
+ importedFiles: importedFiles.length,
1851
+ importedTrashFiles: normalized.trashFiles.length,
1852
+ failedFiles,
1651
1853
  }
1652
1854
  }
1653
1855
 
@@ -1906,11 +2108,14 @@ export class MostBoxEngine extends EventEmitter {
1906
2108
 
1907
2109
  async #getLocalCidContent(cid, options = {}) {
1908
2110
  const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
1909
- const fileRecord = this.#publishedFiles.find(
1910
- f =>
1911
- f.cid === cid &&
1912
- (options.public || this.#recordMatchesOwner(f, ownerAddress))
2111
+ const ownerRecord = this.#getPublishedBucket(ownerAddress).find(
2112
+ f => f.cid === cid
1913
2113
  )
2114
+ const fileRecord =
2115
+ ownerRecord ||
2116
+ (options.public
2117
+ ? this.#allPublishedRecords().find(f => f.cid === cid)
2118
+ : null)
1914
2119
  if (!options.allowHoldingFallback && !fileRecord) {
1915
2120
  return null
1916
2121
  }
@@ -1945,7 +2150,6 @@ export class MostBoxEngine extends EventEmitter {
1945
2150
  cid,
1946
2151
  fileName: holding?.fileName || cid,
1947
2152
  driveName: holding?.driveName || driveName,
1948
- localPath: holding?.localPath || null,
1949
2153
  size,
1950
2154
  ownerAddress,
1951
2155
  },
@@ -2249,9 +2453,36 @@ export class MostBoxEngine extends EventEmitter {
2249
2453
  return trimmed
2250
2454
  }
2251
2455
 
2456
+ setChannelPinned(name, pinned, options = {}) {
2457
+ this.#ensureInitialized()
2458
+ const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
2459
+ if (!ownerAddress) {
2460
+ throw new Error('需要登录才能设置置顶')
2461
+ }
2462
+
2463
+ const channel = this.#channels.find(c => c.name === name)
2464
+ if (!channel) {
2465
+ throw new Error('频道不存在')
2466
+ }
2467
+ this.#assertChannelMember(name, ownerAddress)
2468
+
2469
+ if (!channel.pinnedBy) {
2470
+ channel.pinnedBy = {}
2471
+ }
2472
+
2473
+ if (pinned) {
2474
+ channel.pinnedBy[ownerAddress] = true
2475
+ } else {
2476
+ delete channel.pinnedBy[ownerAddress]
2477
+ }
2478
+
2479
+ this.#saveChannelsMetadata()
2480
+ return Boolean(channel.pinnedBy[ownerAddress])
2481
+ }
2482
+
2252
2483
  /**
2253
2484
  * 列出所有频道
2254
- * @returns {Array<{ name: string, coreKey: string, createdAt: string, type: string, peerCount: number, remark: string }>}
2485
+ * @returns {Array<{ name: string, coreKey: string, createdAt: string, lastMessageAt: string, type: string, peerCount: number, remark: string, pinned: boolean }>}
2255
2486
  */
2256
2487
  listChannels(options = {}) {
2257
2488
  this.#ensureInitialized()
@@ -2273,9 +2504,11 @@ export class MostBoxEngine extends EventEmitter {
2273
2504
  name: c.name,
2274
2505
  coreKey: c.coreKey,
2275
2506
  createdAt: c.createdAt,
2507
+ lastMessageAt: c.lastMessageAt || '',
2276
2508
  type: c.type,
2277
2509
  peerCount: (this.#channelPeers.get(c.name) || new Map()).size,
2278
2510
  remark: ownerAddress && c.remarks ? c.remarks[ownerAddress] || '' : '',
2511
+ pinned: Boolean(ownerAddress && c.pinnedBy?.[ownerAddress]),
2279
2512
  }))
2280
2513
  }
2281
2514
 
@@ -2405,6 +2638,10 @@ export class MostBoxEngine extends EventEmitter {
2405
2638
  }
2406
2639
 
2407
2640
  await core.append(message)
2641
+ if (channel) {
2642
+ channel.lastMessageAt = new Date(message.timestamp).toISOString()
2643
+ this.#saveChannelsMetadata()
2644
+ }
2408
2645
 
2409
2646
  return message
2410
2647
  }
@@ -2626,6 +2863,114 @@ export class MostBoxEngine extends EventEmitter {
2626
2863
  }
2627
2864
  }
2628
2865
 
2866
+ #hashImportPackage(input) {
2867
+ return crypto
2868
+ .createHash('sha256')
2869
+ .update(JSON.stringify(input || {}))
2870
+ .digest('hex')
2871
+ }
2872
+
2873
+ #normalizeUserImportPackage(input = {}) {
2874
+ if (!input || typeof input !== 'object') {
2875
+ throw new ValidationError('import package must be an object')
2876
+ }
2877
+ if (Number(input.schemaVersion) !== USER_DATA_SCHEMA_VERSION) {
2878
+ throw new ValidationError('unsupported import package schemaVersion')
2879
+ }
2880
+
2881
+ const normalizeFile = (record, scope) => {
2882
+ if (!record || typeof record !== 'object') {
2883
+ throw new ValidationError(`${scope} record must be an object`)
2884
+ }
2885
+ const cid = String(record.cid || '').trim()
2886
+ const { driveName } = this.#getCidInfo(cid)
2887
+ const fileName = sanitizeFilename(record.fileName || cid)
2888
+ if (!fileName || fileName === 'unnamed') {
2889
+ throw new ValidationError(`${scope} fileName is invalid`)
2890
+ }
2891
+ return {
2892
+ cid,
2893
+ fileName,
2894
+ driveName,
2895
+ size: Number(record.size) || 0,
2896
+ source: String(record.source || 'imported'),
2897
+ publishedAt:
2898
+ typeof record.publishedAt === 'string' ? record.publishedAt : '',
2899
+ deletedAt: typeof record.deletedAt === 'string' ? record.deletedAt : '',
2900
+ starred: Boolean(record.starred),
2901
+ }
2902
+ }
2903
+
2904
+ const files = Array.isArray(input.files)
2905
+ ? input.files.map(record => normalizeFile(record, 'files'))
2906
+ : []
2907
+ const trashFiles = Array.isArray(input.trashFiles)
2908
+ ? input.trashFiles.map(record => normalizeFile(record, 'trashFiles'))
2909
+ : []
2910
+ const channels = Array.isArray(input.channels)
2911
+ ? input.channels
2912
+ .filter(channel => channel && typeof channel === 'object')
2913
+ .map(channel => ({
2914
+ name: String(channel.name || '').trim(),
2915
+ type: String(channel.type || 'personal').trim() || 'personal',
2916
+ coreKey: String(channel.coreKey || '').trim(),
2917
+ createdAt:
2918
+ typeof channel.createdAt === 'string' ? channel.createdAt : '',
2919
+ lastMessageAt:
2920
+ typeof channel.lastMessageAt === 'string'
2921
+ ? channel.lastMessageAt
2922
+ : '',
2923
+ member:
2924
+ channel.member && typeof channel.member === 'object'
2925
+ ? channel.member
2926
+ : null,
2927
+ remark: String(channel.remark || '').slice(0, 50),
2928
+ pinned: Boolean(channel.pinned),
2929
+ }))
2930
+ .filter(channel => CHANNEL_NAME_REGEX.test(channel.name))
2931
+ : []
2932
+
2933
+ return { files, trashFiles, channels }
2934
+ }
2935
+
2936
+ #applyImportedChannels(ownerAddress, channels = []) {
2937
+ const normalizedOwner = normalizeOwnerAddress(ownerAddress)
2938
+ if (!normalizedOwner) return
2939
+
2940
+ for (const imported of channels) {
2941
+ let channel = this.#channels.find(item => item.name === imported.name)
2942
+ if (!channel) {
2943
+ const discoveryKey = this.#generateChannelDiscoveryKey(imported.name)
2944
+ channel = {
2945
+ name: imported.name,
2946
+ discoveryKey: b4a.toString(discoveryKey, 'hex'),
2947
+ coreKey: imported.coreKey,
2948
+ createdAt: imported.createdAt || new Date().toISOString(),
2949
+ lastMessageAt: imported.lastMessageAt || '',
2950
+ type: imported.type,
2951
+ members: [],
2952
+ remoteCoreKeys: [],
2953
+ }
2954
+ this.#channels.push(channel)
2955
+ }
2956
+
2957
+ this.#upsertChannelMember(channel, {
2958
+ ownerAddress: normalizedOwner,
2959
+ displayName: imported.member?.displayName || imported.member?.name || '',
2960
+ avatar: imported.member?.avatar || '',
2961
+ })
2962
+
2963
+ if (imported.remark) {
2964
+ channel.remarks = channel.remarks || {}
2965
+ channel.remarks[normalizedOwner] = imported.remark
2966
+ }
2967
+ if (imported.pinned) {
2968
+ channel.pinnedBy = channel.pinnedBy || {}
2969
+ channel.pinnedBy[normalizedOwner] = true
2970
+ }
2971
+ }
2972
+ }
2973
+
2629
2974
  #getFileRuntimeStats(cid) {
2630
2975
  const state = this.#fileMonitors.get(cid)
2631
2976
  if (!state) {
@@ -2793,7 +3138,6 @@ export class MostBoxEngine extends EventEmitter {
2793
3138
  cid,
2794
3139
  fileName: record.fileName || cid,
2795
3140
  size,
2796
- localPath: record.localPath || null,
2797
3141
  topic: topicHex,
2798
3142
  driveName,
2799
3143
  source: record.source || 'manual',
@@ -2977,26 +3321,163 @@ export class MostBoxEngine extends EventEmitter {
2977
3321
  return drive
2978
3322
  }
2979
3323
 
2980
- #recordMatchesOwner(record, ownerAddress) {
3324
+ #getOwnerKey(ownerAddress) {
3325
+ return getOwnerBucketKey(ownerAddress)
3326
+ }
3327
+
3328
+ #getPublishedBucket(ownerAddress, create = false) {
3329
+ const ownerKey = this.#getOwnerKey(ownerAddress)
3330
+ if (!this.#publishedFiles[ownerKey] && create) {
3331
+ this.#publishedFiles[ownerKey] = []
3332
+ }
3333
+ return this.#publishedFiles[ownerKey] || []
3334
+ }
3335
+
3336
+ #getTrashBucket(ownerAddress, create = false) {
3337
+ const ownerKey = this.#getOwnerKey(ownerAddress)
3338
+ if (!this.#trashFiles[ownerKey] && create) {
3339
+ this.#trashFiles[ownerKey] = []
3340
+ }
3341
+ return this.#trashFiles[ownerKey] || []
3342
+ }
3343
+
3344
+ #setPublishedBucket(ownerAddress, records) {
3345
+ const ownerKey = this.#getOwnerKey(ownerAddress)
3346
+ const next = Array.isArray(records) ? records : []
3347
+ if (next.length === 0) {
3348
+ delete this.#publishedFiles[ownerKey]
3349
+ } else {
3350
+ this.#publishedFiles[ownerKey] = next
3351
+ }
3352
+ }
3353
+
3354
+ #setTrashBucket(ownerAddress, records) {
3355
+ const ownerKey = this.#getOwnerKey(ownerAddress)
3356
+ const next = Array.isArray(records) ? records : []
3357
+ if (next.length === 0) {
3358
+ delete this.#trashFiles[ownerKey]
3359
+ } else {
3360
+ this.#trashFiles[ownerKey] = next
3361
+ }
3362
+ }
3363
+
3364
+ #allPublishedRecords() {
3365
+ return Object.entries(this.#publishedFiles).flatMap(([owner, records]) =>
3366
+ records.map(record => cloneMetadataRecord(record, owner))
3367
+ )
3368
+ }
3369
+
3370
+ #allTrashRecords() {
3371
+ return Object.entries(this.#trashFiles).flatMap(([owner, records]) =>
3372
+ records.map(record => cloneMetadataRecord(record, owner))
3373
+ )
3374
+ }
3375
+
3376
+ #countBucketRecords(buckets) {
3377
+ return Object.values(buckets || {}).reduce(
3378
+ (sum, records) => sum + (Array.isArray(records) ? records.length : 0),
3379
+ 0
3380
+ )
3381
+ }
3382
+
3383
+ #collectUserCids(ownerAddress) {
3384
+ const cids = new Set()
3385
+ for (const file of this.#getPublishedBucket(ownerAddress)) {
3386
+ cids.add(file.cid)
3387
+ }
3388
+ for (const file of this.#getTrashBucket(ownerAddress)) {
3389
+ cids.add(file.cid)
3390
+ }
3391
+ return cids
3392
+ }
3393
+
3394
+ #removeUserFromChannels(ownerAddress) {
2981
3395
  const normalizedOwner = normalizeOwnerAddress(ownerAddress)
2982
- if (!normalizedOwner) return !record.ownerAddress
2983
- return normalizeOwnerAddress(record.ownerAddress) === normalizedOwner
3396
+ if (!normalizedOwner) return
3397
+
3398
+ this.#channels = this.#channels
3399
+ .map(channel => {
3400
+ const remarks = channel.remarks
3401
+ ? Object.fromEntries(
3402
+ Object.entries(channel.remarks).filter(
3403
+ ([address]) => normalizeOwnerAddress(address) !== normalizedOwner
3404
+ )
3405
+ )
3406
+ : undefined
3407
+ const pinnedBy = channel.pinnedBy
3408
+ ? Object.fromEntries(
3409
+ Object.entries(channel.pinnedBy).filter(
3410
+ ([address]) => normalizeOwnerAddress(address) !== normalizedOwner
3411
+ )
3412
+ )
3413
+ : undefined
3414
+ return {
3415
+ ...channel,
3416
+ remarks:
3417
+ remarks && Object.keys(remarks).length > 0 ? remarks : undefined,
3418
+ pinnedBy:
3419
+ pinnedBy && Object.keys(pinnedBy).length > 0 ? pinnedBy : undefined,
3420
+ members: Array.isArray(channel.members)
3421
+ ? channel.members.filter(
3422
+ member =>
3423
+ normalizeOwnerAddress(member?.address) !== normalizedOwner
3424
+ )
3425
+ : [],
3426
+ }
3427
+ })
3428
+ .filter(channel => channel.members.length > 0)
3429
+ }
3430
+
3431
+ async #cleanupUnreferencedCids(cids) {
3432
+ let removedReplicas = 0
3433
+ for (const cid of cids) {
3434
+ if (this.#hasAnyUserReference(cid)) continue
3435
+ const driveName = this.#getCidInfo(cid).driveName
3436
+ try {
3437
+ const drive = await this.#getOrCreateDrive(driveName)
3438
+ await drive.del('/' + cid)
3439
+ } catch {}
3440
+ await this.#closeDriveForSeed(driveName)
3441
+ await this.#leaveCidTopic(cid)
3442
+ this.#removeHolding(cid)
3443
+ removedReplicas += 1
3444
+ }
3445
+ return removedReplicas
3446
+ }
3447
+
3448
+ async #clearUserDataInternal(ownerAddress) {
3449
+ const affectedCids = this.#collectUserCids(ownerAddress)
3450
+ const removedFiles = this.#getPublishedBucket(ownerAddress).length
3451
+ const removedTrashFiles = this.#getTrashBucket(ownerAddress).length
3452
+
3453
+ this.#setPublishedBucket(ownerAddress, [])
3454
+ this.#setTrashBucket(ownerAddress, [])
3455
+ this.#removeUserFromChannels(ownerAddress)
3456
+ this.#savePublishedMetadata()
3457
+ this.#saveTrashMetadata()
3458
+ this.#saveChannelsMetadata()
3459
+
3460
+ const removedReplicas = await this.#cleanupUnreferencedCids(affectedCids)
3461
+ return {
3462
+ removedFiles,
3463
+ removedTrashFiles,
3464
+ removedReplicas,
3465
+ }
2984
3466
  }
2985
3467
 
2986
3468
  #assertDisplayNameAvailable(fileName, options = {}) {
2987
3469
  const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
3470
+ const files = this.#getPublishedBucket(ownerAddress)
2988
3471
  const safeFileName = sanitizeFilename(fileName)
2989
3472
  const folder = getDisplayPathFolder(safeFileName)
2990
3473
  const baseName = getPathBaseName(safeFileName)
2991
- const conflict = this.#publishedFiles.find(file => {
3474
+ const conflict = files.find(file => {
2992
3475
  if (
2993
3476
  options.excludeCid &&
2994
- file.cid === options.excludeCid &&
2995
- this.#recordMatchesOwner(file, ownerAddress)
3477
+ file.cid === options.excludeCid
2996
3478
  ) {
2997
3479
  return false
2998
3480
  }
2999
- if (!this.#recordMatchesOwner(file, ownerAddress)) return false
3000
3481
  const existingFileName = sanitizeFilename(file.fileName)
3001
3482
  return (
3002
3483
  getDisplayPathFolder(existingFileName) === folder &&
@@ -3009,13 +3490,13 @@ export class MostBoxEngine extends EventEmitter {
3009
3490
  }
3010
3491
 
3011
3492
  #hasPublishedReference(cid) {
3012
- return this.#publishedFiles.some(file => file.cid === cid)
3493
+ return this.#allPublishedRecords().some(file => file.cid === cid)
3013
3494
  }
3014
3495
 
3015
3496
  #hasAnyUserReference(cid) {
3016
3497
  return (
3017
- this.#publishedFiles.some(file => file.cid === cid) ||
3018
- this.#trashFiles.some(file => file.cid === cid)
3498
+ this.#allPublishedRecords().some(file => file.cid === cid) ||
3499
+ this.#allTrashRecords().some(file => file.cid === cid)
3019
3500
  )
3020
3501
  }
3021
3502
 
@@ -3080,7 +3561,13 @@ export class MostBoxEngine extends EventEmitter {
3080
3561
  if (fs.existsSync(metadataPath)) {
3081
3562
  const data = fs.readFileSync(metadataPath, 'utf-8')
3082
3563
  const parsed = JSON.parse(data)
3083
- return parsed.map(f => ({ ...f, starred: f.starred || false }))
3564
+ const buckets = normalizeMetadataBuckets(parsed)
3565
+ for (const records of Object.values(buckets)) {
3566
+ for (const record of records) {
3567
+ record.starred = record.starred || false
3568
+ }
3569
+ }
3570
+ return buckets
3084
3571
  }
3085
3572
  } catch (err) {
3086
3573
  console.warn(
@@ -3134,7 +3621,7 @@ export class MostBoxEngine extends EventEmitter {
3134
3621
  const metadataPath = this.#getTrashMetadataPath()
3135
3622
  if (fs.existsSync(metadataPath)) {
3136
3623
  const data = fs.readFileSync(metadataPath, 'utf-8')
3137
- return JSON.parse(data)
3624
+ return normalizeMetadataBuckets(JSON.parse(data))
3138
3625
  }
3139
3626
  } catch (err) {
3140
3627
  console.warn(
@@ -3181,7 +3668,13 @@ export class MostBoxEngine extends EventEmitter {
3181
3668
  #saveChannelsMetadata() {
3182
3669
  try {
3183
3670
  const metadataPath = this.#getChannelsMetadataPath()
3184
- this.#atomicWrite(metadataPath, JSON.stringify(this.#channels, null, 2))
3671
+ const persistentChannels = this.#channels.filter(
3672
+ channel => !TRANSIENT_CHANNEL_TYPES.has(channel?.type)
3673
+ )
3674
+ this.#atomicWrite(
3675
+ metadataPath,
3676
+ JSON.stringify(persistentChannels, null, 2)
3677
+ )
3185
3678
  } catch (err) {
3186
3679
  console.error('Failed to save channels metadata:', err.message)
3187
3680
  }
@@ -3211,6 +3704,15 @@ export class MostBoxEngine extends EventEmitter {
3211
3704
  try {
3212
3705
  const entry = await core.get(i)
3213
3706
  if (entry && entry.type === 'message') {
3707
+ const channel = this.#channels.find(c => c.name === channelName)
3708
+ if (channel) {
3709
+ const entryTime = Number(entry.timestamp) || Date.now()
3710
+ const currentTime = Date.parse(channel.lastMessageAt || '') || 0
3711
+ if (entryTime > currentTime) {
3712
+ channel.lastMessageAt = new Date(entryTime).toISOString()
3713
+ this.#saveChannelsMetadata()
3714
+ }
3715
+ }
3214
3716
  this.emit('channel:message', {
3215
3717
  channel: channelName,
3216
3718
  message: this.#normalizeChannelMessageForResponse(