most-box 0.0.4 → 0.0.6
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/LICENSE +21 -21
- package/README.md +222 -182
- package/electron/main.js +67 -0
- package/out/404/index.html +2 -2
- package/out/404.html +2 -2
- package/out/__next.__PAGE__.txt +6 -4
- package/out/__next._full.txt +20 -17
- package/out/__next._head.txt +3 -3
- package/out/__next._index.txt +6 -5
- package/out/__next._tree.txt +4 -2
- package/out/_next/static/chunks/00s106sbq8t9v.js +1 -0
- package/out/_next/static/chunks/0174xh3wfsjm1.js +2 -0
- package/out/_next/static/chunks/01xlw8hd842-c.js +1 -0
- package/out/_next/static/chunks/02ou_44kkb5dz.js +1 -0
- package/out/_next/static/chunks/02pr2b_eos3~h.js +1 -0
- package/out/_next/static/chunks/07lsjkarm1p9f.css +1 -0
- package/out/_next/static/chunks/0_-ccbcyh_o30.css +1 -0
- package/out/_next/static/chunks/0_b839~4.q324.js +1 -0
- package/out/_next/static/chunks/0_sna3wdypbzr.js +1 -0
- package/out/_next/static/chunks/0_wia9ofmsi1c.css +2 -0
- package/out/_next/static/chunks/0byj66sc-9o0g.js +1 -0
- package/out/_next/static/chunks/0bzupvr5gt3k9.js +31 -0
- package/out/_next/static/chunks/0d3shmwh5_nmn.js +1 -0
- package/out/_next/static/chunks/0du450zbk4kq_.js +1 -0
- package/out/_next/static/chunks/0e_h0d3ekzks8.css +1 -0
- package/out/_next/static/chunks/0ho~log~~-jwp.css +1 -0
- package/out/_next/static/chunks/0ibjp~7qzxfjv.js +5 -0
- package/out/_next/static/chunks/0imvn_arv36xt.css +1 -0
- package/out/_next/static/chunks/0j9~17180dl8j.js +1 -0
- package/out/_next/static/chunks/0ji.28mehrvdp.js +1 -0
- package/out/_next/static/chunks/0jl~j62iz2uvr.js +1 -0
- package/out/_next/static/chunks/0nct0fubs64d-.js +1 -0
- package/out/_next/static/chunks/{0bogtdbh.dcu1.js → 0n~dq4kpx9xxx.js} +1 -1
- package/out/_next/static/chunks/0pqt~8bl3ukh4.js +4 -0
- package/out/_next/static/chunks/0q7ck9f.90_i9.js +1 -0
- package/out/_next/static/chunks/0qub_r0x_r-e9.css +1 -0
- package/out/_next/static/chunks/0rr4gwjp9z~9a.js +1 -0
- package/out/_next/static/chunks/0ry.po.a~iu4p.js +1 -0
- package/out/_next/static/chunks/0slwj0c46k5cu.js +1 -0
- package/out/_next/static/chunks/0sorqk.oc6b7j.css +1 -0
- package/out/_next/static/chunks/11dalasm30arx.js +1 -0
- package/out/_next/static/chunks/turbopack-0a_g3u0ud~jb8.js +1 -0
- package/out/_not-found/__next._full.txt +19 -15
- package/out/_not-found/__next._head.txt +3 -3
- package/out/_not-found/__next._index.txt +6 -5
- package/out/_not-found/__next._not-found.__PAGE__.txt +9 -0
- package/out/_not-found/__next._not-found.txt +3 -3
- package/out/_not-found/__next._tree.txt +2 -2
- package/out/_not-found/index.html +2 -2
- package/out/_not-found/index.txt +19 -15
- package/out/app/__next._full.txt +20 -0
- package/out/app/__next._head.txt +5 -0
- package/out/app/__next._index.txt +7 -0
- package/out/app/__next._tree.txt +2 -0
- package/out/app/__next.app.__PAGE__.txt +9 -0
- package/out/app/__next.app.txt +5 -0
- package/out/app/index.html +15 -0
- package/out/app/index.txt +20 -0
- package/out/changelog/__next._full.txt +22 -0
- package/out/changelog/__next._head.txt +5 -0
- package/out/changelog/__next._index.txt +7 -0
- package/out/changelog/__next._tree.txt +3 -0
- package/out/changelog/__next.changelog.__PAGE__.txt +10 -0
- package/out/changelog/__next.changelog.txt +5 -0
- package/out/changelog/index.html +15 -0
- package/out/changelog/index.txt +22 -0
- package/out/chat/__next._full.txt +20 -18
- package/out/chat/__next._head.txt +3 -3
- package/out/chat/__next._index.txt +6 -5
- package/out/chat/__next._tree.txt +3 -3
- package/out/chat/__next.chat.__PAGE__.txt +9 -0
- package/out/chat/__next.chat.txt +5 -4
- package/out/chat/index.html +2 -2
- package/out/chat/index.txt +20 -18
- package/out/docs/__next._full.txt +22 -0
- package/out/docs/__next._head.txt +5 -0
- package/out/docs/__next._index.txt +7 -0
- package/out/docs/__next._tree.txt +3 -0
- package/out/docs/__next.docs.__PAGE__.txt +10 -0
- package/out/docs/__next.docs.txt +5 -0
- package/out/docs/getting-started/__next._full.txt +22 -0
- package/out/docs/getting-started/__next._head.txt +5 -0
- package/out/docs/getting-started/__next._index.txt +7 -0
- package/out/docs/getting-started/__next._tree.txt +3 -0
- package/out/docs/getting-started/__next.docs.getting-started.__PAGE__.txt +10 -0
- package/out/docs/getting-started/__next.docs.getting-started.txt +5 -0
- package/out/docs/getting-started/__next.docs.txt +5 -0
- package/out/docs/getting-started/index.html +15 -0
- package/out/docs/getting-started/index.txt +22 -0
- package/out/docs/index.html +15 -0
- package/out/docs/index.txt +22 -0
- package/out/download/__next._full.txt +34 -0
- package/out/download/__next._head.txt +5 -0
- package/out/download/__next._index.txt +7 -0
- package/out/download/__next._tree.txt +4 -0
- package/out/download/__next.download.__PAGE__.txt +16 -0
- package/out/download/__next.download.txt +5 -0
- package/out/download/index.html +15 -0
- package/out/download/index.txt +34 -0
- package/out/fonts/jetbrains-mono-latin-400-normal.woff2 +0 -0
- package/out/fonts/jetbrains-mono-latin-500-normal.woff2 +0 -0
- package/out/fonts/jetbrains-mono-latin-600-normal.woff2 +0 -0
- package/out/fonts/jetbrains-mono-latin-700-normal.woff2 +0 -0
- package/out/index.html +2 -2
- package/out/index.txt +20 -17
- package/out/lottery/__next._full.txt +21 -0
- package/out/lottery/__next._head.txt +5 -0
- package/out/lottery/__next._index.txt +7 -0
- package/out/lottery/__next._tree.txt +3 -0
- package/out/lottery/__next.lottery.__PAGE__.txt +9 -0
- package/out/lottery/__next.lottery.txt +6 -0
- package/out/lottery/index.html +15 -0
- package/out/lottery/index.txt +21 -0
- package/out/ping/__next._full.txt +21 -0
- package/out/ping/__next._head.txt +5 -0
- package/out/ping/__next._index.txt +7 -0
- package/out/ping/__next._tree.txt +4 -0
- package/out/ping/__next.ping.__PAGE__.txt +10 -0
- package/out/ping/__next.ping.txt +5 -0
- package/out/ping/index.html +15 -0
- package/out/ping/index.txt +21 -0
- package/out/pwa-512x512.png +0 -0
- package/out/web3/__next._full.txt +21 -0
- package/out/web3/__next._head.txt +5 -0
- package/out/web3/__next._index.txt +7 -0
- package/out/web3/__next._tree.txt +3 -0
- package/out/web3/__next.web3.__PAGE__.txt +9 -0
- package/out/web3/__next.web3.txt +6 -0
- package/out/web3/ed25519/__next._full.txt +20 -0
- package/out/web3/ed25519/__next._head.txt +5 -0
- package/out/web3/ed25519/__next._index.txt +7 -0
- package/out/web3/ed25519/__next._tree.txt +3 -0
- package/out/web3/ed25519/__next.web3.ed25519.__PAGE__.txt +6 -0
- package/out/web3/ed25519/__next.web3.ed25519.txt +5 -0
- package/out/web3/ed25519/__next.web3.txt +6 -0
- package/out/web3/ed25519/index.html +1 -0
- package/out/web3/ed25519/index.txt +20 -0
- package/out/web3/index.html +15 -0
- package/out/web3/index.txt +21 -0
- package/out/web3/tools/__next._full.txt +20 -0
- package/out/web3/tools/__next._head.txt +5 -0
- package/out/web3/tools/__next._index.txt +7 -0
- package/out/web3/tools/__next._tree.txt +3 -0
- package/out/web3/tools/__next.web3.tools.__PAGE__.txt +6 -0
- package/out/web3/tools/__next.web3.tools.txt +5 -0
- package/out/web3/tools/__next.web3.txt +6 -0
- package/out/web3/tools/index.html +1 -0
- package/out/web3/tools/index.txt +20 -0
- package/package.json +162 -53
- package/public/fonts/jetbrains-mono-latin-400-normal.woff2 +0 -0
- package/public/fonts/jetbrains-mono-latin-500-normal.woff2 +0 -0
- package/public/fonts/jetbrains-mono-latin-600-normal.woff2 +0 -0
- package/public/fonts/jetbrains-mono-latin-700-normal.woff2 +0 -0
- package/public/pwa-512x512.png +0 -0
- package/server/cli.js +3 -0
- package/server/index.js +963 -0
- package/{src → server/src}/config.js +51 -50
- package/{src → server/src}/core/cid.js +157 -150
- package/{src → server/src}/index.js +1950 -1669
- package/server/src/utils/api.js +68 -0
- package/server/src/utils/avatar.js +11 -0
- package/{src → server/src}/utils/errors.js +70 -66
- package/server/src/utils/mostWallet.js +42 -0
- package/server/src/utils/mp.js +117 -0
- package/{src → server/src}/utils/security.js +173 -169
- package/server/src/utils/userIdentity.js +93 -0
- package/cli.js +0 -2
- package/out/_next/static/chunks/00l-yd3t8dvwz.js +0 -5
- package/out/_next/static/chunks/03k8t3tgym~8~.js +0 -1
- package/out/_next/static/chunks/09vfh8lfuacc0.css +0 -1
- package/out/_next/static/chunks/0dbhjjzl8qfwv.js +0 -1
- package/out/_next/static/chunks/0f73psqhr8dre.css +0 -1
- package/out/_next/static/chunks/0fbi7z4_.4j1j.js +0 -1
- package/out/_next/static/chunks/0ht900cau6_ur.js +0 -31
- package/out/_next/static/chunks/0ohm.ia-4ec60.js +0 -1
- package/out/_next/static/chunks/0u5ydb-f0.vxl.js +0 -1
- package/out/_next/static/chunks/14t2m1on-s5v~.js +0 -1
- package/out/_next/static/chunks/turbopack-076ce9exut_h3.js +0 -1
- package/out/_not-found/__next._not-found/__PAGE__.txt +0 -5
- package/out/app.css +0 -1535
- package/out/bundle.js +0 -107
- package/out/bundle.js.map +0 -7
- package/out/chat/__next.chat/__PAGE__.txt +0 -9
- package/out/chat-page.js +0 -112
- package/out/chat.css +0 -378
- package/out/index.js +0 -148
- package/public/app.css +0 -1535
- package/public/bundle.js +0 -107
- package/public/bundle.js.map +0 -7
- package/public/chat-page.js +0 -112
- package/public/chat.css +0 -378
- package/public/index.js +0 -148
- package/server.js +0 -880
- package/src/utils/api.js +0 -6
- /package/out/_next/static/{0h4f4QFk_KC9FlSRfQACk → alUUgRz4oMlw4EtULOYfV}/_buildManifest.js +0 -0
- /package/out/_next/static/{0h4f4QFk_KC9FlSRfQACk → alUUgRz4oMlw4EtULOYfV}/_clientMiddlewareManifest.js +0 -0
- /package/out/_next/static/{0h4f4QFk_KC9FlSRfQACk → alUUgRz4oMlw4EtULOYfV}/_ssgManifest.js +0 -0
|
@@ -1,1669 +1,1950 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* MostBoxEngine - 核心 P2P 引擎
|
|
3
|
-
* 基于 Hyperswarm/Hyperdrive 的跨平台 P2P 文件共享引擎
|
|
4
|
-
*
|
|
5
|
-
* 架构设计:
|
|
6
|
-
* - Hyperdrive: 只负责存储文件内容,key 使用 CID(解耦存储与目录结构)
|
|
7
|
-
* - published-files.json: 维护文件元数据和显示路径(用户看到的文件夹结构)
|
|
8
|
-
* - 移动/重命名只需更新 JSON,零成本,不修改 Hyperdrive
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import EventEmitter from 'eventemitter3'
|
|
12
|
-
import Hyperswarm from 'hyperswarm'
|
|
13
|
-
import Corestore from 'corestore'
|
|
14
|
-
import Hyperdrive from 'hyperdrive'
|
|
15
|
-
import
|
|
16
|
-
import
|
|
17
|
-
import
|
|
18
|
-
import
|
|
19
|
-
import
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
import {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
this.#
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
this.#
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
this.#
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
this
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
this.#
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
const
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
const
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
this.#
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
*
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
}
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
}
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
const
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
}
|
|
1173
|
-
|
|
1174
|
-
const
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
}
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
this.
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
const
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
}
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
const
|
|
1239
|
-
const
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
}
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
this.#
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
this.#
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
const
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
}
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
}
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
.
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
return
|
|
1502
|
-
}
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
}
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
const
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
}
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
}
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1
|
+
/**
|
|
2
|
+
* MostBoxEngine - 核心 P2P 引擎
|
|
3
|
+
* 基于 Hyperswarm/Hyperdrive 的跨平台 P2P 文件共享引擎
|
|
4
|
+
*
|
|
5
|
+
* 架构设计:
|
|
6
|
+
* - Hyperdrive: 只负责存储文件内容,key 使用 CID(解耦存储与目录结构)
|
|
7
|
+
* - published-files.json: 维护文件元数据和显示路径(用户看到的文件夹结构)
|
|
8
|
+
* - 移动/重命名只需更新 JSON,零成本,不修改 Hyperdrive
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import EventEmitter from 'eventemitter3'
|
|
12
|
+
import Hyperswarm from 'hyperswarm'
|
|
13
|
+
import Corestore from 'corestore'
|
|
14
|
+
import Hyperdrive from 'hyperdrive'
|
|
15
|
+
import b4a from 'b4a'
|
|
16
|
+
import crypto from 'node:crypto'
|
|
17
|
+
import { CID } from 'multiformats/cid'
|
|
18
|
+
import fs from 'node:fs'
|
|
19
|
+
import path from 'node:path'
|
|
20
|
+
|
|
21
|
+
import { calculateCid, parseMostLink } from './core/cid.js'
|
|
22
|
+
import {
|
|
23
|
+
sanitizeFilename,
|
|
24
|
+
validateAndSanitizePath,
|
|
25
|
+
validateFileSize,
|
|
26
|
+
checkDirectoryWritable,
|
|
27
|
+
formatFileSize,
|
|
28
|
+
} from './utils/security.js'
|
|
29
|
+
import {
|
|
30
|
+
ValidationError,
|
|
31
|
+
PathSecurityError,
|
|
32
|
+
FileSizeError,
|
|
33
|
+
PeerNotFoundError,
|
|
34
|
+
IntegrityError,
|
|
35
|
+
PermissionError,
|
|
36
|
+
EngineNotInitializedError,
|
|
37
|
+
} from './utils/errors.js'
|
|
38
|
+
import {
|
|
39
|
+
GLOBAL_SHARED_SEED_STRING,
|
|
40
|
+
MAX_FILE_SIZE,
|
|
41
|
+
CONNECTION_TIMEOUT,
|
|
42
|
+
DOWNLOAD_TIMEOUT,
|
|
43
|
+
SWARM_BOOTSTRAP,
|
|
44
|
+
MAX_PEERS,
|
|
45
|
+
SWARM_KEEP_ALIVE_INTERVAL,
|
|
46
|
+
SWARM_RANDOM_PUNCH_INTERVAL,
|
|
47
|
+
DRIVE_ENTRY_TIMEOUT,
|
|
48
|
+
DRIVE_SYNC_TIMEOUT,
|
|
49
|
+
STREAM_READ_TIMEOUT,
|
|
50
|
+
FILE_WRITE_CHUNK_SIZE,
|
|
51
|
+
DOWNLOAD_POLL_INTERVAL_MIN,
|
|
52
|
+
DOWNLOAD_POLL_INTERVAL_MAX,
|
|
53
|
+
DRIVE_UPDATE_INTERVAL,
|
|
54
|
+
PROGRESS_THROTTLE,
|
|
55
|
+
DEFAULT_READ_LIMIT,
|
|
56
|
+
CHANNEL_NAME_MIN_LENGTH,
|
|
57
|
+
CHANNEL_NAME_MAX_LENGTH,
|
|
58
|
+
CHANNEL_NAME_REGEX,
|
|
59
|
+
CHANNEL_NAME_PREFIX,
|
|
60
|
+
CHANNEL_MESSAGE_LIMIT,
|
|
61
|
+
MAX_MESSAGE_LENGTH,
|
|
62
|
+
} from './config.js'
|
|
63
|
+
|
|
64
|
+
export class MostBoxEngine extends EventEmitter {
|
|
65
|
+
#store = null
|
|
66
|
+
#swarm = null
|
|
67
|
+
#drives = new Map()
|
|
68
|
+
#publishedFiles = []
|
|
69
|
+
#trashFiles = []
|
|
70
|
+
#initialized = false
|
|
71
|
+
#options = null
|
|
72
|
+
#activeDownloads = new Map()
|
|
73
|
+
#drivePromises = new Map()
|
|
74
|
+
|
|
75
|
+
#channels = []
|
|
76
|
+
#channelCores = new Map()
|
|
77
|
+
#channelDiscoveries = new Map()
|
|
78
|
+
#channelChatDiscoveries = new Map()
|
|
79
|
+
#channelPeers = new Map()
|
|
80
|
+
|
|
81
|
+
#chatSwarm = null
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 创建新的 MostBoxEngine 实例
|
|
85
|
+
* @param {object} options - 配置选项
|
|
86
|
+
* @param {string} options.dataPath - 存储 P2P 数据的路径(必填)
|
|
87
|
+
* @param {string} [options.downloadPath] - 默认下载路径(可选,默认为 dataPath/downloads)
|
|
88
|
+
* @param {number} [options.maxFileSize] - 最大文件大小(字节)(默认:100GB)
|
|
89
|
+
*/
|
|
90
|
+
constructor(options) {
|
|
91
|
+
super()
|
|
92
|
+
|
|
93
|
+
if (!options || !options.dataPath) {
|
|
94
|
+
throw new Error('dataPath is required')
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
this.#options = {
|
|
98
|
+
dataPath: options.dataPath,
|
|
99
|
+
downloadPath:
|
|
100
|
+
options.downloadPath || path.join(options.dataPath, 'downloads'),
|
|
101
|
+
maxFileSize: options.maxFileSize || MAX_FILE_SIZE,
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 初始化引擎 — 必须在调用其他方法之前调用
|
|
107
|
+
*/
|
|
108
|
+
async start() {
|
|
109
|
+
if (this.#initialized) {
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const { dataPath } = this.#options
|
|
114
|
+
|
|
115
|
+
console.log(`[MostBox] Initializing engine...`)
|
|
116
|
+
console.log(`[MostBox] Storage path: ${dataPath}`)
|
|
117
|
+
|
|
118
|
+
if (!fs.existsSync(dataPath)) {
|
|
119
|
+
fs.mkdirSync(dataPath, { recursive: true })
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const GLOBAL_SHARED_SEED = b4a.alloc(32).fill(GLOBAL_SHARED_SEED_STRING)
|
|
123
|
+
this.#store = new Corestore(dataPath, {
|
|
124
|
+
primaryKey: GLOBAL_SHARED_SEED,
|
|
125
|
+
unsafe: true,
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
await this.#store.ready()
|
|
130
|
+
console.log(`[MostBox] Corestore ready`)
|
|
131
|
+
} catch (err) {
|
|
132
|
+
if (
|
|
133
|
+
err.message &&
|
|
134
|
+
err.message.includes('Another corestore is stored here')
|
|
135
|
+
) {
|
|
136
|
+
console.log(`[MostBox] Resetting corrupt storage...`)
|
|
137
|
+
fs.rmSync(dataPath, { recursive: true, force: true })
|
|
138
|
+
fs.mkdirSync(dataPath, { recursive: true })
|
|
139
|
+
this.#store = new Corestore(dataPath, {
|
|
140
|
+
primaryKey: GLOBAL_SHARED_SEED,
|
|
141
|
+
unsafe: true,
|
|
142
|
+
})
|
|
143
|
+
await this.#store.ready()
|
|
144
|
+
console.log(`[MostBox] Corestore reset and ready`)
|
|
145
|
+
} else if (err.message && err.message.includes('Invalid device file')) {
|
|
146
|
+
throw new Error(`存储文件损坏,请关闭其他访问 ${dataPath} 的程序后重试`)
|
|
147
|
+
} else if (
|
|
148
|
+
err.message &&
|
|
149
|
+
err.message.includes('File descriptor could not be locked')
|
|
150
|
+
) {
|
|
151
|
+
throw new Error(
|
|
152
|
+
`存储文件被锁定,请关闭其他访问 ${dataPath} 的程序后重试`
|
|
153
|
+
)
|
|
154
|
+
} else {
|
|
155
|
+
throw err
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
console.log(`[MostBox] Initializing Hyperswarm...`)
|
|
160
|
+
this.#swarm = new Hyperswarm({
|
|
161
|
+
maxPeers: MAX_PEERS,
|
|
162
|
+
bootstrap: SWARM_BOOTSTRAP,
|
|
163
|
+
firewall: () => false,
|
|
164
|
+
connectionKeepAlive: SWARM_KEEP_ALIVE_INTERVAL,
|
|
165
|
+
randomPunchInterval: SWARM_RANDOM_PUNCH_INTERVAL,
|
|
166
|
+
handshakeTimeout: CONNECTION_TIMEOUT,
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
this.#swarm.on('error', err => {
|
|
170
|
+
if (
|
|
171
|
+
err.code === 'SSL_ERROR' ||
|
|
172
|
+
err.message?.includes('handshake') ||
|
|
173
|
+
err.message?.includes('ECONNRESET')
|
|
174
|
+
) {
|
|
175
|
+
console.warn('[MostBox] Network warning (non-critical):', err.message)
|
|
176
|
+
return
|
|
177
|
+
}
|
|
178
|
+
console.error('[MostBox] Swarm error:', err.message)
|
|
179
|
+
this.emit('error', err)
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
this.#swarm.on('connection', (conn, _info) => {
|
|
183
|
+
conn.on('error', err => {
|
|
184
|
+
if (err.code === 'SSL_ERROR' || err.message?.includes('handshake')) {
|
|
185
|
+
return
|
|
186
|
+
}
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
this.#store.replicate(conn)
|
|
190
|
+
this.emit('connection', conn)
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
this.#chatSwarm = new Hyperswarm({
|
|
194
|
+
maxPeers: MAX_PEERS,
|
|
195
|
+
bootstrap: SWARM_BOOTSTRAP,
|
|
196
|
+
firewall: () => false,
|
|
197
|
+
connectionKeepAlive: SWARM_KEEP_ALIVE_INTERVAL,
|
|
198
|
+
randomPunchInterval: SWARM_RANDOM_PUNCH_INTERVAL,
|
|
199
|
+
handshakeTimeout: CONNECTION_TIMEOUT,
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
this.#chatSwarm.on('error', err => {
|
|
203
|
+
if (
|
|
204
|
+
err.code === 'SSL_ERROR' ||
|
|
205
|
+
err.message?.includes('handshake') ||
|
|
206
|
+
err.message?.includes('ECONNRESET')
|
|
207
|
+
) {
|
|
208
|
+
console.warn(
|
|
209
|
+
'[MostBox] Chat swarm warning (non-critical):',
|
|
210
|
+
err.message
|
|
211
|
+
)
|
|
212
|
+
return
|
|
213
|
+
}
|
|
214
|
+
console.error('[MostBox] Chat swarm error:', err.message)
|
|
215
|
+
this.emit('error', err)
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
this.#chatSwarm.on('connection', (conn, _info) => {
|
|
219
|
+
conn.on('error', err => {
|
|
220
|
+
if (err.code === 'SSL_ERROR' || err.message?.includes('handshake')) {
|
|
221
|
+
return
|
|
222
|
+
}
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
this.#handleChannelConnection(conn).catch(() => {})
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
this.#publishedFiles = this.#loadPublishedMetadata()
|
|
229
|
+
console.log(
|
|
230
|
+
`[MostBox] Loaded ${this.#publishedFiles.length} published files`
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
this.#trashFiles = this.#loadTrashMetadata()
|
|
234
|
+
console.log(`[MostBox] Loaded ${this.#trashFiles.length} trash files`)
|
|
235
|
+
|
|
236
|
+
this.#channels = this.#loadChannelsMetadata()
|
|
237
|
+
console.log(`[MostBox] Loaded ${this.#channels.length} channels`)
|
|
238
|
+
|
|
239
|
+
for (const channel of this.#channels) {
|
|
240
|
+
try {
|
|
241
|
+
const ns = this.#store.namespace(`channel-${channel.name}`)
|
|
242
|
+
const core = ns.get({
|
|
243
|
+
key: b4a.from(channel.coreKey, 'hex'),
|
|
244
|
+
valueEncoding: 'json',
|
|
245
|
+
})
|
|
246
|
+
await core.ready()
|
|
247
|
+
this.#channelCores.set(channel.name, core)
|
|
248
|
+
this.#channelPeers.set(channel.name, new Map())
|
|
249
|
+
this.#setupChannelAppendListener(core, channel.name)
|
|
250
|
+
|
|
251
|
+
const discoveryKey = b4a.from(channel.discoveryKey, 'hex')
|
|
252
|
+
const chatDiscoveryKey = this.#generateChannelChatDiscoveryKey(
|
|
253
|
+
channel.name
|
|
254
|
+
)
|
|
255
|
+
const appDiscovery = this.#swarm.join(discoveryKey, {
|
|
256
|
+
server: true,
|
|
257
|
+
client: true,
|
|
258
|
+
})
|
|
259
|
+
this.#channelDiscoveries.set(channel.name, appDiscovery)
|
|
260
|
+
const chatDiscovery = this.#chatSwarm.join(chatDiscoveryKey, {
|
|
261
|
+
server: true,
|
|
262
|
+
client: true,
|
|
263
|
+
})
|
|
264
|
+
this.#channelChatDiscoveries.set(channel.name, chatDiscovery)
|
|
265
|
+
console.log(`[MostBox] Rejoined channel: ${channel.name}`)
|
|
266
|
+
} catch (err) {
|
|
267
|
+
console.warn(
|
|
268
|
+
`[MostBox] Failed to rejoin channel ${channel.name}:`,
|
|
269
|
+
err.message
|
|
270
|
+
)
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
this.#initialized = true
|
|
275
|
+
console.log(`[MostBox] Engine initialized successfully`)
|
|
276
|
+
this.emit('ready')
|
|
277
|
+
|
|
278
|
+
return this
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* 停止引擎并清理资源
|
|
283
|
+
*/
|
|
284
|
+
async stop() {
|
|
285
|
+
if (!this.#initialized) {
|
|
286
|
+
return
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
for (const task of this.#activeDownloads.values()) {
|
|
290
|
+
task.aborted = true
|
|
291
|
+
if (task.readStream) task.readStream.destroy()
|
|
292
|
+
if (task.writeStream) task.writeStream.destroy()
|
|
293
|
+
}
|
|
294
|
+
this.#activeDownloads.clear()
|
|
295
|
+
|
|
296
|
+
await Promise.allSettled([...this.#drives.values()].map(d => d.close()))
|
|
297
|
+
this.#drives.clear()
|
|
298
|
+
|
|
299
|
+
for (const core of this.#channelCores.values()) {
|
|
300
|
+
try {
|
|
301
|
+
await core.close()
|
|
302
|
+
} catch {}
|
|
303
|
+
}
|
|
304
|
+
this.#channelCores.clear()
|
|
305
|
+
this.#channelDiscoveries.clear()
|
|
306
|
+
this.#channelChatDiscoveries.clear()
|
|
307
|
+
this.#channelPeers.clear()
|
|
308
|
+
this.#channels = []
|
|
309
|
+
|
|
310
|
+
if (this.#swarm) {
|
|
311
|
+
await this.#swarm.destroy()
|
|
312
|
+
this.#swarm = null
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (this.#chatSwarm) {
|
|
316
|
+
await this.#chatSwarm.destroy()
|
|
317
|
+
this.#chatSwarm = null
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (this.#store) {
|
|
321
|
+
await this.#store.close()
|
|
322
|
+
this.#store = null
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
this.#initialized = false
|
|
326
|
+
this.emit('stopped')
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* 获取节点的公钥
|
|
331
|
+
* @returns {string} 节点 ID(十六进制字符串)
|
|
332
|
+
*/
|
|
333
|
+
getNodeId() {
|
|
334
|
+
this.#ensureInitialized()
|
|
335
|
+
return b4a.toString(this.#swarm.keyPair.publicKey, 'hex')
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* 获取当前网络状态
|
|
340
|
+
* @returns {{ peers: number, status: string }}
|
|
341
|
+
*/
|
|
342
|
+
getNetworkStatus() {
|
|
343
|
+
this.#ensureInitialized()
|
|
344
|
+
const appConnections = this.#swarm.connections.size
|
|
345
|
+
const chatConnections = this.#chatSwarm.connections.size
|
|
346
|
+
const total = appConnections + chatConnections
|
|
347
|
+
return {
|
|
348
|
+
peers: total,
|
|
349
|
+
appPeers: appConnections,
|
|
350
|
+
chatPeers: chatConnections,
|
|
351
|
+
status: total > 0 ? 'connected' : 'waiting',
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* 将内容发布到 P2P 网络
|
|
357
|
+
* Hyperdrive 中存储 key 为 '/' + cid,metadata 中存储 displayName(用户看到的路径)
|
|
358
|
+
* @param {string|Buffer} content - 文件路径(字符串)或内容(Buffer)
|
|
359
|
+
* @param {string} [fileName] - 文件名(Buffer 输入时必填)
|
|
360
|
+
* @returns {Promise<{ cid: string, link: string, fileName: string }>}
|
|
361
|
+
*/
|
|
362
|
+
async publishFile(content, fileName) {
|
|
363
|
+
this.#ensureInitialized()
|
|
364
|
+
|
|
365
|
+
let cleanPath = null
|
|
366
|
+
let safeFileName
|
|
367
|
+
let fileSize
|
|
368
|
+
|
|
369
|
+
if (Buffer.isBuffer(content)) {
|
|
370
|
+
if (!fileName) {
|
|
371
|
+
throw new Error('fileName is required when publishing Buffer content')
|
|
372
|
+
}
|
|
373
|
+
safeFileName = sanitizeFilename(fileName)
|
|
374
|
+
fileSize = content.length
|
|
375
|
+
} else {
|
|
376
|
+
cleanPath = content
|
|
377
|
+
const pathValidation = validateAndSanitizePath(cleanPath)
|
|
378
|
+
if (pathValidation.error) {
|
|
379
|
+
throw new PathSecurityError(pathValidation.error)
|
|
380
|
+
}
|
|
381
|
+
cleanPath = pathValidation.cleanPath
|
|
382
|
+
|
|
383
|
+
const sizeValidation = await validateFileSize(
|
|
384
|
+
cleanPath,
|
|
385
|
+
this.#options.maxFileSize
|
|
386
|
+
)
|
|
387
|
+
if (!sizeValidation.valid) {
|
|
388
|
+
throw new FileSizeError(sizeValidation.error, sizeValidation.size)
|
|
389
|
+
}
|
|
390
|
+
fileSize = sizeValidation.size
|
|
391
|
+
|
|
392
|
+
safeFileName = sanitizeFilename(fileName || path.basename(cleanPath))
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (fileSize > this.#options.maxFileSize) {
|
|
396
|
+
const maxGB = Math.round(this.#options.maxFileSize / (1024 * 1024 * 1024))
|
|
397
|
+
throw new FileSizeError(
|
|
398
|
+
`File size exceeds limit of ${maxGB} GB`,
|
|
399
|
+
fileSize
|
|
400
|
+
)
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
this.emit('publish:progress', {
|
|
404
|
+
stage: 'calculating-cid',
|
|
405
|
+
file: safeFileName,
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
const { cid: rootCid } = await calculateCid(content)
|
|
409
|
+
const cidString = rootCid.toString()
|
|
410
|
+
|
|
411
|
+
// 检查相同内容是否已存在
|
|
412
|
+
const existingIndex = this.#publishedFiles.findIndex(
|
|
413
|
+
f => f.cid === cidString
|
|
414
|
+
)
|
|
415
|
+
if (existingIndex !== -1) {
|
|
416
|
+
const existing = this.#publishedFiles[existingIndex]
|
|
417
|
+
return {
|
|
418
|
+
cid: cidString,
|
|
419
|
+
link: `most://${cidString}`,
|
|
420
|
+
fileName: existing.fileName,
|
|
421
|
+
alreadyExists: true,
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// 获取或创建该 CID 对应的 drive
|
|
426
|
+
const hashHex = b4a.toString(rootCid.multihash.digest, 'hex')
|
|
427
|
+
const name = `drive-${hashHex}`
|
|
428
|
+
let drive = this.#drives.get(name)
|
|
429
|
+
|
|
430
|
+
if (!drive) {
|
|
431
|
+
drive = await this.#getOrCreateDrive(name, {
|
|
432
|
+
server: true,
|
|
433
|
+
client: false,
|
|
434
|
+
})
|
|
435
|
+
const discovery = this.#swarm.join(drive.discoveryKey, {
|
|
436
|
+
server: true,
|
|
437
|
+
client: false,
|
|
438
|
+
})
|
|
439
|
+
await discovery.flushed()
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
this.emit('publish:progress', { stage: 'uploading', file: safeFileName })
|
|
443
|
+
|
|
444
|
+
// Hyperdrive 中用 CID 作为 key 存储(解耦目录结构)
|
|
445
|
+
const driveKey = '/' + cidString
|
|
446
|
+
|
|
447
|
+
const ws = drive.createWriteStream(driveKey)
|
|
448
|
+
|
|
449
|
+
if (Buffer.isBuffer(content)) {
|
|
450
|
+
let offset = 0
|
|
451
|
+
const waitForDrain = () =>
|
|
452
|
+
new Promise(resolve => ws.once('drain', resolve))
|
|
453
|
+
|
|
454
|
+
try {
|
|
455
|
+
while (offset < content.length) {
|
|
456
|
+
const chunk = content.slice(offset, offset + FILE_WRITE_CHUNK_SIZE)
|
|
457
|
+
const canContinue = ws.write(chunk)
|
|
458
|
+
offset += chunk.length
|
|
459
|
+
if (!canContinue && offset < content.length) {
|
|
460
|
+
await waitForDrain()
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
ws.end()
|
|
464
|
+
await new Promise((resolve, reject) => {
|
|
465
|
+
ws.on('finish', resolve)
|
|
466
|
+
ws.on('error', reject)
|
|
467
|
+
})
|
|
468
|
+
} catch (err) {
|
|
469
|
+
ws.destroy()
|
|
470
|
+
throw err
|
|
471
|
+
}
|
|
472
|
+
} else {
|
|
473
|
+
const rs = fs.createReadStream(cleanPath)
|
|
474
|
+
await new Promise((resolve, reject) => {
|
|
475
|
+
rs.pipe(ws)
|
|
476
|
+
ws.on('finish', resolve)
|
|
477
|
+
ws.on('error', reject)
|
|
478
|
+
rs.on('error', reject)
|
|
479
|
+
})
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// 存储 displayName(用户看到的文件夹路径),不存储 drivePath
|
|
483
|
+
this.#publishedFiles.push({
|
|
484
|
+
fileName: safeFileName,
|
|
485
|
+
cid: cidString,
|
|
486
|
+
driveName: name,
|
|
487
|
+
publishedAt: new Date().toISOString(),
|
|
488
|
+
starred: false,
|
|
489
|
+
})
|
|
490
|
+
this.#savePublishedMetadata()
|
|
491
|
+
|
|
492
|
+
const result = {
|
|
493
|
+
cid: cidString,
|
|
494
|
+
link: `most://${cidString}?filename=${encodeURIComponent(safeFileName)}`,
|
|
495
|
+
fileName: safeFileName,
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
this.emit('publish:success', result)
|
|
499
|
+
return result
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* 从 P2P 网络下载文件
|
|
504
|
+
* @param {string} link - most:// 链接
|
|
505
|
+
* @param {string} [taskId] - 用于取消的任务 ID
|
|
506
|
+
* @returns {Promise<{ taskId: string, fileName: string, savedPath: string, alreadyExists?: boolean }>}
|
|
507
|
+
*/
|
|
508
|
+
async downloadFile(link, taskId = null) {
|
|
509
|
+
this.#ensureInitialized()
|
|
510
|
+
|
|
511
|
+
taskId =
|
|
512
|
+
taskId || `dl_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
|
|
513
|
+
|
|
514
|
+
console.log(
|
|
515
|
+
`[MostBox] Starting download for link: ${link} (taskId: ${taskId})`
|
|
516
|
+
)
|
|
517
|
+
|
|
518
|
+
const taskState = { aborted: false, readStream: null, writeStream: null }
|
|
519
|
+
this.#activeDownloads.set(taskId, taskState)
|
|
520
|
+
|
|
521
|
+
try {
|
|
522
|
+
const parsed = parseMostLink(link)
|
|
523
|
+
if (parsed.error) {
|
|
524
|
+
throw new ValidationError(parsed.error)
|
|
525
|
+
}
|
|
526
|
+
const cidString = parsed.cid
|
|
527
|
+
console.log(`[MostBox] Parsed CID: ${cidString}`)
|
|
528
|
+
|
|
529
|
+
const existingFile = this.#publishedFiles.find(f => f.cid === cidString)
|
|
530
|
+
if (existingFile) {
|
|
531
|
+
console.log(`[MostBox] File already exists: ${existingFile.fileName}`)
|
|
532
|
+
return {
|
|
533
|
+
taskId,
|
|
534
|
+
fileName: existingFile.fileName,
|
|
535
|
+
alreadyExists: true,
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const linkFileName = parsed.fileName
|
|
540
|
+
|
|
541
|
+
const parsedCid = CID.parse(cidString)
|
|
542
|
+
const hashHex = b4a.toString(parsedCid.multihash.digest, 'hex')
|
|
543
|
+
|
|
544
|
+
if (taskState.aborted) throw new Error('Download cancelled')
|
|
545
|
+
|
|
546
|
+
const name = `drive-${hashHex}`
|
|
547
|
+
let drive = this.#drives.get(name)
|
|
548
|
+
|
|
549
|
+
if (!drive) {
|
|
550
|
+
console.log(`[MostBox] Creating new drive: ${name}`)
|
|
551
|
+
drive = await this.#getOrCreateDrive(name, {
|
|
552
|
+
server: true,
|
|
553
|
+
client: true,
|
|
554
|
+
})
|
|
555
|
+
|
|
556
|
+
this.emit('download:status', { taskId, status: 'connecting' })
|
|
557
|
+
|
|
558
|
+
console.log(`[MostBox] Joining swarm for drive discovery...`)
|
|
559
|
+
await this.#swarm
|
|
560
|
+
.join(drive.discoveryKey, { server: true, client: true })
|
|
561
|
+
.flushed()
|
|
562
|
+
console.log(`[MostBox] Swarm join flushed`)
|
|
563
|
+
} else {
|
|
564
|
+
console.log(`[MostBox] Using existing drive: ${name}`)
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
if (taskState.aborted) throw new Error('Download cancelled')
|
|
568
|
+
|
|
569
|
+
this.emit('download:status', { taskId, status: 'finding-peers' })
|
|
570
|
+
|
|
571
|
+
console.log(
|
|
572
|
+
`[MostBox] Waiting for drive content (timeout: ${DOWNLOAD_TIMEOUT / 1000}s)...`
|
|
573
|
+
)
|
|
574
|
+
const entries = await this.#waitForDriveContent(
|
|
575
|
+
drive,
|
|
576
|
+
DOWNLOAD_TIMEOUT,
|
|
577
|
+
taskId,
|
|
578
|
+
taskState
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
if (entries.length === 0) {
|
|
582
|
+
console.log(`[MostBox] No entries found after timeout`)
|
|
583
|
+
|
|
584
|
+
const peerCount = this.#swarm.connections.size
|
|
585
|
+
let errorMessage = 'No files found in drive. '
|
|
586
|
+
|
|
587
|
+
if (peerCount === 0) {
|
|
588
|
+
errorMessage +=
|
|
589
|
+
'Could not connect to any peers. This may be due to:\n'
|
|
590
|
+
errorMessage += '1. Network firewall blocking P2P connections\n'
|
|
591
|
+
errorMessage += '2. DHT bootstrap nodes unreachable\n'
|
|
592
|
+
errorMessage += '3. NAT traversal failed (try port forwarding)\n'
|
|
593
|
+
errorMessage += '4. No peers are currently sharing this file'
|
|
594
|
+
} else {
|
|
595
|
+
errorMessage += `Connected to ${peerCount} peers but no file data was found. This may be due to:\n`
|
|
596
|
+
errorMessage += '1. Publisher node offline\n'
|
|
597
|
+
errorMessage += '2. File may have been removed by publisher\n'
|
|
598
|
+
errorMessage += '3. File link may be invalid or corrupted'
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
throw new PeerNotFoundError(errorMessage)
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
if (taskState.aborted) throw new Error('Download cancelled')
|
|
605
|
+
|
|
606
|
+
console.log(
|
|
607
|
+
`[MostBox] Found ${entries.length} entries, starting download...`
|
|
608
|
+
)
|
|
609
|
+
|
|
610
|
+
const targetDir = this.#options.dataPath
|
|
611
|
+
|
|
612
|
+
const writableCheck = await checkDirectoryWritable(targetDir)
|
|
613
|
+
if (!writableCheck.writable) {
|
|
614
|
+
throw new PermissionError(writableCheck.error)
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// 下载文件
|
|
618
|
+
for (const entry of entries) {
|
|
619
|
+
const cleanKey = entry.key.replace(/^[\/\\]/, '')
|
|
620
|
+
const sanitizedFileName = linkFileName
|
|
621
|
+
? sanitizeFilename(linkFileName)
|
|
622
|
+
: sanitizeFilename(cleanKey)
|
|
623
|
+
|
|
624
|
+
let totalBytes = 0
|
|
625
|
+
try {
|
|
626
|
+
const stat = await drive.entry(entry.key)
|
|
627
|
+
if (stat && stat.value && stat.value.blob) {
|
|
628
|
+
totalBytes = stat.value.blob.byteLength || 0
|
|
629
|
+
}
|
|
630
|
+
} catch {
|
|
631
|
+
// 忽略
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
const savePath = path.join(targetDir, sanitizedFileName)
|
|
635
|
+
|
|
636
|
+
this.emit('download:status', {
|
|
637
|
+
taskId,
|
|
638
|
+
status: 'downloading',
|
|
639
|
+
file: sanitizedFileName,
|
|
640
|
+
size: totalBytes ? formatFileSize(totalBytes) : null,
|
|
641
|
+
})
|
|
642
|
+
|
|
643
|
+
const rs = drive.createReadStream(entry.key)
|
|
644
|
+
const ws = fs.createWriteStream(savePath)
|
|
645
|
+
|
|
646
|
+
taskState.readStream = rs
|
|
647
|
+
taskState.writeStream = ws
|
|
648
|
+
|
|
649
|
+
let loadedBytes = 0
|
|
650
|
+
let lastProgressUpdate = 0
|
|
651
|
+
|
|
652
|
+
await new Promise((resolve, reject) => {
|
|
653
|
+
rs.on('data', chunk => {
|
|
654
|
+
if (taskState.aborted) {
|
|
655
|
+
rs.destroy()
|
|
656
|
+
ws.destroy()
|
|
657
|
+
fs.unlink(savePath, () => {})
|
|
658
|
+
reject(new Error('Download cancelled'))
|
|
659
|
+
return
|
|
660
|
+
}
|
|
661
|
+
loadedBytes += chunk.length
|
|
662
|
+
const now = Date.now()
|
|
663
|
+
if (
|
|
664
|
+
totalBytes > 0 &&
|
|
665
|
+
now - lastProgressUpdate > PROGRESS_THROTTLE
|
|
666
|
+
) {
|
|
667
|
+
lastProgressUpdate = now
|
|
668
|
+
const percent = Math.round((loadedBytes / totalBytes) * 100)
|
|
669
|
+
this.emit('download:progress', {
|
|
670
|
+
taskId,
|
|
671
|
+
loaded: loadedBytes,
|
|
672
|
+
total: totalBytes,
|
|
673
|
+
percent,
|
|
674
|
+
})
|
|
675
|
+
}
|
|
676
|
+
})
|
|
677
|
+
|
|
678
|
+
rs.pipe(ws)
|
|
679
|
+
ws.on('finish', resolve)
|
|
680
|
+
ws.on('error', reject)
|
|
681
|
+
rs.on('error', reject)
|
|
682
|
+
})
|
|
683
|
+
|
|
684
|
+
if (taskState.aborted) throw new Error('Download cancelled')
|
|
685
|
+
|
|
686
|
+
this.emit('download:status', { taskId, status: 'verifying' })
|
|
687
|
+
|
|
688
|
+
const { cid: downloadedCid } = await calculateCid(savePath)
|
|
689
|
+
const expectedHash = b4a.toString(parsedCid.multihash.digest, 'hex')
|
|
690
|
+
const actualHash = b4a.toString(downloadedCid.multihash.digest, 'hex')
|
|
691
|
+
|
|
692
|
+
if (expectedHash !== actualHash) {
|
|
693
|
+
fs.unlinkSync(savePath)
|
|
694
|
+
throw new IntegrityError(
|
|
695
|
+
`File content CID mismatch. File may be corrupted or tampered.`
|
|
696
|
+
)
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// Write file content to Hyperdrive for seeding to other peers
|
|
700
|
+
const driveKey = '/' + cidString
|
|
701
|
+
const existingEntry = await drive.entry(driveKey)
|
|
702
|
+
if (!existingEntry) {
|
|
703
|
+
const readStream = fs.createReadStream(savePath)
|
|
704
|
+
const writeStream = drive.createWriteStream(driveKey)
|
|
705
|
+
await new Promise((resolve, reject) => {
|
|
706
|
+
readStream.pipe(writeStream)
|
|
707
|
+
writeStream.on('finish', resolve)
|
|
708
|
+
writeStream.on('error', reject)
|
|
709
|
+
readStream.on('error', reject)
|
|
710
|
+
})
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
const result = {
|
|
714
|
+
taskId,
|
|
715
|
+
fileName: sanitizedFileName,
|
|
716
|
+
savedPath: savePath,
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// 将下载的文件添加到已发布文件列表(displayName 用原始文件名)
|
|
720
|
+
const existingIndex = this.#publishedFiles.findIndex(
|
|
721
|
+
f => f.cid === cidString
|
|
722
|
+
)
|
|
723
|
+
if (existingIndex !== -1) {
|
|
724
|
+
const existing = this.#publishedFiles[existingIndex]
|
|
725
|
+
if (existing.fileName !== sanitizedFileName) {
|
|
726
|
+
throw new Error(`文件已存在: ${existing.fileName}`)
|
|
727
|
+
}
|
|
728
|
+
existing.publishedAt = new Date().toISOString()
|
|
729
|
+
} else {
|
|
730
|
+
this.#publishedFiles.push({
|
|
731
|
+
fileName: sanitizedFileName,
|
|
732
|
+
cid: cidString,
|
|
733
|
+
driveName: name,
|
|
734
|
+
publishedAt: new Date().toISOString(),
|
|
735
|
+
starred: false,
|
|
736
|
+
})
|
|
737
|
+
}
|
|
738
|
+
this.#savePublishedMetadata()
|
|
739
|
+
|
|
740
|
+
this.emit('download:success', result)
|
|
741
|
+
return result
|
|
742
|
+
}
|
|
743
|
+
} finally {
|
|
744
|
+
this.#activeDownloads.delete(taskId)
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/**
|
|
749
|
+
* 列出所有已发布文件
|
|
750
|
+
* @param {object} [options] - 筛选选项
|
|
751
|
+
* @param {boolean} [options.starred] - 按收藏状态筛选
|
|
752
|
+
* @returns {Array<{ fileName: string, cid: string, link: string, publishedAt: string, starred: boolean }>}
|
|
753
|
+
*/
|
|
754
|
+
listPublishedFiles(options = {}) {
|
|
755
|
+
this.#ensureInitialized()
|
|
756
|
+
let files = this.#publishedFiles
|
|
757
|
+
|
|
758
|
+
if (options.starred === true) {
|
|
759
|
+
files = files.filter(f => f.starred === true)
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
return files.map(f => ({
|
|
763
|
+
fileName: f.fileName,
|
|
764
|
+
cid: f.cid,
|
|
765
|
+
link: `most://${f.cid}`,
|
|
766
|
+
publishedAt: f.publishedAt,
|
|
767
|
+
starred: f.starred || false,
|
|
768
|
+
}))
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/**
|
|
772
|
+
* 切换文件的收藏状态
|
|
773
|
+
* @param {string} cid - 文件的 CID
|
|
774
|
+
* @returns {object} 更新后的文件信息
|
|
775
|
+
*/
|
|
776
|
+
toggleStarred(cid) {
|
|
777
|
+
this.#ensureInitialized()
|
|
778
|
+
const index = this.#publishedFiles.findIndex(f => f.cid === cid)
|
|
779
|
+
if (index === -1) {
|
|
780
|
+
throw new Error('File not found')
|
|
781
|
+
}
|
|
782
|
+
this.#publishedFiles[index].starred = !this.#publishedFiles[index].starred
|
|
783
|
+
this.#savePublishedMetadata()
|
|
784
|
+
return {
|
|
785
|
+
cid,
|
|
786
|
+
starred: this.#publishedFiles[index].starred,
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
/**
|
|
791
|
+
* 删除已发布文件 — 移至回收站而非永久删除
|
|
792
|
+
* @param {string} cid - 要删除文件的 CID
|
|
793
|
+
* @returns {Promise<Array>} 更新后的已发布文件列表
|
|
794
|
+
*/
|
|
795
|
+
async deletePublishedFile(cid) {
|
|
796
|
+
this.#ensureInitialized()
|
|
797
|
+
const index = this.#publishedFiles.findIndex(f => f.cid === cid)
|
|
798
|
+
if (index !== -1) {
|
|
799
|
+
const fileRecord = this.#publishedFiles[index]
|
|
800
|
+
|
|
801
|
+
this.#trashFiles.push({
|
|
802
|
+
fileName: fileRecord.fileName,
|
|
803
|
+
cid: fileRecord.cid,
|
|
804
|
+
driveName: fileRecord.driveName,
|
|
805
|
+
publishedAt: fileRecord.publishedAt,
|
|
806
|
+
starred: fileRecord.starred || false,
|
|
807
|
+
deletedAt: new Date().toISOString(),
|
|
808
|
+
})
|
|
809
|
+
this.#saveTrashMetadata()
|
|
810
|
+
|
|
811
|
+
this.#publishedFiles.splice(index, 1)
|
|
812
|
+
this.#savePublishedMetadata()
|
|
813
|
+
}
|
|
814
|
+
return this.listPublishedFiles()
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/**
|
|
818
|
+
* 列出回收站中的所有文件
|
|
819
|
+
* @returns {Array} 回收站文件
|
|
820
|
+
*/
|
|
821
|
+
listTrashFiles() {
|
|
822
|
+
this.#ensureInitialized()
|
|
823
|
+
return this.#trashFiles.map(f => ({
|
|
824
|
+
fileName: f.fileName,
|
|
825
|
+
cid: f.cid,
|
|
826
|
+
link: `most://${f.cid}`,
|
|
827
|
+
publishedAt: f.publishedAt,
|
|
828
|
+
starred: f.starred || false,
|
|
829
|
+
deletedAt: f.deletedAt,
|
|
830
|
+
}))
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
/**
|
|
834
|
+
* 从回收站恢复文件
|
|
835
|
+
* @param {string} cid - 要恢复文件的 CID
|
|
836
|
+
* @returns {Array} 更新后的已发布文件列表
|
|
837
|
+
*/
|
|
838
|
+
restoreTrashFile(cid) {
|
|
839
|
+
this.#ensureInitialized()
|
|
840
|
+
const index = this.#trashFiles.findIndex(f => f.cid === cid)
|
|
841
|
+
if (index === -1) {
|
|
842
|
+
throw new Error('File not found in trash')
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
const fileRecord = this.#trashFiles[index]
|
|
846
|
+
|
|
847
|
+
const parsedCid = CID.parse(fileRecord.cid)
|
|
848
|
+
const hashHex = b4a.toString(parsedCid.multihash.digest, 'hex')
|
|
849
|
+
const driveName = `drive-${hashHex}`
|
|
850
|
+
|
|
851
|
+
this.#publishedFiles.push({
|
|
852
|
+
fileName: fileRecord.fileName,
|
|
853
|
+
cid: fileRecord.cid,
|
|
854
|
+
driveName,
|
|
855
|
+
publishedAt: fileRecord.publishedAt,
|
|
856
|
+
starred: fileRecord.starred || false,
|
|
857
|
+
})
|
|
858
|
+
this.#savePublishedMetadata()
|
|
859
|
+
|
|
860
|
+
this.#trashFiles.splice(index, 1)
|
|
861
|
+
this.#saveTrashMetadata()
|
|
862
|
+
|
|
863
|
+
return this.listPublishedFiles()
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
/**
|
|
867
|
+
* 永久删除回收站中的文件
|
|
868
|
+
* @param {string} cid - 要永久删除文件的 CID
|
|
869
|
+
* @returns {Promise<Array>} 更新后的回收站列表
|
|
870
|
+
*/
|
|
871
|
+
async permanentDeleteTrashFile(cid) {
|
|
872
|
+
this.#ensureInitialized()
|
|
873
|
+
const index = this.#trashFiles.findIndex(f => f.cid === cid)
|
|
874
|
+
if (index !== -1) {
|
|
875
|
+
const fileRecord = this.#trashFiles[index]
|
|
876
|
+
const driveName = fileRecord.driveName
|
|
877
|
+
|
|
878
|
+
const drive = this.#drives.get(driveName)
|
|
879
|
+
if (drive) {
|
|
880
|
+
try {
|
|
881
|
+
await drive.del('/' + fileRecord.cid)
|
|
882
|
+
} catch {
|
|
883
|
+
// 文件可能不存在于驱动器中
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
await this.#swarm.leave(drive.discoveryKey)
|
|
887
|
+
await drive.close()
|
|
888
|
+
this.#drives.delete(driveName)
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
this.#trashFiles.splice(index, 1)
|
|
892
|
+
this.#saveTrashMetadata()
|
|
893
|
+
}
|
|
894
|
+
return this.listTrashFiles()
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
/**
|
|
898
|
+
* 清空回收站 — 永久删除所有回收站文件
|
|
899
|
+
* @returns {Promise<Array>} 清空后的回收站列表
|
|
900
|
+
*/
|
|
901
|
+
async emptyTrash() {
|
|
902
|
+
this.#ensureInitialized()
|
|
903
|
+
|
|
904
|
+
for (const fileRecord of this.#trashFiles) {
|
|
905
|
+
const driveName = fileRecord.driveName
|
|
906
|
+
|
|
907
|
+
const drive = this.#drives.get(driveName)
|
|
908
|
+
if (drive) {
|
|
909
|
+
try {
|
|
910
|
+
await drive.del('/' + fileRecord.cid)
|
|
911
|
+
} catch {
|
|
912
|
+
// 文件可能不存在
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
await this.#swarm.leave(drive.discoveryKey)
|
|
916
|
+
await drive.close()
|
|
917
|
+
this.#drives.delete(driveName)
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
this.#trashFiles = []
|
|
922
|
+
this.#saveTrashMetadata()
|
|
923
|
+
|
|
924
|
+
return []
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
/**
|
|
928
|
+
* 获取存储统计信息
|
|
929
|
+
* @returns {Promise<{ total: number, used: number, free: number, fileCount: number, trashCount: number }>}
|
|
930
|
+
*/
|
|
931
|
+
async getStorageStats() {
|
|
932
|
+
this.#ensureInitialized()
|
|
933
|
+
|
|
934
|
+
let totalSize = 0
|
|
935
|
+
let freeSize = 0
|
|
936
|
+
const { dataPath } = this.#options
|
|
937
|
+
|
|
938
|
+
try {
|
|
939
|
+
const stats = fs.statfsSync(dataPath)
|
|
940
|
+
totalSize = stats.bsize * stats.blocks
|
|
941
|
+
freeSize = stats.bsize * stats.bfree
|
|
942
|
+
} catch {
|
|
943
|
+
try {
|
|
944
|
+
fs.statSync(dataPath)
|
|
945
|
+
totalSize = 0
|
|
946
|
+
freeSize = 0
|
|
947
|
+
} catch {
|
|
948
|
+
totalSize = 0
|
|
949
|
+
freeSize = 0
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
let usedSize = 0
|
|
954
|
+
const calculateDirSize = dirPath => {
|
|
955
|
+
try {
|
|
956
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true })
|
|
957
|
+
for (const entry of entries) {
|
|
958
|
+
const fullPath = path.join(dirPath, entry.name)
|
|
959
|
+
if (entry.isDirectory()) {
|
|
960
|
+
if (entry.name !== 'db') {
|
|
961
|
+
calculateDirSize(fullPath)
|
|
962
|
+
}
|
|
963
|
+
} else {
|
|
964
|
+
try {
|
|
965
|
+
const stat = fs.statSync(fullPath)
|
|
966
|
+
usedSize += stat.size
|
|
967
|
+
} catch {
|
|
968
|
+
// 跳过无法访问的文件
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
} catch {
|
|
973
|
+
// 跳过无法访问的目录
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
calculateDirSize(dataPath)
|
|
978
|
+
|
|
979
|
+
return {
|
|
980
|
+
total: totalSize,
|
|
981
|
+
used: usedSize,
|
|
982
|
+
free: freeSize,
|
|
983
|
+
fileCount: this.#publishedFiles.length,
|
|
984
|
+
trashCount: this.#trashFiles.length,
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
/**
|
|
989
|
+
* 移动/重命名已发布文件
|
|
990
|
+
* 只更新 metadata 中的 displayName,不修改 Hyperdrive
|
|
991
|
+
* @param {string} cid - 要移动文件的 CID
|
|
992
|
+
* @param {string} newFileName - 新文件路径
|
|
993
|
+
* @returns {object} 更新后的文件信息
|
|
994
|
+
*/
|
|
995
|
+
moveFile(cid, newFileName) {
|
|
996
|
+
this.#ensureInitialized()
|
|
997
|
+
const index = this.#publishedFiles.findIndex(f => f.cid === cid)
|
|
998
|
+
if (index === -1) {
|
|
999
|
+
throw new Error('File not found')
|
|
1000
|
+
}
|
|
1001
|
+
const safeFileName = sanitizeFilename(newFileName)
|
|
1002
|
+
this.#publishedFiles[index].fileName = safeFileName
|
|
1003
|
+
this.#publishedFiles[index].publishedAt = new Date().toISOString()
|
|
1004
|
+
this.#savePublishedMetadata()
|
|
1005
|
+
return {
|
|
1006
|
+
cid,
|
|
1007
|
+
fileName: safeFileName,
|
|
1008
|
+
link: `most://${cid}`,
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
/**
|
|
1013
|
+
* 重命名文件夹(重命名文件夹内的所有文件 displayName)
|
|
1014
|
+
* 只更新 metadata,不修改 Hyperdrive
|
|
1015
|
+
* @param {string} oldPath - 当前文件夹路径
|
|
1016
|
+
* @param {string} newPath - 新文件夹路径
|
|
1017
|
+
* @returns {object} 更新后的文件信息
|
|
1018
|
+
*/
|
|
1019
|
+
renameFolder(oldPath, newPath) {
|
|
1020
|
+
this.#ensureInitialized()
|
|
1021
|
+
const prefix = oldPath + '/'
|
|
1022
|
+
const updatedFiles = []
|
|
1023
|
+
|
|
1024
|
+
for (const file of this.#publishedFiles) {
|
|
1025
|
+
if (file.fileName.startsWith(prefix)) {
|
|
1026
|
+
const remainder = file.fileName.substring(prefix.length)
|
|
1027
|
+
const newFileName = sanitizeFilename(
|
|
1028
|
+
remainder ? newPath + '/' + remainder : newPath
|
|
1029
|
+
)
|
|
1030
|
+
file.fileName = newFileName
|
|
1031
|
+
file.publishedAt = new Date().toISOString()
|
|
1032
|
+
updatedFiles.push({
|
|
1033
|
+
cid: file.cid,
|
|
1034
|
+
fileName: file.fileName,
|
|
1035
|
+
link: `most://${file.cid}`,
|
|
1036
|
+
})
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
if (updatedFiles.length > 0) {
|
|
1041
|
+
this.#savePublishedMetadata()
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
return { files: updatedFiles }
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
/**
|
|
1048
|
+
* 取消正在进行的下载
|
|
1049
|
+
* @param {string} taskId - 要取消下载的任务 ID
|
|
1050
|
+
*/
|
|
1051
|
+
cancelDownload(taskId) {
|
|
1052
|
+
const task = this.#activeDownloads.get(taskId)
|
|
1053
|
+
if (task) {
|
|
1054
|
+
task.aborted = true
|
|
1055
|
+
if (task.readStream) task.readStream.destroy()
|
|
1056
|
+
if (task.writeStream) task.writeStream.destroy()
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
getPublishedFiles() {
|
|
1061
|
+
return this.#publishedFiles
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
/**
|
|
1065
|
+
* 读取已发布文件的内容(用于预览)
|
|
1066
|
+
* Hyperdrive 中用 CID 作为 key 存储
|
|
1067
|
+
* @param {string} cid - 文件的 CID
|
|
1068
|
+
* @param {number} [offset=0] - 读取起始位置
|
|
1069
|
+
* @param {number} [limit=10000] - 最大读取字节数
|
|
1070
|
+
*/
|
|
1071
|
+
async readFileContent(cid, offset = 0, limit = DEFAULT_READ_LIMIT) {
|
|
1072
|
+
this.#ensureInitialized()
|
|
1073
|
+
|
|
1074
|
+
const fileRecord = this.#publishedFiles.find(f => f.cid === cid)
|
|
1075
|
+
if (!fileRecord) {
|
|
1076
|
+
throw new Error('File not found')
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
const drive = await this.#getDriveForFile(fileRecord)
|
|
1080
|
+
|
|
1081
|
+
// Hyperdrive 中 key 为 '/' + cid
|
|
1082
|
+
const driveKey = '/' + cid
|
|
1083
|
+
const entry = await drive.entry(driveKey, {
|
|
1084
|
+
wait: true,
|
|
1085
|
+
timeout: DRIVE_ENTRY_TIMEOUT,
|
|
1086
|
+
})
|
|
1087
|
+
if (!entry || !entry.value) {
|
|
1088
|
+
throw new Error('File content not available')
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
const chunks = []
|
|
1092
|
+
const stream = drive.createReadStream(driveKey, {
|
|
1093
|
+
start: offset,
|
|
1094
|
+
end: offset + limit - 1,
|
|
1095
|
+
})
|
|
1096
|
+
|
|
1097
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
1098
|
+
setTimeout(
|
|
1099
|
+
() => reject(new Error('Stream read timeout')),
|
|
1100
|
+
STREAM_READ_TIMEOUT
|
|
1101
|
+
)
|
|
1102
|
+
})
|
|
1103
|
+
|
|
1104
|
+
const readPromise = (async () => {
|
|
1105
|
+
for await (const chunk of stream) {
|
|
1106
|
+
chunks.push(chunk)
|
|
1107
|
+
}
|
|
1108
|
+
})()
|
|
1109
|
+
|
|
1110
|
+
await Promise.race([readPromise, timeoutPromise])
|
|
1111
|
+
|
|
1112
|
+
const content = Buffer.concat(chunks).toString('utf8')
|
|
1113
|
+
const hasMore =
|
|
1114
|
+
chunks.length > 0 && chunks[chunks.length - 1].length === limit
|
|
1115
|
+
|
|
1116
|
+
return { content, hasMore }
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
/**
|
|
1120
|
+
* 读取已发布文件的原始内容(用于预览/下载)
|
|
1121
|
+
* Hyperdrive 中用 CID 作为 key 存储
|
|
1122
|
+
* @param {string} cid - 文件的 CID
|
|
1123
|
+
* @param {object} [options] - 选项
|
|
1124
|
+
* @param {number} [options.offset=0] - 读取起始位置
|
|
1125
|
+
* @param {number} [options.limit] - 最大读取字节数,不指定则读取到末尾
|
|
1126
|
+
* @param {number} [options.timeout=10000] - 流读取超时(毫秒)
|
|
1127
|
+
* @returns {Promise<{buffer: Buffer, fileName: string, totalSize: number}>}
|
|
1128
|
+
*/
|
|
1129
|
+
async readFileRaw(cid, options = {}) {
|
|
1130
|
+
this.#ensureInitialized()
|
|
1131
|
+
|
|
1132
|
+
const fileRecord = this.#publishedFiles.find(f => f.cid === cid)
|
|
1133
|
+
if (!fileRecord) {
|
|
1134
|
+
throw new Error('File not found')
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
const drive = await this.#getDriveForFile(fileRecord)
|
|
1138
|
+
|
|
1139
|
+
const driveKey = '/' + cid
|
|
1140
|
+
const entry = await drive.entry(driveKey, {
|
|
1141
|
+
wait: true,
|
|
1142
|
+
timeout: DRIVE_ENTRY_TIMEOUT,
|
|
1143
|
+
})
|
|
1144
|
+
if (!entry || !entry.value || !entry.value.blob) {
|
|
1145
|
+
throw new Error('File content not available')
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
const totalSize = entry.value.blob.byteLength || 0
|
|
1149
|
+
|
|
1150
|
+
const { offset = 0, limit, timeout = STREAM_READ_TIMEOUT } = options
|
|
1151
|
+
const effectiveLimit =
|
|
1152
|
+
limit === undefined || limit === null
|
|
1153
|
+
? totalSize - offset
|
|
1154
|
+
: Math.min(limit, totalSize - offset)
|
|
1155
|
+
|
|
1156
|
+
if (effectiveLimit <= 0) {
|
|
1157
|
+
return {
|
|
1158
|
+
buffer: Buffer.alloc(0),
|
|
1159
|
+
fileName: fileRecord.fileName,
|
|
1160
|
+
totalSize,
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
const chunks = []
|
|
1165
|
+
const stream = drive.createReadStream(driveKey, {
|
|
1166
|
+
start: offset,
|
|
1167
|
+
end: offset + effectiveLimit - 1,
|
|
1168
|
+
})
|
|
1169
|
+
|
|
1170
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
1171
|
+
setTimeout(() => reject(new Error('Stream read timeout')), timeout)
|
|
1172
|
+
})
|
|
1173
|
+
|
|
1174
|
+
const readPromise = (async () => {
|
|
1175
|
+
try {
|
|
1176
|
+
for await (const chunk of stream) {
|
|
1177
|
+
chunks.push(chunk)
|
|
1178
|
+
}
|
|
1179
|
+
} catch (err) {
|
|
1180
|
+
if (err.message !== 'Stream read timeout') {
|
|
1181
|
+
throw err
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
})()
|
|
1185
|
+
|
|
1186
|
+
await Promise.race([readPromise, timeoutPromise])
|
|
1187
|
+
await readPromise.catch(() => {})
|
|
1188
|
+
|
|
1189
|
+
const buffer = Buffer.concat(chunks)
|
|
1190
|
+
return { buffer, fileName: fileRecord.fileName, totalSize }
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
/**
|
|
1194
|
+
* 获取文件对应的 drive,如果不存在则创建并同步
|
|
1195
|
+
*/
|
|
1196
|
+
async #getDriveForFile(fileRecord) {
|
|
1197
|
+
let drive = this.#drives.get(fileRecord.driveName)
|
|
1198
|
+
if (!drive) {
|
|
1199
|
+
drive = await this.#getOrCreateDrive(fileRecord.driveName, {
|
|
1200
|
+
server: true,
|
|
1201
|
+
client: true,
|
|
1202
|
+
})
|
|
1203
|
+
}
|
|
1204
|
+
await this.#syncDrive(drive)
|
|
1205
|
+
return drive
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
// --- 频道管理 ---
|
|
1209
|
+
|
|
1210
|
+
/**
|
|
1211
|
+
* 创建或加入频道
|
|
1212
|
+
* @param {string} name - 频道名
|
|
1213
|
+
* @param {string} [type='personal'] - 频道类型
|
|
1214
|
+
* @returns {Promise<{ name: string, key: string }>}
|
|
1215
|
+
*/
|
|
1216
|
+
async createChannel(name, type = 'personal') {
|
|
1217
|
+
this.#ensureInitialized()
|
|
1218
|
+
|
|
1219
|
+
if (!CHANNEL_NAME_REGEX.test(name)) {
|
|
1220
|
+
throw new Error('频道名只能包含字母、数字、下划线和连字符')
|
|
1221
|
+
}
|
|
1222
|
+
if (name.length < CHANNEL_NAME_MIN_LENGTH) {
|
|
1223
|
+
throw new Error(`频道名至少 ${CHANNEL_NAME_MIN_LENGTH} 个字符`)
|
|
1224
|
+
}
|
|
1225
|
+
if (name.length > CHANNEL_NAME_MAX_LENGTH) {
|
|
1226
|
+
throw new Error(`频道名最多 ${CHANNEL_NAME_MAX_LENGTH} 个字符`)
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
const existing = this.#channels.find(c => c.name === name)
|
|
1230
|
+
if (existing) {
|
|
1231
|
+
return { name: existing.name, key: existing.coreKey }
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
const ns = this.#store.namespace(`channel-${name}`)
|
|
1235
|
+
const core = ns.get({ name: 'messages', valueEncoding: 'json' })
|
|
1236
|
+
await core.ready()
|
|
1237
|
+
|
|
1238
|
+
const discoveryKey = this.#generateChannelDiscoveryKey(name)
|
|
1239
|
+
const chatDiscoveryKey = this.#generateChannelChatDiscoveryKey(name)
|
|
1240
|
+
const appDiscovery = this.#swarm.join(discoveryKey, {
|
|
1241
|
+
server: true,
|
|
1242
|
+
client: true,
|
|
1243
|
+
})
|
|
1244
|
+
await appDiscovery.flushed()
|
|
1245
|
+
const chatDiscovery = this.#chatSwarm.join(chatDiscoveryKey, {
|
|
1246
|
+
server: true,
|
|
1247
|
+
client: true,
|
|
1248
|
+
})
|
|
1249
|
+
await chatDiscovery.flushed()
|
|
1250
|
+
|
|
1251
|
+
this.#setupChannelAppendListener(core, name)
|
|
1252
|
+
|
|
1253
|
+
const channelInfo = {
|
|
1254
|
+
name,
|
|
1255
|
+
discoveryKey: b4a.toString(discoveryKey, 'hex'),
|
|
1256
|
+
coreKey: b4a.toString(core.key, 'hex'),
|
|
1257
|
+
createdAt: new Date().toISOString(),
|
|
1258
|
+
type,
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
this.#channels.push(channelInfo)
|
|
1262
|
+
this.#channelCores.set(name, core)
|
|
1263
|
+
this.#channelPeers.set(name, new Map())
|
|
1264
|
+
this.#channelDiscoveries.set(name, appDiscovery)
|
|
1265
|
+
this.#channelChatDiscoveries.set(name, chatDiscovery)
|
|
1266
|
+
this.#saveChannelsMetadata()
|
|
1267
|
+
|
|
1268
|
+
console.log(`[MostBox] Channel created: ${name}`)
|
|
1269
|
+
this.emit('channel:joined', { name, key: channelInfo.coreKey })
|
|
1270
|
+
|
|
1271
|
+
return { name, key: channelInfo.coreKey }
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
/**
|
|
1275
|
+
* 加入已有频道(通过频道名和 coreKey)
|
|
1276
|
+
* @param {string} name - 频道名
|
|
1277
|
+
* @param {string} [coreKey] - 频道的 coreKey(加入他人创建的频道时必填)
|
|
1278
|
+
* @returns {Promise<{ name: string, key: string }>}
|
|
1279
|
+
*/
|
|
1280
|
+
async joinChannel(name, coreKey = null) {
|
|
1281
|
+
this.#ensureInitialized()
|
|
1282
|
+
|
|
1283
|
+
const existing = this.#channels.find(c => c.name === name)
|
|
1284
|
+
if (existing) {
|
|
1285
|
+
return { name: existing.name, key: existing.coreKey }
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
if (!coreKey) {
|
|
1289
|
+
throw new Error('加入已有频道需要提供 coreKey')
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
const ns = this.#store.namespace(`channel-${name}`)
|
|
1293
|
+
const core = ns.get({
|
|
1294
|
+
key: b4a.from(coreKey, 'hex'),
|
|
1295
|
+
valueEncoding: 'json',
|
|
1296
|
+
})
|
|
1297
|
+
await core.ready()
|
|
1298
|
+
|
|
1299
|
+
const discoveryKey = this.#generateChannelDiscoveryKey(name)
|
|
1300
|
+
const chatDiscoveryKey = this.#generateChannelChatDiscoveryKey(name)
|
|
1301
|
+
const appDiscovery = this.#swarm.join(discoveryKey, {
|
|
1302
|
+
server: true,
|
|
1303
|
+
client: true,
|
|
1304
|
+
})
|
|
1305
|
+
await appDiscovery.flushed()
|
|
1306
|
+
const chatDiscovery = this.#chatSwarm.join(chatDiscoveryKey, {
|
|
1307
|
+
server: true,
|
|
1308
|
+
client: true,
|
|
1309
|
+
})
|
|
1310
|
+
await chatDiscovery.flushed()
|
|
1311
|
+
|
|
1312
|
+
this.#setupChannelAppendListener(core, name)
|
|
1313
|
+
|
|
1314
|
+
const channelInfo = {
|
|
1315
|
+
name,
|
|
1316
|
+
discoveryKey: b4a.toString(discoveryKey, 'hex'),
|
|
1317
|
+
coreKey,
|
|
1318
|
+
createdAt: new Date().toISOString(),
|
|
1319
|
+
type: 'group',
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
this.#channels.push(channelInfo)
|
|
1323
|
+
this.#channelCores.set(name, core)
|
|
1324
|
+
this.#channelPeers.set(name, new Map())
|
|
1325
|
+
this.#channelDiscoveries.set(name, appDiscovery)
|
|
1326
|
+
this.#channelChatDiscoveries.set(name, chatDiscovery)
|
|
1327
|
+
this.#saveChannelsMetadata()
|
|
1328
|
+
|
|
1329
|
+
console.log(`[MostBox] Joined channel: ${name}`)
|
|
1330
|
+
this.emit('channel:joined', { name, key: coreKey })
|
|
1331
|
+
|
|
1332
|
+
return { name, key: coreKey }
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
/**
|
|
1336
|
+
* 离开频道
|
|
1337
|
+
* @param {string} name - 频道名
|
|
1338
|
+
* @returns {Promise<string[]>} 剩余频道列表
|
|
1339
|
+
*/
|
|
1340
|
+
async leaveChannel(name) {
|
|
1341
|
+
this.#ensureInitialized()
|
|
1342
|
+
|
|
1343
|
+
const index = this.#channels.findIndex(c => c.name === name)
|
|
1344
|
+
if (index === -1) {
|
|
1345
|
+
throw new Error('频道不存在')
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
const channel = this.#channels[index]
|
|
1349
|
+
|
|
1350
|
+
const appDiscovery = this.#channelDiscoveries.get(name)
|
|
1351
|
+
if (appDiscovery) {
|
|
1352
|
+
try {
|
|
1353
|
+
await this.#swarm.leave(b4a.from(channel.discoveryKey, 'hex'))
|
|
1354
|
+
} catch (err) {
|
|
1355
|
+
console.warn(
|
|
1356
|
+
`[MostBox] Failed to leave app swarm for ${name}:`,
|
|
1357
|
+
err.message
|
|
1358
|
+
)
|
|
1359
|
+
}
|
|
1360
|
+
this.#channelDiscoveries.delete(name)
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
const chatDiscovery = this.#channelChatDiscoveries.get(name)
|
|
1364
|
+
if (chatDiscovery) {
|
|
1365
|
+
try {
|
|
1366
|
+
const chatDiscoveryKey = this.#generateChannelChatDiscoveryKey(name)
|
|
1367
|
+
await this.#chatSwarm.leave(chatDiscoveryKey)
|
|
1368
|
+
} catch (err) {
|
|
1369
|
+
console.warn(
|
|
1370
|
+
`[MostBox] Failed to leave chat swarm for ${name}:`,
|
|
1371
|
+
err.message
|
|
1372
|
+
)
|
|
1373
|
+
}
|
|
1374
|
+
this.#channelChatDiscoveries.delete(name)
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
const core = this.#channelCores.get(name)
|
|
1378
|
+
if (core) {
|
|
1379
|
+
try {
|
|
1380
|
+
await core.close()
|
|
1381
|
+
} catch (err) {
|
|
1382
|
+
console.warn(
|
|
1383
|
+
`[MostBox] Failed to close channel core for ${name}:`,
|
|
1384
|
+
err.message
|
|
1385
|
+
)
|
|
1386
|
+
}
|
|
1387
|
+
this.#channelCores.delete(name)
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
this.#channelPeers.delete(name)
|
|
1391
|
+
this.#channels.splice(index, 1)
|
|
1392
|
+
this.#saveChannelsMetadata()
|
|
1393
|
+
|
|
1394
|
+
console.log(`[MostBox] Left channel: ${name}`)
|
|
1395
|
+
this.emit('channel:left', { name })
|
|
1396
|
+
|
|
1397
|
+
return this.listChannels()
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
/**
|
|
1401
|
+
* 列出所有频道
|
|
1402
|
+
* @returns {Array<{ name: string, coreKey: string, createdAt: string, type: string, peerCount: number }>}
|
|
1403
|
+
*/
|
|
1404
|
+
listChannels() {
|
|
1405
|
+
this.#ensureInitialized()
|
|
1406
|
+
|
|
1407
|
+
return this.#channels.map(c => ({
|
|
1408
|
+
name: c.name,
|
|
1409
|
+
coreKey: c.coreKey,
|
|
1410
|
+
createdAt: c.createdAt,
|
|
1411
|
+
type: c.type,
|
|
1412
|
+
peerCount: (this.#channelPeers.get(c.name) || new Map()).size,
|
|
1413
|
+
}))
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
/**
|
|
1417
|
+
* 获取频道消息
|
|
1418
|
+
* @param {string} name - 频道名
|
|
1419
|
+
* @param {object} [options] - 选项
|
|
1420
|
+
* @param {number} [options.limit=100] - 消息数量
|
|
1421
|
+
* @param {number} [options.offset=0] - 偏移量
|
|
1422
|
+
* @returns {Promise<Array>}
|
|
1423
|
+
*/
|
|
1424
|
+
async getChannelMessages(name, options = {}) {
|
|
1425
|
+
this.#ensureInitialized()
|
|
1426
|
+
|
|
1427
|
+
const { limit = CHANNEL_MESSAGE_LIMIT, offset = 0 } = options
|
|
1428
|
+
|
|
1429
|
+
const core = this.#channelCores.get(name)
|
|
1430
|
+
if (!core) {
|
|
1431
|
+
throw new Error('频道未初始化')
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
const messages = []
|
|
1435
|
+
const total = core.length
|
|
1436
|
+
const start = Math.max(0, total - offset - limit)
|
|
1437
|
+
const end = total - offset
|
|
1438
|
+
|
|
1439
|
+
for (let i = start; i < end; i++) {
|
|
1440
|
+
try {
|
|
1441
|
+
const entry = await core.get(i)
|
|
1442
|
+
messages.push(entry)
|
|
1443
|
+
} catch {
|
|
1444
|
+
break
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
return messages
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
/**
|
|
1452
|
+
* 发送消息到频道
|
|
1453
|
+
* @param {string} name - 频道名
|
|
1454
|
+
* @param {string} content - 消息内容
|
|
1455
|
+
* @param {string} author - 作者 address
|
|
1456
|
+
* @param {string} authorName - 作者显示名
|
|
1457
|
+
* @returns {Promise<object>}
|
|
1458
|
+
*/
|
|
1459
|
+
async sendMessage(name, content, author, authorName) {
|
|
1460
|
+
this.#ensureInitialized()
|
|
1461
|
+
|
|
1462
|
+
const core = this.#channelCores.get(name)
|
|
1463
|
+
if (!core) {
|
|
1464
|
+
throw new Error('频道未初始化')
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
if (!content || !content.trim()) {
|
|
1468
|
+
throw new Error('消息内容不能为空')
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
const trimmed = content.trim()
|
|
1472
|
+
if (trimmed.length > MAX_MESSAGE_LENGTH) {
|
|
1473
|
+
throw new Error(`消息内容不能超过 ${MAX_MESSAGE_LENGTH} 字符`)
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
const message = {
|
|
1477
|
+
type: 'message',
|
|
1478
|
+
author,
|
|
1479
|
+
authorName,
|
|
1480
|
+
content: trimmed,
|
|
1481
|
+
timestamp: Date.now(),
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
await core.append(message)
|
|
1485
|
+
|
|
1486
|
+
this.emit('channel:message', { channel: name, message })
|
|
1487
|
+
|
|
1488
|
+
return message
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
/**
|
|
1492
|
+
* 获取频道内在线用户
|
|
1493
|
+
* @param {string} name - 频道名
|
|
1494
|
+
* @returns {Array<{ peerId: string, authorName: string, lastSeen: number }>}
|
|
1495
|
+
*/
|
|
1496
|
+
getChannelPeers(name) {
|
|
1497
|
+
this.#ensureInitialized()
|
|
1498
|
+
|
|
1499
|
+
const peers = this.#channelPeers.get(name)
|
|
1500
|
+
if (!peers) {
|
|
1501
|
+
return []
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
return [...peers.values()].map(p => ({
|
|
1505
|
+
peerId: p.peerId,
|
|
1506
|
+
authorName: p.authorName,
|
|
1507
|
+
lastSeen: p.lastSeen,
|
|
1508
|
+
}))
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
/**
|
|
1512
|
+
* 获取显示名
|
|
1513
|
+
* @returns {string|null}
|
|
1514
|
+
*/
|
|
1515
|
+
getDisplayName() {
|
|
1516
|
+
try {
|
|
1517
|
+
const configPath = this.#getConfigPath()
|
|
1518
|
+
if (fs.existsSync(configPath)) {
|
|
1519
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'))
|
|
1520
|
+
return config.displayName || null
|
|
1521
|
+
}
|
|
1522
|
+
} catch {}
|
|
1523
|
+
return null
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
/**
|
|
1527
|
+
* 设置显示名
|
|
1528
|
+
* @param {string} name - 显示名
|
|
1529
|
+
*/
|
|
1530
|
+
setDisplayName(name) {
|
|
1531
|
+
try {
|
|
1532
|
+
const configPath = this.#getConfigPath()
|
|
1533
|
+
const config = fs.existsSync(configPath)
|
|
1534
|
+
? JSON.parse(fs.readFileSync(configPath, 'utf-8'))
|
|
1535
|
+
: {}
|
|
1536
|
+
config.displayName = name.trim()
|
|
1537
|
+
const tmpPath = configPath + '.tmp'
|
|
1538
|
+
fs.writeFileSync(tmpPath, JSON.stringify(config, null, 2), 'utf-8')
|
|
1539
|
+
fs.renameSync(tmpPath, configPath)
|
|
1540
|
+
return true
|
|
1541
|
+
} catch {
|
|
1542
|
+
return false
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
// --- 私有方法 ---
|
|
1547
|
+
|
|
1548
|
+
#ensureInitialized() {
|
|
1549
|
+
if (!this.#initialized) {
|
|
1550
|
+
throw new EngineNotInitializedError()
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
async #getOrCreateDrive(name, _options = { server: true, client: false }) {
|
|
1555
|
+
if (this.#drives.has(name)) return this.#drives.get(name)
|
|
1556
|
+
if (this.#drivePromises.has(name)) return this.#drivePromises.get(name)
|
|
1557
|
+
|
|
1558
|
+
const promise = (async () => {
|
|
1559
|
+
const drive = new Hyperdrive(this.#store.namespace(name))
|
|
1560
|
+
await drive.ready()
|
|
1561
|
+
this.#drives.set(name, drive)
|
|
1562
|
+
return drive
|
|
1563
|
+
})()
|
|
1564
|
+
|
|
1565
|
+
this.#drivePromises.set(name, promise)
|
|
1566
|
+
|
|
1567
|
+
try {
|
|
1568
|
+
const drive = await promise
|
|
1569
|
+
return drive
|
|
1570
|
+
} finally {
|
|
1571
|
+
this.#drivePromises.delete(name)
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
async #syncDrive(drive, timeout = DRIVE_SYNC_TIMEOUT) {
|
|
1576
|
+
const done = drive.findingPeers()
|
|
1577
|
+
this.#swarm
|
|
1578
|
+
.join(drive.discoveryKey, { server: true, client: true })
|
|
1579
|
+
.flushed()
|
|
1580
|
+
.then(done, done)
|
|
1581
|
+
try {
|
|
1582
|
+
const updated = await Promise.race([
|
|
1583
|
+
drive.update(),
|
|
1584
|
+
new Promise((_, reject) =>
|
|
1585
|
+
setTimeout(() => reject(new Error('Sync timeout')), timeout)
|
|
1586
|
+
),
|
|
1587
|
+
])
|
|
1588
|
+
return updated
|
|
1589
|
+
} catch {
|
|
1590
|
+
return false
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
#getMetadataPath() {
|
|
1595
|
+
return path.join(this.#options.dataPath, 'published-files.json')
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
#getTrashMetadataPath() {
|
|
1599
|
+
return path.join(this.#options.dataPath, 'trash-files.json')
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
#atomicWrite(filePath, data) {
|
|
1603
|
+
const tmpPath = filePath + '.tmp'
|
|
1604
|
+
fs.writeFileSync(tmpPath, data, 'utf-8')
|
|
1605
|
+
fs.renameSync(tmpPath, filePath)
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
#loadPublishedMetadata() {
|
|
1609
|
+
try {
|
|
1610
|
+
const metadataPath = this.#getMetadataPath()
|
|
1611
|
+
if (fs.existsSync(metadataPath)) {
|
|
1612
|
+
const data = fs.readFileSync(metadataPath, 'utf-8')
|
|
1613
|
+
const parsed = JSON.parse(data)
|
|
1614
|
+
return parsed.map(f => ({ ...f, starred: f.starred || false }))
|
|
1615
|
+
}
|
|
1616
|
+
} catch (err) {
|
|
1617
|
+
console.warn(
|
|
1618
|
+
'Failed to load published metadata, using empty list:',
|
|
1619
|
+
err.message
|
|
1620
|
+
)
|
|
1621
|
+
}
|
|
1622
|
+
return []
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
#savePublishedMetadata() {
|
|
1626
|
+
try {
|
|
1627
|
+
const metadataPath = this.#getMetadataPath()
|
|
1628
|
+
this.#atomicWrite(
|
|
1629
|
+
metadataPath,
|
|
1630
|
+
JSON.stringify(this.#publishedFiles, null, 2)
|
|
1631
|
+
)
|
|
1632
|
+
} catch (err) {
|
|
1633
|
+
console.error('Failed to save published metadata:', err.message)
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
#loadTrashMetadata() {
|
|
1638
|
+
try {
|
|
1639
|
+
const metadataPath = this.#getTrashMetadataPath()
|
|
1640
|
+
if (fs.existsSync(metadataPath)) {
|
|
1641
|
+
const data = fs.readFileSync(metadataPath, 'utf-8')
|
|
1642
|
+
return JSON.parse(data)
|
|
1643
|
+
}
|
|
1644
|
+
} catch (err) {
|
|
1645
|
+
console.warn(
|
|
1646
|
+
'Failed to load trash metadata, using empty list:',
|
|
1647
|
+
err.message
|
|
1648
|
+
)
|
|
1649
|
+
}
|
|
1650
|
+
return []
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
#saveTrashMetadata() {
|
|
1654
|
+
try {
|
|
1655
|
+
const metadataPath = this.#getTrashMetadataPath()
|
|
1656
|
+
this.#atomicWrite(metadataPath, JSON.stringify(this.#trashFiles, null, 2))
|
|
1657
|
+
} catch (err) {
|
|
1658
|
+
console.error('Failed to save trash metadata:', err.message)
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
#getChannelsMetadataPath() {
|
|
1663
|
+
return path.join(this.#options.dataPath, 'channels.json')
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
#getConfigPath() {
|
|
1667
|
+
return path.join(this.#options.dataPath, 'channel-config.json')
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
#loadChannelsMetadata() {
|
|
1671
|
+
try {
|
|
1672
|
+
const metadataPath = this.#getChannelsMetadataPath()
|
|
1673
|
+
if (fs.existsSync(metadataPath)) {
|
|
1674
|
+
const data = fs.readFileSync(metadataPath, 'utf-8')
|
|
1675
|
+
return JSON.parse(data)
|
|
1676
|
+
}
|
|
1677
|
+
} catch (err) {
|
|
1678
|
+
console.warn(
|
|
1679
|
+
'Failed to load channels metadata, using empty list:',
|
|
1680
|
+
err.message
|
|
1681
|
+
)
|
|
1682
|
+
}
|
|
1683
|
+
return []
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
#saveChannelsMetadata() {
|
|
1687
|
+
try {
|
|
1688
|
+
const metadataPath = this.#getChannelsMetadataPath()
|
|
1689
|
+
this.#atomicWrite(metadataPath, JSON.stringify(this.#channels, null, 2))
|
|
1690
|
+
} catch (err) {
|
|
1691
|
+
console.error('Failed to save channels metadata:', err.message)
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
#generateChannelDiscoveryKey(name) {
|
|
1696
|
+
const hash = crypto
|
|
1697
|
+
.createHash('sha256')
|
|
1698
|
+
.update(`${CHANNEL_NAME_PREFIX}${name}`)
|
|
1699
|
+
.digest()
|
|
1700
|
+
return hash
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
#generateChannelChatDiscoveryKey(name) {
|
|
1704
|
+
const hash = crypto
|
|
1705
|
+
.createHash('sha256')
|
|
1706
|
+
.update(`${CHANNEL_NAME_PREFIX}${name}:chat`)
|
|
1707
|
+
.digest()
|
|
1708
|
+
return hash
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
#setupChannelAppendListener(core, channelName) {
|
|
1712
|
+
let lastCoreLength = core.length
|
|
1713
|
+
core.on('append', async () => {
|
|
1714
|
+
if (core.length > lastCoreLength) {
|
|
1715
|
+
for (let i = lastCoreLength; i < core.length; i++) {
|
|
1716
|
+
try {
|
|
1717
|
+
const entry = await core.get(i)
|
|
1718
|
+
if (entry && entry.type === 'message') {
|
|
1719
|
+
this.emit('channel:message', {
|
|
1720
|
+
channel: channelName,
|
|
1721
|
+
message: entry,
|
|
1722
|
+
})
|
|
1723
|
+
}
|
|
1724
|
+
} catch (err) {
|
|
1725
|
+
console.error(
|
|
1726
|
+
`[MostBox] Failed to read channel message from ${channelName}:`,
|
|
1727
|
+
err.message
|
|
1728
|
+
)
|
|
1729
|
+
continue
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
lastCoreLength = core.length
|
|
1733
|
+
}
|
|
1734
|
+
})
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
async #handleChannelConnection(conn) {
|
|
1738
|
+
const stream = conn
|
|
1739
|
+
let connectedPeerId = null
|
|
1740
|
+
|
|
1741
|
+
const helloMessage = JSON.stringify({
|
|
1742
|
+
type: 'channel-hello',
|
|
1743
|
+
peerId: this.getNodeId(),
|
|
1744
|
+
authorName: this.getNodeId().slice(0, 4),
|
|
1745
|
+
channels: this.#channels.map(c => c.name),
|
|
1746
|
+
})
|
|
1747
|
+
|
|
1748
|
+
try {
|
|
1749
|
+
stream.write(helloMessage)
|
|
1750
|
+
} catch {
|
|
1751
|
+
return
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
stream.on('data', async data => {
|
|
1755
|
+
try {
|
|
1756
|
+
const msg = JSON.parse(data.toString())
|
|
1757
|
+
if (msg.type === 'channel-hello') {
|
|
1758
|
+
connectedPeerId = msg.peerId
|
|
1759
|
+
|
|
1760
|
+
const theirChannels = new Set(msg.channels || [])
|
|
1761
|
+
for (const [name, peers] of this.#channelPeers) {
|
|
1762
|
+
if (theirChannels.has(name)) {
|
|
1763
|
+
peers.set(msg.peerId, {
|
|
1764
|
+
peerId: msg.peerId,
|
|
1765
|
+
authorName: msg.authorName,
|
|
1766
|
+
lastSeen: Date.now(),
|
|
1767
|
+
})
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
this.emit('channel:peer:online', {
|
|
1771
|
+
peerId: msg.peerId,
|
|
1772
|
+
authorName: msg.authorName,
|
|
1773
|
+
})
|
|
1774
|
+
}
|
|
1775
|
+
} catch (err) {
|
|
1776
|
+
console.warn(`[MostBox] Failed to process channel data:`, err.message)
|
|
1777
|
+
}
|
|
1778
|
+
})
|
|
1779
|
+
|
|
1780
|
+
stream.on('close', () => {
|
|
1781
|
+
if (connectedPeerId) {
|
|
1782
|
+
for (const [, peers] of this.#channelPeers) {
|
|
1783
|
+
if (peers.has(connectedPeerId)) {
|
|
1784
|
+
const peer = peers.get(connectedPeerId)
|
|
1785
|
+
peers.delete(connectedPeerId)
|
|
1786
|
+
this.emit('channel:peer:offline', {
|
|
1787
|
+
peerId: connectedPeerId,
|
|
1788
|
+
authorName: peer?.authorName,
|
|
1789
|
+
})
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
})
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
/**
|
|
1797
|
+
* 等待驱动器内容从对等节点或本地可用
|
|
1798
|
+
* @param {Hyperdrive} drive - 要检查的驱动器
|
|
1799
|
+
* @param {number} timeout - 最大等待时间(毫秒)
|
|
1800
|
+
* @param {string} [taskId] - 用于取消的任务 ID
|
|
1801
|
+
* @param {object} [taskState] - 任务状态对象
|
|
1802
|
+
* @returns {Promise<Array>} - 条目列表
|
|
1803
|
+
*/
|
|
1804
|
+
async #waitForDriveContent(drive, timeout, taskId = null, taskState = null) {
|
|
1805
|
+
const startTime = Date.now()
|
|
1806
|
+
let pollInterval = DOWNLOAD_POLL_INTERVAL_MIN
|
|
1807
|
+
let lastPeerCount = 0
|
|
1808
|
+
let lastStatus = ''
|
|
1809
|
+
let bootstrapNodesChecked = false
|
|
1810
|
+
let lastUpdateTime = 0
|
|
1811
|
+
|
|
1812
|
+
const localEntries = []
|
|
1813
|
+
try {
|
|
1814
|
+
for await (const entry of drive.list()) {
|
|
1815
|
+
localEntries.push(entry)
|
|
1816
|
+
}
|
|
1817
|
+
if (localEntries.length > 0) {
|
|
1818
|
+
console.log(`[MostBox] Found ${localEntries.length} entries locally`)
|
|
1819
|
+
this.emit('download:status', { taskId, status: 'syncing' })
|
|
1820
|
+
return localEntries
|
|
1821
|
+
}
|
|
1822
|
+
} catch {}
|
|
1823
|
+
|
|
1824
|
+
const tryUpdateDrive = async () => {
|
|
1825
|
+
const now = Date.now()
|
|
1826
|
+
if (now - lastUpdateTime > DRIVE_UPDATE_INTERVAL) {
|
|
1827
|
+
lastUpdateTime = now
|
|
1828
|
+
try {
|
|
1829
|
+
await drive.update()
|
|
1830
|
+
} catch {}
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
while (Date.now() - startTime < timeout) {
|
|
1835
|
+
if (taskState && taskState.aborted) {
|
|
1836
|
+
throw new Error('Download cancelled')
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
const currentTime = Date.now()
|
|
1840
|
+
const elapsed = Math.round((currentTime - startTime) / 1000)
|
|
1841
|
+
|
|
1842
|
+
const currentPeerCount = this.#swarm.connections.size
|
|
1843
|
+
const hasPeers = currentPeerCount > 0
|
|
1844
|
+
|
|
1845
|
+
if (currentPeerCount !== lastPeerCount) {
|
|
1846
|
+
console.log(
|
|
1847
|
+
`[MostBox] Peer count changed: ${lastPeerCount} -> ${currentPeerCount} (elapsed: ${elapsed}s)`
|
|
1848
|
+
)
|
|
1849
|
+
lastPeerCount = currentPeerCount
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
await tryUpdateDrive()
|
|
1853
|
+
|
|
1854
|
+
const entries = []
|
|
1855
|
+
try {
|
|
1856
|
+
for await (const entry of drive.list()) {
|
|
1857
|
+
entries.push(entry)
|
|
1858
|
+
}
|
|
1859
|
+
} catch {}
|
|
1860
|
+
|
|
1861
|
+
if (entries.length > 0) {
|
|
1862
|
+
console.log(
|
|
1863
|
+
`[MostBox] Found ${entries.length} entries after ${elapsed}s`
|
|
1864
|
+
)
|
|
1865
|
+
this.emit('download:status', { taskId, status: 'syncing' })
|
|
1866
|
+
return entries
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
if (hasPeers) {
|
|
1870
|
+
const newStatus = 'syncing'
|
|
1871
|
+
if (lastStatus !== newStatus) {
|
|
1872
|
+
this.emit('download:status', { taskId, status: newStatus })
|
|
1873
|
+
lastStatus = newStatus
|
|
1874
|
+
}
|
|
1875
|
+
pollInterval = Math.min(pollInterval + 200, DOWNLOAD_POLL_INTERVAL_MAX)
|
|
1876
|
+
} else {
|
|
1877
|
+
const newStatus = 'finding-peers'
|
|
1878
|
+
if (lastStatus !== newStatus) {
|
|
1879
|
+
this.emit('download:status', { taskId, status: newStatus })
|
|
1880
|
+
lastStatus = newStatus
|
|
1881
|
+
}
|
|
1882
|
+
pollInterval = DOWNLOAD_POLL_INTERVAL_MIN
|
|
1883
|
+
|
|
1884
|
+
if (elapsed % 30 === 0 && elapsed > 0) {
|
|
1885
|
+
console.log(
|
|
1886
|
+
`[MostBox] Still waiting for peers... (elapsed: ${elapsed}s, timeout: ${timeout / 1000}s)`
|
|
1887
|
+
)
|
|
1888
|
+
|
|
1889
|
+
if (!bootstrapNodesChecked && elapsed >= 60) {
|
|
1890
|
+
bootstrapNodesChecked = true
|
|
1891
|
+
console.log(
|
|
1892
|
+
`[MostBox] No peers found after 60s. This may indicate:`
|
|
1893
|
+
)
|
|
1894
|
+
console.log(
|
|
1895
|
+
`[MostBox] 1. Network/firewall blocking P2P connections`
|
|
1896
|
+
)
|
|
1897
|
+
console.log(`[MostBox] 2. DHT bootstrap nodes unreachable`)
|
|
1898
|
+
console.log(`[MostBox] 3. Publisher node offline`)
|
|
1899
|
+
console.log(`[MostBox] 4. NAT traversal failed`)
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
await new Promise(resolve => setTimeout(resolve, pollInterval))
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
console.log(
|
|
1908
|
+
`[MostBox] Timeout reached after ${timeout / 1000}s, making final attempt...`
|
|
1909
|
+
)
|
|
1910
|
+
|
|
1911
|
+
await tryUpdateDrive()
|
|
1912
|
+
|
|
1913
|
+
const entries = []
|
|
1914
|
+
try {
|
|
1915
|
+
for await (const entry of drive.list()) {
|
|
1916
|
+
entries.push(entry)
|
|
1917
|
+
}
|
|
1918
|
+
} catch (err) {
|
|
1919
|
+
console.log(`[MostBox] Final attempt failed: ${err.message}`)
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
console.log(`[MostBox] Final entry count: ${entries.length}`)
|
|
1923
|
+
|
|
1924
|
+
if (entries.length === 0) {
|
|
1925
|
+
const peerCount = this.#swarm.connections.size
|
|
1926
|
+
console.log(`[MostBox] Diagnostic information:`)
|
|
1927
|
+
console.log(`[MostBox] - Peer count: ${peerCount}`)
|
|
1928
|
+
console.log(`[MostBox] - Bootstrap nodes: ${SWARM_BOOTSTRAP.length}`)
|
|
1929
|
+
console.log(`[MostBox] - Timeout: ${timeout / 1000}s`)
|
|
1930
|
+
|
|
1931
|
+
if (peerCount === 0) {
|
|
1932
|
+
console.log(
|
|
1933
|
+
`[MostBox] Suggestion: Check network connectivity and firewall settings`
|
|
1934
|
+
)
|
|
1935
|
+
} else {
|
|
1936
|
+
console.log(
|
|
1937
|
+
`[MostBox] Suggestion: Publisher may be offline or file may have been removed`
|
|
1938
|
+
)
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
return entries
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
// 重新导出工具函数
|
|
1947
|
+
export * from './config.js'
|
|
1948
|
+
export * from './core/cid.js'
|
|
1949
|
+
export * from './utils/errors.js'
|
|
1950
|
+
export * from './utils/security.js'
|