most-box 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -16
- package/electron/main.js +72 -8
- package/electron/preload.js +6 -0
- package/out/404/index.html +2 -2
- package/out/404.html +2 -2
- package/out/__next.__PAGE__.txt +6 -6
- package/out/__next._full.txt +23 -20
- package/out/__next._head.txt +3 -3
- package/out/__next._index.txt +8 -6
- package/out/__next._tree.txt +6 -4
- package/out/_next/static/chunks/0-n3pg7th.zza.js +1 -0
- package/out/_next/static/chunks/0.4j.0k5a64vg.js +1 -0
- package/out/_next/static/chunks/0.e2avjgna_b2.js +1 -0
- package/out/_next/static/chunks/0.ozi1_x2.m.~.js +1 -0
- package/out/_next/static/chunks/0.t5wlt51zou5.js +1 -0
- package/out/_next/static/chunks/0.w4hkvap~bva.js +1 -0
- package/out/_next/static/chunks/00d9h1tddnnnd.js +1 -0
- package/out/_next/static/chunks/00tkdqwxch-3s.js +1 -0
- package/out/_next/static/chunks/01l3o90g~1z42.js +1 -0
- package/out/_next/static/chunks/01mfky9camw6i.js +1 -0
- package/out/_next/static/chunks/01r.v-pqs1vrm.js +1 -0
- package/out/_next/static/chunks/03edqrb4zdj~g.js +31 -0
- package/out/_next/static/chunks/03h_6oo-gqkhz.js +1 -0
- package/out/_next/static/chunks/{0ho~log~~-jwp.css → 03h~nhgj0hv3p.css} +1 -1
- package/out/_next/static/chunks/04hcgsanv1hhu.js +1 -0
- package/out/_next/static/chunks/05g2q0w5b34.g.js +1 -0
- package/out/_next/static/chunks/05of77xycbt8~.js +1 -0
- package/out/_next/static/chunks/05zwemzfjx3sh.js +1 -0
- package/out/_next/static/chunks/06dpc5df94.v1.js +1 -0
- package/out/_next/static/chunks/06e1~1-z_ic9a.js +1 -0
- package/out/_next/static/chunks/075s7sn.ns~u5.js +1 -0
- package/out/_next/static/chunks/07dynrbvd3.f4.js +1 -0
- package/out/_next/static/chunks/07p~uva5pwgwe.js +1 -0
- package/out/_next/static/chunks/07r9nn-pzlgg1.js +1 -0
- package/out/_next/static/chunks/07td.jq7xff84.css +1 -0
- package/out/_next/static/chunks/08.72abkgwy9g.js +1 -0
- package/out/_next/static/chunks/08576xhv~~jck.js +1 -0
- package/out/_next/static/chunks/08u211~k~qu52.js +1 -0
- package/out/_next/static/chunks/098.p.2-zm4p7.js +1 -0
- package/out/_next/static/chunks/09ngvtajm7e5y.js +1 -0
- package/out/_next/static/chunks/09ps~-43n5qyo.js +1 -0
- package/out/_next/static/chunks/09v7_0gclxr46.js +1 -0
- package/out/_next/static/chunks/09yql86dir9c4.js +1 -0
- package/out/_next/static/chunks/09zmlfljowj1~.js +1 -0
- package/out/_next/static/chunks/0_s~ebb-7b2hr.js +1 -0
- package/out/_next/static/chunks/0_w-0-2z5oqd_.js +1 -0
- package/out/_next/static/chunks/0adx~d-j05c9d.css +24 -0
- package/out/_next/static/chunks/0ao1lbi4b.sfa.js +1 -0
- package/out/_next/static/chunks/0aq.rc9woa2nz.js +1 -0
- package/out/_next/static/chunks/0bld2u_ld~va2.js +1 -0
- package/out/_next/static/chunks/0bliugh5lxw55.js +1 -0
- package/out/_next/static/chunks/0cn9a7aimbdzq.js +1 -0
- package/out/_next/static/chunks/0d3f-nk3c.2re.js +1 -0
- package/out/_next/static/chunks/0dtohpf7~3d12.js +1 -0
- package/out/_next/static/chunks/0e-3e8h7g99yf.js +1 -0
- package/out/_next/static/chunks/0e531nije_ln2.js +1 -0
- package/out/_next/static/chunks/0e5zvj_rh0z3m.js +1 -0
- package/out/_next/static/chunks/0etes81d_cihn.js +1 -0
- package/out/_next/static/chunks/0f4y~rkk-n81e.js +1 -0
- package/out/_next/static/chunks/0fk~0~p7ivfn1.js +1 -0
- package/out/_next/static/chunks/0fw6juc~lsj3z.js +1 -0
- package/out/_next/static/chunks/0g0u7785a73vo.js +1 -0
- package/out/_next/static/chunks/0g_a~e050bgzg.css +1 -0
- package/out/_next/static/chunks/0g_fpgh7drfda.js +1 -0
- package/out/_next/static/chunks/{0n~dq4kpx9xxx.js → 0gcsdf57gcm6h.js} +1 -1
- package/out/_next/static/chunks/0gwian.hp3-92.js +1 -0
- package/out/_next/static/chunks/0gze5uso1mbe9.js +1 -0
- package/out/_next/static/chunks/0h4r.qtmpa6eh.js +1 -0
- package/out/_next/static/chunks/0hf.aosc-7172.js +1 -0
- package/out/_next/static/chunks/0hgz35c1ejbs9.js +1 -0
- package/out/_next/static/chunks/0hpev4am9jpmu.css +1 -0
- package/out/_next/static/chunks/0hrw-r.xmvmsq.js +1 -0
- package/out/_next/static/chunks/0hzg4al.v~8~m.js +1 -0
- package/out/_next/static/chunks/0ip9xrols_83o.js +1 -0
- package/out/_next/static/chunks/0j4-d0qf.v~kn.js +1 -0
- package/out/_next/static/chunks/0jhdeq.j9_02m.js +1 -0
- package/out/_next/static/chunks/0jy63h3i-y69i.js +1 -0
- package/out/_next/static/chunks/0kdnx_u-60k9s.js +1 -0
- package/out/_next/static/chunks/0kq~edq42o1-c.js +1 -0
- package/out/_next/static/chunks/0l5_.uqb-uqb8.js +1 -0
- package/out/_next/static/chunks/0l682p362d-5w.js +1 -0
- package/out/_next/static/chunks/0m68p9txef5rs.js +1 -0
- package/out/_next/static/chunks/0mex8svsiv-2l.js +1 -0
- package/out/_next/static/chunks/0mme-fm5d2oz2.js +1 -0
- package/out/_next/static/chunks/0myp4sjagr~h0.js +1 -0
- package/out/_next/static/chunks/0myq9gs8szydh.js +1 -0
- package/out/_next/static/chunks/0n.qlfk~z7o.6.js +1 -0
- package/out/_next/static/chunks/0n4t80gjc3q5h.js +1 -0
- package/out/_next/static/chunks/0o9ce4cyf76by.js +736 -0
- package/out/_next/static/chunks/0oz3yl6_-716p.js +1 -0
- package/out/_next/static/chunks/0p0sv~fuddvgr.js +1 -0
- package/out/_next/static/chunks/0pt.5cg1t09qs.js +1 -0
- package/out/_next/static/chunks/0q0ksgxg98xgd.js +1 -0
- package/out/_next/static/chunks/0qgx9t4jx16ua.css +1 -0
- package/out/_next/static/chunks/0qqupeexg83u7.js +1 -0
- package/out/_next/static/chunks/0rb-ri481.kc9.js +1 -0
- package/out/_next/static/chunks/0rsnmahfd.59p.js +1 -0
- package/out/_next/static/chunks/0rt6rgnvr-s_p.js +1 -0
- package/out/_next/static/chunks/0runh28p_gg6..js +1 -0
- package/out/_next/static/chunks/0shy.t1fwqcev.js +1 -0
- package/out/_next/static/chunks/{0d3shmwh5_nmn.js → 0t2xr05rlu96l.js} +1 -1
- package/out/_next/static/chunks/0t6h56rhg1y5i.js +1 -0
- package/out/_next/static/chunks/0tdqd1zunusgk.js +1 -0
- package/out/_next/static/chunks/0ujbnp38x63ek.js +1 -0
- package/out/_next/static/chunks/0ukyg~tkm~h2m.css +1 -0
- package/out/_next/static/chunks/0v68pdrp54lb-.js +1 -0
- package/out/_next/static/chunks/0vsm0m5sxrb.3.js +1 -0
- package/out/_next/static/chunks/0vzlz.iboqo3c.js +1 -0
- package/out/_next/static/chunks/0w87vbpkf-ogd.js +1 -0
- package/out/_next/static/chunks/0wtf0xsiicxx6.js +1 -0
- package/out/_next/static/chunks/0xdwau5k2augv.css +4 -0
- package/out/_next/static/chunks/0xgg0~kmf3gd-.js +1 -0
- package/out/_next/static/chunks/0xj24-70ptdzp.js +1 -0
- package/out/_next/static/chunks/0xxlx772fr3x4.js +1 -0
- package/out/_next/static/chunks/0y.li-~3oybew.js +1 -0
- package/out/_next/static/chunks/0yl2t7cs-n_ng.js +1 -0
- package/out/_next/static/chunks/0yq3kh.hchtm_.js +1 -0
- package/out/_next/static/chunks/0ys0l5au.9c2c.js +1 -0
- package/out/_next/static/chunks/0z48pmi6buytt.js +1 -0
- package/out/_next/static/chunks/0zapnvgy89mg..js +1 -0
- package/out/_next/static/chunks/0~.-vxi5oc.r0.js +1 -0
- package/out/_next/static/chunks/0~3ik-hfp9s-7.js +1 -0
- package/out/_next/static/chunks/0~4f5p6tvn1lq.js +1 -0
- package/out/_next/static/chunks/0~_0ys.2whxbw.js +1 -0
- package/out/_next/static/chunks/0~_ui9l7.2sxf.js +1 -0
- package/out/_next/static/chunks/1037jlyw5~7ht.js +1 -0
- package/out/_next/static/chunks/1045hfzu533z0.js +1 -0
- package/out/_next/static/chunks/104e5nmc.c-pl.js +1 -0
- package/out/_next/static/chunks/109taw1pbh-0b.js +1 -0
- package/out/_next/static/chunks/10x7~onqwp338.js +1 -0
- package/out/_next/static/chunks/10ynz1dy483wf.js +1 -0
- package/out/_next/static/chunks/11hds.mg~4_r-.js +1 -0
- package/out/_next/static/chunks/11ibzaklcauw~.js +1 -0
- package/out/_next/static/chunks/11z.0s6.42b.p.js +1 -0
- package/out/_next/static/chunks/12-9n56l0y3yr.js +1 -0
- package/out/_next/static/chunks/126enaq~f7scl.js +1 -0
- package/out/_next/static/chunks/12nr19.nnn6s3.js +5 -0
- package/out/_next/static/chunks/{0qub_r0x_r-e9.css → 12pep-2t-qg4n.css} +1 -1
- package/out/_next/static/chunks/1380op_pfk.qo.js +1 -0
- package/out/_next/static/chunks/146oiw1bggtn4.js +1 -0
- package/out/_next/static/chunks/14_inksek_rth.js +2 -0
- package/out/_next/static/chunks/14_po2rb_arn4.js +1 -0
- package/out/_next/static/chunks/14a4fwbiq.l3z.js +1 -0
- package/out/_next/static/chunks/14cowsqn95m1k.js +1 -0
- package/out/_next/static/chunks/14dtd3l03v.kx.js +1 -0
- package/out/_next/static/chunks/14tm3qa-v9o-4.js +1 -0
- package/out/_next/static/chunks/15-o4kb-evqd7.js +1 -0
- package/out/_next/static/chunks/153-sz7s.qml2.js +1 -0
- package/out/_next/static/chunks/157z7bowux3xj.js +1 -0
- package/out/_next/static/chunks/15m1_677az2cm.js +1 -0
- package/out/_next/static/chunks/15v.~.ne6ogkk.js +1 -0
- package/out/_next/static/chunks/16i.qbk8t8gf_.js +1 -0
- package/out/_next/static/chunks/16u9f35gylw8l.js +1 -0
- package/out/_next/static/chunks/16xls5tt_68lx.js +1 -0
- package/out/_next/static/chunks/17ajyb5ogk5yj.js +1 -0
- package/out/_next/static/chunks/17dyfxbq8yz8n.js +1 -0
- package/out/_next/static/chunks/180zln9pcq9ih.js +1 -0
- package/out/_next/static/chunks/1814izi5gh.kp.js +1 -0
- package/out/_next/static/chunks/turbopack-0xta0kqwzkf28.js +1 -0
- package/out/_next/static/media/KaTeX_AMS-Regular.0b~8ki5y928w2.woff +0 -0
- package/out/_next/static/media/KaTeX_AMS-Regular.0p1vbqd84i2~o.woff2 +0 -0
- package/out/_next/static/media/KaTeX_AMS-Regular.173t6ktr7uf-w.ttf +0 -0
- package/out/_next/static/media/KaTeX_Caligraphic-Bold.01-pzluls4zgb.woff2 +0 -0
- package/out/_next/static/media/KaTeX_Caligraphic-Bold.0x2v1lwn~880f.woff +0 -0
- package/out/_next/static/media/KaTeX_Caligraphic-Bold.16zv5fax0h0ka.ttf +0 -0
- package/out/_next/static/media/KaTeX_Caligraphic-Regular.02i3z7wig438t.ttf +0 -0
- package/out/_next/static/media/KaTeX_Caligraphic-Regular.0rysu1t-ncjq8.woff2 +0 -0
- package/out/_next/static/media/KaTeX_Caligraphic-Regular.10927swgekwun.woff +0 -0
- package/out/_next/static/media/KaTeX_Fraktur-Bold.0e-16u10iuyyf.woff +0 -0
- package/out/_next/static/media/KaTeX_Fraktur-Bold.0et27v~3~4uhe.ttf +0 -0
- package/out/_next/static/media/KaTeX_Fraktur-Bold.0w23i72~hprpq.woff2 +0 -0
- package/out/_next/static/media/KaTeX_Fraktur-Regular.0b.riegzdfue2.woff +0 -0
- package/out/_next/static/media/KaTeX_Fraktur-Regular.0rekyoa-52fj_.woff2 +0 -0
- package/out/_next/static/media/KaTeX_Fraktur-Regular.0vjwa15znhk~4.ttf +0 -0
- package/out/_next/static/media/KaTeX_Main-Bold.09i7~607shf-h.ttf +0 -0
- package/out/_next/static/media/KaTeX_Main-Bold.09lmynrorhcbw.woff +0 -0
- package/out/_next/static/media/KaTeX_Main-Bold.16pfc63_du6mx.woff2 +0 -0
- package/out/_next/static/media/KaTeX_Main-BoldItalic.0cp37g7x1q8h6.woff +0 -0
- package/out/_next/static/media/KaTeX_Main-BoldItalic.0d54rk08rx11s.woff2 +0 -0
- package/out/_next/static/media/KaTeX_Main-BoldItalic.15j6k~hix2t_0.ttf +0 -0
- package/out/_next/static/media/KaTeX_Main-Italic.0382gqciexmbu.woff +0 -0
- package/out/_next/static/media/KaTeX_Main-Italic.06o5nq0_91v60.woff2 +0 -0
- package/out/_next/static/media/KaTeX_Main-Italic.0su4i6mm18-wo.ttf +0 -0
- package/out/_next/static/media/KaTeX_Main-Regular.08zh8z.7shijf.ttf +0 -0
- package/out/_next/static/media/KaTeX_Main-Regular.0diheg01zyoph.woff +0 -0
- package/out/_next/static/media/KaTeX_Main-Regular.0kaf-ag2_wkm-.woff2 +0 -0
- package/out/_next/static/media/KaTeX_Math-BoldItalic.0ajzxypnbx1h1.ttf +0 -0
- package/out/_next/static/media/KaTeX_Math-BoldItalic.0ck1myuerwyqw.woff +0 -0
- package/out/_next/static/media/KaTeX_Math-BoldItalic.0ja97dn.cpc87.woff2 +0 -0
- package/out/_next/static/media/KaTeX_Math-Italic.09xkhecjcn5r9.woff +0 -0
- package/out/_next/static/media/KaTeX_Math-Italic.0x23a-bmp-5tg.ttf +0 -0
- package/out/_next/static/media/KaTeX_Math-Italic.0zrha2c4sl2je.woff2 +0 -0
- package/out/_next/static/media/KaTeX_SansSerif-Bold.05a9.pc1j_zx9.woff2 +0 -0
- package/out/_next/static/media/KaTeX_SansSerif-Bold.0jcl-ayi1uun0.woff +0 -0
- package/out/_next/static/media/KaTeX_SansSerif-Bold.0re8y.dm7.mt5.ttf +0 -0
- package/out/_next/static/media/KaTeX_SansSerif-Italic.0a0234dc3s62j.woff2 +0 -0
- package/out/_next/static/media/KaTeX_SansSerif-Italic.0judofdln9731.woff +0 -0
- package/out/_next/static/media/KaTeX_SansSerif-Italic.10z1iap9pfus8.ttf +0 -0
- package/out/_next/static/media/KaTeX_SansSerif-Regular.0h9yjlugq4q_e.woff +0 -0
- package/out/_next/static/media/KaTeX_SansSerif-Regular.0v6gcj32-czft.woff2 +0 -0
- package/out/_next/static/media/KaTeX_SansSerif-Regular.0zm18kga42ebc.ttf +0 -0
- package/out/_next/static/media/KaTeX_Script-Regular.0c4.h-mer83d_.woff2 +0 -0
- package/out/_next/static/media/KaTeX_Script-Regular.0q14y6zkzlpob.ttf +0 -0
- package/out/_next/static/media/KaTeX_Script-Regular.0ze6v4r_-99oy.woff +0 -0
- package/out/_next/static/media/KaTeX_Size1-Regular.013x6a4ierotp.woff2 +0 -0
- package/out/_next/static/media/KaTeX_Size1-Regular.0kidw0oi.m68o.woff +0 -0
- package/out/_next/static/media/KaTeX_Size1-Regular.0m6y-i6wfokni.ttf +0 -0
- package/out/_next/static/media/KaTeX_Size2-Regular.0blpmluwilgbg.woff +0 -0
- package/out/_next/static/media/KaTeX_Size2-Regular.0d5inmyp-tyv3.woff2 +0 -0
- package/out/_next/static/media/KaTeX_Size2-Regular.0wnhnvj-.k9d5.ttf +0 -0
- package/out/_next/static/media/KaTeX_Size3-Regular.01h0xm_sfctj3.woff +0 -0
- package/out/_next/static/media/KaTeX_Size3-Regular.0iukctyhw5j56.woff2 +0 -0
- package/out/_next/static/media/KaTeX_Size3-Regular.0jl8mqyf4gzpn.ttf +0 -0
- package/out/_next/static/media/KaTeX_Size4-Regular.0w3.rb_c4stzk.woff2 +0 -0
- package/out/_next/static/media/KaTeX_Size4-Regular.0wr_9l81-mu06.ttf +0 -0
- package/out/_next/static/media/KaTeX_Size4-Regular.12tvaesf3.zl3.woff +0 -0
- package/out/_next/static/media/KaTeX_Typewriter-Regular.0c4zdxz~8frhm.woff2 +0 -0
- package/out/_next/static/media/KaTeX_Typewriter-Regular.0cgrzn5l3kao5.woff +0 -0
- package/out/_next/static/media/KaTeX_Typewriter-Regular.128~qc3858otl.ttf +0 -0
- package/out/_not-found/__next._full.txt +21 -19
- package/out/_not-found/__next._head.txt +3 -3
- package/out/_not-found/__next._index.txt +8 -6
- package/out/_not-found/__next._not-found.__PAGE__.txt +4 -4
- package/out/_not-found/__next._not-found.txt +3 -3
- package/out/_not-found/__next._tree.txt +3 -2
- package/out/_not-found/index.html +2 -2
- package/out/_not-found/index.txt +21 -19
- package/out/admin/__next._full.txt +23 -0
- package/out/admin/__next._head.txt +5 -0
- package/out/admin/__next._index.txt +9 -0
- package/out/admin/__next._tree.txt +5 -0
- package/out/admin/__next.admin.__PAGE__.txt +9 -0
- package/out/admin/__next.admin.txt +5 -0
- package/out/admin/index.html +15 -0
- package/out/admin/index.txt +23 -0
- package/out/app/__next._full.txt +15 -13
- package/out/app/__next._head.txt +3 -3
- package/out/app/__next._index.txt +8 -6
- package/out/app/__next._tree.txt +4 -2
- package/out/app/__next.app.__PAGE__.txt +4 -4
- package/out/app/__next.app.txt +3 -4
- package/out/app/index.html +2 -2
- package/out/app/index.txt +15 -13
- package/out/chat/__next._full.txt +16 -14
- package/out/chat/__next._head.txt +3 -3
- package/out/chat/__next._index.txt +8 -6
- package/out/chat/__next._tree.txt +5 -3
- package/out/chat/__next.chat.__PAGE__.txt +4 -4
- package/out/chat/__next.chat.txt +4 -5
- package/out/chat/index.html +2 -2
- package/out/chat/index.txt +16 -14
- package/out/chat/join/__next._full.txt +25 -0
- package/out/chat/join/__next._head.txt +5 -0
- package/out/chat/join/__next._index.txt +9 -0
- package/out/chat/join/__next._tree.txt +5 -0
- package/out/chat/join/__next.chat.join.__PAGE__.txt +9 -0
- package/out/chat/join/__next.chat.join.txt +5 -0
- package/out/chat/join/__next.chat.txt +5 -0
- package/out/chat/join/index.html +15 -0
- package/out/chat/join/index.txt +25 -0
- package/out/download/__next._full.txt +34 -33
- package/out/download/__next._head.txt +3 -3
- package/out/download/__next._index.txt +8 -6
- package/out/download/__next._tree.txt +6 -4
- package/out/download/__next.download.__PAGE__.txt +13 -14
- package/out/download/__next.download.txt +3 -3
- package/out/download/index.html +2 -2
- package/out/download/index.txt +34 -33
- package/out/index.html +2 -2
- package/out/index.txt +23 -20
- package/out/note/__next._full.txt +24 -0
- package/out/{changelog → note}/__next._head.txt +3 -3
- package/out/note/__next._index.txt +9 -0
- package/out/note/__next._tree.txt +4 -0
- package/out/note/__next.note.__PAGE__.txt +9 -0
- package/out/note/__next.note.txt +5 -0
- package/out/note/index.html +15 -0
- package/out/note/index.txt +24 -0
- package/out/ping/__next._full.txt +23 -20
- package/out/ping/__next._head.txt +3 -3
- package/out/ping/__next._index.txt +8 -6
- package/out/ping/__next._tree.txt +6 -4
- package/out/ping/__next.ping.__PAGE__.txt +5 -5
- package/out/ping/__next.ping.txt +4 -4
- package/out/ping/index.html +2 -2
- package/out/ping/index.txt +23 -20
- package/out/web3/__next._full.txt +16 -14
- package/out/web3/__next._head.txt +3 -3
- package/out/web3/__next._index.txt +8 -6
- package/out/web3/__next._tree.txt +5 -3
- package/out/web3/__next.web3.__PAGE__.txt +4 -4
- package/out/web3/__next.web3.txt +4 -5
- package/out/web3/ed25519/__next._full.txt +14 -12
- package/out/web3/ed25519/__next._head.txt +3 -3
- package/out/web3/ed25519/__next._index.txt +8 -6
- package/out/web3/ed25519/__next._tree.txt +5 -3
- package/out/web3/ed25519/__next.web3.ed25519.__PAGE__.txt +2 -2
- package/out/web3/ed25519/__next.web3.ed25519.txt +3 -3
- package/out/web3/ed25519/__next.web3.txt +4 -5
- package/out/web3/ed25519/index.html +1 -1
- package/out/web3/ed25519/index.txt +14 -12
- package/out/web3/index.html +2 -2
- package/out/web3/index.txt +16 -14
- package/out/web3/tools/__next._full.txt +14 -12
- package/out/web3/tools/__next._head.txt +3 -3
- package/out/web3/tools/__next._index.txt +8 -6
- package/out/web3/tools/__next._tree.txt +5 -3
- package/out/web3/tools/__next.web3.tools.__PAGE__.txt +2 -2
- package/out/web3/tools/__next.web3.tools.txt +3 -3
- package/out/web3/tools/__next.web3.txt +4 -5
- package/out/web3/tools/index.html +1 -1
- package/out/web3/tools/index.txt +14 -12
- package/package.json +30 -13
- package/server/index.js +188 -901
- package/server/src/config.js +5 -1
- package/server/src/core/channelAttachment.js +68 -0
- package/server/src/core/cid.js +6 -71
- package/server/src/core/cidTopic.js +29 -0
- package/server/src/core/mostLink.js +88 -0
- package/server/src/http/access.js +123 -0
- package/server/src/http/app.js +1095 -0
- package/server/src/http/errors.js +35 -0
- package/server/src/http/nodeLogs.js +53 -0
- package/server/src/http/nodeStatus.js +146 -0
- package/server/src/http/staticFiles.js +84 -0
- package/server/src/http/uploads.js +114 -0
- package/server/src/index.js +1539 -301
- package/server/src/node/config.js +191 -0
- package/server/src/node/logs.js +94 -0
- package/server/src/utils/api.js +359 -8
- package/server/src/utils/auth.js +63 -0
- package/server/src/utils/dateTime.js +30 -0
- package/server/src/utils/downloadMessages.js +89 -0
- package/server/src/utils/errors.js +14 -0
- package/server/src/utils/mostWallet.js +185 -1
- package/server/src/utils/mp.js +2 -26
- package/server/src/utils/noteBackup.js +116 -0
- package/server/src/utils/noteUtils.js +128 -0
- package/server/src/utils/userIdentity.js +8 -61
- package/out/_next/static/chunks/003jnm.v5tzw5.js +0 -1
- package/out/_next/static/chunks/00re8v.gbcywn.js +0 -1
- package/out/_next/static/chunks/00s106sbq8t9v.js +0 -1
- package/out/_next/static/chunks/012hi627qrdnn.js +0 -1
- package/out/_next/static/chunks/0174xh3wfsjm1.js +0 -2
- package/out/_next/static/chunks/02~o2nmo5pmy1.js +0 -1
- package/out/_next/static/chunks/07t.dhhokszz5.css +0 -1
- package/out/_next/static/chunks/0_wia9ofmsi1c.css +0 -2
- package/out/_next/static/chunks/0ah8fihozo2_u.js +0 -5
- package/out/_next/static/chunks/0bzupvr5gt3k9.js +0 -31
- package/out/_next/static/chunks/0e_h0d3ekzks8.css +0 -1
- package/out/_next/static/chunks/0gdluj423gso1.js +0 -1
- package/out/_next/static/chunks/0gmoiq06srjay.css +0 -1
- package/out/_next/static/chunks/0imkasy7kb67u.js +0 -1
- package/out/_next/static/chunks/0jjc_b9q_ldi2.js +0 -1
- package/out/_next/static/chunks/0jl~j62iz2uvr.js +0 -1
- package/out/_next/static/chunks/0lqslm813wk_h.js +0 -1
- package/out/_next/static/chunks/0q782fxxd0lx~.js +0 -1
- package/out/_next/static/chunks/0slwj0c46k5cu.js +0 -1
- package/out/_next/static/chunks/0sorqk.oc6b7j.css +0 -1
- package/out/_next/static/chunks/0tapzqc6hgvx-.js +0 -1
- package/out/_next/static/chunks/0xsc7z5x8n7wg.js +0 -1
- package/out/_next/static/chunks/0zm~gys2jwl0g.js +0 -1
- package/out/_next/static/chunks/turbopack-0a_g3u0ud~jb8.js +0 -1
- package/out/changelog/__next._full.txt +0 -22
- package/out/changelog/__next._index.txt +0 -7
- package/out/changelog/__next._tree.txt +0 -3
- package/out/changelog/__next.changelog.__PAGE__.txt +0 -10
- package/out/changelog/__next.changelog.txt +0 -5
- package/out/changelog/index.html +0 -15
- package/out/changelog/index.txt +0 -22
- package/out/docs/__next._full.txt +0 -22
- package/out/docs/__next._head.txt +0 -5
- package/out/docs/__next._index.txt +0 -7
- package/out/docs/__next._tree.txt +0 -3
- package/out/docs/__next.docs.__PAGE__.txt +0 -10
- package/out/docs/__next.docs.txt +0 -5
- package/out/docs/getting-started/__next._full.txt +0 -22
- package/out/docs/getting-started/__next._head.txt +0 -5
- package/out/docs/getting-started/__next._index.txt +0 -7
- package/out/docs/getting-started/__next._tree.txt +0 -3
- package/out/docs/getting-started/__next.docs.getting-started.__PAGE__.txt +0 -10
- package/out/docs/getting-started/__next.docs.getting-started.txt +0 -5
- package/out/docs/getting-started/__next.docs.txt +0 -5
- package/out/docs/getting-started/index.html +0 -15
- package/out/docs/getting-started/index.txt +0 -22
- package/out/docs/index.html +0 -15
- package/out/docs/index.txt +0 -22
- /package/out/_next/static/{iOB2EBwOGZ0iYW7Lbg9u_ → t7ZIeQpVvjz4a7-5Tt-VK}/_buildManifest.js +0 -0
- /package/out/_next/static/{iOB2EBwOGZ0iYW7Lbg9u_ → t7ZIeQpVvjz4a7-5Tt-VK}/_clientMiddlewareManifest.js +0 -0
- /package/out/_next/static/{iOB2EBwOGZ0iYW7Lbg9u_ → t7ZIeQpVvjz4a7-5Tt-VK}/_ssgManifest.js +0 -0
package/server/src/index.js
CHANGED
|
@@ -14,11 +14,12 @@ import Corestore from 'corestore'
|
|
|
14
14
|
import Hyperdrive from 'hyperdrive'
|
|
15
15
|
import b4a from 'b4a'
|
|
16
16
|
import crypto from 'node:crypto'
|
|
17
|
-
import { CID } from 'multiformats/cid'
|
|
18
17
|
import fs from 'node:fs'
|
|
19
18
|
import path from 'node:path'
|
|
20
19
|
|
|
21
20
|
import { calculateCid, parseMostLink } from './core/cid.js'
|
|
21
|
+
import { normalizeChannelAttachment } from './core/channelAttachment.js'
|
|
22
|
+
import { getCidInfo } from './core/cidTopic.js'
|
|
22
23
|
import {
|
|
23
24
|
sanitizeFilename,
|
|
24
25
|
validateAndSanitizePath,
|
|
@@ -33,6 +34,8 @@ import {
|
|
|
33
34
|
PeerNotFoundError,
|
|
34
35
|
IntegrityError,
|
|
35
36
|
PermissionError,
|
|
37
|
+
ConflictError,
|
|
38
|
+
StorageCapacityError,
|
|
36
39
|
EngineNotInitializedError,
|
|
37
40
|
} from './utils/errors.js'
|
|
38
41
|
import {
|
|
@@ -51,6 +54,8 @@ import {
|
|
|
51
54
|
DOWNLOAD_POLL_INTERVAL_MIN,
|
|
52
55
|
DOWNLOAD_POLL_INTERVAL_MAX,
|
|
53
56
|
DRIVE_UPDATE_INTERVAL,
|
|
57
|
+
HOLDING_REJOIN_BATCH_SIZE,
|
|
58
|
+
HOLDING_REJOIN_BATCH_DELAY,
|
|
54
59
|
PROGRESS_THROTTLE,
|
|
55
60
|
DEFAULT_READ_LIMIT,
|
|
56
61
|
CHANNEL_NAME_MIN_LENGTH,
|
|
@@ -61,19 +66,51 @@ import {
|
|
|
61
66
|
MAX_MESSAGE_LENGTH,
|
|
62
67
|
} from './config.js'
|
|
63
68
|
|
|
69
|
+
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
|
|
70
|
+
|
|
71
|
+
function normalizeOwnerAddress(address) {
|
|
72
|
+
const value = String(address || '').trim()
|
|
73
|
+
return /^0x[a-fA-F0-9]{40}$/.test(value) ? value.toLowerCase() : ''
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function createOfflineSwarm() {
|
|
77
|
+
return {
|
|
78
|
+
connections: new Set(),
|
|
79
|
+
keyPair: {
|
|
80
|
+
publicKey: crypto.randomBytes(32),
|
|
81
|
+
},
|
|
82
|
+
on() {},
|
|
83
|
+
join() {
|
|
84
|
+
return {}
|
|
85
|
+
},
|
|
86
|
+
leave() {
|
|
87
|
+
return Promise.resolve()
|
|
88
|
+
},
|
|
89
|
+
destroy() {
|
|
90
|
+
return Promise.resolve()
|
|
91
|
+
},
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
64
95
|
export class MostBoxEngine extends EventEmitter {
|
|
65
96
|
#store = null
|
|
66
97
|
#swarm = null
|
|
67
98
|
#drives = new Map()
|
|
68
99
|
#publishedFiles = []
|
|
100
|
+
#holdings = []
|
|
69
101
|
#trashFiles = []
|
|
70
102
|
#initialized = false
|
|
71
103
|
#options = null
|
|
72
104
|
#activeDownloads = new Map()
|
|
73
105
|
#drivePromises = new Map()
|
|
106
|
+
#fileDiscoveries = new Map()
|
|
107
|
+
#fileMonitors = new Map()
|
|
108
|
+
#seedStates = new Map()
|
|
109
|
+
#holdingResumeTask = null
|
|
74
110
|
|
|
75
111
|
#channels = []
|
|
76
112
|
#channelCores = new Map()
|
|
113
|
+
#channelLocalCoreKey = new Map()
|
|
77
114
|
#channelDiscoveries = new Map()
|
|
78
115
|
#channelChatDiscoveries = new Map()
|
|
79
116
|
#channelPeers = new Map()
|
|
@@ -85,7 +122,9 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
85
122
|
* @param {object} options - 配置选项
|
|
86
123
|
* @param {string} options.dataPath - 存储 P2P 数据的路径(必填)
|
|
87
124
|
* @param {string} [options.downloadPath] - 默认下载路径(可选,默认为 dataPath/downloads)
|
|
88
|
-
* @param {number} [options.maxFileSize] - 最大文件大小(字节)(默认:
|
|
125
|
+
* @param {number} [options.maxFileSize] - 最大文件大小(字节)(默认:10GB)
|
|
126
|
+
* @param {number} [options.capacityBytes] - 节点存储容量上限(字节)(默认:100GB)
|
|
127
|
+
* @param {boolean} [options.disableNetwork] - 测试用:跳过真实 Hyperswarm 网络
|
|
89
128
|
*/
|
|
90
129
|
constructor(options) {
|
|
91
130
|
super()
|
|
@@ -99,6 +138,9 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
99
138
|
downloadPath:
|
|
100
139
|
options.downloadPath || path.join(options.dataPath, 'downloads'),
|
|
101
140
|
maxFileSize: options.maxFileSize || MAX_FILE_SIZE,
|
|
141
|
+
capacityBytes: options.capacityBytes || 100 * 1024 * 1024 * 1024,
|
|
142
|
+
downloadTimeout: options.downloadTimeout || DOWNLOAD_TIMEOUT,
|
|
143
|
+
disableNetwork: options.disableNetwork === true,
|
|
102
144
|
}
|
|
103
145
|
}
|
|
104
146
|
|
|
@@ -157,14 +199,19 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
157
199
|
}
|
|
158
200
|
|
|
159
201
|
console.log(`[MostBox] Initializing Hyperswarm...`)
|
|
160
|
-
this.#
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
202
|
+
if (this.#options.disableNetwork) {
|
|
203
|
+
this.#swarm = createOfflineSwarm()
|
|
204
|
+
this.#chatSwarm = createOfflineSwarm()
|
|
205
|
+
} else {
|
|
206
|
+
this.#swarm = new Hyperswarm({
|
|
207
|
+
maxPeers: MAX_PEERS,
|
|
208
|
+
bootstrap: SWARM_BOOTSTRAP,
|
|
209
|
+
firewall: () => false,
|
|
210
|
+
connectionKeepAlive: SWARM_KEEP_ALIVE_INTERVAL,
|
|
211
|
+
randomPunchInterval: SWARM_RANDOM_PUNCH_INTERVAL,
|
|
212
|
+
handshakeTimeout: CONNECTION_TIMEOUT,
|
|
213
|
+
})
|
|
214
|
+
}
|
|
168
215
|
|
|
169
216
|
this.#swarm.on('error', err => {
|
|
170
217
|
if (
|
|
@@ -190,14 +237,16 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
190
237
|
this.emit('connection', conn)
|
|
191
238
|
})
|
|
192
239
|
|
|
193
|
-
this.#
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
240
|
+
if (!this.#options.disableNetwork) {
|
|
241
|
+
this.#chatSwarm = new Hyperswarm({
|
|
242
|
+
maxPeers: MAX_PEERS,
|
|
243
|
+
bootstrap: SWARM_BOOTSTRAP,
|
|
244
|
+
firewall: () => false,
|
|
245
|
+
connectionKeepAlive: SWARM_KEEP_ALIVE_INTERVAL,
|
|
246
|
+
randomPunchInterval: SWARM_RANDOM_PUNCH_INTERVAL,
|
|
247
|
+
handshakeTimeout: CONNECTION_TIMEOUT,
|
|
248
|
+
})
|
|
249
|
+
}
|
|
201
250
|
|
|
202
251
|
this.#chatSwarm.on('error', err => {
|
|
203
252
|
if (
|
|
@@ -230,6 +279,17 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
230
279
|
`[MostBox] Loaded ${this.#publishedFiles.length} published files`
|
|
231
280
|
)
|
|
232
281
|
|
|
282
|
+
this.#holdings = this.#loadHoldingsMetadata()
|
|
283
|
+
console.log(`[MostBox] Loaded ${this.#holdings.length} node holdings`)
|
|
284
|
+
|
|
285
|
+
for (const holding of this.#holdings) {
|
|
286
|
+
this.#setSeedState(holding.cid, {
|
|
287
|
+
status: 'queued',
|
|
288
|
+
topic: holding.topic,
|
|
289
|
+
driveName: holding.driveName,
|
|
290
|
+
})
|
|
291
|
+
}
|
|
292
|
+
|
|
233
293
|
this.#trashFiles = this.#loadTrashMetadata()
|
|
234
294
|
console.log(`[MostBox] Loaded ${this.#trashFiles.length} trash files`)
|
|
235
295
|
|
|
@@ -244,9 +304,22 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
244
304
|
valueEncoding: 'json',
|
|
245
305
|
})
|
|
246
306
|
await core.ready()
|
|
247
|
-
|
|
307
|
+
const coreKeyHex = b4a.toString(core.key, 'hex')
|
|
308
|
+
if (!this.#channelCores.has(channel.name)) {
|
|
309
|
+
this.#channelCores.set(channel.name, new Map())
|
|
310
|
+
}
|
|
311
|
+
this.#channelCores.get(channel.name).set(coreKeyHex, core)
|
|
312
|
+
this.#channelLocalCoreKey.set(channel.name, coreKeyHex)
|
|
248
313
|
this.#channelPeers.set(channel.name, new Map())
|
|
249
314
|
this.#setupChannelAppendListener(core, channel.name)
|
|
315
|
+
const remoteCoreKeys = Array.isArray(channel.remoteCoreKeys)
|
|
316
|
+
? channel.remoteCoreKeys
|
|
317
|
+
: []
|
|
318
|
+
for (const remoteCoreKey of remoteCoreKeys) {
|
|
319
|
+
if (remoteCoreKey && remoteCoreKey !== coreKeyHex) {
|
|
320
|
+
await this.#openRemoteChannelCore(channel.name, remoteCoreKey)
|
|
321
|
+
}
|
|
322
|
+
}
|
|
250
323
|
|
|
251
324
|
const discoveryKey = b4a.from(channel.discoveryKey, 'hex')
|
|
252
325
|
const chatDiscoveryKey = this.#generateChannelChatDiscoveryKey(
|
|
@@ -274,6 +347,7 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
274
347
|
this.#initialized = true
|
|
275
348
|
console.log(`[MostBox] Engine initialized successfully`)
|
|
276
349
|
this.emit('ready')
|
|
350
|
+
this.#resumeHoldingsInBackground()
|
|
277
351
|
|
|
278
352
|
return this
|
|
279
353
|
}
|
|
@@ -293,21 +367,15 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
293
367
|
}
|
|
294
368
|
this.#activeDownloads.clear()
|
|
295
369
|
|
|
370
|
+
await Promise.allSettled(
|
|
371
|
+
[...this.#fileMonitors.values()].map(item => this.#closeFileMonitor(item))
|
|
372
|
+
)
|
|
373
|
+
this.#fileMonitors.clear()
|
|
296
374
|
await Promise.allSettled([...this.#drives.values()].map(d => d.close()))
|
|
297
375
|
this.#drives.clear()
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
await core.close()
|
|
302
|
-
} catch (err) {
|
|
303
|
-
console.warn('[MostBox] Failed to close channel core:', err.message)
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
this.#channelCores.clear()
|
|
307
|
-
this.#channelDiscoveries.clear()
|
|
308
|
-
this.#channelChatDiscoveries.clear()
|
|
309
|
-
this.#channelPeers.clear()
|
|
310
|
-
this.#channels = []
|
|
376
|
+
this.#fileDiscoveries.clear()
|
|
377
|
+
this.#seedStates.clear()
|
|
378
|
+
this.#holdingResumeTask = null
|
|
311
379
|
|
|
312
380
|
if (this.#swarm) {
|
|
313
381
|
await this.#swarm.destroy()
|
|
@@ -319,6 +387,22 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
319
387
|
this.#chatSwarm = null
|
|
320
388
|
}
|
|
321
389
|
|
|
390
|
+
for (const [, coresMap] of this.#channelCores) {
|
|
391
|
+
for (const [, core] of coresMap) {
|
|
392
|
+
try {
|
|
393
|
+
await core.close()
|
|
394
|
+
} catch (err) {
|
|
395
|
+
console.warn('[MostBox] Failed to close channel core:', err.message)
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
this.#channelCores.clear()
|
|
400
|
+
this.#channelLocalCoreKey.clear()
|
|
401
|
+
this.#channelDiscoveries.clear()
|
|
402
|
+
this.#channelChatDiscoveries.clear()
|
|
403
|
+
this.#channelPeers.clear()
|
|
404
|
+
this.#channels = []
|
|
405
|
+
|
|
322
406
|
if (this.#store) {
|
|
323
407
|
await this.#store.close()
|
|
324
408
|
this.#store = null
|
|
@@ -359,10 +443,13 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
359
443
|
* Hyperdrive 中存储 key 为 '/' + cid,metadata 中存储 displayName(用户看到的路径)
|
|
360
444
|
* @param {string|Buffer} content - 文件路径(字符串)或内容(Buffer)
|
|
361
445
|
* @param {string} [fileName] - 文件名(Buffer 输入时必填)
|
|
446
|
+
* @param {object} [options] - 发布选项
|
|
447
|
+
* @param {string|null} [options.localPath] - 持有记录中的本地路径
|
|
362
448
|
* @returns {Promise<{ cid: string, link: string, fileName: string }>}
|
|
363
449
|
*/
|
|
364
|
-
async publishFile(content, fileName) {
|
|
450
|
+
async publishFile(content, fileName, options = {}) {
|
|
365
451
|
this.#ensureInitialized()
|
|
452
|
+
const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
|
|
366
453
|
|
|
367
454
|
let cleanPath = null
|
|
368
455
|
let safeFileName
|
|
@@ -402,6 +489,8 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
402
489
|
)
|
|
403
490
|
}
|
|
404
491
|
|
|
492
|
+
this.#checkCapacity(fileSize)
|
|
493
|
+
|
|
405
494
|
this.emit('publish:progress', {
|
|
406
495
|
stage: 'calculating-cid',
|
|
407
496
|
file: safeFileName,
|
|
@@ -409,24 +498,37 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
409
498
|
|
|
410
499
|
const { cid: rootCid } = await calculateCid(content)
|
|
411
500
|
const cidString = rootCid.toString()
|
|
501
|
+
const { driveName: name } = this.#getCidInfo(cidString)
|
|
502
|
+
const holdingLocalPath =
|
|
503
|
+
options.localPath === undefined ? cleanPath : options.localPath
|
|
412
504
|
|
|
413
505
|
// 检查相同内容是否已存在
|
|
414
506
|
const existingIndex = this.#publishedFiles.findIndex(
|
|
415
|
-
f => f.cid === cidString
|
|
507
|
+
f => f.cid === cidString && this.#recordMatchesOwner(f, ownerAddress)
|
|
416
508
|
)
|
|
417
509
|
if (existingIndex !== -1) {
|
|
418
510
|
const existing = this.#publishedFiles[existingIndex]
|
|
511
|
+
await this.#joinCidTopicInternal(cidString, {
|
|
512
|
+
server: true,
|
|
513
|
+
client: false,
|
|
514
|
+
})
|
|
515
|
+
this.#upsertHolding({
|
|
516
|
+
cid: cidString,
|
|
517
|
+
fileName: existing.fileName,
|
|
518
|
+
size: fileSize,
|
|
519
|
+
localPath: holdingLocalPath,
|
|
520
|
+
driveName: name,
|
|
521
|
+
source: 'published',
|
|
522
|
+
})
|
|
419
523
|
return {
|
|
420
524
|
cid: cidString,
|
|
421
|
-
link: `most://${cidString}`,
|
|
525
|
+
link: `most://${cidString}?filename=${encodeURIComponent(existing.fileName)}`,
|
|
422
526
|
fileName: existing.fileName,
|
|
423
527
|
alreadyExists: true,
|
|
424
528
|
}
|
|
425
529
|
}
|
|
426
530
|
|
|
427
531
|
// 获取或创建该 CID 对应的 drive
|
|
428
|
-
const hashHex = b4a.toString(rootCid.multihash.digest, 'hex')
|
|
429
|
-
const name = `drive-${hashHex}`
|
|
430
532
|
let drive = this.#drives.get(name)
|
|
431
533
|
|
|
432
534
|
if (!drive) {
|
|
@@ -434,12 +536,11 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
434
536
|
server: true,
|
|
435
537
|
client: false,
|
|
436
538
|
})
|
|
437
|
-
const discovery = this.#swarm.join(drive.discoveryKey, {
|
|
438
|
-
server: true,
|
|
439
|
-
client: false,
|
|
440
|
-
})
|
|
441
|
-
await discovery.flushed()
|
|
442
539
|
}
|
|
540
|
+
await this.#joinCidTopicInternal(cidString, {
|
|
541
|
+
server: true,
|
|
542
|
+
client: false,
|
|
543
|
+
})
|
|
443
544
|
|
|
444
545
|
this.emit('publish:progress', { stage: 'uploading', file: safeFileName })
|
|
445
546
|
|
|
@@ -488,8 +589,17 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
488
589
|
driveName: name,
|
|
489
590
|
publishedAt: new Date().toISOString(),
|
|
490
591
|
starred: false,
|
|
592
|
+
ownerAddress,
|
|
491
593
|
})
|
|
492
594
|
this.#savePublishedMetadata()
|
|
595
|
+
this.#upsertHolding({
|
|
596
|
+
cid: cidString,
|
|
597
|
+
fileName: safeFileName,
|
|
598
|
+
size: fileSize,
|
|
599
|
+
localPath: holdingLocalPath,
|
|
600
|
+
driveName: name,
|
|
601
|
+
source: 'published',
|
|
602
|
+
})
|
|
493
603
|
|
|
494
604
|
const result = {
|
|
495
605
|
cid: cidString,
|
|
@@ -505,13 +615,19 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
505
615
|
* 从 P2P 网络下载文件
|
|
506
616
|
* @param {string} link - most:// 链接
|
|
507
617
|
* @param {string} [taskId] - 用于取消的任务 ID
|
|
618
|
+
* @param {object} [options] - 下载选项
|
|
619
|
+
* @param {number} [options.timeout] - 等待 P2P 内容的超时时间(毫秒)
|
|
620
|
+
* @param {number} [options.streamReadTimeout] - 下载流无进度超时时间(毫秒)
|
|
508
621
|
* @returns {Promise<{ taskId: string, fileName: string, savedPath: string, alreadyExists?: boolean }>}
|
|
509
622
|
*/
|
|
510
|
-
async downloadFile(link, taskId = null) {
|
|
623
|
+
async downloadFile(link, taskId = null, options = {}) {
|
|
511
624
|
this.#ensureInitialized()
|
|
625
|
+
const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
|
|
512
626
|
|
|
513
627
|
taskId =
|
|
514
628
|
taskId || `dl_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
|
|
629
|
+
const downloadTimeout = options.timeout || this.#options.downloadTimeout
|
|
630
|
+
const streamReadTimeout = options.streamReadTimeout ?? STREAM_READ_TIMEOUT
|
|
515
631
|
|
|
516
632
|
console.log(
|
|
517
633
|
`[MostBox] Starting download for link: ${link} (taskId: ${taskId})`
|
|
@@ -527,10 +643,32 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
527
643
|
}
|
|
528
644
|
const cidString = parsed.cid
|
|
529
645
|
console.log(`[MostBox] Parsed CID: ${cidString}`)
|
|
646
|
+
const { driveName: name } = this.#getCidInfo(cidString)
|
|
530
647
|
|
|
531
|
-
const existingFile = this.#publishedFiles.find(
|
|
648
|
+
const existingFile = this.#publishedFiles.find(
|
|
649
|
+
f => f.cid === cidString && this.#recordMatchesOwner(f, ownerAddress)
|
|
650
|
+
)
|
|
532
651
|
if (existingFile) {
|
|
533
652
|
console.log(`[MostBox] File already exists: ${existingFile.fileName}`)
|
|
653
|
+
const existingHolding = this.#holdings.find(
|
|
654
|
+
item => item.cid === cidString
|
|
655
|
+
)
|
|
656
|
+
const existingSize = Number(existingFile.size)
|
|
657
|
+
await this.#joinCidTopicInternal(cidString, {
|
|
658
|
+
server: true,
|
|
659
|
+
client: false,
|
|
660
|
+
})
|
|
661
|
+
this.#upsertHolding({
|
|
662
|
+
cid: cidString,
|
|
663
|
+
fileName: existingFile.fileName,
|
|
664
|
+
size:
|
|
665
|
+
existingHolding?.size ??
|
|
666
|
+
(Number.isFinite(existingSize) ? existingSize : 0),
|
|
667
|
+
localPath:
|
|
668
|
+
existingHolding?.localPath || existingFile.localPath || null,
|
|
669
|
+
driveName: existingFile.driveName || name,
|
|
670
|
+
source: existingHolding?.source || 'published',
|
|
671
|
+
})
|
|
534
672
|
return {
|
|
535
673
|
taskId,
|
|
536
674
|
fileName: existingFile.fileName,
|
|
@@ -540,12 +678,8 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
540
678
|
|
|
541
679
|
const linkFileName = parsed.fileName
|
|
542
680
|
|
|
543
|
-
const parsedCid = CID.parse(cidString)
|
|
544
|
-
const hashHex = b4a.toString(parsedCid.multihash.digest, 'hex')
|
|
545
|
-
|
|
546
681
|
if (taskState.aborted) throw new Error('Download cancelled')
|
|
547
682
|
|
|
548
|
-
const name = `drive-${hashHex}`
|
|
549
683
|
let drive = this.#drives.get(name)
|
|
550
684
|
|
|
551
685
|
if (!drive) {
|
|
@@ -556,35 +690,35 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
556
690
|
})
|
|
557
691
|
|
|
558
692
|
this.emit('download:status', { taskId, status: 'connecting' })
|
|
559
|
-
|
|
560
|
-
console.log(`[MostBox] Joining swarm for drive discovery...`)
|
|
561
|
-
await this.#swarm
|
|
562
|
-
.join(drive.discoveryKey, { server: true, client: true })
|
|
563
|
-
.flushed()
|
|
564
|
-
console.log(`[MostBox] Swarm join flushed`)
|
|
565
693
|
} else {
|
|
566
694
|
console.log(`[MostBox] Using existing drive: ${name}`)
|
|
567
695
|
}
|
|
696
|
+
await this.#joinCidTopicInternal(cidString, {
|
|
697
|
+
server: false,
|
|
698
|
+
client: true,
|
|
699
|
+
})
|
|
568
700
|
|
|
569
701
|
if (taskState.aborted) throw new Error('Download cancelled')
|
|
570
702
|
|
|
571
703
|
this.emit('download:status', { taskId, status: 'finding-peers' })
|
|
572
704
|
|
|
573
705
|
console.log(
|
|
574
|
-
`[MostBox] Waiting for drive
|
|
706
|
+
`[MostBox] Waiting for drive entry /${cidString} (timeout: ${downloadTimeout / 1000}s)...`
|
|
575
707
|
)
|
|
576
|
-
const
|
|
708
|
+
const driveKey = '/' + cidString
|
|
709
|
+
const entry = await this.#waitForDriveEntry(
|
|
577
710
|
drive,
|
|
578
|
-
|
|
711
|
+
driveKey,
|
|
712
|
+
downloadTimeout,
|
|
579
713
|
taskId,
|
|
580
714
|
taskState
|
|
581
715
|
)
|
|
582
716
|
|
|
583
|
-
if (
|
|
584
|
-
console.log(`[MostBox]
|
|
717
|
+
if (!entry) {
|
|
718
|
+
console.log(`[MostBox] Expected drive entry ${driveKey} not found`)
|
|
585
719
|
|
|
586
720
|
const peerCount = this.#swarm.connections.size
|
|
587
|
-
let errorMessage =
|
|
721
|
+
let errorMessage = `Expected file ${driveKey} was not found. `
|
|
588
722
|
|
|
589
723
|
if (peerCount === 0) {
|
|
590
724
|
errorMessage +=
|
|
@@ -606,10 +740,10 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
606
740
|
if (taskState.aborted) throw new Error('Download cancelled')
|
|
607
741
|
|
|
608
742
|
console.log(
|
|
609
|
-
`[MostBox] Found ${
|
|
743
|
+
`[MostBox] Found expected entry ${driveKey}, starting download...`
|
|
610
744
|
)
|
|
611
745
|
|
|
612
|
-
const targetDir = this.#options.
|
|
746
|
+
const targetDir = this.#options.downloadPath
|
|
613
747
|
|
|
614
748
|
const writableCheck = await checkDirectoryWritable(targetDir)
|
|
615
749
|
if (!writableCheck.writable) {
|
|
@@ -617,6 +751,7 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
617
751
|
}
|
|
618
752
|
|
|
619
753
|
// 下载文件
|
|
754
|
+
const entries = [entry]
|
|
620
755
|
for (const entry of entries) {
|
|
621
756
|
const cleanKey = entry.key.replace(/^[\/\\]/, '')
|
|
622
757
|
const sanitizedFileName = linkFileName
|
|
@@ -633,7 +768,15 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
633
768
|
// 忽略
|
|
634
769
|
}
|
|
635
770
|
|
|
771
|
+
if (totalBytes > 0) {
|
|
772
|
+
this.#checkCapacity(totalBytes)
|
|
773
|
+
}
|
|
774
|
+
|
|
636
775
|
const savePath = path.join(targetDir, sanitizedFileName)
|
|
776
|
+
fs.mkdirSync(path.dirname(savePath), { recursive: true })
|
|
777
|
+
if (fs.existsSync(savePath)) {
|
|
778
|
+
throw new ConflictError(`已有同名文件: ${sanitizedFileName}`)
|
|
779
|
+
}
|
|
637
780
|
|
|
638
781
|
this.emit('download:status', {
|
|
639
782
|
taskId,
|
|
@@ -652,14 +795,54 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
652
795
|
let lastProgressUpdate = 0
|
|
653
796
|
|
|
654
797
|
await new Promise((resolve, reject) => {
|
|
798
|
+
let settled = false
|
|
799
|
+
let readTimer = null
|
|
800
|
+
|
|
801
|
+
const clearReadTimer = () => {
|
|
802
|
+
if (readTimer) {
|
|
803
|
+
clearTimeout(readTimer)
|
|
804
|
+
readTimer = null
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
const fail = err => {
|
|
809
|
+
if (settled) return
|
|
810
|
+
settled = true
|
|
811
|
+
clearReadTimer()
|
|
812
|
+
rs.destroy(err)
|
|
813
|
+
ws.destroy()
|
|
814
|
+
fs.unlink(savePath, () => {})
|
|
815
|
+
reject(err)
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
const complete = () => {
|
|
819
|
+
if (settled) return
|
|
820
|
+
settled = true
|
|
821
|
+
clearReadTimer()
|
|
822
|
+
resolve()
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
const resetReadTimer = () => {
|
|
826
|
+
clearReadTimer()
|
|
827
|
+
if (streamReadTimeout > 0) {
|
|
828
|
+
readTimer = setTimeout(() => {
|
|
829
|
+
fail(
|
|
830
|
+
new Error(
|
|
831
|
+
`Download stalled: no data received for ${streamReadTimeout / 1000}s`
|
|
832
|
+
)
|
|
833
|
+
)
|
|
834
|
+
}, streamReadTimeout)
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
resetReadTimer()
|
|
839
|
+
|
|
655
840
|
rs.on('data', chunk => {
|
|
656
841
|
if (taskState.aborted) {
|
|
657
|
-
|
|
658
|
-
ws.destroy()
|
|
659
|
-
fs.unlink(savePath, () => {})
|
|
660
|
-
reject(new Error('Download cancelled'))
|
|
842
|
+
fail(new Error('Download cancelled'))
|
|
661
843
|
return
|
|
662
844
|
}
|
|
845
|
+
resetReadTimer()
|
|
663
846
|
loadedBytes += chunk.length
|
|
664
847
|
const now = Date.now()
|
|
665
848
|
if (
|
|
@@ -678,9 +861,19 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
678
861
|
})
|
|
679
862
|
|
|
680
863
|
rs.pipe(ws)
|
|
681
|
-
ws.on('finish',
|
|
682
|
-
ws.on('error',
|
|
683
|
-
rs.on('error',
|
|
864
|
+
ws.on('finish', complete)
|
|
865
|
+
ws.on('error', fail)
|
|
866
|
+
rs.on('error', fail)
|
|
867
|
+
rs.on('close', () => {
|
|
868
|
+
if (taskState.aborted) {
|
|
869
|
+
fail(new Error('Download cancelled'))
|
|
870
|
+
}
|
|
871
|
+
})
|
|
872
|
+
ws.on('close', () => {
|
|
873
|
+
if (taskState.aborted) {
|
|
874
|
+
fail(new Error('Download cancelled'))
|
|
875
|
+
}
|
|
876
|
+
})
|
|
684
877
|
})
|
|
685
878
|
|
|
686
879
|
if (taskState.aborted) throw new Error('Download cancelled')
|
|
@@ -688,13 +881,12 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
688
881
|
this.emit('download:status', { taskId, status: 'verifying' })
|
|
689
882
|
|
|
690
883
|
const { cid: downloadedCid } = await calculateCid(savePath)
|
|
691
|
-
const
|
|
692
|
-
const actualHash = b4a.toString(downloadedCid.multihash.digest, 'hex')
|
|
884
|
+
const downloadedCidString = downloadedCid.toString()
|
|
693
885
|
|
|
694
|
-
if (
|
|
886
|
+
if (downloadedCidString !== cidString) {
|
|
695
887
|
fs.unlinkSync(savePath)
|
|
696
888
|
throw new IntegrityError(
|
|
697
|
-
`File content CID mismatch.
|
|
889
|
+
`File content CID mismatch. Expected ${cidString}, got ${downloadedCidString}.`
|
|
698
890
|
)
|
|
699
891
|
}
|
|
700
892
|
|
|
@@ -710,7 +902,17 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
710
902
|
writeStream.on('error', reject)
|
|
711
903
|
readStream.on('error', reject)
|
|
712
904
|
})
|
|
905
|
+
const verifyEntry = await drive.entry(driveKey)
|
|
906
|
+
if (!verifyEntry || !verifyEntry.value || !verifyEntry.value.blob) {
|
|
907
|
+
throw new IntegrityError(
|
|
908
|
+
`Failed to write file to Hyperdrive for seeding: ${driveKey}`
|
|
909
|
+
)
|
|
910
|
+
}
|
|
713
911
|
}
|
|
912
|
+
await this.#joinCidTopicInternal(cidString, {
|
|
913
|
+
server: true,
|
|
914
|
+
client: false,
|
|
915
|
+
})
|
|
714
916
|
|
|
715
917
|
const result = {
|
|
716
918
|
taskId,
|
|
@@ -720,7 +922,7 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
720
922
|
|
|
721
923
|
// 将下载的文件添加到已发布文件列表(displayName 用原始文件名)
|
|
722
924
|
const existingIndex = this.#publishedFiles.findIndex(
|
|
723
|
-
f => f.cid === cidString
|
|
925
|
+
f => f.cid === cidString && this.#recordMatchesOwner(f, ownerAddress)
|
|
724
926
|
)
|
|
725
927
|
if (existingIndex !== -1) {
|
|
726
928
|
const existing = this.#publishedFiles[existingIndex]
|
|
@@ -735,9 +937,19 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
735
937
|
driveName: name,
|
|
736
938
|
publishedAt: new Date().toISOString(),
|
|
737
939
|
starred: false,
|
|
940
|
+
ownerAddress,
|
|
738
941
|
})
|
|
739
942
|
}
|
|
740
943
|
this.#savePublishedMetadata()
|
|
944
|
+
const savedSize = totalBytes || fs.statSync(savePath).size
|
|
945
|
+
this.#upsertHolding({
|
|
946
|
+
cid: cidString,
|
|
947
|
+
fileName: sanitizedFileName,
|
|
948
|
+
size: savedSize,
|
|
949
|
+
localPath: savePath,
|
|
950
|
+
driveName: name,
|
|
951
|
+
source: 'downloaded',
|
|
952
|
+
})
|
|
741
953
|
|
|
742
954
|
this.emit('download:success', result)
|
|
743
955
|
return result
|
|
@@ -747,6 +959,84 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
747
959
|
}
|
|
748
960
|
}
|
|
749
961
|
|
|
962
|
+
/**
|
|
963
|
+
* 检测 most:// 链接当前是否能找到可下载内容,但不读取文件内容。
|
|
964
|
+
* @param {string} link - most:// 链接
|
|
965
|
+
* @param {object} [options] - 检测选项
|
|
966
|
+
* @param {number} [options.timeout] - 等待 P2P 内容的超时时间(毫秒)
|
|
967
|
+
* @returns {Promise<{ available: boolean, cid: string, fileName: string, size: number|null, alreadyExists?: boolean }>}
|
|
968
|
+
*/
|
|
969
|
+
async checkDownloadAvailability(link, options = {}) {
|
|
970
|
+
this.#ensureInitialized()
|
|
971
|
+
const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
|
|
972
|
+
|
|
973
|
+
const timeout = options.timeout || DRIVE_ENTRY_TIMEOUT
|
|
974
|
+
const parsed = parseMostLink(link)
|
|
975
|
+
if (parsed.error) {
|
|
976
|
+
throw new ValidationError(parsed.error)
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
const cidString = parsed.cid
|
|
980
|
+
const { driveName: name } = this.#getCidInfo(cidString)
|
|
981
|
+
const existingFile = this.#publishedFiles.find(
|
|
982
|
+
f => f.cid === cidString && this.#recordMatchesOwner(f, ownerAddress)
|
|
983
|
+
)
|
|
984
|
+
if (existingFile) {
|
|
985
|
+
return {
|
|
986
|
+
available: true,
|
|
987
|
+
cid: cidString,
|
|
988
|
+
fileName: existingFile.fileName,
|
|
989
|
+
size: Number(existingFile.size) || null,
|
|
990
|
+
alreadyExists: true,
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
const writableCheck = await checkDirectoryWritable(
|
|
995
|
+
this.#options.downloadPath
|
|
996
|
+
)
|
|
997
|
+
if (!writableCheck.writable) {
|
|
998
|
+
throw new PermissionError(writableCheck.error)
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
let drive = this.#drives.get(name)
|
|
1002
|
+
|
|
1003
|
+
if (!drive) {
|
|
1004
|
+
drive = await this.#getOrCreateDrive(name, {
|
|
1005
|
+
server: true,
|
|
1006
|
+
client: true,
|
|
1007
|
+
})
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
await this.#joinCidTopicInternal(cidString, {
|
|
1011
|
+
server: false,
|
|
1012
|
+
client: true,
|
|
1013
|
+
})
|
|
1014
|
+
|
|
1015
|
+
const driveKey = '/' + cidString
|
|
1016
|
+
const entry = await this.#waitForDriveEntry(drive, driveKey, timeout)
|
|
1017
|
+
|
|
1018
|
+
if (!entry) {
|
|
1019
|
+
throw new PeerNotFoundError(
|
|
1020
|
+
'当前没有发现可下载的在线种子,请稍后重试或确认发布者在线'
|
|
1021
|
+
)
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
let size = null
|
|
1025
|
+
try {
|
|
1026
|
+
const stat = await drive.entry(entry.key)
|
|
1027
|
+
if (stat?.value?.blob) {
|
|
1028
|
+
size = stat.value.blob.byteLength || 0
|
|
1029
|
+
}
|
|
1030
|
+
} catch {}
|
|
1031
|
+
|
|
1032
|
+
return {
|
|
1033
|
+
available: true,
|
|
1034
|
+
cid: cidString,
|
|
1035
|
+
fileName: parsed.fileName,
|
|
1036
|
+
size,
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
|
|
750
1040
|
/**
|
|
751
1041
|
* 列出所有已发布文件
|
|
752
1042
|
* @param {object} [options] - 筛选选项
|
|
@@ -756,6 +1046,11 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
756
1046
|
listPublishedFiles(options = {}) {
|
|
757
1047
|
this.#ensureInitialized()
|
|
758
1048
|
let files = this.#publishedFiles
|
|
1049
|
+
const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
|
|
1050
|
+
|
|
1051
|
+
if (ownerAddress) {
|
|
1052
|
+
files = files.filter(f => this.#recordMatchesOwner(f, ownerAddress))
|
|
1053
|
+
}
|
|
759
1054
|
|
|
760
1055
|
if (options.starred === true) {
|
|
761
1056
|
files = files.filter(f => f.starred === true)
|
|
@@ -764,9 +1059,10 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
764
1059
|
return files.map(f => ({
|
|
765
1060
|
fileName: f.fileName,
|
|
766
1061
|
cid: f.cid,
|
|
767
|
-
link: `most://${f.cid}`,
|
|
1062
|
+
link: `most://${f.cid}?filename=${encodeURIComponent(f.fileName)}`,
|
|
768
1063
|
publishedAt: f.publishedAt,
|
|
769
1064
|
starred: f.starred || false,
|
|
1065
|
+
ownerAddress: f.ownerAddress || '',
|
|
770
1066
|
}))
|
|
771
1067
|
}
|
|
772
1068
|
|
|
@@ -775,9 +1071,12 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
775
1071
|
* @param {string} cid - 文件的 CID
|
|
776
1072
|
* @returns {object} 更新后的文件信息
|
|
777
1073
|
*/
|
|
778
|
-
toggleStarred(cid) {
|
|
1074
|
+
toggleStarred(cid, options = {}) {
|
|
779
1075
|
this.#ensureInitialized()
|
|
780
|
-
const
|
|
1076
|
+
const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
|
|
1077
|
+
const index = this.#publishedFiles.findIndex(
|
|
1078
|
+
f => f.cid === cid && this.#recordMatchesOwner(f, ownerAddress)
|
|
1079
|
+
)
|
|
781
1080
|
if (index === -1) {
|
|
782
1081
|
throw new Error('File not found')
|
|
783
1082
|
}
|
|
@@ -794,40 +1093,62 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
794
1093
|
* @param {string} cid - 要删除文件的 CID
|
|
795
1094
|
* @returns {Promise<Array>} 更新后的已发布文件列表
|
|
796
1095
|
*/
|
|
797
|
-
async deletePublishedFile(cid) {
|
|
1096
|
+
async deletePublishedFile(cid, options = {}) {
|
|
798
1097
|
this.#ensureInitialized()
|
|
799
|
-
const
|
|
1098
|
+
const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
|
|
1099
|
+
const index = this.#publishedFiles.findIndex(
|
|
1100
|
+
f => f.cid === cid && this.#recordMatchesOwner(f, ownerAddress)
|
|
1101
|
+
)
|
|
800
1102
|
if (index !== -1) {
|
|
801
1103
|
const fileRecord = this.#publishedFiles[index]
|
|
1104
|
+
const holding = this.#holdings.find(item => item.cid === fileRecord.cid)
|
|
802
1105
|
|
|
803
1106
|
this.#trashFiles.push({
|
|
804
1107
|
fileName: fileRecord.fileName,
|
|
805
1108
|
cid: fileRecord.cid,
|
|
806
|
-
driveName:
|
|
1109
|
+
driveName:
|
|
1110
|
+
fileRecord.driveName || this.#getCidInfo(fileRecord.cid).driveName,
|
|
1111
|
+
size: holding?.size ?? fileRecord.size ?? 0,
|
|
1112
|
+
localPath: holding?.localPath || fileRecord.localPath || null,
|
|
1113
|
+
source: holding?.source || 'published',
|
|
807
1114
|
publishedAt: fileRecord.publishedAt,
|
|
808
1115
|
starred: fileRecord.starred || false,
|
|
1116
|
+
ownerAddress: fileRecord.ownerAddress || ownerAddress,
|
|
809
1117
|
deletedAt: new Date().toISOString(),
|
|
810
1118
|
})
|
|
811
1119
|
this.#saveTrashMetadata()
|
|
812
1120
|
|
|
813
1121
|
this.#publishedFiles.splice(index, 1)
|
|
814
1122
|
this.#savePublishedMetadata()
|
|
1123
|
+
|
|
1124
|
+
if (!this.#hasPublishedReference(fileRecord.cid)) {
|
|
1125
|
+
await this.#leaveCidTopic(fileRecord.cid)
|
|
1126
|
+
await this.#closeDriveForSeed(
|
|
1127
|
+
fileRecord.driveName || this.#getCidInfo(fileRecord.cid).driveName
|
|
1128
|
+
)
|
|
1129
|
+
this.#removeHolding(fileRecord.cid)
|
|
1130
|
+
}
|
|
815
1131
|
}
|
|
816
|
-
return this.listPublishedFiles()
|
|
1132
|
+
return this.listPublishedFiles({ ownerAddress })
|
|
817
1133
|
}
|
|
818
1134
|
|
|
819
1135
|
/**
|
|
820
1136
|
* 列出回收站中的所有文件
|
|
821
1137
|
* @returns {Array} 回收站文件
|
|
822
1138
|
*/
|
|
823
|
-
listTrashFiles() {
|
|
1139
|
+
listTrashFiles(options = {}) {
|
|
824
1140
|
this.#ensureInitialized()
|
|
825
|
-
|
|
1141
|
+
const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
|
|
1142
|
+
const files = ownerAddress
|
|
1143
|
+
? this.#trashFiles.filter(f => this.#recordMatchesOwner(f, ownerAddress))
|
|
1144
|
+
: this.#trashFiles
|
|
1145
|
+
return files.map(f => ({
|
|
826
1146
|
fileName: f.fileName,
|
|
827
1147
|
cid: f.cid,
|
|
828
|
-
link: `most://${f.cid}`,
|
|
1148
|
+
link: `most://${f.cid}?filename=${encodeURIComponent(f.fileName)}`,
|
|
829
1149
|
publishedAt: f.publishedAt,
|
|
830
1150
|
starred: f.starred || false,
|
|
1151
|
+
ownerAddress: f.ownerAddress || '',
|
|
831
1152
|
deletedAt: f.deletedAt,
|
|
832
1153
|
}))
|
|
833
1154
|
}
|
|
@@ -835,20 +1156,21 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
835
1156
|
/**
|
|
836
1157
|
* 从回收站恢复文件
|
|
837
1158
|
* @param {string} cid - 要恢复文件的 CID
|
|
838
|
-
* @returns {Array} 更新后的已发布文件列表
|
|
1159
|
+
* @returns {Promise<Array>} 更新后的已发布文件列表
|
|
839
1160
|
*/
|
|
840
|
-
restoreTrashFile(cid) {
|
|
1161
|
+
async restoreTrashFile(cid, options = {}) {
|
|
841
1162
|
this.#ensureInitialized()
|
|
842
|
-
const
|
|
1163
|
+
const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
|
|
1164
|
+
const index = this.#trashFiles.findIndex(
|
|
1165
|
+
f => f.cid === cid && this.#recordMatchesOwner(f, ownerAddress)
|
|
1166
|
+
)
|
|
843
1167
|
if (index === -1) {
|
|
844
1168
|
throw new Error('File not found in trash')
|
|
845
1169
|
}
|
|
846
1170
|
|
|
847
1171
|
const fileRecord = this.#trashFiles[index]
|
|
848
1172
|
|
|
849
|
-
const
|
|
850
|
-
const hashHex = b4a.toString(parsedCid.multihash.digest, 'hex')
|
|
851
|
-
const driveName = `drive-${hashHex}`
|
|
1173
|
+
const { driveName } = this.#getCidInfo(fileRecord.cid)
|
|
852
1174
|
|
|
853
1175
|
this.#publishedFiles.push({
|
|
854
1176
|
fileName: fileRecord.fileName,
|
|
@@ -856,13 +1178,27 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
856
1178
|
driveName,
|
|
857
1179
|
publishedAt: fileRecord.publishedAt,
|
|
858
1180
|
starred: fileRecord.starred || false,
|
|
1181
|
+
ownerAddress: fileRecord.ownerAddress || ownerAddress,
|
|
859
1182
|
})
|
|
860
1183
|
this.#savePublishedMetadata()
|
|
861
1184
|
|
|
862
1185
|
this.#trashFiles.splice(index, 1)
|
|
863
1186
|
this.#saveTrashMetadata()
|
|
864
1187
|
|
|
865
|
-
|
|
1188
|
+
await this.#joinCidTopicInternal(fileRecord.cid, {
|
|
1189
|
+
server: true,
|
|
1190
|
+
client: false,
|
|
1191
|
+
})
|
|
1192
|
+
this.#upsertHolding({
|
|
1193
|
+
cid: fileRecord.cid,
|
|
1194
|
+
fileName: fileRecord.fileName,
|
|
1195
|
+
size: Number(fileRecord.size) || 0,
|
|
1196
|
+
localPath: fileRecord.localPath || null,
|
|
1197
|
+
driveName,
|
|
1198
|
+
source: fileRecord.source || 'published',
|
|
1199
|
+
})
|
|
1200
|
+
|
|
1201
|
+
return this.listPublishedFiles({ ownerAddress })
|
|
866
1202
|
}
|
|
867
1203
|
|
|
868
1204
|
/**
|
|
@@ -870,68 +1206,81 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
870
1206
|
* @param {string} cid - 要永久删除文件的 CID
|
|
871
1207
|
* @returns {Promise<Array>} 更新后的回收站列表
|
|
872
1208
|
*/
|
|
873
|
-
async permanentDeleteTrashFile(cid) {
|
|
1209
|
+
async permanentDeleteTrashFile(cid, options = {}) {
|
|
874
1210
|
this.#ensureInitialized()
|
|
875
|
-
const
|
|
1211
|
+
const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
|
|
1212
|
+
const index = this.#trashFiles.findIndex(
|
|
1213
|
+
f => f.cid === cid && this.#recordMatchesOwner(f, ownerAddress)
|
|
1214
|
+
)
|
|
876
1215
|
if (index !== -1) {
|
|
877
1216
|
const fileRecord = this.#trashFiles[index]
|
|
878
|
-
const driveName =
|
|
1217
|
+
const driveName =
|
|
1218
|
+
fileRecord.driveName || this.#getCidInfo(fileRecord.cid).driveName
|
|
1219
|
+
|
|
1220
|
+
this.#trashFiles.splice(index, 1)
|
|
1221
|
+
this.#saveTrashMetadata()
|
|
879
1222
|
|
|
880
|
-
|
|
881
|
-
if (drive) {
|
|
1223
|
+
if (!this.#hasAnyUserReference(fileRecord.cid)) {
|
|
882
1224
|
try {
|
|
1225
|
+
const drive = await this.#getOrCreateDrive(driveName)
|
|
883
1226
|
await drive.del('/' + fileRecord.cid)
|
|
884
1227
|
} catch {
|
|
885
1228
|
// 文件可能不存在于驱动器中
|
|
886
1229
|
}
|
|
887
|
-
|
|
888
|
-
await this.#
|
|
889
|
-
|
|
890
|
-
this.#drives.delete(driveName)
|
|
1230
|
+
await this.#closeDriveForSeed(driveName)
|
|
1231
|
+
await this.#leaveCidTopic(fileRecord.cid)
|
|
1232
|
+
this.#removeHolding(fileRecord.cid)
|
|
891
1233
|
}
|
|
892
|
-
|
|
893
|
-
this.#trashFiles.splice(index, 1)
|
|
894
|
-
this.#saveTrashMetadata()
|
|
895
1234
|
}
|
|
896
|
-
return this.listTrashFiles()
|
|
1235
|
+
return this.listTrashFiles({ ownerAddress })
|
|
897
1236
|
}
|
|
898
1237
|
|
|
899
1238
|
/**
|
|
900
1239
|
* 清空回收站 — 永久删除所有回收站文件
|
|
901
1240
|
* @returns {Promise<Array>} 清空后的回收站列表
|
|
902
1241
|
*/
|
|
903
|
-
async emptyTrash() {
|
|
1242
|
+
async emptyTrash(options = {}) {
|
|
904
1243
|
this.#ensureInitialized()
|
|
1244
|
+
const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
|
|
1245
|
+
const remainingTrash = []
|
|
1246
|
+
const removedTrash = []
|
|
905
1247
|
|
|
906
1248
|
for (const fileRecord of this.#trashFiles) {
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
if (drive) {
|
|
911
|
-
try {
|
|
912
|
-
await drive.del('/' + fileRecord.cid)
|
|
913
|
-
} catch {
|
|
914
|
-
// 文件可能不存在
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
await this.#swarm.leave(drive.discoveryKey)
|
|
918
|
-
await drive.close()
|
|
919
|
-
this.#drives.delete(driveName)
|
|
1249
|
+
if (ownerAddress && !this.#recordMatchesOwner(fileRecord, ownerAddress)) {
|
|
1250
|
+
remainingTrash.push(fileRecord)
|
|
1251
|
+
continue
|
|
920
1252
|
}
|
|
1253
|
+
removedTrash.push(fileRecord)
|
|
921
1254
|
}
|
|
922
1255
|
|
|
923
|
-
this.#trashFiles =
|
|
1256
|
+
this.#trashFiles = remainingTrash
|
|
924
1257
|
this.#saveTrashMetadata()
|
|
925
1258
|
|
|
926
|
-
|
|
1259
|
+
for (const fileRecord of removedTrash) {
|
|
1260
|
+
if (this.#hasAnyUserReference(fileRecord.cid)) continue
|
|
1261
|
+
const driveName =
|
|
1262
|
+
fileRecord.driveName || this.#getCidInfo(fileRecord.cid).driveName
|
|
1263
|
+
try {
|
|
1264
|
+
const drive = await this.#getOrCreateDrive(driveName)
|
|
1265
|
+
await drive.del('/' + fileRecord.cid)
|
|
1266
|
+
} catch {
|
|
1267
|
+
// 文件可能不存在
|
|
1268
|
+
}
|
|
1269
|
+
await this.#closeDriveForSeed(driveName)
|
|
1270
|
+
await this.#leaveCidTopic(fileRecord.cid)
|
|
1271
|
+
this.#removeHolding(fileRecord.cid)
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
return this.listTrashFiles({ ownerAddress })
|
|
927
1275
|
}
|
|
928
1276
|
|
|
929
1277
|
/**
|
|
930
1278
|
* 获取存储统计信息
|
|
931
1279
|
* @returns {Promise<{ total: number, used: number, free: number, fileCount: number, trashCount: number }>}
|
|
932
1280
|
*/
|
|
933
|
-
async getStorageStats() {
|
|
1281
|
+
async getStorageStats(options = {}) {
|
|
934
1282
|
this.#ensureInitialized()
|
|
1283
|
+
const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
|
|
935
1284
|
|
|
936
1285
|
let totalSize = 0
|
|
937
1286
|
let freeSize = 0
|
|
@@ -982,8 +1331,16 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
982
1331
|
total: totalSize,
|
|
983
1332
|
used: usedSize,
|
|
984
1333
|
free: freeSize,
|
|
985
|
-
fileCount:
|
|
986
|
-
|
|
1334
|
+
fileCount: ownerAddress
|
|
1335
|
+
? this.#publishedFiles.filter(f =>
|
|
1336
|
+
this.#recordMatchesOwner(f, ownerAddress)
|
|
1337
|
+
).length
|
|
1338
|
+
: this.#publishedFiles.length,
|
|
1339
|
+
trashCount: ownerAddress
|
|
1340
|
+
? this.#trashFiles.filter(f =>
|
|
1341
|
+
this.#recordMatchesOwner(f, ownerAddress)
|
|
1342
|
+
).length
|
|
1343
|
+
: this.#trashFiles.length,
|
|
987
1344
|
}
|
|
988
1345
|
}
|
|
989
1346
|
|
|
@@ -994,9 +1351,12 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
994
1351
|
* @param {string} newFileName - 新文件路径
|
|
995
1352
|
* @returns {object} 更新后的文件信息
|
|
996
1353
|
*/
|
|
997
|
-
moveFile(cid, newFileName) {
|
|
1354
|
+
moveFile(cid, newFileName, options = {}) {
|
|
998
1355
|
this.#ensureInitialized()
|
|
999
|
-
const
|
|
1356
|
+
const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
|
|
1357
|
+
const index = this.#publishedFiles.findIndex(
|
|
1358
|
+
f => f.cid === cid && this.#recordMatchesOwner(f, ownerAddress)
|
|
1359
|
+
)
|
|
1000
1360
|
if (index === -1) {
|
|
1001
1361
|
throw new Error('File not found')
|
|
1002
1362
|
}
|
|
@@ -1007,7 +1367,7 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1007
1367
|
return {
|
|
1008
1368
|
cid,
|
|
1009
1369
|
fileName: safeFileName,
|
|
1010
|
-
link: `most://${cid}`,
|
|
1370
|
+
link: `most://${cid}?filename=${encodeURIComponent(safeFileName)}`,
|
|
1011
1371
|
}
|
|
1012
1372
|
}
|
|
1013
1373
|
|
|
@@ -1018,13 +1378,17 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1018
1378
|
* @param {string} newPath - 新文件夹路径
|
|
1019
1379
|
* @returns {object} 更新后的文件信息
|
|
1020
1380
|
*/
|
|
1021
|
-
renameFolder(oldPath, newPath) {
|
|
1381
|
+
renameFolder(oldPath, newPath, options = {}) {
|
|
1022
1382
|
this.#ensureInitialized()
|
|
1383
|
+
const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
|
|
1023
1384
|
const prefix = oldPath + '/'
|
|
1024
1385
|
const updatedFiles = []
|
|
1025
1386
|
|
|
1026
1387
|
for (const file of this.#publishedFiles) {
|
|
1027
|
-
if (
|
|
1388
|
+
if (
|
|
1389
|
+
file.fileName.startsWith(prefix) &&
|
|
1390
|
+
this.#recordMatchesOwner(file, ownerAddress)
|
|
1391
|
+
) {
|
|
1028
1392
|
const remainder = file.fileName.substring(prefix.length)
|
|
1029
1393
|
const newFileName = sanitizeFilename(
|
|
1030
1394
|
remainder ? newPath + '/' + remainder : newPath
|
|
@@ -1034,7 +1398,7 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1034
1398
|
updatedFiles.push({
|
|
1035
1399
|
cid: file.cid,
|
|
1036
1400
|
fileName: file.fileName,
|
|
1037
|
-
link: `most://${file.cid}`,
|
|
1401
|
+
link: `most://${file.cid}?filename=${encodeURIComponent(file.fileName)}`,
|
|
1038
1402
|
})
|
|
1039
1403
|
}
|
|
1040
1404
|
}
|
|
@@ -1054,85 +1418,337 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1054
1418
|
const task = this.#activeDownloads.get(taskId)
|
|
1055
1419
|
if (task) {
|
|
1056
1420
|
task.aborted = true
|
|
1057
|
-
|
|
1421
|
+
const err = new Error('Download cancelled')
|
|
1422
|
+
if (task.readStream) task.readStream.destroy(err)
|
|
1058
1423
|
if (task.writeStream) task.writeStream.destroy()
|
|
1059
1424
|
}
|
|
1060
1425
|
}
|
|
1061
1426
|
|
|
1062
|
-
|
|
1063
|
-
|
|
1427
|
+
hasDownloadNameConflict(fileName) {
|
|
1428
|
+
this.#ensureInitialized()
|
|
1429
|
+
const sanitizedFileName = sanitizeFilename(fileName)
|
|
1430
|
+
const savePath = path.join(this.#options.downloadPath, sanitizedFileName)
|
|
1431
|
+
return fs.existsSync(savePath)
|
|
1064
1432
|
}
|
|
1065
1433
|
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
async readFileContent(cid, offset = 0, limit = DEFAULT_READ_LIMIT) {
|
|
1074
|
-
this.#ensureInitialized()
|
|
1434
|
+
setMaxFileSize(maxFileSize) {
|
|
1435
|
+
const parsed = Number(maxFileSize)
|
|
1436
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
1437
|
+
throw new ValidationError('maxFileSize must be a non-negative number')
|
|
1438
|
+
}
|
|
1439
|
+
this.#options.maxFileSize = Math.floor(parsed)
|
|
1440
|
+
}
|
|
1075
1441
|
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1442
|
+
getPublishedFiles(options = {}) {
|
|
1443
|
+
const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
|
|
1444
|
+
return ownerAddress
|
|
1445
|
+
? this.#publishedFiles.filter(f =>
|
|
1446
|
+
this.#recordMatchesOwner(f, ownerAddress)
|
|
1447
|
+
)
|
|
1448
|
+
: this.#publishedFiles
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
listUsers() {
|
|
1452
|
+
this.#ensureInitialized()
|
|
1453
|
+
const users = new Map()
|
|
1454
|
+
const ensure = address => {
|
|
1455
|
+
const ownerAddress = normalizeOwnerAddress(address)
|
|
1456
|
+
if (!ownerAddress) return null
|
|
1457
|
+
if (!users.has(ownerAddress)) {
|
|
1458
|
+
users.set(ownerAddress, {
|
|
1459
|
+
address: ownerAddress,
|
|
1460
|
+
fileCount: 0,
|
|
1461
|
+
trashCount: 0,
|
|
1462
|
+
cidCount: 0,
|
|
1463
|
+
cids: new Set(),
|
|
1464
|
+
})
|
|
1465
|
+
}
|
|
1466
|
+
return users.get(ownerAddress)
|
|
1079
1467
|
}
|
|
1080
1468
|
|
|
1081
|
-
const
|
|
1469
|
+
for (const file of this.#publishedFiles) {
|
|
1470
|
+
const entry = ensure(file.ownerAddress)
|
|
1471
|
+
if (!entry) continue
|
|
1472
|
+
entry.fileCount += 1
|
|
1473
|
+
entry.cids.add(file.cid)
|
|
1474
|
+
}
|
|
1475
|
+
for (const file of this.#trashFiles) {
|
|
1476
|
+
const entry = ensure(file.ownerAddress)
|
|
1477
|
+
if (!entry) continue
|
|
1478
|
+
entry.trashCount += 1
|
|
1479
|
+
entry.cids.add(file.cid)
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
return [...users.values()].map(user => ({
|
|
1483
|
+
address: user.address,
|
|
1484
|
+
fileCount: user.fileCount,
|
|
1485
|
+
trashCount: user.trashCount,
|
|
1486
|
+
cidCount: user.cids.size,
|
|
1487
|
+
}))
|
|
1488
|
+
}
|
|
1082
1489
|
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
const
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
})
|
|
1089
|
-
if (!entry || !entry.value) {
|
|
1090
|
-
throw new Error('File content not available')
|
|
1490
|
+
async clearUserData(ownerAddressInput) {
|
|
1491
|
+
this.#ensureInitialized()
|
|
1492
|
+
const ownerAddress = normalizeOwnerAddress(ownerAddressInput)
|
|
1493
|
+
if (!ownerAddress) {
|
|
1494
|
+
throw new ValidationError('valid owner address is required')
|
|
1091
1495
|
}
|
|
1092
1496
|
|
|
1093
|
-
const
|
|
1094
|
-
const
|
|
1095
|
-
|
|
1096
|
-
end: offset + limit - 1,
|
|
1097
|
-
})
|
|
1497
|
+
const affectedCids = new Set()
|
|
1498
|
+
const beforeFiles = this.#publishedFiles.length
|
|
1499
|
+
const beforeTrash = this.#trashFiles.length
|
|
1098
1500
|
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
()
|
|
1102
|
-
|
|
1103
|
-
|
|
1501
|
+
this.#publishedFiles = this.#publishedFiles.filter(file => {
|
|
1502
|
+
if (this.#recordMatchesOwner(file, ownerAddress)) {
|
|
1503
|
+
affectedCids.add(file.cid)
|
|
1504
|
+
return false
|
|
1505
|
+
}
|
|
1506
|
+
return true
|
|
1104
1507
|
})
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1508
|
+
this.#trashFiles = this.#trashFiles.filter(file => {
|
|
1509
|
+
if (this.#recordMatchesOwner(file, ownerAddress)) {
|
|
1510
|
+
affectedCids.add(file.cid)
|
|
1511
|
+
return false
|
|
1109
1512
|
}
|
|
1110
|
-
|
|
1513
|
+
return true
|
|
1514
|
+
})
|
|
1515
|
+
this.#channels = this.#channels
|
|
1516
|
+
.map(channel => ({
|
|
1517
|
+
...channel,
|
|
1518
|
+
members: Array.isArray(channel.members)
|
|
1519
|
+
? channel.members.filter(
|
|
1520
|
+
member => normalizeOwnerAddress(member) !== ownerAddress
|
|
1521
|
+
)
|
|
1522
|
+
: [],
|
|
1523
|
+
}))
|
|
1524
|
+
.filter(channel => channel.members.length > 0)
|
|
1111
1525
|
|
|
1112
|
-
|
|
1526
|
+
this.#savePublishedMetadata()
|
|
1527
|
+
this.#saveTrashMetadata()
|
|
1528
|
+
this.#saveChannelsMetadata()
|
|
1113
1529
|
|
|
1114
|
-
|
|
1115
|
-
const
|
|
1116
|
-
|
|
1530
|
+
let removedReplicas = 0
|
|
1531
|
+
for (const cid of affectedCids) {
|
|
1532
|
+
if (this.#hasAnyUserReference(cid)) continue
|
|
1533
|
+
const driveName = this.#getCidInfo(cid).driveName
|
|
1534
|
+
try {
|
|
1535
|
+
const drive = await this.#getOrCreateDrive(driveName)
|
|
1536
|
+
await drive.del('/' + cid)
|
|
1537
|
+
} catch {}
|
|
1538
|
+
await this.#closeDriveForSeed(driveName)
|
|
1539
|
+
await this.#leaveCidTopic(cid)
|
|
1540
|
+
this.#removeHolding(cid)
|
|
1541
|
+
removedReplicas += 1
|
|
1542
|
+
}
|
|
1117
1543
|
|
|
1118
|
-
return {
|
|
1544
|
+
return {
|
|
1545
|
+
ownerAddress,
|
|
1546
|
+
removedFiles: beforeFiles - this.#publishedFiles.length,
|
|
1547
|
+
removedTrashFiles: beforeTrash - this.#trashFiles.length,
|
|
1548
|
+
removedReplicas,
|
|
1549
|
+
}
|
|
1119
1550
|
}
|
|
1120
1551
|
|
|
1121
1552
|
/**
|
|
1122
|
-
*
|
|
1123
|
-
*
|
|
1124
|
-
* @param {string} cid - 文件的 CID
|
|
1125
|
-
* @param {object} [options] - 选项
|
|
1126
|
-
* @param {number} [options.offset=0] - 读取起始位置
|
|
1127
|
-
* @param {number} [options.limit] - 最大读取字节数,不指定则读取到末尾
|
|
1128
|
-
* @param {number} [options.timeout=10000] - 流读取超时(毫秒)
|
|
1129
|
-
* @returns {Promise<{buffer: Buffer, fileName: string, totalSize: number}>}
|
|
1553
|
+
* 列出当前节点持有的可做种文件副本
|
|
1554
|
+
* @returns {Array}
|
|
1130
1555
|
*/
|
|
1131
|
-
|
|
1556
|
+
listHoldings() {
|
|
1132
1557
|
this.#ensureInitialized()
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1558
|
+
return this.#holdings.map(holding => {
|
|
1559
|
+
const seedState = this.#seedStates.get(holding.cid)
|
|
1560
|
+
const status =
|
|
1561
|
+
seedState?.status ||
|
|
1562
|
+
(this.#fileDiscoveries.has(holding.cid) ? 'active' : 'queued')
|
|
1563
|
+
return {
|
|
1564
|
+
...holding,
|
|
1565
|
+
joined: status === 'active' && this.#fileDiscoveries.has(holding.cid),
|
|
1566
|
+
seedStatus: status,
|
|
1567
|
+
seedError: seedState?.error,
|
|
1568
|
+
seedStatusUpdatedAt: seedState?.updatedAt,
|
|
1569
|
+
...this.#getFileRuntimeStats(holding.cid),
|
|
1570
|
+
link: `most://${holding.cid}?filename=${encodeURIComponent(holding.fileName || holding.cid)}`,
|
|
1571
|
+
}
|
|
1572
|
+
})
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
/**
|
|
1576
|
+
* 手动记录节点已持有的文件副本
|
|
1577
|
+
* @param {object} record - 持有记录
|
|
1578
|
+
*/
|
|
1579
|
+
async addHolding(record) {
|
|
1580
|
+
this.#ensureInitialized()
|
|
1581
|
+
const holding = this.#normalizeHolding(record)
|
|
1582
|
+
await this.#joinCidTopicInternal(holding.cid, {
|
|
1583
|
+
server: true,
|
|
1584
|
+
client: false,
|
|
1585
|
+
})
|
|
1586
|
+
return this.#upsertHolding(holding)
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
/**
|
|
1590
|
+
* 按 CID digest topic 拉取完整文件副本
|
|
1591
|
+
* @param {object} input - 拉取参数
|
|
1592
|
+
* @param {string} [input.link] - most:// 链接
|
|
1593
|
+
* @param {string} [input.cid] - 文件 CID
|
|
1594
|
+
* @param {string} [input.fileName] - 保存文件名
|
|
1595
|
+
* @param {string} [input.taskId] - 下载任务 ID
|
|
1596
|
+
* @param {number} [input.timeout] - 等待 P2P 内容的超时时间
|
|
1597
|
+
*/
|
|
1598
|
+
async pullByCid(input = {}) {
|
|
1599
|
+
this.#ensureInitialized()
|
|
1600
|
+
|
|
1601
|
+
if (input.link) {
|
|
1602
|
+
const parsed = parseMostLink(input.link)
|
|
1603
|
+
if (parsed.error) {
|
|
1604
|
+
throw new ValidationError(parsed.error)
|
|
1605
|
+
}
|
|
1606
|
+
const result = await this.downloadFile(input.link, input.taskId || null, {
|
|
1607
|
+
timeout: input.timeout,
|
|
1608
|
+
ownerAddress: input.ownerAddress,
|
|
1609
|
+
})
|
|
1610
|
+
return {
|
|
1611
|
+
...result,
|
|
1612
|
+
cid: parsed.cid,
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
const cid = input.cid
|
|
1617
|
+
if (!cid) {
|
|
1618
|
+
throw new ValidationError('cid is required')
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
this.#getCidInfo(cid)
|
|
1622
|
+
const fileName = sanitizeFilename(input.fileName || `${cid}.bin`)
|
|
1623
|
+
const link = `most://${cid}?filename=${encodeURIComponent(fileName)}`
|
|
1624
|
+
const result = await this.downloadFile(link, input.taskId || null, {
|
|
1625
|
+
timeout: input.timeout,
|
|
1626
|
+
ownerAddress: input.ownerAddress,
|
|
1627
|
+
})
|
|
1628
|
+
|
|
1629
|
+
return {
|
|
1630
|
+
...result,
|
|
1631
|
+
cid,
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
/**
|
|
1636
|
+
* 按 CID digest 加入文件 topic
|
|
1637
|
+
* @param {string} cid - 文件 CID
|
|
1638
|
+
* @param {object} [options] - Hyperswarm join 选项
|
|
1639
|
+
*/
|
|
1640
|
+
async joinCidTopic(cid, options = {}) {
|
|
1641
|
+
this.#ensureInitialized()
|
|
1642
|
+
return this.#joinCidTopicInternal(cid, options)
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
/**
|
|
1646
|
+
* 用内存复制流连接两个本地引擎,供本地集成测试和诊断使用。
|
|
1647
|
+
*/
|
|
1648
|
+
replicateWith(peerEngine) {
|
|
1649
|
+
this.#ensureInitialized()
|
|
1650
|
+
peerEngine.#ensureInitialized()
|
|
1651
|
+
|
|
1652
|
+
const left = this.#store.replicate(true, { live: true })
|
|
1653
|
+
const right = peerEngine.#store.replicate(false, { live: true })
|
|
1654
|
+
|
|
1655
|
+
left.on('error', () => {})
|
|
1656
|
+
right.on('error', () => {})
|
|
1657
|
+
left.pipe(right).pipe(left)
|
|
1658
|
+
|
|
1659
|
+
return {
|
|
1660
|
+
close: () => {
|
|
1661
|
+
left.destroy()
|
|
1662
|
+
right.destroy()
|
|
1663
|
+
},
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
/**
|
|
1668
|
+
* 读取已发布文件的内容(用于预览)
|
|
1669
|
+
* Hyperdrive 中用 CID 作为 key 存储
|
|
1670
|
+
* @param {string} cid - 文件的 CID
|
|
1671
|
+
* @param {number} [offset=0] - 读取起始位置
|
|
1672
|
+
* @param {number} [limit=10000] - 最大读取字节数
|
|
1673
|
+
*/
|
|
1674
|
+
async readFileContent(
|
|
1675
|
+
cid,
|
|
1676
|
+
offset = 0,
|
|
1677
|
+
limit = DEFAULT_READ_LIMIT,
|
|
1678
|
+
options = {}
|
|
1679
|
+
) {
|
|
1680
|
+
this.#ensureInitialized()
|
|
1681
|
+
const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
|
|
1682
|
+
|
|
1683
|
+
const fileRecord = this.#publishedFiles.find(
|
|
1684
|
+
f =>
|
|
1685
|
+
f.cid === cid &&
|
|
1686
|
+
(options.public || this.#recordMatchesOwner(f, ownerAddress))
|
|
1687
|
+
)
|
|
1688
|
+
if (!fileRecord) {
|
|
1689
|
+
throw new Error('File not found')
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
const drive = await this.#getDriveForFile(fileRecord)
|
|
1693
|
+
|
|
1694
|
+
// Hyperdrive 中 key 为 '/' + cid
|
|
1695
|
+
const driveKey = '/' + cid
|
|
1696
|
+
const entry = await drive.entry(driveKey, {
|
|
1697
|
+
wait: true,
|
|
1698
|
+
timeout: DRIVE_ENTRY_TIMEOUT,
|
|
1699
|
+
})
|
|
1700
|
+
if (!entry || !entry.value) {
|
|
1701
|
+
throw new Error('File content not available')
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
const chunks = []
|
|
1705
|
+
const stream = drive.createReadStream(driveKey, {
|
|
1706
|
+
start: offset,
|
|
1707
|
+
end: offset + limit - 1,
|
|
1708
|
+
})
|
|
1709
|
+
|
|
1710
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
1711
|
+
setTimeout(
|
|
1712
|
+
() => reject(new Error('Stream read timeout')),
|
|
1713
|
+
STREAM_READ_TIMEOUT
|
|
1714
|
+
)
|
|
1715
|
+
})
|
|
1716
|
+
|
|
1717
|
+
const readPromise = (async () => {
|
|
1718
|
+
for await (const chunk of stream) {
|
|
1719
|
+
chunks.push(chunk)
|
|
1720
|
+
}
|
|
1721
|
+
})()
|
|
1722
|
+
|
|
1723
|
+
await Promise.race([readPromise, timeoutPromise])
|
|
1724
|
+
|
|
1725
|
+
const content = Buffer.concat(chunks).toString('utf8')
|
|
1726
|
+
const hasMore =
|
|
1727
|
+
chunks.length > 0 && chunks[chunks.length - 1].length === limit
|
|
1728
|
+
|
|
1729
|
+
return { content, hasMore }
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
/**
|
|
1733
|
+
* 读取已发布文件的原始内容(用于预览/下载)
|
|
1734
|
+
* Hyperdrive 中用 CID 作为 key 存储
|
|
1735
|
+
* @param {string} cid - 文件的 CID
|
|
1736
|
+
* @param {object} [options] - 选项
|
|
1737
|
+
* @param {number} [options.offset=0] - 读取起始位置
|
|
1738
|
+
* @param {number} [options.limit] - 最大读取字节数,不指定则读取到末尾
|
|
1739
|
+
* @param {number} [options.timeout=10000] - 流读取超时(毫秒)
|
|
1740
|
+
* @returns {Promise<{buffer: Buffer, fileName: string, totalSize: number}>}
|
|
1741
|
+
*/
|
|
1742
|
+
async readFileRaw(cid, options = {}) {
|
|
1743
|
+
this.#ensureInitialized()
|
|
1744
|
+
const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
|
|
1745
|
+
|
|
1746
|
+
const fileRecord = this.#publishedFiles.find(
|
|
1747
|
+
f =>
|
|
1748
|
+
f.cid === cid &&
|
|
1749
|
+
(options.public || this.#recordMatchesOwner(f, ownerAddress))
|
|
1750
|
+
)
|
|
1751
|
+
if (!fileRecord) {
|
|
1136
1752
|
throw new Error('File not found')
|
|
1137
1753
|
}
|
|
1138
1754
|
|
|
@@ -1215,8 +1831,9 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1215
1831
|
* @param {string} [type='personal'] - 频道类型
|
|
1216
1832
|
* @returns {Promise<{ name: string, key: string }>}
|
|
1217
1833
|
*/
|
|
1218
|
-
async createChannel(name, type = 'personal') {
|
|
1834
|
+
async createChannel(name, type = 'personal', options = {}) {
|
|
1219
1835
|
this.#ensureInitialized()
|
|
1836
|
+
const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
|
|
1220
1837
|
|
|
1221
1838
|
if (!CHANNEL_NAME_REGEX.test(name)) {
|
|
1222
1839
|
throw new Error('频道名只能包含字母、数字、下划线和连字符')
|
|
@@ -1230,6 +1847,13 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1230
1847
|
|
|
1231
1848
|
const existing = this.#channels.find(c => c.name === name)
|
|
1232
1849
|
if (existing) {
|
|
1850
|
+
if (ownerAddress && !Array.isArray(existing.members)) {
|
|
1851
|
+
existing.members = []
|
|
1852
|
+
}
|
|
1853
|
+
if (ownerAddress && !existing.members.includes(ownerAddress)) {
|
|
1854
|
+
existing.members.push(ownerAddress)
|
|
1855
|
+
this.#saveChannelsMetadata()
|
|
1856
|
+
}
|
|
1233
1857
|
return { name: existing.name, key: existing.coreKey }
|
|
1234
1858
|
}
|
|
1235
1859
|
|
|
@@ -1243,12 +1867,10 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1243
1867
|
server: true,
|
|
1244
1868
|
client: true,
|
|
1245
1869
|
})
|
|
1246
|
-
await appDiscovery.flushed()
|
|
1247
1870
|
const chatDiscovery = this.#chatSwarm.join(chatDiscoveryKey, {
|
|
1248
1871
|
server: true,
|
|
1249
1872
|
client: true,
|
|
1250
1873
|
})
|
|
1251
|
-
await chatDiscovery.flushed()
|
|
1252
1874
|
|
|
1253
1875
|
this.#setupChannelAppendListener(core, name)
|
|
1254
1876
|
|
|
@@ -1258,10 +1880,18 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1258
1880
|
coreKey: b4a.toString(core.key, 'hex'),
|
|
1259
1881
|
createdAt: new Date().toISOString(),
|
|
1260
1882
|
type,
|
|
1883
|
+
ownerAddress,
|
|
1884
|
+
members: ownerAddress ? [ownerAddress] : [],
|
|
1885
|
+
remoteCoreKeys: [],
|
|
1261
1886
|
}
|
|
1262
1887
|
|
|
1263
1888
|
this.#channels.push(channelInfo)
|
|
1264
|
-
|
|
1889
|
+
const coreKeyHex = b4a.toString(core.key, 'hex')
|
|
1890
|
+
if (!this.#channelCores.has(name)) {
|
|
1891
|
+
this.#channelCores.set(name, new Map())
|
|
1892
|
+
}
|
|
1893
|
+
this.#channelCores.get(name).set(coreKeyHex, core)
|
|
1894
|
+
this.#channelLocalCoreKey.set(name, coreKeyHex)
|
|
1265
1895
|
this.#channelPeers.set(name, new Map())
|
|
1266
1896
|
this.#channelDiscoveries.set(name, appDiscovery)
|
|
1267
1897
|
this.#channelChatDiscoveries.set(name, chatDiscovery)
|
|
@@ -1279,11 +1909,29 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1279
1909
|
* @param {string} [coreKey] - 频道的 coreKey(加入他人创建的频道时必填)
|
|
1280
1910
|
* @returns {Promise<{ name: string, key: string }>}
|
|
1281
1911
|
*/
|
|
1282
|
-
async joinChannel(name, coreKey = null) {
|
|
1912
|
+
async joinChannel(name, coreKey = null, options = {}) {
|
|
1283
1913
|
this.#ensureInitialized()
|
|
1914
|
+
const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
|
|
1284
1915
|
|
|
1285
1916
|
const existing = this.#channels.find(c => c.name === name)
|
|
1286
1917
|
if (existing) {
|
|
1918
|
+
if (ownerAddress && !Array.isArray(existing.members)) {
|
|
1919
|
+
existing.members = []
|
|
1920
|
+
}
|
|
1921
|
+
if (ownerAddress && !existing.members.includes(ownerAddress)) {
|
|
1922
|
+
existing.members.push(ownerAddress)
|
|
1923
|
+
this.#saveChannelsMetadata()
|
|
1924
|
+
}
|
|
1925
|
+
if (coreKey && coreKey !== existing.coreKey) {
|
|
1926
|
+
if (!Array.isArray(existing.remoteCoreKeys)) {
|
|
1927
|
+
existing.remoteCoreKeys = []
|
|
1928
|
+
}
|
|
1929
|
+
if (!existing.remoteCoreKeys.includes(coreKey)) {
|
|
1930
|
+
existing.remoteCoreKeys.push(coreKey)
|
|
1931
|
+
this.#saveChannelsMetadata()
|
|
1932
|
+
}
|
|
1933
|
+
await this.#openRemoteChannelCore(name, coreKey)
|
|
1934
|
+
}
|
|
1287
1935
|
return { name: existing.name, key: existing.coreKey }
|
|
1288
1936
|
}
|
|
1289
1937
|
|
|
@@ -1292,11 +1940,13 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1292
1940
|
}
|
|
1293
1941
|
|
|
1294
1942
|
const ns = this.#store.namespace(`channel-${name}`)
|
|
1295
|
-
const
|
|
1296
|
-
|
|
1943
|
+
const remoteCoreKeyHex = b4a.toString(b4a.from(coreKey, 'hex'), 'hex')
|
|
1944
|
+
const localCore = ns.get({
|
|
1945
|
+
name: `messages-${this.getNodeId()}`,
|
|
1297
1946
|
valueEncoding: 'json',
|
|
1298
1947
|
})
|
|
1299
|
-
await
|
|
1948
|
+
await localCore.ready()
|
|
1949
|
+
const localCoreKeyHex = b4a.toString(localCore.key, 'hex')
|
|
1300
1950
|
|
|
1301
1951
|
const discoveryKey = this.#generateChannelDiscoveryKey(name)
|
|
1302
1952
|
const chatDiscoveryKey = this.#generateChannelChatDiscoveryKey(name)
|
|
@@ -1304,34 +1954,43 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1304
1954
|
server: true,
|
|
1305
1955
|
client: true,
|
|
1306
1956
|
})
|
|
1307
|
-
await appDiscovery.flushed()
|
|
1308
1957
|
const chatDiscovery = this.#chatSwarm.join(chatDiscoveryKey, {
|
|
1309
1958
|
server: true,
|
|
1310
1959
|
client: true,
|
|
1311
1960
|
})
|
|
1312
|
-
await chatDiscovery.flushed()
|
|
1313
1961
|
|
|
1314
|
-
this.#setupChannelAppendListener(
|
|
1962
|
+
this.#setupChannelAppendListener(localCore, name)
|
|
1315
1963
|
|
|
1316
1964
|
const channelInfo = {
|
|
1317
1965
|
name,
|
|
1318
1966
|
discoveryKey: b4a.toString(discoveryKey, 'hex'),
|
|
1319
|
-
coreKey,
|
|
1967
|
+
coreKey: localCoreKeyHex,
|
|
1320
1968
|
createdAt: new Date().toISOString(),
|
|
1321
1969
|
type: 'group',
|
|
1970
|
+
ownerAddress,
|
|
1971
|
+
members: ownerAddress ? [ownerAddress] : [],
|
|
1972
|
+
remoteCoreKeys:
|
|
1973
|
+
remoteCoreKeyHex === localCoreKeyHex ? [] : [remoteCoreKeyHex],
|
|
1322
1974
|
}
|
|
1323
1975
|
|
|
1324
1976
|
this.#channels.push(channelInfo)
|
|
1325
|
-
this.#channelCores.
|
|
1977
|
+
if (!this.#channelCores.has(name)) {
|
|
1978
|
+
this.#channelCores.set(name, new Map())
|
|
1979
|
+
}
|
|
1980
|
+
this.#channelCores.get(name).set(localCoreKeyHex, localCore)
|
|
1981
|
+
this.#channelLocalCoreKey.set(name, localCoreKeyHex)
|
|
1326
1982
|
this.#channelPeers.set(name, new Map())
|
|
1327
1983
|
this.#channelDiscoveries.set(name, appDiscovery)
|
|
1328
1984
|
this.#channelChatDiscoveries.set(name, chatDiscovery)
|
|
1329
1985
|
this.#saveChannelsMetadata()
|
|
1986
|
+
if (remoteCoreKeyHex !== localCoreKeyHex) {
|
|
1987
|
+
await this.#openRemoteChannelCore(name, remoteCoreKeyHex)
|
|
1988
|
+
}
|
|
1330
1989
|
|
|
1331
1990
|
console.log(`[MostBox] Joined channel: ${name}`)
|
|
1332
|
-
this.emit('channel:joined', { name, key:
|
|
1991
|
+
this.emit('channel:joined', { name, key: localCoreKeyHex })
|
|
1333
1992
|
|
|
1334
|
-
return { name, key:
|
|
1993
|
+
return { name, key: localCoreKeyHex }
|
|
1335
1994
|
}
|
|
1336
1995
|
|
|
1337
1996
|
/**
|
|
@@ -1339,8 +1998,9 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1339
1998
|
* @param {string} name - 频道名
|
|
1340
1999
|
* @returns {Promise<string[]>} 剩余频道列表
|
|
1341
2000
|
*/
|
|
1342
|
-
async leaveChannel(name) {
|
|
2001
|
+
async leaveChannel(name, options = {}) {
|
|
1343
2002
|
this.#ensureInitialized()
|
|
2003
|
+
const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
|
|
1344
2004
|
|
|
1345
2005
|
const index = this.#channels.findIndex(c => c.name === name)
|
|
1346
2006
|
if (index === -1) {
|
|
@@ -1348,46 +2008,54 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1348
2008
|
}
|
|
1349
2009
|
|
|
1350
2010
|
const channel = this.#channels[index]
|
|
2011
|
+
if (ownerAddress && Array.isArray(channel.members)) {
|
|
2012
|
+
channel.members = channel.members.filter(
|
|
2013
|
+
member => normalizeOwnerAddress(member) !== ownerAddress
|
|
2014
|
+
)
|
|
2015
|
+
if (channel.members.length > 0) {
|
|
2016
|
+
this.#saveChannelsMetadata()
|
|
2017
|
+
return this.listChannels({ ownerAddress })
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
1351
2020
|
|
|
1352
2021
|
const appDiscovery = this.#channelDiscoveries.get(name)
|
|
1353
2022
|
if (appDiscovery && this.#swarm) {
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
} catch (err) {
|
|
2023
|
+
this.#channelDiscoveries.delete(name)
|
|
2024
|
+
this.#swarm.leave(b4a.from(channel.discoveryKey, 'hex')).catch(err => {
|
|
1357
2025
|
console.warn(
|
|
1358
2026
|
`[MostBox] Failed to leave app swarm for ${name}:`,
|
|
1359
2027
|
err.message
|
|
1360
2028
|
)
|
|
1361
|
-
}
|
|
1362
|
-
this.#channelDiscoveries.delete(name)
|
|
2029
|
+
})
|
|
1363
2030
|
}
|
|
1364
2031
|
|
|
1365
2032
|
const chatDiscovery = this.#channelChatDiscoveries.get(name)
|
|
1366
2033
|
if (chatDiscovery && this.#chatSwarm) {
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
} catch (err) {
|
|
2034
|
+
this.#channelChatDiscoveries.delete(name)
|
|
2035
|
+
const chatDiscoveryKey = this.#generateChannelChatDiscoveryKey(name)
|
|
2036
|
+
this.#chatSwarm.leave(chatDiscoveryKey).catch(err => {
|
|
1371
2037
|
console.warn(
|
|
1372
2038
|
`[MostBox] Failed to leave chat swarm for ${name}:`,
|
|
1373
2039
|
err.message
|
|
1374
2040
|
)
|
|
1375
|
-
}
|
|
1376
|
-
this.#channelChatDiscoveries.delete(name)
|
|
2041
|
+
})
|
|
1377
2042
|
}
|
|
1378
2043
|
|
|
1379
|
-
const
|
|
1380
|
-
if (
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
2044
|
+
const coresMap = this.#channelCores.get(name)
|
|
2045
|
+
if (coresMap) {
|
|
2046
|
+
for (const [, core] of coresMap) {
|
|
2047
|
+
try {
|
|
2048
|
+
await core.close()
|
|
2049
|
+
} catch (err) {
|
|
2050
|
+
console.warn(
|
|
2051
|
+
`[MostBox] Failed to close channel core for ${name}:`,
|
|
2052
|
+
err.message
|
|
2053
|
+
)
|
|
2054
|
+
}
|
|
1388
2055
|
}
|
|
1389
2056
|
this.#channelCores.delete(name)
|
|
1390
2057
|
}
|
|
2058
|
+
this.#channelLocalCoreKey.delete(name)
|
|
1391
2059
|
|
|
1392
2060
|
this.#channelPeers.delete(name)
|
|
1393
2061
|
this.#channels.splice(index, 1)
|
|
@@ -1396,23 +2064,61 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1396
2064
|
console.log(`[MostBox] Left channel: ${name}`)
|
|
1397
2065
|
this.emit('channel:left', { name })
|
|
1398
2066
|
|
|
1399
|
-
return this.listChannels()
|
|
2067
|
+
return this.listChannels({ ownerAddress })
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
setChannelRemark(name, remark, options = {}) {
|
|
2071
|
+
this.#ensureInitialized()
|
|
2072
|
+
const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
|
|
2073
|
+
if (!ownerAddress) {
|
|
2074
|
+
throw new Error('需要登录才能设置备注')
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
const channel = this.#channels.find(c => c.name === name)
|
|
2078
|
+
if (!channel) {
|
|
2079
|
+
throw new Error('频道不存在')
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
const trimmed = (remark || '').trim()
|
|
2083
|
+
if (trimmed.length > 50) {
|
|
2084
|
+
throw new Error('备注最多 50 个字符')
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
if (!channel.remarks) {
|
|
2088
|
+
channel.remarks = {}
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2091
|
+
if (trimmed) {
|
|
2092
|
+
channel.remarks[ownerAddress] = trimmed
|
|
2093
|
+
} else {
|
|
2094
|
+
delete channel.remarks[ownerAddress]
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
this.#saveChannelsMetadata()
|
|
2098
|
+
return trimmed
|
|
1400
2099
|
}
|
|
1401
2100
|
|
|
1402
2101
|
/**
|
|
1403
2102
|
* 列出所有频道
|
|
1404
|
-
* @returns {Array<{ name: string, coreKey: string, createdAt: string, type: string, peerCount: number }>}
|
|
2103
|
+
* @returns {Array<{ name: string, coreKey: string, createdAt: string, type: string, peerCount: number, remark: string }>}
|
|
1405
2104
|
*/
|
|
1406
|
-
listChannels() {
|
|
2105
|
+
listChannels(options = {}) {
|
|
1407
2106
|
this.#ensureInitialized()
|
|
2107
|
+
const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
|
|
1408
2108
|
|
|
1409
|
-
return this.#channels
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
2109
|
+
return this.#channels
|
|
2110
|
+
.filter(c => {
|
|
2111
|
+
if (!ownerAddress) return true
|
|
2112
|
+
return Array.isArray(c.members) && c.members.includes(ownerAddress)
|
|
2113
|
+
})
|
|
2114
|
+
.map(c => ({
|
|
2115
|
+
name: c.name,
|
|
2116
|
+
coreKey: c.coreKey,
|
|
2117
|
+
createdAt: c.createdAt,
|
|
2118
|
+
type: c.type,
|
|
2119
|
+
peerCount: (this.#channelPeers.get(c.name) || new Map()).size,
|
|
2120
|
+
remark: ownerAddress && c.remarks ? c.remarks[ownerAddress] || '' : '',
|
|
2121
|
+
}))
|
|
1416
2122
|
}
|
|
1417
2123
|
|
|
1418
2124
|
/**
|
|
@@ -1425,29 +2131,48 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1425
2131
|
*/
|
|
1426
2132
|
async getChannelMessages(name, options = {}) {
|
|
1427
2133
|
this.#ensureInitialized()
|
|
2134
|
+
this.#assertChannelMember(name, options.ownerAddress)
|
|
1428
2135
|
|
|
1429
2136
|
const { limit = CHANNEL_MESSAGE_LIMIT, offset = 0 } = options
|
|
1430
2137
|
|
|
1431
|
-
const
|
|
1432
|
-
if (!
|
|
2138
|
+
const coresMap = this.#channelCores.get(name)
|
|
2139
|
+
if (!coresMap || coresMap.size === 0) {
|
|
1433
2140
|
throw new Error('频道未初始化')
|
|
1434
2141
|
}
|
|
1435
2142
|
|
|
1436
|
-
const
|
|
1437
|
-
const
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
2143
|
+
const allMessages = []
|
|
2144
|
+
for (const [coreKeyHex, core] of coresMap) {
|
|
2145
|
+
for (let i = 0; i < core.length; i++) {
|
|
2146
|
+
try {
|
|
2147
|
+
const entry = await core.get(i)
|
|
2148
|
+
if (entry && entry.type === 'message') {
|
|
2149
|
+
allMessages.push({
|
|
2150
|
+
...entry,
|
|
2151
|
+
_coreKey: coreKeyHex,
|
|
2152
|
+
_index: i,
|
|
2153
|
+
})
|
|
2154
|
+
}
|
|
2155
|
+
} catch {
|
|
2156
|
+
break
|
|
2157
|
+
}
|
|
1447
2158
|
}
|
|
1448
2159
|
}
|
|
1449
2160
|
|
|
1450
|
-
|
|
2161
|
+
const seen = new Set()
|
|
2162
|
+
const unique = allMessages.filter(m => {
|
|
2163
|
+
const key = `${m._coreKey}:${m.author}:${m.timestamp}:${m.content}`
|
|
2164
|
+
if (seen.has(key)) return false
|
|
2165
|
+
seen.add(key)
|
|
2166
|
+
return true
|
|
2167
|
+
})
|
|
2168
|
+
|
|
2169
|
+
unique.sort((a, b) => a.timestamp - b.timestamp)
|
|
2170
|
+
|
|
2171
|
+
const total = unique.length
|
|
2172
|
+
const start = Math.max(0, total - offset - limit)
|
|
2173
|
+
const end = total - offset
|
|
2174
|
+
|
|
2175
|
+
return unique.slice(start, end).map(({ _coreKey, _index, ...msg }) => msg)
|
|
1451
2176
|
}
|
|
1452
2177
|
|
|
1453
2178
|
/**
|
|
@@ -1456,14 +2181,18 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1456
2181
|
* @param {string} content - 消息内容
|
|
1457
2182
|
* @param {string} author - 作者 address
|
|
1458
2183
|
* @param {string} authorName - 作者显示名
|
|
2184
|
+
* @param {object} [options.attachment] - 附件元数据
|
|
1459
2185
|
* @returns {Promise<object>}
|
|
1460
2186
|
*/
|
|
1461
|
-
async sendMessage(name, content, author, authorName) {
|
|
2187
|
+
async sendMessage(name, content, author, authorName, options = {}) {
|
|
1462
2188
|
this.#ensureInitialized()
|
|
2189
|
+
this.#assertChannelMember(name, options.ownerAddress)
|
|
1463
2190
|
|
|
1464
|
-
const
|
|
2191
|
+
const localKeyHex = this.#channelLocalCoreKey.get(name)
|
|
2192
|
+
const coresMap = this.#channelCores.get(name)
|
|
2193
|
+
const core = localKeyHex && coresMap ? coresMap.get(localKeyHex) : null
|
|
1465
2194
|
if (!core) {
|
|
1466
|
-
throw new Error('
|
|
2195
|
+
throw new Error('频道未初始化或无可写 core')
|
|
1467
2196
|
}
|
|
1468
2197
|
|
|
1469
2198
|
if (!content || !content.trim()) {
|
|
@@ -1474,6 +2203,10 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1474
2203
|
if (trimmed.length > MAX_MESSAGE_LENGTH) {
|
|
1475
2204
|
throw new Error(`消息内容不能超过 ${MAX_MESSAGE_LENGTH} 字符`)
|
|
1476
2205
|
}
|
|
2206
|
+
const attachment = normalizeChannelAttachment(options.attachment)
|
|
2207
|
+
if (attachment && trimmed !== attachment.link) {
|
|
2208
|
+
throw new ValidationError('attachment content must match link')
|
|
2209
|
+
}
|
|
1477
2210
|
|
|
1478
2211
|
const message = {
|
|
1479
2212
|
type: 'message',
|
|
@@ -1482,11 +2215,12 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1482
2215
|
content: trimmed,
|
|
1483
2216
|
timestamp: Date.now(),
|
|
1484
2217
|
}
|
|
2218
|
+
if (attachment) {
|
|
2219
|
+
message.attachment = attachment
|
|
2220
|
+
}
|
|
1485
2221
|
|
|
1486
2222
|
await core.append(message)
|
|
1487
2223
|
|
|
1488
|
-
this.emit('channel:message', { channel: name, message })
|
|
1489
|
-
|
|
1490
2224
|
return message
|
|
1491
2225
|
}
|
|
1492
2226
|
|
|
@@ -1495,8 +2229,9 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1495
2229
|
* @param {string} name - 频道名
|
|
1496
2230
|
* @returns {Array<{ peerId: string, authorName: string, lastSeen: number }>}
|
|
1497
2231
|
*/
|
|
1498
|
-
getChannelPeers(name) {
|
|
2232
|
+
getChannelPeers(name, options = {}) {
|
|
1499
2233
|
this.#ensureInitialized()
|
|
2234
|
+
this.#assertChannelMember(name, options.ownerAddress)
|
|
1500
2235
|
|
|
1501
2236
|
const peers = this.#channelPeers.get(name)
|
|
1502
2237
|
if (!peers) {
|
|
@@ -1553,6 +2288,429 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1553
2288
|
}
|
|
1554
2289
|
}
|
|
1555
2290
|
|
|
2291
|
+
#assertChannelMember(name, ownerAddress) {
|
|
2292
|
+
const normalizedOwner = normalizeOwnerAddress(ownerAddress)
|
|
2293
|
+
if (!normalizedOwner) return
|
|
2294
|
+
|
|
2295
|
+
const channel = this.#channels.find(c => c.name === name)
|
|
2296
|
+
if (!channel) {
|
|
2297
|
+
throw new Error('频道不存在')
|
|
2298
|
+
}
|
|
2299
|
+
if (
|
|
2300
|
+
!Array.isArray(channel.members) ||
|
|
2301
|
+
!channel.members.includes(normalizedOwner)
|
|
2302
|
+
) {
|
|
2303
|
+
throw new PermissionError('未加入该频道')
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
|
|
2307
|
+
#getCidInfo(cid) {
|
|
2308
|
+
return getCidInfo(cid)
|
|
2309
|
+
}
|
|
2310
|
+
|
|
2311
|
+
#setSeedState(cid, patch = {}) {
|
|
2312
|
+
const previous = this.#seedStates.get(cid) || {}
|
|
2313
|
+
const next = {
|
|
2314
|
+
...previous,
|
|
2315
|
+
cid,
|
|
2316
|
+
...patch,
|
|
2317
|
+
updatedAt: new Date().toISOString(),
|
|
2318
|
+
}
|
|
2319
|
+
this.#seedStates.set(cid, next)
|
|
2320
|
+
this.emit('seed:state', next)
|
|
2321
|
+
return next
|
|
2322
|
+
}
|
|
2323
|
+
|
|
2324
|
+
#clearSeedState(cid) {
|
|
2325
|
+
if (this.#seedStates.delete(cid)) {
|
|
2326
|
+
this.emit('seed:state:removed', { cid })
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
|
|
2330
|
+
#getFileRuntimeStats(cid) {
|
|
2331
|
+
const state = this.#fileMonitors.get(cid)
|
|
2332
|
+
if (!state) {
|
|
2333
|
+
return {
|
|
2334
|
+
peerCount: 0,
|
|
2335
|
+
lastServedAt: null,
|
|
2336
|
+
totalServedBytes: 0,
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
|
|
2340
|
+
return {
|
|
2341
|
+
peerCount: state.peerCount || 0,
|
|
2342
|
+
lastServedAt: state.lastServedAt || null,
|
|
2343
|
+
totalServedBytes: state.totalServedBytes || 0,
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
|
|
2347
|
+
async #ensureFileMonitor(cid, drive = null) {
|
|
2348
|
+
const existing = this.#fileMonitors.get(cid)
|
|
2349
|
+
if (existing) return existing
|
|
2350
|
+
|
|
2351
|
+
const { driveName } = this.#getCidInfo(cid)
|
|
2352
|
+
const monitoredDrive = drive || (await this.#getOrCreateDrive(driveName))
|
|
2353
|
+
const monitor = monitoredDrive.monitor('/' + cid)
|
|
2354
|
+
const state = {
|
|
2355
|
+
cid,
|
|
2356
|
+
monitor,
|
|
2357
|
+
peerCount: 0,
|
|
2358
|
+
lastServedAt: null,
|
|
2359
|
+
totalServedBytes: 0,
|
|
2360
|
+
uploadBytes: 0,
|
|
2361
|
+
uploadBlocks: 0,
|
|
2362
|
+
lastMetricsEmittedAt: 0,
|
|
2363
|
+
cleanup: null,
|
|
2364
|
+
}
|
|
2365
|
+
this.#fileMonitors.set(cid, state)
|
|
2366
|
+
|
|
2367
|
+
const emitMetrics = (force = false) => {
|
|
2368
|
+
const now = Date.now()
|
|
2369
|
+
if (!force && now - state.lastMetricsEmittedAt < 1000) return
|
|
2370
|
+
state.lastMetricsEmittedAt = now
|
|
2371
|
+
this.emit('seed:metrics', {
|
|
2372
|
+
cid,
|
|
2373
|
+
...this.#getFileRuntimeStats(cid),
|
|
2374
|
+
})
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2377
|
+
const updatePeerCount = () => {
|
|
2378
|
+
const nextPeerCount = Number(monitor.peers) || 0
|
|
2379
|
+
if (nextPeerCount !== state.peerCount) {
|
|
2380
|
+
state.peerCount = nextPeerCount
|
|
2381
|
+
emitMetrics(true)
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
2384
|
+
|
|
2385
|
+
const updateTransferStats = () => {
|
|
2386
|
+
updatePeerCount()
|
|
2387
|
+
const uploadStats = monitor.uploadStats || {}
|
|
2388
|
+
const uploadBytes = Number(uploadStats.monitoringBytes) || 0
|
|
2389
|
+
const uploadBlocks = Number(uploadStats.blocks) || 0
|
|
2390
|
+
const servedMore =
|
|
2391
|
+
uploadBytes > state.uploadBytes || uploadBlocks > state.uploadBlocks
|
|
2392
|
+
|
|
2393
|
+
if (servedMore) {
|
|
2394
|
+
state.lastServedAt = new Date().toISOString()
|
|
2395
|
+
state.totalServedBytes = uploadBytes
|
|
2396
|
+
}
|
|
2397
|
+
|
|
2398
|
+
state.uploadBytes = uploadBytes
|
|
2399
|
+
state.uploadBlocks = uploadBlocks
|
|
2400
|
+
if (servedMore) emitMetrics()
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2403
|
+
monitor.on('update', updateTransferStats)
|
|
2404
|
+
try {
|
|
2405
|
+
await monitor.ready()
|
|
2406
|
+
const blobs = monitor.blobs
|
|
2407
|
+
const onPeerUpdate = () => {
|
|
2408
|
+
updatePeerCount()
|
|
2409
|
+
}
|
|
2410
|
+
blobs?.core?.on('peer-add', onPeerUpdate)
|
|
2411
|
+
blobs?.core?.on('peer-remove', onPeerUpdate)
|
|
2412
|
+
state.cleanup = () => {
|
|
2413
|
+
blobs?.core?.off('peer-add', onPeerUpdate)
|
|
2414
|
+
blobs?.core?.off('peer-remove', onPeerUpdate)
|
|
2415
|
+
}
|
|
2416
|
+
updateTransferStats()
|
|
2417
|
+
} catch (err) {
|
|
2418
|
+
this.#fileMonitors.delete(cid)
|
|
2419
|
+
monitor.off('update', updateTransferStats)
|
|
2420
|
+
await monitor.close().catch(() => {})
|
|
2421
|
+
throw err
|
|
2422
|
+
}
|
|
2423
|
+
|
|
2424
|
+
return state
|
|
2425
|
+
}
|
|
2426
|
+
|
|
2427
|
+
async #closeFileMonitor(state) {
|
|
2428
|
+
if (!state) return
|
|
2429
|
+
try {
|
|
2430
|
+
state.cleanup?.()
|
|
2431
|
+
await state.monitor.close()
|
|
2432
|
+
} catch {}
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2435
|
+
#resumeHoldingsInBackground() {
|
|
2436
|
+
if (this.#holdingResumeTask || this.#holdings.length === 0) {
|
|
2437
|
+
return
|
|
2438
|
+
}
|
|
2439
|
+
|
|
2440
|
+
const holdings = [...this.#holdings]
|
|
2441
|
+
this.#holdingResumeTask = (async () => {
|
|
2442
|
+
for (
|
|
2443
|
+
let index = 0;
|
|
2444
|
+
index < holdings.length && this.#initialized;
|
|
2445
|
+
index += HOLDING_REJOIN_BATCH_SIZE
|
|
2446
|
+
) {
|
|
2447
|
+
const batch = holdings.slice(index, index + HOLDING_REJOIN_BATCH_SIZE)
|
|
2448
|
+
await Promise.allSettled(
|
|
2449
|
+
batch.map(async holding => {
|
|
2450
|
+
if (!this.#holdings.some(current => current.cid === holding.cid)) {
|
|
2451
|
+
return
|
|
2452
|
+
}
|
|
2453
|
+
await this.#joinCidTopicInternal(holding.cid, {
|
|
2454
|
+
server: true,
|
|
2455
|
+
client: false,
|
|
2456
|
+
})
|
|
2457
|
+
console.log(`[MostBox] Rejoined CID topic: ${holding.cid}`)
|
|
2458
|
+
})
|
|
2459
|
+
)
|
|
2460
|
+
|
|
2461
|
+
if (
|
|
2462
|
+
index + HOLDING_REJOIN_BATCH_SIZE < holdings.length &&
|
|
2463
|
+
this.#initialized
|
|
2464
|
+
) {
|
|
2465
|
+
await sleep(HOLDING_REJOIN_BATCH_DELAY)
|
|
2466
|
+
}
|
|
2467
|
+
}
|
|
2468
|
+
})()
|
|
2469
|
+
.catch(err => {
|
|
2470
|
+
console.warn('[MostBox] Failed to resume holdings:', err.message)
|
|
2471
|
+
})
|
|
2472
|
+
.finally(() => {
|
|
2473
|
+
this.#holdingResumeTask = null
|
|
2474
|
+
})
|
|
2475
|
+
}
|
|
2476
|
+
|
|
2477
|
+
#normalizeHolding(record = {}) {
|
|
2478
|
+
const cid = record.cid
|
|
2479
|
+
if (!cid) {
|
|
2480
|
+
throw new ValidationError('cid is required')
|
|
2481
|
+
}
|
|
2482
|
+
|
|
2483
|
+
const { topicHex, driveName } = this.#getCidInfo(cid)
|
|
2484
|
+
if (record.topic && record.topic !== topicHex) {
|
|
2485
|
+
throw new ValidationError('topic must match CID digest')
|
|
2486
|
+
}
|
|
2487
|
+
|
|
2488
|
+
const size = Number(record.size)
|
|
2489
|
+
if (!Number.isFinite(size) || size < 0) {
|
|
2490
|
+
throw new ValidationError('size must be a non-negative number')
|
|
2491
|
+
}
|
|
2492
|
+
|
|
2493
|
+
return {
|
|
2494
|
+
cid,
|
|
2495
|
+
fileName: record.fileName || cid,
|
|
2496
|
+
size,
|
|
2497
|
+
localPath: record.localPath || null,
|
|
2498
|
+
topic: topicHex,
|
|
2499
|
+
driveName: record.driveName || driveName,
|
|
2500
|
+
source: record.source || 'manual',
|
|
2501
|
+
}
|
|
2502
|
+
}
|
|
2503
|
+
|
|
2504
|
+
#upsertHolding(record) {
|
|
2505
|
+
const holding = this.#normalizeHolding(record)
|
|
2506
|
+
const now = new Date().toISOString()
|
|
2507
|
+
const index = this.#holdings.findIndex(f => f.cid === holding.cid)
|
|
2508
|
+
const next =
|
|
2509
|
+
index === -1
|
|
2510
|
+
? { ...holding, createdAt: now, updatedAt: now }
|
|
2511
|
+
: { ...this.#holdings[index], ...holding, updatedAt: now }
|
|
2512
|
+
|
|
2513
|
+
if (index === -1) {
|
|
2514
|
+
this.#holdings.push(next)
|
|
2515
|
+
} else {
|
|
2516
|
+
this.#holdings[index] = next
|
|
2517
|
+
}
|
|
2518
|
+
|
|
2519
|
+
this.#saveHoldingsMetadata()
|
|
2520
|
+
this.emit('holding:updated', next)
|
|
2521
|
+
this.#ensureFileMonitor(next.cid).catch(err => {
|
|
2522
|
+
this.#setSeedState(next.cid, {
|
|
2523
|
+
status: 'error',
|
|
2524
|
+
error: err.message,
|
|
2525
|
+
})
|
|
2526
|
+
})
|
|
2527
|
+
const seedState = this.#seedStates.get(next.cid)
|
|
2528
|
+
return {
|
|
2529
|
+
...next,
|
|
2530
|
+
joined: this.#fileDiscoveries.has(next.cid),
|
|
2531
|
+
seedStatus:
|
|
2532
|
+
seedState?.status ||
|
|
2533
|
+
(this.#fileDiscoveries.has(next.cid) ? 'active' : 'queued'),
|
|
2534
|
+
seedError: seedState?.error,
|
|
2535
|
+
seedStatusUpdatedAt: seedState?.updatedAt,
|
|
2536
|
+
...this.#getFileRuntimeStats(next.cid),
|
|
2537
|
+
}
|
|
2538
|
+
}
|
|
2539
|
+
|
|
2540
|
+
#removeHolding(cid) {
|
|
2541
|
+
const before = this.#holdings.length
|
|
2542
|
+
this.#holdings = this.#holdings.filter(holding => holding.cid !== cid)
|
|
2543
|
+
if (this.#holdings.length !== before) {
|
|
2544
|
+
this.#saveHoldingsMetadata()
|
|
2545
|
+
this.emit('holding:removed', { cid })
|
|
2546
|
+
}
|
|
2547
|
+
this.#closeFileMonitor(this.#fileMonitors.get(cid))
|
|
2548
|
+
this.#fileMonitors.delete(cid)
|
|
2549
|
+
this.#clearSeedState(cid)
|
|
2550
|
+
}
|
|
2551
|
+
|
|
2552
|
+
async #joinCidTopicInternal(cid, options = {}) {
|
|
2553
|
+
const { topic, topicHex, driveName } = this.#getCidInfo(cid)
|
|
2554
|
+
const requestedServer = options.server !== false
|
|
2555
|
+
const requestedClient = options.client === true
|
|
2556
|
+
this.#setSeedState(cid, {
|
|
2557
|
+
status: 'joining',
|
|
2558
|
+
topic: topicHex,
|
|
2559
|
+
driveName,
|
|
2560
|
+
error: undefined,
|
|
2561
|
+
})
|
|
2562
|
+
|
|
2563
|
+
try {
|
|
2564
|
+
const drive = await this.#getOrCreateDrive(driveName)
|
|
2565
|
+
|
|
2566
|
+
const existing = this.#fileDiscoveries.get(cid)
|
|
2567
|
+
if (existing) {
|
|
2568
|
+
const nextServer = existing.server || requestedServer
|
|
2569
|
+
const nextClient = existing.client || requestedClient
|
|
2570
|
+
const needsRoleUpgrade =
|
|
2571
|
+
nextServer !== existing.server || nextClient !== existing.client
|
|
2572
|
+
|
|
2573
|
+
if (!needsRoleUpgrade) {
|
|
2574
|
+
if (this.#holdings.some(holding => holding.cid === cid)) {
|
|
2575
|
+
this.#ensureFileMonitor(cid, drive).catch(err => {
|
|
2576
|
+
this.#setSeedState(cid, {
|
|
2577
|
+
status: 'error',
|
|
2578
|
+
error: err.message,
|
|
2579
|
+
})
|
|
2580
|
+
})
|
|
2581
|
+
}
|
|
2582
|
+
this.#setSeedState(cid, {
|
|
2583
|
+
status: 'active',
|
|
2584
|
+
topic: topicHex,
|
|
2585
|
+
driveName,
|
|
2586
|
+
error: undefined,
|
|
2587
|
+
})
|
|
2588
|
+
return {
|
|
2589
|
+
cid,
|
|
2590
|
+
topic: topicHex,
|
|
2591
|
+
driveName,
|
|
2592
|
+
joined: true,
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2595
|
+
|
|
2596
|
+
await this.#swarm.leave(topic).catch(err => {
|
|
2597
|
+
console.warn(
|
|
2598
|
+
`[MostBox] Failed to upgrade CID topic role for ${cid}:`,
|
|
2599
|
+
err.message
|
|
2600
|
+
)
|
|
2601
|
+
})
|
|
2602
|
+
this.#fileDiscoveries.delete(cid)
|
|
2603
|
+
}
|
|
2604
|
+
|
|
2605
|
+
const server = existing?.server || requestedServer
|
|
2606
|
+
const client = existing?.client || requestedClient
|
|
2607
|
+
const discovery = this.#swarm.join(topic, {
|
|
2608
|
+
server,
|
|
2609
|
+
client,
|
|
2610
|
+
})
|
|
2611
|
+
|
|
2612
|
+
this.#fileDiscoveries.set(cid, {
|
|
2613
|
+
discovery,
|
|
2614
|
+
topic: topicHex,
|
|
2615
|
+
driveName,
|
|
2616
|
+
server,
|
|
2617
|
+
client,
|
|
2618
|
+
})
|
|
2619
|
+
this.#setSeedState(cid, {
|
|
2620
|
+
status: 'active',
|
|
2621
|
+
topic: topicHex,
|
|
2622
|
+
driveName,
|
|
2623
|
+
error: undefined,
|
|
2624
|
+
})
|
|
2625
|
+
if (this.#holdings.some(holding => holding.cid === cid)) {
|
|
2626
|
+
this.#ensureFileMonitor(cid, drive).catch(err => {
|
|
2627
|
+
this.#setSeedState(cid, {
|
|
2628
|
+
status: 'error',
|
|
2629
|
+
error: err.message,
|
|
2630
|
+
})
|
|
2631
|
+
})
|
|
2632
|
+
}
|
|
2633
|
+
this.emit('file:topic:joined', { cid, topic: topicHex, driveName })
|
|
2634
|
+
|
|
2635
|
+
return {
|
|
2636
|
+
cid,
|
|
2637
|
+
topic: topicHex,
|
|
2638
|
+
driveName,
|
|
2639
|
+
joined: true,
|
|
2640
|
+
}
|
|
2641
|
+
} catch (err) {
|
|
2642
|
+
this.#setSeedState(cid, {
|
|
2643
|
+
status: 'error',
|
|
2644
|
+
topic: topicHex,
|
|
2645
|
+
driveName,
|
|
2646
|
+
error: err.message,
|
|
2647
|
+
})
|
|
2648
|
+
throw err
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
2651
|
+
|
|
2652
|
+
async #leaveCidTopic(cid) {
|
|
2653
|
+
const existing = this.#fileDiscoveries.get(cid)
|
|
2654
|
+
if (!existing || !this.#swarm) {
|
|
2655
|
+
this.#setSeedState(cid, { status: 'paused' })
|
|
2656
|
+
return
|
|
2657
|
+
}
|
|
2658
|
+
|
|
2659
|
+
this.#fileDiscoveries.delete(cid)
|
|
2660
|
+
this.#swarm.leave(b4a.from(existing.topic, 'hex')).catch(err => {
|
|
2661
|
+
console.warn(`[MostBox] Failed to leave CID topic ${cid}:`, err.message)
|
|
2662
|
+
})
|
|
2663
|
+
this.#setSeedState(cid, {
|
|
2664
|
+
status: 'paused',
|
|
2665
|
+
topic: existing.topic,
|
|
2666
|
+
driveName: existing.driveName,
|
|
2667
|
+
})
|
|
2668
|
+
}
|
|
2669
|
+
|
|
2670
|
+
async #closeDriveForSeed(driveName) {
|
|
2671
|
+
const drive = this.#drives.get(driveName)
|
|
2672
|
+
if (!drive) {
|
|
2673
|
+
return null
|
|
2674
|
+
}
|
|
2675
|
+
|
|
2676
|
+
await drive.close()
|
|
2677
|
+
this.#drives.delete(driveName)
|
|
2678
|
+
return drive
|
|
2679
|
+
}
|
|
2680
|
+
|
|
2681
|
+
#recordMatchesOwner(record, ownerAddress) {
|
|
2682
|
+
const normalizedOwner = normalizeOwnerAddress(ownerAddress)
|
|
2683
|
+
if (!normalizedOwner) return !record.ownerAddress
|
|
2684
|
+
return normalizeOwnerAddress(record.ownerAddress) === normalizedOwner
|
|
2685
|
+
}
|
|
2686
|
+
|
|
2687
|
+
#hasPublishedReference(cid) {
|
|
2688
|
+
return this.#publishedFiles.some(file => file.cid === cid)
|
|
2689
|
+
}
|
|
2690
|
+
|
|
2691
|
+
#hasAnyUserReference(cid) {
|
|
2692
|
+
return (
|
|
2693
|
+
this.#publishedFiles.some(file => file.cid === cid) ||
|
|
2694
|
+
this.#trashFiles.some(file => file.cid === cid)
|
|
2695
|
+
)
|
|
2696
|
+
}
|
|
2697
|
+
|
|
2698
|
+
#getUsedBytes() {
|
|
2699
|
+
return this.#holdings.reduce((sum, h) => sum + (h.size || 0), 0)
|
|
2700
|
+
}
|
|
2701
|
+
|
|
2702
|
+
#checkCapacity(additionalBytes) {
|
|
2703
|
+
const used = this.#getUsedBytes()
|
|
2704
|
+
const capacity = this.#options.capacityBytes
|
|
2705
|
+
if (used + additionalBytes > capacity) {
|
|
2706
|
+
const usedGB = (used / (1024 * 1024 * 1024)).toFixed(2)
|
|
2707
|
+
const capacityGB = (capacity / (1024 * 1024 * 1024)).toFixed(2)
|
|
2708
|
+
throw new StorageCapacityError(
|
|
2709
|
+
`Storage capacity exceeded: used ${usedGB} GB, capacity ${capacityGB} GB`
|
|
2710
|
+
)
|
|
2711
|
+
}
|
|
2712
|
+
}
|
|
2713
|
+
|
|
1556
2714
|
async #getOrCreateDrive(name, _options = { server: true, client: false }) {
|
|
1557
2715
|
if (this.#drives.has(name)) return this.#drives.get(name)
|
|
1558
2716
|
if (this.#drivePromises.has(name)) return this.#drivePromises.get(name)
|
|
@@ -1575,11 +2733,6 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1575
2733
|
}
|
|
1576
2734
|
|
|
1577
2735
|
async #syncDrive(drive, timeout = DRIVE_SYNC_TIMEOUT) {
|
|
1578
|
-
const done = drive.findingPeers()
|
|
1579
|
-
this.#swarm
|
|
1580
|
-
.join(drive.discoveryKey, { server: true, client: true })
|
|
1581
|
-
.flushed()
|
|
1582
|
-
.then(done, done)
|
|
1583
2736
|
try {
|
|
1584
2737
|
const updated = await Promise.race([
|
|
1585
2738
|
drive.update(),
|
|
@@ -1597,6 +2750,10 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1597
2750
|
return path.join(this.#options.dataPath, 'published-files.json')
|
|
1598
2751
|
}
|
|
1599
2752
|
|
|
2753
|
+
#getHoldingsMetadataPath() {
|
|
2754
|
+
return path.join(this.#options.dataPath, 'node-holdings.json')
|
|
2755
|
+
}
|
|
2756
|
+
|
|
1600
2757
|
#getTrashMetadataPath() {
|
|
1601
2758
|
return path.join(this.#options.dataPath, 'trash-files.json')
|
|
1602
2759
|
}
|
|
@@ -1636,6 +2793,32 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1636
2793
|
}
|
|
1637
2794
|
}
|
|
1638
2795
|
|
|
2796
|
+
#loadHoldingsMetadata() {
|
|
2797
|
+
try {
|
|
2798
|
+
const metadataPath = this.#getHoldingsMetadataPath()
|
|
2799
|
+
if (fs.existsSync(metadataPath)) {
|
|
2800
|
+
const data = fs.readFileSync(metadataPath, 'utf-8')
|
|
2801
|
+
const parsed = JSON.parse(data)
|
|
2802
|
+
return parsed.map(record => this.#normalizeHolding(record))
|
|
2803
|
+
}
|
|
2804
|
+
} catch (err) {
|
|
2805
|
+
console.warn(
|
|
2806
|
+
'Failed to load node holdings metadata, using empty list:',
|
|
2807
|
+
err.message
|
|
2808
|
+
)
|
|
2809
|
+
}
|
|
2810
|
+
return []
|
|
2811
|
+
}
|
|
2812
|
+
|
|
2813
|
+
#saveHoldingsMetadata() {
|
|
2814
|
+
try {
|
|
2815
|
+
const metadataPath = this.#getHoldingsMetadataPath()
|
|
2816
|
+
this.#atomicWrite(metadataPath, JSON.stringify(this.#holdings, null, 2))
|
|
2817
|
+
} catch (err) {
|
|
2818
|
+
console.error('Failed to save node holdings metadata:', err.message)
|
|
2819
|
+
}
|
|
2820
|
+
}
|
|
2821
|
+
|
|
1639
2822
|
#loadTrashMetadata() {
|
|
1640
2823
|
try {
|
|
1641
2824
|
const metadataPath = this.#getTrashMetadataPath()
|
|
@@ -1736,15 +2919,57 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1736
2919
|
})
|
|
1737
2920
|
}
|
|
1738
2921
|
|
|
2922
|
+
async #openRemoteChannelCore(channelName, coreKeyHex) {
|
|
2923
|
+
const coresMap = this.#channelCores.get(channelName)
|
|
2924
|
+
if (!coresMap) return
|
|
2925
|
+
if (coresMap.has(coreKeyHex)) return
|
|
2926
|
+
|
|
2927
|
+
try {
|
|
2928
|
+
const ns = this.#store.namespace(`channel-${channelName}`)
|
|
2929
|
+
const core = ns.get({
|
|
2930
|
+
key: b4a.from(coreKeyHex, 'hex'),
|
|
2931
|
+
valueEncoding: 'json',
|
|
2932
|
+
})
|
|
2933
|
+
await core.ready()
|
|
2934
|
+
const normalizedCoreKey = b4a.toString(core.key, 'hex')
|
|
2935
|
+
coresMap.set(normalizedCoreKey, core)
|
|
2936
|
+
this.#setupChannelAppendListener(core, channelName)
|
|
2937
|
+
const channel = this.#channels.find(c => c.name === channelName)
|
|
2938
|
+
if (channel && normalizedCoreKey !== channel.coreKey) {
|
|
2939
|
+
if (!Array.isArray(channel.remoteCoreKeys)) {
|
|
2940
|
+
channel.remoteCoreKeys = []
|
|
2941
|
+
}
|
|
2942
|
+
if (!channel.remoteCoreKeys.includes(normalizedCoreKey)) {
|
|
2943
|
+
channel.remoteCoreKeys.push(normalizedCoreKey)
|
|
2944
|
+
this.#saveChannelsMetadata()
|
|
2945
|
+
}
|
|
2946
|
+
}
|
|
2947
|
+
console.log(
|
|
2948
|
+
`[MostBox] Opened remote channel core ${normalizedCoreKey.slice(0, 8)}... for ${channelName}`
|
|
2949
|
+
)
|
|
2950
|
+
} catch (err) {
|
|
2951
|
+
console.warn(
|
|
2952
|
+
`[MostBox] Failed to open remote channel core for ${channelName}:`,
|
|
2953
|
+
err.message
|
|
2954
|
+
)
|
|
2955
|
+
}
|
|
2956
|
+
}
|
|
2957
|
+
|
|
1739
2958
|
async #handleChannelConnection(conn) {
|
|
1740
2959
|
const stream = conn
|
|
1741
2960
|
let connectedPeerId = null
|
|
1742
2961
|
|
|
2962
|
+
const coreKeys = {}
|
|
2963
|
+
for (const [name, localKeyHex] of this.#channelLocalCoreKey) {
|
|
2964
|
+
coreKeys[name] = localKeyHex
|
|
2965
|
+
}
|
|
2966
|
+
|
|
1743
2967
|
const helloMessage = JSON.stringify({
|
|
1744
2968
|
type: 'channel-hello',
|
|
1745
2969
|
peerId: this.getNodeId(),
|
|
1746
2970
|
authorName: this.getNodeId().slice(0, 4),
|
|
1747
2971
|
channels: this.#channels.map(c => c.name),
|
|
2972
|
+
coreKeys,
|
|
1748
2973
|
})
|
|
1749
2974
|
|
|
1750
2975
|
try {
|
|
@@ -1769,6 +2994,17 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1769
2994
|
})
|
|
1770
2995
|
}
|
|
1771
2996
|
}
|
|
2997
|
+
|
|
2998
|
+
if (msg.coreKeys && typeof msg.coreKeys === 'object') {
|
|
2999
|
+
for (const [channelName, coreKeyHex] of Object.entries(
|
|
3000
|
+
msg.coreKeys
|
|
3001
|
+
)) {
|
|
3002
|
+
if (this.#channelCores.has(channelName) && coreKeyHex) {
|
|
3003
|
+
await this.#openRemoteChannelCore(channelName, coreKeyHex)
|
|
3004
|
+
}
|
|
3005
|
+
}
|
|
3006
|
+
}
|
|
3007
|
+
|
|
1772
3008
|
this.emit('channel:peer:online', {
|
|
1773
3009
|
peerId: msg.peerId,
|
|
1774
3010
|
authorName: msg.authorName,
|
|
@@ -1796,14 +3032,21 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1796
3032
|
}
|
|
1797
3033
|
|
|
1798
3034
|
/**
|
|
1799
|
-
*
|
|
3035
|
+
* 等待指定 Hyperdrive key 从对等节点或本地可用。
|
|
1800
3036
|
* @param {Hyperdrive} drive - 要检查的驱动器
|
|
3037
|
+
* @param {string} key - 期望的 Hyperdrive key,固定为 /<cid>
|
|
1801
3038
|
* @param {number} timeout - 最大等待时间(毫秒)
|
|
1802
3039
|
* @param {string} [taskId] - 用于取消的任务 ID
|
|
1803
3040
|
* @param {object} [taskState] - 任务状态对象
|
|
1804
|
-
* @returns {Promise<
|
|
3041
|
+
* @returns {Promise<object|null>} - Hyperdrive entry
|
|
1805
3042
|
*/
|
|
1806
|
-
async #
|
|
3043
|
+
async #waitForDriveEntry(
|
|
3044
|
+
drive,
|
|
3045
|
+
key,
|
|
3046
|
+
timeout,
|
|
3047
|
+
taskId = null,
|
|
3048
|
+
taskState = null
|
|
3049
|
+
) {
|
|
1807
3050
|
const startTime = Date.now()
|
|
1808
3051
|
let pollInterval = DOWNLOAD_POLL_INTERVAL_MIN
|
|
1809
3052
|
let lastPeerCount = 0
|
|
@@ -1811,15 +3054,12 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1811
3054
|
let bootstrapNodesChecked = false
|
|
1812
3055
|
let lastUpdateTime = 0
|
|
1813
3056
|
|
|
1814
|
-
const localEntries = []
|
|
1815
3057
|
try {
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
this.emit('download:status', { taskId, status: 'syncing' })
|
|
1822
|
-
return localEntries
|
|
3058
|
+
const localEntry = await drive.entry(key)
|
|
3059
|
+
if (localEntry) {
|
|
3060
|
+
console.log(`[MostBox] Found expected entry ${key} locally`)
|
|
3061
|
+
if (taskId) this.emit('download:status', { taskId, status: 'syncing' })
|
|
3062
|
+
return localEntry
|
|
1823
3063
|
}
|
|
1824
3064
|
} catch {}
|
|
1825
3065
|
|
|
@@ -1853,32 +3093,32 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1853
3093
|
|
|
1854
3094
|
await tryUpdateDrive()
|
|
1855
3095
|
|
|
1856
|
-
const entries = []
|
|
1857
3096
|
try {
|
|
1858
|
-
|
|
1859
|
-
|
|
3097
|
+
const entry = await drive.entry(key)
|
|
3098
|
+
if (entry) {
|
|
3099
|
+
console.log(`[MostBox] Found ${key} after ${elapsed}s`)
|
|
3100
|
+
if (taskId) {
|
|
3101
|
+
this.emit('download:status', { taskId, status: 'syncing' })
|
|
3102
|
+
}
|
|
3103
|
+
return entry
|
|
1860
3104
|
}
|
|
1861
3105
|
} catch {}
|
|
1862
3106
|
|
|
1863
|
-
if (entries.length > 0) {
|
|
1864
|
-
console.log(
|
|
1865
|
-
`[MostBox] Found ${entries.length} entries after ${elapsed}s`
|
|
1866
|
-
)
|
|
1867
|
-
this.emit('download:status', { taskId, status: 'syncing' })
|
|
1868
|
-
return entries
|
|
1869
|
-
}
|
|
1870
|
-
|
|
1871
3107
|
if (hasPeers) {
|
|
1872
3108
|
const newStatus = 'syncing'
|
|
1873
3109
|
if (lastStatus !== newStatus) {
|
|
1874
|
-
|
|
3110
|
+
if (taskId) {
|
|
3111
|
+
this.emit('download:status', { taskId, status: newStatus })
|
|
3112
|
+
}
|
|
1875
3113
|
lastStatus = newStatus
|
|
1876
3114
|
}
|
|
1877
3115
|
pollInterval = Math.min(pollInterval + 200, DOWNLOAD_POLL_INTERVAL_MAX)
|
|
1878
3116
|
} else {
|
|
1879
3117
|
const newStatus = 'finding-peers'
|
|
1880
3118
|
if (lastStatus !== newStatus) {
|
|
1881
|
-
|
|
3119
|
+
if (taskId) {
|
|
3120
|
+
this.emit('download:status', { taskId, status: newStatus })
|
|
3121
|
+
}
|
|
1882
3122
|
lastStatus = newStatus
|
|
1883
3123
|
}
|
|
1884
3124
|
pollInterval = DOWNLOAD_POLL_INTERVAL_MIN
|
|
@@ -1912,36 +3152,34 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1912
3152
|
|
|
1913
3153
|
await tryUpdateDrive()
|
|
1914
3154
|
|
|
1915
|
-
const entries = []
|
|
1916
3155
|
try {
|
|
1917
|
-
|
|
1918
|
-
|
|
3156
|
+
const entry = await drive.entry(key)
|
|
3157
|
+
if (entry) {
|
|
3158
|
+
console.log(`[MostBox] Found ${key} on final attempt`)
|
|
3159
|
+
return entry
|
|
1919
3160
|
}
|
|
1920
3161
|
} catch (err) {
|
|
1921
3162
|
console.log(`[MostBox] Final attempt failed: ${err.message}`)
|
|
1922
3163
|
}
|
|
1923
3164
|
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
console.log(`[MostBox] - Bootstrap nodes: ${SWARM_BOOTSTRAP.length}`)
|
|
1931
|
-
console.log(`[MostBox] - Timeout: ${timeout / 1000}s`)
|
|
3165
|
+
const peerCount = this.#swarm.connections.size
|
|
3166
|
+
console.log(`[MostBox] Diagnostic information:`)
|
|
3167
|
+
console.log(`[MostBox] - Expected key: ${key}`)
|
|
3168
|
+
console.log(`[MostBox] - Peer count: ${peerCount}`)
|
|
3169
|
+
console.log(`[MostBox] - Bootstrap nodes: ${SWARM_BOOTSTRAP.length}`)
|
|
3170
|
+
console.log(`[MostBox] - Timeout: ${timeout / 1000}s`)
|
|
1932
3171
|
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
}
|
|
3172
|
+
if (peerCount === 0) {
|
|
3173
|
+
console.log(
|
|
3174
|
+
`[MostBox] Suggestion: Check network connectivity and firewall settings`
|
|
3175
|
+
)
|
|
3176
|
+
} else {
|
|
3177
|
+
console.log(
|
|
3178
|
+
`[MostBox] Suggestion: Publisher may be offline or file may have been removed`
|
|
3179
|
+
)
|
|
1942
3180
|
}
|
|
1943
3181
|
|
|
1944
|
-
return
|
|
3182
|
+
return null
|
|
1945
3183
|
}
|
|
1946
3184
|
}
|
|
1947
3185
|
|