most-box 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -14
- package/electron/main.js +72 -8
- package/electron/preload.js +6 -0
- package/out/404/index.html +2 -2
- package/out/404.html +2 -2
- package/out/__next.__PAGE__.txt +6 -6
- package/out/__next._full.txt +16 -16
- package/out/__next._head.txt +3 -3
- package/out/__next._index.txt +8 -8
- package/out/__next._tree.txt +5 -5
- package/out/_next/static/chunks/0.e2avjgna_b2.js +1 -0
- package/out/_next/static/chunks/{0ho~log~~-jwp.css → 03h~nhgj0hv3p.css} +1 -1
- package/out/_next/static/chunks/07td.jq7xff84.css +1 -0
- package/out/_next/static/chunks/{0qou.u2e2dy48.css → 0adx~d-j05c9d.css} +2 -2
- package/out/_next/static/chunks/0aq.rc9woa2nz.js +1 -0
- package/out/_next/static/chunks/0etes81d_cihn.js +1 -0
- package/out/_next/static/chunks/0g_a~e050bgzg.css +1 -0
- package/out/_next/static/chunks/{0o6lrkxy4jwag.js → 0gcsdf57gcm6h.js} +1 -1
- package/out/_next/static/chunks/0gwian.hp3-92.js +1 -0
- package/out/_next/static/chunks/0hpev4am9jpmu.css +1 -0
- package/out/_next/static/chunks/0l5_.uqb-uqb8.js +1 -0
- package/out/_next/static/chunks/0mex8svsiv-2l.js +1 -0
- package/out/_next/static/chunks/0myq9gs8szydh.js +1 -0
- package/out/_next/static/chunks/{0usvo~vu7r8np.js → 0o9ce4cyf76by.js} +1 -1
- package/out/_next/static/chunks/0p0sv~fuddvgr.js +1 -0
- package/out/_next/static/chunks/{0o98f1yq..o.8.js → 0pt.5cg1t09qs.js} +1 -1
- package/out/_next/static/chunks/0q0ksgxg98xgd.js +1 -0
- package/out/_next/static/chunks/0qgx9t4jx16ua.css +1 -0
- package/out/_next/static/chunks/0ukyg~tkm~h2m.css +1 -0
- package/out/_next/static/chunks/0wtf0xsiicxx6.js +1 -0
- package/out/_next/static/chunks/0xdwau5k2augv.css +4 -0
- package/out/_next/static/chunks/12nr19.nnn6s3.js +5 -0
- package/out/_next/static/chunks/{0qub_r0x_r-e9.css → 12pep-2t-qg4n.css} +1 -1
- package/out/_next/static/chunks/14_inksek_rth.js +2 -0
- package/out/_next/static/chunks/153-sz7s.qml2.js +1 -0
- package/out/_next/static/chunks/16xls5tt_68lx.js +1 -0
- package/out/_next/static/chunks/{turbopack-0xs6mybc~5t_3.js → turbopack-0xta0kqwzkf28.js} +1 -1
- package/out/_not-found/__next._full.txt +13 -13
- package/out/_not-found/__next._head.txt +3 -3
- package/out/_not-found/__next._index.txt +8 -8
- package/out/_not-found/__next._not-found.__PAGE__.txt +4 -4
- package/out/_not-found/__next._not-found.txt +3 -3
- package/out/_not-found/__next._tree.txt +3 -3
- package/out/_not-found/index.html +2 -2
- package/out/_not-found/index.txt +13 -13
- package/out/admin/__next._full.txt +15 -15
- package/out/admin/__next._head.txt +3 -3
- package/out/admin/__next._index.txt +8 -8
- package/out/admin/__next._tree.txt +4 -4
- package/out/admin/__next.admin.__PAGE__.txt +4 -4
- package/out/admin/__next.admin.txt +4 -4
- package/out/admin/index.html +2 -2
- package/out/admin/index.txt +15 -15
- package/out/app/__next._full.txt +14 -14
- package/out/app/__next._head.txt +3 -3
- package/out/app/__next._index.txt +8 -8
- package/out/app/__next._tree.txt +3 -3
- package/out/app/__next.app.__PAGE__.txt +4 -4
- package/out/app/__next.app.txt +3 -3
- package/out/app/index.html +2 -2
- package/out/app/index.txt +14 -14
- package/out/chat/__next._full.txt +15 -15
- package/out/chat/__next._head.txt +3 -3
- package/out/chat/__next._index.txt +8 -8
- package/out/chat/__next._tree.txt +4 -4
- package/out/chat/__next.chat.__PAGE__.txt +4 -4
- package/out/chat/__next.chat.txt +4 -4
- package/out/chat/index.html +2 -2
- package/out/chat/index.txt +15 -15
- package/out/chat/join/__next._full.txt +25 -0
- package/out/chat/join/__next._head.txt +5 -0
- package/out/{changelog → chat/join}/__next._index.txt +8 -8
- package/out/chat/join/__next._tree.txt +5 -0
- package/out/chat/join/__next.chat.join.__PAGE__.txt +9 -0
- package/out/chat/join/__next.chat.join.txt +5 -0
- package/out/chat/join/__next.chat.txt +5 -0
- package/out/chat/join/index.html +15 -0
- package/out/chat/join/index.txt +25 -0
- package/out/download/__next._full.txt +21 -21
- package/out/download/__next._head.txt +3 -3
- package/out/download/__next._index.txt +8 -8
- package/out/download/__next._tree.txt +5 -5
- package/out/download/__next.download.__PAGE__.txt +9 -9
- package/out/download/__next.download.txt +3 -3
- package/out/download/index.html +2 -2
- package/out/download/index.txt +21 -21
- package/out/index.html +2 -2
- package/out/index.txt +16 -16
- package/out/note/__next._full.txt +14 -14
- package/out/note/__next._head.txt +3 -3
- package/out/note/__next._index.txt +8 -8
- package/out/note/__next._tree.txt +3 -3
- package/out/note/__next.note.__PAGE__.txt +4 -4
- package/out/note/__next.note.txt +3 -3
- package/out/note/index.html +2 -2
- package/out/note/index.txt +14 -14
- package/out/ping/__next._full.txt +16 -16
- package/out/ping/__next._head.txt +3 -3
- package/out/ping/__next._index.txt +8 -8
- package/out/ping/__next._tree.txt +5 -5
- package/out/ping/__next.ping.__PAGE__.txt +5 -5
- package/out/ping/__next.ping.txt +4 -4
- package/out/ping/index.html +2 -2
- package/out/ping/index.txt +16 -16
- package/out/web3/__next._full.txt +15 -15
- package/out/web3/__next._head.txt +3 -3
- package/out/web3/__next._index.txt +8 -8
- package/out/web3/__next._tree.txt +4 -4
- package/out/web3/__next.web3.__PAGE__.txt +4 -4
- package/out/web3/__next.web3.txt +4 -4
- package/out/web3/ed25519/__next._full.txt +13 -13
- package/out/web3/ed25519/__next._head.txt +3 -3
- package/out/web3/ed25519/__next._index.txt +8 -8
- package/out/web3/ed25519/__next._tree.txt +4 -4
- package/out/web3/ed25519/__next.web3.ed25519.__PAGE__.txt +2 -2
- package/out/web3/ed25519/__next.web3.ed25519.txt +3 -3
- package/out/web3/ed25519/__next.web3.txt +4 -4
- package/out/web3/ed25519/index.html +1 -1
- package/out/web3/ed25519/index.txt +13 -13
- package/out/web3/index.html +2 -2
- package/out/web3/index.txt +15 -15
- package/out/web3/tools/__next._full.txt +13 -13
- package/out/web3/tools/__next._head.txt +3 -3
- package/out/web3/tools/__next._index.txt +8 -8
- package/out/web3/tools/__next._tree.txt +4 -4
- package/out/web3/tools/__next.web3.tools.__PAGE__.txt +2 -2
- package/out/web3/tools/__next.web3.tools.txt +3 -3
- package/out/web3/tools/__next.web3.txt +4 -4
- package/out/web3/tools/index.html +1 -1
- package/out/web3/tools/index.txt +13 -13
- package/package.json +19 -9
- package/server/index.js +133 -1303
- package/server/src/config.js +1 -1
- package/server/src/core/channelAttachment.js +68 -0
- package/server/src/core/cid.js +2 -88
- package/server/src/core/cidTopic.js +29 -0
- package/server/src/core/mostLink.js +88 -0
- package/server/src/http/access.js +123 -0
- package/server/src/http/app.js +1095 -0
- package/server/src/http/errors.js +35 -0
- package/server/src/http/nodeLogs.js +53 -0
- package/server/src/http/nodeStatus.js +146 -0
- package/server/src/http/staticFiles.js +84 -0
- package/server/src/http/uploads.js +114 -0
- package/server/src/index.js +799 -211
- package/server/src/node/config.js +38 -6
- package/server/src/utils/api.js +287 -14
- package/server/src/utils/auth.js +63 -0
- package/server/src/utils/dateTime.js +30 -0
- package/server/src/utils/downloadMessages.js +89 -0
- package/server/src/utils/errors.js +7 -0
- package/server/src/utils/mostWallet.js +151 -0
- package/server/src/utils/mp.js +2 -26
- package/server/src/utils/noteBackup.js +2 -5
- package/server/src/utils/noteUtils.js +11 -3
- package/server/src/utils/userIdentity.js +0 -1
- package/out/_next/static/chunks/00-u5nq76f0.j.js +0 -1
- package/out/_next/static/chunks/00fm8lijienf1.js +0 -1
- package/out/_next/static/chunks/00o9ht.f2qm00.css +0 -4
- package/out/_next/static/chunks/00zi-erhjrny2.js +0 -2
- package/out/_next/static/chunks/084xf0edl9sfo.js +0 -1
- package/out/_next/static/chunks/09f1gfke9m5wg.css +0 -1
- package/out/_next/static/chunks/09xyi6fpro_d-.css +0 -1
- package/out/_next/static/chunks/0_npg_pcoywti.js +0 -5
- package/out/_next/static/chunks/0_r_mk1~6bosc.js +0 -1
- package/out/_next/static/chunks/0arm0a6adt7cc.css +0 -1
- package/out/_next/static/chunks/0c9j3eq_14vv2.css +0 -1
- package/out/_next/static/chunks/0d4bueddmcnca.js +0 -1
- package/out/_next/static/chunks/0gtwvy1z9ksa7.css +0 -1
- package/out/_next/static/chunks/0j27tcmtt4ly7.js +0 -1
- package/out/_next/static/chunks/0j3v4mq67wtnh.js +0 -1
- package/out/_next/static/chunks/0lkmf5ry.s_7w.js +0 -1
- package/out/_next/static/chunks/0p486m03-zfoi.js +0 -1
- package/out/_next/static/chunks/0r1~k82nji8sf.js +0 -1
- package/out/_next/static/chunks/0v7qp4hv-_._r.js +0 -1
- package/out/_next/static/chunks/0wuwlgcn6gxqt.js +0 -1
- package/out/_next/static/chunks/0xl5_avhu._i8.js +0 -1
- package/out/_next/static/chunks/10kvl8vj_plm-.js +0 -1
- package/out/_next/static/chunks/16m27azcs4k6w.js +0 -1
- package/out/changelog/__next._full.txt +0 -25
- package/out/changelog/__next._head.txt +0 -5
- package/out/changelog/__next._tree.txt +0 -5
- package/out/changelog/__next.changelog.__PAGE__.txt +0 -10
- package/out/changelog/__next.changelog.txt +0 -5
- package/out/changelog/index.html +0 -15
- package/out/changelog/index.txt +0 -25
- package/out/docs/__next._full.txt +0 -25
- package/out/docs/__next._head.txt +0 -5
- package/out/docs/__next._index.txt +0 -9
- package/out/docs/__next._tree.txt +0 -5
- package/out/docs/__next.docs.__PAGE__.txt +0 -10
- package/out/docs/__next.docs.txt +0 -5
- package/out/docs/getting-started/__next._full.txt +0 -25
- package/out/docs/getting-started/__next._head.txt +0 -5
- package/out/docs/getting-started/__next._index.txt +0 -9
- package/out/docs/getting-started/__next._tree.txt +0 -5
- package/out/docs/getting-started/__next.docs.getting-started.__PAGE__.txt +0 -10
- package/out/docs/getting-started/__next.docs.getting-started.txt +0 -5
- package/out/docs/getting-started/__next.docs.txt +0 -5
- package/out/docs/getting-started/index.html +0 -15
- package/out/docs/getting-started/index.txt +0 -25
- package/out/docs/index.html +0 -15
- package/out/docs/index.txt +0 -25
- package/out/note/edit/__next._full.txt +0 -24
- package/out/note/edit/__next._head.txt +0 -5
- package/out/note/edit/__next._index.txt +0 -9
- package/out/note/edit/__next._tree.txt +0 -4
- package/out/note/edit/__next.note.edit.__PAGE__.txt +0 -9
- package/out/note/edit/__next.note.edit.txt +0 -5
- package/out/note/edit/__next.note.txt +0 -5
- package/out/note/edit/index.html +0 -15
- package/out/note/edit/index.txt +0 -24
- /package/out/_next/static/{sIuUKxnnGU7K9Tu9UDKE8 → t7ZIeQpVvjz4a7-5Tt-VK}/_buildManifest.js +0 -0
- /package/out/_next/static/{sIuUKxnnGU7K9Tu9UDKE8 → t7ZIeQpVvjz4a7-5Tt-VK}/_clientMiddlewareManifest.js +0 -0
- /package/out/_next/static/{sIuUKxnnGU7K9Tu9UDKE8 → t7ZIeQpVvjz4a7-5Tt-VK}/_ssgManifest.js +0 -0
package/server/src/index.js
CHANGED
|
@@ -14,11 +14,12 @@ import Corestore from 'corestore'
|
|
|
14
14
|
import Hyperdrive from 'hyperdrive'
|
|
15
15
|
import b4a from 'b4a'
|
|
16
16
|
import crypto from 'node:crypto'
|
|
17
|
-
import { CID } from 'multiformats/cid'
|
|
18
17
|
import fs from 'node:fs'
|
|
19
18
|
import path from 'node:path'
|
|
20
19
|
|
|
21
20
|
import { calculateCid, parseMostLink } from './core/cid.js'
|
|
21
|
+
import { normalizeChannelAttachment } from './core/channelAttachment.js'
|
|
22
|
+
import { getCidInfo } from './core/cidTopic.js'
|
|
22
23
|
import {
|
|
23
24
|
sanitizeFilename,
|
|
24
25
|
validateAndSanitizePath,
|
|
@@ -34,6 +35,7 @@ import {
|
|
|
34
35
|
IntegrityError,
|
|
35
36
|
PermissionError,
|
|
36
37
|
ConflictError,
|
|
38
|
+
StorageCapacityError,
|
|
37
39
|
EngineNotInitializedError,
|
|
38
40
|
} from './utils/errors.js'
|
|
39
41
|
import {
|
|
@@ -66,6 +68,30 @@ import {
|
|
|
66
68
|
|
|
67
69
|
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
|
|
68
70
|
|
|
71
|
+
function normalizeOwnerAddress(address) {
|
|
72
|
+
const value = String(address || '').trim()
|
|
73
|
+
return /^0x[a-fA-F0-9]{40}$/.test(value) ? value.toLowerCase() : ''
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function createOfflineSwarm() {
|
|
77
|
+
return {
|
|
78
|
+
connections: new Set(),
|
|
79
|
+
keyPair: {
|
|
80
|
+
publicKey: crypto.randomBytes(32),
|
|
81
|
+
},
|
|
82
|
+
on() {},
|
|
83
|
+
join() {
|
|
84
|
+
return {}
|
|
85
|
+
},
|
|
86
|
+
leave() {
|
|
87
|
+
return Promise.resolve()
|
|
88
|
+
},
|
|
89
|
+
destroy() {
|
|
90
|
+
return Promise.resolve()
|
|
91
|
+
},
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
69
95
|
export class MostBoxEngine extends EventEmitter {
|
|
70
96
|
#store = null
|
|
71
97
|
#swarm = null
|
|
@@ -78,11 +104,13 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
78
104
|
#activeDownloads = new Map()
|
|
79
105
|
#drivePromises = new Map()
|
|
80
106
|
#fileDiscoveries = new Map()
|
|
107
|
+
#fileMonitors = new Map()
|
|
81
108
|
#seedStates = new Map()
|
|
82
109
|
#holdingResumeTask = null
|
|
83
110
|
|
|
84
111
|
#channels = []
|
|
85
112
|
#channelCores = new Map()
|
|
113
|
+
#channelLocalCoreKey = new Map()
|
|
86
114
|
#channelDiscoveries = new Map()
|
|
87
115
|
#channelChatDiscoveries = new Map()
|
|
88
116
|
#channelPeers = new Map()
|
|
@@ -94,7 +122,9 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
94
122
|
* @param {object} options - 配置选项
|
|
95
123
|
* @param {string} options.dataPath - 存储 P2P 数据的路径(必填)
|
|
96
124
|
* @param {string} [options.downloadPath] - 默认下载路径(可选,默认为 dataPath/downloads)
|
|
97
|
-
* @param {number} [options.maxFileSize] - 最大文件大小(字节)(默认:
|
|
125
|
+
* @param {number} [options.maxFileSize] - 最大文件大小(字节)(默认:10GB)
|
|
126
|
+
* @param {number} [options.capacityBytes] - 节点存储容量上限(字节)(默认:100GB)
|
|
127
|
+
* @param {boolean} [options.disableNetwork] - 测试用:跳过真实 Hyperswarm 网络
|
|
98
128
|
*/
|
|
99
129
|
constructor(options) {
|
|
100
130
|
super()
|
|
@@ -108,7 +138,9 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
108
138
|
downloadPath:
|
|
109
139
|
options.downloadPath || path.join(options.dataPath, 'downloads'),
|
|
110
140
|
maxFileSize: options.maxFileSize || MAX_FILE_SIZE,
|
|
141
|
+
capacityBytes: options.capacityBytes || 100 * 1024 * 1024 * 1024,
|
|
111
142
|
downloadTimeout: options.downloadTimeout || DOWNLOAD_TIMEOUT,
|
|
143
|
+
disableNetwork: options.disableNetwork === true,
|
|
112
144
|
}
|
|
113
145
|
}
|
|
114
146
|
|
|
@@ -167,14 +199,19 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
167
199
|
}
|
|
168
200
|
|
|
169
201
|
console.log(`[MostBox] Initializing Hyperswarm...`)
|
|
170
|
-
this.#
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
202
|
+
if (this.#options.disableNetwork) {
|
|
203
|
+
this.#swarm = createOfflineSwarm()
|
|
204
|
+
this.#chatSwarm = createOfflineSwarm()
|
|
205
|
+
} else {
|
|
206
|
+
this.#swarm = new Hyperswarm({
|
|
207
|
+
maxPeers: MAX_PEERS,
|
|
208
|
+
bootstrap: SWARM_BOOTSTRAP,
|
|
209
|
+
firewall: () => false,
|
|
210
|
+
connectionKeepAlive: SWARM_KEEP_ALIVE_INTERVAL,
|
|
211
|
+
randomPunchInterval: SWARM_RANDOM_PUNCH_INTERVAL,
|
|
212
|
+
handshakeTimeout: CONNECTION_TIMEOUT,
|
|
213
|
+
})
|
|
214
|
+
}
|
|
178
215
|
|
|
179
216
|
this.#swarm.on('error', err => {
|
|
180
217
|
if (
|
|
@@ -200,14 +237,16 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
200
237
|
this.emit('connection', conn)
|
|
201
238
|
})
|
|
202
239
|
|
|
203
|
-
this.#
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
240
|
+
if (!this.#options.disableNetwork) {
|
|
241
|
+
this.#chatSwarm = new Hyperswarm({
|
|
242
|
+
maxPeers: MAX_PEERS,
|
|
243
|
+
bootstrap: SWARM_BOOTSTRAP,
|
|
244
|
+
firewall: () => false,
|
|
245
|
+
connectionKeepAlive: SWARM_KEEP_ALIVE_INTERVAL,
|
|
246
|
+
randomPunchInterval: SWARM_RANDOM_PUNCH_INTERVAL,
|
|
247
|
+
handshakeTimeout: CONNECTION_TIMEOUT,
|
|
248
|
+
})
|
|
249
|
+
}
|
|
211
250
|
|
|
212
251
|
this.#chatSwarm.on('error', err => {
|
|
213
252
|
if (
|
|
@@ -265,9 +304,22 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
265
304
|
valueEncoding: 'json',
|
|
266
305
|
})
|
|
267
306
|
await core.ready()
|
|
268
|
-
|
|
307
|
+
const coreKeyHex = b4a.toString(core.key, 'hex')
|
|
308
|
+
if (!this.#channelCores.has(channel.name)) {
|
|
309
|
+
this.#channelCores.set(channel.name, new Map())
|
|
310
|
+
}
|
|
311
|
+
this.#channelCores.get(channel.name).set(coreKeyHex, core)
|
|
312
|
+
this.#channelLocalCoreKey.set(channel.name, coreKeyHex)
|
|
269
313
|
this.#channelPeers.set(channel.name, new Map())
|
|
270
314
|
this.#setupChannelAppendListener(core, channel.name)
|
|
315
|
+
const remoteCoreKeys = Array.isArray(channel.remoteCoreKeys)
|
|
316
|
+
? channel.remoteCoreKeys
|
|
317
|
+
: []
|
|
318
|
+
for (const remoteCoreKey of remoteCoreKeys) {
|
|
319
|
+
if (remoteCoreKey && remoteCoreKey !== coreKeyHex) {
|
|
320
|
+
await this.#openRemoteChannelCore(channel.name, remoteCoreKey)
|
|
321
|
+
}
|
|
322
|
+
}
|
|
271
323
|
|
|
272
324
|
const discoveryKey = b4a.from(channel.discoveryKey, 'hex')
|
|
273
325
|
const chatDiscoveryKey = this.#generateChannelChatDiscoveryKey(
|
|
@@ -315,25 +367,16 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
315
367
|
}
|
|
316
368
|
this.#activeDownloads.clear()
|
|
317
369
|
|
|
370
|
+
await Promise.allSettled(
|
|
371
|
+
[...this.#fileMonitors.values()].map(item => this.#closeFileMonitor(item))
|
|
372
|
+
)
|
|
373
|
+
this.#fileMonitors.clear()
|
|
318
374
|
await Promise.allSettled([...this.#drives.values()].map(d => d.close()))
|
|
319
375
|
this.#drives.clear()
|
|
320
376
|
this.#fileDiscoveries.clear()
|
|
321
377
|
this.#seedStates.clear()
|
|
322
378
|
this.#holdingResumeTask = null
|
|
323
379
|
|
|
324
|
-
for (const core of this.#channelCores.values()) {
|
|
325
|
-
try {
|
|
326
|
-
await core.close()
|
|
327
|
-
} catch (err) {
|
|
328
|
-
console.warn('[MostBox] Failed to close channel core:', err.message)
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
this.#channelCores.clear()
|
|
332
|
-
this.#channelDiscoveries.clear()
|
|
333
|
-
this.#channelChatDiscoveries.clear()
|
|
334
|
-
this.#channelPeers.clear()
|
|
335
|
-
this.#channels = []
|
|
336
|
-
|
|
337
380
|
if (this.#swarm) {
|
|
338
381
|
await this.#swarm.destroy()
|
|
339
382
|
this.#swarm = null
|
|
@@ -344,6 +387,22 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
344
387
|
this.#chatSwarm = null
|
|
345
388
|
}
|
|
346
389
|
|
|
390
|
+
for (const [, coresMap] of this.#channelCores) {
|
|
391
|
+
for (const [, core] of coresMap) {
|
|
392
|
+
try {
|
|
393
|
+
await core.close()
|
|
394
|
+
} catch (err) {
|
|
395
|
+
console.warn('[MostBox] Failed to close channel core:', err.message)
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
this.#channelCores.clear()
|
|
400
|
+
this.#channelLocalCoreKey.clear()
|
|
401
|
+
this.#channelDiscoveries.clear()
|
|
402
|
+
this.#channelChatDiscoveries.clear()
|
|
403
|
+
this.#channelPeers.clear()
|
|
404
|
+
this.#channels = []
|
|
405
|
+
|
|
347
406
|
if (this.#store) {
|
|
348
407
|
await this.#store.close()
|
|
349
408
|
this.#store = null
|
|
@@ -390,6 +449,7 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
390
449
|
*/
|
|
391
450
|
async publishFile(content, fileName, options = {}) {
|
|
392
451
|
this.#ensureInitialized()
|
|
452
|
+
const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
|
|
393
453
|
|
|
394
454
|
let cleanPath = null
|
|
395
455
|
let safeFileName
|
|
@@ -429,6 +489,8 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
429
489
|
)
|
|
430
490
|
}
|
|
431
491
|
|
|
492
|
+
this.#checkCapacity(fileSize)
|
|
493
|
+
|
|
432
494
|
this.emit('publish:progress', {
|
|
433
495
|
stage: 'calculating-cid',
|
|
434
496
|
file: safeFileName,
|
|
@@ -442,7 +504,7 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
442
504
|
|
|
443
505
|
// 检查相同内容是否已存在
|
|
444
506
|
const existingIndex = this.#publishedFiles.findIndex(
|
|
445
|
-
f => f.cid === cidString
|
|
507
|
+
f => f.cid === cidString && this.#recordMatchesOwner(f, ownerAddress)
|
|
446
508
|
)
|
|
447
509
|
if (existingIndex !== -1) {
|
|
448
510
|
const existing = this.#publishedFiles[existingIndex]
|
|
@@ -457,7 +519,6 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
457
519
|
localPath: holdingLocalPath,
|
|
458
520
|
driveName: name,
|
|
459
521
|
source: 'published',
|
|
460
|
-
temporary: false,
|
|
461
522
|
})
|
|
462
523
|
return {
|
|
463
524
|
cid: cidString,
|
|
@@ -475,10 +536,6 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
475
536
|
server: true,
|
|
476
537
|
client: false,
|
|
477
538
|
})
|
|
478
|
-
this.#swarm.join(drive.discoveryKey, {
|
|
479
|
-
server: true,
|
|
480
|
-
client: false,
|
|
481
|
-
})
|
|
482
539
|
}
|
|
483
540
|
await this.#joinCidTopicInternal(cidString, {
|
|
484
541
|
server: true,
|
|
@@ -532,6 +589,7 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
532
589
|
driveName: name,
|
|
533
590
|
publishedAt: new Date().toISOString(),
|
|
534
591
|
starred: false,
|
|
592
|
+
ownerAddress,
|
|
535
593
|
})
|
|
536
594
|
this.#savePublishedMetadata()
|
|
537
595
|
this.#upsertHolding({
|
|
@@ -541,7 +599,6 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
541
599
|
localPath: holdingLocalPath,
|
|
542
600
|
driveName: name,
|
|
543
601
|
source: 'published',
|
|
544
|
-
temporary: false,
|
|
545
602
|
})
|
|
546
603
|
|
|
547
604
|
const result = {
|
|
@@ -565,6 +622,7 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
565
622
|
*/
|
|
566
623
|
async downloadFile(link, taskId = null, options = {}) {
|
|
567
624
|
this.#ensureInitialized()
|
|
625
|
+
const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
|
|
568
626
|
|
|
569
627
|
taskId =
|
|
570
628
|
taskId || `dl_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
|
|
@@ -585,10 +643,11 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
585
643
|
}
|
|
586
644
|
const cidString = parsed.cid
|
|
587
645
|
console.log(`[MostBox] Parsed CID: ${cidString}`)
|
|
588
|
-
const parsedCid = CID.parse(cidString)
|
|
589
646
|
const { driveName: name } = this.#getCidInfo(cidString)
|
|
590
647
|
|
|
591
|
-
const existingFile = this.#publishedFiles.find(
|
|
648
|
+
const existingFile = this.#publishedFiles.find(
|
|
649
|
+
f => f.cid === cidString && this.#recordMatchesOwner(f, ownerAddress)
|
|
650
|
+
)
|
|
592
651
|
if (existingFile) {
|
|
593
652
|
console.log(`[MostBox] File already exists: ${existingFile.fileName}`)
|
|
594
653
|
const existingHolding = this.#holdings.find(
|
|
@@ -609,7 +668,6 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
609
668
|
existingHolding?.localPath || existingFile.localPath || null,
|
|
610
669
|
driveName: existingFile.driveName || name,
|
|
611
670
|
source: existingHolding?.source || 'published',
|
|
612
|
-
temporary: existingHolding?.temporary === true,
|
|
613
671
|
})
|
|
614
672
|
return {
|
|
615
673
|
taskId,
|
|
@@ -632,18 +690,11 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
632
690
|
})
|
|
633
691
|
|
|
634
692
|
this.emit('download:status', { taskId, status: 'connecting' })
|
|
635
|
-
|
|
636
|
-
console.log(`[MostBox] Joining swarm for drive discovery...`)
|
|
637
|
-
this.#swarm.join(drive.discoveryKey, {
|
|
638
|
-
server: true,
|
|
639
|
-
client: true,
|
|
640
|
-
})
|
|
641
|
-
console.log(`[MostBox] Swarm join requested`)
|
|
642
693
|
} else {
|
|
643
694
|
console.log(`[MostBox] Using existing drive: ${name}`)
|
|
644
695
|
}
|
|
645
696
|
await this.#joinCidTopicInternal(cidString, {
|
|
646
|
-
server:
|
|
697
|
+
server: false,
|
|
647
698
|
client: true,
|
|
648
699
|
})
|
|
649
700
|
|
|
@@ -717,6 +768,10 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
717
768
|
// 忽略
|
|
718
769
|
}
|
|
719
770
|
|
|
771
|
+
if (totalBytes > 0) {
|
|
772
|
+
this.#checkCapacity(totalBytes)
|
|
773
|
+
}
|
|
774
|
+
|
|
720
775
|
const savePath = path.join(targetDir, sanitizedFileName)
|
|
721
776
|
fs.mkdirSync(path.dirname(savePath), { recursive: true })
|
|
722
777
|
if (fs.existsSync(savePath)) {
|
|
@@ -826,13 +881,12 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
826
881
|
this.emit('download:status', { taskId, status: 'verifying' })
|
|
827
882
|
|
|
828
883
|
const { cid: downloadedCid } = await calculateCid(savePath)
|
|
829
|
-
const
|
|
830
|
-
const actualHash = b4a.toString(downloadedCid.multihash.digest, 'hex')
|
|
884
|
+
const downloadedCidString = downloadedCid.toString()
|
|
831
885
|
|
|
832
|
-
if (
|
|
886
|
+
if (downloadedCidString !== cidString) {
|
|
833
887
|
fs.unlinkSync(savePath)
|
|
834
888
|
throw new IntegrityError(
|
|
835
|
-
`File content CID mismatch.
|
|
889
|
+
`File content CID mismatch. Expected ${cidString}, got ${downloadedCidString}.`
|
|
836
890
|
)
|
|
837
891
|
}
|
|
838
892
|
|
|
@@ -848,7 +902,17 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
848
902
|
writeStream.on('error', reject)
|
|
849
903
|
readStream.on('error', reject)
|
|
850
904
|
})
|
|
905
|
+
const verifyEntry = await drive.entry(driveKey)
|
|
906
|
+
if (!verifyEntry || !verifyEntry.value || !verifyEntry.value.blob) {
|
|
907
|
+
throw new IntegrityError(
|
|
908
|
+
`Failed to write file to Hyperdrive for seeding: ${driveKey}`
|
|
909
|
+
)
|
|
910
|
+
}
|
|
851
911
|
}
|
|
912
|
+
await this.#joinCidTopicInternal(cidString, {
|
|
913
|
+
server: true,
|
|
914
|
+
client: false,
|
|
915
|
+
})
|
|
852
916
|
|
|
853
917
|
const result = {
|
|
854
918
|
taskId,
|
|
@@ -858,7 +922,7 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
858
922
|
|
|
859
923
|
// 将下载的文件添加到已发布文件列表(displayName 用原始文件名)
|
|
860
924
|
const existingIndex = this.#publishedFiles.findIndex(
|
|
861
|
-
f => f.cid === cidString
|
|
925
|
+
f => f.cid === cidString && this.#recordMatchesOwner(f, ownerAddress)
|
|
862
926
|
)
|
|
863
927
|
if (existingIndex !== -1) {
|
|
864
928
|
const existing = this.#publishedFiles[existingIndex]
|
|
@@ -873,6 +937,7 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
873
937
|
driveName: name,
|
|
874
938
|
publishedAt: new Date().toISOString(),
|
|
875
939
|
starred: false,
|
|
940
|
+
ownerAddress,
|
|
876
941
|
})
|
|
877
942
|
}
|
|
878
943
|
this.#savePublishedMetadata()
|
|
@@ -884,7 +949,6 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
884
949
|
localPath: savePath,
|
|
885
950
|
driveName: name,
|
|
886
951
|
source: 'downloaded',
|
|
887
|
-
temporary: true,
|
|
888
952
|
})
|
|
889
953
|
|
|
890
954
|
this.emit('download:success', result)
|
|
@@ -904,6 +968,7 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
904
968
|
*/
|
|
905
969
|
async checkDownloadAvailability(link, options = {}) {
|
|
906
970
|
this.#ensureInitialized()
|
|
971
|
+
const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
|
|
907
972
|
|
|
908
973
|
const timeout = options.timeout || DRIVE_ENTRY_TIMEOUT
|
|
909
974
|
const parsed = parseMostLink(link)
|
|
@@ -913,7 +978,9 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
913
978
|
|
|
914
979
|
const cidString = parsed.cid
|
|
915
980
|
const { driveName: name } = this.#getCidInfo(cidString)
|
|
916
|
-
const existingFile = this.#publishedFiles.find(
|
|
981
|
+
const existingFile = this.#publishedFiles.find(
|
|
982
|
+
f => f.cid === cidString && this.#recordMatchesOwner(f, ownerAddress)
|
|
983
|
+
)
|
|
917
984
|
if (existingFile) {
|
|
918
985
|
return {
|
|
919
986
|
available: true,
|
|
@@ -938,15 +1005,10 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
938
1005
|
server: true,
|
|
939
1006
|
client: true,
|
|
940
1007
|
})
|
|
941
|
-
|
|
942
|
-
this.#swarm.join(drive.discoveryKey, {
|
|
943
|
-
server: true,
|
|
944
|
-
client: true,
|
|
945
|
-
})
|
|
946
1008
|
}
|
|
947
1009
|
|
|
948
1010
|
await this.#joinCidTopicInternal(cidString, {
|
|
949
|
-
server:
|
|
1011
|
+
server: false,
|
|
950
1012
|
client: true,
|
|
951
1013
|
})
|
|
952
1014
|
|
|
@@ -984,6 +1046,11 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
984
1046
|
listPublishedFiles(options = {}) {
|
|
985
1047
|
this.#ensureInitialized()
|
|
986
1048
|
let files = this.#publishedFiles
|
|
1049
|
+
const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
|
|
1050
|
+
|
|
1051
|
+
if (ownerAddress) {
|
|
1052
|
+
files = files.filter(f => this.#recordMatchesOwner(f, ownerAddress))
|
|
1053
|
+
}
|
|
987
1054
|
|
|
988
1055
|
if (options.starred === true) {
|
|
989
1056
|
files = files.filter(f => f.starred === true)
|
|
@@ -995,6 +1062,7 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
995
1062
|
link: `most://${f.cid}?filename=${encodeURIComponent(f.fileName)}`,
|
|
996
1063
|
publishedAt: f.publishedAt,
|
|
997
1064
|
starred: f.starred || false,
|
|
1065
|
+
ownerAddress: f.ownerAddress || '',
|
|
998
1066
|
}))
|
|
999
1067
|
}
|
|
1000
1068
|
|
|
@@ -1003,9 +1071,12 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1003
1071
|
* @param {string} cid - 文件的 CID
|
|
1004
1072
|
* @returns {object} 更新后的文件信息
|
|
1005
1073
|
*/
|
|
1006
|
-
toggleStarred(cid) {
|
|
1074
|
+
toggleStarred(cid, options = {}) {
|
|
1007
1075
|
this.#ensureInitialized()
|
|
1008
|
-
const
|
|
1076
|
+
const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
|
|
1077
|
+
const index = this.#publishedFiles.findIndex(
|
|
1078
|
+
f => f.cid === cid && this.#recordMatchesOwner(f, ownerAddress)
|
|
1079
|
+
)
|
|
1009
1080
|
if (index === -1) {
|
|
1010
1081
|
throw new Error('File not found')
|
|
1011
1082
|
}
|
|
@@ -1022,9 +1093,12 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1022
1093
|
* @param {string} cid - 要删除文件的 CID
|
|
1023
1094
|
* @returns {Promise<Array>} 更新后的已发布文件列表
|
|
1024
1095
|
*/
|
|
1025
|
-
async deletePublishedFile(cid) {
|
|
1096
|
+
async deletePublishedFile(cid, options = {}) {
|
|
1026
1097
|
this.#ensureInitialized()
|
|
1027
|
-
const
|
|
1098
|
+
const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
|
|
1099
|
+
const index = this.#publishedFiles.findIndex(
|
|
1100
|
+
f => f.cid === cid && this.#recordMatchesOwner(f, ownerAddress)
|
|
1101
|
+
)
|
|
1028
1102
|
if (index !== -1) {
|
|
1029
1103
|
const fileRecord = this.#publishedFiles[index]
|
|
1030
1104
|
const holding = this.#holdings.find(item => item.cid === fileRecord.cid)
|
|
@@ -1039,34 +1113,42 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1039
1113
|
source: holding?.source || 'published',
|
|
1040
1114
|
publishedAt: fileRecord.publishedAt,
|
|
1041
1115
|
starred: fileRecord.starred || false,
|
|
1116
|
+
ownerAddress: fileRecord.ownerAddress || ownerAddress,
|
|
1042
1117
|
deletedAt: new Date().toISOString(),
|
|
1043
1118
|
})
|
|
1044
1119
|
this.#saveTrashMetadata()
|
|
1045
1120
|
|
|
1046
|
-
await this.#leaveCidTopic(fileRecord.cid)
|
|
1047
|
-
await this.#closeDriveForSeed(
|
|
1048
|
-
fileRecord.driveName || this.#getCidInfo(fileRecord.cid).driveName
|
|
1049
|
-
)
|
|
1050
|
-
this.#removeHolding(fileRecord.cid)
|
|
1051
|
-
|
|
1052
1121
|
this.#publishedFiles.splice(index, 1)
|
|
1053
1122
|
this.#savePublishedMetadata()
|
|
1123
|
+
|
|
1124
|
+
if (!this.#hasPublishedReference(fileRecord.cid)) {
|
|
1125
|
+
await this.#leaveCidTopic(fileRecord.cid)
|
|
1126
|
+
await this.#closeDriveForSeed(
|
|
1127
|
+
fileRecord.driveName || this.#getCidInfo(fileRecord.cid).driveName
|
|
1128
|
+
)
|
|
1129
|
+
this.#removeHolding(fileRecord.cid)
|
|
1130
|
+
}
|
|
1054
1131
|
}
|
|
1055
|
-
return this.listPublishedFiles()
|
|
1132
|
+
return this.listPublishedFiles({ ownerAddress })
|
|
1056
1133
|
}
|
|
1057
1134
|
|
|
1058
1135
|
/**
|
|
1059
1136
|
* 列出回收站中的所有文件
|
|
1060
1137
|
* @returns {Array} 回收站文件
|
|
1061
1138
|
*/
|
|
1062
|
-
listTrashFiles() {
|
|
1139
|
+
listTrashFiles(options = {}) {
|
|
1063
1140
|
this.#ensureInitialized()
|
|
1064
|
-
|
|
1141
|
+
const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
|
|
1142
|
+
const files = ownerAddress
|
|
1143
|
+
? this.#trashFiles.filter(f => this.#recordMatchesOwner(f, ownerAddress))
|
|
1144
|
+
: this.#trashFiles
|
|
1145
|
+
return files.map(f => ({
|
|
1065
1146
|
fileName: f.fileName,
|
|
1066
1147
|
cid: f.cid,
|
|
1067
1148
|
link: `most://${f.cid}?filename=${encodeURIComponent(f.fileName)}`,
|
|
1068
1149
|
publishedAt: f.publishedAt,
|
|
1069
1150
|
starred: f.starred || false,
|
|
1151
|
+
ownerAddress: f.ownerAddress || '',
|
|
1070
1152
|
deletedAt: f.deletedAt,
|
|
1071
1153
|
}))
|
|
1072
1154
|
}
|
|
@@ -1076,18 +1158,19 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1076
1158
|
* @param {string} cid - 要恢复文件的 CID
|
|
1077
1159
|
* @returns {Promise<Array>} 更新后的已发布文件列表
|
|
1078
1160
|
*/
|
|
1079
|
-
async restoreTrashFile(cid) {
|
|
1161
|
+
async restoreTrashFile(cid, options = {}) {
|
|
1080
1162
|
this.#ensureInitialized()
|
|
1081
|
-
const
|
|
1163
|
+
const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
|
|
1164
|
+
const index = this.#trashFiles.findIndex(
|
|
1165
|
+
f => f.cid === cid && this.#recordMatchesOwner(f, ownerAddress)
|
|
1166
|
+
)
|
|
1082
1167
|
if (index === -1) {
|
|
1083
1168
|
throw new Error('File not found in trash')
|
|
1084
1169
|
}
|
|
1085
1170
|
|
|
1086
1171
|
const fileRecord = this.#trashFiles[index]
|
|
1087
1172
|
|
|
1088
|
-
const
|
|
1089
|
-
const hashHex = b4a.toString(parsedCid.multihash.digest, 'hex')
|
|
1090
|
-
const driveName = `drive-${hashHex}`
|
|
1173
|
+
const { driveName } = this.#getCidInfo(fileRecord.cid)
|
|
1091
1174
|
|
|
1092
1175
|
this.#publishedFiles.push({
|
|
1093
1176
|
fileName: fileRecord.fileName,
|
|
@@ -1095,6 +1178,7 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1095
1178
|
driveName,
|
|
1096
1179
|
publishedAt: fileRecord.publishedAt,
|
|
1097
1180
|
starred: fileRecord.starred || false,
|
|
1181
|
+
ownerAddress: fileRecord.ownerAddress || ownerAddress,
|
|
1098
1182
|
})
|
|
1099
1183
|
this.#savePublishedMetadata()
|
|
1100
1184
|
|
|
@@ -1112,10 +1196,9 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1112
1196
|
localPath: fileRecord.localPath || null,
|
|
1113
1197
|
driveName,
|
|
1114
1198
|
source: fileRecord.source || 'published',
|
|
1115
|
-
temporary: fileRecord.source === 'downloaded',
|
|
1116
1199
|
})
|
|
1117
1200
|
|
|
1118
|
-
return this.listPublishedFiles()
|
|
1201
|
+
return this.listPublishedFiles({ ownerAddress })
|
|
1119
1202
|
}
|
|
1120
1203
|
|
|
1121
1204
|
/**
|
|
@@ -1123,41 +1206,60 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1123
1206
|
* @param {string} cid - 要永久删除文件的 CID
|
|
1124
1207
|
* @returns {Promise<Array>} 更新后的回收站列表
|
|
1125
1208
|
*/
|
|
1126
|
-
async permanentDeleteTrashFile(cid) {
|
|
1209
|
+
async permanentDeleteTrashFile(cid, options = {}) {
|
|
1127
1210
|
this.#ensureInitialized()
|
|
1128
|
-
const
|
|
1211
|
+
const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
|
|
1212
|
+
const index = this.#trashFiles.findIndex(
|
|
1213
|
+
f => f.cid === cid && this.#recordMatchesOwner(f, ownerAddress)
|
|
1214
|
+
)
|
|
1129
1215
|
if (index !== -1) {
|
|
1130
1216
|
const fileRecord = this.#trashFiles[index]
|
|
1131
1217
|
const driveName =
|
|
1132
1218
|
fileRecord.driveName || this.#getCidInfo(fileRecord.cid).driveName
|
|
1133
1219
|
|
|
1134
|
-
try {
|
|
1135
|
-
const drive = await this.#getOrCreateDrive(driveName)
|
|
1136
|
-
await drive.del('/' + fileRecord.cid)
|
|
1137
|
-
} catch {
|
|
1138
|
-
// 文件可能不存在于驱动器中
|
|
1139
|
-
}
|
|
1140
|
-
await this.#closeDriveForSeed(driveName)
|
|
1141
|
-
|
|
1142
|
-
await this.#leaveCidTopic(fileRecord.cid)
|
|
1143
|
-
this.#removeHolding(fileRecord.cid)
|
|
1144
1220
|
this.#trashFiles.splice(index, 1)
|
|
1145
1221
|
this.#saveTrashMetadata()
|
|
1222
|
+
|
|
1223
|
+
if (!this.#hasAnyUserReference(fileRecord.cid)) {
|
|
1224
|
+
try {
|
|
1225
|
+
const drive = await this.#getOrCreateDrive(driveName)
|
|
1226
|
+
await drive.del('/' + fileRecord.cid)
|
|
1227
|
+
} catch {
|
|
1228
|
+
// 文件可能不存在于驱动器中
|
|
1229
|
+
}
|
|
1230
|
+
await this.#closeDriveForSeed(driveName)
|
|
1231
|
+
await this.#leaveCidTopic(fileRecord.cid)
|
|
1232
|
+
this.#removeHolding(fileRecord.cid)
|
|
1233
|
+
}
|
|
1146
1234
|
}
|
|
1147
|
-
return this.listTrashFiles()
|
|
1235
|
+
return this.listTrashFiles({ ownerAddress })
|
|
1148
1236
|
}
|
|
1149
1237
|
|
|
1150
1238
|
/**
|
|
1151
1239
|
* 清空回收站 — 永久删除所有回收站文件
|
|
1152
1240
|
* @returns {Promise<Array>} 清空后的回收站列表
|
|
1153
1241
|
*/
|
|
1154
|
-
async emptyTrash() {
|
|
1242
|
+
async emptyTrash(options = {}) {
|
|
1155
1243
|
this.#ensureInitialized()
|
|
1244
|
+
const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
|
|
1245
|
+
const remainingTrash = []
|
|
1246
|
+
const removedTrash = []
|
|
1156
1247
|
|
|
1157
1248
|
for (const fileRecord of this.#trashFiles) {
|
|
1249
|
+
if (ownerAddress && !this.#recordMatchesOwner(fileRecord, ownerAddress)) {
|
|
1250
|
+
remainingTrash.push(fileRecord)
|
|
1251
|
+
continue
|
|
1252
|
+
}
|
|
1253
|
+
removedTrash.push(fileRecord)
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
this.#trashFiles = remainingTrash
|
|
1257
|
+
this.#saveTrashMetadata()
|
|
1258
|
+
|
|
1259
|
+
for (const fileRecord of removedTrash) {
|
|
1260
|
+
if (this.#hasAnyUserReference(fileRecord.cid)) continue
|
|
1158
1261
|
const driveName =
|
|
1159
1262
|
fileRecord.driveName || this.#getCidInfo(fileRecord.cid).driveName
|
|
1160
|
-
|
|
1161
1263
|
try {
|
|
1162
1264
|
const drive = await this.#getOrCreateDrive(driveName)
|
|
1163
1265
|
await drive.del('/' + fileRecord.cid)
|
|
@@ -1169,18 +1271,16 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1169
1271
|
this.#removeHolding(fileRecord.cid)
|
|
1170
1272
|
}
|
|
1171
1273
|
|
|
1172
|
-
this
|
|
1173
|
-
this.#saveTrashMetadata()
|
|
1174
|
-
|
|
1175
|
-
return []
|
|
1274
|
+
return this.listTrashFiles({ ownerAddress })
|
|
1176
1275
|
}
|
|
1177
1276
|
|
|
1178
1277
|
/**
|
|
1179
1278
|
* 获取存储统计信息
|
|
1180
1279
|
* @returns {Promise<{ total: number, used: number, free: number, fileCount: number, trashCount: number }>}
|
|
1181
1280
|
*/
|
|
1182
|
-
async getStorageStats() {
|
|
1281
|
+
async getStorageStats(options = {}) {
|
|
1183
1282
|
this.#ensureInitialized()
|
|
1283
|
+
const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
|
|
1184
1284
|
|
|
1185
1285
|
let totalSize = 0
|
|
1186
1286
|
let freeSize = 0
|
|
@@ -1231,8 +1331,16 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1231
1331
|
total: totalSize,
|
|
1232
1332
|
used: usedSize,
|
|
1233
1333
|
free: freeSize,
|
|
1234
|
-
fileCount:
|
|
1235
|
-
|
|
1334
|
+
fileCount: ownerAddress
|
|
1335
|
+
? this.#publishedFiles.filter(f =>
|
|
1336
|
+
this.#recordMatchesOwner(f, ownerAddress)
|
|
1337
|
+
).length
|
|
1338
|
+
: this.#publishedFiles.length,
|
|
1339
|
+
trashCount: ownerAddress
|
|
1340
|
+
? this.#trashFiles.filter(f =>
|
|
1341
|
+
this.#recordMatchesOwner(f, ownerAddress)
|
|
1342
|
+
).length
|
|
1343
|
+
: this.#trashFiles.length,
|
|
1236
1344
|
}
|
|
1237
1345
|
}
|
|
1238
1346
|
|
|
@@ -1243,9 +1351,12 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1243
1351
|
* @param {string} newFileName - 新文件路径
|
|
1244
1352
|
* @returns {object} 更新后的文件信息
|
|
1245
1353
|
*/
|
|
1246
|
-
moveFile(cid, newFileName) {
|
|
1354
|
+
moveFile(cid, newFileName, options = {}) {
|
|
1247
1355
|
this.#ensureInitialized()
|
|
1248
|
-
const
|
|
1356
|
+
const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
|
|
1357
|
+
const index = this.#publishedFiles.findIndex(
|
|
1358
|
+
f => f.cid === cid && this.#recordMatchesOwner(f, ownerAddress)
|
|
1359
|
+
)
|
|
1249
1360
|
if (index === -1) {
|
|
1250
1361
|
throw new Error('File not found')
|
|
1251
1362
|
}
|
|
@@ -1267,13 +1378,17 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1267
1378
|
* @param {string} newPath - 新文件夹路径
|
|
1268
1379
|
* @returns {object} 更新后的文件信息
|
|
1269
1380
|
*/
|
|
1270
|
-
renameFolder(oldPath, newPath) {
|
|
1381
|
+
renameFolder(oldPath, newPath, options = {}) {
|
|
1271
1382
|
this.#ensureInitialized()
|
|
1383
|
+
const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
|
|
1272
1384
|
const prefix = oldPath + '/'
|
|
1273
1385
|
const updatedFiles = []
|
|
1274
1386
|
|
|
1275
1387
|
for (const file of this.#publishedFiles) {
|
|
1276
|
-
if (
|
|
1388
|
+
if (
|
|
1389
|
+
file.fileName.startsWith(prefix) &&
|
|
1390
|
+
this.#recordMatchesOwner(file, ownerAddress)
|
|
1391
|
+
) {
|
|
1277
1392
|
const remainder = file.fileName.substring(prefix.length)
|
|
1278
1393
|
const newFileName = sanitizeFilename(
|
|
1279
1394
|
remainder ? newPath + '/' + remainder : newPath
|
|
@@ -1324,8 +1439,114 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1324
1439
|
this.#options.maxFileSize = Math.floor(parsed)
|
|
1325
1440
|
}
|
|
1326
1441
|
|
|
1327
|
-
getPublishedFiles() {
|
|
1328
|
-
|
|
1442
|
+
getPublishedFiles(options = {}) {
|
|
1443
|
+
const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
|
|
1444
|
+
return ownerAddress
|
|
1445
|
+
? this.#publishedFiles.filter(f =>
|
|
1446
|
+
this.#recordMatchesOwner(f, ownerAddress)
|
|
1447
|
+
)
|
|
1448
|
+
: this.#publishedFiles
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
listUsers() {
|
|
1452
|
+
this.#ensureInitialized()
|
|
1453
|
+
const users = new Map()
|
|
1454
|
+
const ensure = address => {
|
|
1455
|
+
const ownerAddress = normalizeOwnerAddress(address)
|
|
1456
|
+
if (!ownerAddress) return null
|
|
1457
|
+
if (!users.has(ownerAddress)) {
|
|
1458
|
+
users.set(ownerAddress, {
|
|
1459
|
+
address: ownerAddress,
|
|
1460
|
+
fileCount: 0,
|
|
1461
|
+
trashCount: 0,
|
|
1462
|
+
cidCount: 0,
|
|
1463
|
+
cids: new Set(),
|
|
1464
|
+
})
|
|
1465
|
+
}
|
|
1466
|
+
return users.get(ownerAddress)
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
for (const file of this.#publishedFiles) {
|
|
1470
|
+
const entry = ensure(file.ownerAddress)
|
|
1471
|
+
if (!entry) continue
|
|
1472
|
+
entry.fileCount += 1
|
|
1473
|
+
entry.cids.add(file.cid)
|
|
1474
|
+
}
|
|
1475
|
+
for (const file of this.#trashFiles) {
|
|
1476
|
+
const entry = ensure(file.ownerAddress)
|
|
1477
|
+
if (!entry) continue
|
|
1478
|
+
entry.trashCount += 1
|
|
1479
|
+
entry.cids.add(file.cid)
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
return [...users.values()].map(user => ({
|
|
1483
|
+
address: user.address,
|
|
1484
|
+
fileCount: user.fileCount,
|
|
1485
|
+
trashCount: user.trashCount,
|
|
1486
|
+
cidCount: user.cids.size,
|
|
1487
|
+
}))
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
async clearUserData(ownerAddressInput) {
|
|
1491
|
+
this.#ensureInitialized()
|
|
1492
|
+
const ownerAddress = normalizeOwnerAddress(ownerAddressInput)
|
|
1493
|
+
if (!ownerAddress) {
|
|
1494
|
+
throw new ValidationError('valid owner address is required')
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
const affectedCids = new Set()
|
|
1498
|
+
const beforeFiles = this.#publishedFiles.length
|
|
1499
|
+
const beforeTrash = this.#trashFiles.length
|
|
1500
|
+
|
|
1501
|
+
this.#publishedFiles = this.#publishedFiles.filter(file => {
|
|
1502
|
+
if (this.#recordMatchesOwner(file, ownerAddress)) {
|
|
1503
|
+
affectedCids.add(file.cid)
|
|
1504
|
+
return false
|
|
1505
|
+
}
|
|
1506
|
+
return true
|
|
1507
|
+
})
|
|
1508
|
+
this.#trashFiles = this.#trashFiles.filter(file => {
|
|
1509
|
+
if (this.#recordMatchesOwner(file, ownerAddress)) {
|
|
1510
|
+
affectedCids.add(file.cid)
|
|
1511
|
+
return false
|
|
1512
|
+
}
|
|
1513
|
+
return true
|
|
1514
|
+
})
|
|
1515
|
+
this.#channels = this.#channels
|
|
1516
|
+
.map(channel => ({
|
|
1517
|
+
...channel,
|
|
1518
|
+
members: Array.isArray(channel.members)
|
|
1519
|
+
? channel.members.filter(
|
|
1520
|
+
member => normalizeOwnerAddress(member) !== ownerAddress
|
|
1521
|
+
)
|
|
1522
|
+
: [],
|
|
1523
|
+
}))
|
|
1524
|
+
.filter(channel => channel.members.length > 0)
|
|
1525
|
+
|
|
1526
|
+
this.#savePublishedMetadata()
|
|
1527
|
+
this.#saveTrashMetadata()
|
|
1528
|
+
this.#saveChannelsMetadata()
|
|
1529
|
+
|
|
1530
|
+
let removedReplicas = 0
|
|
1531
|
+
for (const cid of affectedCids) {
|
|
1532
|
+
if (this.#hasAnyUserReference(cid)) continue
|
|
1533
|
+
const driveName = this.#getCidInfo(cid).driveName
|
|
1534
|
+
try {
|
|
1535
|
+
const drive = await this.#getOrCreateDrive(driveName)
|
|
1536
|
+
await drive.del('/' + cid)
|
|
1537
|
+
} catch {}
|
|
1538
|
+
await this.#closeDriveForSeed(driveName)
|
|
1539
|
+
await this.#leaveCidTopic(cid)
|
|
1540
|
+
this.#removeHolding(cid)
|
|
1541
|
+
removedReplicas += 1
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
return {
|
|
1545
|
+
ownerAddress,
|
|
1546
|
+
removedFiles: beforeFiles - this.#publishedFiles.length,
|
|
1547
|
+
removedTrashFiles: beforeTrash - this.#trashFiles.length,
|
|
1548
|
+
removedReplicas,
|
|
1549
|
+
}
|
|
1329
1550
|
}
|
|
1330
1551
|
|
|
1331
1552
|
/**
|
|
@@ -1345,6 +1566,7 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1345
1566
|
seedStatus: status,
|
|
1346
1567
|
seedError: seedState?.error,
|
|
1347
1568
|
seedStatusUpdatedAt: seedState?.updatedAt,
|
|
1569
|
+
...this.#getFileRuntimeStats(holding.cid),
|
|
1348
1570
|
link: `most://${holding.cid}?filename=${encodeURIComponent(holding.fileName || holding.cid)}`,
|
|
1349
1571
|
}
|
|
1350
1572
|
})
|
|
@@ -1383,6 +1605,7 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1383
1605
|
}
|
|
1384
1606
|
const result = await this.downloadFile(input.link, input.taskId || null, {
|
|
1385
1607
|
timeout: input.timeout,
|
|
1608
|
+
ownerAddress: input.ownerAddress,
|
|
1386
1609
|
})
|
|
1387
1610
|
return {
|
|
1388
1611
|
...result,
|
|
@@ -1400,6 +1623,7 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1400
1623
|
const link = `most://${cid}?filename=${encodeURIComponent(fileName)}`
|
|
1401
1624
|
const result = await this.downloadFile(link, input.taskId || null, {
|
|
1402
1625
|
timeout: input.timeout,
|
|
1626
|
+
ownerAddress: input.ownerAddress,
|
|
1403
1627
|
})
|
|
1404
1628
|
|
|
1405
1629
|
return {
|
|
@@ -1447,10 +1671,20 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1447
1671
|
* @param {number} [offset=0] - 读取起始位置
|
|
1448
1672
|
* @param {number} [limit=10000] - 最大读取字节数
|
|
1449
1673
|
*/
|
|
1450
|
-
async readFileContent(
|
|
1674
|
+
async readFileContent(
|
|
1675
|
+
cid,
|
|
1676
|
+
offset = 0,
|
|
1677
|
+
limit = DEFAULT_READ_LIMIT,
|
|
1678
|
+
options = {}
|
|
1679
|
+
) {
|
|
1451
1680
|
this.#ensureInitialized()
|
|
1681
|
+
const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
|
|
1452
1682
|
|
|
1453
|
-
const fileRecord = this.#publishedFiles.find(
|
|
1683
|
+
const fileRecord = this.#publishedFiles.find(
|
|
1684
|
+
f =>
|
|
1685
|
+
f.cid === cid &&
|
|
1686
|
+
(options.public || this.#recordMatchesOwner(f, ownerAddress))
|
|
1687
|
+
)
|
|
1454
1688
|
if (!fileRecord) {
|
|
1455
1689
|
throw new Error('File not found')
|
|
1456
1690
|
}
|
|
@@ -1507,8 +1741,13 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1507
1741
|
*/
|
|
1508
1742
|
async readFileRaw(cid, options = {}) {
|
|
1509
1743
|
this.#ensureInitialized()
|
|
1744
|
+
const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
|
|
1510
1745
|
|
|
1511
|
-
const fileRecord = this.#publishedFiles.find(
|
|
1746
|
+
const fileRecord = this.#publishedFiles.find(
|
|
1747
|
+
f =>
|
|
1748
|
+
f.cid === cid &&
|
|
1749
|
+
(options.public || this.#recordMatchesOwner(f, ownerAddress))
|
|
1750
|
+
)
|
|
1512
1751
|
if (!fileRecord) {
|
|
1513
1752
|
throw new Error('File not found')
|
|
1514
1753
|
}
|
|
@@ -1592,8 +1831,9 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1592
1831
|
* @param {string} [type='personal'] - 频道类型
|
|
1593
1832
|
* @returns {Promise<{ name: string, key: string }>}
|
|
1594
1833
|
*/
|
|
1595
|
-
async createChannel(name, type = 'personal') {
|
|
1834
|
+
async createChannel(name, type = 'personal', options = {}) {
|
|
1596
1835
|
this.#ensureInitialized()
|
|
1836
|
+
const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
|
|
1597
1837
|
|
|
1598
1838
|
if (!CHANNEL_NAME_REGEX.test(name)) {
|
|
1599
1839
|
throw new Error('频道名只能包含字母、数字、下划线和连字符')
|
|
@@ -1607,6 +1847,13 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1607
1847
|
|
|
1608
1848
|
const existing = this.#channels.find(c => c.name === name)
|
|
1609
1849
|
if (existing) {
|
|
1850
|
+
if (ownerAddress && !Array.isArray(existing.members)) {
|
|
1851
|
+
existing.members = []
|
|
1852
|
+
}
|
|
1853
|
+
if (ownerAddress && !existing.members.includes(ownerAddress)) {
|
|
1854
|
+
existing.members.push(ownerAddress)
|
|
1855
|
+
this.#saveChannelsMetadata()
|
|
1856
|
+
}
|
|
1610
1857
|
return { name: existing.name, key: existing.coreKey }
|
|
1611
1858
|
}
|
|
1612
1859
|
|
|
@@ -1633,10 +1880,18 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1633
1880
|
coreKey: b4a.toString(core.key, 'hex'),
|
|
1634
1881
|
createdAt: new Date().toISOString(),
|
|
1635
1882
|
type,
|
|
1883
|
+
ownerAddress,
|
|
1884
|
+
members: ownerAddress ? [ownerAddress] : [],
|
|
1885
|
+
remoteCoreKeys: [],
|
|
1636
1886
|
}
|
|
1637
1887
|
|
|
1638
1888
|
this.#channels.push(channelInfo)
|
|
1639
|
-
|
|
1889
|
+
const coreKeyHex = b4a.toString(core.key, 'hex')
|
|
1890
|
+
if (!this.#channelCores.has(name)) {
|
|
1891
|
+
this.#channelCores.set(name, new Map())
|
|
1892
|
+
}
|
|
1893
|
+
this.#channelCores.get(name).set(coreKeyHex, core)
|
|
1894
|
+
this.#channelLocalCoreKey.set(name, coreKeyHex)
|
|
1640
1895
|
this.#channelPeers.set(name, new Map())
|
|
1641
1896
|
this.#channelDiscoveries.set(name, appDiscovery)
|
|
1642
1897
|
this.#channelChatDiscoveries.set(name, chatDiscovery)
|
|
@@ -1654,11 +1909,29 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1654
1909
|
* @param {string} [coreKey] - 频道的 coreKey(加入他人创建的频道时必填)
|
|
1655
1910
|
* @returns {Promise<{ name: string, key: string }>}
|
|
1656
1911
|
*/
|
|
1657
|
-
async joinChannel(name, coreKey = null) {
|
|
1912
|
+
async joinChannel(name, coreKey = null, options = {}) {
|
|
1658
1913
|
this.#ensureInitialized()
|
|
1914
|
+
const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
|
|
1659
1915
|
|
|
1660
1916
|
const existing = this.#channels.find(c => c.name === name)
|
|
1661
1917
|
if (existing) {
|
|
1918
|
+
if (ownerAddress && !Array.isArray(existing.members)) {
|
|
1919
|
+
existing.members = []
|
|
1920
|
+
}
|
|
1921
|
+
if (ownerAddress && !existing.members.includes(ownerAddress)) {
|
|
1922
|
+
existing.members.push(ownerAddress)
|
|
1923
|
+
this.#saveChannelsMetadata()
|
|
1924
|
+
}
|
|
1925
|
+
if (coreKey && coreKey !== existing.coreKey) {
|
|
1926
|
+
if (!Array.isArray(existing.remoteCoreKeys)) {
|
|
1927
|
+
existing.remoteCoreKeys = []
|
|
1928
|
+
}
|
|
1929
|
+
if (!existing.remoteCoreKeys.includes(coreKey)) {
|
|
1930
|
+
existing.remoteCoreKeys.push(coreKey)
|
|
1931
|
+
this.#saveChannelsMetadata()
|
|
1932
|
+
}
|
|
1933
|
+
await this.#openRemoteChannelCore(name, coreKey)
|
|
1934
|
+
}
|
|
1662
1935
|
return { name: existing.name, key: existing.coreKey }
|
|
1663
1936
|
}
|
|
1664
1937
|
|
|
@@ -1667,11 +1940,13 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1667
1940
|
}
|
|
1668
1941
|
|
|
1669
1942
|
const ns = this.#store.namespace(`channel-${name}`)
|
|
1670
|
-
const
|
|
1671
|
-
|
|
1943
|
+
const remoteCoreKeyHex = b4a.toString(b4a.from(coreKey, 'hex'), 'hex')
|
|
1944
|
+
const localCore = ns.get({
|
|
1945
|
+
name: `messages-${this.getNodeId()}`,
|
|
1672
1946
|
valueEncoding: 'json',
|
|
1673
1947
|
})
|
|
1674
|
-
await
|
|
1948
|
+
await localCore.ready()
|
|
1949
|
+
const localCoreKeyHex = b4a.toString(localCore.key, 'hex')
|
|
1675
1950
|
|
|
1676
1951
|
const discoveryKey = this.#generateChannelDiscoveryKey(name)
|
|
1677
1952
|
const chatDiscoveryKey = this.#generateChannelChatDiscoveryKey(name)
|
|
@@ -1684,27 +1959,38 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1684
1959
|
client: true,
|
|
1685
1960
|
})
|
|
1686
1961
|
|
|
1687
|
-
this.#setupChannelAppendListener(
|
|
1962
|
+
this.#setupChannelAppendListener(localCore, name)
|
|
1688
1963
|
|
|
1689
1964
|
const channelInfo = {
|
|
1690
1965
|
name,
|
|
1691
1966
|
discoveryKey: b4a.toString(discoveryKey, 'hex'),
|
|
1692
|
-
coreKey,
|
|
1967
|
+
coreKey: localCoreKeyHex,
|
|
1693
1968
|
createdAt: new Date().toISOString(),
|
|
1694
1969
|
type: 'group',
|
|
1970
|
+
ownerAddress,
|
|
1971
|
+
members: ownerAddress ? [ownerAddress] : [],
|
|
1972
|
+
remoteCoreKeys:
|
|
1973
|
+
remoteCoreKeyHex === localCoreKeyHex ? [] : [remoteCoreKeyHex],
|
|
1695
1974
|
}
|
|
1696
1975
|
|
|
1697
1976
|
this.#channels.push(channelInfo)
|
|
1698
|
-
this.#channelCores.
|
|
1977
|
+
if (!this.#channelCores.has(name)) {
|
|
1978
|
+
this.#channelCores.set(name, new Map())
|
|
1979
|
+
}
|
|
1980
|
+
this.#channelCores.get(name).set(localCoreKeyHex, localCore)
|
|
1981
|
+
this.#channelLocalCoreKey.set(name, localCoreKeyHex)
|
|
1699
1982
|
this.#channelPeers.set(name, new Map())
|
|
1700
1983
|
this.#channelDiscoveries.set(name, appDiscovery)
|
|
1701
1984
|
this.#channelChatDiscoveries.set(name, chatDiscovery)
|
|
1702
1985
|
this.#saveChannelsMetadata()
|
|
1986
|
+
if (remoteCoreKeyHex !== localCoreKeyHex) {
|
|
1987
|
+
await this.#openRemoteChannelCore(name, remoteCoreKeyHex)
|
|
1988
|
+
}
|
|
1703
1989
|
|
|
1704
1990
|
console.log(`[MostBox] Joined channel: ${name}`)
|
|
1705
|
-
this.emit('channel:joined', { name, key:
|
|
1991
|
+
this.emit('channel:joined', { name, key: localCoreKeyHex })
|
|
1706
1992
|
|
|
1707
|
-
return { name, key:
|
|
1993
|
+
return { name, key: localCoreKeyHex }
|
|
1708
1994
|
}
|
|
1709
1995
|
|
|
1710
1996
|
/**
|
|
@@ -1712,8 +1998,9 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1712
1998
|
* @param {string} name - 频道名
|
|
1713
1999
|
* @returns {Promise<string[]>} 剩余频道列表
|
|
1714
2000
|
*/
|
|
1715
|
-
async leaveChannel(name) {
|
|
2001
|
+
async leaveChannel(name, options = {}) {
|
|
1716
2002
|
this.#ensureInitialized()
|
|
2003
|
+
const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
|
|
1717
2004
|
|
|
1718
2005
|
const index = this.#channels.findIndex(c => c.name === name)
|
|
1719
2006
|
if (index === -1) {
|
|
@@ -1721,6 +2008,15 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1721
2008
|
}
|
|
1722
2009
|
|
|
1723
2010
|
const channel = this.#channels[index]
|
|
2011
|
+
if (ownerAddress && Array.isArray(channel.members)) {
|
|
2012
|
+
channel.members = channel.members.filter(
|
|
2013
|
+
member => normalizeOwnerAddress(member) !== ownerAddress
|
|
2014
|
+
)
|
|
2015
|
+
if (channel.members.length > 0) {
|
|
2016
|
+
this.#saveChannelsMetadata()
|
|
2017
|
+
return this.listChannels({ ownerAddress })
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
1724
2020
|
|
|
1725
2021
|
const appDiscovery = this.#channelDiscoveries.get(name)
|
|
1726
2022
|
if (appDiscovery && this.#swarm) {
|
|
@@ -1745,18 +2041,21 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1745
2041
|
})
|
|
1746
2042
|
}
|
|
1747
2043
|
|
|
1748
|
-
const
|
|
1749
|
-
if (
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
2044
|
+
const coresMap = this.#channelCores.get(name)
|
|
2045
|
+
if (coresMap) {
|
|
2046
|
+
for (const [, core] of coresMap) {
|
|
2047
|
+
try {
|
|
2048
|
+
await core.close()
|
|
2049
|
+
} catch (err) {
|
|
2050
|
+
console.warn(
|
|
2051
|
+
`[MostBox] Failed to close channel core for ${name}:`,
|
|
2052
|
+
err.message
|
|
2053
|
+
)
|
|
2054
|
+
}
|
|
1757
2055
|
}
|
|
1758
2056
|
this.#channelCores.delete(name)
|
|
1759
2057
|
}
|
|
2058
|
+
this.#channelLocalCoreKey.delete(name)
|
|
1760
2059
|
|
|
1761
2060
|
this.#channelPeers.delete(name)
|
|
1762
2061
|
this.#channels.splice(index, 1)
|
|
@@ -1765,23 +2064,61 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1765
2064
|
console.log(`[MostBox] Left channel: ${name}`)
|
|
1766
2065
|
this.emit('channel:left', { name })
|
|
1767
2066
|
|
|
1768
|
-
return this.listChannels()
|
|
2067
|
+
return this.listChannels({ ownerAddress })
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
setChannelRemark(name, remark, options = {}) {
|
|
2071
|
+
this.#ensureInitialized()
|
|
2072
|
+
const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
|
|
2073
|
+
if (!ownerAddress) {
|
|
2074
|
+
throw new Error('需要登录才能设置备注')
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
const channel = this.#channels.find(c => c.name === name)
|
|
2078
|
+
if (!channel) {
|
|
2079
|
+
throw new Error('频道不存在')
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
const trimmed = (remark || '').trim()
|
|
2083
|
+
if (trimmed.length > 50) {
|
|
2084
|
+
throw new Error('备注最多 50 个字符')
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
if (!channel.remarks) {
|
|
2088
|
+
channel.remarks = {}
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2091
|
+
if (trimmed) {
|
|
2092
|
+
channel.remarks[ownerAddress] = trimmed
|
|
2093
|
+
} else {
|
|
2094
|
+
delete channel.remarks[ownerAddress]
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
this.#saveChannelsMetadata()
|
|
2098
|
+
return trimmed
|
|
1769
2099
|
}
|
|
1770
2100
|
|
|
1771
2101
|
/**
|
|
1772
2102
|
* 列出所有频道
|
|
1773
|
-
* @returns {Array<{ name: string, coreKey: string, createdAt: string, type: string, peerCount: number }>}
|
|
2103
|
+
* @returns {Array<{ name: string, coreKey: string, createdAt: string, type: string, peerCount: number, remark: string }>}
|
|
1774
2104
|
*/
|
|
1775
|
-
listChannels() {
|
|
2105
|
+
listChannels(options = {}) {
|
|
1776
2106
|
this.#ensureInitialized()
|
|
2107
|
+
const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
|
|
1777
2108
|
|
|
1778
|
-
return this.#channels
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
2109
|
+
return this.#channels
|
|
2110
|
+
.filter(c => {
|
|
2111
|
+
if (!ownerAddress) return true
|
|
2112
|
+
return Array.isArray(c.members) && c.members.includes(ownerAddress)
|
|
2113
|
+
})
|
|
2114
|
+
.map(c => ({
|
|
2115
|
+
name: c.name,
|
|
2116
|
+
coreKey: c.coreKey,
|
|
2117
|
+
createdAt: c.createdAt,
|
|
2118
|
+
type: c.type,
|
|
2119
|
+
peerCount: (this.#channelPeers.get(c.name) || new Map()).size,
|
|
2120
|
+
remark: ownerAddress && c.remarks ? c.remarks[ownerAddress] || '' : '',
|
|
2121
|
+
}))
|
|
1785
2122
|
}
|
|
1786
2123
|
|
|
1787
2124
|
/**
|
|
@@ -1794,29 +2131,48 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1794
2131
|
*/
|
|
1795
2132
|
async getChannelMessages(name, options = {}) {
|
|
1796
2133
|
this.#ensureInitialized()
|
|
2134
|
+
this.#assertChannelMember(name, options.ownerAddress)
|
|
1797
2135
|
|
|
1798
2136
|
const { limit = CHANNEL_MESSAGE_LIMIT, offset = 0 } = options
|
|
1799
2137
|
|
|
1800
|
-
const
|
|
1801
|
-
if (!
|
|
2138
|
+
const coresMap = this.#channelCores.get(name)
|
|
2139
|
+
if (!coresMap || coresMap.size === 0) {
|
|
1802
2140
|
throw new Error('频道未初始化')
|
|
1803
2141
|
}
|
|
1804
2142
|
|
|
1805
|
-
const
|
|
1806
|
-
const
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
2143
|
+
const allMessages = []
|
|
2144
|
+
for (const [coreKeyHex, core] of coresMap) {
|
|
2145
|
+
for (let i = 0; i < core.length; i++) {
|
|
2146
|
+
try {
|
|
2147
|
+
const entry = await core.get(i)
|
|
2148
|
+
if (entry && entry.type === 'message') {
|
|
2149
|
+
allMessages.push({
|
|
2150
|
+
...entry,
|
|
2151
|
+
_coreKey: coreKeyHex,
|
|
2152
|
+
_index: i,
|
|
2153
|
+
})
|
|
2154
|
+
}
|
|
2155
|
+
} catch {
|
|
2156
|
+
break
|
|
2157
|
+
}
|
|
1816
2158
|
}
|
|
1817
2159
|
}
|
|
1818
2160
|
|
|
1819
|
-
|
|
2161
|
+
const seen = new Set()
|
|
2162
|
+
const unique = allMessages.filter(m => {
|
|
2163
|
+
const key = `${m._coreKey}:${m.author}:${m.timestamp}:${m.content}`
|
|
2164
|
+
if (seen.has(key)) return false
|
|
2165
|
+
seen.add(key)
|
|
2166
|
+
return true
|
|
2167
|
+
})
|
|
2168
|
+
|
|
2169
|
+
unique.sort((a, b) => a.timestamp - b.timestamp)
|
|
2170
|
+
|
|
2171
|
+
const total = unique.length
|
|
2172
|
+
const start = Math.max(0, total - offset - limit)
|
|
2173
|
+
const end = total - offset
|
|
2174
|
+
|
|
2175
|
+
return unique.slice(start, end).map(({ _coreKey, _index, ...msg }) => msg)
|
|
1820
2176
|
}
|
|
1821
2177
|
|
|
1822
2178
|
/**
|
|
@@ -1825,14 +2181,18 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1825
2181
|
* @param {string} content - 消息内容
|
|
1826
2182
|
* @param {string} author - 作者 address
|
|
1827
2183
|
* @param {string} authorName - 作者显示名
|
|
2184
|
+
* @param {object} [options.attachment] - 附件元数据
|
|
1828
2185
|
* @returns {Promise<object>}
|
|
1829
2186
|
*/
|
|
1830
|
-
async sendMessage(name, content, author, authorName) {
|
|
2187
|
+
async sendMessage(name, content, author, authorName, options = {}) {
|
|
1831
2188
|
this.#ensureInitialized()
|
|
2189
|
+
this.#assertChannelMember(name, options.ownerAddress)
|
|
1832
2190
|
|
|
1833
|
-
const
|
|
2191
|
+
const localKeyHex = this.#channelLocalCoreKey.get(name)
|
|
2192
|
+
const coresMap = this.#channelCores.get(name)
|
|
2193
|
+
const core = localKeyHex && coresMap ? coresMap.get(localKeyHex) : null
|
|
1834
2194
|
if (!core) {
|
|
1835
|
-
throw new Error('
|
|
2195
|
+
throw new Error('频道未初始化或无可写 core')
|
|
1836
2196
|
}
|
|
1837
2197
|
|
|
1838
2198
|
if (!content || !content.trim()) {
|
|
@@ -1843,6 +2203,10 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1843
2203
|
if (trimmed.length > MAX_MESSAGE_LENGTH) {
|
|
1844
2204
|
throw new Error(`消息内容不能超过 ${MAX_MESSAGE_LENGTH} 字符`)
|
|
1845
2205
|
}
|
|
2206
|
+
const attachment = normalizeChannelAttachment(options.attachment)
|
|
2207
|
+
if (attachment && trimmed !== attachment.link) {
|
|
2208
|
+
throw new ValidationError('attachment content must match link')
|
|
2209
|
+
}
|
|
1846
2210
|
|
|
1847
2211
|
const message = {
|
|
1848
2212
|
type: 'message',
|
|
@@ -1851,11 +2215,12 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1851
2215
|
content: trimmed,
|
|
1852
2216
|
timestamp: Date.now(),
|
|
1853
2217
|
}
|
|
2218
|
+
if (attachment) {
|
|
2219
|
+
message.attachment = attachment
|
|
2220
|
+
}
|
|
1854
2221
|
|
|
1855
2222
|
await core.append(message)
|
|
1856
2223
|
|
|
1857
|
-
this.emit('channel:message', { channel: name, message })
|
|
1858
|
-
|
|
1859
2224
|
return message
|
|
1860
2225
|
}
|
|
1861
2226
|
|
|
@@ -1864,8 +2229,9 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1864
2229
|
* @param {string} name - 频道名
|
|
1865
2230
|
* @returns {Array<{ peerId: string, authorName: string, lastSeen: number }>}
|
|
1866
2231
|
*/
|
|
1867
|
-
getChannelPeers(name) {
|
|
2232
|
+
getChannelPeers(name, options = {}) {
|
|
1868
2233
|
this.#ensureInitialized()
|
|
2234
|
+
this.#assertChannelMember(name, options.ownerAddress)
|
|
1869
2235
|
|
|
1870
2236
|
const peers = this.#channelPeers.get(name)
|
|
1871
2237
|
if (!peers) {
|
|
@@ -1922,27 +2288,26 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1922
2288
|
}
|
|
1923
2289
|
}
|
|
1924
2290
|
|
|
1925
|
-
#
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
} catch (err) {
|
|
1939
|
-
if (err instanceof ValidationError) {
|
|
1940
|
-
throw err
|
|
1941
|
-
}
|
|
1942
|
-
throw new ValidationError('Invalid CID format')
|
|
2291
|
+
#assertChannelMember(name, ownerAddress) {
|
|
2292
|
+
const normalizedOwner = normalizeOwnerAddress(ownerAddress)
|
|
2293
|
+
if (!normalizedOwner) return
|
|
2294
|
+
|
|
2295
|
+
const channel = this.#channels.find(c => c.name === name)
|
|
2296
|
+
if (!channel) {
|
|
2297
|
+
throw new Error('频道不存在')
|
|
2298
|
+
}
|
|
2299
|
+
if (
|
|
2300
|
+
!Array.isArray(channel.members) ||
|
|
2301
|
+
!channel.members.includes(normalizedOwner)
|
|
2302
|
+
) {
|
|
2303
|
+
throw new PermissionError('未加入该频道')
|
|
1943
2304
|
}
|
|
1944
2305
|
}
|
|
1945
2306
|
|
|
2307
|
+
#getCidInfo(cid) {
|
|
2308
|
+
return getCidInfo(cid)
|
|
2309
|
+
}
|
|
2310
|
+
|
|
1946
2311
|
#setSeedState(cid, patch = {}) {
|
|
1947
2312
|
const previous = this.#seedStates.get(cid) || {}
|
|
1948
2313
|
const next = {
|
|
@@ -1962,6 +2327,111 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1962
2327
|
}
|
|
1963
2328
|
}
|
|
1964
2329
|
|
|
2330
|
+
#getFileRuntimeStats(cid) {
|
|
2331
|
+
const state = this.#fileMonitors.get(cid)
|
|
2332
|
+
if (!state) {
|
|
2333
|
+
return {
|
|
2334
|
+
peerCount: 0,
|
|
2335
|
+
lastServedAt: null,
|
|
2336
|
+
totalServedBytes: 0,
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
|
|
2340
|
+
return {
|
|
2341
|
+
peerCount: state.peerCount || 0,
|
|
2342
|
+
lastServedAt: state.lastServedAt || null,
|
|
2343
|
+
totalServedBytes: state.totalServedBytes || 0,
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
|
|
2347
|
+
async #ensureFileMonitor(cid, drive = null) {
|
|
2348
|
+
const existing = this.#fileMonitors.get(cid)
|
|
2349
|
+
if (existing) return existing
|
|
2350
|
+
|
|
2351
|
+
const { driveName } = this.#getCidInfo(cid)
|
|
2352
|
+
const monitoredDrive = drive || (await this.#getOrCreateDrive(driveName))
|
|
2353
|
+
const monitor = monitoredDrive.monitor('/' + cid)
|
|
2354
|
+
const state = {
|
|
2355
|
+
cid,
|
|
2356
|
+
monitor,
|
|
2357
|
+
peerCount: 0,
|
|
2358
|
+
lastServedAt: null,
|
|
2359
|
+
totalServedBytes: 0,
|
|
2360
|
+
uploadBytes: 0,
|
|
2361
|
+
uploadBlocks: 0,
|
|
2362
|
+
lastMetricsEmittedAt: 0,
|
|
2363
|
+
cleanup: null,
|
|
2364
|
+
}
|
|
2365
|
+
this.#fileMonitors.set(cid, state)
|
|
2366
|
+
|
|
2367
|
+
const emitMetrics = (force = false) => {
|
|
2368
|
+
const now = Date.now()
|
|
2369
|
+
if (!force && now - state.lastMetricsEmittedAt < 1000) return
|
|
2370
|
+
state.lastMetricsEmittedAt = now
|
|
2371
|
+
this.emit('seed:metrics', {
|
|
2372
|
+
cid,
|
|
2373
|
+
...this.#getFileRuntimeStats(cid),
|
|
2374
|
+
})
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2377
|
+
const updatePeerCount = () => {
|
|
2378
|
+
const nextPeerCount = Number(monitor.peers) || 0
|
|
2379
|
+
if (nextPeerCount !== state.peerCount) {
|
|
2380
|
+
state.peerCount = nextPeerCount
|
|
2381
|
+
emitMetrics(true)
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
2384
|
+
|
|
2385
|
+
const updateTransferStats = () => {
|
|
2386
|
+
updatePeerCount()
|
|
2387
|
+
const uploadStats = monitor.uploadStats || {}
|
|
2388
|
+
const uploadBytes = Number(uploadStats.monitoringBytes) || 0
|
|
2389
|
+
const uploadBlocks = Number(uploadStats.blocks) || 0
|
|
2390
|
+
const servedMore =
|
|
2391
|
+
uploadBytes > state.uploadBytes || uploadBlocks > state.uploadBlocks
|
|
2392
|
+
|
|
2393
|
+
if (servedMore) {
|
|
2394
|
+
state.lastServedAt = new Date().toISOString()
|
|
2395
|
+
state.totalServedBytes = uploadBytes
|
|
2396
|
+
}
|
|
2397
|
+
|
|
2398
|
+
state.uploadBytes = uploadBytes
|
|
2399
|
+
state.uploadBlocks = uploadBlocks
|
|
2400
|
+
if (servedMore) emitMetrics()
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2403
|
+
monitor.on('update', updateTransferStats)
|
|
2404
|
+
try {
|
|
2405
|
+
await monitor.ready()
|
|
2406
|
+
const blobs = monitor.blobs
|
|
2407
|
+
const onPeerUpdate = () => {
|
|
2408
|
+
updatePeerCount()
|
|
2409
|
+
}
|
|
2410
|
+
blobs?.core?.on('peer-add', onPeerUpdate)
|
|
2411
|
+
blobs?.core?.on('peer-remove', onPeerUpdate)
|
|
2412
|
+
state.cleanup = () => {
|
|
2413
|
+
blobs?.core?.off('peer-add', onPeerUpdate)
|
|
2414
|
+
blobs?.core?.off('peer-remove', onPeerUpdate)
|
|
2415
|
+
}
|
|
2416
|
+
updateTransferStats()
|
|
2417
|
+
} catch (err) {
|
|
2418
|
+
this.#fileMonitors.delete(cid)
|
|
2419
|
+
monitor.off('update', updateTransferStats)
|
|
2420
|
+
await monitor.close().catch(() => {})
|
|
2421
|
+
throw err
|
|
2422
|
+
}
|
|
2423
|
+
|
|
2424
|
+
return state
|
|
2425
|
+
}
|
|
2426
|
+
|
|
2427
|
+
async #closeFileMonitor(state) {
|
|
2428
|
+
if (!state) return
|
|
2429
|
+
try {
|
|
2430
|
+
state.cleanup?.()
|
|
2431
|
+
await state.monitor.close()
|
|
2432
|
+
} catch {}
|
|
2433
|
+
}
|
|
2434
|
+
|
|
1965
2435
|
#resumeHoldingsInBackground() {
|
|
1966
2436
|
if (this.#holdingResumeTask || this.#holdings.length === 0) {
|
|
1967
2437
|
return
|
|
@@ -2028,7 +2498,6 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
2028
2498
|
topic: topicHex,
|
|
2029
2499
|
driveName: record.driveName || driveName,
|
|
2030
2500
|
source: record.source || 'manual',
|
|
2031
|
-
temporary: record.temporary === true,
|
|
2032
2501
|
}
|
|
2033
2502
|
}
|
|
2034
2503
|
|
|
@@ -2049,6 +2518,12 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
2049
2518
|
|
|
2050
2519
|
this.#saveHoldingsMetadata()
|
|
2051
2520
|
this.emit('holding:updated', next)
|
|
2521
|
+
this.#ensureFileMonitor(next.cid).catch(err => {
|
|
2522
|
+
this.#setSeedState(next.cid, {
|
|
2523
|
+
status: 'error',
|
|
2524
|
+
error: err.message,
|
|
2525
|
+
})
|
|
2526
|
+
})
|
|
2052
2527
|
const seedState = this.#seedStates.get(next.cid)
|
|
2053
2528
|
return {
|
|
2054
2529
|
...next,
|
|
@@ -2058,6 +2533,7 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
2058
2533
|
(this.#fileDiscoveries.has(next.cid) ? 'active' : 'queued'),
|
|
2059
2534
|
seedError: seedState?.error,
|
|
2060
2535
|
seedStatusUpdatedAt: seedState?.updatedAt,
|
|
2536
|
+
...this.#getFileRuntimeStats(next.cid),
|
|
2061
2537
|
}
|
|
2062
2538
|
}
|
|
2063
2539
|
|
|
@@ -2068,11 +2544,15 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
2068
2544
|
this.#saveHoldingsMetadata()
|
|
2069
2545
|
this.emit('holding:removed', { cid })
|
|
2070
2546
|
}
|
|
2547
|
+
this.#closeFileMonitor(this.#fileMonitors.get(cid))
|
|
2548
|
+
this.#fileMonitors.delete(cid)
|
|
2071
2549
|
this.#clearSeedState(cid)
|
|
2072
2550
|
}
|
|
2073
2551
|
|
|
2074
2552
|
async #joinCidTopicInternal(cid, options = {}) {
|
|
2075
2553
|
const { topic, topicHex, driveName } = this.#getCidInfo(cid)
|
|
2554
|
+
const requestedServer = options.server !== false
|
|
2555
|
+
const requestedClient = options.client === true
|
|
2076
2556
|
this.#setSeedState(cid, {
|
|
2077
2557
|
status: 'joining',
|
|
2078
2558
|
topic: topicHex,
|
|
@@ -2081,33 +2561,60 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
2081
2561
|
})
|
|
2082
2562
|
|
|
2083
2563
|
try {
|
|
2084
|
-
await this.#getOrCreateDrive(driveName)
|
|
2564
|
+
const drive = await this.#getOrCreateDrive(driveName)
|
|
2085
2565
|
|
|
2086
2566
|
const existing = this.#fileDiscoveries.get(cid)
|
|
2087
2567
|
if (existing) {
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2568
|
+
const nextServer = existing.server || requestedServer
|
|
2569
|
+
const nextClient = existing.client || requestedClient
|
|
2570
|
+
const needsRoleUpgrade =
|
|
2571
|
+
nextServer !== existing.server || nextClient !== existing.client
|
|
2572
|
+
|
|
2573
|
+
if (!needsRoleUpgrade) {
|
|
2574
|
+
if (this.#holdings.some(holding => holding.cid === cid)) {
|
|
2575
|
+
this.#ensureFileMonitor(cid, drive).catch(err => {
|
|
2576
|
+
this.#setSeedState(cid, {
|
|
2577
|
+
status: 'error',
|
|
2578
|
+
error: err.message,
|
|
2579
|
+
})
|
|
2580
|
+
})
|
|
2581
|
+
}
|
|
2582
|
+
this.#setSeedState(cid, {
|
|
2583
|
+
status: 'active',
|
|
2584
|
+
topic: topicHex,
|
|
2585
|
+
driveName,
|
|
2586
|
+
error: undefined,
|
|
2587
|
+
})
|
|
2588
|
+
return {
|
|
2589
|
+
cid,
|
|
2590
|
+
topic: topicHex,
|
|
2591
|
+
driveName,
|
|
2592
|
+
joined: true,
|
|
2593
|
+
}
|
|
2099
2594
|
}
|
|
2595
|
+
|
|
2596
|
+
await this.#swarm.leave(topic).catch(err => {
|
|
2597
|
+
console.warn(
|
|
2598
|
+
`[MostBox] Failed to upgrade CID topic role for ${cid}:`,
|
|
2599
|
+
err.message
|
|
2600
|
+
)
|
|
2601
|
+
})
|
|
2602
|
+
this.#fileDiscoveries.delete(cid)
|
|
2100
2603
|
}
|
|
2101
2604
|
|
|
2605
|
+
const server = existing?.server || requestedServer
|
|
2606
|
+
const client = existing?.client || requestedClient
|
|
2102
2607
|
const discovery = this.#swarm.join(topic, {
|
|
2103
|
-
server
|
|
2104
|
-
client
|
|
2608
|
+
server,
|
|
2609
|
+
client,
|
|
2105
2610
|
})
|
|
2106
2611
|
|
|
2107
2612
|
this.#fileDiscoveries.set(cid, {
|
|
2108
2613
|
discovery,
|
|
2109
2614
|
topic: topicHex,
|
|
2110
2615
|
driveName,
|
|
2616
|
+
server,
|
|
2617
|
+
client,
|
|
2111
2618
|
})
|
|
2112
2619
|
this.#setSeedState(cid, {
|
|
2113
2620
|
status: 'active',
|
|
@@ -2115,6 +2622,14 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
2115
2622
|
driveName,
|
|
2116
2623
|
error: undefined,
|
|
2117
2624
|
})
|
|
2625
|
+
if (this.#holdings.some(holding => holding.cid === cid)) {
|
|
2626
|
+
this.#ensureFileMonitor(cid, drive).catch(err => {
|
|
2627
|
+
this.#setSeedState(cid, {
|
|
2628
|
+
status: 'error',
|
|
2629
|
+
error: err.message,
|
|
2630
|
+
})
|
|
2631
|
+
})
|
|
2632
|
+
}
|
|
2118
2633
|
this.emit('file:topic:joined', { cid, topic: topicHex, driveName })
|
|
2119
2634
|
|
|
2120
2635
|
return {
|
|
@@ -2158,19 +2673,44 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
2158
2673
|
return null
|
|
2159
2674
|
}
|
|
2160
2675
|
|
|
2161
|
-
if (this.#swarm) {
|
|
2162
|
-
this.#swarm.leave(drive.discoveryKey).catch(err => {
|
|
2163
|
-
console.warn(
|
|
2164
|
-
`[MostBox] Failed to leave drive discovery ${driveName}:`,
|
|
2165
|
-
err.message
|
|
2166
|
-
)
|
|
2167
|
-
})
|
|
2168
|
-
}
|
|
2169
2676
|
await drive.close()
|
|
2170
2677
|
this.#drives.delete(driveName)
|
|
2171
2678
|
return drive
|
|
2172
2679
|
}
|
|
2173
2680
|
|
|
2681
|
+
#recordMatchesOwner(record, ownerAddress) {
|
|
2682
|
+
const normalizedOwner = normalizeOwnerAddress(ownerAddress)
|
|
2683
|
+
if (!normalizedOwner) return !record.ownerAddress
|
|
2684
|
+
return normalizeOwnerAddress(record.ownerAddress) === normalizedOwner
|
|
2685
|
+
}
|
|
2686
|
+
|
|
2687
|
+
#hasPublishedReference(cid) {
|
|
2688
|
+
return this.#publishedFiles.some(file => file.cid === cid)
|
|
2689
|
+
}
|
|
2690
|
+
|
|
2691
|
+
#hasAnyUserReference(cid) {
|
|
2692
|
+
return (
|
|
2693
|
+
this.#publishedFiles.some(file => file.cid === cid) ||
|
|
2694
|
+
this.#trashFiles.some(file => file.cid === cid)
|
|
2695
|
+
)
|
|
2696
|
+
}
|
|
2697
|
+
|
|
2698
|
+
#getUsedBytes() {
|
|
2699
|
+
return this.#holdings.reduce((sum, h) => sum + (h.size || 0), 0)
|
|
2700
|
+
}
|
|
2701
|
+
|
|
2702
|
+
#checkCapacity(additionalBytes) {
|
|
2703
|
+
const used = this.#getUsedBytes()
|
|
2704
|
+
const capacity = this.#options.capacityBytes
|
|
2705
|
+
if (used + additionalBytes > capacity) {
|
|
2706
|
+
const usedGB = (used / (1024 * 1024 * 1024)).toFixed(2)
|
|
2707
|
+
const capacityGB = (capacity / (1024 * 1024 * 1024)).toFixed(2)
|
|
2708
|
+
throw new StorageCapacityError(
|
|
2709
|
+
`Storage capacity exceeded: used ${usedGB} GB, capacity ${capacityGB} GB`
|
|
2710
|
+
)
|
|
2711
|
+
}
|
|
2712
|
+
}
|
|
2713
|
+
|
|
2174
2714
|
async #getOrCreateDrive(name, _options = { server: true, client: false }) {
|
|
2175
2715
|
if (this.#drives.has(name)) return this.#drives.get(name)
|
|
2176
2716
|
if (this.#drivePromises.has(name)) return this.#drivePromises.get(name)
|
|
@@ -2193,11 +2733,6 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
2193
2733
|
}
|
|
2194
2734
|
|
|
2195
2735
|
async #syncDrive(drive, timeout = DRIVE_SYNC_TIMEOUT) {
|
|
2196
|
-
const done = drive.findingPeers()
|
|
2197
|
-
this.#swarm
|
|
2198
|
-
.join(drive.discoveryKey, { server: true, client: true })
|
|
2199
|
-
.flushed()
|
|
2200
|
-
.then(done, done)
|
|
2201
2736
|
try {
|
|
2202
2737
|
const updated = await Promise.race([
|
|
2203
2738
|
drive.update(),
|
|
@@ -2384,15 +2919,57 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
2384
2919
|
})
|
|
2385
2920
|
}
|
|
2386
2921
|
|
|
2922
|
+
async #openRemoteChannelCore(channelName, coreKeyHex) {
|
|
2923
|
+
const coresMap = this.#channelCores.get(channelName)
|
|
2924
|
+
if (!coresMap) return
|
|
2925
|
+
if (coresMap.has(coreKeyHex)) return
|
|
2926
|
+
|
|
2927
|
+
try {
|
|
2928
|
+
const ns = this.#store.namespace(`channel-${channelName}`)
|
|
2929
|
+
const core = ns.get({
|
|
2930
|
+
key: b4a.from(coreKeyHex, 'hex'),
|
|
2931
|
+
valueEncoding: 'json',
|
|
2932
|
+
})
|
|
2933
|
+
await core.ready()
|
|
2934
|
+
const normalizedCoreKey = b4a.toString(core.key, 'hex')
|
|
2935
|
+
coresMap.set(normalizedCoreKey, core)
|
|
2936
|
+
this.#setupChannelAppendListener(core, channelName)
|
|
2937
|
+
const channel = this.#channels.find(c => c.name === channelName)
|
|
2938
|
+
if (channel && normalizedCoreKey !== channel.coreKey) {
|
|
2939
|
+
if (!Array.isArray(channel.remoteCoreKeys)) {
|
|
2940
|
+
channel.remoteCoreKeys = []
|
|
2941
|
+
}
|
|
2942
|
+
if (!channel.remoteCoreKeys.includes(normalizedCoreKey)) {
|
|
2943
|
+
channel.remoteCoreKeys.push(normalizedCoreKey)
|
|
2944
|
+
this.#saveChannelsMetadata()
|
|
2945
|
+
}
|
|
2946
|
+
}
|
|
2947
|
+
console.log(
|
|
2948
|
+
`[MostBox] Opened remote channel core ${normalizedCoreKey.slice(0, 8)}... for ${channelName}`
|
|
2949
|
+
)
|
|
2950
|
+
} catch (err) {
|
|
2951
|
+
console.warn(
|
|
2952
|
+
`[MostBox] Failed to open remote channel core for ${channelName}:`,
|
|
2953
|
+
err.message
|
|
2954
|
+
)
|
|
2955
|
+
}
|
|
2956
|
+
}
|
|
2957
|
+
|
|
2387
2958
|
async #handleChannelConnection(conn) {
|
|
2388
2959
|
const stream = conn
|
|
2389
2960
|
let connectedPeerId = null
|
|
2390
2961
|
|
|
2962
|
+
const coreKeys = {}
|
|
2963
|
+
for (const [name, localKeyHex] of this.#channelLocalCoreKey) {
|
|
2964
|
+
coreKeys[name] = localKeyHex
|
|
2965
|
+
}
|
|
2966
|
+
|
|
2391
2967
|
const helloMessage = JSON.stringify({
|
|
2392
2968
|
type: 'channel-hello',
|
|
2393
2969
|
peerId: this.getNodeId(),
|
|
2394
2970
|
authorName: this.getNodeId().slice(0, 4),
|
|
2395
2971
|
channels: this.#channels.map(c => c.name),
|
|
2972
|
+
coreKeys,
|
|
2396
2973
|
})
|
|
2397
2974
|
|
|
2398
2975
|
try {
|
|
@@ -2417,6 +2994,17 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
2417
2994
|
})
|
|
2418
2995
|
}
|
|
2419
2996
|
}
|
|
2997
|
+
|
|
2998
|
+
if (msg.coreKeys && typeof msg.coreKeys === 'object') {
|
|
2999
|
+
for (const [channelName, coreKeyHex] of Object.entries(
|
|
3000
|
+
msg.coreKeys
|
|
3001
|
+
)) {
|
|
3002
|
+
if (this.#channelCores.has(channelName) && coreKeyHex) {
|
|
3003
|
+
await this.#openRemoteChannelCore(channelName, coreKeyHex)
|
|
3004
|
+
}
|
|
3005
|
+
}
|
|
3006
|
+
}
|
|
3007
|
+
|
|
2420
3008
|
this.emit('channel:peer:online', {
|
|
2421
3009
|
peerId: msg.peerId,
|
|
2422
3010
|
authorName: msg.authorName,
|