most-box 0.1.7 → 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 (175) hide show
  1. package/out/404/index.html +2 -2
  2. package/out/404.html +2 -2
  3. package/out/__next.__PAGE__.txt +4 -4
  4. package/out/__next._full.txt +13 -13
  5. package/out/__next._head.txt +3 -3
  6. package/out/__next._index.txt +7 -7
  7. package/out/__next._tree.txt +2 -2
  8. package/out/_next/static/chunks/00t5ddkkjek9v.js +1 -0
  9. package/out/_next/static/chunks/0hodisxqbtmye.js +1 -0
  10. package/out/_next/static/chunks/0jinrmgt26crz.js +1 -0
  11. package/out/_next/static/chunks/0mslhkdi-msxv.js +1 -0
  12. package/out/_next/static/chunks/1x76jmdus29em.js +1 -0
  13. package/out/_next/static/chunks/22vt64m0krmev.js +1 -0
  14. package/out/_next/static/chunks/266xuv6ts5889.js +5 -0
  15. package/out/_next/static/chunks/{0nwdp-n94owg0.css → 2jnstwjg45vfp.css} +1 -1
  16. package/out/_next/static/chunks/2o-0n-9r5u1oh.js +1 -0
  17. package/out/_next/static/chunks/2pkm-6u9yq_cq.js +1 -0
  18. package/out/_next/static/chunks/31zdi2pl6em6-.css +1 -0
  19. package/out/_next/static/chunks/393isf5crmqic.js +1 -0
  20. package/out/_next/static/chunks/3ej6im7cbakx3.js +1 -0
  21. package/out/_next/static/chunks/{0s7i7nfr9j3b1.js → 3m7_o1ihz8ay_.js} +1 -1
  22. package/out/_next/static/chunks/3qm2vbylm324o.css +1 -0
  23. package/out/_next/static/chunks/{43l72x-2z3zxb.js → 3uxltn4677hjy.js} +1 -1
  24. package/out/_next/static/chunks/3w2qk10_udrd9.js +1 -0
  25. package/out/_next/static/chunks/3xl0fq2d3dle6.js +1 -0
  26. package/out/_next/static/chunks/{418_591yp64lx.css → 440es_b4nw-7c.css} +2 -2
  27. package/out/_next/static/chunks/{turbopack-026p0g9v9_pva.js → turbopack-3vvza9m6839dy.js} +1 -1
  28. package/out/_not-found/__next._full.txt +12 -12
  29. package/out/_not-found/__next._head.txt +3 -3
  30. package/out/_not-found/__next._index.txt +7 -7
  31. package/out/_not-found/__next._not-found.__PAGE__.txt +4 -4
  32. package/out/_not-found/__next._not-found.txt +3 -3
  33. package/out/_not-found/__next._tree.txt +2 -2
  34. package/out/_not-found/index.html +2 -2
  35. package/out/_not-found/index.txt +12 -12
  36. package/out/admin/__next._full.txt +13 -13
  37. package/out/admin/__next._head.txt +3 -3
  38. package/out/admin/__next._index.txt +7 -7
  39. package/out/admin/__next._tree.txt +2 -2
  40. package/out/admin/__next.admin.__PAGE__.txt +4 -4
  41. package/out/admin/__next.admin.txt +3 -3
  42. package/out/admin/index.html +2 -2
  43. package/out/admin/index.txt +13 -13
  44. package/out/app/__next._full.txt +13 -13
  45. package/out/app/__next._head.txt +3 -3
  46. package/out/app/__next._index.txt +7 -7
  47. package/out/app/__next._tree.txt +2 -2
  48. package/out/app/__next.app.__PAGE__.txt +4 -4
  49. package/out/app/__next.app.txt +3 -3
  50. package/out/app/index.html +2 -2
  51. package/out/app/index.txt +13 -13
  52. package/out/chat/__next._full.txt +14 -14
  53. package/out/chat/__next._head.txt +3 -3
  54. package/out/chat/__next._index.txt +7 -7
  55. package/out/chat/__next._tree.txt +3 -3
  56. package/out/chat/__next.chat.__PAGE__.txt +4 -4
  57. package/out/chat/__next.chat.txt +4 -4
  58. package/out/chat/index.html +2 -2
  59. package/out/chat/index.txt +14 -14
  60. package/out/chat/join/__next._full.txt +14 -14
  61. package/out/chat/join/__next._head.txt +3 -3
  62. package/out/chat/join/__next._index.txt +7 -7
  63. package/out/chat/join/__next._tree.txt +3 -3
  64. package/out/chat/join/__next.chat.join.__PAGE__.txt +4 -4
  65. package/out/chat/join/__next.chat.join.txt +3 -3
  66. package/out/chat/join/__next.chat.txt +4 -4
  67. package/out/chat/join/index.html +2 -2
  68. package/out/chat/join/index.txt +14 -14
  69. package/out/demo/__next._full.txt +15 -14
  70. package/out/demo/__next._head.txt +3 -3
  71. package/out/demo/__next._index.txt +7 -7
  72. package/out/demo/__next._tree.txt +4 -3
  73. package/out/demo/__next.demo.__PAGE__.txt +6 -5
  74. package/out/demo/__next.demo.txt +3 -3
  75. package/out/demo/index.html +3 -3
  76. package/out/demo/index.txt +15 -14
  77. package/out/download/__next._full.txt +36 -34
  78. package/out/download/__next._head.txt +3 -3
  79. package/out/download/__next._index.txt +7 -7
  80. package/out/download/__next._tree.txt +2 -2
  81. package/out/download/__next.download.__PAGE__.txt +5 -5
  82. package/out/download/__next.download.txt +3 -3
  83. package/out/download/index.html +2 -2
  84. package/out/download/index.txt +36 -34
  85. package/out/game/__next._full.txt +11 -11
  86. package/out/game/__next._head.txt +3 -3
  87. package/out/game/__next._index.txt +7 -7
  88. package/out/game/__next._tree.txt +2 -2
  89. package/out/game/__next.game.__PAGE__.txt +2 -2
  90. package/out/game/__next.game.txt +3 -3
  91. package/out/game/gandengyan/__next._full.txt +13 -13
  92. package/out/game/gandengyan/__next._head.txt +3 -3
  93. package/out/game/gandengyan/__next._index.txt +7 -7
  94. package/out/game/gandengyan/__next._tree.txt +2 -2
  95. package/out/game/gandengyan/__next.game.gandengyan.__PAGE__.txt +4 -4
  96. package/out/game/gandengyan/__next.game.gandengyan.txt +3 -3
  97. package/out/game/gandengyan/__next.game.txt +3 -3
  98. package/out/game/gandengyan/index.html +2 -2
  99. package/out/game/gandengyan/index.txt +13 -13
  100. package/out/game/index.html +1 -1
  101. package/out/game/index.txt +11 -11
  102. package/out/game/zhajinhua/__next._full.txt +13 -13
  103. package/out/game/zhajinhua/__next._head.txt +3 -3
  104. package/out/game/zhajinhua/__next._index.txt +7 -7
  105. package/out/game/zhajinhua/__next._tree.txt +2 -2
  106. package/out/game/zhajinhua/__next.game.txt +3 -3
  107. package/out/game/zhajinhua/__next.game.zhajinhua.__PAGE__.txt +4 -4
  108. package/out/game/zhajinhua/__next.game.zhajinhua.txt +3 -3
  109. package/out/game/zhajinhua/index.html +2 -2
  110. package/out/game/zhajinhua/index.txt +13 -13
  111. package/out/index.html +2 -2
  112. package/out/index.txt +13 -13
  113. package/out/note/__next._full.txt +13 -13
  114. package/out/note/__next._head.txt +3 -3
  115. package/out/note/__next._index.txt +7 -7
  116. package/out/note/__next._tree.txt +2 -2
  117. package/out/note/__next.note.__PAGE__.txt +4 -4
  118. package/out/note/__next.note.txt +3 -3
  119. package/out/note/index.html +2 -2
  120. package/out/note/index.txt +13 -13
  121. package/out/ping/__next._full.txt +13 -13
  122. package/out/ping/__next._head.txt +3 -3
  123. package/out/ping/__next._index.txt +7 -7
  124. package/out/ping/__next._tree.txt +2 -2
  125. package/out/ping/__next.ping.__PAGE__.txt +4 -4
  126. package/out/ping/__next.ping.txt +3 -3
  127. package/out/ping/index.html +2 -2
  128. package/out/ping/index.txt +13 -13
  129. package/out/web3/__next._full.txt +14 -14
  130. package/out/web3/__next._head.txt +3 -3
  131. package/out/web3/__next._index.txt +7 -7
  132. package/out/web3/__next._tree.txt +3 -3
  133. package/out/web3/__next.web3.__PAGE__.txt +4 -4
  134. package/out/web3/__next.web3.txt +4 -4
  135. package/out/web3/ed25519/__next._full.txt +12 -12
  136. package/out/web3/ed25519/__next._head.txt +3 -3
  137. package/out/web3/ed25519/__next._index.txt +7 -7
  138. package/out/web3/ed25519/__next._tree.txt +3 -3
  139. package/out/web3/ed25519/__next.web3.ed25519.__PAGE__.txt +2 -2
  140. package/out/web3/ed25519/__next.web3.ed25519.txt +3 -3
  141. package/out/web3/ed25519/__next.web3.txt +4 -4
  142. package/out/web3/ed25519/index.html +1 -1
  143. package/out/web3/ed25519/index.txt +12 -12
  144. package/out/web3/index.html +2 -2
  145. package/out/web3/index.txt +14 -14
  146. package/out/web3/tools/__next._full.txt +12 -12
  147. package/out/web3/tools/__next._head.txt +3 -3
  148. package/out/web3/tools/__next._index.txt +7 -7
  149. package/out/web3/tools/__next._tree.txt +3 -3
  150. package/out/web3/tools/__next.web3.tools.__PAGE__.txt +2 -2
  151. package/out/web3/tools/__next.web3.tools.txt +3 -3
  152. package/out/web3/tools/__next.web3.txt +4 -4
  153. package/out/web3/tools/index.html +1 -1
  154. package/out/web3/tools/index.txt +12 -12
  155. package/package.json +1 -1
  156. package/server/src/http/app.js +66 -1
  157. package/server/src/http/nodeStatus.js +18 -0
  158. package/server/src/index.js +659 -164
  159. package/out/_next/static/chunks/0-c8h4u1i2f78.js +0 -1
  160. package/out/_next/static/chunks/02j6ojja7vzp8.js +0 -1
  161. package/out/_next/static/chunks/06im3as4nwfil.js +0 -1
  162. package/out/_next/static/chunks/0sq_0pwxnhby2.js +0 -1
  163. package/out/_next/static/chunks/1ei8b9nw-ea8p.css +0 -1
  164. package/out/_next/static/chunks/22h69_zwmttt4.js +0 -1
  165. package/out/_next/static/chunks/23zlmarf-go2x.js +0 -1
  166. package/out/_next/static/chunks/24k_4-y4ym0df.js +0 -1
  167. package/out/_next/static/chunks/2693fczty5yr8.js +0 -1
  168. package/out/_next/static/chunks/2_i1b78h0yiu_.js +0 -1
  169. package/out/_next/static/chunks/2fmkcw8ui5xp3.js +0 -5
  170. package/out/_next/static/chunks/2nbju8t9-ux58.css +0 -1
  171. package/out/_next/static/chunks/3fea100s764m-.js +0 -1
  172. package/out/_next/static/chunks/3mccxoa60ley8.js +0 -1
  173. /package/out/_next/static/{eXddCOt9eyz0j78b1m4Ex → TXy93SnAZQ7OQ-aVUkmHa}/_buildManifest.js +0 -0
  174. /package/out/_next/static/{eXddCOt9eyz0j78b1m4Ex → TXy93SnAZQ7OQ-aVUkmHa}/_clientMiddlewareManifest.js +0 -0
  175. /package/out/_next/static/{eXddCOt9eyz0j78b1m4Ex → TXy93SnAZQ7OQ-aVUkmHa}/_ssgManifest.js +0 -0
@@ -68,12 +68,44 @@ import {
68
68
  const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
69
69
  const CHAT_FILE_ROOT = 'chat-file'
70
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
71
75
 
72
76
  function normalizeOwnerAddress(address) {
73
77
  const value = String(address || '').trim()
74
78
  return /^0x[a-fA-F0-9]{40}$/.test(value) ? value.toLowerCase() : ''
75
79
  }
76
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
+
77
109
  function getPathBaseName(fileName) {
78
110
  const parts = String(fileName || '').split('/').filter(Boolean)
79
111
  return parts[parts.length - 1] || 'unnamed_file'
@@ -123,12 +155,13 @@ export class MostBoxEngine extends EventEmitter {
123
155
  #store = null
124
156
  #swarm = null
125
157
  #drives = new Map()
126
- #publishedFiles = []
158
+ #publishedFiles = {}
127
159
  #holdings = []
128
- #trashFiles = []
160
+ #trashFiles = {}
129
161
  #initialized = false
130
162
  #options = null
131
163
  #activeDownloads = new Map()
164
+ #importChecks = new Map()
132
165
  #drivePromises = new Map()
133
166
  #fileDiscoveries = new Map()
134
167
  #fileMonitors = new Map()
@@ -303,7 +336,7 @@ export class MostBoxEngine extends EventEmitter {
303
336
 
304
337
  this.#publishedFiles = this.#loadPublishedMetadata()
305
338
  console.log(
306
- `[MostBox] Loaded ${this.#publishedFiles.length} published files`
339
+ `[MostBox] Loaded ${this.#countBucketRecords(this.#publishedFiles)} published files`
307
340
  )
308
341
 
309
342
  this.#holdings = this.#loadHoldingsMetadata()
@@ -318,7 +351,9 @@ export class MostBoxEngine extends EventEmitter {
318
351
  }
319
352
 
320
353
  this.#trashFiles = this.#loadTrashMetadata()
321
- console.log(`[MostBox] Loaded ${this.#trashFiles.length} trash files`)
354
+ console.log(
355
+ `[MostBox] Loaded ${this.#countBucketRecords(this.#trashFiles)} trash files`
356
+ )
322
357
 
323
358
  this.#channels = this.#loadChannelsMetadata()
324
359
  console.log(`[MostBox] Loaded ${this.#channels.length} channels`)
@@ -429,6 +464,7 @@ export class MostBoxEngine extends EventEmitter {
429
464
  this.#channelChatDiscoveries.clear()
430
465
  this.#channelPeers.clear()
431
466
  this.#channels = []
467
+ this.#importChecks.clear()
432
468
 
433
469
  if (this.#store) {
434
470
  await this.#store.close()
@@ -471,7 +507,6 @@ export class MostBoxEngine extends EventEmitter {
471
507
  * @param {string|Buffer} content - 文件路径(字符串)或内容(Buffer)
472
508
  * @param {string} [fileName] - 文件名(Buffer 输入时必填)
473
509
  * @param {object} [options] - 发布选项
474
- * @param {string|null} [options.localPath] - 持有记录中的本地路径
475
510
  * @returns {Promise<{ cid: string, link: string, fileName: string }>}
476
511
  */
477
512
  async publishFile(content, fileName, options = {}) {
@@ -526,15 +561,11 @@ export class MostBoxEngine extends EventEmitter {
526
561
  const { cid: rootCid } = await calculateCid(content)
527
562
  const cidString = rootCid.toString()
528
563
  const { driveName: name } = this.#getCidInfo(cidString)
529
- const holdingLocalPath =
530
- options.localPath === undefined ? cleanPath : options.localPath
531
-
564
+ const publishedBucket = this.#getPublishedBucket(ownerAddress, true)
532
565
  // 检查相同内容是否已存在
533
- const existingIndex = this.#publishedFiles.findIndex(
534
- f => f.cid === cidString && this.#recordMatchesOwner(f, ownerAddress)
535
- )
566
+ const existingIndex = publishedBucket.findIndex(f => f.cid === cidString)
536
567
  if (existingIndex !== -1) {
537
- const existing = this.#publishedFiles[existingIndex]
568
+ const existing = publishedBucket[existingIndex]
538
569
  await this.#joinCidTopicInternal(cidString, {
539
570
  server: true,
540
571
  client: false,
@@ -543,7 +574,6 @@ export class MostBoxEngine extends EventEmitter {
543
574
  cid: cidString,
544
575
  fileName: existing.fileName,
545
576
  size: fileSize,
546
- localPath: holdingLocalPath,
547
577
  driveName: name,
548
578
  source: 'published',
549
579
  })
@@ -614,20 +644,18 @@ export class MostBoxEngine extends EventEmitter {
614
644
  }
615
645
 
616
646
  // 存储 displayName(用户看到的文件夹路径),不存储 drivePath
617
- this.#publishedFiles.push({
647
+ publishedBucket.push({
618
648
  fileName: safeFileName,
619
649
  cid: cidString,
620
650
  driveName: name,
621
651
  publishedAt: new Date().toISOString(),
622
652
  starred: false,
623
- ownerAddress,
624
653
  })
625
654
  this.#savePublishedMetadata()
626
655
  this.#upsertHolding({
627
656
  cid: cidString,
628
657
  fileName: safeFileName,
629
658
  size: fileSize,
630
- localPath: holdingLocalPath,
631
659
  driveName: name,
632
660
  source: 'published',
633
661
  })
@@ -701,8 +729,6 @@ export class MostBoxEngine extends EventEmitter {
701
729
  size:
702
730
  existingHolding?.size ??
703
731
  (Number.isFinite(localContent.size) ? localContent.size : 0),
704
- localPath:
705
- existingHolding?.localPath || existingFile?.localPath || null,
706
732
  driveName: existingFile?.driveName || name,
707
733
  source: existingHolding?.source || 'published',
708
734
  })
@@ -960,27 +986,26 @@ export class MostBoxEngine extends EventEmitter {
960
986
  savedPath: savePath,
961
987
  }
962
988
 
963
- // 将下载的文件添加到已发布文件列表(displayName 用原始文件名)
964
- const existingIndex = this.#publishedFiles.findIndex(
965
- 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
966
992
  )
967
993
  this.#assertDisplayNameAvailable(sanitizedFileName, {
968
994
  ownerAddress,
969
995
  excludeCid: cidString,
970
996
  })
971
997
  if (existingIndex !== -1) {
972
- const existing = this.#publishedFiles[existingIndex]
998
+ const existing = publishedBucket[existingIndex]
973
999
  existing.fileName = sanitizedFileName
974
1000
  existing.driveName = name
975
1001
  existing.publishedAt = new Date().toISOString()
976
1002
  } else {
977
- this.#publishedFiles.push({
1003
+ publishedBucket.push({
978
1004
  fileName: sanitizedFileName,
979
1005
  cid: cidString,
980
1006
  driveName: name,
981
1007
  publishedAt: new Date().toISOString(),
982
1008
  starred: false,
983
- ownerAddress,
984
1009
  })
985
1010
  }
986
1011
  this.#savePublishedMetadata()
@@ -989,7 +1014,6 @@ export class MostBoxEngine extends EventEmitter {
989
1014
  cid: cidString,
990
1015
  fileName: sanitizedFileName,
991
1016
  size: savedSize,
992
- localPath: savePath,
993
1017
  driveName: name,
994
1018
  source: 'downloaded',
995
1019
  })
@@ -1119,12 +1143,8 @@ export class MostBoxEngine extends EventEmitter {
1119
1143
  */
1120
1144
  listPublishedFiles(options = {}) {
1121
1145
  this.#ensureInitialized()
1122
- let files = this.#publishedFiles
1123
1146
  const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
1124
-
1125
- if (ownerAddress) {
1126
- files = files.filter(f => this.#recordMatchesOwner(f, ownerAddress))
1127
- }
1147
+ let files = this.#getPublishedBucket(ownerAddress)
1128
1148
 
1129
1149
  if (options.starred === true) {
1130
1150
  files = files.filter(f => f.starred === true)
@@ -1136,7 +1156,7 @@ export class MostBoxEngine extends EventEmitter {
1136
1156
  link: `most://${f.cid}?filename=${encodeURIComponent(f.fileName)}`,
1137
1157
  publishedAt: f.publishedAt,
1138
1158
  starred: f.starred || false,
1139
- ownerAddress: f.ownerAddress || '',
1159
+ ownerAddress: ownerAddress || '',
1140
1160
  }))
1141
1161
  }
1142
1162
 
@@ -1148,17 +1168,16 @@ export class MostBoxEngine extends EventEmitter {
1148
1168
  toggleStarred(cid, options = {}) {
1149
1169
  this.#ensureInitialized()
1150
1170
  const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
1151
- const index = this.#publishedFiles.findIndex(
1152
- f => f.cid === cid && this.#recordMatchesOwner(f, ownerAddress)
1153
- )
1171
+ const files = this.#getPublishedBucket(ownerAddress)
1172
+ const index = files.findIndex(f => f.cid === cid)
1154
1173
  if (index === -1) {
1155
1174
  throw new Error('File not found')
1156
1175
  }
1157
- this.#publishedFiles[index].starred = !this.#publishedFiles[index].starred
1176
+ files[index].starred = !files[index].starred
1158
1177
  this.#savePublishedMetadata()
1159
1178
  return {
1160
1179
  cid,
1161
- starred: this.#publishedFiles[index].starred,
1180
+ starred: files[index].starred,
1162
1181
  }
1163
1182
  }
1164
1183
 
@@ -1170,29 +1189,28 @@ export class MostBoxEngine extends EventEmitter {
1170
1189
  async deletePublishedFile(cid, options = {}) {
1171
1190
  this.#ensureInitialized()
1172
1191
  const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
1173
- const index = this.#publishedFiles.findIndex(
1174
- f => f.cid === cid && this.#recordMatchesOwner(f, ownerAddress)
1175
- )
1192
+ const files = this.#getPublishedBucket(ownerAddress)
1193
+ const trashFiles = this.#getTrashBucket(ownerAddress, true)
1194
+ const index = files.findIndex(f => f.cid === cid)
1176
1195
  if (index !== -1) {
1177
- const fileRecord = this.#publishedFiles[index]
1196
+ const fileRecord = files[index]
1178
1197
  const holding = this.#holdings.find(item => item.cid === fileRecord.cid)
1179
1198
 
1180
- this.#trashFiles.push({
1199
+ trashFiles.push({
1181
1200
  fileName: fileRecord.fileName,
1182
1201
  cid: fileRecord.cid,
1183
1202
  driveName:
1184
1203
  fileRecord.driveName || this.#getCidInfo(fileRecord.cid).driveName,
1185
1204
  size: holding?.size ?? fileRecord.size ?? 0,
1186
- localPath: holding?.localPath || fileRecord.localPath || null,
1187
1205
  source: holding?.source || 'published',
1188
1206
  publishedAt: fileRecord.publishedAt,
1189
1207
  starred: fileRecord.starred || false,
1190
- ownerAddress: fileRecord.ownerAddress || ownerAddress,
1191
1208
  deletedAt: new Date().toISOString(),
1192
1209
  })
1193
1210
  this.#saveTrashMetadata()
1194
1211
 
1195
- this.#publishedFiles.splice(index, 1)
1212
+ files.splice(index, 1)
1213
+ this.#setPublishedBucket(ownerAddress, files)
1196
1214
  this.#savePublishedMetadata()
1197
1215
 
1198
1216
  if (!this.#hasPublishedReference(fileRecord.cid)) {
@@ -1213,16 +1231,14 @@ export class MostBoxEngine extends EventEmitter {
1213
1231
  listTrashFiles(options = {}) {
1214
1232
  this.#ensureInitialized()
1215
1233
  const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
1216
- const files = ownerAddress
1217
- ? this.#trashFiles.filter(f => this.#recordMatchesOwner(f, ownerAddress))
1218
- : this.#trashFiles
1234
+ const files = this.#getTrashBucket(ownerAddress)
1219
1235
  return files.map(f => ({
1220
1236
  fileName: f.fileName,
1221
1237
  cid: f.cid,
1222
1238
  link: `most://${f.cid}?filename=${encodeURIComponent(f.fileName)}`,
1223
1239
  publishedAt: f.publishedAt,
1224
1240
  starred: f.starred || false,
1225
- ownerAddress: f.ownerAddress || '',
1241
+ ownerAddress: ownerAddress || '',
1226
1242
  deletedAt: f.deletedAt,
1227
1243
  }))
1228
1244
  }
@@ -1235,22 +1251,23 @@ export class MostBoxEngine extends EventEmitter {
1235
1251
  async restoreTrashFile(cid, options = {}) {
1236
1252
  this.#ensureInitialized()
1237
1253
  const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
1238
- const index = this.#trashFiles.findIndex(
1239
- f => f.cid === cid && this.#recordMatchesOwner(f, ownerAddress)
1240
- )
1254
+ const trashFiles = this.#getTrashBucket(ownerAddress)
1255
+ const publishedFiles = this.#getPublishedBucket(ownerAddress, true)
1256
+ const index = trashFiles.findIndex(f => f.cid === cid)
1241
1257
  if (index === -1) {
1242
1258
  throw new Error('File not found in trash')
1243
1259
  }
1244
1260
 
1245
- const fileRecord = this.#trashFiles[index]
1261
+ const fileRecord = trashFiles[index]
1246
1262
 
1247
1263
  const { driveName } = this.#getCidInfo(fileRecord.cid)
1248
1264
 
1249
- const existingIndex = this.#publishedFiles.findIndex(
1250
- f => f.cid === fileRecord.cid && this.#recordMatchesOwner(f, ownerAddress)
1265
+ const existingIndex = publishedFiles.findIndex(
1266
+ f => f.cid === fileRecord.cid
1251
1267
  )
1252
1268
  if (existingIndex !== -1) {
1253
- this.#trashFiles.splice(index, 1)
1269
+ trashFiles.splice(index, 1)
1270
+ this.#setTrashBucket(ownerAddress, trashFiles)
1254
1271
  this.#saveTrashMetadata()
1255
1272
  return this.listPublishedFiles({ ownerAddress })
1256
1273
  }
@@ -1260,17 +1277,17 @@ export class MostBoxEngine extends EventEmitter {
1260
1277
  excludeCid: fileRecord.cid,
1261
1278
  })
1262
1279
 
1263
- this.#publishedFiles.push({
1280
+ publishedFiles.push({
1264
1281
  fileName: fileRecord.fileName,
1265
1282
  cid: fileRecord.cid,
1266
1283
  driveName,
1267
1284
  publishedAt: fileRecord.publishedAt,
1268
1285
  starred: fileRecord.starred || false,
1269
- ownerAddress: fileRecord.ownerAddress || ownerAddress,
1270
1286
  })
1271
1287
  this.#savePublishedMetadata()
1272
1288
 
1273
- this.#trashFiles.splice(index, 1)
1289
+ trashFiles.splice(index, 1)
1290
+ this.#setTrashBucket(ownerAddress, trashFiles)
1274
1291
  this.#saveTrashMetadata()
1275
1292
 
1276
1293
  await this.#joinCidTopicInternal(fileRecord.cid, {
@@ -1281,7 +1298,6 @@ export class MostBoxEngine extends EventEmitter {
1281
1298
  cid: fileRecord.cid,
1282
1299
  fileName: fileRecord.fileName,
1283
1300
  size: Number(fileRecord.size) || 0,
1284
- localPath: fileRecord.localPath || null,
1285
1301
  driveName,
1286
1302
  source: fileRecord.source || 'published',
1287
1303
  })
@@ -1297,15 +1313,15 @@ export class MostBoxEngine extends EventEmitter {
1297
1313
  async permanentDeleteTrashFile(cid, options = {}) {
1298
1314
  this.#ensureInitialized()
1299
1315
  const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
1300
- const index = this.#trashFiles.findIndex(
1301
- f => f.cid === cid && this.#recordMatchesOwner(f, ownerAddress)
1302
- )
1316
+ const trashFiles = this.#getTrashBucket(ownerAddress)
1317
+ const index = trashFiles.findIndex(f => f.cid === cid)
1303
1318
  if (index !== -1) {
1304
- const fileRecord = this.#trashFiles[index]
1319
+ const fileRecord = trashFiles[index]
1305
1320
  const driveName =
1306
1321
  fileRecord.driveName || this.#getCidInfo(fileRecord.cid).driveName
1307
1322
 
1308
- this.#trashFiles.splice(index, 1)
1323
+ trashFiles.splice(index, 1)
1324
+ this.#setTrashBucket(ownerAddress, trashFiles)
1309
1325
  this.#saveTrashMetadata()
1310
1326
 
1311
1327
  if (!this.#hasAnyUserReference(fileRecord.cid)) {
@@ -1330,18 +1346,8 @@ export class MostBoxEngine extends EventEmitter {
1330
1346
  async emptyTrash(options = {}) {
1331
1347
  this.#ensureInitialized()
1332
1348
  const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
1333
- const remainingTrash = []
1334
- const removedTrash = []
1335
-
1336
- for (const fileRecord of this.#trashFiles) {
1337
- if (ownerAddress && !this.#recordMatchesOwner(fileRecord, ownerAddress)) {
1338
- remainingTrash.push(fileRecord)
1339
- continue
1340
- }
1341
- removedTrash.push(fileRecord)
1342
- }
1343
-
1344
- this.#trashFiles = remainingTrash
1349
+ const removedTrash = [...this.#getTrashBucket(ownerAddress)]
1350
+ this.#setTrashBucket(ownerAddress, [])
1345
1351
  this.#saveTrashMetadata()
1346
1352
 
1347
1353
  for (const fileRecord of removedTrash) {
@@ -1420,15 +1426,11 @@ export class MostBoxEngine extends EventEmitter {
1420
1426
  used: usedSize,
1421
1427
  free: freeSize,
1422
1428
  fileCount: ownerAddress
1423
- ? this.#publishedFiles.filter(f =>
1424
- this.#recordMatchesOwner(f, ownerAddress)
1425
- ).length
1426
- : this.#publishedFiles.length,
1429
+ ? this.#getPublishedBucket(ownerAddress).length
1430
+ : this.#countBucketRecords(this.#publishedFiles),
1427
1431
  trashCount: ownerAddress
1428
- ? this.#trashFiles.filter(f =>
1429
- this.#recordMatchesOwner(f, ownerAddress)
1430
- ).length
1431
- : this.#trashFiles.length,
1432
+ ? this.#getTrashBucket(ownerAddress).length
1433
+ : this.#countBucketRecords(this.#trashFiles),
1432
1434
  }
1433
1435
  }
1434
1436
 
@@ -1442,9 +1444,8 @@ export class MostBoxEngine extends EventEmitter {
1442
1444
  moveFile(cid, newFileName, options = {}) {
1443
1445
  this.#ensureInitialized()
1444
1446
  const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
1445
- const index = this.#publishedFiles.findIndex(
1446
- f => f.cid === cid && this.#recordMatchesOwner(f, ownerAddress)
1447
- )
1447
+ const files = this.#getPublishedBucket(ownerAddress)
1448
+ const index = files.findIndex(f => f.cid === cid)
1448
1449
  if (index === -1) {
1449
1450
  throw new Error('File not found')
1450
1451
  }
@@ -1453,8 +1454,8 @@ export class MostBoxEngine extends EventEmitter {
1453
1454
  ownerAddress,
1454
1455
  excludeCid: cid,
1455
1456
  })
1456
- this.#publishedFiles[index].fileName = safeFileName
1457
- this.#publishedFiles[index].publishedAt = new Date().toISOString()
1457
+ files[index].fileName = safeFileName
1458
+ files[index].publishedAt = new Date().toISOString()
1458
1459
  this.#savePublishedMetadata()
1459
1460
  return {
1460
1461
  cid,
@@ -1475,12 +1476,10 @@ export class MostBoxEngine extends EventEmitter {
1475
1476
  const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
1476
1477
  const prefix = oldPath + '/'
1477
1478
  const updates = []
1479
+ const files = this.#getPublishedBucket(ownerAddress)
1478
1480
 
1479
- for (const file of this.#publishedFiles) {
1480
- if (
1481
- file.fileName.startsWith(prefix) &&
1482
- this.#recordMatchesOwner(file, ownerAddress)
1483
- ) {
1481
+ for (const file of files) {
1482
+ if (file.fileName.startsWith(prefix)) {
1484
1483
  const remainder = file.fileName.substring(prefix.length)
1485
1484
  const newFileName = sanitizeFilename(
1486
1485
  remainder ? newPath + '/' + remainder : newPath
@@ -1545,10 +1544,8 @@ export class MostBoxEngine extends EventEmitter {
1545
1544
  getPublishedFiles(options = {}) {
1546
1545
  const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
1547
1546
  return ownerAddress
1548
- ? this.#publishedFiles.filter(f =>
1549
- this.#recordMatchesOwner(f, ownerAddress)
1550
- )
1551
- : this.#publishedFiles
1547
+ ? this.#getPublishedBucket(ownerAddress)
1548
+ : this.#allPublishedRecords()
1552
1549
  }
1553
1550
 
1554
1551
  listUsers() {
@@ -1569,17 +1566,21 @@ export class MostBoxEngine extends EventEmitter {
1569
1566
  return users.get(ownerAddress)
1570
1567
  }
1571
1568
 
1572
- for (const file of this.#publishedFiles) {
1573
- const entry = ensure(file.ownerAddress)
1569
+ for (const [ownerAddress, files] of Object.entries(this.#publishedFiles)) {
1570
+ const entry = ensure(ownerAddress)
1574
1571
  if (!entry) continue
1575
- entry.fileCount += 1
1576
- entry.cids.add(file.cid)
1572
+ entry.fileCount += files.length
1573
+ for (const file of files) {
1574
+ entry.cids.add(file.cid)
1575
+ }
1577
1576
  }
1578
- for (const file of this.#trashFiles) {
1579
- const entry = ensure(file.ownerAddress)
1577
+ for (const [ownerAddress, files] of Object.entries(this.#trashFiles)) {
1578
+ const entry = ensure(ownerAddress)
1580
1579
  if (!entry) continue
1581
- entry.trashCount += 1
1582
- entry.cids.add(file.cid)
1580
+ entry.trashCount += files.length
1581
+ for (const file of files) {
1582
+ entry.cids.add(file.cid)
1583
+ }
1583
1584
  }
1584
1585
 
1585
1586
  return [...users.values()].map(user => ({
@@ -1597,58 +1598,258 @@ export class MostBoxEngine extends EventEmitter {
1597
1598
  throw new ValidationError('valid owner address is required')
1598
1599
  }
1599
1600
 
1600
- const affectedCids = new Set()
1601
- const beforeFiles = this.#publishedFiles.length
1602
- const beforeTrash = this.#trashFiles.length
1601
+ const result = await this.#clearUserDataInternal(ownerAddress)
1603
1602
 
1604
- this.#publishedFiles = this.#publishedFiles.filter(file => {
1605
- if (this.#recordMatchesOwner(file, ownerAddress)) {
1606
- affectedCids.add(file.cid)
1607
- return false
1608
- }
1609
- return true
1610
- })
1611
- this.#trashFiles = this.#trashFiles.filter(file => {
1612
- if (this.#recordMatchesOwner(file, ownerAddress)) {
1613
- affectedCids.add(file.cid)
1614
- return false
1615
- }
1616
- return true
1617
- })
1618
- 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))
1619
1637
  .map(channel => ({
1620
- ...channel,
1621
- members: Array.isArray(channel.members)
1622
- ? channel.members.filter(
1623
- member => normalizeOwnerAddress(member?.address) !== ownerAddress
1624
- )
1625
- : [],
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]),
1626
1648
  }))
1627
- .filter(channel => channel.members.length > 0)
1628
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)
1629
1820
  this.#savePublishedMetadata()
1630
1821
  this.#saveTrashMetadata()
1631
1822
  this.#saveChannelsMetadata()
1823
+ previous.removedReplicas = await this.#cleanupUnreferencedCids(previousCids)
1632
1824
 
1633
- let removedReplicas = 0
1634
- for (const cid of affectedCids) {
1635
- if (this.#hasAnyUserReference(cid)) continue
1636
- const driveName = this.#getCidInfo(cid).driveName
1825
+ const importedFiles = []
1826
+ const failedFiles = []
1827
+ for (const file of normalized.files) {
1637
1828
  try {
1638
- const drive = await this.#getOrCreateDrive(driveName)
1639
- await drive.del('/' + cid)
1640
- } catch {}
1641
- await this.#closeDriveForSeed(driveName)
1642
- await this.#leaveCidTopic(cid)
1643
- this.#removeHolding(cid)
1644
- 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
+ }
1645
1843
  }
1646
1844
 
1647
1845
  return {
1846
+ success: failedFiles.length === 0,
1648
1847
  ownerAddress,
1649
- removedFiles: beforeFiles - this.#publishedFiles.length,
1650
- removedTrashFiles: beforeTrash - this.#trashFiles.length,
1651
- removedReplicas,
1848
+ replacedFiles: previous.removedFiles,
1849
+ replacedTrashFiles: previous.removedTrashFiles,
1850
+ importedFiles: importedFiles.length,
1851
+ importedTrashFiles: normalized.trashFiles.length,
1852
+ failedFiles,
1652
1853
  }
1653
1854
  }
1654
1855
 
@@ -1907,11 +2108,14 @@ export class MostBoxEngine extends EventEmitter {
1907
2108
 
1908
2109
  async #getLocalCidContent(cid, options = {}) {
1909
2110
  const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
1910
- const fileRecord = this.#publishedFiles.find(
1911
- f =>
1912
- f.cid === cid &&
1913
- (options.public || this.#recordMatchesOwner(f, ownerAddress))
2111
+ const ownerRecord = this.#getPublishedBucket(ownerAddress).find(
2112
+ f => f.cid === cid
1914
2113
  )
2114
+ const fileRecord =
2115
+ ownerRecord ||
2116
+ (options.public
2117
+ ? this.#allPublishedRecords().find(f => f.cid === cid)
2118
+ : null)
1915
2119
  if (!options.allowHoldingFallback && !fileRecord) {
1916
2120
  return null
1917
2121
  }
@@ -1946,7 +2150,6 @@ export class MostBoxEngine extends EventEmitter {
1946
2150
  cid,
1947
2151
  fileName: holding?.fileName || cid,
1948
2152
  driveName: holding?.driveName || driveName,
1949
- localPath: holding?.localPath || null,
1950
2153
  size,
1951
2154
  ownerAddress,
1952
2155
  },
@@ -2250,9 +2453,36 @@ export class MostBoxEngine extends EventEmitter {
2250
2453
  return trimmed
2251
2454
  }
2252
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
+
2253
2483
  /**
2254
2484
  * 列出所有频道
2255
- * @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 }>}
2256
2486
  */
2257
2487
  listChannels(options = {}) {
2258
2488
  this.#ensureInitialized()
@@ -2274,9 +2504,11 @@ export class MostBoxEngine extends EventEmitter {
2274
2504
  name: c.name,
2275
2505
  coreKey: c.coreKey,
2276
2506
  createdAt: c.createdAt,
2507
+ lastMessageAt: c.lastMessageAt || '',
2277
2508
  type: c.type,
2278
2509
  peerCount: (this.#channelPeers.get(c.name) || new Map()).size,
2279
2510
  remark: ownerAddress && c.remarks ? c.remarks[ownerAddress] || '' : '',
2511
+ pinned: Boolean(ownerAddress && c.pinnedBy?.[ownerAddress]),
2280
2512
  }))
2281
2513
  }
2282
2514
 
@@ -2406,6 +2638,10 @@ export class MostBoxEngine extends EventEmitter {
2406
2638
  }
2407
2639
 
2408
2640
  await core.append(message)
2641
+ if (channel) {
2642
+ channel.lastMessageAt = new Date(message.timestamp).toISOString()
2643
+ this.#saveChannelsMetadata()
2644
+ }
2409
2645
 
2410
2646
  return message
2411
2647
  }
@@ -2627,6 +2863,114 @@ export class MostBoxEngine extends EventEmitter {
2627
2863
  }
2628
2864
  }
2629
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
+
2630
2974
  #getFileRuntimeStats(cid) {
2631
2975
  const state = this.#fileMonitors.get(cid)
2632
2976
  if (!state) {
@@ -2794,7 +3138,6 @@ export class MostBoxEngine extends EventEmitter {
2794
3138
  cid,
2795
3139
  fileName: record.fileName || cid,
2796
3140
  size,
2797
- localPath: record.localPath || null,
2798
3141
  topic: topicHex,
2799
3142
  driveName,
2800
3143
  source: record.source || 'manual',
@@ -2978,26 +3321,163 @@ export class MostBoxEngine extends EventEmitter {
2978
3321
  return drive
2979
3322
  }
2980
3323
 
2981
- #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) {
2982
3395
  const normalizedOwner = normalizeOwnerAddress(ownerAddress)
2983
- if (!normalizedOwner) return !record.ownerAddress
2984
- 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
+ }
2985
3466
  }
2986
3467
 
2987
3468
  #assertDisplayNameAvailable(fileName, options = {}) {
2988
3469
  const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
3470
+ const files = this.#getPublishedBucket(ownerAddress)
2989
3471
  const safeFileName = sanitizeFilename(fileName)
2990
3472
  const folder = getDisplayPathFolder(safeFileName)
2991
3473
  const baseName = getPathBaseName(safeFileName)
2992
- const conflict = this.#publishedFiles.find(file => {
3474
+ const conflict = files.find(file => {
2993
3475
  if (
2994
3476
  options.excludeCid &&
2995
- file.cid === options.excludeCid &&
2996
- this.#recordMatchesOwner(file, ownerAddress)
3477
+ file.cid === options.excludeCid
2997
3478
  ) {
2998
3479
  return false
2999
3480
  }
3000
- if (!this.#recordMatchesOwner(file, ownerAddress)) return false
3001
3481
  const existingFileName = sanitizeFilename(file.fileName)
3002
3482
  return (
3003
3483
  getDisplayPathFolder(existingFileName) === folder &&
@@ -3010,13 +3490,13 @@ export class MostBoxEngine extends EventEmitter {
3010
3490
  }
3011
3491
 
3012
3492
  #hasPublishedReference(cid) {
3013
- return this.#publishedFiles.some(file => file.cid === cid)
3493
+ return this.#allPublishedRecords().some(file => file.cid === cid)
3014
3494
  }
3015
3495
 
3016
3496
  #hasAnyUserReference(cid) {
3017
3497
  return (
3018
- this.#publishedFiles.some(file => file.cid === cid) ||
3019
- this.#trashFiles.some(file => file.cid === cid)
3498
+ this.#allPublishedRecords().some(file => file.cid === cid) ||
3499
+ this.#allTrashRecords().some(file => file.cid === cid)
3020
3500
  )
3021
3501
  }
3022
3502
 
@@ -3081,7 +3561,13 @@ export class MostBoxEngine extends EventEmitter {
3081
3561
  if (fs.existsSync(metadataPath)) {
3082
3562
  const data = fs.readFileSync(metadataPath, 'utf-8')
3083
3563
  const parsed = JSON.parse(data)
3084
- 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
3085
3571
  }
3086
3572
  } catch (err) {
3087
3573
  console.warn(
@@ -3135,7 +3621,7 @@ export class MostBoxEngine extends EventEmitter {
3135
3621
  const metadataPath = this.#getTrashMetadataPath()
3136
3622
  if (fs.existsSync(metadataPath)) {
3137
3623
  const data = fs.readFileSync(metadataPath, 'utf-8')
3138
- return JSON.parse(data)
3624
+ return normalizeMetadataBuckets(JSON.parse(data))
3139
3625
  }
3140
3626
  } catch (err) {
3141
3627
  console.warn(
@@ -3218,6 +3704,15 @@ export class MostBoxEngine extends EventEmitter {
3218
3704
  try {
3219
3705
  const entry = await core.get(i)
3220
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
+ }
3221
3716
  this.emit('channel:message', {
3222
3717
  channel: channelName,
3223
3718
  message: this.#normalizeChannelMessageForResponse(