From c073fc0a67f1cfc54365e6a2b6b7040fff79417b Mon Sep 17 00:00:00 2001 From: Levi Date: Mon, 16 Oct 2023 09:47:31 +0800 Subject: [PATCH] feat: extension support (#78) --- .donate/index.md | 27 - .donate/qr/alipay.jpg | Bin 83440 -> 0 bytes .donate/qr/wechatpay.jpg | Bin 78273 -> 0 bytes .github/FUNDING.yml | 2 +- .github/workflows/build.yml | 4 +- .github/workflows/release.yml | 2 +- .github/workflows/test.yml | 4 +- .gitignore | 6 +- README.md | 4 +- README_ja-JP.md | 2 +- README_zh-CN.md | 6 +- bind/desktop/main.go | 1 + bind/mobile/main.go | 1 + cmd/api/main.go | 2 +- cmd/web/flags.go | 2 - cmd/web/main.go | 17 +- go.mod | 34 +- go.sum | 122 +- internal/controller/controller.go | 3 +- internal/fetcher/fetcher.go | 26 +- internal/logger/logger.go | 52 + internal/logger/logger_test.go | 35 + internal/protocol/bt/fetcher.go | 71 +- internal/protocol/bt/fetcher_test.go | 14 +- internal/protocol/http/fetcher.go | 123 +- internal/protocol/http/fetcher_test.go | 22 +- internal/test/httptest.go | 39 +- pkg/base/model.go | 50 +- pkg/download/downloader.go | 399 +-- pkg/download/downloader_test.go | 46 +- pkg/download/engine/engine.go | 125 + pkg/download/engine/engine_test.go | 291 +++ pkg/download/engine/inject/file/module.go | 32 + pkg/download/engine/inject/formdata/module.go | 68 + pkg/download/engine/inject/util.go | 9 + pkg/download/engine/inject/xhr/module.go | 417 +++ pkg/download/engine/polyfill/out/index.js | 1 + .../engine/polyfill/package-lock.json | 2236 +++++++++++++++++ pkg/download/engine/polyfill/package.json | 22 + .../engine/polyfill/src/blob/index.js | 3 + .../engine/polyfill/src/crypto/index.js | 13 + .../engine/polyfill/src/fetch/index.js | 1 + pkg/download/engine/polyfill/src/index.js | 3 + .../engine/polyfill/webpack.config.js | 12 + pkg/download/event.go | 1 - pkg/download/extension.go | 549 ++++ pkg/download/extension_test.go | 245 ++ pkg/download/model.go | 6 +- pkg/download/storage.go | 21 +- .../testdata/extensions/basic/index.js | 13 + .../testdata/extensions/basic/manifest.json | 21 + .../testdata/extensions/settings_all/index.js | 43 + .../extensions/settings_all/manifest.json | 64 + .../extensions/settings_empty/index.js | 17 + .../extensions/settings_empty/manifest.json | 14 + .../testdata/extensions/update/index.js | 3 + .../testdata/extensions/update/manifest.json | 21 + pkg/rest/api.go | 131 +- pkg/rest/model/extension.go | 18 + pkg/rest/model/server.go | 2 + pkg/rest/model/task.go | 6 +- pkg/rest/server.go | 45 +- pkg/rest/server_test.go | 222 +- pkg/util/matcher.go | 75 + pkg/util/matcher_test.go | 36 + pkg/util/path.go | 113 +- pkg/util/path_test.go | 63 +- ui/flutter/.gitignore | 2 + ui/flutter/assets/extension/default_icon.png | Bin 0 -> 1054 bytes ui/flutter/assets/locales/en_US.json | 10 +- ui/flutter/assets/locales/fa_IR.json | 2 +- ui/flutter/assets/locales/ja_JP.json | 2 +- ui/flutter/assets/locales/ru_RU.json | 2 +- ui/flutter/assets/locales/zh_CN.json | 10 +- ui/flutter/lib/api/api.dart | 68 +- ui/flutter/lib/api/model/create_task.dart | 4 +- ui/flutter/lib/api/model/create_task.g.dart | 6 +- ui/flutter/lib/api/model/extension.dart | 95 + ui/flutter/lib/api/model/extension.g.dart | 109 + .../lib/api/model/install_extension.dart | 18 + .../lib/api/model/install_extension.g.dart | 19 + ui/flutter/lib/api/model/meta.dart | 3 +- ui/flutter/lib/api/model/meta.g.dart | 25 +- ui/flutter/lib/api/model/resolve_result.dart | 2 +- .../lib/api/model/resolve_result.g.dart | 2 +- ui/flutter/lib/api/model/resource.dart | 21 +- ui/flutter/lib/api/model/resource.g.dart | 38 +- .../lib/api/model/switch_extension.dart | 16 + .../lib/api/model/switch_extension.g.dart | 17 + .../model/update_check_extension_resp.dart | 16 + .../model/update_check_extension_resp.g.dart | 19 + .../api/model/update_extension_settings.dart | 16 + .../model/update_extension_settings.g.dart | 19 + .../app/controllers/app_controller.dart | 2 +- .../create/controllers/create_controller.dart | 2 +- .../app/modules/create/views/create_view.dart | 161 +- .../extension/bindings/extension_binding.dart | 12 + .../controllers/extension_controller.dart | 46 + .../extension/views/extension_view.dart | 438 ++++ .../lib/app/modules/home/views/home_view.dart | 40 +- .../modules/setting/views/setting_view.dart | 1 - .../controllers/task_files_controller.dart | 2 +- .../modules/task/views/task_files_view.dart | 18 +- ui/flutter/lib/app/routes/app_pages.dart | 7 + ui/flutter/lib/app/routes/app_routes.dart | 2 + .../lib/app/views/buid_task_list_view.dart | 153 +- ui/flutter/lib/app/views/file_list_view.dart | 20 +- .../lib/app/views/icon_button_loading.dart | 53 + .../lib/app/views/text_button_loading.dart | 53 + .../lib/core/entry/libgopeed_boot_native.dart | 11 +- ui/flutter/lib/generated/locales.g.dart | 28 +- ui/flutter/lib/main.dart | 3 + ui/flutter/lib/util/file_icon.dart | 6 +- ui/flutter/lib/util/log_util.dart | 2 +- ui/flutter/lib/util/message.dart | 10 +- ui/flutter/lib/util/util.dart | 35 +- ui/flutter/pubspec.lock | 16 + ui/flutter/pubspec.yaml | 11 +- 118 files changed, 6971 insertions(+), 780 deletions(-) delete mode 100644 .donate/index.md delete mode 100644 .donate/qr/alipay.jpg delete mode 100644 .donate/qr/wechatpay.jpg create mode 100644 internal/logger/logger.go create mode 100644 internal/logger/logger_test.go create mode 100644 pkg/download/engine/engine.go create mode 100644 pkg/download/engine/engine_test.go create mode 100644 pkg/download/engine/inject/file/module.go create mode 100644 pkg/download/engine/inject/formdata/module.go create mode 100644 pkg/download/engine/inject/util.go create mode 100644 pkg/download/engine/inject/xhr/module.go create mode 100644 pkg/download/engine/polyfill/out/index.js create mode 100644 pkg/download/engine/polyfill/package-lock.json create mode 100644 pkg/download/engine/polyfill/package.json create mode 100644 pkg/download/engine/polyfill/src/blob/index.js create mode 100644 pkg/download/engine/polyfill/src/crypto/index.js create mode 100644 pkg/download/engine/polyfill/src/fetch/index.js create mode 100644 pkg/download/engine/polyfill/src/index.js create mode 100644 pkg/download/engine/polyfill/webpack.config.js create mode 100644 pkg/download/extension.go create mode 100644 pkg/download/extension_test.go create mode 100644 pkg/download/testdata/extensions/basic/index.js create mode 100644 pkg/download/testdata/extensions/basic/manifest.json create mode 100644 pkg/download/testdata/extensions/settings_all/index.js create mode 100644 pkg/download/testdata/extensions/settings_all/manifest.json create mode 100644 pkg/download/testdata/extensions/settings_empty/index.js create mode 100644 pkg/download/testdata/extensions/settings_empty/manifest.json create mode 100644 pkg/download/testdata/extensions/update/index.js create mode 100644 pkg/download/testdata/extensions/update/manifest.json create mode 100644 pkg/rest/model/extension.go create mode 100644 pkg/util/matcher.go create mode 100644 pkg/util/matcher_test.go create mode 100644 ui/flutter/assets/extension/default_icon.png create mode 100644 ui/flutter/lib/api/model/extension.dart create mode 100644 ui/flutter/lib/api/model/extension.g.dart create mode 100644 ui/flutter/lib/api/model/install_extension.dart create mode 100644 ui/flutter/lib/api/model/install_extension.g.dart create mode 100644 ui/flutter/lib/api/model/switch_extension.dart create mode 100644 ui/flutter/lib/api/model/switch_extension.g.dart create mode 100644 ui/flutter/lib/api/model/update_check_extension_resp.dart create mode 100644 ui/flutter/lib/api/model/update_check_extension_resp.g.dart create mode 100644 ui/flutter/lib/api/model/update_extension_settings.dart create mode 100644 ui/flutter/lib/api/model/update_extension_settings.g.dart create mode 100644 ui/flutter/lib/app/modules/extension/bindings/extension_binding.dart create mode 100644 ui/flutter/lib/app/modules/extension/controllers/extension_controller.dart create mode 100644 ui/flutter/lib/app/modules/extension/views/extension_view.dart create mode 100644 ui/flutter/lib/app/views/icon_button_loading.dart create mode 100644 ui/flutter/lib/app/views/text_button_loading.dart diff --git a/.donate/index.md b/.donate/index.md deleted file mode 100644 index 752aa024..00000000 --- a/.donate/index.md +++ /dev/null @@ -1,27 +0,0 @@ -## Donate - -🙏 Thank you for considering a donation to Gopeed project! Your support helps us continue to develop and maintain our software. - -
- - - - - - - - - - - - -
微信支付宝Ko-fiPayPal
wechatpayalipayClick MeClick Me
-
- -### Crypto - -- **BTC**: 35jYkPN8bgkh7AWjt3B1VgMQHqtGYcvF6p -- **ETH:** 0x928a7fd2398c941Ac6713704e606Fa414595e3A0 -- **Matic:** 0x928a7fd2398c941Ac6713704e606Fa414595e3A0 -- **BNB:** 0x928a7fd2398c941Ac6713704e606Fa414595e3A0 -- **USDT(ERC20)**: 0x928a7fd2398c941Ac6713704e606Fa414595e3A0 \ No newline at end of file diff --git a/.donate/qr/alipay.jpg b/.donate/qr/alipay.jpg deleted file mode 100644 index d7d0fd623a9f0df450d20f0ef61037ab6330ab7f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 83440 zcmcF~1ymhNx8^yx1a~L6ySux)6Wk$Ka0%`NcemidB{&3vySux)W=?Yd|K0oMy;(D} zW=);b-Mee=uG%eCU+=G~e=Ypl03gdqNJ{{KKmY&;`T>4z0T-l2MGciy6eXnP#Q#$K z1b`9*698am@8YB)DMGBJtwa1f@88MD#QEdD<^N_2D)(;gcj*AYH2r_0|KDZ7nwmMA zfQ&9c3z-wBa*(s=AQ|1_FPZAMZ2XtZ_gi*z`RD?&QTi=Asi}y9WK)n#W$_Q$_#d*# zN2lNRQ6L*WJ6qS^b^T7iJ$`3qudWK(!hjZhfHOb^APErp`}`g90RXOB008{U-+9Jq z0611EC40|Gl1jQGC&jn4hHu74@wZA zKO_t!Bm@K`95gf(%saSu@8IF!;Smr~kr5D45aHpGF_BTw&@nJD-XUROW1?fDqGO={ zE(8b;ItBs~77`K`9RVHz{r@`s>H?s^fQ5jCfCEVYU?@Ov6yUEu03qniL9YFN691iG zVc>w!V31G{;2^yNG5`n$0R{;Ujf@8O4jKpm2ZDhEARtkopkZK9-!r2LE8}PyCDlz% zqhnxVlW;jYySRRiOGvDp!Xjm1W#`}&QBhUb_!OU4Q$KS-Movk^CaGrP)J-8OE@5nH z77&?{H z1Azhw^`0437!6(7$dQEQbDT&G2CIs3-mfLVJ8&TAWWiAYf&dLTVhoSEHb3u=aF|;r z@UQL7XBP_4m3tgiB}OZ}#I0H8x%=ewR7>&bTNU!3$q)fuE-6H&K3^FGem*OchE!#d zNI#{`_$=nc;PyjZjVyOZnvqwqbk)H@jM{EyI_E-dLcJ8ub5`VLN zR_16Bqe5W7QRK>(kN4u0_gxzip~~>g=R9;@*uRW%*lYk?g<|jHLu2m)gdyC$7sp00 z;_7ic$GEDQLl0v+$TLz?WfIXhWfIZ$g!xc?z9QMXi@l$_;b4s=XG zj81orIyj!^$v9K=mWtpRx@jAtJDhBAcZS@S<~cQ;Eh-i(M`%W(2x>;RFZ}%7itptD z&cdeFgs;T(&EVx@O#pZf^`c-(C-k2+KIyWVgsh3&MRlimH#*kwEeAF$gY0lAanZNJ z!m=Y`N2R+BK8t@GfXZ1qS*WN>lk%PQa+_blhop5;tsQjZfcPE?gD>q5+<(?_tAA^t zRdb}O8r2y;S!{Pn5~|kvG5R9@S(7==ju$Esss2@#uK8VzNBEbaFABW9s@NW?o90?_ zT)|GCIhGoSViEFyRe?ds!kjmXzWB*SuF!0L{ax|Z7g-j5rDpd}OzEN6PCAKix!B7W zNq+gmUCc^(MmVvfe1O;~aJa{W0};h}!b62b^qOnvpVxrIgwEX<(QlgIa3$Jn;T|>F z+`jORwOxSxRP= zM!Ho+;zE)SoQtXe+ZqaNNtV+i#TSXmhvT|^>xJJp?~(5oHUOQMDwOS%12z; zptpoFN53B;H*1`@D$>_c6Vc#^w~v>&T=c)vwGOmX4r3tZCZ8o2I$!OK5@^?mfSag; zunlZ^d_6j3jadm3;cOFl)qs5?=OX?EFcf?;`Gz{<7~|OG81oSVSs3`E?=(-E%wHG_ zMK_w(kWgE15;_BA{Br-%v$L}fQ;P4MMz74K4~4VhMZDklDsYXfx6s`vr?t@C*g`(RcO92CZ}XU_#gUJD*VrnpCd9ChX%G zkmR7d!+ld|Ittyg*R_nRZ(CwiDg|Uz+wXzbB=<|aTNBtRhfd{3lD@!*HVwt|!uvA( z2MO?P8qUo}ITMG8yAhvy9=0>`%IQP&ISh8?uX3E!bUj?n#}Tk1;*UdNgdH7k%tHkFf_?!Ab>5$8%RnF{Ox|F%c~h7dPF zD)H!13tcsac^bic;qkYKH*D$x!yzdYjkmk^lkhv2ZX?xs_upcd1AW{CF{lVOhm;KB z){{;91=3E!Q1C82xK~1Ej$6^k^QGGe=gkJ>c#5^Je*Dh{H45Gw zxr9pF_}l*51c=-t_}s~<9#3b~6+=p2PfDqsK6!L4OVe-wzTSP{2W}#x=}kqrSmmw~ z$CAm#j6Pz0MtqhwL0{OP}9!l4kdVYoU@NOV$imWc4q$@Uv0|GX0Q6lb0o?-z%*>@{p3| zXo21HzwC`tT$LMm7@F_m?I-Zc$VfIP%}MkU%eYtJf2 zm|tXq-Ej;hFI61-1&Hp1)qJ9BqhF_b^9B0FG0>ixWF1&N6+2~M?_$bqEt#|Ry5h4c zrB#<1rCBGync3J5f6L0n7Dx?xZ7$0f&0`KE1S}W*={>P;b9OBK%vT`)E!9N$WBIp>5!T?{mgRuVv zpgQw)E&Ab8;oJ_L==S@@bj)$D`A-3bkd5xGtD%P!19%x{kbVJ}7Zmq}PhJ2ypmSY) z8*y$&^wMO2uHNkrcb7!k@)TP|Y@m8MXC&pBw41vt&Qh^Qk|KU)LN$_Zm0CT45nozP z@-fJ1+dSUA+)Pa9S2k$yXCu65inZsgJM*mG9> z3wBfyN$4T{RT~1nLhpr_)U;hb23?CAN@CX&8oZKkp6f0?fPqg?*Q!ViNt(h`thrZG zjmg<(w_5S4(wGS^T0CzlFZ} zUg&e1R+j{Q%`~Vlx2dhR=QL?W-T#h2Kj2i3RzC$)S#ip)FCum%i~9gAXJt{8mEvRD z91yA`qn6Nzjfkh8;6k(D;9jd6E|>}d z%~zF+l&Y(&MHGuPq#dWX15Mjk#pUD^_emsAz>0%boj>?pHCmFZ8Jcfg?acvYxfBTrl~H5`MSFHS%0{b6aOWj(g0?oiWO^^dksbB= zP-9Y|uhdm~wc`lz=>wkqP%Cs3GPGXh@?=?nJ9t?7y3hR%;rjQxdOMR!EQy){CcRU# zUjYATV6le9X1fu$-TRb9c!3JLNa|TK^*vLK-gqGDS3A2>@4_Kn;o3~0MYxVPyeP2X zU351)+T{?dQrg|-Z8iq5&W}G69DQC2CJCU*v&iiQ17|)RiT3s#%-ZDpS{|%NIIiq{ z*sKvvDU*?DpsF929l44S$)v%w`hjAX*T3=s`wP_N5Ld86j1q*eclPrF+T$zsE3yAZ zt`w=M&g3I`Ej~q;p3Zi@pfj#6?@jNJ9scFJU?e7HmV+ejLuGD;A**TD_aHa%bmcj6 z`KHk6V*UrqVj7-gvy>wF4 zGC*dDNUu)1BjiCCZ?S!HIjl@r;@@igxW@NGLD`03xY!EyuvlIWY$+V5E6-MV3GI3g zY?Eqc?)?yq2Y|-AGp2P_VY4C{gU>3=xj<>D6_v7uEHo#~t}=VIQy(%-oQ(I`mEmXn zb|9izS9^r2n5HA&)%buAYIZ{b&pnZ2FDOMU$opIP*WTpfbaAob!F|MHulqH6i+BYu zLt3G1aRxgRyfqo59otkwAHjSZ)CslDXH1EQ9;FRU_Et}0Is(flc7Wm_1Y&VRx_a}*2YmpS+lQ)0)>H<${CY}Qf=^KC~6WsHJJl4VF!vv6B7Tc=G-8|&ikXD z+&R+RJ7^dC7*(;Tc)C3TM`i5jTZ(52J<8L>iUot$#ZSI3psqOyqz3C zx#PWdezy`D8rmI5MpU%v$yPYT%^gCvWYwofkObi`-Usb2-i`8z**5WQb%9Ls6eXa& zqFU0HRVTY}re-<*`bv>O|E|qETcc;nHt*vgwENaOh={(Ql!)q*as?n@F)W7^;8l#b z7d>7-$>?1#J?5V5DVNWPPZ~J>WNnsIJ8c}z$B|t4C9XZ&NsJC>Ra-|=jl!HlYxt%z zWlrDDfKMv2a3#?AUKkmLHF_gljosjoN^$0vp!LheC6+b%{h{+Ti!;^E9{Gx@0?Ee7 zS3DHQ=5u6%J53)|rr3Ys9np@{8VK)5zAftC%Jm3*A96Dl&o=do8}`)kh4Fu^;hfsl zoQZh>$8f87MbEw!c|(a5Gj=E%sxBpXfTB6qexl|-3VtC4p4_-ycbTPZ^0VGF=8xE2 zdl9A6au;-FgVS}w9_AD^V465&EWAt*IpRao(+m~8k!DH zVf;LgbH9mf@1TZE(?W>#<)c&YJnKqkB$|-bDHGrEV$s7N*gC|YG}UK9y3`+K?0)Rn zGj5Vzb&vo*DSml1TolZPp ztnTqh6P1OfYSk286Gj1xU0bO1C#iZ*#|X71V5b1ZSj6*RfZ%sl!~qV-fjZgZ+nM)6 zFXVDv&7LO6x=0kP6SOrYmJsZFi zE%YmF;$Cys2MoW2B$bS8w@LI9n&%c3YdbVHK*Mo->M*02TT&!UMF{OIW<{whV=pMt zJ921lza;(#r2$I`Au;xKE+)&lv}5bOFYq5$WN~TKzT* z<1A@JtT>!cZT7=5)eCG147FnWESr^GvIuK#k?cAq7s?@jrzhNuxelo3`1~}9FrXsU z0T}Vp@_nr7oytdy*2fhm+WU)INM8cZ71ks4Y8A(2U&9VBMSn7DX?1D)1sFzumR9IV z=EOCND9lsu=NG*p4c|+?{}OHd#FLGaAx~Kl@3kccIh{dw$&m5*oidh1 zBJqsBC;n#dFlNRyF?lL2ky=IvPLEn+qyD_1>C#^>yDOBmlw0$so-LPOQ28sS0WlCn zT55v$PY?yN0Ln|z;uEqqmAw>w=e#m6s0M0s#AyKeP#m_e=f?9YkGUZ0-n+kiy zHNX`esMQhE^i-nCKX@LFa};IyZ@wIs971BU!bDQVw{&LR$!kmEvP|Tc zj*CxD_G7U+?HIQD%QFB35pzG73>|8)CyHi;w?p(L@G!?~vy5m!O;s}39w$YC>;jn5 zK}MA+noHhcUXD`PnPj@+!wX`JUM2jrK&w6cIhDmYTVJb8;bYMvPcK(YpS zwe?@|6Em!DT{g*3h46?jSUN) zOOyjm$M4KP__Kz26Jv1#X*@8QuqAtEM=SNhe|qVI$2_a(g)b;Ht6bRN$F3zzC3GE` zd?5nC9~SxjvGk6B}If+bVNs zJ>>PKnOjw8a|_VeeLCI9W4=!87vLMk_q6B%+;b^8G?fwCxc&v>FG~bv>ZPoxPCp%u zU>WEkD2{mjJNY2ZH^NJ2BB#*K)Ug^*h5thm5@{x}b|tw9dQK8Q{jT)4gk0em9$HrS zDXrP^8V3Zq1hFcNQi?5V2FvWINW$_4Y3*Nl#FcA8=t`y+3WkO|mSbep@LYu|F$ql_ zc%O)IUBq9_L5*_wCR}kECtTD7#q00r@&oPdKR{U-mc;HT^m@JF7=6Fj8o29;@`HHY zXWVp&Tf)u~k^Gz6mmE_G4%Iu-rP!&<@_PR%TBw8Bpdv>G&eXJ|V!2TY`4V#S#HYw2 z8YTIJHhbyyUKgm*MMa?(El$H!#6D5P^5j@QR~!DXpO|NzCy&pHEE*8Ixlx z7ntE&t9bZ{t}0C$5bq0Y4<3uFv<}$~atby)4iD2Fp&*72(&HWdlLxX6&;;079X`YV0%-Kvy@6a4bU6mO_TIPlm9=(R`h_uo>>Jz4&CCx9YrDZX zZi+@Ql`ULY;@zh?EiHXa zDn;=AU|}5x2slHGh5es0IoI68CXXbnVDBlS{BAY8E{j~kCs~F6xWPNy^GxXaFnt!LOF_z(BRzRq3 zY>mnJRbg0hd@e~~*r`@QAb!y(Q7Ss{m0YE~;U^F0#MaTN&H zbEn>yV@~j?1mg6dSC#bR<S=tnt9|N@{Yz}LE>iT3JLotaCHw! zh^gb#3~`hM@fFW-F*K!&bJQ|Wo?}xj^TyZH7vjmPr8L7(4!LMHsCos1V&zMlj*7_u%ww-Jh(Rwc76`JS?@1!o58V(*N&OOQc zWh@8@#xvWCxA`cx&nNb0wzM7ZWM#kGP^nM3y-Z2@-`hvl*L1p*x{%V_9qQC~mPXgz zFesXFz7>j&y4Vp1_DodOzf3T4u!T&KcwufF*$Eg&og{vTbuxBsOjHZ$QExuqBF4Z4 z%?I#WdOpX5pIzRo7Z{FDXqGM|29Xsym3x?Qu2NR8^)Lm>mB)4b51;{_T~{}Xn@`;f zf~YLZ80orjV!gI)1VYL>F}nx@j{Ymb9Ma7Y&SycCm7Hen>^04bX}}2g#ZICX+Z&{% ziRYASxSRLT$4b{H+p*GLUJ<^+q%{ikJv(OQ`K2{IPd=9Hg&d!U8>ZmaJ6E%R6UW2c zJhG!-qBqcIWvLQWv1*`)!-6Qxh!t7_qk)V(-#qvc*x(Fs;U2Esc-u7u_q&V;_ODdI z70JcGmB;P%n>7D^u~g49jJu~`xgYp3gv_qdhOHkxJc#YUkvQq=nbLN1mwhvcv4zk> z!SEXB!?!XxL_5#W4IRc9#|(~W+dePx7CpLu&d=pR&nV>`Crqs)6vx|{gRND)A<=Xb z+L^}45C%$9x{tC5;oLM2EOyk|kvUQ3nP3i%j z4IXt%VSlygKJxaj0 zr%Gc}Y<$j_Q{Frp1m!1@!u5U~>c(fdI^JIZI^Ikoxzs`O7IFdX>xF?BS{^WIm zT<7w^frO$&MNrD21>JYKZauJ+sE~6dLeOsUyvuqN2wC4fjHEa2%xX76GjoUk(w<01>jQn6z|t1;raE6 z?Y76z%HjnAqB4LaR-IHLyZMhwcvfn%qzdgf9fpkB3KPMv7LDt<$-N}~3jl=v+e}F2 zfQ0H6M4cT6?ORd4KoI=H3`na86WKb#d7zwT5J&xF!hA(wg$t_Axjx|yj7=aD*L%m_ z(Xu=GA_T2pwsZ?FL1i#0iLIqyG#Cw`(!j1iKF+i-Yv-fCVq?90yV9H2UAuASgPj-| z#om7zMGQXe=L@mfdqW?j!(xqOPLfwkfA>AiZn1E<^-(6#HtyS!T0v(G@Wc)TlzQh` z1&F>Vbt?wZW$gKubAFjvywZXjq(WA4>DV?}=DMm^_wTKtakrLSg)uzRpmChCQD}nb z8yFMQWi2^W!|>jVoo_aGQHl0&^;Pjy)h8Z%&)e_t@wt0%P6Tq-VnJsr$1tT+-7qtS ztFokL7IyDq!b&Y#&CGFA=9Lwwrf0D z!rq+ie43`2u(cq2Aoh3JySeoTss_^oCpcYtAcJH3G~tF zM8&6o=ft;k;w<4qCiI0f9-qEs1=C#RO=QrzCJOso-UrK8{Q^9itLGf;ck(LJK7I9M zj~5KWxJ&ly5>uUaO*$sc_7ia zrN2o&c8^(=a(ACvTrN1{o191fIdS=lu5{#Fo9`MTw#Er5Exjq|(Yb^Pm4H zjORyMFFQ(4xeRNvSW){N=em+(FR0@vR$)S|R}?7P|vaq5{} z&;619Ar;r|wa<4D*T#OsIl4k!eoaT=w9j8=0l5v^a!*q+J;|{1pjJDTnCD$st0&YV z(u85?1_LGIj=kAN(*@ump+m6rWY5_L(aqxzFe@IEg_saiv`C7Y9}z6)4r^7-rSFrd zZaRANU=7BGze)?3S#g3bZ*Wj4%-xdd2^%ZCPX4q<$!GOapiBLkX?ljyztAuw?pmDT z;WSt*-vNfnkkGg+qX3$fvUax#%c@tB9g_C&JEjgnWG}Fs`PYL;f_500*j%E;U^%=; zA*L$9-h-NWcj%yo`RoMAN~Mxm=;TaeXWnhKhGzHj0dq$XLA}E%6YcQ&7oeCI?&PRt zN)7%cR{8fp1KP59G`Z8UNK|tEE}`QV5{dg;MAK?^5ry($*OZ}$1Dl7o3w?ur6S5oC z(j5nHE;jXsNf=a@J?T@Y{*is|BR(H+uo9c63W6>bSe!BpO6WLQX{cFaA*lTA z8bcBjnqwfkq|9D=+F^vBX84}Dwx>)5MXhmZ8uAb3R3rx>hnLSluL=HP7)1inIFZH0 zE^~EWlwYZTX#44lBB&mfXBkVi72}8^#z+|1HD_=w4Wvu8S24ga!5S@tWO)YZ4cL+AkD4spc2NCn+ zdw6)Xk~GuScAxkTl?hT|XR#Nt+MD`R4eIT1zrb<_?VaEK_A4AAc`+%}*%?aJ(EuZx zod!c3a?mP?Wt-dzYJNdSC9lo-Gw^UDN>9w{hv$HcPd9dUd1yJ;u2Yh=9_S^=7EbJ_ z*FI02Mqx^unuGWpA`F%h!(-qVK;a>B_WWL4bn&Rba@wy9cRWuV)b&;$KQ+|{|6p&8 zHctDD#;W*Lj=@}eonR-SacM>^!T5q?Bf8H3j3fq%8MTI|>L!V|L-OnTv<|2RV3*Hu zul(V?wcjcJY-@2_f`ePokxyiR{Y`p9^D*BHOW*1Wz7x-@wN=q?9ax0lNtWNSzzj zBs*tQkYrv$t(Y({{K?*KhiTZCRS}`6RG1ozZb2`upB&zvd~T{fqezH~un*cWyd(K& zt_@wr_8uvvMbp7NC5#7InYkRuB-@$c$ z%_6&DTu)l(=W;>ALC3_=yokkT{4jJih5WW2p_58R4_}$JKFX4id zhsdZMFcCg#`nXRUEq_o98+4&(=l>d6g4Kl(XDH){*8lj|cv@gtbb+ScA6y_Nw?#p3 z{kp{N+WiN(5dWBE{uy@jPx>aNa#V0IJrCARF;n<-OXEE@Uv+rB=q0bLc+4YwG5tUk0h?Qe-}>hw>1!w!HXy!9^Ns0 z{vSpE(-G9Bywg6=2=I5=zaU-GoSoaEXy7c9fmeW=v0E%^f>krw@?bRw!31CJ6~(1S z-d9~j#!-R0v4J@H2l$B`IE^7V3bal*?8G!Gc=JQ8W^A!oCp8)NgpR$bFX=Ti9V9*t_ z;EcXq6!gE5&qrt$bDkb2X6A_3Ms6brP9mYP`C7-`AaH3_Uj(bg+r|4b;a|QZw(*}xKw zag>!(Kf~0Dc{L;5NHeHn`H2#^YM0nrWVHT)%$|sO>#R(_6#aCY7atD1nhhgyo0ccP z;aFXwvv2U+981V^>?G(9k?cask2|q@pD=iQEj^iL8%Tg#e6w#Wpd4Tcg#BZ|X^;K< z2bl2@ln165fwI(4%2C+w`mwXe5O41aZc4)-rxAu#L>|5jBzSQB( zgLc4~fO#oUNLFM`slRL2FSUx-r9VuXac?cJR`6Q!;?w(VA! zc{;^Yg60urRYJ4w@qN*WCe1dmOE-)#kL~ez4Amij0S2A)e~M1=y(3w*H}HDOsRZo` ziS+4QoVaqV5A~{T-gvwX%yGi9Q}ko^SFZ|QElbcPXZI}BL`*ku9#tt6PVY<|8^Osy zqt>5aQvmzXnlJ7)VQl09hjlUZ4w+IPiH>Z00X*AI%tC?W5p-@oh?2o!+nKmz+wvcp z&+@WyEKgYsVT{@8M>1RogPGZqWb;_g?DD9U0F_0$e(;|@VE%030Nc5He_$Q!Y$L{_ z1ORSlo+Bx!2~U;|@O49s85whTeXTHjOJr$>VDsy1Q6Oh=!dbX^KYryJE`BoH!;rVIG|HVS@^ZG_flbI9ew7Zh$Ubd8Oe+l*$SAvw?1jG2;!GD6`YwMcZ2U z+tQmojq~iH6>xTtt}6c3Z)6Q4a60`0h{(aq3}*Vy>YwNb$D5U`6b=v{` zXc0{ZM$_)eX-vxm5z$pydc@uI3iutD;chkjG&PbYeE-9IYh85t;#;2cciibZXfEb# zilVFhEX^`G1=-N)+Hb+lY7Efe9pF6y9H9jhBokTyqW9Qd9^bwgcljAHYu3Lk+!#+Z zgC7zhd47)dPel?bU-)6_+-JyjIZ(~2KdWrMYiPP0-Q*ConC4l;T7qegNLDeu_if4a zi1l2abyy8EDX7vkIv3d^3hcF)NUlCdz3!qkz zcyp9Bfyg0eKfSQJiqC}alks2T-Mqli6yD9EWYyyviIk3v89CFF3-k?tM?0oj6b)DY zwi7%WD#z_Xp)pcNUx83aOtx^g!ksH%E5{i6d+IO<`A3Z)W-Z+C-1A0%0nV}O^{tf$ zpZH$NHFgb*`X}pc8B~N?YMQWRljIe(H(8a^UDGO92YI$;vdRJ)E9AQbX}5u3_Qb1` zkH)U9_|DT$3LungLnn~Lzj;dt;We#>1VZwEhHOW&|2NvjEYkiP?MxbFE|ic})9 z9}OmlRs;CMvh;t$U&7yDS!Cq9K-=9VEcHC*cOPYt^R2Zt+#G4RPVcD_C5Z&Q|3w~H2+Dx)YGFmUpqw*ZH(da0F z+_3#GZgBQdAGKJ%-Wyjhg?^uK)vNb?uF*UdT|I!U#$Li|xW5sjIcm0i^BcPLo#9`L zb&GH9xr!9HJSj%(afM49wYGwHEZ(C&V+lx%EFVQNGMWZbY-9OfL>uNTiEXUnUSw90 z^mK|tX+Ux-_^@N2!>wGX zz^{PPodg#(YS?$2zO!eFZ^oAn1IH`#?P>G0OPZ>G!vBJ~lr3n=0^(p;v_!+PuWzO> z=(%4%l-w-wQPp2Bl48exp9#;#PSBLUnPd7+bU92HG#YSz)^5|l=$3oba&Uid(6E&9 zeF9DPkIiVG$Qp>9|MbMDeJZ z^}BKNnrLl^Kco$)&lvS)H|R!FRaHBLBQYgZTd7S`0@2?mVE%lV{LL_-2-7Ou9pEE= zdZ}kh`Tbk(KU+^slhW54^`!R85-2JR%d73{cfbpF^3wAQ>Z%JT%1G1S zBx2%7T*ZB`5B5?*6zid%+wGKnDrzZp;=F&4_NcRR<3;>gvm$V@kk>K-SNz~Kiao@f z^|2-4>graGh?8L@QgNmjh3vpJF^JBV?om_-#%os@PO}>}&cMitP7(9njBc$%tHCwp(xrYQJ;el}@DJMUL|^ z7IHvYU~b&+QCN<(ZNoYl1+(PW8*RoqD%}V948p zUywHHA~T&jNrT)}&!=(SR@+K89Fhib9Uy5A{CtWS2YM6XLP{ZGjt+xz#rL5`9vf@o zC;ItBt93`M=$0pQ7?_UEw?9RRy^YUt9IITGK<%XOz^0=uB_eT?we8M?YXC#{!GB1= zP9RDLjd2DMx_5ct3Q7RXxZ09KiIdb11@Rg7*x)Cw08Zi05e-`Ww)%403W~%-DHRpq zx_uwS;U@MH3cN%U0pXDUV=Dlk~+ zkZs_yo&+q!()0FycDh(pV&8De&Z3Q;MB(z(*{ltqK0hE&#yvpd zbh#SHdiS9wOmo)puMPw8KKH2kuMQ*BYXVKvPtN>(JDSd%@O(K7`k+pO;Sbh2gkkn3 zX%|X6YgqZwCiplsloFLAcYzvD$~^| z)CUKX8W(i}EwMPH*5t&h%ClodUn;dvMcWTx3!Eo`%3$x^)W;>OTW7Opc-I0!57hdq z^|&>`xsW_^`9-}`SV^+yFpDln%`Pd-=2Eo!)2RBHh&qyLrX7uXc#Se9-e=w?5_LNv zrvR=~343Bz&$tAV4?I;p2Zr((&+g0os1~hL49eDFE2Mmb6}nLz{fFo5FS{TGoBDK&Kb7% zoN0Sms>lI+a_U|?XMbvQkbmcDVRDLBx0DiN^N@fzQ)ouMZYHOi>D`)Nzd*z>CElJ2 zCFG7BT4sVF3fxo7D2QkRI`YGGO2`)3-Q<+#} zLHJrmr28CXg}c;uEqn?NrKv9yomt!+<=ItgM$J{O1yISvHWDLK3?9S?b0GJ5MrfDc zp9Z$i+1$GG{FaaZk|CFC)jvQzn7773Q%2W`(Yl?%A|Bj$jO8a=Uz_ORDhhwHUN0DE zA8KQ}`fH}O)l*Y|*k;Xt@B)clh5FR6?bv4*H$oCFhV~KzSA#duvyQ?ir z$~i5rS5(O{@;~KwyjJ|A%fX6O4|ejqR@+xCYrezEwXKaf^7kpx&@VWOb0pZuu+y5b zhwf*{8aM@WR$vj~L9H~Wwb`nP3ytFxTW`JDV7Q;1@dK@iGd{+UhV6&T=4bnBjmvt! zSMPH-Zx<<&nl71DbZxC&la0`_gI~f!pwk&vL>u`)ve$hO^T9R-D+F&wY1FS%5AUqb zu)NX?&znPM`&dB&eq2KM9q}{o;~!R*d4m{Z3az z-!v6Vg6~U0H~WiVn6)buB?W~#vz%2hi^|rJKc0PVU~f9`=Zdi7G<;{d;qTV(KD=V) zrxoRDbCq+>i1Qp(eT@80;v&fPD_Ishb8E;2PD!A>!-}2Qsz&QeNlhbszvfMv;T);b z9#kk-j@B4e#<(r4PT>U8+v9Z}>IWsb=dsT6|=D@0%Y^ z`FfG!Pu2;mGkrowRC}#%kF4FrD=YPLzW{H1%mgrOZq)~aaaXCd$m7#TtS4iFOa{f& z)}}=^OPI7W7=FfJBL5j3wS+F14PH%ry`u(hjlI%8kctVMYEW8;cq93&Gkyg%uQ6U2 zX$W#FiaY4kB^-%RozV-#7;RwaM*?R=W0`1~^qrxV)Dm_EZ54eV`$DiL6?-s$P68%` zb4xNiQ~=%}v%U|nigy!tag~W)D(Phub3s)Tjf@mFEe@E(aUGT4F8~Q!HskDS^i5x% z>f?B@BNS1g(TU|@`Jl80z7Lvh~7x)|{IpvW{im0NT6owN`t1up~6?CSvkWCMF0Um^7jOMj6 zFUz)?ulb$^WUg@70kbPHM$hk}bN~I6vh~lw-bXCpqYnzh_`wb>0|zP0xd9Vk9&l26dl5H1W_cz-0Kj zqG1mcfjbuSAQf4h-F7R8cfr@*8sdQAmVbnC5qJUW6t< znA*;xw4kO~(8ou=0P`eths2WM0lSzX8LAO7_ChVN#Co57-9Jm}o2wymabo}V{PXNVIv}_G!M}_spMK5TFvQl8 zuQedX&M~%m=&c4w%ht~8$z&e7@v?X3mm0B-PV@4Pb40bG7jVcMYR#=E)SDWUqT6pv z6G_RbV)vPDxY{GJaw-(P@{66%7x|VbtM{d{?1}!~=^M*A$Hl(@n*IhrxXzIrYu0ZC zwamZ zBTA3U|JlNG&GO7^BLk#?Z2`N(s}d8=uC!Onxn{)i`qLk9(cw887V5wlNY6_q)j@=N z<3u*TS%zvSNi_*vJe~*xGLYs$U(ohU^2U_#-IBc>_Po{@2E_actbfPgpvWKM6hdL_ zH#;u7fkQHuI z;Im?~YFeeDQ8&->^??YHn{M@gaP^j9aRgh}=-|QKJ-E9=aDo%ug9UfDAi*61gKKbi z_uv}b26wkWaLDbP^WN|M?)}lv)YDxv(^Ea&yK3#V*0!iD!DQPRMz$vno8N|fLP)2} zfa!~SPYoYz>{bPtkfZ!tOM!-ti!uPZ)Zh z#4KmD<=s}!d)^OHU&1J6@i_`>KjXWomJh5d3vNb}vNV06J!uaVo+i2&&U^j@o3s(R zFz-4g-Z^RF>$6pZKvW}#3hjz7y@tyWSr;-$2yvT$e9^~wEE3#umQ`yB`fhRnEHIkx z-(_a}_;}qTcl_hmSKfRq)B|e&Z@SN6-1bu@F?Gh-h8)~W`_Nwq(K`Nt{R7_|IlZ@- znS3|+Ar$dil2mb*7(3rruG@N$2~URh9B+lyA-7&Si0eS2ATJSW-ERi=sV<#(k}k^e z)JK@v-jKxYVLX=Zx^RQwXQdWl>9{^9f@A#GCwAz(EGU^tb8ciJk+=u}C2deV_hO0m;^tq5ZL;?5Xir!gZtEIW28 z5a7COK5-sf2XmJfYthxZy7R6Sb%xQR${4a>6`}qx#;IBi++a^?8uvatCJ#RI{7AXn zr@X{+m|N(NUbjvX`0Znea~!Y*A`pze=YWNRi-B<_xu)ZGnnvd2&gs3}sifnOvB{W# z?13{vAQ_(_Vzj-K`YjRfhS7U8eS+K=e^O*(y}Wjac94d?x$_?;-zx!^ajypsOOnp5 z@uyxlJE|x(7eiF$!!SC-uq8=;ZF;08t;wJwFGdpsbwaqhZy=lh01O=?DLWjpa|HQ9 z+{oooT1ivsRjs>Oz0eaa{ke@WGwc<$&SliYDbZ%JjM*I>_SziE*VFisop;w{3?ko@ z0Pxphk_F-VE>j1{>JmVkCM;qNGGVKsF4BSr#y-LSM3VTF$5(jM%PR7nkRFeC#}kkk zk3U)9o=3glm;sntz#Myxo%U89MS@uCCLZ0iuGmp@awPnoD$RXBJEa+&dWtt3w8d2! z=(0=NABl#{;P^QOLba8_tFmih%eoUo&K#vFJ?56@Z>Yk}s{!D{iF|Hi974;|U)^KT zcfY59sQl=v$cbIeM}_llk3+0SG`93Aq1>%uGK$vY@Q!r|46E3pSfPlZi>-9V4?jZO zt9leT*B=p0W0fZwt*rfqD(eY5X#WutPhm!CQzonSg(%?yF%U9*fc^tFU_#@2Tpd1o zKKi{RULfDH)?`#w`&H`o=hCc;DpS46+sZ{Esm6)%xIBSRI;!$u4c^M&p0Cg^pf%|X z7Y=J>rLB-e-cE~T-owXP6LA0moetQ!0)oc;zbw-Bx;3FVSOduep-SHLWBG%RVfz=z z{+_-*&0?U$ml+ZN;*+<5X;ol#<|AMADv>_+*71)d9yFio`M4WW=W-+qUJ}8?wB(UIbeFrgq$#PLEjrLJa0Hh!GnwlT4*di{ zmWa0~)Sy;X3h=&-5290~xovvJo&l;8XZDPwvN_>diI7EnYR7do!)LG{3Y~&~bl5Q# zwh+9jnr1OxA#rpO1s39l^sq333*S55kgo(mwZF*n=F1xp_0qnlcvufKv*v@TOP37{ zmfZ&42N)=SWWR}hNW#fc(x)!YS_k{$dq8{GqkSh>#f01A>HiWcp6ibf^Z5sZ&MGhhsfm^HOkHhz=;RN6caodw>-7HwqQD@kh&W$_ z&?^75%%*i!ftB!5ccRMqUql*>F&#3%o&*q5Rw%&qMOpw} zX?v8AT}9{8>9$nln-@(*?LdmDKZ$XFESa25CMxeKjwcm3fjMu6qQnqXo9u|G5{^qH z``zE)m@~U51!AwDHPYHvuM{E??mqOz1|dCp5cX*;9vMog06&6^XL!&3esm{piK=gy zwbDZ-Kw{9a8i*a=Ax6YUJsDcho0$NK+ykoTovY5F&YhMuLkJV0cBpy9q^W@s<33qd z+aon^pOKdx75J0W3LhUejLC$Tlq!%nP5TQPHgPn~KMV>Cz8%OhSfTlBb}Icu5xJ?d z0F_Y^L5b@{(4NK_L9E<@}7d?cK;QERj`bart`yeM+8uiZ6vh?mDOJmwt>RQit;g$ zdWCb4&${;O@2R0!x?;43O0L3cSxqM%E(A^b8+FI}-xkH2_Oeh&fcs0}XUyy*+Z!3E zI#}n})Pf-;L>%^AY9O?GAuHILnD8+sWBbMoz4M z@?HGC;`L}=Pvc~1K#>^uagc$SzRR#)i$T+Dj)rrxGD5W2_%_4G;cJI#!jGabSV=RXcfJ`UzR z&pgCDnObRTPd`rZFPLGBEG6qpj@b^dR6%vCd~t*jUp7GzQBl&l89PT>=EK)O-cof~ z>mthdb8;n9OEgeL?0<1LG03A*PX4Ip*EROVz~rYkis5~g3%3LvPzv?<>5zqwnHm1U zy3X58yA}26BCCMkNokVg;s~pAMIaN3mMvv}#hWM!T;3bgB54|>05rC_O)D5+p0UeW z|L+eUer^J2d&tR}ao1na&jsI2<`=8$hhfUYrmFd*%rv#)A;gkVicgat>XQXez)P7- za2?TP>LJk0bZ*YTk5m}yaz6TSmlj+KSQ71i5;=Q;o(UrKROKReAIkGxd6@cl4F~_V z;C`6RppC0ry)i9_8XX8_I+LRnc3L}@(}4bh_CH6i=iSBND&hPtc16^zz-nqsJyBMU zOqXJKx1#nbf!T9&qF44uA4XW?CQorOe+z^f;Es`pux7LJ8148`-9UQc%a;3+Ws}SJ zn8ET&oIBv4)pzz%;5e5=DQNpfyfFQF`Jq?m(hqn39P@JjZ3T3J8i#g)-14;8^d_qx zz>8=NHdE8$Ykk86`UP~Mq{c@Xi2Fk{Q&u6=SJ|BdTil3O#P|mIAmj z=-V9O)_V+Vrpmt{RIAM_o-C+OKbveC*EvvFM%yeQ3bttVsURqsomG!J3fiacFHe|; zB7!fADJwWlo`NF#PRcr1<~M?I zueh^s?Cxqb%~76xfwK1IrGr?&v?9xP858lE5l89gJ0Bum4j#<5PCP}*Wl0S@V+ zaasOniBYF!$R`b`Ekva)k+e0wTN&QLVGB<27AaQ2jkY!7f6Mx{@ssg5xz1*!B<`h* z73u)@ErO${am#X*(E{R&ZJiAKzo4j6%M!z1D4!_y2R(+J8&^514&4<^Uk-D9$>n!5 zSOnFvN*6Md{S9Y#8NUAhhD4$By=g#<6@~A1P~nfd5sGx)Q6Uj?d?DYJ6a(1CXh;K& zw)DbG)zC)vp~%WX_q+(!J0EVarkz(o@0i_?vT>A+&9=gz7A5(;c_w|K-6o)RPl1L7 zMMvm$>3_vtyy&@1@#iMhkNJO_FmRJBkLJ-`mRT1}Q3F%RlM{D_+K22*93E{iq!Cv| zHi?Ty+C(1ya*rDPZDI0iT98D<`c~w=ytFj*4=1zC$)%aDmwbUwSDWbr8ugw~+%tbc z8}FxY-Q$T0Wu8-rNknHbwHY-|tpCUf6hY_pkGOrCs?Q5(Cnia0pCDyoU>&Q!Yt5gt z7H3Ge^b3s_h7pKkq;|p&Em&y$DdJ$e@o37m&;+!YfELuZgT#g3oP!&dFYy9v+PLw# z5}k%TtuME4UiR2NTUTLb@?9bOwWu~~rk$Z`t7$$aiDBS}y&y3BGjvC$n@o#figV-r zMTiCJJIc)z*mA?u*lGt-42)GN>Rc9hEf1#I>}l!mCNi@;=BBZDHfk$v8iVsu>!NV2 zOe*U8y3S}5lu6B>5`P%W;^7~1)Csom^HoiHZrb%KufG?*e9ZixdDCt%;3y-^dlJ@c zz?M^w4Y*Q#JGmV_`!ZX5irs+C!qDbP`!iJ`3dUPrAuxe&S*BgNaKfSD&8FLkwmI{J_;G z%FJWw1WL1=P#f!R|0pkaQLw`Xp z*faQ4&Q_UO^I6|!VGwsA^I7XM=`zmy9W3v`3`@Mr&b8RhYVdzHaUP# z@O6C!Kd=smoh7cg3UFM9m5G8l9$p2%uEAsX(C%9vjs&E-#D#@lOE)L5xM zAz0wrG6QPg`fs`l)E3!Mj_r{;nd3-MeE%2JJug8yAQd_tEL)Bj{W!nDT{BmXD~B^8 zO0QE~)Fk8%#;|Lm)}L^@>D7taEtKKydS{rek5chGKJ;Oi>1lxU|7ycv(_Zy7O=VXR zvu_bkCDO*f;ulm2o^jR>Ewd3A)lbx%V(vCb3YG~j9#xrZ>5rUTmfEPOc3?nOej)Hb23|FQL5xi!sa;bD&LkdQS(P^aL^XF9=cPC`Q7T#ANhDjqBRlUo#KM<2EXbzWn`992fBu0C}Txua@AS(I)+Ch*g+xAo1nR z5eMoA%-{?9YGsk#Rabr!dV^BynTC3WF?(_2X74IfzFzKky`R`SmD|@!DB>x-rUL$O z_Ebvc&Zx*w2oyDAR_qA#$(PNVGt3=`Tb2K4Y%L0#?g$L6)EkG1fxjX#%W?uXs6LKH zmVha!4HT(!2A=S(DqQE6X6ykCq(92;$5P7SWwD&acN#^Yq~;UUdp9xpH%)SYq5)IN zSo?_<-=}GDUof;Y)Qr;KWdO)3zC@(y2_D8W$)1%I>_Vtx&OcWZR13u>k+{Se2lS^qR_sW`y z?WNAj3P8Phh(Ve{_oP?h6MGqGTEi2POKOSE$9mYuVRh1(QF6?cSh}+6 z9om>(VB(SpH*#wtrgCC9DbTi(`aOm{NbO|4@mz?fk=3zhUj*SR^zMoXJZh5dsv8GX znIz8zQe6(}3W>z$%pRu*DE%S<=}MKk9B0-p`ln^JX*u~QIoRlL9c~Evz#w1%0|mVJ z*wx@qOi+tL{$n&|W3^H;4l~f4JMq3QT@8{o@j`*c_i#z?1!#sM8o)Wngr7}`R1PqR z3KVYq2)M-l$)%@Wxo|sy(_o&<;Yai2c8SL~A@V&N`=wP@Gs^ig`N%upsDrD6ZJT`u zkE^1pz~leoWZ5zKRs)6X<|+5P*F0tScIrPSWynUH@n;cgkA`J8Tu!tW+awAC(d
UBpEXo~PH4iro>^=_%{Cc|nJ<;TGC{wSQLs#FR#W0HIya<@H0^WyW@C1L{9P%A*p zPQKHUK~}5e$%SEA5v+Ww${Fyd@PXiZWw`1tJ0o2?sy2vo(KM%hLF5>dyS!YBXjLhw z*_+bWD0BdNHs%`v5}p;E1$|VO@v|VrwP<}$ z0#x&cquxS{L_3pwcl2_YfKff|#`3F~t(zr{`2$&VG=^bx3nrc~s^(umnG+Y|wW&8_ z+x`yQ|q|L48p|NDW^t$T|f8lP1YlNx*- z!eW5~sJlExtfI@~S2IP8tqUar`5Wp?#3FG711ymTg)g$}!nNW~%$rtK+RYQN+IP+W!WmQm91qjOyF&!=6e?!y-n1yaN;f_o|}fQ8VV+|J&K zA5y<-efdNxhchlF2kQ?N#a4&&GYrX01E}sJ_(J6)4anvFuYK!-y~FT*8zJl(v|PtB z;U$NpEmOMV@#4Mav~VPb80G`Q>-a%XIPHt-*zZh|ZHDbSC>#U=myD#hytK_L@LR+H zZQT$_F`$SnrR95}5zXfS4N`prEk}%Xf4POc*UBLB;rMaxi>_YGp46JI>Fr4iy_$vf zOw72_e9H)}7wCw{XAp@z8wUOe6Tbgk@V&#Q73Zx9+YlIu2=QWnu%fkD23BiZ227ee zv-;}yYsP~%-?K8AdqkBLF)OGclDH!7?+LAIt8uj2N0kx|BvMK&@0vnKt-hru1G5Sg zPpmf8@{9<-wi&isWTlom(@?+aw+Wc(?e3X`ioRB&t3vb}6`ob#uE~VZSz~R0b>-;d z%#SMa_&7LkTN9n%bV`zRmF_BYcmvm^o}Ch#MjdD4GCWs@5Cg*Ygm?-Q?1wWb?$RMG zhiRE^z&^;la zDgkaJeUHDOJm6lsA3p{URi(S_;S3SDl8A=MdAk=|!e)?Pt8!79=R?LkybW_R_!?nU zgemcD9pto9|Ac}(zSY#FEIPsA@?oGvHP;;-<7p zwHbp%2Pb*f6hjs`kbTQo^s@w%H-wq0A|{@p+rTH*Z^ju7vkW>Z4S|JmRu-)#YHN4IeSe}_C+=vKJ}4b`OU>fMsV9kRO+K30LYC@63%Ey8{1zpG z9(i!iY43Q2J$wBzM|krQ5vft{62`6^tBwkRnd>&t#jY4`51v(Z!W8qmWLI;gLfW0$K-AFgInXQuhFSJ2 zxee};)|14hT4dZhUGI=!OFgL#`8nh(3XBZh%w0hYQ+L%_xL)|PM=lK1k}rig0wZ~g z=;f@+Uyvef|J>243}J2(UWfgcM+hs)zC zDjBb+N74N;@}zp7{bCw#A8V#J0&9goR(<$-*j99Ebul>*8u6!OD(jxR%bs+l73o5} z-Z*ym$D*WqW6~7SXZW)D0|rjl%FdHw8VsBr4@~D_H~2UhuSoFsry-Lw-9k6?aZF%%)bTrXEF4QC!l1lS%Hg|hmv{Y~{TtO5GGHC;CP zXx9jMCp?N(;+E@>TleJyC|GY$|J#EP-$5wRD2EnUU@Zn)yRWfaDx;7*`&zN!7sF&B z1BiCmy>oLf(8PeE6TvH%xR~{;#;xN>d$(ZWTv2&Lq9gmPdGWm33bGFD#ikePZWH`G zoqtt|@$1&K$ki>WKJ(A#pLxF9M+lfrz#6!%Il|%Tk>QFoBA*910bYLiZ)R_`w?2vp zge>@fv%sFCi-)c*13Y+QZ&4I{lF+wB}cDP8{f6=@E4$ z)q!4~QZeXa)iZS>@~!*I-Y!u#?qCWE8a`N@w0DC1!yN6`ebm)KL~N?JHIwow*p-@x z^s~Pkhw1ufBjmyP$v^Pw-hkg0n~E{f^gf8A+wzY3%g49-UF$zsSMRpd2m;B~8^~1H z+53rpq$k#F4dcg=4cTZUool_RMSd9%tSxl$q)xovodM0(AaNP~dg&d{1r%L%S4@UE zLP)&hPYB&)5&cDrX-!qE72J%mHsMrKhP1jalP%OurOXV{+CnnWA~NEDA0X&IOHbYr zMdt_kni-vt6|4@aDr5d6sVsb*{zd~HrK$$C(u}B$Y;P+)9DPY&sxMCUPcE&P3n2s_Ge`vtrb`ImFWn+xVx=?Z2R}A_!VbnJ?nnndnf8~j}m}BDf z27$ zdF!6y^!&ucODbang~I$6A0Te0S&3Xq@-|@swy-M=@ah7hJV~aDy<#`Wd7w}S3;qb* zCds6m%b8$>y}_+u@kfjcv52!WLW%K{w2X4%lb(aA`91VJ5>`b5f(2Ci2I>#Q-3rV2 zUzgwC$nX27=j&~Wd(s8eLpU@h+G)tE)b>x%*jL&=l@KE{kbd(*xB2HMH#FT<(-VU% zM6cR(#xD^w;B)A_fQ~VLe7+@*T?$Tf23NVF1?44Ux;pr+O{30Uea1f2nj-tt^X~~D z*z9#*^$N3zF2=_!cTju&33YF*1275OX-s6oUX7nx6Mc^1@M!vFRw;JE;9x9|&GPFz zbIGUYB3a}c>OfBeqZ~eDz0_J%lg25bc&2j!?u2UzniU2)t>18uExmLVn{ImOAbJ$! z@Nm)Cv#(GX=%wkYqZ?TP5lCTIC>^|E#8QP(EVRnf)UD|rpGI_d^1$C*SJo$?<_^Nn z>IXLfpH5uB!jpWwxQqME70+#w!e2(G=s1cZqhRXcjiz5`ZP`-LlST z&&FLm7W&w$x$T$a>V;Bh(yHHkHOIs8NX zu;rm511KKGxg6!w$to)(3Wa)^D->uy2q}g=T7;9IR3+ShqMPvvjWKI0pmX)*0_F0C zOTmfVNCyA!J|Kk%<=$KKypjg@nkS)>ion!jbU$Mt!Z1=tEiMdR5DosV!hLnXUt%UW z@p6u5Hi$aj-7k(T1V>FAU^Q?SIM;6Ey?CSWU+*;tf4mHHc+oYP=EUrO&DXr*sAdA6 z%Y2J*|3z(e|Nn{FgAxOqZq7&uUK@fYiUQSdt*qF|SX2$WkWN@TQZn~OEv_EHt73!z z*1}`v#ev4`98xX?w%jaHB71z$ef(B#axVD`)nf9;*9XQRC$QL5ie9z zaLCm`VEq~p#XXOFGsK`(Vhfyr6b;FmxJmUzli#B%r9;4G)5E)`vW)(c+to~4D!N*O zX1wH~sOvWOFh-haG*LLs@21bBhmo(CZZ&{wLnWbyQ0!9 z-S!SJFCCfnoZuYPKAnj+%$EdFW;lCq9>2&K91N)DUDTS=014tZ`J45^k6{tl#Rt_H z9+3iul+iTKl|$4n4uz30%zyY5>_RDRAg#`xkKfLF#S5=7`TmFpWwT)@Z&_h`(B)1x zU<5j2w!5j$%G*j;E-3RR<~JESb2^MZM6LvlQ1%5WFC&~G@Cn1k$6c8?pO)Wvb9{>u zr&uHv7OlXnRPIE&gh}Rv`3urF3XB_yPFJz@^7BHgq0VY?M1|6nI?K%A#~=GOG%6pg zwZ6iRG&CdnhXXa6=JbC34ef*IX#;Vkjo6RifW(QvAgh_D0Y?!Gch`}ln5AAmp;FaI<{ zZ72{F%=^e=n*N{J-5mMQmOU?TLx% zfvyAz(kC(c4ZlktlgiLSl3{*p~u7)CUKdVZqDOFBqITvSenvJ1qg%J0#ZLMJT_L zACuUk4@y}~mh`?$#YgI;vxUNzNikU!iJH$$LghdFthWcPNhx)~^Z2Sac8xYX;!HiG1jXGt=; zollFYpPs{M8!$+rxZFsrqde+X;u3r3%Z}<*iRMhON(!hinBBLup5{jG&H?p!U>%ZM&)|Wuzs5dpZAfTn8HK1miG8&adH`Pvw1yPfznjw*ogzg`UtQ+Zr#BB+y(ob^l?|PUK1Jx5Gtxw2je9lzvV#zDT=SB1X zEa(2aN`cyK`W;+O@ika}Kw%LZpIK^iabB9ji^DC-FKuN*@u@5|cvM^^mZ{?KrRkb8 z2_c<2k5=N^tafOFHD=X9A4XS{Aw+8!y(`dc}BGBUmZr+#M_YtgYAEjRJ!!{Pa&1l+R~hU}5Yb7})JN zxms^6iz&IQK-`_uhVEF*kZg&*!a$)?xNgY5unKO?fZYTvWagd8V%XOf7hiQJg_{PJ z#a~*!mvHEnbiFSN(M;L4W8sCsB;q$c7)V^pH6m;(ZalsX`@CcLxPSN`8(13ZWF6#0 zJJ79Jz!6GNEC*-z%L0#M_hnWa%C|`c0Wl5Dy$S;TNHY4AD~=hnNx)e&Oh=|9TSvZ5 zufkNAWHXLaEje7zprV2i^{h2zRierg9nMvZ^WD6lF}=6e&BrT$$F?->T|-vKH6x_0 zD-k8G?Se@Qt>(_zi8UCbK1~w7P@RM-IRWbDN`(mojL?!V;PoB09`~Pt!L>j#1`AzG z^aGjp6JY25o&3G{5yD|VAny3*R|`D=9EV2)iUP&qlZChnQz=@zW;T5dnz_d_8zbo~ zTvv`;WwMUkD^udvdbUrfN~XTj752{KW1W7c1bynkJnu6;nX|axsK&Y{%tb}e%H-?* z!=BnY@nD4@{-5j|@xMe}-{+IL1AwdA;mE@hX>F7rzmsS*i=AYq6G%fS>MH>|tR1V@$p z!OBz|;`;NiC3lYZGDTTAu$WQKp~FuTE#G2i@-G4(*rZ-GHL6W)CLQu$C3HEfTQuC0 zsj2D9cTE~|$DwE=Yh&y|(vuw=;}%r#xe&!2fLE268qGeC%HFTf)8o-mezXZ8?E=&BpNMfAct7nPWt_L_RA zxvV{fd=i!~yvWH0laKm<0Ir0L3rb5P4LYE~r*iTMMfQ%X_6AqD zrz;#2KeV>{NG?^4=mEI|jaToTuqw-ziLdo}ffnhEB|r653Q4=D;ja!GB8qUEi zI8S;yfdxPYqy&`YPGBhN2Hzg%0d-e$YRHNeSdi8FI<`^iXR@M5sMW1JxJu^hJgK2( ziAG*M1oO)o|94voakp9q3K*a&M{{8=E3RdoLL%(#*CLMdp>~5!YB&IX!t(mg!Wr$a~PqhqVS8Xk^8wAziXB)W2`IU$FA0?ddg{cd`{oX5`yO){obGR2BVkV zS!S1!jxeCqa~GCwGXK&|u6#Il(t-IyS@P=`Z#3ZZ=)49Veo8rlCdNU2ITG9{*2m(J zJXWTMocgSV)=e*5)U%B5G6rH=uT7!Hj+KxhgE|dTU9$muZ z7uX+&(5hq{{f@W(f}GN?rqR5m&a&60LwHIGvw5#4TVC>WpRhR^(b~Umyx)+z@(1`2 zv|qWU3@NyN)f6gQYu4@iYpKbXsY^1rN8*%n$0-#yW+&t;UnO{@p<2Qs#9+Ych-#B( zZL)M=*BP<$DdpsYez#_umzrSE8txYY6ASxEPR}w0T@X z;0uiV!0#Wb(nv`(D)#dFYO$0d41;hCaol6t2WzBZ@Y-+%pTqTu|phu1Rve!#(%s0%L z2=~}339hjDGKOT&=weS$fRD>QwhpC^wP(mc=Y$H(WRZe@Wfifab%%eg0|OWE3i`L^fL(d;W5gISQq69lMr8aT3E*;X`KwE!sR$PbD9q0!ZX$1kZ@Y5LMoM85e;O68FGMLwz;>95$4- z{Ir_9!!BEq|6%q0D7C!Lk(~cG_3-S^{dgSw zYF!fs(B?RNctrp0g<2Me#UL}|{!pCOvKZO@&)xOmb(QP5{N;d!G=l!zlo3@P{Is}= zJ^O2{wRr?dUMnb3`X_$fAv}dTB#krnp2TVA>t|rtG8M=5{a-g30u}b0<1kikrZk$h z%srINg!PpNyz(u4Vio+|Q;LfTSjw#A7?7Y6?0r8ENTdZ))WxeO>*#5d9E73wyZ&mQ zNH%StQ}G*mhep|ABx%FY`hJTZveoYi>e@J zsQGA~=#3duS4D+kYCV=rrjlx2wi1RcPh%ww#u^bd@WMoD%ybI;i5ao)8?g(utk&~i z*Y=_SdhI)*X)!%R{aL?v@frS@`@)k@0lxv{->D~i$t+%GJN1^>r|9VFai%Jm;PA87 zjxeU`Pd8mc#`%`WwKS7^`PCb7ZTz!BF>$M(B-aVjLl5_abbL1Lms$2o~Tz_PmyUT0sK6dzFb6^@{?L#A3v@I4YodI#@H+{ zj0q`LmB|$o%abtqHUF9+eqhcug|d+fp2ol|Ce!gb_!qya*1r_nDt#%oGFfw^qMelR zh-SASN=c*UZKzc@1s=oIferRsopjYz#UiJEV)x_I?)Ds)`X>n8p97r=0}<)mq|ePO z5G3rzHyy$9&Hz0x#)4u=YmO}U;4M(}2K}RR@a_J9U@r%Av4p9jh)O@Vu&Kttpjz6= zT3V_C0~bXh+eBtrAnIcb^iT}2vO>c+QRt<(7=8%enD{xb*lQF3v?@Kfus24J*a3PM z8a9xE0Jt#M2wggt|Njmg(UM^&w8mtu1mVDYcB#}npnDb&FaR70_S$R)+f#$~oK62f z#VxTKN;3m}woS=>xri-gQ1$tZWKd=uiPfByl&iSY`M$g;@s3-I12vo099x$$u!-1Z zF~~?c@EY%De>LTQfa_m?@DRy2zRJ71l-s!5$jTx@S{VYtEtnwpsmqyM^fxT7HL{4) zg~kc=P88A0@B;Z`qe@8}Ym?O9I%*AJ)?i(0-PTzjl6OAx_CDi7UcyCuFCPeX7yN&iFc>n3jn1qfR;4@vY!gH_gZPYD|782W zpskl4Hy4r=n&CzR7Pc`B+Hp&Bg}v$7lnyA@12x1$vNg$b@9aeFf*0|`DP=Fw0lwVcb!^qTl`5g4ik>d zg#)^(1s*77@Y{gDo?zm5v^VpFg78USmXOTJ`wvk!&gbf<)vXb^l|95>OEsEDEs7Jb zKd76l{(|<*z$z>6=62;?@SQDBsC9@V5#H^-rk$7R{s}u3OBYvlrhpvC4Fn$Xp&Pr_!toe31|hEvw)5`;2)=_7-b?xlJk(`%t}X8ygO9NIo-uD! zESrgoi!4ldX#3tFbw+1K0uRs~>G*o8qnHY8m1@RVKaa;>%PN`5u8riVUz4jls4IQk+Yp%K~`t8W}26>5(RsaKw##m9d#8}7u zoTQ;>Zdf>48k;WlqoqysKJMW~Fft9f8aH60a+p$D)xf9h>Ym6grYW?pP#O~E3)h_T&D+*%s3%0g!b z`yZc^qg*yYbMw5p7O+7Bz9%QwDzk$| z^NwF(aJ_)CI61d}Ua0WG2SpWd15~^Iq4G7idjAoQBaC=`B-jJCfA2bTI-EmgTTuGS zffw<^4=&+0?Co7t$M#e)(SMi5m=aIOl*gz*7`p$b>^8Ids2+XF)Ssr@qGgTfK8lEc zkQkZ?hr?S0vY%|nsa$NI@MPmWmFwlEDMDs|aG^un$p)H1j4x&!xx0)r9^jeWu@AZb zwCyU+jaPmQDPdQ_Qr2ps-b;$-2GRoZv*Mo++y@85tWbmhKlQmR59QuQqJCNZZ^@Ct zv=vb%*+Y=n$JY-LzjF@at0FHZ2R&ui@vMJUR)T+3Rt@lz`CC6C3eJ%P&tYxhHuLsX zvkm(I8tZ%uD1en+^Squ0d&e&Wl&|D>$hQEUelxo7U`LfpgZ>NBAQ8W`Fvd}$$7J{P zv%HU>>})C11~vJbjXe#%ncHz96tusCy**Lcr}{m7Xf|!KTcf@x!TfTAi~g$A14AMh zw}NoDroGDhf&wj6cNE*74{8Pr7(IwHti$qhm90{;+^lK-JL>)7TT#HsZU57~a;}f5 z=8>k4GfvC8&)>U6XCI;rfr&(gE?h$nl(_(?(SgauYf>^S=6j^?Zdg|@21<@&`c_W* zEtiD6wAQ1{x`~88yVx>JcHWNr{6ZQi&5IJzM9nuFP-C*5V&BnX&fjU9rFb6$(71rv z1T2ut#{Ye`pI+C(#`i>gjUP^MugjIm6B9GxBg5ApnWAZvSc1>8Gt?$i-MqL00R?$7 zUj;TeJn;1iob(-TE}J+kc5D}j=omadqcB01zH-A1Zo)r&H2mKkr2lEM{P$Q}jJ^M2 zh?{%g;R-oNXoucqSfo-#j|$4ftgpaHt0e&vBSU>-D&6;8w+x7@Ju7bl61d5sK93N7 zI!?IUquGIN1_~dO*zIwuQnkQe9MB(f(Cn7mFw5IbahB+>=;(gq?5lt)zPy2ua)Fu5(I`-Pi_ttu39Qcu zVOXi7vFSc~cMaTeBql(xpfKy4{sSA|?}PVgm$*dcm6@YD8kceY4(YmaLUYx=ESJO_ z{M$pHQO!e4&Av+pdU%PH9nf07+p`Us>WD5tUd_F)n?l82)p|>A-IT9ni?$&c<^IVb zpJksS?2w|)u%ZUV2WQhG3_BAiB*E#}&=H%Dd`druPi|Gk6@(eoX$xH)nXLG*&sb7Z zt7@Og!uwbNC2-gEZgN;Q#KXPcaT62aQ^`#S>trfM%MgqQ;7A>m&l1EYOu z7CwbVc3?Vlq?RSjg8*il>uT%|7az2N!9NU(ocsQG9(fx*y$o2F>~Jq^l(vt(NX(d5 zfrZ7^fJHP(0Hwj|V24XC2YLh2#20hDrVq5H00~y0VvY z2w1f?CgD#RKas_>F7MgdlmX?Ce7R zM3rU8%f_0QBB7B+-$2%QNEHYkM=9cfcnC1M6p8&vC_tkCDHYD&m2I$Rkd`XV)L92z z;H{goIPy)BWrn*VRU(#W%M<`NKFQv-H7lLFtm|5`XTyU$f&<|4 z16ooA=0Ys3g4U0@=KGry)tT5mAIP$Yom2Q%Me(W}4fl_ol4-bx+=mIJ`ePDK2-V`o z9-Ea4>=k82SlARDByz{brYNW<3J!`4R$688YsGG~>Cy&-ORYJxC{D)`N*E5X-G0|( zgKO#$2wt5$a$GeQ@#zFUx-}lJyf^$gN{!ulhLe4F*y-a5GcYGZqICK>qiA15dR*yj zC4T`Bha|OzW8`PPACHqq$1gI-_jU_{Ya48dxJt{#6VcyS8`&e&OH?pBt@`ClJX+g) zinu*NV)yuQhI%59{1%J&kx6Y72A2s)C zP3-gR^H#X|WXD?Rnw40laNE=To_aBjj%bp+6$pjiQq$loas*^P<7NMYrnU7SBK6qi zwbEv`8!7wuy{i$c>yut*25=LOscAKzgZsuFkCpj&)pW0!z{bg_)Z5u2Mni5mY7&9|7Q_LL8OfkpI%rP@Fqs%NbGc$9{ z%p5~L<$1q7b#`lis7j-mQd#5CJ+1D(x|{h6($CK}mN0h#mhE-rr!hAlxiT+lqHXg6 zuef`27Jnh~8&Ss3`}+ z8$mtKeN7#bbd3>C04FHzyfrjX(G>)CbZ7%a!MP_nB@AM`x%*GRlo&F?pxeW24KzZe z+(bqGeoS0i_otn0%;$MIA4V3lO=Mo9phOo4=HUWpd5J`j@*T+3?e{{cy}0@iLv}%E zb|HQD=OX?960Vm2t3SKq{|_e6CdPbcSbUd!`jrBD)Q08OUmrtbfsukBp&X)XsU32 zVaBl%3xW!HM)Nfs931JEmY|tVd#&39F-ctEKgKzDkdtN^^S7*nd*(i{|EAjpRbek* za>1PYz<8CF^)$gZ$=u?p5+wHoPrcEft#p=ioxw%LI@HNrTvS6tq=D0pQ!Tq;Yg%;V z^#;Oh;zn>JT%Ti!HQpNDsDZvI8(PshL_Ke_T9f~G**3m_;IEfFUY$u?atb@J-;$(- zU~fIzC--@+IHi?3x=xLT%ThH0aF@`#e?gukCZjmh($ZPY41a$XJl*xFgZ><#cRhmw zSBXpoCq3u6?KHWVqyFPdg#(vu4#=)s#=aNgvX_dz3j-tLgx!UDV#k`q?{;1YkH^K5 z-S0{R&TH#uzX%`FO$n$|^S1h5WX`q|OD$jH*Y2l^Ai*mzDc#VQG0u)6bFLsiW-TY$ zpTP0O=#P0~1IObJ`F;g~yky$?5?L{Ss%04cW!NC7gSvnrLY(i$)LnlZLc2^mz7R@8 z<-DE@5xt~<|GBPU7pk+~|MiLs_J8D?*CCh)pxjLSy25gd3}$ViPfBoqG_W%MHkLRk zs{k;8>_%jpc={JNB!pFxd_t55We(M6*Y!s!NI{u5O?MEWJ{hvUn+aqxo}t5892%OJ z*CK3;9+#8!Rz6e1;kdIoI$1rDZ^%4c;Ett12>T=dd2F=or2GE7Q^x(LZAs>*foA`} z(&of%AKdP3^erez=JOIDB66BDp+rZ{>+#^nbILWU3-+zoRUytlqELkL3@$e#U2-T9 zHwaaf$L0*>`5Y{h{hv4c6^od~%Ph616?)JBU;zGP{p48soX<}mo|0-hH#VoI;%~}W zVNP50tNWM#&pP;r4*cT)@Es>1_tQ$Kptk()*QMUVnUwVF*>dNECGIpn#e=q5Gm3(p zKQ0_o-27wnS{_Wvq;wE^|1ssZ=5X0yI0Eevb)Km|`>Q1^2@m3UFvPiXvMmQ|;BJx5 zI;o0w^uAqA8+AuTSIk;=F5iwDEV|;HD{(mPFU}%|mewT}`XzvQ&vZLzG!#vq-3v70)_$mj1GG#$rlHP+mZG$b1J_h>z6mcUp)Mv!^l+u=aK|ZWW6r<{X-( zxYwA^{c4^lQ3a@NB#Q<}Xx(?+o|d{uP|~hKSTnLpAw^W#!MgeCqI9&?WH+@$2wT zn)_!X2_uK1+7is{X7SP&m5UEnoWqk?>pzL?iW%lC;pwKqj&n+}Yz3dXH%YbzoVNJN z7sV76!AczMs>6jKQLCK8%)$Cm>wlIDvEd=%Ld zTt&MUU#bY*e)Tn``<63ZTEza$cY0}Nx!JXmmjFCkhrd~0ua6oh3b77nB zPhsmoCc|ctp-Mx#ZwZk}aZ)t9D-sbK4foQhs?)z4UMrJQK%gO5!$L*X25aCm3`-QmJZR*YGGxoli14>@pCEMglV-^I~NU zEEK1A)-0PUMxu;BgG(k*O@@pVQqPr!u(5}rPTbH?yXvxI*rxvFPgriQNmhxBBh^<< zQ`@L_JR55v9y^=fJ0|KPBKPO0@q=pVwjC3@6*_+MmCnM2!!HBvHKW{gyVm1gpBN)4 zp`L2D+#dayIXuiX9ZkKENUB`B({VuVIeMi-Bh``YEjI`6NWVNQqxjt%_g;OTEehJ+ z`AvegO$$xH78LQ*MC@b}r=YNibh|(*Q|jUp!c-;3F%n7ya(n7nBK~lYr5^u7(B3B2 zovRdrVL2yqKE(xN*&X%X+rLH^sKKXdrAqd=AgGOL%W!|+QYf6&gDK?YME_Lfv;G8ECmEpsQ(U(kHZ}?z zNdK9&;Fr@@y$;xX4yQ7 z-}oJ>%$lVK(yFK|ZDphOq?0W~lC&VrRa&TCNBxyu!6ZsyyJS|)xnV2V9wLf{8B!dT z2YXWUpJ*YzJsyR~n6iR`VyK0ala^R8(n!)j4}*gpGKY>32BGWE8w6X7mYr`k1jog- zHwnLtbrucIL82_5FJ>9SQthZ5k0R;nKUE1(vq~Vue-h#85e2ugA@=Dhw&`xkYzJNY zwa7o4AN;pK%KxVoK@RQ47H4#qZh>3NAGB=5ss!8LJi*T2IacAX%cPl{Px1182s;#Y zeEAGJ-`Gh->F=nvnf8p)MWYo~0h)&C%QupYvWE#o_S~#Y5Og>s$l~jMCOuTP;PST) z;onpQ51rru{=lCqQ_3*eSsIa}O&4Y1R$jui;YT7xRDse7B^-?{6NS=-&4eUXV0os~ zAE(y;f;?vCFGPn6?p*C7VejHvWt(zY_*dA&OgaCgWYR_&C`kNjK5^>NPN&wk{wc04 zoW4&qS`g*>X8TZJVmnoYx1owH}v={F91?ZE`TdNE=Sm~t(?~rz0miNdv^ zK2DnKw=Qn{*g|^}GP)$q;7K=2`aclj$zE?~?tt422>HHqPODws5aD3C8K2;33h(gZ zm{0_!s6wi)x22UtZc0eSrCKFIyoDG%J8u)com=3qJ(EiYtb z6AM(Cu%x0Gn0Lt{Ww<1XPtA4-=&u>e>sr%=H%AWu_HdD`WTK%hY9BYaAgn54_d%^U zbttj#8Mp6xUKEQ!DF+e_Fcs*@c8uw#_w%#GYUXuLKuK z5C~7C)-gkT4qxMM)Jz?v^%sJTg#h^TH^L=)8Q*zFw3)&vZ>Z@AGAW`$IQSzRw~ zU1&&r>Q1OM@cL}@PoYVNi95lrTirSDMb_(-=auhm*DS$G)*epw2&UTVml00tg-Ff1 z`f%St@hjskoEC&Cck@|vWL?OZ-Mjpx(KOXVM#S0z60_0QW~kZxupe_1MY=2FEF9iI zsIk$DK4vU|7nhTzfK(|!L>p@&DX;=zj;k@4k5vf#s;$G#y@-g8<6KV zyt5hH1_tMCWjsd&zKvj3FLL4!i&YtVjAnbv8&u*%Sb|R4C>4(8UTpv~Ya`()en+$j8jY)q5jlFe3Q^Gwb>7)kHr&|0$l%@mJo=SuRJ+_|+XHgz1 zp{kJT7(+j}0BwESr*$XD5*=}K?-79S4fjrS#)KBz8LDsjljT8;8lTkg9nH+NbLrAG zRGSh(0R+-5+e479YZM<<9B9BH?G4S_JDphKeY5MZJ35kng2@1e_u< z_PJtQDh-2xn#onN@^YY1VgtSe>~{P>NHRFCuus*FW3vq?5E4UV=4-DPBkidEXC`yl z+frqE<(Axv5$TO#jd5l$hq^y?E|yptg+#~3zW{X5<>dRo>s+e^?=C|PF@Va{HgG4rQx$miOFeS2{ZkeykgZ(ZS4+!IQVNVLZ8E$ zoj8lTgF&sH5}igY<2^KpY3)GG$xltHDBcg3k`ADtX|+n=x$J=9stbWE3&XTv)Th8F z;JI$W!8uqB?_Nls{cU$EtbWl@R}88c6X31zf*3WEl$C3h8pOCeDHM8_ndb|ZV?=O} zfz$u(nj!i8*~lGNOZzfR{nI#?T(d^@eCn@EuN)x|XD;p52kQqQHC-Xl9{wVU1}G z_FSdGQ5%ttTB?xpegcQC3CSe?Vd$WG4cq<-4XiddpAWIbBtQWqCX`Goem#okq)JWF zqY-0*|IMYGhKQZ>S-{s!MrPr)3#nAPNJ48q$ zM@poLs)oaMMS^=_d$hYZWQW#6ds{!?_@6EXmSe*!w^H(!aZ9RV^zKpTB!`zVtejuJ zkK^(@eg#gHn|#%ZP5)_bs}gp{C75U)zH-Mxm{_B78lkn8k@jt!|1z1<*xJh=>_U$+ zP_NBazZLKNH(x`1qwad0t@jffo(uA~0Y>@fveEF?W%0gIL;11_ieQ)k$fl1N+f29+ z_7@GoKvp1Et_?`Ok#8?{tZl3*e7i++WqUSyIkG{$s1l8oJ>-B0c>xN}%OF`Z04ag_ z*oN?J=_p!=JxyjIt6&$;33M7VokkC1gE1v~64UQm8Nn>!%6KWbNshiGxbAd^!>Q3Q zY*^OTsi?Q8D9hM*ftu8k%h~p3?S=ND60eswkgD1qk!m9&O=UnS+l`9O^J_Kc+4zNk zQ$u%|lHnV0#B^`i4XwMD{2_m@%rjf>qzTUiT!S5S4O>Sr$bM4dGL##vUQo8!y3r!H zuRI}t9&f?$oFk?M66PrKmko#T=cg{o%%Nyv@fd~QAsH`qNVj7dOK>Z$6(+rs%If{S z^JIy|Merly6bY0}sf$~0?P|>2Rzjo`Nhu+@pCdHku`N6yaiu7jzHo6&$ls%3+nqjk zy#vx!pH*92jr|sIq?J^G#wLrpivSfNsxYU!RXx&>jx!Hp0{iQ?oU8qG$z8Y2%-ML9FR#gp~& zO~|KOYCu0#2>=Z=^#JIu65KdI$TG;6uiWC<`OGiNUW!*L%4IxjcNwoz(I-E5C+Roifo zT8kOmG1ybN%nX#ke z0FDdrX5v1k3iC&?T4%b*f)|6^G@IcK94$GEjW{I^2}(JMAWlbobEj0_V*S4m&ylgj z3z@UO5rKi~YxskwQ#WC9xp+p0>Y-Z2dSe8j($RSqeLM%Y`xlE*j)}G)%;2~ZEJwG} z9zJvt0%Lp&uDZ_2W-(&CmHW`HH&MCfDur*kBMboLVGrhCH;h{5dWuz2$=Mk?(r~g_ z4n?Jxgb4;*Xk0iEU59Ywgu77g?w#-8HluPzn0)8>IX)Y-&t>JqBvzbX%TCIX6r9+Z z%8pd&Y>|p-Hq{CGAQ7|odyP>7%LEHs|3Yw|g4f%BAmiwNAcLHN$0p3ex7;f9aX=e6 zWLE(%*Vr$K#P$Rp4YlM!?f6|PJ}oyTUk#0wZY|eFt_)ngLtz6Y#nZBbIA*7v<2MJ~ zN=m7jNAE{uO{Qa`-sRHO|l#%TkIzpB_}Z_mJU^$`H)looSbG zcqbJqbuA*4hia!fm}|tuWTJwj8u#EX%ljQ0zoulqtP!Z7qu@gupqN3g9p63pjtlpB z{yb3_Ow0T&2>DXb&?fNyqkj52C;bn=!HmM5$p3BTKf@4s+UY;Tkm}PdnC%3&LI1R# zf!zmNj4d+%i}O`0#I(S)(*r{mwHviY&fg8vsnB;JSlwmIaq z?C7I-q>g5TtTx>u%_XX2=-yi|bkinclX6R6&9({KzbYa7_sxd9%v1$Y+!KYy32irb zv_g;IYp(-bAo!1x+D4*oWR#R_5+q~3@0Yodvqy1CR(yXV4}-cEqlsUtDT1EDY}E&1 z!dwz@HK8=Y7Q#TU1{@Wj-9Gdjjr`7HURxI6@b%HueR);KzHsWogWTP&!*i1xM4`Jh zkMSeF^O@+&21}sRYqiZ*de^fv#upZ=v+1}@jlriEhIMikW4zStBY+LrN9=@T>m1dU zdt3id2bxB6N-6NDF9|yIok>pYhMl+`0XSNF%66%qo#g~~9qBo9xJ@NXwK-z-0KaWr z1$hPrqg4`WGSep>Y=>xQ%>OXgalfgwJAApbuld{}@Y>SG(%5P3)Gt*t*2xF%j63)H zHu~ln!DbI+bRSVwlS=|%2*k%pJ>Rol{X@IJXfMOT9ggPpPdvGo?m|e znv6~%<_0{Y>J6yOQVnUX{ zVoL{zT0kQDu5w4p&{Q%z+EQFeaT`_eVX~pC59z3RFTSye%2>Q7l{Rw(@#b3K^g!fD z@a{`P6#Ts#8gE^g!?~}m`5OJsi#AzVpHY6?IqCV&A2ZWdoTj9Mby;P*ThP_oBgt3U z8k;v*+^~#A#ifvzob7o^ly}V^)Vj!C?=^_7RntgB&;pGShfQ%FKKy|WBKcIYJbSk7 zyI-+5c2uyOUkY~f$ZmEpUI8dMM%~X~9@EW?1d@#U14t||=yz@NaXA59!V1sX;W116 zaI5s2AH$IpyV;MAV7R6p95fDNDN1eM0rn{u8uQ(ZskQ2}bGJMaf`33W*;|W7w-@Fl zxerThNPVw1s8eKbxZPE|`JM_Gr9oK3hLmY~MkLPxyPKx6tMF(@9B`up?VeG@c~1v^ zZm-prSP@>&HiA5-=q?*3jM-DP`ggyFl+{O6aoFFPDg1@N%$Kp@RvGs%Fh0W$v(={4pTcg zO^q%{Qjpp3qwmn!8IUKgZk@IyPD0GJirsB->}*ZmX9zHXh!ajAf!7zCJnJR?@Ex3vifUP&)Sxd#aU(Ra(-@a0Ioa z+u@t0Oi!*`S)QqVn24Cq4zWo`i9#l!a3W)f2HN~!3qRuY04uaw%DMPlEC(*0^BEP# z_*DJVjG*TWHrIqWo7)HH%g4Q_{i;Pb`g-X!UD7T^sY*dEtjj% zr?7>>IT1fXOrb+q#G%|fs#7*E06Sd<*h^<^rzORfY_<0K$Cg`aSQa>2I|PT7UM91U zuSDDYLWQttJFen7O7Lm$QA*x-K5@==tRubAQBDLw_A!D$ZE!FaReG1VaJ-IZJxk(y zq8?FcuCQjP_aU$OVR@DnfR*?nAGBG`Dwo)Qo3OCQWcDk~A;^|7GLn)D0xE}JU}F;v zp)NmlAePLc0F9lQv+2&QX|$G7hS;d$$1D|fii-@OK?;#mAyWoRQf-W#1*xBI$Lof_ z0Cf+K?HFsB_Q4g4Rsi&QEgOQeWG2&}AI8;v*lX{FLFd7n9zYx_H@fF|t`pgOvh?Ql z7;-jS_!SnGEMifnXd>S{6$tdOZrYIzojR{!%Tu`M{a)`B_<4NyjgpgQl21{)%#NK^ zFKPb}vhlIiH4H%zUn4Bh|He5P-Bn+tJ^~a~(YrXtkq{o}Ra8}+>5*^s%V!@h*`m5m zSkb1Oh^Ab6q+$XzM4rHoUQRWj=Xyusv~d|T$|_l+{QHmzz^Xpmlq^wHG|`aUx~RxW^1d$(PJXOqqu)6_JKtGhc3U>h{#Pfp z?+nSE_6GM5U5mbYB+c3Mq?~LKB2+WPmK)>{Pb6sCeLlT09!+xBtAAu?!+kjRj^kwY zF+!r1!$wP|S@Nh~Q(zIemcfqH&H$l5ZwYI;-m{7Kxh(lMM!u6>pgfvWf?Iz6*2pwC z(zZTBOU2IDOKPw10vbzpmo8Z-6X5D8%KnKqTW*k%QJqW9LT?n}lEU_U7}4w3@JOtF z`^$%JXH3z=Fwo}8K`>EY+fqf%*@INZvb?|8y5m|*`MkW+RfLNY0`UsA!E2~}f(}uL zHY-;a+ERSffN-EIht|mr+OXUHkw?n@gG#&+PsL#YT16?{rK%wDP)0T!+JTF|wQzio zz<^%HAzb+zf~FP|3P-y=^vBa9d5z=p%@KOVS7Ko4AKvQRfyoN4tVpLe>TzIR<}z*H z8Ez&%Ex8>@_z63q7e_nrCUKSr=*aa+ z6mu-6iHKPzD0GYgriqe?h-{Z{j5=Ywk39fq1l>Imr^u}TZSwxN>A#&d3of47@~#9I z&+sRZ^+6vRb^aOdc+rYI1<$jINQTwBMg);dM%5r}T62>yxqBb5>R=|okw?i}qg8>^ zEYIL(?I)>6cFREFTt%j>o{kIk>*nw>-;ohBAtxc8q2{c)g79TFCw&LV>#B)`M(Z>C z)&(AcJSZ-q8bLod%XivT&w2$w0}Zw{2G%7OG6-CR_`mrpB)LNKkB3y@WuT**^)ax2 zo|#N-kfycdgeytn>1GQRy*1EzX4TiP5s-3|&N>0k5mvu+C^T1YfkVRuq`DiUnLf*@M$MS2HggISuv0u? zASV5H3d6q;%5@v_Se^vp+BnR<2`u0QC(TO>|9g8;7+9Tfp;XJ9;(sNhi}u8XB|d`Q zKDZ$7w}PMhRHo|E-zLlIMeFKdoQZP6g68@1*_bR1?W#2Mcy`B98vqt$`w7wnv2ZX? zg)_9%8z2hQ&s5+frwy%O1Z@02!W<$SS{1}px5fxdPOvByXmp)H`#Cj$f&EuoHZxP@ zA67}y=2+A=pbaP{<_>XY^_DNMpL=<<`~7d~Cbu8*2p-MjZ`lXehiCqSs0tDkN)sE} zmo!)R@``*!jRNNr2%Ex)YJiYIwq}-1HmkUjmQY^mBz*m`$Rg8kCY~tX`74nH=6>7h?Y++3ymy3P%+TfruuJQNBN@or%WA zrug`{j&KW4@d$NmgV+zV-Ovzm(HA|#_-XDXVE-R7-5@#GI%wB3e|*7z3@;8$Pvu zi?IYu##w`v07#-5ccJN{7|2GKB5|y*PUJ} zBigY;tb#x5gB3%a3pb`=>w(Hy(qd|{S;CqwWA%Xd*e_TI0Z~+Jzcx{-U9!)OA_747 z+{J{@k4gsU$|D&G!k46rPV=~HwB~!2Ibs&)xhYS5wVy$Zsawg`loJPf(o6+5i#34jN~`QyxF2CV{6y&T+`L%_wMwm0kNdX6^TB!q#8DZz%iMZic7LX}BM>Od%T^*mX7)b~_Ml(tD7iutA!LWs!wHbeta#^LKW+t)OSzC>?bqEf48> z(-<+e&p``q_?cMn&0(@BP$AwA7B4clR#@L{PM z_V7$TQ=|>xqBoUe8NRs4^0HB8c$XDpmX??fl#zRicIGjRI-thtw1p<4-8lFWn|wgm zHvK7SlX)0+YD6c35>U52smk&=3!1_kS{YPpBg+-mtxA%n=Vtbu<4Mk(Mnfz}qc-4aUjwGPWaD&=NGNS6*&qv);HDw3;6t*^o)aey3kE8Q_Jsk{ED=)dvnGS2lVAzgli{{ zq0~dO3KQ+Bi@Roo*myN9^EQgoe1r);Bol1Fu>=YKEgq@?NT;?QNMGATHmh?5>5wGg zR=Pt^edn^|*$+2i4)?EgS>OgiUYBBcYI8lZ6AfOOx>ey5q<7Za4h@#~1-TlAtM6?| zJ~{3;Z#hNniQ%~S^M5*Q&fK%~;xl3@EnyaOtOesYsAj6Jf!7S~<}H z2r8m3+LR0OFtgWVo>S9);w)R-hr{zFO;n&)wAdD@2uR<05=+F4MVa>}J9+p;(Y-p| zVxDjlp~%WBagJ4llP7i~;!Bio?Sv_8Uq!hFp4iv0k*a zq9xlh4Y&8RvzFjJsGDV59GjU}E7b1ar(mbj+Zf6#8h-5s9XyvdVcK0tQ7i-0#fo$W z*ql^O1TsMr-^sMJH<~9d&hPIpV;aZ4m#BW}Pt%KM`Q06}!dC!n0!-dkti3je^|#E@ zS*Yw`!VhsNT@kSW^M6`p1+RUK#x_}$Y~IMryF2s5*qQ~iob776A#CznrI&Q?hEprC zaB;~Rw~xH^DyAitPnOFRPbs7(atmD}I@>syf-)&7>^`qw`Jz=gJbXWew#q88*||Ml z@!@FpurygVh7C*)qcwnR}RzkW&qCF-IRJb9kI^qW+Ez>PM0 zsu;vBl+W~FXpt(08scj+kojqA_;qiD+Cm{%OPY{EsyQ=X-l+UMV@!{MukYJ*83euM zBMF7m8oy_?S|_uDy__s46kop&yZ^v&sy`dyo*Q`>_l5}&Jvu(t>6*Ka;<($|!63ko zP5q>{rtbx@{E4lZZd4)N72+*yvwWLm!N7Iclz$QM859yx4zyq+qAcM}t4g0i+2FUf7g$Bwrnr!7;o*rf5PN{ENYF4`KIi*>+(2D>4~_ zMqbJVYJHJk?ZF3($M*_+$vDxMug&Wbp=W5M_Drw^2VI!OJC*!ovE_dLM#T`I}0ppeF zXN*vA$0)AwJs816iW#63=?b2C5l*rYCD4=R{CqYWChAadQz$Ve+7(TV$~G~r@$$pqJhUxA-x4Lft~cPTVVd3puE6nuttVXSGtEu#XdtPsBv zVnSwi?={cW4nzRBhbVY__cL7odpfpPTG%b8@$sx*Spkaiu*Y8r6m-i>CEH|LW?F{m za97lnb%oJYX>6$d1Z!@)R{z)kH?@0t?c2KuJNva!$sh7~tw9H&ut?Suln023>E4Lk z;}{9E#XsrnzIrth@E zX(E<-OmAncK;ab(pNT;`SX_`9OCLvvTZpB*Z!;Y=Ef%lK*yMO}89|{X3@x_|j4nn8 zsu3@eW*aB5WD_ic?Zbu* z#<{7|#vk@7of&}dyX>)=pY48jL1*^iHr0@GOJryhxBw}X=OW^4GijXkp?HYUhIqjb zh^U~no(&^K%ZHJ3+9IO3XhQ~Ly0?R`s-xir>nJH|O#%K$!;g97v^xYgUG8< zhPUb*+CBS6!!jdUq3Cn!YJAZmS3G6Rsbk{YeR9irxoOeN&{zsO)*ZRuM64yW^i5O{ zP2?}1kdGsx+OaKI>LV%qu{}n`6lagqlN&m*F zg89=ERK@yFBRDI*St(|4Gz2s?)Tn|CRdskFM|gU%pkqLECdPXMB#a?g!`+N~-g>PG zSNkX7&JIZa8DZpf`X(90s+{-RGAk>a&)0rhD7}oMqu@jg&0dLX3R;ImlE9+&gE22j;l2|@HXkB`}-tM;j z-LdMWWmxW4!o;AOP3B7x-O`@V20wqH?e~?NsZ5#pn8sj=ExJ){MM&<4 z)nchLvy#e%nw5)%#tpfU)FpFGh-eclx8O+PB{sl;I)^+ksy?m2ivM|Bzn4`=7vDfJNcBYG*lbfiqhLK?z`_j>~1w<}uPvLJzRMqNdMyQ8TyI8HxlqB}#@%=}D2fW&)N zd3DF?DrTCE_lk$e$TELU$m<);{(;_no!ucHG|1BjZn=8JKH|XtWBd5etBE1 zp@Du&k0y(Gcvo3-sv{G`O^V!aQ7-orH-^l?UBM%8gj3xame`I!Vq0 z7cA~nlE2Y3XWUE@G$f35?e!#K%&+cDx}Ro`p6a-)M(knSHjHO@*%TA*bBRUtLOWJV z-!pazNciUcDUWbKrVXymk-nAe>$wCuc)T>V{4p!vo7%`F9@ZF>*J4Jyv=W$LWu1WeumD%)6~n9k-;oqPr7& zigu{vRwP)M9p^oH1{ZAY7nQ}#X=Q3tQN-OgEvO=e69|mNrWV;LymUJ00S!kV%s6qI zZq5Pf@2vbEzo!g>zAYNg;a>%$t^xB->nu>7dgU$v-%t3~r%j&nz&nlRi2r|fD$fx^ zlF{!}y897;sVZ%hcM7Q*ycJL|^(0tgR1&_D(0TZku~WQ6NvMPBWOY77=!vf`e4KWWMP zA_bHkk<&Oa($*@fU&E5MP?wbmo>n}Dowmntbc3r*@ywT=vbJ-u+fMHU4}`0si-2ue zsF4KUWWmjfCXHzH=*x$73ciI}IhwAXJ`WRJpdu2i(u4}gNhin`!~ZiqiXtxDsq!s9 z$r}yJCMiv~Xr8&rx+x{OC=Mx%8$T8|O=WG_IUqU3Cglv27czsIV4;v;%>e)SDUbN` zGxJK2=%VQy`Q-z+#;kYIWDcxOCawX06TNWZM12bE_a?8zsrn{OcgUI#Ftb`H9#>SV zV>$)M*-#YQQc^%qwq##l6=ebHE30%CiarsGNA~!zM(58H6WW?OI%~azOpUFjIt*k& z8=*^Y7~(H9R-b%g6lUvJTa%Vkq=mW%zJ%A^+q6NBSs>fQeNFTG_#f13j6S_LviqHF z!n>aNJon50hS4aKHI!9COg|i8n&F7AlhD9KnVOIseL{(-3}YYuCmXOPQ2B{KP*o*t z+q0q28%la?ay2^$<<&^3y@26@Of)(5oD1BvKsno}wU2~QN3N3Czeu4lDzq_9r>3?D z0ivO$2po3DFVF!klO>UooKy0X`+iu7_%5T`o=djO;eS=VJtMChqW-7UI%miaEYxpE zuE>SU&{7a_ac#oqf>lxGZ6x+aMiIgH8GSTZwM6M3U3fn&wg2g*|DQdtLxw01S{%Wz zJ3!as(&_&l^TSfx-|Je8MZVV$Z`6pQ#)_l+ z@#5BqO)hYgraIlUB!N9>k zdZ4<)>ikbeyY_>5zx+<5^V5gf(`^8P*16+65jy`lQhtu2X%0jDK+6v(!13ZLR`U2X z+g`agyo^{S!pK%K8f3dc6f#;G6e~zDjTx;_)6C3BzIfKR6WocVcFJfG9`3r9X-KP* zfZ2|A*eoJrR5(lE8Y>!c>mmvYJ1bOs`sxS!^)%1|ZCSG7lI;Tls^8>#??Rkn={}D` z+#SQns@@g$6}hh)dp=u`#}o+y8n=mDH>oy(2$aX=5cuV6pwXic!->MIzf&n{1r6bDY>hH3i%! zZBYUvTg%|PZPqh_qkc=$uiRkpis^@cC}@=RY$GDjE^0o3N}vjf2GKbEvReDJLoV2R zoLe3SUF|nD8R`eOdWj-J=>UeBG48uLWYj}t=ixnCXAyT$N(4LU@{)smD&I#nZ6G@h#1Dt{L|Yjf{B*_oU-f3ex_v zU#KQX0hgLgG%_@n^3*b*WKdlDKRvOOyKMi3pc|V$SMqhuuUP3TUG{scpck$K52g64 zLq|E)HNLc?_28o+A*v}xdFbWv@<-CG?7@!i(nVGIs6=(M?S(To?z*a?Lt*AttHd@y z)Ix6Y=Et12&(X!w%U7E`(q)Nf6@RyMO}ZeH3Z37NZnPkT&I;pvt&bSZoZ4zBB8wV3G!R=^IA;*dL zZC{G=tZhWbmqJH{D}-A7%Mfw<})P2jDV_Br-= zM_aw_);C^n-@4XtOGd5Xu-Gx}1q-afsg{ZIx-UiZaP-(EjtNocS9B_>LYg-AJXf)N zsjOelB5x#&zbcgCqoB^s3@saY{4$TWSbEPnmVvCKZ+Ik2IU;tAinjN-w&oRJ_%&?_ zp6=rZVlxfu`Iua;vPa6_RZmS@oPLrlt7gX!Io@w5P^zZd(2|wW?d@7eu3MQT z7Zy4k?+_E>BdIz>HN=h|ba5x9i>zwJWqdbW)Ly1>M~BUKU-Z!=c-nHc@2*pDx^rxb zaxrlFjVfl}_{uRX+@vw%GEAxwFCr4SGA zt6H*9yk#0uDAyY}XPPL5NnmtndF5(Gdirfp4>p|42dwjGBFExwT-WuSc}+)WI%=x= zokU*OE!{oyOmr*!Ve+IB%DEg3p772Q)NEXfILWQ43tc!({)*>EPD#pfH!el(*cT=( zh%GAxN_wZcqD-0ajxvM^+*}-wTugx z)`VKUxKv9ec8}^kr-oaI>$5sw6P$XR-z>EyUxL?ck_wK(V}uL+t3{V}6ebH}*$i7_ptj@0HZh z;(Jkw>G5`sdkK@{9`@9Z0FIb9yHb%1Wg{m;fPX=pdO~4G4#hy+b<>PH!;jgBH49CY z6aa@pSQl9i+3xVsS*$K+;0dC`8;zRJVBlYfWjtIExKi$Z=tu7?u_JA$-~ z%V2gf%?e~k<^ZAc9>KVdfSW5hQXGMda+@d)Yykswz&-2;T?%@kyuv{?osr=Sm!87` z&$Y^e3PO1OlY@WFwKN5UTaV<~V90XCyi`Ik%z+CnMf89$Yx_qLsr+`aj57N-YyjDHY-RbPP%Fk;ki_o+;F5L2(AMqE$_^X*o zot~Li$_sW{rVDT#))5_BMEa3LsH2W+B8y51g|L{vo@@$qXF+Q`w`TKPr#|K)`bR-b z-BWFDV4>UPfJ8TKI2w%k7ELc-}Nr`IG|B#s9zt)!O$1|_m19`{x zF>b|yA%)t}wA|UVBzMDFFGo;@>5%@!X+<G^8`+|%OFjeEqXD{ z4gXd5>Ne!1vlqq6VQA)#_EDHa-AyVPCGk<~17rTbSGOl(MSRU6g*gfK z!eDR@AWJxU@2bpVD=~gR4B8fqv>CiCHApS}H*28`9?IlKgFqEWYvB_QJunW7lvP`q#Assmg!%x?vT*;LhNA^pL&9?(*Iq?EH2>Z zc4aQ$-jm_9>o}~tBE(60_m^#@d>BnezAk4!>l`J9lW0F+>h_vo^w|!~Cr_N|488{%M2n}{kYT@uL%3Y&k9`4@^J^nz(HAXU9xup zhEH-^o*TvxBwIxe*GK4uN>itniq)5w4)*uY|(Ip^kzw%m!B>%Ft+_Java!{hm-)vopFk2+WYieygH z!cZD10DI0K+gO>ZTY#HGeB z@W`;QpfX$?bkCltZ+hVkZrQpGx?1hJWci|ftt;WzR?;TtGWx@kLI8#kuu0jrU-M7u zgX#fiwukxi9d#^4t$40qgnzw65qmq$v0!lXPRP0t6z&1Xj5V{f64id0C^aB>E+g;= zHlZD_kQ?C{cNE2-)kaO_I!KtCPIoXGRNwBr6E` zcqL6>fJgN_v~m&z2|ks|Hxngr4+Y;I+>O?%K+zL|WGpe9ITK&R%ijitJb1Q96^EIKqwb<218Y=P8ro9byRNae;}>=(OC7!HuA-)2#{2Kh>WL5qQ=6LF7}hy zI03O~c`7Ax+XS3#9>mKi3+5|_ab6EKj_YsQJ=3%~U0S4Uj@|@p8IosTq0FDYtZm!C@R-Z0NrOQh0O+wbi!tH_G40|wfJblW`a@ zqmaEQcV&rS>BYo#D}U$rsmZ@@0AX{fyL+jK{kOOOYnv~fkK>_9%I1`a2oiEgR@Oc8 z+bdtavn8~v4%ypw*FUabtaynwaYc$86jy3!jUj`njjzHs{r@obmC;dj+qNAyqQu>a zJ8^fHj=PZ%Hz4j#JRNs;fhaNJLfqY*xEm2(!FTRG=Z*318}~<#-n+Z1t83e;J=a=u z7HbG^cQNRjl%DVwF?tBG8QSC7w=hvuWCAMe-px0ZtUt1(EEhDBmBN8ze(c{XCu25kONr}N?VxX}bGn*TE z+YR2>08u+#1yS0=I`cV196ow6JK=8;$6+zVB=t~OlXEEW-TVl>$e`G@C;O>yM0Azo z6PsyiXg*NPrJ6z&$8ReJ)QZ+h4dbowJD4!NcAmyb80U)5bl;v&t3E7UHP;=3Z1|`X z0~6%NJ-R6nU}mh~oYjl$xAFb7+y1yqL1U{%Bi*TEkkF(f>eOSl5Rr0N+>Re9G4T?t z{;V7uNcn~LgD+K)XW1YA#TliU2*0w)b4i3Xcl?Su5wguO%A2m=pwQBK`VF%$fEMDx zKBgx?@Y$n*kS$0R<-*?I4}>jDIp-{WlH z9)y^-XS)jK@P>$;b&D*1QI{tkVqfnRj>psu?_@g|+KS(508S&&LLh3BIHxl)BsLwn znS9|RJ}MxCMu_~ebLcS=N<+|x=0^?A)|UkYHpVpL$b-}ZR2BDSgJOQ8qzqJw?xkCK z$J)X;x7WS+>-)NEklyv3WB^_&_~DL-0%e5K=jU0Hu5dH#)_@H}g=`RxbEVL(Q-2ds zQ+YOm?i3R}=Ly7MYOE8YHg*cJ6o_#Uh-Q!y!hJ}FI@sS2gnGt86xb3vI5W??wT_HT zHxU@Wsp?j^JlIbaxKhE!oZN|v1qQV zxyLJg?B#i!-rxxp9_QqEei=0x8WWjFr+AA~>+3I#ni{wJ`GGylRjW~NNL`wMk zME-h+cPpQ*7m5fMPtFZb-dNTv7vX_>#_sQx4qOiDx-O-T4QD6x(_ukwVBc5*shX(#_x{rz_Bd=pP6Z{`t8qVdQ|(SdA!#*e$H2 z?$y)J_wkL~Ls86r`uedl{#;(eQB{?E8r{4Mv0-mkt!kSi?7mv}QEnVVcmqY*h%k-U z0mX;ADegat56v~NCMBGJ7@H?Q|3&@7hts`Y=xCxJp}S?QhkmI&IRo4RaUq1f;diMp z0yU=^a--})H9p3#>bP@vdOi*o)e@_&AT0{qiRu>EQ;2X0HH~gHks7pJ9Mwb$Sf2HV z%9X$7tLe8_u;h16ep9yPh*7OLTwcOak~lWuX47XVKnWGy$qPJhLj5?8A(HTY!5le* zf9z#aa^TyJtxX`V7lZXIT58gA|5j&y{|Zu!W(iu|p`_M%IcWtvsk`9vTkN)v({A zj$ho9!FM4{^v!MB29zZ~vm&(+Cl`^Gvhe=Vl~m~U%~%Xu7+`cIkC6y=v>T2YH$%cgrf3~DaQim zDhfYqk-B(e9snk>SO8Hl+?TLYaOa+C8i*oObj9c`%HzfaP~VPxxB`1*IXqZ>Z^|kT zu9DFaiozGO$)9P_pXom~6Sf{J3=V7HCa245nqb8@p_^e19vPtSJ7NA~xPBauQ8iNv zhNTDy{yH+NUgTSkHjlXQc~3lFe54at@>ZupiKs(>`#`@sL&u(O?TPX&S{zF1Sl7=0 z?j^iUpYx}B`$@((HMnBCyGA9+3l5$I0uqU-si~e)$odufa^S=weUCMhLZVtvDv)C& zxrbt{K$SH1iLKAify7~|mKjs!xo=PgjQD-AcYU!*dC4-^IU5DY=X=GCue6u>%BD2D z=lU+OUQuc(>s4?}Gc{(=ASy&4$r5*>hM!XSZ^|dfG^ntjUo4d9Ig>eMxgOc5qvp_Cm zD%g`Mrn`l5;6N}Ky zuaYDYwA%-neRLHiCA``MSgX3!q!wDk&0m^ec#DEkf7r;APA&ue3aiB8K0}-cQxP~#9n01TEi7X z(Q`FiH1`f~XlRrRN?oA_4-a=k1s_GiJ|=Z~$rR(G!6=9)UNpU93tu)%)rTUCc)VPH z%Hj#zYb1K=k(V=t=c_M5zx$zYM~LkWH~kI&yBIiewa1aM!ku+23WEBwOm@Rej%(`qSDcN&{`tnAUw?zt*Z+R_DfK+Q=;QE29rfjv zw1Ho^)1J|^^`|sscUL{<5>f_xsTmZ1Z3oy+>e5ug@_~zgHkxYaTPBAUS38?u)Rf$K z5k^Nby>uNjb2xn{)vx=_RG%GdNZ*rLJfm~5)!!2IUeA@!5NsqW$qC9zl*xBbdttcI zlb`xm+~hmhgkP;S`Yj}6`qj^H!Bh;(L=su}u)O53Z&C^$bR?!qNO;F$tH-_HE zo*{_C@xY;X2sJ=qRLW$)6+qhC^EVAX4fm_k9or%pe;c)<4w}q05#~2~inUmAeT=!}uGLXu6Z%lkc@j59kX~5k<8%sf!Sy_RH zs|X<9(4mVCK@*^au7ip4jgW($tN9s;`bntXo#;iX{>z_UY{e4U3RKu`CRT+P%yOXXMtc?#G24ArN2S@d|#f+&|iLP$^^ePl=%(f8}&Pp^}7@0 z`iX!~PHe(Q3Xg5s>R9sBdQ)?HggSE0R`7N?XA_3yyPa1{C(|+_6A;`QMpGtph!TS! z0@3u1=%w8p@IzeE)hxVyzY{|0kP_e=d`c9T;XNwC$`m@2LPC$UBw#IY3ywb)T^hnn zEXDuA@F~O^y`DAwt-dQk%DL+Djon@{!x$>T!9p1RoKf{0aZbA3m@pQ0VNo}zR+K0h zwaD%t?p0z<#E$>Tv=N3Lh))9&@H_4jxf5X$f~h0l@!6!Qd)=Wkv@6Gl%Rao0W!@-3 zbe2f#{^Qt6Es(im_iF1m=wYB>j>PDf(6Xxz*zur_6c_h2)#EO< z9z4Dxmh7SSx%jIxd?63;{Kl28uWv`cr+O7k-%BrJZL9Y!r#kW#iJWG@8>fSaqk}ou z`j$~~zN2YiAN$eON0pf(k*RXJ-0lhp8ztRGC6W=saVZetP(ifB73>1@Cg7V9z?U2* z=@zx5(B|m0%(-fTntwu5=izoL>NwflPwrQJVq7g2WFk??BK4dH|MBPa+Td(9Yz+}I z(4i8#w4S?4G*Jp#&t4UbwinHO0p{@6lB*Pjr>!BoxxRZ&MDxRFbQIHHnSC57iWaE! zShXWfs>>#2)C)A^j+)mLiA9Se}y{Ybgk|SKhDBe+IUYb$0(*Lr40Q@7DHJ>w;w{)pt8o_S5d`w2HExNx_PQ;ro#j-Ldi z>R_X13c}$!#KsOmIc-u!@RM}xs$`TkkvY%P{3d2;+$GBQ-Q>-Ere$nZ>FG(I>$ZnE zDtBES*=9rxNw02@YWd{*r5($6YZm?~ubu3j76e73i9kA?P_&(n(OCu31V!6Szd^&l za4a=BXUan?_4I6X-vzgXFu_qT`-C^GqDz$R7c{@vmieBsZ<;Xf%GoP;FBp%wNsBa(x}jX?B%0QkHvt%kI_J?H`3TvOjDCnT z0=dz`ve8}tuKL#s0xOE7=k(Q>SK3QM%<+n>POP4<$M^@4xti4s|LPq@5=~$4%(sX1 z)%zH~Xwk|$gp~_}a`?LRtd8_EAoZ;cjl65L zdXlBCqVjaHHp`XdC`UqVJw~3B25+YvR^AjP#*qilBuOXl{llAck`U;RDWl!OGx zn~*QRTv&Wz6jr?{?;^Y0$q9<`(5nrq(n17Sq-X?*;jFoleuHk`+!m|V6lPO^!$F(# zA%pw7i=oZC)v{Qjyc*8%*qz#k$j=76(FAD4lYV|;aGH5%PeP3**i~=D{8rf zaa+*a?Zz$)YJ*n3hgaksU<|K9Pd4ZIb!6`N5b>c~Y&UiErDb9HPXou{8Z|}P2aV$t z5Gq>Uyp;dz)80>lmcE=LQRb;9W|jJey(jX6a`cH>430`g`3A1LPFi_iUz0b(zdOpx!QR^+Ym=Bq)ly{}Rn1)>_W9FI3=*USZRI2NyGZ$V# zZ)wONBKKnc^m3YgOA)usS5a?YUxop-|8UJ_%&4rIvsVY}Hw?)}!z*rh?_GVifoBs}KT;eVo}?(8EQ*wgXT?4oVz()k41}XzRY>X(y&}z?@+!aS!g#xq=0{MNV&*@5bpjqozgI`pa!FgNiCWU*QVDGd1km3@JCb zzj-lCICla%DWS>;7+)#WPryjC1DJOkV9X%sVv$VIL>y6MWtK=}fJ%qlJukRrosE`Y z`01i+iq{x*%4w$H{u65}t1QGu^Bb1egL{i?K@mZ36kwVX1fz>F= zbGM#}e^+7W0ujj($pc{`^6-vquQq0%Z6T-hVY#JJzm}fv-}>q8v__b#uF@Hpb?G%d z?RhvKO5JJp`Jf|yHL7OiE7t!N?=$O6lRl+~kz7Ch^vQX=NY*!WMtHij8*?txFQ%RE zy4>%ly>ywcRjNO>9q%u8GkZ7h`lcNo@;U}^ zJp3~>9lNFBl98t!(@9sf=H+vhA!&K`+k?bF?!1P#+>|j-U;sf`bU+6;B`|I;xx9b0 zHT5@B`B%(7!Gr?lCK^lG#-#*YpdiA@b(too9@A84&zr9_lqlEShKqB8q_uGMCRP%V z0FHAKCc|6bf)|E1A7qp^C;-bo;F?rzpLwcx_4g*m+sGBCC0k=1b0y|AF3ZY;)$S7Hsw^sJr_X7cw>054DI-OYJtO*B=0iGFf zQ>EfB#H>_my;Ih!)a3`SLQ)&enp?v7oRLgp)%disVNQ1Xw?^sCCCln%$l_B5wUAs{ z@>B9uAM6uCj(C@7-aRdoqLzB-B1ZHfx&+g<(gqhFqE4+g@>A~BX(&o2)FR8+2Sd@x zarSbqWp@-)l(s$qOxPJdRR0 zKGdEl%PE@4+o}w=`!}xIt1E?2$nhY{!3Lsjz0wJ&BH~Ce8(=tB{SA8dRqyK5N_clG z)WnT`Y&P7k6_tCW87Q*5D1v}b$hiv=sI7oCJ_i=>PI4s-)q)~;?FHi}@OxO!ZB@D^;N8mI-4cs(aSi7{NiKB^3sztj`O9G_G6XMS;*bSze~)-?~DAKQ?1ZX!ec!>N9)-0 z(JULMKRfrSgXFOs@;%-;k#?~g8&l6Zc-G&we_TY zM~vdtQ)A~2Xy<4z;fM!YNI5@GUnoBNl(*~d)erQ@>nq56%W!c=C2 zlBpMbCo8NbEs<+U(J2{LXi4~3RopFmumrVi5ZvUO6Z(JvGx?CNJ~)u_l{@ z{Y@;41N)WI;9KEI8`I=<3z<>wLve`Px;qbodq)kFRxHt8>BLa}FxVc*r(!i}q9k-l#&`h9D--(ueybUZ-y<&9{1psjeG$-3))JootT5^l`JyKmQ_9mJAw^AwM* z(nHV|FGhuC@EW=6>0ft9*IP5(Z#TJLDQP!7U;jDvLhzH2qJYtqaQm#>tOlVnsfX!n zc@}>U%aG%$mHlOZe_@I=k$X2kjNav00vfip6lP|-reI`X6Cp=+v=nCIw2B`nXa`w; zsf@Yyg?yC%7_?H^AgraijM+F$FZWHVli05Xre+l-*6{s|F^uQsnM)`SO2U6v{FT3o z!Gd^uIA{%9Li#S#|Vyo+y{?+xv-RSIb>T&eg#`N7UaYOdO_f-l3mL9 zg>U?#!P+_Tpbk}ccK5Kw!<-wLIJm0Dv!tfU{&>3VoLHvyvl+A0Cdu$gf~~-0 z=gck=ZI>_B z1B7x6E4=g>^bIhoznfl7~0o8!Ig!@aT3dF#FGRvlDaIn6FccQT&n|6O7lo785- z9Pt4TU~`ZE4XPKo4`n9Wu}rvQQCRzVCM_5>CA{z$vgaX*?`uXXtAp0v)5cT(QC*3#Tv2G1opL!^MV$T+?#a^&qTZP-4Ic2ovYm zYZFCZ#8L%TPfJ3oLMEBxdV31d=v=ArgNPsIBT729gxWv~{8?&_=iqCUuUGHuTA)Wm zJ$0ZJdqmN%d$ocDcPtIY6zvL!TMElt+3dl#;_hXnZD!TLn>V1I%}WsnV; zW{J6WWiBv(2LeHNqCvyl0)~r!Md`X3h;Hft^%((am(uTjgvNu-CKfUdTq+J_e4!%R zW?!PletG2}c6xiwR6x17fv9dST5L}kZ=<}jNGHNRJTj|p0db|T)uADIPBIZ0P3-S> zHY23sZ6u=yezoumAtH}k*d-2q;6a4+G~#^b@a}qap&Xf6JaEJqe)(y;Z~g0Hf@;Ll zfvl(|d7)+oT!c{%ww%FERz;*pI)h)1((3lZnr$=WsGltf3@Rro#+#mYN5MGo;Tq|W z@uyXvS4f)1M)!ltykKww$j_DeuI48Gv&)Ycs`!vhT;&2 z`z5};-{K=!G+`YEj10s6PI}cH*!88<*q7IX=D+L=Zp`b;Astbl2D%IoHd1UpV?sVI zd6`JzsV>Kfw0-})Q4+Fzwi+^VuE42T{{#@G0Hs;i<@Zg{*ZCLY(f6EjvP>udA_8(G z|H$@o2sZHpy}^PU@d!M(NOH()lE9=U6o|^QR)>cjPe;sY7)W4 z8}wS-`~E?sv~DLhw*{nc{=DUnty3Cc_>HUxEF}vwneRvh3SY6E+34JV#ff|+;Fndg z^!c$YkJeaDQPdFbYbEbbIJ3@d-K>e#7}bqPqKy#&M?IbU8l+FP;CHnHGKVAzh=U57 zy8E_OUw1|^3kO^3akFS>?1M85>A8|`3LX!hEdV*4?5#vyy$j~Bg$4PM*Q z!V2!|XTun`K)iG`)O%L!hT9omwvf^4yx!IXUIIE*HJlVV;F(+-Ib)|TTIJ!u_@v-B zh>(A2ji(nsI+#=Ii^gGP?g>rv;sB|-@#ujCTc1DzRY$Ko#TIIk1Jmy=4J$Dqn8x)ZmM(!??UA-X> z|0dnRvPP^}Gsj+l&ed0`Br$mHk-UmJz(pxc5ze?70sX$gmp*i}jK1NU{}nbpgUVR~ zvCNHXXAF3w!lA3l^QD3P!n`SWlV2KNp|lW8f1SLlsvan<`wA|;GcLXzruolq@JVr+ zo|`4tlUnA`9beUR`y!j7`{5|4s0?Q*i9XC}iWp42Pi(=U9Z?KFiKO|(1fd4BCI}v1 zR=e*;$Km|bU=e*!$?P5LMShL3PgcV6_X|KUU((Bx$VD#GURkoM%CK6`aWIXLvn=ZY zpE7fdDl&G{mB7pQAEAI;f`RSbzgm^3x}2XzfaBV&Oq1PSSRZ>PLEj^IY%Wc4yX)%` zqO5+NBuH*`(O=P**6T#!NgcgJv*2^eL+IwiWiAgybQR_(UR}G!$Qli?peaBBOu`1JR)=YYN`h zR1Mr)hx0zq`fW@6oV`-Zs`?q7QrCbquwO!wsOJ0-rm;0A5mCcHyC1B%HpP{Jl8yOr zpI1mk-&=TI!#=7_&kgXnBM$3Un{H(a6$^947j1dA1*a3lgDrO7uiDq+$a87F3(;|; zh0S)1Abd0vY8o;UhamDG1Q1{76z-k!Q5zF%p?rAxC3KPB$&Wd%q;ah=%-1|vQoLuF zuTVCSqpLSp72u-bmYQzyJ)q5!mzJ*=f0Hb|SWm9Tnf&@SS>7hj?35p8O=3*EZVD@N zq%Gv~#Ef93aa!{YBp7sg90&ak3d&4VnJrH46E4wxpDFuO#qa=cv{(w_TZEVT=k$U* zg1YEa)+lEv;*D9-`$@AAgd}%cv3nhW!3g`u}}Q-l`vTvJ$B!Q^&D zctU6D zWvW3eq~v+ju3$U_&nK?EsV-mJWuxWB#O0B|TQ)V;uH#fSlob^xaD(fyuoHe%_hNST z+1n7bM$Zax2&Qd8UG@%NUN(%`ga9z~R4t=og$Jx#Mz+71Li6;?)9J+;EDPzWDEIpKod&3~7Y>JFLrz15~ zR+-T0*sOeo6kwP|nyv@z*qO0aa{+E7EIYp<9$Y;Ki>!5K>2vf^wD1{%ljKz8SO`Ig zh)Va29Q}iVfSRw7rC#Q=L+zGDCKxvac9j7Z8gUIV;;uPEXC=df{mUx#*~KK#78ts$ zTziCCI$P%?Qlq3Fj>49-aUj`A9-JRU>%V%ax2z@*!LWP~!B^QM#Y;--OB(#_Rt~9Z z8E$x!9EN2iOAnT-fhEE)>#5Zh3z&VwB8{5MbZNa-Zccj#6PvbX!g!I7-C8yk#+azD zD%*<{4B_!Umx=>BwmHSVQ5mYFQpmKiR6$nVr6r(HY8&NCtb%nS4_+Z1@vMi&g7TkV zk_@{1FSdie_pz^!`%_wTN0O2O3o!d~~u0_H6Q<01_ zwOH}SNzJr9`+-bBe>Q>N0Q)>nCBm+hVPm6tM^mgTpwu!Ip%sD#i`fQTPXsc@{doT6 zcJd6c5b8oKa1X90-H+mkmA#=h+wWJw(i~q3;~VO29LWY776V~=EB>*}z7fC9-d_#u zgJh0a>8_W5ScfKFRdCB=u;hD!Vt+VC59GteBs`cPBDa$#uJ|>IaKYDIk{CTks@~N; zPI#bw@#!n|TJ-r@|DM**LToT+Q5EL{uW$?8C~hu7S;|`URc;Y-k=)cY^ZPgNXf0@T z%5Yk)M(Gjl1tEPivMVg2%?FW4m+xGKQe*kO^Q@Q{Qr2GzU1XQpa1^C5BCm>G`Cid&hT!Hc&hJplJ$FQNIS=9NpgmmH4mdr_u)4pGOM0W9+)U` zW=Ey?e9jOCdgx|4CUnf&)3fCoC=S6?Rhb9NoU=r??dya-F3;d0i(MO+1oTov=K4#G ze11`QhNUE3(dB^80p*n^*3qHayO1ax|=x}T3+1Z}I^n|oVRYCV; zI5ePkN*;&^at<9Wv#g_#>3j%JJ6QC1%aGp z*Qv)km<4mXDzZ*@*0nPhaOd#n*0-v?d=gGlxgXywv*AYaPNN5W&@Wk@r8^r~rP7Ws zZ!8)dt>X%=Feyt40oi*w982j2Wjs7Lx$q~>4!tAgCaPrT7)SHN8t!BVco+tt`uRGE zsj6}S89{>q3O==@B=-_4?xQmK@Pium1t3(JxHgNnjE)?{_6QqemcN}M@=X#Z<wKqy zDY`AV-+~B|sn{Cm=0}qP5cT=}2AN>v#WS3(02%mRtl!D7Pxu|uA1oaV7vKBCHeMoY zK&^X`Tv+FWh+!$8?bp11mBQw%Ei%IN?9=c0hkV2M`AKM=ekO~c+-_`KVW`D+8V!E! zBaM^*YUDzyLMGKM8e~yS>j(lU===r^LRrPqCX%&c8J%W7Q8HU4Ow9^$vDfz&oQ!;J zd%IVoP#(Jb2ytG3}GT2(@sQ@ux$3 z6iLO7#L^j>Ld&!yuz{KBPM(!u{x|Fd=YASYqF7SmNn;^%WMPA-{tagCfYzx5TwL@Y z+ulNs1s{KdybC%ImvyTM{V=TKqu(Du9A~HvEF~;0b!)e6>I*~{CEPG%3R)-$wJl%( zx^UqN4SGFXlcXdp59yDbS|hz_vQ%!ssNkv_=yGVCayrL)K0B@rZR#b~kRQDfP1fKYeti%9OPNfRBv zu@^d6@=r}|JD3s7=2`O~!|g5v5!M5YwOpmoC;`^%Ubjlmso_tti1nm~&y<@0$sjvG zGUzQxIpN`U9U1m4I`Baep&%yDy*+ekCPZ@wTnI^mVao_a4HwvMaI)vlvt7Zos$BDI z_5L{kyJ*~gr9V!V%~%6vKB5p=YTJ{f%813o0VpbLVcsIt;lx)_$W^KYGGE?g{RS5eloNL%i$k2eS9$2EhHdC$(z43$Y(lRWSk^~>iED9sbY`H-n6G)5`SWWbpI|ZM2r#} zN!DUX2dc0@>^uXL$J2sHHk02VsR`6U$8# z+I1=5E6&!hKM}W{OJRRoA4Up>$Z~c zOOcExm2^!yR}qnf-H+_}#1u$|{b&1>1$#PYo{V=ybf|f~=TuR!#Tv)o+kMx!!-JPr z_tM-XwXwof8whvuYBi7^QcP)IDd@lgRG{B4+n=1$s>jFbi}&8|S!Qqxpz{}@ToDny z^uv5n8I|muRoVvv;|elP)zGKBXB1I2*1R)05Bb`6K@YKrPnkZA{)~F6NDVV~tYl?) zm{1J^Gql#}+^Po29(KHK;zJ7284N3t{qx)XD^C-zJI53jR&T=|w{g35@o1@FO^)*Y+_`5~t+4dET5fnnTBvAPQz7}L{Lm?J$S}ey2 z$5fR9oI2UkPKLZ|ES;TFF76lnXT1cU4F!986K8S<%!fBFAx%AM0Od5KOu;g+xJWlm z-3GF_tB1S@E8t~Z@|cU03rVH+{sXmc-7sNN#Wl?+l1j<<4u3V;@^cwJyQT>6ASj5x zrUhc*cPH0EV5-&b*iJo7`sr=C`ZIez>rK+uPJr9M_svLOY3J4lmN3xhl!w>%PhQw! zjQ+T-{GtGAK{;EpPw&5tBh-p&-;DDtRzyq#ZGsIx+2rbiT8T+$q^R`~`>esV~A zlz%RDZb_C+_(5!!^20*D_lTU=)j?2!wZmIwoeJBr8S78KVY4*zWcBDC1Q8UKPA$5j z(b!0)56k`Q7QR=tMLt49^`tG@`q2SXn99fQSb)7~SE)ZkJv!YkD>1;yZpg8n6583& zf&pQ`-SViRL-o(<)7EI!gP%k}8c1J>noj*?`sGb> zGWqU9dFhiHnytX-M07fR1>R9pXlI1fK3ya~71B2?EOxlki{G-cjMNI6m`b6NQyVnx zj{5rtz7KYef>2?AwkUROlF}@yFbo zfqR9r@$~$9*kZ-d=Qo;@uUYrs7vX1Rcxu|Xm{}AyZH?1a?aFxkT%W|nMSR`+0pUp^ zC*1Vfra?Yq;tL#ieHj|f!()+nukW!d4MF#Hqz^KLBpyJbeK;ZYh%x6< zJMxI6cpFx)i+>~kg=x6Tw?k_*HP5wTABG#EO{;;Oh=sVcjY~zjH95-{Y8~4SK;}b& zTc5$njTrg)9)pzUIBtI?8S#?@LipZacT&Ade7efYXELY1E`%@5Sfqck@--@8O6EkE zU+LS}TQJDUhi;)`;znuV7nV^IN`}O2KW35PGDa3L=u1NgGLz#%^_3~J<@N2!X{eGl z{p;Z?<>hCv;xr1HpvKI`b=_2xdC}1^9^Vnb3pe>xvFoVXnb+}VSHCvZ%z`^>HX zJPn4_%atnVJbfpN#wi?^LrQwj!mVmL?0)vPWmQxprN?1uq&cV3ou-F<#^{&K1h9<{<%_MC(<13=GMyrqpPmT-mDe$-F2v|F1V;a zR~;|+#n^woPdNP<$*o>eG)>;c+Q>BJxHqs6USWm}&dmibi(yalqfax3%iANXB_#Yp z{NBqs4VPx>)H&><;V3N44zn+tl1F0c>nnK15R%M(nhN3N=vIwPkA@NpIfL#Ws@rBr zG39&YNC2t{RRa%`0X`MXO`gyy=%UA?6;Y$$K)3`Pul;uu;)ZEitQ>=384@Ga#o`&x zP-4am#u#oNq=KI!CYukiaBnySCU5`&TcraZI*zUZJ$0r0?+h9Zkx-YNJgGGUk-Hs* zJoj5%jmaPReR!lZN79qxl9SSGg18EWdyvE-5|+J~ZO##=4-Az)vsq*hfJdGg+XfsI zH7xq^BvK3~q-ZOmyBM!dGfRBPv7T0o$2d$7gCxCHoy~jx9y44(2gtG**kO%*xpYRb zCB3hCZ+H>uiqHV1(*?PI`3;ie4Qhz>))^d(^1Dm;dvqk9{#T(=<^|FjII|b=dqDT* z4TfvYStOQD3lXDJwF{fDYzVSYc<4OB7uO+6!v!6rcL*! z2M+i1r#UvlR7cGy`WYt!RGk z;G|4`Vh;Q((yBIkyW-3&$fw zj>pL*@Qp?e84-uZ;Wi5VdM;=q*9x7?vY=#XBUnNck;_NBw6mzYN@W+K}8xFPqCWzb8<#<4s)^WXaDxQ?NmJcCtRrFoL6fF6*nA+6?a@pVDqUBp#%1?>{1NorYjmi$HBDYG=E1 zSVoGKX{Oqu(KIyEFns$99@NT}Qc^G8&eI6`MkYhu}{w2CKBzn)53P zoRwuoiOI_~wf@|nJJp=cuw^eu1>q3D7dMZV9+foGzig#{{c|=n$g*Dlnrr8TNhR`zbhcS7)e@+D!2_F5PS;4oe zF5*=b>`Ga7(9Ir=!w$4tTL+rJzBvyMcZIbzm_o_=NOfA9FwhlDvjdV9=!madxHX}C z8-)pxGy};}`|~8AS7^^!Zych9KX|E>nMFtmdz48P4SJN#E$gY!6|`X?Zc!k|u#Ys! zZaa!YkIC?zqqb-Loe45z3EWVRH@U8ZIng*V%OdZ5nz+Y-F%LcVhVL9&>#6BN-Ppj9eyg@1%r=h#}r1!xtAt?20Bp9U^-iU$*#$uQe^6t2V=l) z@t`Xx!YuI3rX0X#+J|)#zwmKhi~W>Ao7Yo>ZkQ6%6Is zS5ae^a>t1w1jIUMU?U!^cC#gSqo)xG#aN-oVp3P2&vb(|3K)JO$uY1w#hIYzD5L4g-uR6kc&EVl>Co3#Yxrd3JHIiv#NA{2lTO8RSrf_B&ei&gWF#UfP_f<^;gL^v^ge?I=lko{jqO&ItZHTW9QPK$me zYrticdEUvwqc}qjJlA5|NsYRbP}d}rumILHV-l-DNN2i9M^yPSj(du5m+zSIE2+=i z2L9?wo113Vai3@ep8DYGEnmL{4Ze$|^$jf7upEQlj5`SaQbp1;Hm>pSI#K1dHSl#3 zN{(N}!bKx4ZzoV|lEdkxARHa5Ep%4mT5%HkC^`~9u zp8E}2E{57YrKMPFPA7K>IOM9_?H|sHPfZjrLoH(P$oE&Fb3+MtoYNM)-HyP> zbq!yW5q@+f7twimUk>@$4?f2Q{Z3K1Rrr=dS9X7uY_b+*tm|VDDzFnmM%X0%hZR zu(bWXoodnF%1E)4>ZyU^kN4#yu6%Ho2)whGo-VFDsLqHCx^}ph)QrPG45KR3h>fG= zCJM*TGS+{oUoka0o8JgS!*lJ-AzNZy>lsAh<(t4Xz)8g+1&YszNI!b9S6lCBr0R1P7*w&a z)vTq6WK_&k=wUWXuN^SfzjiOj_(KJzV83;oh0@BU@C=0Urg^_mlxZ-Tg(wliFlE~ZT$HD_qR%Wk&CA_phW>tNZ z{0Y@8G2!G)vEj!rm64>8>FW5z8pTBLBgC;0RMQdu%!nf2fiQ=1oZYY%LbH@lX}N8Y zYmT^Wprd7vMo`ql`){3FN$E$Wsii$X+wjYsALJHlM~-QXaz8 zqkq=$)Pj`4GRQHi=6-Y(7;o3U-rWF24q{ZT_+Lz-4X0^8?bq;x0(v5{)6&dlr8Zb~ z$*PwY#9nDRa_URL9>t>BLp3H6I4gQ#ca&`zt_DPU7D?*v5tU;wepeU1-y=|vZrSr5 zmlA-9;b)LrgaW7X%r#pHO^!q#nj*M(a1IBDk1Mv{8=mU@_tx-6!k6~om9?uaJUkN7 z)&}iwlD=wEDbv-=MDCT;VTI^lie)(wFR0lvL%N)!wu?2D*T~D>eOyWQ(euAY(MFto z^V*9o?{VSJk3Cz9&(@iiZCuc?nDZ2v1$(n@=)pT+Xm zUQYTuU`bgx9E)-?7ET<%Anh+=C&L=0v1U?uPO_DyYBXW2TbPuU4V$!(6a^`e-EhLzd5{`@8aHzwUV1G_*FFKzcpDBQOA=WC(> z;Q5rgm?E`jeX~+S+!U*Pg0yv>CgR{(vpP(Qfjc!#{6dq4D|?jJsCa^F#uhj$J!B*p z2#G?$pvvtLmgGQS{0ub^>lDG*BHI>zJScpKM^?7!-O%_lUMam?eOGI^mX3DXZBJtg zLc5*4(kl!zpoLF<{TvOi`ehb|8Cmf+i1bzL+GZ9!%oi_>`pilxuHzTq`eSZ2ICCq7 z|Jc#=c4$F_wFhjt)#v4w(z?F3OGwp`2q|j({IgqzQwqK^b?+z5o@97@=*~qoTlUMc zNv+SSt!O)`O-G6WySCcOqLW(Zjb&W`HL+0gEtpAfM|C2uVc}Z?rx6x9W9mD%lEIGr zjGIJrmYm^-1fb?ZQ2;fZt#7%`^n5On`+M6>X^qGR-?0aLN&ngij%(~`2Gwg?N3(G-lJG5`3~mQR)OZl%91zncx*m`KXIQTKbJF2dgtm` z3KOd!7C0P6U96VQ>rB3%gZZ+Bsiot9UbU5teTGYgrn2{`QHyp*Gm_D+ZOuy-&5;5T zc8Nx1<8zD?>M3!heO~|<5X4&EI`L{piW8=2%EF&op+k+3HU4}{CTc($_igzTNrHRe z1?sp4L-m`c#iSceFFKr>-#So7QTINxj5q7K#Q zjq?h-ap{Bn1_k=s2|~TU2PsC{>a;4wy99p?LSCl5${RFkg+k|L%VAVCh;n>cJrpvk zll17$X5?7xFR0o4XR$S6s5_P>GVxb?*lp$On^F07Y)Dt{+u{Oy&OIdm+eq=c)ozc( zrG=$Mo{nl$WbOmk*5Mk(c$ZEH++D47R}AB5p=W(WR!A z3WjGFP4aT24!f*lg%OKiFM%$eR>z2C*FJmgJ?*2x#PE)*>ujX8Iu=46IdQjqMZy`; zv<%%i8s(BXPH=F*&{&Ny_JcSCTjT|XENkWx*n|BG-8!NNTFIntKY^>&Dd>Fu)ojSX z_s<#N*6b+BhNKA5LXVK?R($wT9U|bzbhe@N18?*}AvZ&2&e>3R<1Rk5RFaQnHT#fqxhjL1kRClI(DFN7#3saA(b`K2x*u`6 zipg`IX}~w#17Aq|wYOnWx6np4SN(RX;HFXFRh`L1`qYUES(Wgq%QZAdQ% zVD&NlKFHSI?XZ*K0Kb%&M1nae`I>3x$k;?Ae5S_Zdv87WrsjEhz7y z{C0f4l=+>O!Yj zlPxB58}S*{qD%(52qAbD!bmkcK8@PiGpZ&kk9mR$Tys4Q>z|50&xg!z3*p>qfWkD- ze7FVXuT?dG4?m`ONQ^(CyFjc^)bdkfi?2G*Uc|JtM&FJJFc$eBWuER&5Y+fttc;?8 zVW3=R3IfI|CJlKD+B9it?7*v!)rkB`$HX2yQ#!9&G)*g`%*X={b;LAF@g0uOW`R^B z$RPZ1{QNW(s#Q(NWih@aODcHt>=!B-#>BB=L6V$E4!ldHW~j zl1~>OL?7udbsk=;rIyTWuYOdgO(inX)PZy0uWzAYAReZ3f{C#+V$6BxRyvrSpMI0L zU=>`MP3O`NM_kp#nCuXzBD7zJKrLb`3HZXYJ-`=QFA?TOf~Mc_v;LwR%o&Hb+?0gy zQQYgYm4wATu`P%#fw$*{MmngS;8_N>IXc3e8aaHsgmf!rvr`}`c1;aouMTIh5?$bi zH;t!mZR_-ZgZOVao9oud1T}Mx89Fd7WX<%6Y-HC0&c@>c#%FQ2cLeCJu8g;J-i-cd z!tfU))6Gg3ndp3ReAzq{HE(|R(gWG=sr{8uzY)k5g2W0nY;S+bt;AL*pW< zDa9k7x%t>uis~7docACYq14XQ+1=8!M2_;*$pt5tWKpndwdeIvNB;6RFsI+puf}s@ z5r%U?es>#B0A5f~n`w}I+|#%yyaJc?^#5>(0s+kF@}F-Zwik;Lmda;vOL&M#9O-z6BA`BZ3!8XI7|fgR(T(NGraZEL zfY4Gt)l>K()zj6Cl^(=&YaP;_e5@8a^fLz}*CiF{p_4cLxCDWQ4CXiSmk?T6LO?0tRuS6QdxW5Afw?b^5bL?A_0E*w;M5P zugY|JC`vR+RFfLHX?Vibu@GSsk~|zSwYm?w#tCwD(|bH*qaG)w6iNIvHP8GFfHhgO zJWP2@gly%We?;8fEyGW;$Ofo(aRrB`cf1|G$v0@-RLZCuK{jp!@ARp%2!`*z8%iaN z&ZSN*C)XpIG3XZ`AxDHg-cqG87UgodJ5G7EftfeX&AW~KA!v)7X9d`{#+5P_CGm4| zny6W(xav#K@cugw*x4_39Q#c4l3(z_M)gRA5IXmz0>48`jY~*%P`ekI=TY?Ot&dQq z=xd5(f@hlMFHn5vobVr_qCW1+J=8Jc;At(u)R;cKeszD!^oOV+r3LrmuF(Ea(A>Y6 z3nwL3DDXD0ONmRsf!mxmgJuE99hJKyalN5iCnyFo?yk?_H?y5k7C+g(x>2C7qCxm6DEZ3NB=;Jcy6`9gV436wCv{*FmT^a z0v4-%wfC8BOsc~yC9*BjdF3+c(ctL_)!r}&qC%h=bY+e8kee>-mp5o_@@n>y|#kex9 z`7FdaLs_zCEg+oE1S(Ylc(Mx7+(%r&i5bRRwVun{EHwp5%v23tDQh(a8e<2nq>>)c zDKUWI!2E#4s6Xzjm|IEeAh0=XjO<5`1_U!5&qe8#5=!1?LItxU(ft=m=M>zIHO%Fd ze@4=+n;^51u=)m!pr6q)5awvF#KlmSN|DLx*g%l3p(uzKeH&PS-$P;M&mVpotDp;O zP%f64cV=Ns;m-)&jA`HAvVR5Tr76G&?p0kk{iW87hs@K=vyZ|Xb2xhOMLs|EqK(B4iwlOnP>3bgt<`3Z_NzoRwg*n(5Tp|#; zr6Xvj6h#LkDxO)L%iiYi4reYTioqp3N*5ph9msLuCUW5vH%A@EFN#A?NHR#0`#2lXI&ScylAQc2QD%4 z!9SYGhW=KXqbb9 z5(N#5#T0<;urc3(5`43Urvzp-x3&A-B~p=sgE!pIPC+B-t)&H@nRI<>a`PQe!vq&BOr@=Axpia$W_O#%*$K?m#+0r0CQNByA4-=MOP$-g(d z`9DOoO}PBPw#bQzo2p4c^T5zVH3YmgrVNL^t1*KUgC;55>*Yw7DtHmBU@d>pB?0pO zt@OodB@g4VZT9}=JF>}M|DuIYja426gPFfUjzGP;Rseh$B|^8{dHTD-%I4$rGABp8 z$R}Jo^I#@zhm1$0MfY7B4qIzvr}k+i*f<{9tNgmN@2=z@#N#%h);!VY60Ob%le1sp z(-m*84Yri0z1PVC^k&*1y}D?Lwn6?pE%7#DKn6OeLCf|}DXj!K>FCl51oZ6swkd2x z>wyH1yNV5mh32)e^vZfZHYukqeI)MST1j5)(cxJG2jeBXUV}F_FZH3NO~4?7*dFj@ z#5PH@2&MxDi4vmCQbJeKZu8Z)$bG(g4#w9;iU68%$Kx;0+GSB}V&(cO-8AO&2Tcv4%d$pRHLT@BHhI?d*^M!H$35Fg+v;ZBZ@W5x(4P>sl>a{Zp}jw`%^YQOP#z z^tIJjgVa~`4DUR13r(3tl>6MInONluR111DoSKTjaN1X5kq}XEM7K;!o&7!{`V{F@ zD0eCOR%2`j7SFOH0TL z^IzVrk{OIIlw@UD2D2DcaJ&>RpJdO9 zPz1E>rU=VAC+^ui@80lOfF>Ys%1myG`}1F$%v9Q&l*8T(n z1@YOsE%;nIEk6O3pIQjQFq^yumN5^ZSiLb<(I*^@mrmHmG5wQ}DWe$sYBK z!~2N)UZ)xl<&0!0&ngsE+8Xe6LyAz66q*rdCfT+>f3oj~umH|UP>e(@TFOM~G@LOn z*_844$o|)Rz&+{d80dOfC7F?ZH~~xw6rz51BzgCUWA(*Ohj+x5 z?&L)iUm5v~BdHJ0<9uqjH6&u-NddfMAmsV)^j&XV~?;g?7Egd9zs6VOHY? ztPvZuwf$!}IC1$Fz4)ZGIJwz2b<9(f8%psC&5EG$m_MaRY{Wm?Ww)jS@|B(tco8!5ZNzQ<(YV*{!LUr6yUOd0FzVNH$<(taI@ythix zq&w7*J)mjoFGQ`tZP|>dQ%!779`7Wsc@~*{yev@cV;3_G}pnNXEV<#;_ z>oMTuB{q$T$9Dd6o7^CaX_*#K?(lSZ@f#%YN~=lXdb9N*Z$Bn1&=Y3A`^bl1!x}%; zYq@ZqxBCX2Sf$qDWu98vKXpjY}%JeeQOa-Ce&PHkyVV0vjE zt0!{4QIvKEhY#Y2obgezd$m8Y&+phl5(VA0R0@5Urj!^FQ>BR5IU zPHwcRMsq8Qa#)6VSa51XF^dl#;j1Z%`#|S|-;zPKr4-4u063oN!YA(WA$<$C3y<2X zSPYNHRv|0WulIeu+Kcwu_Ttt{`sGsRV5Um>3q?pc526w=_GkAKN<=GP>8LIT&$TvK zuha9cZ?9Ln2X3; zKzFh8x=DJSs{LSoF3g3a@Lk2wXYOv4@8?pW7eY`(WkoZR7W-i2LR@0F)y1!+PxUV4 z5-B`7j_r9QtrYaI3le35R2Qh_-AkLvAE?h!$ewEqiF|L@H&Q zx;w%_iYTb*ZIBTHEBc?rZ@@;*akoYsCYV3mrO#xX-4U+Ol^u64&|O7k;#m~s$@h%l zuI{w9hVpv6CIel`oDbv~V(DKcSwq6w1{e#*My8UxKT=3UR0Qe}1I5~6Y_u8qW@CdD zlIX`*jN(T<0(@a20Yy@0&?=uOMxER0BbB+wr@i}3jPZQ4A02nnBd1!E*F0bxM}l8B zboA^KW_9-qN{E_goT`UNUhgt|>M80xfC3Yl(h0D^h&)1UE8MhHknZ%@cVXxxi`1Bp z%7OZ&lFSD{8agb%1z;Ot5ula-LM1A8MYI?uwBD*KdJx;Lxmn(b!s14LPfS;lvqHD@4tIW;8kSD!^1Lt)?m=yag7{D!r8ri%>csyVB_cQHA; zMc4;vlhKeS$rOzu&BB_~eyl0%`7KdBSd>SZ=O`j*V+Q>CEDBO5?oR0Rb6mBVUbm3`7bE%treN!3PY z0Q^&PWDd2N;2Iep9?QQK9}-JfCQ6#T?H4HL^`w0SV@RaFJq&*FttT|cksN^F>HUPy zhw%5}APc1g=Bosswz3!m=L%?SJ-uaz4|wNR>aDb2SHjUqGRY;03Pez4P!Ool#Rzw3 zmc86J3K|v{NBN2tjX`JF^utl``a38CElOuLpp54aK@^V~!}gMLbg8XOJ11PZ?-y4- zlX|wCjvYUiWDkQfg0C*AJ$2D71j8%J82^WZK|^ObklH!d%CfGVS(n2T7?ybY^C`YH zJDUFdt1hh(kT=3%W{^Hi^yZXn_Z&z@OhSht138nNy*)NSJ+^XAg9Rm+eQg!!8f5Z? zmQajZ97}>P*zvE*}^3w3WB*}2L zPiZ(zDhi{KRc=|uK^IiWkQbqN*v$gR-+qk ziWO|!MiP@@UHaQ)m#&{SuUlxjX6+=6TwxAQ|E`Upi_Cccfn3Qm`WwO%QJ&zXPAR2u z!2Hyt45@7>S1S*$2!5Mua^52dnMHy1Pw2#Mb>w;=Heb; zaJ?%YakDOW9;oo}$T46OK&^t)_2!pG6G(%^{zgn^zI|w*lJ6FgYz@q0aMYCj?h>|O zI|-dN5Sv;^&0UZHZO%XvFM4)q{o}NTkuO}KqWv`c1nI{)mr;C~+3iD)S!`@IE}i#F z=0*G|EVe3=NeNH@bcpDqnxV4&gQJpSY^G;lR~1XOe5D2))b6{+UASQ3sRaZ@EjynT zIOeQvYT+h+WM*`Jf*H@Te|XCrHxB_yupAQEa@F|6Rf;i)Gr8=@(K&(aPuc4+&h>%# zFgO}foR3c6pHXebl_@6t%3utOC0IbHz5bEFFqu-cc|`Aql2Xt;;rbS2VToS+r?kA~ z*ZzSIT;X?t7MAM+N|(}<^@q2GxP~DwEz*<5m|595E?mx!zS8JB@2O*jQu8ON7kB`H zSn}F&(sD;>^7V+Cwr}SuD8NPEXo5Pwe7|tktFaS`=@3iVcq%5{z9?CJH^ z1Lu3^6XJe?s4}S!DG9}DHWkl0eF@@v_6_u9HniO!BU(Es|2iT!iv&W`$of5Xk}GqcK^IZSsXVfq&J z3L_X%?e$UW(5-QiJM_RpAKbT^D%|49w9-!+8Lo9nNtNes6lddF5+KnDFO{$;CcJFw zOsfDm`KWm)p0u=3kBDM7XaylQ@bYvg@i)kR+E7^eGS`7?`tCrXs6Z{bhV!O-G-F+|5CD)y ze(3-H6?d;gsiHm?^<${Yzt$a4+oG>}2tlhbkY_O!N(NiHku0sOC2m%lb0gqP5jX1E(PPc`Be@tXLKC^TR8E|Hl2LM9fr4cH5;2~anZc4&p1I{R!&2+V_) z8h-g8v?dJmMZ^fE&8l;q)oz*`$U53W7CWwRdmnhuFe<7N+ZnVjDLx%aQK+_lgzCsj z6zE=SU1h^qE*mdWezGe!JenMeRm)5#jXkGqrjr>`NBH?Dw-drFNm#Sf+981iIc5~ z#9jb}a^0&K|c(; zJ}NtRpx&YC9QMu1q6FV5L3VAfA8Y`cJ>tvo^ce&Hs-tNFF zns~5Xu@5FenP;pmwLgw{>hzL#{Lnl;n`x_EvpMgsyHW z{rX{d)^Ff65(8gRzqWtL$zr8MV3sap5<3_X;ss@9P`i!^6~Ce1i@Ioy^~TMM1gn_7?ygXDQ&qJB9=$Pp2i(ANQ@Il*s18Ezl$&M;X8z-zn|aj@L# zdpR8-J61QHOj(noZ(+fd+S@Y!zQ-b8Hr;-prFR692S`vVZob0C{!j!&nRaFj&Lx+S znWE^%P`5G*z^b@)4FE5^x=F723D(|9E7*Y-DISM!5!6YC`-oOejI?)F=m^Y+fDjvR{j3*lY za981ycQm^@9*Y@(vjz2`gAU&v;P7VEl|da;Jb2m=@|HL#?7(ZheZY^dZK%Y@R%YOs z8-aw3@oTVh%v!&WO@iQL(qlTw2u`!jEg(??*~*H;*E{2Kkzz?f) z!J6ocD{Aik_m~fXtopQto1Y6DKNR)jMjsHue@=kMkdWZ9X%G*;BK-(WG@XuL!z=g9{5E9SjES-v`}A(1q(GRoa6-z zhHCuh8s-+-#~3kr!2mbzmead4JpY0kwKs`%DfPqPrTb1Zhd@z=uhT_x5#&_-5l3n1 zARhsz#~S%hb+5$3S=g2`{L*~m1CyvGECNVY44*pYt;bZ zati@8ri%~tC038}XC9T>9s}vlo(54x4~_YGPAQAz$vqg%P7V zjjK!ewaS-nOjr?)d|(J9@2WbshsHV zQh-p87^M4|D}s?{lqlysV36}{vsY#4?K?!|^SMU90~nc71to=@4#`Ma&D8sevU$5u zYODg^H8yyv5;(9}R&8_EdJ!p50l&N`Utr>M= zTZ6@f`=$FTjHK;(lDbxiVal(=*cb zDlwL=@l_*GI#2qo#QG%@8a~5Xb!Fi5l1Z1`-04KE78!`s-&}?6V0Fs%~f(^tY+Szg2oJ#rv*XdvtU>LCD>y*-m9Ga8Q8_nc@U z`jr#h%rx6(r5IaPcH>-yZ_(Lgh5KnJ>YZKeCymMLNM)!~0RGc!UDy*K62i~^wXBiw zs$@-c@TLZ|E>?kPyMGXczcaHUg1r}A(g-c#)r}cm;Jz={&T!HF*qmdM`4Uit!vP_I z-t`|*t`9*6dnB;VLg`P^pZL2K5BOEMb|64JMEGYmJLmaUqX`!$Lhqe)od$*E+F-JZB#3-j{_@-8ZZD^QD5RbON2o51u2Cmk*Opwigz zsHr}yY9BUsY?}E}8Jmt;Bz%9M=V>%g!Un7kM&YkztSp~2M4m;Qp)cPb?V!gO=`skO zKDM8B>+nyQ(Gi<0DdKaEBy+6tjnm|4nL?HpmjsR*D4dR^!KW5|wd4Du+LNPw36{4@ zf8cuLEfGI2x~ z;v-BL$nIl^gB1z^0B~YgP2e3T``vt*4WVHDrSs<+;f^iG*t^Ta#rn4MA$+q$z8VJ7 z>J3RkfG5MpY|Wo3rE!t%U+pOFKszHUs|y0V>xGQGah%I3{MWPh7@x+950JmoYAB7$ z=Ygpq%qXPm5n-}A$go(5f+jr~Z9@$Ct9oTr9ucVs=uM1J99~iv*OfdB$FAA?))^Rk zWJuGc(j>froJ!cUY-C)ed|cZ*XsvAN)+JcmeQ`XRVcLO342-1Uh^$nVBX>tcJzd*C z>-^ZMPq^nCBrB^_mP>(-A&Sf)bccVPHm3iF4;d*jml+Kbv<0w`@-^jxVuiM(OH)gCu>C#QMD=MGG~lchn;z8HW4w z=O^h4P0-OOu_TA&U6y5d*Y1Jr6gJJQbc8w}l96>8)?-7!vLrgaG2LrCmR%8V^zhLK zO|$$$?eZ`yCThjRI-FE>*|{;uoJJNL%x}VUUV0RxLoUg+Cbd$-(gdOUvK1BwdOQp(Ccxgq#N+lvB^Hk zuDJR)k^!9Iuk}wWEt*(!5=R~sxaqjnKe(kWmsaHW<0HY*F85%*#u$k5m7E7zaP_Ykk=$hPAMNZ zUA%&p>{rJ{DGMXgq4@XCF^SMGX_ja64yp_H#Ec(|;p92$K} zBLNa%OS5R8ne>Vb&$eVdG>*8Qnj6wtr|#ThAaWyZDOWFRNl z?fSvj&`|I%*8@CNx#Qz-@c^4%kvrjekeT@F0WyYcAi%{owu1-^2oSrM5`r}io zGrhRvb&?Fo-57=`#!R1hl^s#W@fce0xt>}2$xF#dR;#Mr!28SIk>4QwNoN;kS<`7O zS(`_?kGL;13g?Yg4mjc_=k~0UInHM)=&T)DN+w2B`G!OJV-Y!z8DV#X8GzbjmcaB* z0KszXJnJuZUvV&oWoo3zr}b&%s8VA*V|r~C-3o4qj&}V(*)JBHeQ-g2<U7S|eKi zOgvfM+osO|u6qjTr9uDA-~abd&jzxofdRu(oX}{&QbIz8?fP-@)tWdW7_T`ovbu@$ zW~c+{CtEyst%_03%t_4;TRZAm>M)4H)4QC04AIxDRMV`42wyfb_|OlA^K|Q6*%yMFDt3NX&{nBfz~cyT3^#U09C=W z&2lH^)J=b{=n5Rnjrw};KR2LISgNFRVY10*N(@&zu?)Cn$2pg#U;4i-6F!)(0;hMI p=LT(}_hzit?TuA2H-`-D+TKfd%3dci!c?>8BF})}F8S}p{{@M%W3~VQ diff --git a/.donate/qr/wechatpay.jpg b/.donate/qr/wechatpay.jpg deleted file mode 100644 index b8bc54ce4ff9b31e8c5f32c0a5fbbbb1e733cb4b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 78273 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

U(PybMhql}K>n $&!(9*0}u^l9i+hu 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 diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 3800c8b7..01e9a05f 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -12,4 +12,4 @@ #custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] # github: monkeyWie ko_fi: gopeed -custom: https://github.com/GopeedLab/gopeed/blob/main/.donate/index.md#donate +custom: https://docs.gopeed.com/donate diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d9543c71..953034d1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,8 +4,8 @@ on: workflow_dispatch: env: - GO_VERSION: "1.20" - FLUTTER_VERSION: 3.10.6 + GO_VERSION: "1.19" + FLUTTER_VERSION: "3.10.6" permissions: contents: write diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0debd6ba..8a6c4d52 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,7 +17,7 @@ on: - "Dockerfile" env: - GO_VERSION: "1.20" + GO_VERSION: "1.19" permissions: contents: write diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 87eb4b15..ffaec7db 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,8 +27,8 @@ on: - "go.sum" env: - GO_VERSION: "1.20" - FLUTTER_VERSION: 3.10.6 + GO_VERSION: "1.19" + FLUTTER_VERSION: "3.10.6" jobs: # lint: diff --git a/.gitignore b/.gitignore index dff3bdf4..de95dc47 100644 --- a/.gitignore +++ b/.gitignore @@ -25,4 +25,8 @@ bin/ .DS_Store -dist/ \ No newline at end of file +node_modules/ +cmd/web/dist/ +.test_storage/ +.test_download/ +/extensions \ No newline at end of file diff --git a/README.md b/README.md index 154f4c6d..21daefb0 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![Codecov](https://codecov.io/gh/GopeedLab/gopeed/branch/main/graph/badge.svg)](https://codecov.io/gh/GopeedLab/gopeed) [![Release](https://img.shields.io/github/release/GopeedLab/gopeed.svg)](https://github.com/GopeedLab/gopeed/releases) [![Download](https://img.shields.io/github/downloads/GopeedLab/gopeed/total.svg)](https://github.com/GopeedLab/gopeed/releases) -[![Donate](https://img.shields.io/badge/%24-donate-ff69b4.svg)](https://github.com/GopeedLab/gopeed/blob/main/.donate/index.md#donate) +[![Donate](https://img.shields.io/badge/%24-donate-ff69b4.svg)](https://docs.gopeed.com/donate) [![Discord](https://img.shields.io/discord/1037992631881449472?label=Discord&logo=discord&style=social)](https://discord.gg/ZUJqJrwCGB) ![](_docs/img/banner.png) @@ -61,7 +61,7 @@ When the docker container is running, you can access the web page through `http: ## Donate -If you like this project, please consider [donating](/.donate/index.md#donate) to support the development of this project, thank you! +If you like this project, please consider [donating](https://docs.gopeed.com/donate) to support the development of this project, thank you! ## Showcase diff --git a/README_ja-JP.md b/README_ja-JP.md index 2f5f4965..42bce9ba 100644 --- a/README_ja-JP.md +++ b/README_ja-JP.md @@ -2,7 +2,7 @@ [![Codecov](https://codecov.io/gh/GopeedLab/gopeed/branch/main/graph/badge.svg)](https://codecov.io/gh/GopeedLab/gopeed) [![Release](https://img.shields.io/github/release/GopeedLab/gopeed.svg)](https://github.com/GopeedLab/gopeed/releases) [![Download](https://img.shields.io/github/downloads/GopeedLab/gopeed/total.svg)](https://github.com/GopeedLab/gopeed/releases) -[![Donate](https://img.shields.io/badge/%24-donate-ff69b4.svg)](https://github.com/GopeedLab/gopeed/blob/main/.donate/index.md#donate) +[![Donate](https://img.shields.io/badge/%24-donate-ff69b4.svg)](https://docs.gopeed.com/donate) [![Discord](https://img.shields.io/discord/1037992631881449472?label=Discord&logo=discord&style=social)](https://discord.gg/ZUJqJrwCGB) ![](_docs/img/banner.png) diff --git a/README_zh-CN.md b/README_zh-CN.md index 06bb7d9f..bc1864f5 100644 --- a/README_zh-CN.md +++ b/README_zh-CN.md @@ -2,7 +2,7 @@ [![Codecov](https://codecov.io/gh/GopeedLab/gopeed/branch/main/graph/badge.svg)](https://codecov.io/gh/GopeedLab/gopeed) [![Release](https://img.shields.io/github/release/GopeedLab/gopeed.svg)](https://github.com/GopeedLab/gopeed/releases) [![Download](https://img.shields.io/github/downloads/GopeedLab/gopeed/total.svg)](https://github.com/GopeedLab/gopeed/releases) -[![Donate](https://img.shields.io/badge/%24-donate-ff69b4.svg)](https://github.com/GopeedLab/gopeed/blob/main/.donate/index.md#donate) +[![Donate](https://img.shields.io/badge/%24-donate-ff69b4.svg)](https://docs.gopeed.com/zh/donate) [![Discord](https://img.shields.io/discord/1037992631881449472?label=Discord&logo=discord&style=social)](https://discord.gg/ZUJqJrwCGB) ![](_docs/img/banner.png) @@ -59,9 +59,9 @@ docker-compose up -d 当 docker 容器运行时,可以通过 `http://localhost:9999` 访问 web 页面。 -## 打赏 +## 赞助 -如果觉得项目对你有帮助,请考虑[打赏](/.donate/index.md#donate)以支持这个项目的发展,非常感谢! +如果觉得项目对你有帮助,请考虑[赞助](https://docs.gopeed.com/zh/donate)以支持这个项目的发展,非常感谢! ## 界面展示 diff --git a/bind/desktop/main.go b/bind/desktop/main.go index 37e3cd31..5b63770f 100644 --- a/bind/desktop/main.go +++ b/bind/desktop/main.go @@ -15,6 +15,7 @@ func Start(cfg *C.char) (int, *C.char) { if err := json.Unmarshal([]byte(C.GoString(cfg)), &config); err != nil { return 0, C.CString(err.Error()) } + config.ProductionMode = true realPort, err := rest.Start(&config) if err != nil { return 0, C.CString(err.Error()) diff --git a/bind/mobile/main.go b/bind/mobile/main.go index 451f65a6..0a81ba53 100644 --- a/bind/mobile/main.go +++ b/bind/mobile/main.go @@ -13,6 +13,7 @@ func Start(cfg string) (int, error) { if err := json.Unmarshal([]byte(cfg), &config); err != nil { return 0, err } + config.ProductionMode = true return rest.Start(&config) } diff --git a/cmd/api/main.go b/cmd/api/main.go index 4c87cb9e..fbbb7d17 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -1,11 +1,11 @@ package main import ( - _ "embed" "github.com/GopeedLab/gopeed/cmd" "github.com/GopeedLab/gopeed/pkg/rest/model" ) +// only for local development func main() { cfg := &model.StartConfig{ Network: "tcp", diff --git a/cmd/web/flags.go b/cmd/web/flags.go index f894eb43..52de292f 100644 --- a/cmd/web/flags.go +++ b/cmd/web/flags.go @@ -28,8 +28,6 @@ func parse() *args { cliArgs.configPath = flag.String("c", "./config.json", "Config file path") flag.Parse() - fmt.Println("parse") - // args priority: config file > cli args cfgArgs := loadConfig(*cliArgs.configPath) if cfgArgs.Address == nil { diff --git a/cmd/web/main.go b/cmd/web/main.go index 3b01870d..7858ac31 100644 --- a/cmd/web/main.go +++ b/cmd/web/main.go @@ -30,15 +30,16 @@ func main() { } cfg := &model.StartConfig{ - Network: "tcp", - Address: fmt.Sprintf("%s:%d", *args.Address, *args.Port), - Storage: model.StorageBolt, - ApiToken: *args.ApiToken, - WebEnable: true, - WebFS: sub, - WebBasicAuth: webBasicAuth, + Network: "tcp", + Address: fmt.Sprintf("%s:%d", *args.Address, *args.Port), + Storage: model.StorageBolt, + ApiToken: *args.ApiToken, + ProductionMode: true, + WebEnable: true, + WebFS: sub, + WebBasicAuth: webBasicAuth, } - cmd.Start(cfg) + cmd.Start(cfg, true) } func isNotBlank(str *string) bool { diff --git a/go.mod b/go.mod index 4c479b4b..6d38757b 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,13 @@ module github.com/GopeedLab/gopeed -go 1.20 +go 1.19 require ( github.com/anacrolix/missinggo/v2 v2.7.2-0.20230527121029-a582b4f397b9 github.com/anacrolix/torrent v1.52.5 + github.com/dop251/goja v0.0.0-20230828202809-3dbe69dd2b8e + github.com/dop251/goja_nodejs v0.0.0-20230821135201-94e508132562 + github.com/go-git/go-git/v5 v5.8.1 github.com/gorilla/handlers v1.5.1 github.com/gorilla/mux v1.8.0 github.com/matoous/go-nanoid/v2 v2.0.0 @@ -15,7 +18,11 @@ require ( require ( crawshaw.io/sqlite v0.3.3-0.20220618202545-d1964889ea3c // indirect + dario.cat/mergo v1.0.0 // indirect + github.com/Microsoft/go-winio v0.6.1 // indirect + github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95 // indirect github.com/RoaringBitmap/roaring v1.2.3 // indirect + github.com/acomagu/bufpipe v1.0.4 // indirect github.com/ajwerner/btree v0.0.0-20211221152037-f427b3e689c0 // indirect github.com/alecthomas/atomic v0.1.0-alpha2 // indirect github.com/anacrolix/chansync v0.3.0 // indirect @@ -36,17 +43,29 @@ require ( github.com/benbjohnson/immutable v0.3.0 // indirect github.com/bits-and-blooms/bitset v1.2.2 // indirect github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 // indirect + github.com/cloudflare/circl v1.3.3 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dlclark/regexp2 v1.7.0 // indirect github.com/dustin/go-humanize v1.0.0 // indirect github.com/edsrzf/mmap-go v1.1.0 // indirect + github.com/emirpasic/gods v1.18.1 // indirect github.com/felixge/httpsnoop v1.0.1 // indirect + github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect + github.com/go-git/go-billy/v5 v5.4.1 // indirect github.com/go-logr/logr v1.2.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/google/btree v1.1.2 // indirect + github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect github.com/google/uuid v1.3.0 // indirect github.com/gorilla/websocket v1.5.0 // indirect github.com/huandu/xstrings v1.3.2 // indirect + github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect + github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/lispad/go-generics-tools v1.1.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect github.com/mschoch/smat v0.2.0 // indirect github.com/pion/datachannel v1.5.2 // indirect github.com/pion/dtls/v2 v2.2.4 // indirect @@ -66,14 +85,23 @@ require ( github.com/pion/turn/v2 v2.0.8 // indirect github.com/pion/udp v0.1.4 // indirect github.com/pion/webrtc/v3 v3.1.42 // indirect + github.com/pjbgf/sha1cd v0.3.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417 // indirect + github.com/rs/zerolog v1.31.0 // indirect + github.com/sergi/go-diff v1.1.0 // indirect + github.com/skeema/knownhosts v1.2.0 // indirect github.com/tidwall/btree v1.6.0 // indirect + github.com/xanzy/ssh-agent v0.3.3 // indirect go.opentelemetry.io/otel v1.8.0 // indirect go.opentelemetry.io/otel/trace v1.8.0 // indirect - golang.org/x/crypto v0.5.0 // indirect + golang.org/x/crypto v0.11.0 // indirect golang.org/x/exp v0.0.0-20220613132600-b0d781184e0d // indirect - golang.org/x/net v0.7.0 // indirect + golang.org/x/mod v0.8.0 // indirect + golang.org/x/net v0.12.0 // indirect golang.org/x/sys v0.13.0 // indirect + golang.org/x/text v0.11.0 // indirect golang.org/x/time v0.0.0-20220609170525-579cf78fd858 // indirect + golang.org/x/tools v0.6.0 // indirect + gopkg.in/warnings.v0 v0.1.2 // indirect ) diff --git a/go.sum b/go.sum index 6a276bf3..c67af1db 100644 --- a/go.sum +++ b/go.sum @@ -5,8 +5,16 @@ crawshaw.io/iox v0.0.0-20181124134642-c51c3df30797/go.mod h1:sXBiorCo8c46JlQV3oX crawshaw.io/sqlite v0.3.2/go.mod h1:igAO5JulrQ1DbdZdtVq48mnZUBAPOeFzer7VhDWNtW4= crawshaw.io/sqlite v0.3.3-0.20220618202545-d1964889ea3c h1:wvzox0eLO6CKQAMcOqz7oH3UFqMpMmK7kwmwV+22HIs= crawshaw.io/sqlite v0.3.3-0.20220618202545-d1964889ea3c/go.mod h1:igAO5JulrQ1DbdZdtVq48mnZUBAPOeFzer7VhDWNtW4= +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= filippo.io/edwards25519 v1.0.0-rc.1 h1:m0VOOB23frXZvAOK44usCgLWvtsxIoMCTBGJZlpmGfU= +filippo.io/edwards25519 v1.0.0-rc.1/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95 h1:KLq8BE0KwCL+mmXnjLWEAOYO+2l2AE4YMmqG1ZpZHBs= +github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= github.com/RoaringBitmap/roaring v0.4.7/go.mod h1:8khRDP4HmeXns4xIj9oGrKSz7XTQiJx2zgh7AcNke4w= github.com/RoaringBitmap/roaring v0.4.17/go.mod h1:D3qVegWTmfCaX4Bl5CrBE9hfrSrrXIr8KVNvRsDi1NI= github.com/RoaringBitmap/roaring v0.4.23/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06Mq5mKs52e1TwOo= @@ -14,6 +22,8 @@ github.com/RoaringBitmap/roaring v1.2.3 h1:yqreLINqIrX22ErkKI0vY47/ivtJr6n+kMhVO github.com/RoaringBitmap/roaring v1.2.3/go.mod h1:plvDsJQpxOC5bw8LRteu/MLWHsHez/3y6cubLI4/1yE= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= +github.com/acomagu/bufpipe v1.0.4 h1:e3H4WUzM3npvo5uv95QuJM3cQspFNtFBzvJ2oNjKIDQ= +github.com/acomagu/bufpipe v1.0.4/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4= github.com/ajwerner/btree v0.0.0-20211221152037-f427b3e689c0 h1:byYvvbfSo3+9efR4IeReh77gVs4PnNDR3AMOE9NJ7a0= github.com/ajwerner/btree v0.0.0-20211221152037-f427b3e689c0/go.mod h1:q37NoqncT41qKc048STsifIt69LfUJ8SrWWcz/yam5k= github.com/alecthomas/assert/v2 v2.0.0-alpha3 h1:pcHeMvQ3OMstAWgaeaXIAL8uzB9xMm2zlxt+/4ml8lk= @@ -24,6 +34,7 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alexflint/go-scalar v1.1.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o= github.com/anacrolix/chansync v0.3.0 h1:lRu9tbeuw3wl+PhMu/r+JJCRu5ArFXIluOgdF0ao6/U= github.com/anacrolix/chansync v0.3.0/go.mod h1:DZsatdsdXxD0WiwcGl0nJVwyjCKMDv+knl1q2iBjA2k= github.com/anacrolix/dht/v2 v2.19.2-0.20221121215055-066ad8494444 h1:8V0K09lrGoeT2KRJNOtspA7q+OMxGwQqK/Ug0IiaaRE= @@ -63,6 +74,7 @@ github.com/anacrolix/mmsg v1.0.0 h1:btC7YLjOn29aTUAExJiVUhQOuf/8rhm+/nWCMAnL3Hg= github.com/anacrolix/mmsg v1.0.0/go.mod h1:x8kRaJY/dCrY9Al0PEcj1mb/uFHwP6GCJ9fLl4thEPc= github.com/anacrolix/multiless v0.3.0 h1:5Bu0DZncjE4e06b9r1Ap2tUY4Au0NToBP5RpuEngSis= github.com/anacrolix/multiless v0.3.0/go.mod h1:TrCLEZfIDbMVfLoQt5tOoiBS/uq4y8+ojuEVVvTNPX4= +github.com/anacrolix/publicip v0.2.0/go.mod h1:67G1lVkLo8UjdEcJkwScWVTvlJ35OCDsRJoWXl/wi4g= github.com/anacrolix/stm v0.2.0/go.mod h1:zoVQRvSiGjGoTmbM0vSLIiaKjWtNPeTvXUSdJQA4hsg= github.com/anacrolix/stm v0.4.0 h1:tOGvuFwaBjeu1u9X1eIh9TX8OEedEiEQ1se1FjhFnXY= github.com/anacrolix/stm v0.4.0/go.mod h1:GCkwqWoAsP7RfLW+jw+Z0ovrt2OO7wRzcTtFYMYY5t8= @@ -73,13 +85,16 @@ github.com/anacrolix/sync v0.4.0/go.mod h1:BbecHL6jDSExojhNtgTFSBcdGerzNc64tz3DC github.com/anacrolix/tagflag v0.0.0-20180109131632-2146c8d41bf0/go.mod h1:1m2U/K6ZT+JZG0+bdMK6qauP49QT4wE5pmhJXOKKCHw= github.com/anacrolix/tagflag v1.0.0/go.mod h1:1m2U/K6ZT+JZG0+bdMK6qauP49QT4wE5pmhJXOKKCHw= github.com/anacrolix/tagflag v1.1.0/go.mod h1:Scxs9CV10NQatSmbyjqmqmeQNwGzlNe0CMUMIxqHIG8= +github.com/anacrolix/tagflag v1.3.0/go.mod h1:Scxs9CV10NQatSmbyjqmqmeQNwGzlNe0CMUMIxqHIG8= github.com/anacrolix/torrent v1.52.5 h1:jWowdx+EU6zFVfBwmnL0d3H4J6vTFEGOrHI35YdfIT8= github.com/anacrolix/torrent v1.52.5/go.mod h1:CcM8oPMYye5J42cSqJrmUpqwRFgSsJQ1jCEHwygqnqQ= github.com/anacrolix/upnp v0.1.3-0.20220123035249-922794e51c96 h1:QAVZ3pN/J4/UziniAhJR2OZ9Ox5kOY2053tBbbqUPYA= github.com/anacrolix/upnp v0.1.3-0.20220123035249-922794e51c96/go.mod h1:Wa6n8cYIdaG35x15aH3Zy6d03f7P728QfdcDeD/IEOs= github.com/anacrolix/utp v0.1.0 h1:FOpQOmIwYsnENnz7tAGohA+r6iXpRjrq8ssKSre2Cp4= github.com/anacrolix/utp v0.1.0/go.mod h1:MDwc+vsGEq7RMw6lr2GKOEqjWny5hO5OZXRVNaBJ2Dk= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/benbjohnson/immutable v0.2.0/go.mod h1:uc6OHo6PN2++n98KHLxW8ef4W42ylHiQSENghE1ezxI= @@ -95,13 +110,31 @@ github.com/bradfitz/iter v0.0.0-20140124041915-454541ec3da2/go.mod h1:PyRFw1Lt2w github.com/bradfitz/iter v0.0.0-20190303215204-33e6a9893b0c/go.mod h1:PyRFw1Lt2wKX4ZVSQ2mk+PeDa1rxyObEDlApuIsUKuo= github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 h1:GKTyiRCL6zVf5wWaqKnf+7Qs6GbEPfd4iMOitWzXJx8= github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8/go.mod h1:spo1JLcs67NmW1aVLEgtA8Yy1elc+X8y5SRW1sFW4Og= +github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY= +github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic= +github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs= +github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= +github.com/dlclark/regexp2 v1.7.0 h1:7lJfhqlPssTb1WQx4yvTHN0uElPEv52sbaECrAQxjAo= +github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= +github.com/dop251/goja v0.0.0-20211022113120-dc8c55024d06/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk= +github.com/dop251/goja v0.0.0-20230531210528-d7324b2d74f7/go.mod h1:QMWlm50DNe14hD7t24KEqZuUdC9sOTy8W6XbCU1mlw4= +github.com/dop251/goja v0.0.0-20230828202809-3dbe69dd2b8e h1:UvQD6hTSfeM6hhTQ24Dlw2RppP05W7SWbWb6kubJAog= +github.com/dop251/goja v0.0.0-20230828202809-3dbe69dd2b8e/go.mod h1:QMWlm50DNe14hD7t24KEqZuUdC9sOTy8W6XbCU1mlw4= +github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y= +github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d/go.mod h1:DngW8aVqWbuLRMHItjPUyqdj+HWPvnQe8V8y1nDpIbM= +github.com/dop251/goja_nodejs v0.0.0-20230821135201-94e508132562 h1:ObbB2tzHWWAxzsG5futqeq2Ual2zYlo/+eMkSc5sn8w= +github.com/dop251/goja_nodejs v0.0.0-20230821135201-94e508132562/go.mod h1:X2TOTJ+Uamd454RFp7ig2tmP3hQg0Z2Qk8gbVQmU0mk= github.com/dustin/go-humanize v0.0.0-20180421182945-02af3965c54e/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= @@ -110,18 +143,30 @@ github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1 github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= github.com/edsrzf/mmap-go v1.1.0 h1:6EUwBLQ/Mcr1EYLE4Tn1VdW1A4ckqCQWZBw8Hr0kjpQ= github.com/edsrzf/mmap-go v1.1.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q= +github.com/elazarl/goproxy v0.0.0-20221015165544-a0805db90819 h1:RIB4cRk+lBqKK3Oy0r2gRX4ui7tuhiZq2SuTtTCi0/0= +github.com/elliotchance/orderedmap v1.4.0/go.mod h1:wsDwEaX5jEoyhbs7x93zk2H/qv0zwuhg4inXhDkYqys= +github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ= github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/frankban/quicktest v1.9.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y= github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY= github.com/glycerine/go-unsnap-stream v0.0.0-20180323001048-9f0cb55181dd/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE= github.com/glycerine/go-unsnap-stream v0.0.0-20181221182339-f9677308dec2/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE= github.com/glycerine/go-unsnap-stream v0.0.0-20190901134440-81cf024a9e0a/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE= github.com/glycerine/goconvey v0.0.0-20180728074245-46e3a41ad493/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24= github.com/glycerine/goconvey v0.0.0-20190315024820-982ee783a72e/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24= github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= +github.com/go-git/go-billy/v5 v5.4.1 h1:Uwp5tDRkPr+l/TnbHOQzp+tmJfLceOlbVucgpTz8ix4= +github.com/go-git/go-billy/v5 v5.4.1/go.mod h1:vjbugF6Fz7JIflbVpl1hJsGjSHNltrSw45YK/ukIvQg= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20230305113008-0c11038e723f h1:Pz0DHeFij3XFhoBRGUDPzSJ+w2UcK5/0JvF8DRI58r8= +github.com/go-git/go-git/v5 v5.8.1 h1:Zo79E4p7TRk0xoRgMq0RShiTHGKcKI4+DI6BfJc/Q+A= +github.com/go-git/go-git/v5 v5.8.1/go.mod h1:FHFuoD6yGz5OSKEBK+aWN9Oah0q54Jxl0abmj6GnqAo= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= @@ -131,13 +176,18 @@ github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU= +github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -163,6 +213,8 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20230207041349-798e818bf904 h1:4/hN5RUoecvl+RmJRE2YxKWtnnQls6rQjjW5oV7qg2U= +github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= @@ -186,16 +238,23 @@ github.com/huandu/xstrings v1.3.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw= github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= +github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -207,6 +266,13 @@ github.com/lispad/go-generics-tools v1.1.0/go.mod h1:2csd1EJljo/gy5qG4khXol7ivCP github.com/matoous/go-nanoid v1.5.0/go.mod h1:zyD2a71IubI24efhpvkJz+ZwfwagzgSO6UNiFsZKN7U= github.com/matoous/go-nanoid/v2 v2.0.0 h1:d19kur2QuLeHmJBkvYkFdhFBzLoo1XVm2GgTpL+9Tj0= github.com/matoous/go-nanoid/v2 v2.0.0/go.mod h1:FtS4aGPVfEkxKxhdWPAspZpZSh1cOjtM7Ej/So3hR0g= +github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A= +github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -215,7 +281,9 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg= github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM= github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw= +github.com/multiformats/go-base36 v0.1.0/go.mod h1:kFGE83c6s80PklsHO9sRn2NCoffoRdUUOENyW/Vv6sM= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= @@ -273,6 +341,8 @@ github.com/pion/udp v0.1.4 h1:OowsTmu1Od3sD6i3fQUJxJn2fEvJO6L1TidgadtbTI8= github.com/pion/udp v0.1.4/go.mod h1:G8LDo56HsFwC24LIcnT4YIDU5qcB6NepqqjP0keL2us= github.com/pion/webrtc/v3 v3.1.42 h1:wJEQFIXVanptnQcHOLTuIo4AtGB2+mG2x4OhIhnITOA= github.com/pion/webrtc/v3 v3.1.42/go.mod h1:ffD9DulDrPxyWvDPUIPAOSAWx9GUlOExiJPf7cCcMLA= +github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= +github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -297,16 +367,25 @@ github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsT github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/prometheus/procfs v0.0.11/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417 h1:Lt9DzQALzHoDwMBGJ6v8ObDPR0dzr2a6sXTB1Fq7IHs= github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417/go.mod h1:qe5TWALJ8/a1Lqznoc5BDHpYX/8HU60Hm2AwRmqzxqA= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A= +github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 h1:GHRpF1pTW19a8tTFrMLUcfWwyC0pnifVo2ClaLq+hP8= github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46/go.mod h1:uAQ5PCi+MFsC7HjREoAz1BU+Mq60+05gifQSsHSDG/8= github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw= +github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= +github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/skeema/knownhosts v1.2.0 h1:h9r9cf0+u7wSE+M183ZtMGgOJKiL96brpaz5ekfJCpM= +github.com/skeema/knownhosts v1.2.0/go.mod h1:g4fPeYpque7P0xefxtGzV81ihjC8sX2IqpAoNkjxbMo= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v0.0.0-20190215210624-980c5ac6f3ac/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= @@ -335,6 +414,8 @@ github.com/virtuald/go-paniclog v0.0.0-20190812204905-43a7fa316459 h1:x9pIfbdIjn github.com/virtuald/go-paniclog v0.0.0-20190812204905-43a7fa316459/go.mod h1:nFvuG3SWu3VWqobG3cX8nt57wXU0OOFapeCs/8axIuM= github.com/willf/bitset v1.1.9/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= +github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= +github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.etcd.io/bbolt v1.3.7-0.20231005155708-db45d0d6b1f6 h1:NkHVT1GBVV9hUj1iWglFxAuNlvLHELFn71U2H7aOIog= @@ -354,8 +435,12 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220516162934-403b01795ae8/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= +golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= +golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20220613132600-b0d781184e0d h1:vtUKgx8dahOomfFzLREU8nSv25YHnTgLBn4rDnWZdU0= golang.org/x/exp v0.0.0-20220613132600-b0d781184e0d/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA= @@ -365,6 +450,8 @@ golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTk golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -390,9 +477,14 @@ golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su golang.org/x/net v0.0.0-20220531201128-c960675eff93/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= -golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50= +golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -403,6 +495,7 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -416,6 +509,7 @@ golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -424,31 +518,48 @@ golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220608164250-635b8c9b7f68/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= +golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/time v0.0.0-20220609170525-579cf78fd858 h1:Dpdu/EMxGMFgq0CeYMh4fazTD2vtlZRYE7wyynxJb9U= golang.org/x/time v0.0.0-20220609170525-579cf78fd858/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -460,6 +571,8 @@ golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -485,14 +598,19 @@ gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLks gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/internal/controller/controller.go b/internal/controller/controller.go index c33d2c72..f99b8456 100644 --- a/internal/controller/controller.go +++ b/internal/controller/controller.go @@ -1,7 +1,6 @@ package controller import ( - "errors" "os" "path/filepath" ) @@ -34,7 +33,7 @@ func (c *DefaultFileController) Touch(name string, size int64) (file *os.File, e return } file, err = os.Create(name) - if err != nil && !errors.Is(err, os.ErrExist) { + if err != nil { return } if size > 0 { diff --git a/internal/fetcher/fetcher.go b/internal/fetcher/fetcher.go index 0624fc73..a31929e1 100644 --- a/internal/fetcher/fetcher.go +++ b/internal/fetcher/fetcher.go @@ -12,14 +12,13 @@ type Fetcher interface { // Name return the name of the protocol. Name() string - Setup(ctl *controller.Controller) error + Setup(ctl *controller.Controller) // Resolve resource info from request Resolve(req *base.Request) error // Create ready to download, but not started Create(opts *base.Options) error Start() error Pause() error - Continue() error Close() error // Meta returns the meta information of the download. @@ -37,12 +36,25 @@ type FetcherMeta struct { Opts *base.Options `json:"opts"` } -func (m *FetcherMeta) Filepath(file *base.FileInfo) string { - finalName := m.Opts.Name - if finalName == "" { - finalName = file.Name +// FolderPath return the folder path of the meta info. +func (m *FetcherMeta) FolderPath() string { + // check if rename folder + folder := m.Res.Name + if m.Opts.Name != "" { + folder = m.Opts.Name } - return path.Join(m.Opts.Path, m.Res.RootDir, finalName) + return path.Join(m.Opts.Path, folder) +} + +// SingleFilepath return the single file path of the meta info. +func (m *FetcherMeta) SingleFilepath() string { + // check if rename file + file := m.Res.Files[0] + fileName := file.Name + if m.Opts.Name != "" { + fileName = m.Opts.Name + } + return path.Join(m.Opts.Path, file.Path, fileName) } // FetcherBuilder defines the interface for a fetcher builder. diff --git a/internal/logger/logger.go b/internal/logger/logger.go new file mode 100644 index 00000000..a85f3d38 --- /dev/null +++ b/internal/logger/logger.go @@ -0,0 +1,52 @@ +package logger + +import ( + "github.com/GopeedLab/gopeed/pkg/util" + "github.com/rs/zerolog" + "io" + "os" + "path/filepath" +) + +type Logger struct { + zerolog.Logger + logFile *os.File +} + +func (l *Logger) CLose() { + l.logFile.Close() +} + +// NewLogger create a new logger +func NewLogger(logFile bool, logPath string) *Logger { + var out io.Writer + if logFile { + // log to file + logDir := filepath.Dir(logPath) + if err := util.CreateDirIfNotExist(logDir); err != nil { + panic(err) + } + var ( + logfile *os.File + err error + ) + logfile, err = os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) + if err != nil { + panic(err) + } + out = logfile + } else { + out = os.Stdout + } + + logger := &Logger{} + if logFile { + logger.logFile = out.(*os.File) + } + logger.Logger = zerolog.New(zerolog.ConsoleWriter{ + NoColor: true, + Out: out, + TimeFormat: "2006-01-02 15:04:05", + }).With().Timestamp().Logger() + return logger +} diff --git a/internal/logger/logger_test.go b/internal/logger/logger_test.go new file mode 100644 index 00000000..0b71c9e1 --- /dev/null +++ b/internal/logger/logger_test.go @@ -0,0 +1,35 @@ +package logger + +import ( + "os" + "testing" +) + +func TestNewLogger(t *testing.T) { + logger := NewLogger(false, "") + logger.Info().Msg("test") +} + +func TestNewLoggerFile(t *testing.T) { + logPath := "./testdata/test.log" + logger := NewLogger(true, logPath) + defer func() { + logger.CLose() + os.Remove(logPath) + }() + logger.Info().Msg("test") + + // read log file, if not exist or empty, test fail. + file, err := os.Open(logPath) + if err != nil { + t.Fatal(err) + } + defer file.Close() + stat, err := file.Stat() + if err != nil { + t.Fatal(err) + } + if stat.Size() == 0 { + t.Fatal("log file is empty") + } +} diff --git a/internal/protocol/bt/fetcher.go b/internal/protocol/bt/fetcher.go index bad75c7e..8a804cab 100644 --- a/internal/protocol/bt/fetcher.go +++ b/internal/protocol/bt/fetcher.go @@ -9,7 +9,6 @@ import ( "github.com/GopeedLab/gopeed/pkg/util" "github.com/anacrolix/torrent" "github.com/anacrolix/torrent/metainfo" - "path" "path/filepath" "sync" "sync/atomic" @@ -38,7 +37,7 @@ func (f *Fetcher) Name() string { return "bt" } -func (f *Fetcher) Setup(ctl *controller.Controller) (err error) { +func (f *Fetcher) Setup(ctl *controller.Controller) { f.ctl = ctl if f.meta == nil { f.meta = &fetcher.FetcherMeta{} @@ -77,51 +76,17 @@ func (f *Fetcher) Resolve(req *base.Request) error { if err := f.addTorrent(req); err != nil { return err } - go func() { - // recycle unused torrent resource - time.Sleep(time.Minute * 3) - if !f.create.Load() { - f.torrentReady.Store(false) - f.safeDrop() - } - }() - res := &base.Resource{ - Name: f.torrent.Name(), - Range: true, - RootDir: f.torrent.Name(), - Files: make([]*base.FileInfo, len(f.torrent.Files())), - Hash: f.torrent.InfoHash().String(), - } - for i, file := range f.torrent.Files() { - res.Files[i] = &base.FileInfo{ - Name: filepath.Base(file.DisplayPath()), - Path: util.Dir(file.Path()), - Size: file.Length(), - } - res.Size += file.Length() - } + f.updateRes() f.meta.Req = req - f.meta.Res = res return nil } func (f *Fetcher) Create(opts *base.Options) (err error) { f.create.Store(true) f.meta.Opts = opts - if len(opts.SelectFiles) == 0 { - opts.SelectFiles = make([]int, 0) - for i := range f.torrent.Files() { - opts.SelectFiles = append(opts.SelectFiles, i) - } + if f.meta.Res != nil { + torrentDirMap[f.meta.Res.Hash] = f.meta.FolderPath() } - if opts.Path != "" { - torrentDirMap[f.meta.Res.Hash] = path.Join(f.meta.Opts.Path, f.meta.Res.RootDir) - if ft, ok := ftMap[f.meta.Res.Hash]; ok { - // reuse resolve fetcher - ft.setTorrentDir(torrentDirMap[f.meta.Res.Hash]) - } - } - f.progress = make(fetcher.Progress, len(f.meta.Opts.SelectFiles)) return nil } @@ -132,6 +97,9 @@ func (f *Fetcher) Start() (err error) { return } } + if ft, ok := ftMap[f.meta.Res.Hash]; ok { + ft.setTorrentDir(f.meta.FolderPath()) + } files := f.torrent.Files() if len(f.meta.Opts.SelectFiles) == len(files) { f.torrent.DownloadAll() @@ -150,10 +118,6 @@ func (f *Fetcher) Pause() (err error) { return } -func (f *Fetcher) Continue() (err error) { - return f.Start() -} - func (f *Fetcher) Close() (err error) { f.safeDrop() return nil @@ -217,6 +181,27 @@ func (f *Fetcher) Progress() fetcher.Progress { return f.progress } +func (f *Fetcher) updateRes() { + res := &base.Resource{ + Name: f.torrent.Name(), + Range: true, + Files: make([]*base.FileInfo, len(f.torrent.Files())), + Hash: f.torrent.InfoHash().String(), + } + for i, file := range f.torrent.Files() { + res.Files[i] = &base.FileInfo{ + Name: filepath.Base(file.DisplayPath()), + Path: util.Dir(file.Path()), + Size: file.Length(), + } + } + res.CalcSize() + f.meta.Res = res + if f.meta.Opts != nil { + f.meta.Opts.InitSelectFiles(len(res.Files)) + } +} + func (f *Fetcher) addTorrent(req *base.Request) (err error) { if err = f.initClient(); err != nil { return diff --git a/internal/protocol/bt/fetcher_test.go b/internal/protocol/bt/fetcher_test.go index 030d78a7..44a83804 100644 --- a/internal/protocol/bt/fetcher_test.go +++ b/internal/protocol/bt/fetcher_test.go @@ -33,10 +33,9 @@ func TestFetcher_Resolve_DataUri_Torrent(t *testing.T) { } want := &base.Resource{ - Name: "ubuntu-22.04-live-server-amd64.iso", - Size: 1466714112, - Range: true, - RootDir: "ubuntu-22.04-live-server-amd64.iso", + Name: "ubuntu-22.04-live-server-amd64.iso", + Size: 1466714112, + Range: true, Files: []*base.FileInfo{ { Name: "ubuntu-22.04-live-server-amd64.iso", @@ -69,10 +68,9 @@ func doResolve(t *testing.T, fetcher fetcher.Fetcher) { } want := &base.Resource{ - Name: "ubuntu-22.04-live-server-amd64.iso", - Size: 1466714112, - Range: true, - RootDir: "ubuntu-22.04-live-server-amd64.iso", + Name: "ubuntu-22.04-live-server-amd64.iso", + Size: 1466714112, + Range: true, Files: []*base.FileInfo{ { Name: "ubuntu-22.04-live-server-amd64.iso", diff --git a/internal/protocol/http/fetcher.go b/internal/protocol/http/fetcher.go index d53d0ae6..acf10403 100644 --- a/internal/protocol/http/fetcher.go +++ b/internal/protocol/http/fetcher.go @@ -3,6 +3,7 @@ package http import ( "bytes" "context" + "errors" "fmt" "github.com/GopeedLab/gopeed/internal/controller" "github.com/GopeedLab/gopeed/internal/fetcher" @@ -33,7 +34,6 @@ func (re *RequestError) Error() string { } type chunk struct { - Status base.Status Begin int64 End int64 Downloaded int64 @@ -41,9 +41,8 @@ type chunk struct { func newChunk(begin int64, end int64) *chunk { return &chunk{ - Status: base.DownloadStatusReady, - Begin: begin, - End: end, + Begin: begin, + End: end, } } @@ -52,23 +51,20 @@ type Fetcher struct { doneCh chan error meta *fetcher.FetcherMeta - status base.Status chunks []*chunk - file *os.File - ctx context.Context - cancel context.CancelFunc - pauseCh chan interface{} + file *os.File + cancel context.CancelFunc + eg *errgroup.Group } func (f *Fetcher) Name() string { return "http" } -func (f *Fetcher) Setup(ctl *controller.Controller) (err error) { +func (f *Fetcher) Setup(ctl *controller.Controller) { f.ctl = ctl f.doneCh = make(chan error, 1) - f.pauseCh = make(chan interface{}, 1) if f.meta == nil { f.meta = &fetcher.FetcherMeta{} } @@ -139,10 +135,9 @@ func (f *Fetcher) Resolve(req *base.Request) error { } // unknown file filePath if file.Name == "" || file.Name == "/" || file.Name == "." { - file.Name = "unknown" + file.Name = httpReq.URL.Hostname() } res.Files = append(res.Files, file) - res.Name = file.Name f.meta.Req = req f.meta.Res = res return nil @@ -150,7 +145,6 @@ func (f *Fetcher) Resolve(req *base.Request) error { func (f *Fetcher) Create(opts *base.Options) error { f.meta.Opts = opts - f.status = base.DownloadStatusReady if err := base.ParseReqExtra[fhttp.ReqExtra](f.meta.Req); err != nil { return err @@ -178,44 +172,38 @@ func (f *Fetcher) Create(opts *base.Options) error { } func (f *Fetcher) Start() (err error) { - // 创建文件 - name := f.filepath() - f.file, err = f.ctl.Touch(name, f.meta.Res.Size) + name := f.meta.SingleFilepath() + // if file not exist, create it, else open it + _, err = os.Stat(name) + if err != nil { + if os.IsNotExist(err) { + f.file, err = f.ctl.Touch(name, f.meta.Res.Size) + } else { + return + } + } else { + f.file, err = os.OpenFile(name, os.O_RDWR, os.ModeAppend) + } if err != nil { return err } - f.status = base.DownloadStatusRunning - f.chunks = f.splitChunk() + if f.chunks == nil { + f.chunks = f.splitChunk() + } f.fetch() return } func (f *Fetcher) Pause() (err error) { - if base.DownloadStatusRunning != f.status { - return - } - f.status = base.DownloadStatusPause - if f.cancel != nil { f.cancel() - <-f.pauseCh + // wait for pause handle complete + f.eg.Wait() + f.file.Close() } return } -func (f *Fetcher) Continue() (err error) { - if base.DownloadStatusRunning == f.status || base.DownloadStatusDone == f.status { - return - } - f.status = base.DownloadStatusRunning - f.file, err = os.OpenFile(f.filepath(), os.O_RDWR, os.ModeAppend) - if err != nil { - return err - } - f.fetch() - return -} - func (f *Fetcher) Close() (err error) { if err = f.Pause(); err != nil { return @@ -243,41 +231,32 @@ func (f *Fetcher) Wait() (err error) { return <-f.doneCh } -func (f *Fetcher) filepath() string { - return f.meta.Filepath(f.meta.Res.Files[0]) -} - func (f *Fetcher) fetch() { - f.ctx, f.cancel = context.WithCancel(context.Background()) - eg, _ := errgroup.WithContext(f.ctx) + var ctx context.Context + ctx, f.cancel = context.WithCancel(context.Background()) + f.eg, _ = errgroup.WithContext(ctx) for i := 0; i < len(f.chunks); i++ { i := i - eg.Go(func() error { - return f.fetchChunk(i) + f.eg.Go(func() error { + return f.fetchChunk(i, ctx) }) } go func() { - err := eg.Wait() - // 下载停止,关闭文件句柄 - f.file.Close() - if f.status == base.DownloadStatusPause { - f.pauseCh <- nil - } else { - if err != nil { - f.status = base.DownloadStatusError - } else { - f.status = base.DownloadStatusDone - } - f.doneCh <- err + err := f.eg.Wait() + // check if canceled + if errors.Is(err, context.Canceled) { + return } + f.file.Close() + f.doneCh <- err }() } -func (f *Fetcher) fetchChunk(index int) (err error) { +func (f *Fetcher) fetchChunk(index int, ctx context.Context) (err error) { chunk := f.chunks[index] - httpReq, err := buildRequest(f.ctx, f.meta.Req) + httpReq, err := buildRequest(ctx, f.meta.Req) if err != nil { return err } @@ -287,14 +266,10 @@ func (f *Fetcher) fetchChunk(index int) (err error) { ) // 重试5次 for i := 0; i < 5; i++ { - // 如果下载完成直接返回 - if chunk.Status == base.DownloadStatusDone { + // if chunk is completed, return + if f.meta.Res.Range && chunk.Downloaded >= chunk.End-chunk.Begin+1 { return } - // 如果已暂停直接跳出 - if f.status == base.DownloadStatusPause { - break - } var ( resp *http.Response retry bool @@ -317,7 +292,7 @@ func (f *Fetcher) fetchChunk(index int) (err error) { return nil }() if err != nil { - // 请求失败重试 + // retry request continue } // 请求成功就重置错误次数,连续失败5次才终止 @@ -347,19 +322,6 @@ func (f *Fetcher) fetchChunk(index int) (err error) { break } } - - if err != nil { - chunk.Status = base.DownloadStatusError - return - } - - isComplete := f.meta.Res.Range && chunk.Downloaded >= chunk.End-chunk.Begin+1 - if f.status == base.DownloadStatusPause && !isComplete { - chunk.Status = base.DownloadStatusPause - return - } - - chunk.Status = base.DownloadStatusDone return } @@ -474,7 +436,6 @@ func (fb *FetcherBuilder) Restore() (v any, f func(meta *fetcher.FetcherMeta, v fb := &FetcherBuilder{} fetcher := fb.Build().(*Fetcher) fetcher.meta = meta - fetcher.status = base.DownloadStatusPause base.ParseReqExtra[fhttp.ReqExtra](fetcher.meta.Req) base.ParseOptsExtra[fhttp.OptsExtra](fetcher.meta.Opts) if len(fd.Chunks) == 0 { diff --git a/internal/protocol/http/fetcher_test.go b/internal/protocol/http/fetcher_test.go index f9290b19..7704f0fb 100644 --- a/internal/protocol/http/fetcher_test.go +++ b/internal/protocol/http/fetcher_test.go @@ -16,7 +16,6 @@ import ( func TestFetcher_Resolve(t *testing.T) { testResolve(test.StartTestFileServer, test.BuildName, &base.Resource{ - Name: test.BuildName, Size: test.BuildSize, Range: true, Files: []*base.FileInfo{ @@ -27,7 +26,6 @@ func TestFetcher_Resolve(t *testing.T) { }, }, t) testResolve(test.StartTestCustomServer, "disposition", &base.Resource{ - Name: test.BuildName, Size: test.BuildSize, Range: false, Files: []*base.FileInfo{ @@ -38,7 +36,6 @@ func TestFetcher_Resolve(t *testing.T) { }, }, t) testResolve(test.StartTestCustomServer, test.BuildName, &base.Resource{ - Name: test.BuildName, Size: 0, Range: false, Files: []*base.FileInfo{ @@ -48,6 +45,17 @@ func TestFetcher_Resolve(t *testing.T) { }, }, }, t) + + fetcher := buildFetcher() + err := fetcher.Resolve(&base.Request{ + URL: "http://github.com", + }) + if err != nil { + t.Fatal(err) + } + if fetcher.Meta().Res.Files[0].Name != "github.com" { + t.Errorf("Resolve() got = %v, want %v", fetcher.Meta().Res, "github.com") + } } func testResolve(startTestServer func() net.Listener, path string, want *base.Resource, t *testing.T) { @@ -125,7 +133,9 @@ func TestFetcher_DownloadResume(t *testing.T) { } func TestFetcher_Config(t *testing.T) { - fetcher := doDownloadReady(buildConfigFetcher(), test.StartTestFileServer(), 0, t) + listener := test.StartTestFileServer() + defer listener.Close() + fetcher := doDownloadReady(buildConfigFetcher(), listener, 0, t) err := fetcher.Start() if err != nil { t.Fatal(err) @@ -222,7 +232,7 @@ func downloadContinue(listener net.Listener, connections int, t *testing.T) { t.Fatal(err) } time.Sleep(time.Millisecond * 50) - if err := fetcher.Continue(); err != nil { + if err := fetcher.Start(); err != nil { t.Fatal(err) } err = fetcher.Wait() @@ -270,7 +280,7 @@ func downloadResume(listener net.Listener, connections int, t *testing.T) { t.Fatal(err) } fetcher.Setup(controller.NewController()) - fetcher.Continue() + fetcher.Start() err = fetcher.Wait() if err != nil { diff --git a/internal/test/httptest.go b/internal/test/httptest.go index 7f6aab47..07fb0804 100644 --- a/internal/test/httptest.go +++ b/internal/test/httptest.go @@ -1,6 +1,7 @@ package test import ( + "context" "encoding/json" "fmt" "io" @@ -17,8 +18,10 @@ const ( Dir = "./" BuildFile = Dir + BuildName - DownloadName = "download.data" - DownloadFile = Dir + DownloadName + DownloadName = "download.data" + DownloadRename = "download (1).data" + DownloadFile = Dir + DownloadName + DownloadRenameFile = Dir + DownloadRename ) func StartTestFileServer() net.Listener { @@ -140,6 +143,7 @@ func startTestServer(serverHandle func() http.Handler) net.Listener { if err != nil { panic(err) } + defer file.Close() // 随机生成一个文件 l := int64(8192) buf := make([]byte, l) @@ -160,28 +164,29 @@ func startTestServer(serverHandle func() http.Handler) net.Listener { server.Handler = serverHandle() go server.Serve(listener) - return &delFileListener{ - File: file, + return &shutdownListener{ + server: server, Listener: listener, } } -type delFileListener struct { - *os.File +type shutdownListener struct { + server *http.Server net.Listener } -func (c *delFileListener) Close() error { - defer func() { - c.File.Close() - if err := ifExistAndRemove(c.File.Name()); err != nil { - fmt.Println(err) - } - if err := ifExistAndRemove(DownloadFile); err != nil { - fmt.Println(err) - } - }() - return c.Listener.Close() +func (c *shutdownListener) Close() error { + closeErr := c.server.Shutdown(context.Background()) + if err := ifExistAndRemove(BuildFile); err != nil { + fmt.Println(err) + } + if err := ifExistAndRemove(DownloadFile); err != nil { + fmt.Println(err) + } + if err := ifExistAndRemove(DownloadRenameFile); err != nil { + fmt.Println(err) + } + return closeErr } func ifExistAndRemove(name string) error { diff --git a/pkg/base/model.go b/pkg/base/model.go index 7a3f8abe..b27facfd 100644 --- a/pkg/base/model.go +++ b/pkg/base/model.go @@ -1,6 +1,7 @@ package base import ( + "fmt" "github.com/GopeedLab/gopeed/pkg/util" ) @@ -12,20 +13,46 @@ type Request struct { // Resource download resource type Resource struct { + // if name is not empty, the resource is a folder and the name is the folder name Name string `json:"name"` Size int64 `json:"size"` // is support range download - Range bool `json:"range"` - RootDir string `json:"rootDir"` + Range bool `json:"range"` // file list Files []*FileInfo `json:"files"` Hash string `json:"hash"` } +func (r *Resource) Validate() error { + if r.Name == "" { + return fmt.Errorf("invalid resource name") + } + if r.Files == nil || len(r.Files) == 0 { + return fmt.Errorf("invalid resource files") + } + for _, file := range r.Files { + if file.Name == "" { + return fmt.Errorf("invalid resource file name") + } + } + return nil + +} + +func (r *Resource) CalcSize() { + var size int64 + for _, file := range r.Files { + size += file.Size + } + r.Size = size +} + type FileInfo struct { Name string `json:"name"` Path string `json:"path"` Size int64 `json:"size"` + + Req *Request `json:"req"` } // Options for download @@ -40,6 +67,25 @@ type Options struct { Extra any `json:"extra"` } +func (o *Options) InitSelectFiles(fileSize int) { + // if selectFiles is empty, select all files + if len(o.SelectFiles) == 0 { + o.SelectFiles = make([]int, fileSize) + for i := 0; i < fileSize; i++ { + o.SelectFiles[i] = i + } + } +} + +func (o *Options) Clone() *Options { + return &Options{ + Name: o.Name, + Path: o.Path, + SelectFiles: o.SelectFiles, + Extra: o.Extra, + } +} + func ParseReqExtra[E any](req *Request) error { if req.Extra == nil { return nil diff --git a/pkg/download/downloader.go b/pkg/download/downloader.go index 126be482..740b4d25 100644 --- a/pkg/download/downloader.go +++ b/pkg/download/downloader.go @@ -4,13 +4,13 @@ import ( "errors" "github.com/GopeedLab/gopeed/internal/controller" "github.com/GopeedLab/gopeed/internal/fetcher" + "github.com/GopeedLab/gopeed/internal/logger" "github.com/GopeedLab/gopeed/pkg/base" "github.com/GopeedLab/gopeed/pkg/util" gonanoid "github.com/matoous/go-nanoid/v2" "github.com/virtuald/go-paniclog" "math" "os" - "path" "path/filepath" "sort" "strings" @@ -26,6 +26,12 @@ const ( bucketSave = "save" // downloader config bucket bucketConfig = "config" + // downloader extension bucket + bucketExtension = "extension" +) + +var ( + ErrTaskNotFound = errors.New("task not found") ) type Listener func(event *Event) @@ -40,16 +46,21 @@ type Progress struct { } type Downloader struct { - cfg *DownloaderConfig - fetchBuilders map[string]fetcher.FetcherBuilder - fetcherMap map[string]fetcher.Fetcher - storage Storage - tasks []*Task - listener Listener + Logger *logger.Logger + ExtensionLogger *logger.Logger + + cfg *DownloaderConfig + fetcherBuilders map[string]fetcher.FetcherBuilder + fetcherCache map[string]fetcher.Fetcher + storage Storage + tasks []*Task + listener Listener lock *sync.Mutex fetcherMapLock *sync.RWMutex closed atomic.Bool + + extensions []*Extension } func NewDownloader(cfg *DownloaderConfig) *Downloader { @@ -61,26 +72,32 @@ func NewDownloader(cfg *DownloaderConfig) *Downloader { d := &Downloader{ cfg: cfg, - fetchBuilders: make(map[string]fetcher.FetcherBuilder), - fetcherMap: make(map[string]fetcher.Fetcher), + fetcherBuilders: make(map[string]fetcher.FetcherBuilder), + fetcherCache: make(map[string]fetcher.Fetcher), storage: cfg.Storage, lock: &sync.Mutex{}, fetcherMapLock: &sync.RWMutex{}, + + extensions: make([]*Extension, 0), } for _, f := range cfg.FetchBuilders { for _, p := range f.Schemes() { - d.fetchBuilders[strings.ToUpper(p)] = f + d.fetcherBuilders[strings.ToUpper(p)] = f } } - logPanic(filepath.Join(cfg.StorageDir, "logs")) + d.Logger = logger.NewLogger(cfg.ProductionMode, filepath.Join(cfg.StorageDir, "logs", "core.log")) + d.ExtensionLogger = logger.NewLogger(cfg.ProductionMode, filepath.Join(cfg.StorageDir, "logs", "extension.log")) + if cfg.ProductionMode { + logPanic(filepath.Join(cfg.StorageDir, "logs")) + } return d } func (d *Downloader) Setup() error { // setup storage - if err := d.storage.Setup([]string{bucketTask, bucketSave, bucketConfig}); err != nil { + if err := d.storage.Setup([]string{bucketTask, bucketSave, bucketConfig, bucketExtension}); err != nil { return err } // load config from storage @@ -100,7 +117,7 @@ func (d *Downloader) Setup() error { d.cfg.DownloaderStoreConfig.Init() // load tasks from storage var tasks []*Task - if err := d.storage.List(bucketTask, &tasks); err != nil { + if err = d.storage.List(bucketTask, &tasks); err != nil { return err } if tasks == nil { @@ -118,16 +135,25 @@ func (d *Downloader) Setup() error { task.Status = base.DownloadStatusPause } } - // sort by create time - sort.Slice(tasks, func(i, j int) bool { - return tasks[i].CreatedAt.Before(tasks[j].CreatedAt) - }) } d.tasks = tasks + // sort by create time + sort.Slice(d.tasks, func(i, j int) bool { + return d.tasks[i].CreatedAt.Before(d.tasks[j].CreatedAt) + }) - // 每个tick统计一次下载速度 + // load extensions from storage + var extensions []*Extension + if err = d.storage.List(bucketExtension, &extensions); err != nil { + return err + } + if extensions == nil { + extensions = make([]*Extension, 0) + } + d.extensions = extensions + + // calculate download speed every tick go func() { - for !d.closed.Load() { if len(d.tasks) > 0 { for _, task := range d.tasks { @@ -151,13 +177,16 @@ func (d *Downloader) Setup() error { // store fetcher progress data, err := task.fetcherBuilder.Store(task.fetcher) if err != nil { - return // TODO log + d.Logger.Error().Stack().Err(err).Msgf("serialize fetcher failed: %s", task.ID) + return } if err := d.storage.Put(bucketSave, task.ID, data); err != nil { - return // TODO log + d.Logger.Error().Stack().Err(err).Msgf("persist fetcher failed: %s", task.ID) + return } if err := d.storage.Put(bucketTask, task.ID, task); err != nil { - return // TODO log + d.Logger.Error().Stack().Err(err).Msgf("persist task failed: %s", task.ID) + return } }() } @@ -170,7 +199,7 @@ func (d *Downloader) Setup() error { func (d *Downloader) parseFb(url string) (fetcher.FetcherBuilder, error) { schema := util.ParseSchema(url) - fetchBuilder, ok := d.fetchBuilders[schema] + fetchBuilder, ok := d.fetcherBuilders[schema] if ok { return fetchBuilder, nil } @@ -186,22 +215,29 @@ func (d *Downloader) setupFetcher(fetcher fetcher.Fetcher) { } func (d *Downloader) Resolve(req *base.Request) (rr *ResolveResult, err error) { - fb, err := d.parseFb(req.URL) - if err != nil { - return - } - fetcher := fb.Build() - d.setupFetcher(fetcher) - err = fetcher.Resolve(req) - if err != nil { - return - } rrId, err := gonanoid.New() if err != nil { return } + + res := d.triggerOnResolve(req) + if res != nil && len(res.Files) > 0 { + rr = &ResolveResult{ + Res: res, + } + return + } + + fetcher, err := d.buildFetcher(req.URL) + if err != nil { + return + } + err = fetcher.Resolve(req) + if err != nil { + return + } d.fetcherMapLock.Lock() - d.fetcherMap[rrId] = fetcher + d.fetcherCache[rrId] = fetcher d.fetcherMapLock.Unlock() rr = &ResolveResult{ ID: rrId, @@ -211,11 +247,13 @@ func (d *Downloader) Resolve(req *base.Request) (rr *ResolveResult, err error) { } func (d *Downloader) CreateDirect(req *base.Request, opts *base.Options) (taskId string, err error) { - rr, err := d.Resolve(req) + var fetcher fetcher.Fetcher + fetcher, err = d.buildFetcher(req.URL) if err != nil { return } - return d.Create(rr.ID, opts) + fetcher.Meta().Req = req + return d.doCreate(fetcher, opts) } func (d *Downloader) Create(rrId string, opts *base.Options) (taskId string, err error) { @@ -223,95 +261,23 @@ func (d *Downloader) Create(rrId string, opts *base.Options) (taskId string, err opts = &base.Options{} } d.fetcherMapLock.RLock() - fetcher, ok := d.fetcherMap[rrId] + fetcher, ok := d.fetcherCache[rrId] d.fetcherMapLock.RUnlock() if !ok { return "", errors.New("invalid resource id") } defer func() { d.fetcherMapLock.Lock() - delete(d.fetcherMap, rrId) + delete(d.fetcherCache, rrId) d.fetcherMapLock.Unlock() }() - - meta := fetcher.Meta() - meta.Opts = opts - res := meta.Res - if len(opts.SelectFiles) == 0 { - opts.SelectFiles = make([]int, 0) - for i := range res.Files { - opts.SelectFiles = append(opts.SelectFiles, i) - } - } - if opts.Path == "" { - storeConfig, err := d.GetConfig() - if err != nil { - return "", err - } - opts.Path = storeConfig.DownloadDir - } - - fb, err := d.parseFb(fetcher.Meta().Req.URL) - if err != nil { - return - } - - // check if the download file is duplicated and rename it automatically. - files := res.Files - if res.RootDir != "" { - fullDirPath := path.Join(opts.Path, res.RootDir) - newName, err := util.CheckDuplicateAndRename(fullDirPath) - if err != nil { - return "", err - } - res.RootDir = newName - } else if len(files) == 1 { - fullPath := meta.Filepath(files[0]) - newName, err := util.CheckDuplicateAndRename(fullPath) - if err != nil { - return "", err - } - opts.Name = newName - } - - err = fetcher.Create(opts) - if err != nil { - return - } - - task := NewTask() - task.fetcherBuilder = fb - task.fetcher = fetcher - task.Meta = fetcher.Meta() - task.Progress = &Progress{} - task.Status = base.DownloadStatusReady - // calculate select files size - for _, selectIndex := range opts.SelectFiles { - task.Size += res.Files[selectIndex].Size - } - initTask(task) - err = func() error { - d.lock.Lock() - defer d.lock.Unlock() - d.tasks = append(d.tasks, task) - return d.storage.Put(bucketTask, task.ID, task.clone()) - }() - if err != nil { - return - } - taskId = task.ID - d.tryRunning(func(remain int) { - if remain > 0 { - err = d.start(task) - } - }) - return + return d.doCreate(fetcher, opts) } func (d *Downloader) Pause(id string) (err error) { task := d.GetTask(id) if task == nil { - return + return ErrTaskNotFound } if err = d.doPause(task, base.DownloadStatusPause); err != nil { return err @@ -322,7 +288,7 @@ func (d *Downloader) Pause(id string) (err error) { func (d *Downloader) Continue(id string) (err error) { task := d.GetTask(id) if task == nil { - return + return ErrTaskNotFound } d.tryRunning(func(remain int) { if remain == 0 { @@ -347,14 +313,7 @@ func (d *Downloader) Continue(id string) (err error) { func (d *Downloader) PauseAll() (err error) { d.lock.Lock() defer d.lock.Unlock() - for _, task := range d.tasks { - if task.Status == base.DownloadStatusRunning { - if err = d.doPause(task, base.DownloadStatusPause); err != nil { - return - } - } - } - return + return d.doPauseAll() } func (d *Downloader) ContinueAll() (err error) { @@ -384,7 +343,7 @@ func (d *Downloader) Delete(id string, force bool) (err error) { defer d.lock.Unlock() task := d.GetTask(id) if task == nil { - return + return ErrTaskNotFound } task.lock.Lock() defer task.lock.Unlock() @@ -408,16 +367,14 @@ func (d *Downloader) Delete(id string, force bool) (err error) { if err != nil { return } - if force { - if task.Meta.Res.RootDir != "" { - if err = os.RemoveAll(path.Join(task.Meta.Opts.Path, task.Meta.Res.RootDir)); err != nil { + if force && task.Meta.Res != nil { + if task.Meta.Res.Name != "" { + if err = os.RemoveAll(task.Meta.FolderPath()); err != nil { return } } else { - for _, file := range task.Meta.Res.Files { - if err = util.SafeRemove(task.Meta.Filepath(file)); err != nil { - return err - } + if err = util.SafeRemove(task.Meta.SingleFilepath()); err != nil { + return err } } } @@ -435,16 +392,20 @@ func (d *Downloader) Close() error { d.lock.Lock() defer d.lock.Unlock() d.closed.Store(true) - for _, task := range d.tasks { - d.doPause(task, base.DownloadStatusPause) + if err := d.doPauseAll(); err != nil { + return err } return d.storage.Close() } func (d *Downloader) Clear() error { - if err := d.Close(); err != nil { - return err + if !d.closed.Load() { + if err := d.Close(); err != nil { + return err + } } + d.tasks = make([]*Task, 0) + d.extensions = make([]*Extension, 0) if err := d.storage.Clear(); err != nil { return err } @@ -479,7 +440,12 @@ func (d *Downloader) GetTask(id string) *Task { } func (d *Downloader) GetTasks() []*Task { - return d.tasks + // reverse tasks, so that the latest task is in the front + tasks := make([]*Task, len(d.tasks)) + for i := 0; i < len(d.tasks); i++ { + tasks[i] = d.tasks[len(d.tasks)-i-1] + } + return tasks } func (d *Downloader) GetConfig() (*DownloaderStoreConfig, error) { @@ -552,21 +518,71 @@ func (d *Downloader) restoreFetcher(task *Task) error { return nil }() if err != nil { - // TODO log + d.Logger.Error().Stack().Err(err).Msgf("deserialize fetcher failed, task id: %s", task.ID) } if task.fetcher == nil { task.fetcher = fb.Build() } d.setupFetcher(task.fetcher) - task.fetcher.Create(task.Meta.Opts) + go d.watch(task) } + task.fetcher.Create(task.Meta.Opts) return nil } -func (d *Downloader) tryRunning(cb func(remain int)) { - d.lock.Lock() - defer d.lock.Unlock() +func (d *Downloader) doCreate(fetcher fetcher.Fetcher, opts *base.Options) (taskId string, err error) { + if opts == nil { + opts = &base.Options{} + } + meta := fetcher.Meta() + meta.Opts = opts + if opts.Path == "" { + storeConfig, err := d.GetConfig() + if err != nil { + return "", err + } + opts.Path = storeConfig.DownloadDir + } + + fb, err := d.parseFb(fetcher.Meta().Req.URL) + if err != nil { + return + } + + task := NewTask() + task.fetcherBuilder = fb + task.fetcher = fetcher + task.Meta = fetcher.Meta() + task.Progress = &Progress{} + task.Status = base.DownloadStatusReady + initTask(task) + if err = fetcher.Create(opts); err != nil { + return + } + if err = d.storage.Put(bucketTask, task.ID, task.clone()); err != nil { + return + } + taskId = task.ID + + func() { + d.lock.Lock() + defer d.lock.Unlock() + d.tasks = append(d.tasks, task) + }() + go d.watch(task) + go d.tryRunning(func(remain int) { + if remain > 0 { + if err = d.start(task); err != nil { + d.Logger.Error().Stack().Err(err).Msgf("start task failed, task id: %s", task.ID) + return + } + } + }) + return +} + +func (d *Downloader) tryRunning(cb func(remain int)) { runningCount := 0 for _, task := range d.tasks { if task.Status == base.DownloadStatusRunning { @@ -592,39 +608,75 @@ func (d *Downloader) doNotifyRunning() error { return nil } +func (d *Downloader) doPauseAll() (err error) { + for _, task := range d.tasks { + if err = d.doPause(task, base.DownloadStatusPause); err != nil { + return + } + } + return +} + func (d *Downloader) start(task *Task) error { - var event EventKey - if task.Status == base.DownloadStatusReady { - event = EventKeyStart - } else { - event = EventKeyContinue + task.lock.Lock() + defer task.lock.Unlock() + isCreate := task.Status == base.DownloadStatusReady + task.Status = base.DownloadStatusRunning + if task.Meta.Res == nil { + err := task.fetcher.Resolve(task.Meta.Req) + if err != nil { + task.Status = base.DownloadStatusError + d.emit(EventKeyError, task, err) + return err + } + isCreate = true } - task.Status = base.DownloadStatusRunning + if isCreate { + d.lock.Lock() + defer d.lock.Unlock() + // check if the download file is duplicated and rename it automatically. + if task.Meta.Res.Name != "" { + fullDirPath := task.Meta.FolderPath() + newName, err := util.CheckDuplicateAndRename(fullDirPath) + if err != nil { + return err + } + task.Meta.Opts.Name = newName + } else { + fullFilePath := task.Meta.SingleFilepath() + newName, err := util.CheckDuplicateAndRename(fullFilePath) + if err != nil { + return err + } + task.Meta.Opts.Name = newName + } + } + if err := d.doStart(task); err != nil { + task.Status = base.DownloadStatusError + d.emit(EventKeyError, task, err) + return err + } + return nil +} + +func (d *Downloader) doStart(task *Task) error { task.Progress.Speed = 0 task.timer.Start() if err := d.storage.Put(bucketTask, task.ID, task.clone()); err != nil { return err } - var err error - if event == EventKeyStart { - err = task.fetcher.Start() - } - if event == EventKeyContinue { - err = task.fetcher.Continue() - } - if err != nil { + if err := task.fetcher.Start(); err != nil { return err } - go d.watch(task) - d.emit(event, task) + d.emit(EventKeyStart, task) return nil } func (d *Downloader) doPause(task *Task, status base.Status) (err error) { task.lock.Lock() defer task.lock.Unlock() - if task.Status == base.DownloadStatusRunning { + if task.Status != base.DownloadStatusDone { task.Status = status task.timer.Pause() task.fetcher.Pause() @@ -635,15 +687,26 @@ func (d *Downloader) doPause(task *Task, status base.Status) (err error) { } func (d *Downloader) doContinue(task *Task) (err error) { - task.lock.Lock() - defer task.lock.Unlock() - if task.Status != base.DownloadStatusRunning && task.Status != base.DownloadStatusDone { - err = d.restoreFetcher(task) - if err != nil { + err = func() error { + task.lock.Lock() + defer task.lock.Unlock() + if task.Status != base.DownloadStatusRunning && task.Status != base.DownloadStatusDone { + err := d.restoreFetcher(task) + if err != nil { + return err + } + } + return nil + }() + if err != nil { + return + } + go func() { + if err := d.start(task); err != nil { + d.Logger.Error().Stack().Err(err).Msgf("start task failed, task id: %s", task.ID) return } - err = d.start(task) - } + }() return } @@ -659,6 +722,16 @@ func logPanic(logDir string) { paniclog.RedirectStderr(f) } +func (d *Downloader) buildFetcher(url string) (fetcher.Fetcher, error) { + fb, err := d.parseFb(url) + if err != nil { + return nil, err + } + fetcher := fb.Build() + d.setupFetcher(fetcher) + return fetcher, nil +} + func initTask(task *Task) { task.timer = util.NewTimer(task.Progress.Used) task.lock = &sync.Mutex{} diff --git a/pkg/download/downloader_test.go b/pkg/download/downloader_test.go index 8761b3a1..39a2e50d 100644 --- a/pkg/download/downloader_test.go +++ b/pkg/download/downloader_test.go @@ -27,7 +27,6 @@ func TestDownloader_Resolve(t *testing.T) { t.Fatal(err) } want := &base.Resource{ - Name: test.BuildName, Size: test.BuildSize, Range: true, Files: []*base.FileInfo{ @@ -85,6 +84,51 @@ func TestDownloader_Create(t *testing.T) { } } +func TestDownloader_CreateRename(t *testing.T) { + listener := test.StartTestFileServer() + defer listener.Close() + + downloader := NewDownloader(nil) + if err := downloader.Setup(); err != nil { + t.Fatal(err) + } + defer downloader.Clear() + + req := &base.Request{ + URL: "http://" + listener.Addr().String() + "/" + test.BuildName, + } + var wg sync.WaitGroup + wg.Add(2) + downloader.Listener(func(event *Event) { + if event.Key == EventKeyDone { + wg.Done() + } + }) + for i := 0; i < 2; i++ { + _, err := downloader.CreateDirect(req, &base.Options{ + Path: test.Dir, + Name: test.DownloadName, + Extra: http.OptsExtra{ + Connections: 4, + }, + }) + if err != nil { + t.Fatal(err) + } + } + wg.Wait() + + want := test.FileMd5(test.BuildFile) + got := test.FileMd5(test.DownloadFile) + if want != got { + t.Errorf("Downloader_CreateRename() got = %v, want %v", got, want) + } + got = test.FileMd5(test.DownloadRenameFile) + if want != got { + t.Errorf("Downloader_CreateRename() got = %v, want %v", got, want) + } +} + func TestDownloader_StoreAndRestore(t *testing.T) { listener := test.StartTestSlowFileServer(time.Millisecond * 2000) defer listener.Close() diff --git a/pkg/download/engine/engine.go b/pkg/download/engine/engine.go new file mode 100644 index 00000000..3bd6ae80 --- /dev/null +++ b/pkg/download/engine/engine.go @@ -0,0 +1,125 @@ +package engine + +import ( + _ "embed" + "errors" + "fmt" + "github.com/GopeedLab/gopeed/pkg/download/engine/inject/file" + "github.com/GopeedLab/gopeed/pkg/download/engine/inject/formdata" + "github.com/GopeedLab/gopeed/pkg/download/engine/inject/xhr" + "github.com/dop251/goja" + "github.com/dop251/goja_nodejs/eventloop" + "github.com/dop251/goja_nodejs/process" + "github.com/dop251/goja_nodejs/url" +) + +//go:embed polyfill/out/index.js +var polyfillScript string + +type Engine struct { + loop *eventloop.EventLoop + + Runtime *goja.Runtime +} + +// RunString executes the script and returns the go type value +// if script result is promise, it will be resolved +func (e *Engine) RunString(script string) (value any, err error) { + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("%v", r) + } + }() + var result goja.Value + e.loop.Run(func(runtime *goja.Runtime) { + result, err = runtime.RunString(script) + }) + if err != nil { + return + } + return resolveResult(result) +} + +// CallFunction calls the function and returns the go type value +// if function result is promise, it will be resolved +func (e *Engine) CallFunction(fn goja.Callable, args ...any) (value any, err error) { + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("%v", r) + } + }() + var result goja.Value + e.loop.Run(func(runtime *goja.Runtime) { + if args == nil { + result, err = fn(nil) + } else { + var jsArgs []goja.Value + for _, arg := range args { + jsArgs = append(jsArgs, runtime.ToValue(arg)) + } + result, err = fn(nil, jsArgs...) + } + }) + if err != nil { + return nil, err + } + return resolveResult(result) +} + +func (e *Engine) Close() { + e.loop.Stop() +} + +func NewEngine() *Engine { + loop := eventloop.NewEventLoop() + engine := &Engine{ + loop: loop, + } + loop.Run(func(runtime *goja.Runtime) { + engine.Runtime = runtime + runtime.SetFieldNameMapper(goja.TagFieldNameMapper("json", true)) + url.Enable(runtime) + process.Enable(runtime) + if err := file.Enable(runtime); err != nil { + return + } + if err := formdata.Enable(runtime); err != nil { + return + } + if err := xhr.Enable(runtime); err != nil { + return + } + if _, err := runtime.RunString(polyfillScript); err != nil { + return + } + // polyfill global + if err := runtime.Set("global", runtime.GlobalObject()); err != nil { + return + } + return + }) + return engine +} + +func Run(script string) (value any, err error) { + engine := NewEngine() + return engine.RunString(script) +} + +// if the value is Promise, it will be resolved and return the result. +func resolveResult(value goja.Value) (any, error) { + export := value.Export() + switch export.(type) { + case *goja.Promise: + p := export.(*goja.Promise) + switch p.State() { + case goja.PromiseStatePending: + return nil, nil + case goja.PromiseStateFulfilled: + return p.Result().Export(), nil + case goja.PromiseStateRejected: + return nil, errors.New(p.Result().String()) + } + } + return export, nil +} diff --git a/pkg/download/engine/engine_test.go b/pkg/download/engine/engine_test.go new file mode 100644 index 00000000..e8c6ffbf --- /dev/null +++ b/pkg/download/engine/engine_test.go @@ -0,0 +1,291 @@ +package engine + +import ( + "crypto/md5" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "github.com/GopeedLab/gopeed/internal/test" + "github.com/GopeedLab/gopeed/pkg/download/engine/inject/file" + "github.com/dop251/goja" + "io" + "net" + "net/http" + "strconv" + "strings" + "testing" + "time" +) + +func TestPolyfill(t *testing.T) { + doTestPolyfill(t, "XMLHttpRequest") + doTestPolyfill(t, "Blob") + doTestPolyfill(t, "FormData") + doTestPolyfill(t, "fetch") +} + +func TestFetch(t *testing.T) { + server := startServer() + defer server.Close() + engine := NewEngine() + if _, err := engine.RunString(fmt.Sprintf("var host = 'http://%s';", server.Addr().String())); err != nil { + t.Fatal(err) + } + _, err := engine.RunString(` +async function testGet(){ + const resp = await fetch(host+'/get'); + return resp.status; +} + +async function testText(){ + const resp = await fetch(host+'/text',{ + method: 'POST', + body: 'test' + }); + return await resp.text(); +} + +async function testOctetStream(file){ + const resp = await fetch(host+'/octetStream',{ + method: 'POST', + body: file + }); + return await resp.text(); +} + +async function testFormData(file){ + const formData = new FormData(); + formData.append('name', 'test'); + formData.append('f', file); + const resp = await fetch(host+'/formData',{ + method: 'POST', + body: formData + }); + return await resp.json(); +} + +function testProgress(){ + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + xhr.open('GET', host+'/get'); + const xhrUploadPromise = new Promise((resolve, reject) => { + xhr.upload.onprogress = function(e){ + if(e.loaded === e.total){ + resolve(); + } + } + }); + const xhrPromise = new Promise((resolve, reject) => { + xhr.onprogress = function(e){ + if(e.loaded === e.total){ + resolve(); + } + } + }); + Promise.all([xhrUploadPromise, xhrPromise]).then(() => { + resolve(); + }); + xhr.send(); + setTimeout(() => { + reject('timeout'); + }, 1000); + }); +} + +function testAbort(){ + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + xhr.open('GET', host+'/timeout?duration=500'); + xhr.onabort = function() { + resolve(); + }; + xhr.send(); + setTimeout(() => { + xhr.abort(); + }, 200); + setTimeout(() => { + reject('timeout'); + }, 1000); + }); +} + +function testTimeout(){ + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + const t = 500; + xhr.open('GET', host+'/timeout?duration='+t); + xhr.timeout = t - 200; + xhr.onload = function() { + resolve(); + }; + xhr.ontimeout = function() { + reject('timeout'); + }; + xhr.send(); + }); +} +`) + if err != nil { + t.Fatal(err) + } + + result, err := callTestFun(engine, "testGet") + if err != nil { + t.Fatal(err) + } + if result != int64(200) { + t.Fatalf("testGet failed, want %d, got %d", 200, result) + } + + result, err = callTestFun(engine, "testText") + if err != nil { + t.Fatal(err) + } + if result != "test" { + t.Fatalf("testText failed, want %s, got %s", "test", result) + } + + func() { + jsFile, _, md5 := buildFile(t, engine.Runtime) + result, err = callTestFun(engine, "testOctetStream", jsFile) + if err != nil { + t.Fatal(err) + } + if result != md5 { + t.Fatalf("testOctetStream failed, want %s, got %s", md5, result) + } + }() + + func() { + jsFile, goFile, md5 := buildFile(t, engine.Runtime) + result, err = callTestFun(engine, "testFormData", jsFile) + if err != nil { + t.Fatal(err) + } + want := map[string]any{ + "name": "test", + "f": map[string]string{ + "filename": goFile.Name, + "md5": md5, + }, + } + if !test.JsonEqual(result, want) { + t.Fatalf("testFormData failed, want %v, got %v", want, result) + } + }() + + _, err = callTestFun(engine, "testProgress") + if err != nil { + t.Fatal("progress test failed") + } + + _, err = callTestFun(engine, "testAbort") + if err != nil { + t.Fatal("abort test failed") + } + + _, err = callTestFun(engine, "testTimeout") + if err == nil || err.Error() != "timeout" { + t.Fatalf("timeout test failed, want %s, got %s", "timeout", err) + } +} + +func doTestPolyfill(t *testing.T, module string) { + value, err := Run(fmt.Sprintf(` +!!globalThis['%s'] +`, module)) + if err != nil { + t.Fatal(err) + } + if !value.(bool) { + t.Fatalf("module %s not polyfilled", module) + } +} + +func startServer() net.Listener { + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + panic(err) + } + server := &http.Server{} + mux := http.NewServeMux() + mux.HandleFunc("/get", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte("ok")) + }) + mux.HandleFunc("/text", func(w http.ResponseWriter, r *http.Request) { + buf, _ := io.ReadAll(r.Body) + w.WriteHeader(http.StatusOK) + w.Write(buf) + }) + mux.HandleFunc("/octetStream", func(w http.ResponseWriter, r *http.Request) { + md5 := calcMd5(r.Body) + w.WriteHeader(http.StatusOK) + w.Write([]byte(md5)) + }) + mux.HandleFunc("/formData", func(w http.ResponseWriter, r *http.Request) { + err := r.ParseMultipartForm(1024 * 1024 * 30) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(err.Error())) + return + } + result := make(map[string]any) + for k, v := range r.MultipartForm.Value { + result[k] = v[0] + } + for k, v := range r.MultipartForm.File { + f, _ := v[0].Open() + result[k] = map[string]string{ + "filename": v[0].Filename, + "md5": calcMd5(f), + } + } + w.WriteHeader(http.StatusOK) + buf, _ := json.Marshal(result) + w.Write(buf) + }) + mux.HandleFunc("/timeout", func(w http.ResponseWriter, r *http.Request) { + duration := r.URL.Query().Get("duration") + t, _ := strconv.Atoi(duration) + time.Sleep(time.Duration(t) * time.Millisecond) + w.WriteHeader(http.StatusOK) + w.Write([]byte("ok")) + }) + server.Handler = mux + go server.Serve(listener) + return listener +} + +func buildFile(t *testing.T, runtime *goja.Runtime) (goja.Value, *file.File, string) { + jsFile, err := file.NewJsFile(runtime) + if err != nil { + t.Fatal(err) + } + f := jsFile.Export().(*file.File) + data := "test" + f.Reader = strings.NewReader(data) + f.Name = "test.txt" + f.Size = int64(len(data)) + return jsFile, f, calcMd5(strings.NewReader(data)) +} + +func callTestFun(engine *Engine, fun string, args ...any) (any, error) { + test, ok := goja.AssertFunction(engine.Runtime.Get(fun)) + if !ok { + return nil, errors.New("function not found:" + fun) + } + return engine.CallFunction(test, args...) +} + +func calcMd5(reader io.Reader) string { + // Open a new hash interface to write to + hash := md5.New() + + // Copy the file in the hash interface and check for any error + if _, err := io.Copy(hash, reader); err != nil { + return "" + } + return hex.EncodeToString(hash.Sum(nil)) +} diff --git a/pkg/download/engine/inject/file/module.go b/pkg/download/engine/inject/file/module.go new file mode 100644 index 00000000..b3335e3a --- /dev/null +++ b/pkg/download/engine/inject/file/module.go @@ -0,0 +1,32 @@ +package file + +import ( + "errors" + "github.com/dop251/goja" + "io" +) + +type File struct { + io.Reader `json:""` + io.Closer `json:""` + Name string `json:"name"` + Size int64 `json:"size"` +} + +func NewJsFile(runtime *goja.Runtime) (goja.Value, error) { + fileCtor, ok := goja.AssertConstructor(runtime.Get("File")) + if !ok { + return nil, errors.New("file is not defined") + } + return fileCtor(nil) +} + +func Enable(runtime *goja.Runtime) error { + file := runtime.ToValue(func(call goja.ConstructorCall) *goja.Object { + instance := &File{} + instanceValue := runtime.ToValue(instance).(*goja.Object) + instanceValue.SetPrototype(call.This.Prototype()) + return instanceValue + }) + return runtime.Set("File", file) +} diff --git a/pkg/download/engine/inject/formdata/module.go b/pkg/download/engine/inject/formdata/module.go new file mode 100644 index 00000000..7c5388e7 --- /dev/null +++ b/pkg/download/engine/inject/formdata/module.go @@ -0,0 +1,68 @@ +package formdata + +import "github.com/dop251/goja" + +type FormData struct { + data map[string]any +} + +func (fd *FormData) Append(name string, value any) { + fd.data[name] = value +} + +func (fd *FormData) Delete(name string) { + delete(fd.data, name) +} + +func (fd *FormData) Entries() []any { + var entries []any + for k, v := range fd.data { + entries = append(entries, []any{k, v}) + } + return entries +} + +func (fd *FormData) Get(name string) any { + return fd.data[name] +} + +func (fd *FormData) GetAll(name string) []any { + return []any{fd.data[name]} +} + +func (fd *FormData) Has(name string) bool { + _, ok := fd.data[name] + return ok +} + +func (fd *FormData) Keys() []string { + var keys []string + for k := range fd.data { + keys = append(keys, k) + } + return keys +} + +func (fd *FormData) Set(name string, value any) { + fd.data[name] = value +} + +func (fd *FormData) Values() []any { + var values []any + for _, v := range fd.data { + values = append(values, v) + } + return values +} + +func Enable(runtime *goja.Runtime) error { + file := runtime.ToValue(func(call goja.ConstructorCall) *goja.Object { + instance := &FormData{ + data: make(map[string]any), + } + instanceValue := runtime.ToValue(instance).(*goja.Object) + instanceValue.SetPrototype(call.This.Prototype()) + return instanceValue + }) + return runtime.Set("FormData", file) +} diff --git a/pkg/download/engine/inject/util.go b/pkg/download/engine/inject/util.go new file mode 100644 index 00000000..d00504be --- /dev/null +++ b/pkg/download/engine/inject/util.go @@ -0,0 +1,9 @@ +package inject + +import ( + "github.com/dop251/goja" +) + +func ThrowTypeError(vm *goja.Runtime, msg string) { + panic(vm.NewTypeError(msg)) +} diff --git a/pkg/download/engine/inject/xhr/module.go b/pkg/download/engine/inject/xhr/module.go new file mode 100644 index 00000000..f96f2775 --- /dev/null +++ b/pkg/download/engine/inject/xhr/module.go @@ -0,0 +1,417 @@ +package xhr + +import ( + "bytes" + "github.com/GopeedLab/gopeed/pkg/download/engine/inject" + "github.com/GopeedLab/gopeed/pkg/download/engine/inject/file" + "github.com/GopeedLab/gopeed/pkg/download/engine/inject/formdata" + "github.com/dop251/goja" + "io" + "mime/multipart" + "net" + "net/http" + "time" +) + +const ( + eventLoad = "load" + eventReadystatechange = "readystatechange" + eventProgress = "progress" + eventAbort = "abort" + eventError = "error" + eventTimeout = "timeout" +) + +type ProgressEvent struct { + Type string `json:"type"` + LengthComputable bool `json:"lengthComputable"` + Loaded int64 `json:"loaded"` + Total int64 `json:"total"` +} + +type EventProp struct { + eventListeners map[string]func(event *ProgressEvent) + Onload func(event *ProgressEvent) `json:"onload"` + Onprogress func(event *ProgressEvent) `json:"onprogress"` + Onabort func(event *ProgressEvent) `json:"onabort"` + Onerror func(event *ProgressEvent) `json:"onerror"` + Ontimeout func(event *ProgressEvent) `json:"ontimeout"` +} + +func (ep *EventProp) AddEventListener(event string, cb func(event *ProgressEvent)) { + ep.eventListeners[event] = cb +} + +func (ep *EventProp) RemoveEventListener(event string) { + delete(ep.eventListeners, event) +} + +func (ep *EventProp) callOnload() { + event := &ProgressEvent{ + Type: eventLoad, + LengthComputable: false, + } + if ep.Onload != nil { + ep.Onload(event) + } + ep.callEventListener(event) +} + +func (ep *EventProp) callOnprogress(loaded, total int64) { + event := &ProgressEvent{ + Type: eventProgress, + LengthComputable: true, + Loaded: loaded, + Total: total, + } + if ep.Onprogress != nil { + ep.Onprogress(event) + } + ep.callEventListener(event) +} + +func (ep *EventProp) callOnabort() { + event := &ProgressEvent{ + Type: eventAbort, + LengthComputable: false, + } + if ep.Onabort != nil { + ep.Onabort(event) + } + ep.callEventListener(event) +} + +func (ep *EventProp) callOnerror() { + event := &ProgressEvent{ + Type: eventError, + LengthComputable: false, + } + if ep.Onerror != nil { + ep.Onerror(event) + } + ep.callEventListener(event) +} + +func (ep *EventProp) callOntimeout() { + event := &ProgressEvent{ + Type: eventTimeout, + LengthComputable: false, + } + if ep.Ontimeout != nil { + ep.Ontimeout(event) + } + ep.callEventListener(event) +} + +func (ep *EventProp) callEventListener(event *ProgressEvent) { + if cb, ok := ep.eventListeners[event.Type]; ok { + cb(event) + } +} + +type XMLHttpRequestUpload struct { + *EventProp +} + +type XMLHttpRequest struct { + method string + url string + requestHeaders map[string]string + responseHeaders map[string]string + aborted bool + + Upload *XMLHttpRequestUpload `json:"upload"` + Timeout int `json:"timeout"` + ReadyState int `json:"readyState"` + Status int `json:"status"` + StatusText string `json:"statusText"` + Response string `json:"response"` + ResponseText string `json:"responseText"` + *EventProp + Onreadystatechange func(event *ProgressEvent) `json:"onreadystatechange"` +} + +func (xhr *XMLHttpRequest) Open(method, url string) { + xhr.method = method + xhr.url = url + xhr.requestHeaders = make(map[string]string) + xhr.responseHeaders = make(map[string]string) + xhr.doReadystatechange(1) +} + +func (xhr *XMLHttpRequest) SetRequestHeader(key, value string) { + xhr.requestHeaders[key] = value +} + +func (xhr *XMLHttpRequest) Send(data goja.Value) { + var req *http.Request + var err error + d := xhr.parseData(data) + var ( + contentType string + contentLength int64 + ) + if d == nil || xhr.method == "GET" || xhr.method == "HEAD" { + req, err = http.NewRequest(xhr.method, xhr.url, nil) + } else { + switch d.(type) { + case string: + req, err = http.NewRequest(xhr.method, xhr.url, bytes.NewBufferString(d.(string))) + contentType = "text/plain;charset=UTF-8" + contentLength = int64(len(d.(string))) + case *file.File: + req, err = http.NewRequest(xhr.method, xhr.url, d.(*file.File).Reader) + contentType = "application/octet-stream" + contentLength = d.(*file.File).Size + case *formdata.FormData: + pr, pw := io.Pipe() + mw := NewMultipart(pw) + for _, e := range d.(*formdata.FormData).Entries() { + arr := e.([]any) + k := arr[0].(string) + v := arr[1] + switch v.(type) { + case string: + mw.WriteField(k, v.(string)) + case *file.File: + mw.WriteFile(k, v.(*file.File)) + } + } + go func() { + defer pw.Close() + defer mw.Close() + mw.Send() + }() + req, err = http.NewRequest(xhr.method, xhr.url, pr) + contentType = mw.FormDataContentType() + contentLength = mw.Size() + } + } + if err != nil { + xhr.callOnerror() + return + } + if contentType != "" { + req.Header.Set("Content-Type", contentType) + } + if contentLength > 0 { + req.ContentLength = contentLength + } + for k, v := range xhr.requestHeaders { + req.Header.Set(k, v) + } + client := &http.Client{ + Timeout: time.Duration(xhr.Timeout) * time.Millisecond, + } + resp, err := client.Do(req) + if err != nil { + // handle timeout error + if err, ok := err.(net.Error); ok && err.Timeout() { + if xhr.Timeout > 0 { + xhr.Upload.callOntimeout() + xhr.callOntimeout() + } + return + } + xhr.Upload.callOnerror() + xhr.callOnerror() + return + } + defer resp.Body.Close() + xhr.Upload.callOnprogress(contentLength, contentLength) + if !xhr.aborted { + xhr.Upload.callOnload() + } + for k, v := range resp.Header { + xhr.responseHeaders[k] = v[0] + } + xhr.Status = resp.StatusCode + xhr.StatusText = resp.Status + xhr.doReadystatechange(2) + buf, err := io.ReadAll(resp.Body) + if err != nil { + xhr.callOnerror() + return + } + xhr.doReadystatechange(3) + xhr.Response = string(buf) + xhr.ResponseText = xhr.Response + xhr.doReadystatechange(4) + respBodyLen := int64(len(buf)) + xhr.callOnprogress(respBodyLen, respBodyLen) + if !xhr.aborted { + xhr.callOnload() + } + return +} + +func (xhr *XMLHttpRequest) Abort() { + xhr.doReadystatechange(0) + xhr.aborted = true + xhr.Upload.callOnabort() + xhr.callOnabort() +} + +func (xhr *XMLHttpRequest) GetResponseHeader(key string) string { + return xhr.responseHeaders[key] +} + +func (xhr *XMLHttpRequest) GetAllResponseHeaders() string { + var buf bytes.Buffer + for k, v := range xhr.responseHeaders { + buf.WriteString(k) + buf.WriteString(": ") + buf.WriteString(v) + buf.WriteString("\r\n") + } + return buf.String() +} + +func (xhr *XMLHttpRequest) callOnreadystatechange() { + event := &ProgressEvent{ + Type: eventReadystatechange, + LengthComputable: false, + } + if xhr.Onreadystatechange != nil { + xhr.Onreadystatechange(event) + } + xhr.callEventListener(event) +} + +func (xhr *XMLHttpRequest) doReadystatechange(state int) { + if xhr.aborted { + return + } + xhr.ReadyState = state + xhr.callOnreadystatechange() +} + +// parse js data to go struct +func (xhr *XMLHttpRequest) parseData(data goja.Value) any { + // check if data is null or undefined + if data == nil || goja.IsNull(data) || goja.IsUndefined(data) || goja.IsNaN(data) { + return nil + } + // check if data is File + f, ok := data.Export().(*file.File) + if ok { + return f + } + // check if data is FormData + fd, ok := data.Export().(*formdata.FormData) + if ok { + return fd + } + // otherwise, return data as string + return data.String() +} + +func Enable(runtime *goja.Runtime) error { + progressEvent := runtime.ToValue(func(call goja.ConstructorCall) *goja.Object { + if len(call.Arguments) < 1 { + inject.ThrowTypeError(runtime, "Failed to construct 'ProgressEvent': 1 argument required, but only 0 present.") + } + instance := &ProgressEvent{ + Type: call.Argument(0).String(), + } + instanceValue := runtime.ToValue(instance).(*goja.Object) + instanceValue.SetPrototype(call.This.Prototype()) + return instanceValue + }) + xhr := runtime.ToValue(func(call goja.ConstructorCall) *goja.Object { + instance := &XMLHttpRequest{ + Upload: &XMLHttpRequestUpload{ + EventProp: &EventProp{ + eventListeners: make(map[string]func(event *ProgressEvent)), + }, + }, + EventProp: &EventProp{ + eventListeners: make(map[string]func(event *ProgressEvent)), + }, + } + instanceValue := runtime.ToValue(instance).(*goja.Object) + instanceValue.SetPrototype(call.This.Prototype()) + return instanceValue + }) + if err := runtime.Set("ProgressEvent", progressEvent); err != nil { + return err + } + if err := runtime.Set("XMLHttpRequest", xhr); err != nil { + return err + } + return nil +} + +// Wrap multipart.Writer and stat content length +type multipartWrapper struct { + statBuffer *bytes.Buffer + statWriter *multipart.Writer + writer *multipart.Writer + fields map[string]any +} + +func NewMultipart(w io.Writer) *multipartWrapper { + var buf bytes.Buffer + return &multipartWrapper{ + statBuffer: &buf, + statWriter: multipart.NewWriter(&buf), + writer: multipart.NewWriter(w), + fields: make(map[string]any), + } +} + +func (w *multipartWrapper) WriteField(fieldname string, value string) error { + w.fields[fieldname] = value + return w.statWriter.WriteField(fieldname, value) +} + +func (w *multipartWrapper) WriteFile(fieldname string, file *file.File) error { + w.fields[fieldname] = file + _, err := w.statWriter.CreateFormFile(fieldname, file.Name) + if err != nil { + return err + } + return nil +} + +func (w *multipartWrapper) Size() int64 { + w.statWriter.Close() + size := int64(w.statBuffer.Len()) + for _, v := range w.fields { + switch v.(type) { + case *file.File: + f := v.(*file.File) + size += f.Size + } + } + return size +} + +func (w *multipartWrapper) Send() error { + for k, v := range w.fields { + switch v.(type) { + case string: + if err := w.writer.WriteField(k, v.(string)); err != nil { + return err + } + case *file.File: + f := v.(*file.File) + fw, err := w.writer.CreateFormFile(k, f.Name) + if err != nil { + return err + } + if _, err = io.Copy(fw, f); err != nil { + return err + } + } + } + return nil +} + +func (w *multipartWrapper) FormDataContentType() string { + return w.writer.FormDataContentType() +} + +func (w *multipartWrapper) Close() error { + return w.writer.Close() +} diff --git a/pkg/download/engine/polyfill/out/index.js b/pkg/download/engine/polyfill/out/index.js new file mode 100644 index 00000000..9e18f3c9 --- /dev/null +++ b/pkg/download/engine/polyfill/out/index.js @@ -0,0 +1 @@ +(()=>{var t={725:function(t,e,r){var o,n,i;i="undefined"!=typeof self&&self||"undefined"!=typeof window&&window||void 0!==r.g&&r.g||this,o=function(t){"use strict";var e=i.BlobBuilder||i.WebKitBlobBuilder||i.MSBlobBuilder||i.MozBlobBuilder,r=i.URL||i.webkitURL||function(t,e){return(e=document.createElement("a")).href=t,e},o=i.Blob,n=r.createObjectURL,a=r.revokeObjectURL,s=i.Symbol&&i.Symbol.toStringTag,u=!1,f=!1,l=e&&e.prototype.append&&e.prototype.getBlob;try{u=2===new Blob(["ä"]).size,f=2===new Blob([new Uint8Array([1,2])]).size}catch(t){}function c(t){return t.map((function(t){if(t.buffer instanceof ArrayBuffer){var e=t.buffer;if(t.byteLength!==e.byteLength){var r=new Uint8Array(t.byteLength);r.set(new Uint8Array(e,t.byteOffset,t.byteLength)),e=r.buffer}return e}return t}))}function h(t,r){r=r||{};var o=new e;return c(t).forEach((function(t){o.append(t)})),r.type?o.getBlob(r.type):o.getBlob()}function p(t,e){return new o(c(t),e||{})}i.Blob&&(h.prototype=Blob.prototype,p.prototype=Blob.prototype);var y="function"==typeof TextEncoder?TextEncoder.prototype.encode.bind(new TextEncoder):function(t){for(var e=0,r=t.length,o=i.Uint8Array||Array,n=0,a=Math.max(32,r+(r>>1)+7),s=new o(a>>3<<3);e=55296&&u<=56319){if(e=55296&&u<=56319)continue}if(n+4>s.length){a+=8,a=(a*=1+e/t.length*2)>>3<<3;var l=new Uint8Array(a);l.set(s),s=l}if(0!=(4294967168&u)){if(0==(4294965248&u))s[n++]=u>>6&31|192;else if(0==(4294901760&u))s[n++]=u>>12&15|224,s[n++]=u>>6&63|128;else{if(0!=(4292870144&u))continue;s[n++]=u>>18&7|240,s[n++]=u>>12&63|128,s[n++]=u>>6&63|128}s[n++]=63&u|128}else s[n++]=u}return s.slice(0,n)},d="function"==typeof TextDecoder?TextDecoder.prototype.decode.bind(new TextDecoder):function(t){for(var e=t.length,r=[],o=0;o239?4:u>223?3:u>191?2:1;if(o+l<=e)switch(l){case 1:u<128&&(f=u);break;case 2:128==(192&(n=t[o+1]))&&(s=(31&u)<<6|63&n)>127&&(f=s);break;case 3:n=t[o+1],i=t[o+2],128==(192&n)&&128==(192&i)&&(s=(15&u)<<12|(63&n)<<6|63&i)>2047&&(s<55296||s>57343)&&(f=s);break;case 4:n=t[o+1],i=t[o+2],a=t[o+3],128==(192&n)&&128==(192&i)&&128==(192&a)&&(s=(15&u)<<18|(63&n)<<12|(63&i)<<6|63&a)>65535&&s<1114112&&(f=s)}null===f?(f=65533,l=1):f>65535&&(f-=65536,r.push(f>>>10&1023|55296),f=56320|1023&f),r.push(f),o+=l}for(var c=r.length,h="",p=0;p>2,l=(3&n)<<4|a>>4,c=(15&a)<<2|u>>6,h=63&u;s||(h=64,i||(c=64)),r.push(e[f],e[l],e[c],e[h])}return r.join("")}var s=Object.create||function(t){function e(){}return e.prototype=t,new e};function u(t){return Object.prototype.toString.call(t).slice(8,-1)}function f(t,e){return"object"==typeof t&&Object.prototype.isPrototypeOf.call(t.prototype,e)}var l=["Int8Array","Uint8Array","Uint8ClampedArray","Int16Array","Uint16Array","Int32Array","Uint32Array","Float32Array","Float64Array","ArrayBuffer"];function c(t){return e=l,r=u(t),-1!==e.indexOf(r)||f(i.ArrayBuffer,t);var e,r}function h(t,r){r=null==r?{}:r;for(var o=0,n=(t=t?t.slice():[]).length;o=e.size&&r.close()}))}})}}catch(t){try{new ReadableStream({}),w=function(t){var e=0;return new ReadableStream({pull:function(r){return t.slice(e,e+524288).arrayBuffer().then((function(o){e+=o.byteLength;var n=new Uint8Array(o);r.enqueue(n),e==t.size&&r.close()}))}})}}catch(t){try{new Response("").body.getReader().read(),w=function(){return new Response(this).body}}catch(t){w=function(){throw new Error("Include https://github.com/MattiasBuelens/web-streams-polyfill")}}}}function v(t){return new Promise((function(e,r){t.onload=t.onerror=function(o){t.onload=t.onerror=null,"load"===o.type?e(t.result||t):r(new Error("Failed to read the blob/file"))}}))}m.arrayBuffer||(m.arrayBuffer=function(){var t=new FileReader;return t.readAsArrayBuffer(this),v(t)}),m.text||(m.text=function(){var t=new FileReader;return t.readAsText(this),v(t)}),m.stream||(m.stream=w)},void 0===(n=o.apply(e,[e]))||(t.exports=n)}},e={};function r(o){var n=e[o];if(void 0!==n)return n.exports;var i=e[o]={exports:{}};return t[o].call(i.exports,i,i.exports,r),i.exports}r.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(t){if("object"==typeof window)return window}}(),(()=>{"use strict";var t=r(725);globalThis.Blob=t.Blob,globalThis.crypto={getRandomValues(t){for(let e=0,r=t.length;e(t^this.getRandomValues(new Uint8Array(1))[0]&15>>t/4).toString(16)))}};var e="undefined"!=typeof globalThis&&globalThis||"undefined"!=typeof self&&self||void 0!==e&&e,o={searchParams:"URLSearchParams"in e,iterable:"Symbol"in e&&"iterator"in Symbol,blob:"FileReader"in e&&"Blob"in e&&function(){try{return new Blob,!0}catch(t){return!1}}(),formData:"FormData"in e,arrayBuffer:"ArrayBuffer"in e};if(o.arrayBuffer)var n=["[object Int8Array]","[object Uint8Array]","[object Uint8ClampedArray]","[object Int16Array]","[object Uint16Array]","[object Int32Array]","[object Uint32Array]","[object Float32Array]","[object Float64Array]"],i=ArrayBuffer.isView||function(t){return t&&n.indexOf(Object.prototype.toString.call(t))>-1};function a(t){if("string"!=typeof t&&(t=String(t)),/[^a-z0-9\-#$%&'*+.^_`|~!]/i.test(t)||""===t)throw new TypeError('Invalid character in header field name: "'+t+'"');return t.toLowerCase()}function s(t){return"string"!=typeof t&&(t=String(t)),t}function u(t){var e={next:function(){var e=t.shift();return{done:void 0===e,value:e}}};return o.iterable&&(e[Symbol.iterator]=function(){return e}),e}function f(t){this.map={},t instanceof f?t.forEach((function(t,e){this.append(e,t)}),this):Array.isArray(t)?t.forEach((function(t){this.append(t[0],t[1])}),this):t&&Object.getOwnPropertyNames(t).forEach((function(e){this.append(e,t[e])}),this)}function l(t){if(t.bodyUsed)return Promise.reject(new TypeError("Already read"));t.bodyUsed=!0}function c(t){return new Promise((function(e,r){t.onload=function(){e(t.result)},t.onerror=function(){r(t.error)}}))}function h(t){var e=new FileReader,r=c(e);return e.readAsArrayBuffer(t),r}function p(t){if(t.slice)return t.slice(0);var e=new Uint8Array(t.byteLength);return e.set(new Uint8Array(t)),e.buffer}function y(){return this.bodyUsed=!1,this._initBody=function(t){var e;this.bodyUsed=this.bodyUsed,this._bodyInit=t,t?"string"==typeof t?this._bodyText=t:o.blob&&Blob.prototype.isPrototypeOf(t)?this._bodyBlob=t:o.formData&&FormData.prototype.isPrototypeOf(t)?this._bodyFormData=t:o.searchParams&&URLSearchParams.prototype.isPrototypeOf(t)?this._bodyText=t.toString():o.arrayBuffer&&o.blob&&(e=t)&&DataView.prototype.isPrototypeOf(e)?(this._bodyArrayBuffer=p(t.buffer),this._bodyInit=new Blob([this._bodyArrayBuffer])):o.arrayBuffer&&(ArrayBuffer.prototype.isPrototypeOf(t)||i(t))?this._bodyArrayBuffer=p(t):this._bodyText=t=Object.prototype.toString.call(t):this._bodyText="",this.headers.get("content-type")||("string"==typeof t?this.headers.set("content-type","text/plain;charset=UTF-8"):this._bodyBlob&&this._bodyBlob.type?this.headers.set("content-type",this._bodyBlob.type):o.searchParams&&URLSearchParams.prototype.isPrototypeOf(t)&&this.headers.set("content-type","application/x-www-form-urlencoded;charset=UTF-8"))},o.blob&&(this.blob=function(){var t=l(this);if(t)return t;if(this._bodyBlob)return Promise.resolve(this._bodyBlob);if(this._bodyArrayBuffer)return Promise.resolve(new Blob([this._bodyArrayBuffer]));if(this._bodyFormData)throw new Error("could not read FormData body as blob");return Promise.resolve(new Blob([this._bodyText]))},this.arrayBuffer=function(){return this._bodyArrayBuffer?l(this)||(ArrayBuffer.isView(this._bodyArrayBuffer)?Promise.resolve(this._bodyArrayBuffer.buffer.slice(this._bodyArrayBuffer.byteOffset,this._bodyArrayBuffer.byteOffset+this._bodyArrayBuffer.byteLength)):Promise.resolve(this._bodyArrayBuffer)):this.blob().then(h)}),this.text=function(){var t,e,r,o=l(this);if(o)return o;if(this._bodyBlob)return t=this._bodyBlob,r=c(e=new FileReader),e.readAsText(t),r;if(this._bodyArrayBuffer)return Promise.resolve(function(t){for(var e=new Uint8Array(t),r=new Array(e.length),o=0;o-1?o:r),this.mode=e.mode||this.mode||null,this.signal=e.signal||this.signal,this.referrer=null,("GET"===this.method||"HEAD"===this.method)&&n)throw new TypeError("Body not allowed for GET or HEAD requests");if(this._initBody(n),!("GET"!==this.method&&"HEAD"!==this.method||"no-store"!==e.cache&&"no-cache"!==e.cache)){var i=/([?&])_=[^&]*/;i.test(this.url)?this.url=this.url.replace(i,"$1_="+(new Date).getTime()):this.url+=(/\?/.test(this.url)?"&":"?")+"_="+(new Date).getTime()}}function w(t){var e=new FormData;return t.trim().split("&").forEach((function(t){if(t){var r=t.split("="),o=r.shift().replace(/\+/g," "),n=r.join("=").replace(/\+/g," ");e.append(decodeURIComponent(o),decodeURIComponent(n))}})),e}function m(t,e){if(!(this instanceof m))throw new TypeError('Please use the "new" operator, this DOM object constructor cannot be called as a function.');e||(e={}),this.type="default",this.status=void 0===e.status?200:e.status,this.ok=this.status>=200&&this.status<300,this.statusText=void 0===e.statusText?"":""+e.statusText,this.headers=new f(e.headers),this.url=e.url||"",this._initBody(t)}b.prototype.clone=function(){return new b(this,{body:this._bodyInit})},y.call(b.prototype),y.call(m.prototype),m.prototype.clone=function(){return new m(this._bodyInit,{status:this.status,statusText:this.statusText,headers:new f(this.headers),url:this.url})},m.error=function(){var t=new m(null,{status:0,statusText:""});return t.type="error",t};var v=[301,302,303,307,308];m.redirect=function(t,e){if(-1===v.indexOf(e))throw new RangeError("Invalid status code");return new m(null,{status:e,headers:{location:t}})};var g=e.DOMException;try{new g}catch(t){(g=function(t,e){this.message=t,this.name=e;var r=Error(t);this.stack=r.stack}).prototype=Object.create(Error.prototype),g.prototype.constructor=g}function A(t,r){return new Promise((function(n,i){var a=new b(t,r);if(a.signal&&a.signal.aborted)return i(new g("Aborted","AbortError"));var u=new XMLHttpRequest;function l(){u.abort()}u.onload=function(){var t,e,r={status:u.status,statusText:u.statusText,headers:(t=u.getAllResponseHeaders()||"",e=new f,t.replace(/\r?\n[\t ]+/g," ").split("\r").map((function(t){return 0===t.indexOf("\n")?t.substr(1,t.length):t})).forEach((function(t){var r=t.split(":"),o=r.shift().trim();if(o){var n=r.join(":").trim();e.append(o,n)}})),e)};r.url="responseURL"in u?u.responseURL:r.headers.get("X-Request-URL");var o="response"in u?u.response:u.responseText;setTimeout((function(){n(new m(o,r))}),0)},u.onerror=function(){setTimeout((function(){i(new TypeError("Network request failed"))}),0)},u.ontimeout=function(){setTimeout((function(){i(new TypeError("Network request failed"))}),0)},u.onabort=function(){setTimeout((function(){i(new g("Aborted","AbortError"))}),0)},u.open(a.method,function(t){try{return""===t&&e.location.href?e.location.href:t}catch(e){return t}}(a.url),!0),"include"===a.credentials?u.withCredentials=!0:"omit"===a.credentials&&(u.withCredentials=!1),"responseType"in u&&(o.blob?u.responseType="blob":o.arrayBuffer&&a.headers.get("Content-Type")&&-1!==a.headers.get("Content-Type").indexOf("application/octet-stream")&&(u.responseType="arraybuffer")),!r||"object"!=typeof r.headers||r.headers instanceof f?a.headers.forEach((function(t,e){u.setRequestHeader(e,t)})):Object.getOwnPropertyNames(r.headers).forEach((function(t){u.setRequestHeader(t,s(r.headers[t]))})),a.signal&&(a.signal.addEventListener("abort",l),u.onreadystatechange=function(){4===u.readyState&&a.signal.removeEventListener("abort",l)}),u.send(void 0===a._bodyInit?null:a._bodyInit)}))}A.polyfill=!0,e.fetch||(e.fetch=A,e.Headers=f,e.Request=b,e.Response=m)})()})(); \ No newline at end of file diff --git a/pkg/download/engine/polyfill/package-lock.json b/pkg/download/engine/polyfill/package-lock.json new file mode 100644 index 00000000..7bf817b5 --- /dev/null +++ b/pkg/download/engine/polyfill/package-lock.json @@ -0,0 +1,2236 @@ +{ + "name": "polyfill", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "polyfill", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "blob-polyfill": "^7.0.20220408", + "whatwg-fetch": "^3.6.2" + }, + "devDependencies": { + "webpack": "^5.75.0", + "webpack-cli": "^5.0.1" + } + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmmirror.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.3", + "resolved": "https://registry.npmmirror.com/@jridgewell/source-map/-/source-map-0.3.3.tgz", + "integrity": "sha512-b+fsZXeLYi9fEULmfBrhxn4IrPlINf8fiNarzTof004v3lFdntdwa9PF7vFJqm3mg7s+ScJMxXaE3Acp1irZcg==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.18", + "resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", + "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" + } + }, + "node_modules/@types/eslint": { + "version": "8.37.0", + "resolved": "https://registry.npmmirror.com/@types/eslint/-/eslint-8.37.0.tgz", + "integrity": "sha512-Piet7dG2JBuDIfohBngQ3rCt7MgO9xCO4xIMKxBThCq5PNRB91IjlJ10eJVwfoNtvTErmxLzwBZ7rHZtbOMmFQ==", + "dev": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.4", + "resolved": "https://registry.npmmirror.com/@types/eslint-scope/-/eslint-scope-3.7.4.tgz", + "integrity": "sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==", + "dev": true, + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "0.0.51", + "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-0.0.51.tgz", + "integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==", + "dev": true + }, + "node_modules/@types/json-schema": { + "version": "7.0.11", + "resolved": "https://registry.npmmirror.com/@types/json-schema/-/json-schema-7.0.11.tgz", + "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", + "dev": true + }, + "node_modules/@types/node": { + "version": "18.15.11", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-18.15.11.tgz", + "integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q==", + "dev": true + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/ast/-/ast-1.11.1.tgz", + "integrity": "sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==", + "dev": true, + "dependencies": { + "@webassemblyjs/helper-numbers": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz", + "integrity": "sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz", + "integrity": "sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz", + "integrity": "sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz", + "integrity": "sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ==", + "dev": true, + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.11.1", + "@webassemblyjs/helper-api-error": "1.11.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz", + "integrity": "sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz", + "integrity": "sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-buffer": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/wasm-gen": "1.11.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz", + "integrity": "sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ==", + "dev": true, + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/leb128/-/leb128-1.11.1.tgz", + "integrity": "sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw==", + "dev": true, + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/utf8/-/utf8-1.11.1.tgz", + "integrity": "sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ==", + "dev": true + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz", + "integrity": "sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-buffer": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/helper-wasm-section": "1.11.1", + "@webassemblyjs/wasm-gen": "1.11.1", + "@webassemblyjs/wasm-opt": "1.11.1", + "@webassemblyjs/wasm-parser": "1.11.1", + "@webassemblyjs/wast-printer": "1.11.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz", + "integrity": "sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/ieee754": "1.11.1", + "@webassemblyjs/leb128": "1.11.1", + "@webassemblyjs/utf8": "1.11.1" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz", + "integrity": "sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-buffer": "1.11.1", + "@webassemblyjs/wasm-gen": "1.11.1", + "@webassemblyjs/wasm-parser": "1.11.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz", + "integrity": "sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-api-error": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/ieee754": "1.11.1", + "@webassemblyjs/leb128": "1.11.1", + "@webassemblyjs/utf8": "1.11.1" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz", + "integrity": "sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webpack-cli/configtest": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/@webpack-cli/configtest/-/configtest-2.0.1.tgz", + "integrity": "sha512-njsdJXJSiS2iNbQVS0eT8A/KPnmyH4pv1APj2K0d1wrZcBLw+yppxOy4CGqa0OxDJkzfL/XELDhD8rocnIwB5A==", + "dev": true, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/info": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/@webpack-cli/info/-/info-2.0.1.tgz", + "integrity": "sha512-fE1UEWTwsAxRhrJNikE7v4EotYflkEhBL7EbajfkPlf6E37/2QshOy/D48Mw8G5XMFlQtS6YV42vtbG9zBpIQA==", + "dev": true, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/serve": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/@webpack-cli/serve/-/serve-2.0.1.tgz", + "integrity": "sha512-0G7tNyS+yW8TdgHwZKlDWYXFA6OJQnoLCQvYKkQP0Q2X205PSQ6RNUj0M+1OB/9gRQaUZ/ccYfaxd0nhaWKfjw==", + "dev": true, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + }, + "peerDependenciesMeta": { + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmmirror.com/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true + }, + "node_modules/acorn": { + "version": "8.8.2", + "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.8.2.tgz", + "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-assertions": { + "version": "1.8.0", + "resolved": "https://registry.npmmirror.com/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz", + "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==", + "dev": true, + "peerDependencies": { + "acorn": "^8" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmmirror.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/blob-polyfill": { + "version": "7.0.20220408", + "resolved": "https://registry.npmmirror.com/blob-polyfill/-/blob-polyfill-7.0.20220408.tgz", + "integrity": "sha512-oD8Ydw+5lNoqq+en24iuPt1QixdPpe/nUF8azTHnviCZYu9zUC+TwdzIp5orpblJosNlgNbVmmAb//c6d6ImUQ==" + }, + "node_modules/browserslist": { + "version": "4.21.5", + "resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.21.5.tgz", + "integrity": "sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==", + "dev": true, + "dependencies": { + "caniuse-lite": "^1.0.30001449", + "electron-to-chromium": "^1.4.284", + "node-releases": "^2.0.8", + "update-browserslist-db": "^1.0.10" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001478", + "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001478.tgz", + "integrity": "sha512-gMhDyXGItTHipJj2ApIvR+iVB5hd0KP3svMWWXDvZOmjzJJassGLMfxRkQCSYgGd2gtdL/ReeiyvMSFD1Ss6Mw==", + "dev": true + }, + "node_modules/chrome-trace-event": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", + "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", + "dev": true, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/colorette": { + "version": "2.0.19", + "resolved": "https://registry.npmmirror.com/colorette/-/colorette-2.0.19.tgz", + "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==", + "dev": true + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmmirror.com/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.4.359", + "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.4.359.tgz", + "integrity": "sha512-OoVcngKCIuNXtZnsYoqlCvr0Cf3NIPzDIgwUfI9bdTFjXCrr79lI0kwQstLPZ7WhCezLlGksZk/BFAzoXC7GDw==", + "dev": true + }, + "node_modules/enhanced-resolve": { + "version": "5.12.0", + "resolved": "https://registry.npmmirror.com/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz", + "integrity": "sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/envinfo": { + "version": "7.8.1", + "resolved": "https://registry.npmmirror.com/envinfo/-/envinfo-7.8.1.tgz", + "integrity": "sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==", + "dev": true, + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/es-module-lexer": { + "version": "0.9.3", + "resolved": "https://registry.npmmirror.com/es-module-lexer/-/es-module-lexer-0.9.3.tgz", + "integrity": "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==", + "dev": true + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmmirror.com/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmmirror.com/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmmirror.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true, + "engines": { + "node": ">= 4.9.1" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/import-local": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/import-local/-/import-local-3.1.0.tgz", + "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "dev": true, + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/is-core-module": { + "version": "2.12.0", + "resolved": "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.12.0.tgz", + "integrity": "sha512-RECHCBCd/viahWmwj6enj19sKbHfJrddi/6cBDsNTKbNq0f7VeaUkBo60BqzvPqo/W54ChS62Z5qyun7cfOMqQ==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmmirror.com/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmmirror.com/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "dev": true, + "engines": { + "node": ">=6.11.5" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmmirror.com/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, + "node_modules/node-releases": { + "version": "2.0.10", + "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.10.tgz", + "integrity": "sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==", + "dev": true + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmmirror.com/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/resolve": { + "version": "1.22.2", + "resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.2.tgz", + "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==", + "dev": true, + "dependencies": { + "is-core-module": "^2.11.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + }, + "node_modules/schema-utils": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/schema-utils/-/schema-utils-3.1.1.tgz", + "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/serialize-javascript/-/serialize-javascript-6.0.1.tgz", + "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmmirror.com/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmmirror.com/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/terser": { + "version": "5.16.9", + "resolved": "https://registry.npmmirror.com/terser/-/terser-5.16.9.tgz", + "integrity": "sha512-HPa/FdTB9XGI2H1/keLFZHxl6WNvAI4YalHGtDQTlMnJcoqSab1UwL4l1hGEhs6/GmLHBZIg/YgB++jcbzoOEg==", + "dev": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.2", + "acorn": "^8.5.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.7", + "resolved": "https://registry.npmmirror.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.7.tgz", + "integrity": "sha512-AfKwIktyP7Cu50xNjXF/6Qb5lBNzYaWpU6YfoX3uZicTx0zTy0stDDCsvjDapKsSDvOeWo5MEq4TmdBy2cNoHw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.17", + "jest-worker": "^27.4.5", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.1", + "terser": "^5.16.5" + }, + "engines": { + "node": ">= 10.13.0" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.10", + "resolved": "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", + "integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==", + "dev": true, + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "browserslist-lint": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmmirror.com/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/watchpack": { + "version": "2.4.0", + "resolved": "https://registry.npmmirror.com/watchpack/-/watchpack-2.4.0.tgz", + "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "dev": true, + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack": { + "version": "5.78.0", + "resolved": "https://registry.npmmirror.com/webpack/-/webpack-5.78.0.tgz", + "integrity": "sha512-gT5DP72KInmE/3azEaQrISjTvLYlSM0j1Ezhht/KLVkrqtv10JoP/RXhwmX/frrutOPuSq3o5Vq0ehR/4Vmd1g==", + "dev": true, + "dependencies": { + "@types/eslint-scope": "^3.7.3", + "@types/estree": "^0.0.51", + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/wasm-edit": "1.11.1", + "@webassemblyjs/wasm-parser": "1.11.1", + "acorn": "^8.7.1", + "acorn-import-assertions": "^1.7.6", + "browserslist": "^4.14.5", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.10.0", + "es-module-lexer": "^0.9.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.9", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.1.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.1.3", + "watchpack": "^2.4.0", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-cli": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/webpack-cli/-/webpack-cli-5.0.1.tgz", + "integrity": "sha512-S3KVAyfwUqr0Mo/ur3NzIp6jnerNpo7GUO6so51mxLi1spqsA17YcMXy0WOIJtBSnj748lthxC6XLbNKh/ZC+A==", + "dev": true, + "dependencies": { + "@discoveryjs/json-ext": "^0.5.0", + "@webpack-cli/configtest": "^2.0.1", + "@webpack-cli/info": "^2.0.1", + "@webpack-cli/serve": "^2.0.1", + "colorette": "^2.0.14", + "commander": "^9.4.1", + "cross-spawn": "^7.0.3", + "envinfo": "^7.7.3", + "fastest-levenshtein": "^1.0.12", + "import-local": "^3.0.2", + "interpret": "^3.1.1", + "rechoir": "^0.8.0", + "webpack-merge": "^5.7.3" + }, + "bin": { + "webpack-cli": "bin/cli.js" + }, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x" + }, + "peerDependenciesMeta": { + "@webpack-cli/generators": { + "optional": true + }, + "webpack-bundle-analyzer": { + "optional": true + }, + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/webpack-cli/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmmirror.com/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/webpack-merge": { + "version": "5.8.0", + "resolved": "https://registry.npmmirror.com/webpack-merge/-/webpack-merge-5.8.0.tgz", + "integrity": "sha512-/SaI7xY0831XwP6kzuwhKWVKDP9t1QY1h65lAFLbZqMPIuYcD9QAW4u9STIbU9kaJbPBB/geU/gLr1wDjOhQ+Q==", + "dev": true, + "dependencies": { + "clone-deep": "^4.0.1", + "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmmirror.com/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/whatwg-fetch": { + "version": "3.6.2", + "resolved": "https://registry.npmmirror.com/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz", + "integrity": "sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wildcard": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/wildcard/-/wildcard-2.0.0.tgz", + "integrity": "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==", + "dev": true + } + }, + "dependencies": { + "@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmmirror.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "dev": true + }, + "@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "requires": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true + }, + "@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true + }, + "@jridgewell/source-map": { + "version": "0.3.3", + "resolved": "https://registry.npmmirror.com/@jridgewell/source-map/-/source-map-0.3.3.tgz", + "integrity": "sha512-b+fsZXeLYi9fEULmfBrhxn4IrPlINf8fiNarzTof004v3lFdntdwa9PF7vFJqm3mg7s+ScJMxXaE3Acp1irZcg==", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true + }, + "@jridgewell/trace-mapping": { + "version": "0.3.18", + "resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", + "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" + } + }, + "@types/eslint": { + "version": "8.37.0", + "resolved": "https://registry.npmmirror.com/@types/eslint/-/eslint-8.37.0.tgz", + "integrity": "sha512-Piet7dG2JBuDIfohBngQ3rCt7MgO9xCO4xIMKxBThCq5PNRB91IjlJ10eJVwfoNtvTErmxLzwBZ7rHZtbOMmFQ==", + "dev": true, + "requires": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "@types/eslint-scope": { + "version": "3.7.4", + "resolved": "https://registry.npmmirror.com/@types/eslint-scope/-/eslint-scope-3.7.4.tgz", + "integrity": "sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==", + "dev": true, + "requires": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "@types/estree": { + "version": "0.0.51", + "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-0.0.51.tgz", + "integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==", + "dev": true + }, + "@types/json-schema": { + "version": "7.0.11", + "resolved": "https://registry.npmmirror.com/@types/json-schema/-/json-schema-7.0.11.tgz", + "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", + "dev": true + }, + "@types/node": { + "version": "18.15.11", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-18.15.11.tgz", + "integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q==", + "dev": true + }, + "@webassemblyjs/ast": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/ast/-/ast-1.11.1.tgz", + "integrity": "sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==", + "dev": true, + "requires": { + "@webassemblyjs/helper-numbers": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1" + } + }, + "@webassemblyjs/floating-point-hex-parser": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz", + "integrity": "sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ==", + "dev": true + }, + "@webassemblyjs/helper-api-error": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz", + "integrity": "sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg==", + "dev": true + }, + "@webassemblyjs/helper-buffer": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz", + "integrity": "sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA==", + "dev": true + }, + "@webassemblyjs/helper-numbers": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz", + "integrity": "sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ==", + "dev": true, + "requires": { + "@webassemblyjs/floating-point-hex-parser": "1.11.1", + "@webassemblyjs/helper-api-error": "1.11.1", + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/helper-wasm-bytecode": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz", + "integrity": "sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q==", + "dev": true + }, + "@webassemblyjs/helper-wasm-section": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz", + "integrity": "sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-buffer": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/wasm-gen": "1.11.1" + } + }, + "@webassemblyjs/ieee754": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz", + "integrity": "sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ==", + "dev": true, + "requires": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "@webassemblyjs/leb128": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/leb128/-/leb128-1.11.1.tgz", + "integrity": "sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw==", + "dev": true, + "requires": { + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/utf8": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/utf8/-/utf8-1.11.1.tgz", + "integrity": "sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ==", + "dev": true + }, + "@webassemblyjs/wasm-edit": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz", + "integrity": "sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-buffer": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/helper-wasm-section": "1.11.1", + "@webassemblyjs/wasm-gen": "1.11.1", + "@webassemblyjs/wasm-opt": "1.11.1", + "@webassemblyjs/wasm-parser": "1.11.1", + "@webassemblyjs/wast-printer": "1.11.1" + } + }, + "@webassemblyjs/wasm-gen": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz", + "integrity": "sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/ieee754": "1.11.1", + "@webassemblyjs/leb128": "1.11.1", + "@webassemblyjs/utf8": "1.11.1" + } + }, + "@webassemblyjs/wasm-opt": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz", + "integrity": "sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-buffer": "1.11.1", + "@webassemblyjs/wasm-gen": "1.11.1", + "@webassemblyjs/wasm-parser": "1.11.1" + } + }, + "@webassemblyjs/wasm-parser": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz", + "integrity": "sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-api-error": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/ieee754": "1.11.1", + "@webassemblyjs/leb128": "1.11.1", + "@webassemblyjs/utf8": "1.11.1" + } + }, + "@webassemblyjs/wast-printer": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz", + "integrity": "sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.1", + "@xtuc/long": "4.2.2" + } + }, + "@webpack-cli/configtest": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/@webpack-cli/configtest/-/configtest-2.0.1.tgz", + "integrity": "sha512-njsdJXJSiS2iNbQVS0eT8A/KPnmyH4pv1APj2K0d1wrZcBLw+yppxOy4CGqa0OxDJkzfL/XELDhD8rocnIwB5A==", + "dev": true, + "requires": {} + }, + "@webpack-cli/info": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/@webpack-cli/info/-/info-2.0.1.tgz", + "integrity": "sha512-fE1UEWTwsAxRhrJNikE7v4EotYflkEhBL7EbajfkPlf6E37/2QshOy/D48Mw8G5XMFlQtS6YV42vtbG9zBpIQA==", + "dev": true, + "requires": {} + }, + "@webpack-cli/serve": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/@webpack-cli/serve/-/serve-2.0.1.tgz", + "integrity": "sha512-0G7tNyS+yW8TdgHwZKlDWYXFA6OJQnoLCQvYKkQP0Q2X205PSQ6RNUj0M+1OB/9gRQaUZ/ccYfaxd0nhaWKfjw==", + "dev": true, + "requires": {} + }, + "@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true + }, + "@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmmirror.com/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true + }, + "acorn": { + "version": "8.8.2", + "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.8.2.tgz", + "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", + "dev": true + }, + "acorn-import-assertions": { + "version": "1.8.0", + "resolved": "https://registry.npmmirror.com/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz", + "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==", + "dev": true, + "requires": {} + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmmirror.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "requires": {} + }, + "blob-polyfill": { + "version": "7.0.20220408", + "resolved": "https://registry.npmmirror.com/blob-polyfill/-/blob-polyfill-7.0.20220408.tgz", + "integrity": "sha512-oD8Ydw+5lNoqq+en24iuPt1QixdPpe/nUF8azTHnviCZYu9zUC+TwdzIp5orpblJosNlgNbVmmAb//c6d6ImUQ==" + }, + "browserslist": { + "version": "4.21.5", + "resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.21.5.tgz", + "integrity": "sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001449", + "electron-to-chromium": "^1.4.284", + "node-releases": "^2.0.8", + "update-browserslist-db": "^1.0.10" + } + }, + "buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "caniuse-lite": { + "version": "1.0.30001478", + "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001478.tgz", + "integrity": "sha512-gMhDyXGItTHipJj2ApIvR+iVB5hd0KP3svMWWXDvZOmjzJJassGLMfxRkQCSYgGd2gtdL/ReeiyvMSFD1Ss6Mw==", + "dev": true + }, + "chrome-trace-event": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", + "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", + "dev": true + }, + "clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + } + }, + "colorette": { + "version": "2.0.19", + "resolved": "https://registry.npmmirror.com/colorette/-/colorette-2.0.19.tgz", + "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==", + "dev": true + }, + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmmirror.com/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "electron-to-chromium": { + "version": "1.4.359", + "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.4.359.tgz", + "integrity": "sha512-OoVcngKCIuNXtZnsYoqlCvr0Cf3NIPzDIgwUfI9bdTFjXCrr79lI0kwQstLPZ7WhCezLlGksZk/BFAzoXC7GDw==", + "dev": true + }, + "enhanced-resolve": { + "version": "5.12.0", + "resolved": "https://registry.npmmirror.com/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz", + "integrity": "sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + } + }, + "envinfo": { + "version": "7.8.1", + "resolved": "https://registry.npmmirror.com/envinfo/-/envinfo-7.8.1.tgz", + "integrity": "sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==", + "dev": true + }, + "es-module-lexer": { + "version": "0.9.3", + "resolved": "https://registry.npmmirror.com/es-module-lexer/-/es-module-lexer-0.9.3.tgz", + "integrity": "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==", + "dev": true + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true + }, + "eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + } + }, + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "requires": { + "estraverse": "^5.2.0" + }, + "dependencies": { + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmmirror.com/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + } + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + }, + "events": { + "version": "3.3.0", + "resolved": "https://registry.npmmirror.com/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmmirror.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true + }, + "graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "import-local": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/import-local/-/import-local-3.1.0.tgz", + "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "dev": true, + "requires": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + } + }, + "interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true + }, + "is-core-module": { + "version": "2.12.0", + "resolved": "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.12.0.tgz", + "integrity": "sha512-RECHCBCd/viahWmwj6enj19sKbHfJrddi/6cBDsNTKbNq0f7VeaUkBo60BqzvPqo/W54ChS62Z5qyun7cfOMqQ==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true + }, + "jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmmirror.com/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "requires": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + } + }, + "json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmmirror.com/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true + }, + "loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "dev": true + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "requires": { + "mime-db": "1.52.0" + } + }, + "neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmmirror.com/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, + "node-releases": { + "version": "2.0.10", + "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.10.tgz", + "integrity": "sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==", + "dev": true + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "requires": { + "find-up": "^4.0.0" + } + }, + "punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "dev": true + }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmmirror.com/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, + "requires": { + "resolve": "^1.20.0" + } + }, + "resolve": { + "version": "1.22.2", + "resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.2.tgz", + "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==", + "dev": true, + "requires": { + "is-core-module": "^2.11.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "requires": { + "resolve-from": "^5.0.0" + } + }, + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + }, + "schema-utils": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/schema-utils/-/schema-utils-3.1.1.tgz", + "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + } + }, + "serialize-javascript": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/serialize-javascript/-/serialize-javascript-6.0.1.tgz", + "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } + }, + "shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "requires": { + "kind-of": "^6.0.2" + } + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmmirror.com/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true + }, + "tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmmirror.com/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true + }, + "terser": { + "version": "5.16.9", + "resolved": "https://registry.npmmirror.com/terser/-/terser-5.16.9.tgz", + "integrity": "sha512-HPa/FdTB9XGI2H1/keLFZHxl6WNvAI4YalHGtDQTlMnJcoqSab1UwL4l1hGEhs6/GmLHBZIg/YgB++jcbzoOEg==", + "dev": true, + "requires": { + "@jridgewell/source-map": "^0.3.2", + "acorn": "^8.5.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + } + }, + "terser-webpack-plugin": { + "version": "5.3.7", + "resolved": "https://registry.npmmirror.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.7.tgz", + "integrity": "sha512-AfKwIktyP7Cu50xNjXF/6Qb5lBNzYaWpU6YfoX3uZicTx0zTy0stDDCsvjDapKsSDvOeWo5MEq4TmdBy2cNoHw==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "^0.3.17", + "jest-worker": "^27.4.5", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.1", + "terser": "^5.16.5" + } + }, + "update-browserslist-db": { + "version": "1.0.10", + "resolved": "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", + "integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==", + "dev": true, + "requires": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + } + }, + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmmirror.com/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "watchpack": { + "version": "2.4.0", + "resolved": "https://registry.npmmirror.com/watchpack/-/watchpack-2.4.0.tgz", + "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "dev": true, + "requires": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + } + }, + "webpack": { + "version": "5.78.0", + "resolved": "https://registry.npmmirror.com/webpack/-/webpack-5.78.0.tgz", + "integrity": "sha512-gT5DP72KInmE/3azEaQrISjTvLYlSM0j1Ezhht/KLVkrqtv10JoP/RXhwmX/frrutOPuSq3o5Vq0ehR/4Vmd1g==", + "dev": true, + "requires": { + "@types/eslint-scope": "^3.7.3", + "@types/estree": "^0.0.51", + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/wasm-edit": "1.11.1", + "@webassemblyjs/wasm-parser": "1.11.1", + "acorn": "^8.7.1", + "acorn-import-assertions": "^1.7.6", + "browserslist": "^4.14.5", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.10.0", + "es-module-lexer": "^0.9.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.9", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.1.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.1.3", + "watchpack": "^2.4.0", + "webpack-sources": "^3.2.3" + } + }, + "webpack-cli": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/webpack-cli/-/webpack-cli-5.0.1.tgz", + "integrity": "sha512-S3KVAyfwUqr0Mo/ur3NzIp6jnerNpo7GUO6so51mxLi1spqsA17YcMXy0WOIJtBSnj748lthxC6XLbNKh/ZC+A==", + "dev": true, + "requires": { + "@discoveryjs/json-ext": "^0.5.0", + "@webpack-cli/configtest": "^2.0.1", + "@webpack-cli/info": "^2.0.1", + "@webpack-cli/serve": "^2.0.1", + "colorette": "^2.0.14", + "commander": "^9.4.1", + "cross-spawn": "^7.0.3", + "envinfo": "^7.7.3", + "fastest-levenshtein": "^1.0.12", + "import-local": "^3.0.2", + "interpret": "^3.1.1", + "rechoir": "^0.8.0", + "webpack-merge": "^5.7.3" + }, + "dependencies": { + "commander": { + "version": "9.5.0", + "resolved": "https://registry.npmmirror.com/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true + } + } + }, + "webpack-merge": { + "version": "5.8.0", + "resolved": "https://registry.npmmirror.com/webpack-merge/-/webpack-merge-5.8.0.tgz", + "integrity": "sha512-/SaI7xY0831XwP6kzuwhKWVKDP9t1QY1h65lAFLbZqMPIuYcD9QAW4u9STIbU9kaJbPBB/geU/gLr1wDjOhQ+Q==", + "dev": true, + "requires": { + "clone-deep": "^4.0.1", + "wildcard": "^2.0.0" + } + }, + "webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmmirror.com/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "dev": true + }, + "whatwg-fetch": { + "version": "3.6.2", + "resolved": "https://registry.npmmirror.com/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz", + "integrity": "sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==" + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "wildcard": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/wildcard/-/wildcard-2.0.0.tgz", + "integrity": "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==", + "dev": true + } + } +} diff --git a/pkg/download/engine/polyfill/package.json b/pkg/download/engine/polyfill/package.json new file mode 100644 index 00000000..b123c732 --- /dev/null +++ b/pkg/download/engine/polyfill/package.json @@ -0,0 +1,22 @@ +{ + "name": "polyfill", + "version": "1.0.0", + "description": "", + "main": "index.js", + "type": "module", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "dev": "webpack --mode production --watch", + "build": "webpack --mode production" + }, + "author": "", + "license": "ISC", + "devDependencies": { + "webpack": "^5.75.0", + "webpack-cli": "^5.0.1" + }, + "dependencies": { + "blob-polyfill": "^7.0.20220408", + "whatwg-fetch": "^3.6.2" + } +} diff --git a/pkg/download/engine/polyfill/src/blob/index.js b/pkg/download/engine/polyfill/src/blob/index.js new file mode 100644 index 00000000..1d854e16 --- /dev/null +++ b/pkg/download/engine/polyfill/src/blob/index.js @@ -0,0 +1,3 @@ +import { Blob } from "blob-polyfill"; + +globalThis.Blob = Blob; \ No newline at end of file diff --git a/pkg/download/engine/polyfill/src/crypto/index.js b/pkg/download/engine/polyfill/src/crypto/index.js new file mode 100644 index 00000000..fed0c8f0 --- /dev/null +++ b/pkg/download/engine/polyfill/src/crypto/index.js @@ -0,0 +1,13 @@ +globalThis.crypto = { + getRandomValues(arr) { + for (let i = 0, len = arr.length; i < len; i++) { + arr[i] = Math.floor(Math.random() * 256); + } + return arr; + }, + randomUUID() { + return '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, (c) => { + return (c ^ (this.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))).toString(16) + }) + } +} \ No newline at end of file diff --git a/pkg/download/engine/polyfill/src/fetch/index.js b/pkg/download/engine/polyfill/src/fetch/index.js new file mode 100644 index 00000000..35dae088 --- /dev/null +++ b/pkg/download/engine/polyfill/src/fetch/index.js @@ -0,0 +1 @@ +import 'whatwg-fetch' \ No newline at end of file diff --git a/pkg/download/engine/polyfill/src/index.js b/pkg/download/engine/polyfill/src/index.js new file mode 100644 index 00000000..abf0c1bd --- /dev/null +++ b/pkg/download/engine/polyfill/src/index.js @@ -0,0 +1,3 @@ +import "./blob/index.js" +import "./crypto/index.js" +import "./fetch/index.js" \ No newline at end of file diff --git a/pkg/download/engine/polyfill/webpack.config.js b/pkg/download/engine/polyfill/webpack.config.js new file mode 100644 index 00000000..499e1a22 --- /dev/null +++ b/pkg/download/engine/polyfill/webpack.config.js @@ -0,0 +1,12 @@ +import path from "path"; +import { fileURLToPath } from "url"; + +const __dirname = fileURLToPath(import.meta.url); + +export default { + entry: "./src/index.js", + output: { + filename: "index.js", + path: path.resolve(__dirname, "../out"), + }, +}; diff --git a/pkg/download/event.go b/pkg/download/event.go index 87a4361d..fc410944 100644 --- a/pkg/download/event.go +++ b/pkg/download/event.go @@ -5,7 +5,6 @@ type EventKey string const ( EventKeyStart = "start" EventKeyPause = "pause" - EventKeyContinue = "continue" EventKeyProgress = "progress" EventKeyError = "error" EventKeyDelete = "delete" diff --git a/pkg/download/extension.go b/pkg/download/extension.go new file mode 100644 index 00000000..4a3eef29 --- /dev/null +++ b/pkg/download/extension.go @@ -0,0 +1,549 @@ +package download + +import ( + "encoding/json" + "fmt" + "github.com/GopeedLab/gopeed/internal/logger" + "github.com/GopeedLab/gopeed/pkg/base" + "github.com/GopeedLab/gopeed/pkg/download/engine" + "github.com/GopeedLab/gopeed/pkg/util" + "github.com/dop251/goja" + "github.com/go-git/go-git/v5" + "io" + "os" + "path" + "path/filepath" + "strconv" + "strings" + "time" +) + +var ( + gitSuffix = ".git" + + tempExtensionsDir = ".extensions" + extensionsDir = "extensions" + extensionIgnoreDirs = []string{gitSuffix, "node_modules"} + + ErrExtensionNoManifest = fmt.Errorf("manifest.json not found") + ErrExtensionNotFound = fmt.Errorf("extension not found") +) + +type ActivationEvent string + +const ( + EventOnResolve ActivationEvent = "onResolve" + //EventOnError ActivationEvent = "onError" + //EventOnDone ActivationEvent = "onDone" +) + +func (d *Downloader) InstallExtensionByGit(url string) (*Extension, error) { + return d.fetchExtensionByGit(url, d.InstallExtensionByFolder) +} + +func (d *Downloader) InstallExtensionByFolder(path string, devMode bool) (*Extension, error) { + ext, err := d.parseExtensionByPath(path) + if err != nil { + return nil, err + } + + // if dev mode, don't copy the extension to the extensions' directory + if devMode { + ext.DevMode = true + ext.DevPath, _ = filepath.Abs(path) + } else { + if err = util.CopyDir(path, d.ExtensionPath(ext), extensionIgnoreDirs...); err != nil { + return nil, err + } + } + + // if extension is not installed, add it to the list, otherwise update it + installedExt := d.getExtension(ext.Identity) + if installedExt == nil { + ext.CreatedAt = time.Now() + ext.UpdatedAt = ext.CreatedAt + installedExt = ext + d.extensions = append(d.extensions, installedExt) + } else { + installedExt.update(ext) + } + if err = d.storage.Put(bucketExtension, installedExt.Identity, installedExt); err != nil { + return nil, err + } + return installedExt, nil +} + +// UpgradeCheckExtension Check if there is a new version for the extension. +func (d *Downloader) UpgradeCheckExtension(identity string) (newVersion string, err error) { + ext, err := d.GetExtension(identity) + if err != nil { + return + } + installUrl := ext.buildInstallUrl() + if installUrl == "" { + return + } + _, err = d.fetchExtensionByGit(installUrl, func(tempExtPath string, devMode bool) (*Extension, error) { + tempExt, err := d.parseExtensionByPath(tempExtPath) + if err != nil { + return nil, err + } + if tempExt.Version != ext.Version { + newVersion = tempExt.Version + } + return tempExt, nil + }) + return +} + +func (d *Downloader) UpgradeExtension(identity string) error { + ext, err := d.GetExtension(identity) + if err != nil { + return err + } + installUrl := ext.buildInstallUrl() + if installUrl == "" { + return nil + } + if _, err := d.InstallExtensionByGit(installUrl); err != nil { + return err + } + return nil +} + +func (d *Downloader) UpdateExtensionSettings(identity string, settings map[string]any) error { + ext, err := d.GetExtension(identity) + if err != nil { + return err + } + for _, setting := range ext.Settings { + if value, ok := settings[setting.Name]; ok { + setting.Value = tryParse(value, setting.Type) + } + } + return d.storage.Put(bucketExtension, ext.Identity, ext) +} + +func (d *Downloader) SwitchExtension(identity string, status bool) error { + ext, err := d.GetExtension(identity) + if err != nil { + return err + } + ext.Disabled = !status + return d.storage.Put(bucketExtension, ext.Identity, ext) +} + +func (d *Downloader) DeleteExtension(identity string) error { + ext, err := d.GetExtension(identity) + if err != nil { + return err + } + // remove from disk + if !ext.DevMode { + if err := os.RemoveAll(d.ExtensionPath(ext)); err != nil { + return err + } + } + // remove from extensions + for i, e := range d.extensions { + if e.Identity == identity { + d.extensions = append(d.extensions[:i], d.extensions[i+1:]...) + break + } + } + return d.storage.Delete(bucketExtension, identity) +} + +func (d *Downloader) GetExtensions() []*Extension { + return d.extensions +} + +func (d *Downloader) GetExtension(identity string) (*Extension, error) { + extension := d.getExtension(identity) + if extension == nil { + return nil, ErrExtensionNotFound + } + return extension, nil +} + +func (d *Downloader) getExtension(identity string) *Extension { + for _, ext := range d.extensions { + if ext.Identity == identity { + return ext + } + } + return nil +} + +func (d *Downloader) fetchExtensionByGit(url string, handler func(tempExtPath string, devMode bool) (*Extension, error)) (*Extension, error) { + if !strings.HasPrefix(url, "https://") && !strings.HasPrefix(url, "http://") { + url = "https://" + url + } + + // resolve project path + parentPath, projectPath := path.Split(url) + // resolve project name and sub path + pathArr := strings.SplitN(projectPath, "#", 2) + projectPath = strings.TrimSuffix(pathArr[0], gitSuffix) + subPath := "" + if len(pathArr) > 1 { + subPath = pathArr[1] + } + + tempExtDir := filepath.Join(d.cfg.StorageDir, tempExtensionsDir, projectPath) + if err := util.RmAndMkDirAll(tempExtDir); err != nil { + return nil, err + } + // clone project to extension temp dir + gitUrl := parentPath + projectPath + gitSuffix + if _, err := git.PlainClone(tempExtDir, false, &git.CloneOptions{ + URL: gitUrl, + Depth: 1, + }); err != nil { + return nil, err + } + defer os.RemoveAll(tempExtDir) + + return handler(filepath.Join(tempExtDir, subPath), false) +} + +func (d *Downloader) parseExtensionByPath(path string) (*Extension, error) { + // resolve extension manifest + manifestTempPath := filepath.Join(path, "manifest.json") + if _, err := os.Stat(manifestTempPath); os.IsNotExist(err) { + return nil, ErrExtensionNoManifest + } + file, err := os.ReadFile(manifestTempPath) + if err != nil { + return nil, err + } + var ext Extension + if err = json.Unmarshal(file, &ext); err != nil { + return nil, err + } + if err = ext.validate(); err != nil { + return nil, err + } + ext.Identity = ext.buildIdentity() + return &ext, nil +} + +func (d *Downloader) triggerOnResolve(req *base.Request) (res *base.Resource) { + // init extension global object + gopeed := &Instance{ + Events: make(InstanceEvents), + } + ctx := &Context{ + Req: req, + } + var err error + for _, ext := range d.extensions { + if ext.Disabled { + continue + } + for _, script := range ext.Scripts { + if script.match(EventOnResolve, req.URL) { + gopeed.Logger = newInstanceLogger(ext, d.ExtensionLogger) + scriptFilePath := filepath.Join(d.ExtensionPath(ext), script.Entry) + if _, err = os.Stat(scriptFilePath); os.IsNotExist(err) { + continue + } + func() { + var scriptFile *os.File + scriptFile, err = os.Open(scriptFilePath) + if err != nil { + gopeed.Logger.logger.Error().Err(err).Msgf("[%s] open script file failed", ext.buildIdentity()) + return + } + defer scriptFile.Close() + ctx.Settings = parseSettings(ext.Settings) + var scriptBuf []byte + scriptBuf, err = io.ReadAll(scriptFile) + if err != nil { + gopeed.Logger.logger.Error().Err(err).Msgf("[%s] read script file failed", ext.buildIdentity()) + return + } + engine := engine.NewEngine() + defer engine.Close() + err = engine.Runtime.Set("gopeed", gopeed) + if err != nil { + gopeed.Logger.logger.Error().Err(err).Msgf("[%s] engine inject failed", ext.buildIdentity()) + return + } + _, err = engine.RunString(string(scriptBuf)) + if err != nil { + gopeed.Logger.logger.Error().Err(err).Msgf("[%s] run script failed", ext.buildIdentity()) + return + } + if fn, ok := gopeed.Events[EventOnResolve]; ok { + _, err = engine.CallFunction(fn, ctx) + if err != nil { + gopeed.Logger.logger.Error().Err(err).Msgf("[%s] call function failed: %s", EventOnResolve, ext.buildIdentity()) + return + } + // calculate resource total size + if ctx.Res != nil && len(ctx.Res.Files) > 0 { + if err = ctx.Res.Validate(); err != nil { + gopeed.Logger.logger.Warn().Err(err).Msgf("[%s] resource invalid", ext.buildIdentity()) + return + } + ctx.Res.CalcSize() + } + res = ctx.Res + } + }() + } + } + } + return +} + +func (d *Downloader) ExtensionPath(ext *Extension) string { + if ext.DevMode { + return ext.DevPath + } + return filepath.Join(d.cfg.StorageDir, extensionsDir, ext.Identity) +} + +type Extension struct { + // Identity is global unique for an extension, it's a combination of author and name + Identity string `json:"identity"` + Name string `json:"name"` + Author string `json:"author"` + Title string `json:"title"` + Description string `json:"description"` + Icon string `json:"icon"` + // Version semantic version string, like: 1.0.0 + Version string `json:"version"` + // Homepage homepage url + Homepage string `json:"homepage"` + // Repository git repository info + Repository *Repository `json:"repository"` + Scripts []*Script `json:"scripts"` + Settings []*Setting `json:"settings"` + // Disabled if true, this extension will be ignored + Disabled bool `json:"disabled"` + + DevMode bool `json:"devMode"` + // DevPath is the local path of extension source code + DevPath string `json:"devPath"` + + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} + +func (e *Extension) validate() error { + if e.Name == "" { + return fmt.Errorf("extension name is required") + } + if e.Title == "" { + return fmt.Errorf("extension title is required") + } + if e.Version == "" { + return fmt.Errorf("extension version is required") + } + return nil +} + +func (e *Extension) buildIdentity() string { + if e.Author == "" { + return e.Name + } + return e.Author + "@" + e.Name +} + +func (e *Extension) buildInstallUrl() string { + if e.Repository == nil || e.Repository.Url == "" { + return "" + } + repoUrl := e.Repository.Url + if e.Repository.Directory != "" { + if strings.HasSuffix(repoUrl, "/") { + repoUrl = repoUrl[:len(repoUrl)-1] + } + dir := e.Repository.Directory + if strings.HasPrefix(dir, "/") { + dir = dir[1:] + } + repoUrl = repoUrl + "#" + dir + } + return repoUrl +} + +func (e *Extension) update(newExt *Extension) error { + e.Title = newExt.Title + e.Description = newExt.Description + e.Icon = newExt.Icon + e.Version = newExt.Version + e.Homepage = newExt.Homepage + e.Repository = newExt.Repository + e.Scripts = newExt.Scripts + // don't override settings + //e.Settings = newExt.Settings + e.UpdatedAt = time.Now() + return nil +} + +type Repository struct { + Url string `json:"url"` + Directory string `json:"directory"` +} + +type Script struct { + // Event active event name + Event string `json:"event"` + // Matches match request url pattern list + Matches []string `json:"matches"` + // Entry js script file path + Entry string `json:"entry"` +} + +func (s *Script) match(event ActivationEvent, url string) bool { + if s.Event == "" { + return false + } + if s.Event != string(event) { + return false + } + if len(s.Matches) == 0 { + return false + } + for _, match := range s.Matches { + if util.Match(match, url) { + return true + } + } + return false +} + +type SettingType string + +const ( + SettingTypeString SettingType = "string" + SettingTypeNumber SettingType = "number" + SettingTypeBoolean SettingType = "boolean" +) + +type Setting struct { + Name string `json:"name"` + Title string `json:"title"` + Description string `json:"description"` + Required bool `json:"required"` + // setting type + Type SettingType `json:"type"` + // setting value + Value any `json:"value"` + //Multiple bool `json:"multiple"` + Options []*Option `json:"options"` +} + +type Option struct { + Label string `json:"label"` + Value any `json:"value"` +} + +// Instance inject to js context +type Instance struct { + Events InstanceEvents `json:"events"` + Logger *InstanceLogger `json:"logger"` +} + +type InstanceEvents map[ActivationEvent]goja.Callable + +func (h InstanceEvents) register(name ActivationEvent, fn goja.Callable) { + h[name] = fn +} + +func (h InstanceEvents) OnResolve(fn goja.Callable) { + h.register(EventOnResolve, fn) +} + +//func (h InstanceEvents) OnError(fn goja.Callable) { +// h.register(HookEventOnError, fn) +//} +// +//func (h InstanceEvents) OnDone(fn goja.Callable) { +// h.register(HookEventOnDone, fn) +//} + +type InstanceLogger struct { + identity string + devMode bool + logger *logger.Logger +} + +func (l *InstanceLogger) Debug(msg ...any) { + if l.devMode { + l.logger.Debug().Msg(l.append(msg...)) + } +} + +func (l *InstanceLogger) Info(msg ...any) { + l.logger.Info().Msg(l.append(msg...)) +} + +func (l *InstanceLogger) Warn(msg ...any) { + l.logger.Warn().Msg(l.append(msg...)) +} + +func (l *InstanceLogger) Error(msg ...any) { + l.logger.Error().Msg(l.append(msg...)) +} + +func (l *InstanceLogger) append(msg ...any) string { + strMsg := make([]string, len(msg)) + for i, m := range msg { + strMsg[i] = fmt.Sprint(m) + } + return fmt.Sprintf("[%s] %s", l.identity, strings.Join(strMsg, " ")) +} + +func newInstanceLogger(extension *Extension, logger *logger.Logger) *InstanceLogger { + return &InstanceLogger{ + identity: extension.buildIdentity(), + devMode: extension.DevMode, + logger: logger, + } +} + +type Context struct { + Req *base.Request `json:"req"` + Res *base.Resource `json:"res"` + Settings map[string]any `json:"settings"` +} + +func parseSettings(settings []*Setting) map[string]any { + m := make(map[string]any) + for _, s := range settings { + var val any + if s.Value != nil { + val = s.Value + } + m[s.Name] = tryParse(val, s.Type) + } + return m +} + +func tryParse(val any, settingType SettingType) any { + if val == nil { + return nil + } + switch settingType { + case SettingTypeString: + return fmt.Sprint(val) + case SettingTypeNumber: + vv, err := strconv.ParseFloat(fmt.Sprint(val), 64) + if err != nil { + return nil + } + return vv + case SettingTypeBoolean: + vv, err := strconv.ParseBool(fmt.Sprint(val)) + if err != nil { + return nil + } + return vv + default: + return nil + } +} diff --git a/pkg/download/extension_test.go b/pkg/download/extension_test.go new file mode 100644 index 00000000..a8cb13d3 --- /dev/null +++ b/pkg/download/extension_test.go @@ -0,0 +1,245 @@ +package download + +import ( + "github.com/GopeedLab/gopeed/internal/logger" + "github.com/GopeedLab/gopeed/pkg/base" + "os" + "testing" +) + +func TestDownloader_InstallExtensionByFolder(t *testing.T) { + setupDownloader(func(downloader *Downloader) { + if _, err := downloader.InstallExtensionByFolder("./testdata/extensions/basic", false); err != nil { + t.Fatal(err) + } + rr, err := downloader.Resolve(&base.Request{ + URL: "https://github.com/test", + }) + if err != nil { + t.Fatal(err) + } + if len(rr.Res.Files) == 1 { + t.Fatal("resolve error") + } + }) +} + +func TestDownloader_InstallExtensionByFolderDevMode(t *testing.T) { + setupDownloader(func(downloader *Downloader) { + if _, err := downloader.InstallExtensionByFolder("./testdata/extensions/basic", true); err != nil { + t.Fatal(err) + } + rr, err := downloader.Resolve(&base.Request{ + URL: "https://github.com/test", + }) + if err != nil { + t.Fatal(err) + } + if len(rr.Res.Files) == 1 { + t.Fatal("resolve error") + } + }) +} + +func TestDownloader_InstallExtensionByGit(t *testing.T) { + setupDownloader(func(downloader *Downloader) { + if _, err := downloader.InstallExtensionByGit("https://github.com/GopeedLab/gopeed-extension-samples#github-release-sample"); err != nil { + t.Fatal(err) + } + rr, err := downloader.Resolve(&base.Request{ + URL: "https://github.com/GopeedLab/gopeed/releases", + }) + if err != nil { + t.Fatal(err) + } + if len(rr.Res.Files) == 1 { + t.Fatal("resolve error") + } + }) +} + +func TestDownloader_InstallExtensionByGitSimple(t *testing.T) { + setupDownloader(func(downloader *Downloader) { + if _, err := downloader.InstallExtensionByGit("github.com/GopeedLab/gopeed-extension-samples#github-release-sample"); err != nil { + t.Fatal(err) + } + rr, err := downloader.Resolve(&base.Request{ + URL: "https://github.com/GopeedLab/gopeed/releases", + }) + if err != nil { + t.Fatal(err) + } + if len(rr.Res.Files) == 1 { + t.Fatal("resolve error") + } + }) +} + +func TestDownloader_InstallExtensionByGitFull(t *testing.T) { + setupDownloader(func(downloader *Downloader) { + if _, err := downloader.InstallExtensionByGit("https://github.com/GopeedLab/gopeed-extension-samples.git#github-release-sample"); err != nil { + t.Fatal(err) + } + rr, err := downloader.Resolve(&base.Request{ + URL: "https://github.com/GopeedLab/gopeed/releases", + }) + if err != nil { + t.Fatal(err) + } + if len(rr.Res.Files) == 1 { + t.Fatal("resolve error") + } + }) +} + +func TestDownloader_UpgradeExtension(t *testing.T) { + setupDownloader(func(downloader *Downloader) { + installedExt, err := downloader.InstallExtensionByFolder("./testdata/extensions/update", false) + if err != nil { + t.Fatal(err) + } + extensions := downloader.GetExtensions() + if len(extensions) == 0 { + t.Fatal("extension not installed") + } + oldVersion := installedExt.Version + // fetch new version from git + newVersion, err := downloader.UpgradeCheckExtension(installedExt.Identity) + if err != nil { + t.Fatal(err) + } + if newVersion == "" { + t.Fatal("new version not found") + } + // update extension + if err = downloader.UpgradeExtension(installedExt.Identity); err != nil { + t.Fatal(err) + } + upgradeExt := downloader.getExtension(installedExt.Identity) + if upgradeExt.Version == oldVersion { + t.Fatal("extension update fail") + } + + rr, err := downloader.Resolve(&base.Request{ + URL: "https://github.com/GopeedLab/gopeed/releases", + }) + if err != nil { + t.Fatal(err) + } + if len(rr.Res.Files) == 1 { + t.Fatal("resolve error") + } + }) +} + +func TestDownloader_Extension_Settings(t *testing.T) { + setupDownloader(func(downloader *Downloader) { + if _, err := downloader.InstallExtensionByFolder("./testdata/extensions/settings_empty", false); err != nil { + t.Fatal(err) + } + rr, err := downloader.Resolve(&base.Request{ + URL: "https://github.com/test", + }) + if err != nil { + t.Fatal(err) + } + if len(rr.Res.Files) == 1 { + t.Fatal("settings parse error") + } + }) + + setupDownloader(func(downloader *Downloader) { + installedExt, err := downloader.InstallExtensionByFolder("./testdata/extensions/settings_all", false) + if err != nil { + t.Fatal(err) + } + downloader.UpdateExtensionSettings(installedExt.Identity, map[string]any{ + "stringValued": "valued", + "numberValued": 1.1, + "booleanValued": true, + }) + rr, err := downloader.Resolve(&base.Request{ + URL: "https://github.com/test", + }) + if err != nil { + t.Fatal(err) + } + if len(rr.Res.Files) == 1 { + t.Fatal("settings parse error") + } + }) +} + +func TestDownloader_SwitchExtension(t *testing.T) { + setupDownloader(func(downloader *Downloader) { + installedExt, err := downloader.InstallExtensionByFolder("./testdata/extensions/basic", false) + if err != nil { + t.Fatal(err) + } + if installedExt.Disabled == true { + t.Fatal("extension disabled") + } + if err = downloader.SwitchExtension(installedExt.Identity, false); err != nil { + t.Fatal(err) + } + if installedExt.Disabled == false { + t.Fatal("extension enabled") + } + }) +} + +func TestDownloader_DeleteExtension(t *testing.T) { + setupDownloader(func(downloader *Downloader) { + installedExt, err := downloader.InstallExtensionByFolder("./testdata/extensions/settings_all", false) + if err != nil { + t.Fatal(err) + } + extensions := downloader.GetExtensions() + if err := downloader.DeleteExtension(installedExt.Identity); err != nil { + t.Fatal(err) + } + extensions = downloader.GetExtensions() + if len(extensions) != 0 { + t.Fatal("extension delete fail") + } + }) +} + +func TestDownloader_Extension_OnResolve(t *testing.T) { + setupDownloader(func(downloader *Downloader) { + installedExt, err := downloader.InstallExtensionByFolder("./testdata/extensions/settings_all", false) + if err != nil { + t.Fatal(err) + } + if err := downloader.DeleteExtension(installedExt.Identity); err != nil { + t.Fatal(err) + } + extensions := downloader.GetExtensions() + if len(extensions) != 0 { + t.Fatal("extension delete fail") + } + }) +} + +func TestDownloader_Extension_Logger(t *testing.T) { + logger := logger.NewLogger(false, "") + il := newInstanceLogger(&Extension{ + Name: "test", + }, logger) + il.Debug("msg1", "msg2") + il.Info("msg1", "msg2") + il.Warn("msg1", "msg2") + il.Error("msg1", "msg2") +} + +func setupDownloader(fn func(downloader *Downloader)) { + defaultDownloader.Setup() + defaultDownloader.cfg.StorageDir = ".test_storage" + defaultDownloader.cfg.DownloadDir = ".test_download" + defer func() { + defaultDownloader.Clear() + os.RemoveAll(defaultDownloader.cfg.StorageDir) + os.RemoveAll(defaultDownloader.cfg.DownloadDir) + }() + fn(defaultDownloader) +} diff --git a/pkg/download/model.go b/pkg/download/model.go index 582d6075..43d066b2 100644 --- a/pkg/download/model.go +++ b/pkg/download/model.go @@ -75,6 +75,8 @@ type DownloaderConfig struct { Storage Storage StorageDir string + ProductionMode bool + *DownloaderStoreConfig } @@ -99,7 +101,7 @@ func (cfg *DownloaderConfig) Init() *DownloaderConfig { // DownloaderStoreConfig is the config that can restore the downloader. type DownloaderStoreConfig struct { - FirstLoad bool `json:"-"` // fromNoStore is the flag that the config is first time init and not from store + FirstLoad bool `json:"-"` // FirstLoad is the flag that the config is first time init and not from store DownloadDir string `json:"downloadDir"` // DownloadDir is the default directory to save the downloaded files MaxRunning int `json:"maxRunning"` // MaxRunning is the max running download count @@ -109,7 +111,7 @@ type DownloaderStoreConfig struct { func (cfg *DownloaderStoreConfig) Init() *DownloaderStoreConfig { if cfg.MaxRunning == 0 { - cfg.MaxRunning = 3 + cfg.MaxRunning = 5 } return cfg } diff --git a/pkg/download/storage.go b/pkg/download/storage.go index a8d40926..3daff0f4 100644 --- a/pkg/download/storage.go +++ b/pkg/download/storage.go @@ -6,6 +6,7 @@ import ( "os" "path/filepath" "reflect" + "sync" ) type Storage interface { @@ -47,10 +48,13 @@ func changeValue(p any, v any) { var memData = make(map[string]map[string]any) type MemStorage struct { + lock *sync.RWMutex } func NewMemStorage() *MemStorage { - return &MemStorage{} + return &MemStorage{ + lock: &sync.RWMutex{}, + } } func (n *MemStorage) Setup(buckets []string) error { @@ -63,11 +67,15 @@ func (n *MemStorage) Setup(buckets []string) error { } func (n *MemStorage) Put(bucket string, key string, v any) error { + n.lock.Lock() + defer n.lock.Unlock() memData[bucket][key] = v return nil } func (n *MemStorage) Get(bucket string, key string, v any) (bool, error) { + n.lock.RLock() + defer n.lock.RUnlock() if dv, ok := memData[bucket][key]; ok { changeValue(v, dv) return true, nil @@ -76,6 +84,8 @@ func (n *MemStorage) Get(bucket string, key string, v any) (bool, error) { } func (n *MemStorage) List(bucket string, v any) error { + n.lock.RLock() + defer n.lock.RUnlock() data := memData[bucket] list := make([]any, 0) for _, v := range data { @@ -86,6 +96,8 @@ func (n *MemStorage) List(bucket string, v any) error { } func (n *MemStorage) Pop(bucket string, key string, v any) error { + n.lock.Lock() + defer n.lock.Unlock() data := memData[bucket] changeValue(v, data[key]) delete(data, key) @@ -93,6 +105,8 @@ func (n *MemStorage) Pop(bucket string, key string, v any) error { } func (n *MemStorage) Delete(bucket string, key string) error { + n.lock.Lock() + defer n.lock.Unlock() delete(memData[bucket], key) return nil } @@ -102,6 +116,8 @@ func (n *MemStorage) Close() error { } func (n *MemStorage) Clear() error { + n.lock.Lock() + defer n.lock.Unlock() memData = make(map[string]map[string]any) return nil } @@ -116,6 +132,9 @@ type BoltStorage struct { } func NewBoltStorage(dir string) *BoltStorage { + if err := os.MkdirAll(dir, 0755); err != nil { + panic(err) + } path := filepath.Join(dir, dbFile) db, err := bbolt.Open(path, 0600, nil) if err != nil { diff --git a/pkg/download/testdata/extensions/basic/index.js b/pkg/download/testdata/extensions/basic/index.js new file mode 100644 index 00000000..79176b6b --- /dev/null +++ b/pkg/download/testdata/extensions/basic/index.js @@ -0,0 +1,13 @@ +gopeed.events.onResolve(async function (ctx) { + ctx.res = { + name: "test", + files: Array(2).fill(true).map((_, i) => ({ + name: `test-${i}.txt`, + size: 1024, + req: { + url: ctx.req.url + "/" + i, + } + }), + ), + }; +}); diff --git a/pkg/download/testdata/extensions/basic/manifest.json b/pkg/download/testdata/extensions/basic/manifest.json new file mode 100644 index 00000000..ab8ad2bc --- /dev/null +++ b/pkg/download/testdata/extensions/basic/manifest.json @@ -0,0 +1,21 @@ +{ + "name": "basic", + "title": "gopeed extension basic test", + "version": "0.0.1", + "scripts": [ + { + "event": "onResolve", + "matches": [ + "*://github.com/*" + ], + "entry": "index.js" + } + ], + "settings": [ + { + "name": "ua", + "title": "User-Agent", + "type": "string" + } + ] +} \ No newline at end of file diff --git a/pkg/download/testdata/extensions/settings_all/index.js b/pkg/download/testdata/extensions/settings_all/index.js new file mode 100644 index 00000000..4454268b --- /dev/null +++ b/pkg/download/testdata/extensions/settings_all/index.js @@ -0,0 +1,43 @@ +gopeed.events.onResolve(async function (ctx) { + if (ctx.settings.string != null) { + throw new Error("string is not null"); + } + if (ctx.settings.number != null) { + throw new Error("number is not null"); + } + if (ctx.settings.boolean != null) { + throw new Error("boolean is not null"); + } + + if (ctx.settings.stringDefault !== "default") { + throw new Error("string default value is incorrect"); + } + if (ctx.settings.numberDefault !== 1) { + throw new Error("number default value is incorrect"); + } + if (ctx.settings.booleanDefault !== true) { + throw new Error("boolean default value is incorrect"); + } + + if (ctx.settings.stringValued !== "valued") { + throw new Error("string value is incorrect"); + } + if (ctx.settings.numberValued !== 1.1) { + throw new Error("number value is incorrect"); + } + if (ctx.settings.booleanValued !== true) { + throw new Error("boolean value is incorrect"); + } + + ctx.res = { + name: "test", + files: Array(2).fill(true).map((_, i) => ({ + name: `test-${i}.txt`, + size: 1024, + req: { + url: ctx.req.url + "/" + i, + } + }), + ), + }; +}); diff --git a/pkg/download/testdata/extensions/settings_all/manifest.json b/pkg/download/testdata/extensions/settings_all/manifest.json new file mode 100644 index 00000000..ae6b9540 --- /dev/null +++ b/pkg/download/testdata/extensions/settings_all/manifest.json @@ -0,0 +1,64 @@ +{ + "name": "settings-all", + "title": "gopeed extension settings all type test", + "version": "0.0.1", + "scripts": [ + { + "event": "onResolve", + "matches": [ + "*://*/*" + ], + "entry": "index.js" + } + ], + "settings": [ + { + "name": "string", + "title": "string null test", + "type": "string" + }, + { + "name": "number", + "title": "number null test", + "type": "number" + }, + { + "name": "boolean", + "title": "boolean null test", + "type": "boolean" + }, + { + "name": "stringDefault", + "title": "string default test", + "type": "string", + "value": "default" + }, + { + "name": "numberDefault", + "title": "number default test", + "type": "number", + "value": 1 + }, + { + "name": "booleanDefault", + "title": "boolean default test", + "type": "boolean", + "value": true + }, + { + "name": "stringValued", + "title": "string valued test", + "type": "string" + }, + { + "name": "numberValued", + "title": "number valued test", + "type": "number" + }, + { + "name": "booleanValued", + "title": "boolean valued test", + "type": "boolean" + } + ] +} \ No newline at end of file diff --git a/pkg/download/testdata/extensions/settings_empty/index.js b/pkg/download/testdata/extensions/settings_empty/index.js new file mode 100644 index 00000000..4d47d019 --- /dev/null +++ b/pkg/download/testdata/extensions/settings_empty/index.js @@ -0,0 +1,17 @@ +gopeed.events.onResolve(async function (ctx) { + if (Object.keys(ctx.settings).length > 0){ + throw new Error("settings is not empty"); + } + + ctx.res = { + name: "test", + files: Array(2).fill(true).map((_, i) => ({ + name: `test-${i}.txt`, + size: 1024, + req: { + url: ctx.req.url + "/" + i, + } + }), + ), + }; +}); diff --git a/pkg/download/testdata/extensions/settings_empty/manifest.json b/pkg/download/testdata/extensions/settings_empty/manifest.json new file mode 100644 index 00000000..0ae921ae --- /dev/null +++ b/pkg/download/testdata/extensions/settings_empty/manifest.json @@ -0,0 +1,14 @@ +{ + "name": "settings-empty", + "title": "gopeed extension settings empty test", + "version": "0.0.1", + "scripts": [ + { + "event": "onResolve", + "matches": [ + "*://*/*" + ], + "entry": "index.js" + } + ] +} \ No newline at end of file diff --git a/pkg/download/testdata/extensions/update/index.js b/pkg/download/testdata/extensions/update/index.js new file mode 100644 index 00000000..4376e703 --- /dev/null +++ b/pkg/download/testdata/extensions/update/index.js @@ -0,0 +1,3 @@ +gopeed.events.onResolve(async function (ctx) { + // do nothing for test +}); diff --git a/pkg/download/testdata/extensions/update/manifest.json b/pkg/download/testdata/extensions/update/manifest.json new file mode 100644 index 00000000..30522af3 --- /dev/null +++ b/pkg/download/testdata/extensions/update/manifest.json @@ -0,0 +1,21 @@ +{ + "name": "github-release", + "author": "gopeed", + "title": "Github Release", + "description": "Download github release assets", + "version": "0.0.1", + "homepage": "https://gopeed.com", + "repository": { + "url": "https://github.com/GopeedLab/gopeed-extension-samples", + "directory": "github-release-sample" + }, + "scripts": [ + { + "event": "onResolve", + "matches": [ + "*://github.com/*/releases/*" + ], + "entry": "index.js" + } + ] +} \ No newline at end of file diff --git a/pkg/rest/api.go b/pkg/rest/api.go index 4443e7b3..a0fc3ea8 100644 --- a/pkg/rest/api.go +++ b/pkg/rest/api.go @@ -8,7 +8,6 @@ import ( "io" "net/http" "net/url" - "strings" ) func Resolve(w http.ResponseWriter, r *http.Request) { @@ -31,9 +30,9 @@ func CreateTask(w http.ResponseWriter, r *http.Request) { err error ) if req.Rid != "" { - taskId, err = Downloader.Create(req.Rid, req.Opts) + taskId, err = Downloader.Create(req.Rid, req.Opt) } else if req.Req != nil { - taskId, err = Downloader.CreateDirect(req.Req, req.Opts) + taskId, err = Downloader.CreateDirect(req.Req, req.Opt) } else { WriteJson(w, model.NewErrorResult("param invalid: rid or req", model.CodeInvalidParam)) return @@ -121,21 +120,16 @@ func GetTask(w http.ResponseWriter, r *http.Request) { } func GetTasks(w http.ResponseWriter, r *http.Request) { - status := r.FormValue("status") - if status == "" { - status = strings.Join([]string{ - string(base.DownloadStatusReady), - string(base.DownloadStatusRunning), - string(base.DownloadStatusPause), - string(base.DownloadStatusError), - string(base.DownloadStatusDone), - }, ",") - } - statusArr := strings.Split(status, ",") + r.ParseForm() + status := r.Form["status"] tasks := Downloader.GetTasks() + if len(status) == 0 { + WriteJson(w, model.NewOkResult(tasks)) + return + } result := make([]*download.Task, 0) for _, task := range tasks { - for _, s := range statusArr { + for _, s := range status { if task.Status == base.Status(s) { result = append(result, task) } @@ -159,6 +153,101 @@ func PutConfig(w http.ResponseWriter, r *http.Request) { WriteJson(w, model.NewNilResult()) } +func InstallExtension(w http.ResponseWriter, r *http.Request) { + var req model.InstallExtension + if ReadJson(r, w, &req) { + var ( + installedExt *download.Extension + err error + ) + if req.DevMode { + installedExt, err = Downloader.InstallExtensionByFolder(req.URL, true) + } else { + installedExt, err = Downloader.InstallExtensionByGit(req.URL) + } + if err != nil { + WriteJson(w, model.NewErrorResult(err.Error())) + return + } + WriteJson(w, model.NewOkResult(installedExt.Identity)) + } +} + +func GetExtensions(w http.ResponseWriter, r *http.Request) { + list := Downloader.GetExtensions() + WriteJson(w, model.NewOkResult(list)) +} + +func GetExtension(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + identity := vars["identity"] + ext, err := Downloader.GetExtension(identity) + if err != nil { + WriteJson(w, model.NewErrorResult(err.Error())) + return + } + WriteJson(w, model.NewOkResult(ext)) +} + +func UpdateExtensionSettings(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + identity := vars["identity"] + var req model.UpdateExtensionSettings + if ReadJson(r, w, &req) { + if err := Downloader.UpdateExtensionSettings(identity, req.Settings); err != nil { + WriteJson(w, model.NewErrorResult(err.Error())) + return + } + } + WriteJson(w, model.NewNilResult()) +} + +func SwitchExtension(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + identity := vars["identity"] + var switchExtension model.SwitchExtension + if ReadJson(r, w, &switchExtension) { + if err := Downloader.SwitchExtension(identity, switchExtension.Status); err != nil { + WriteJson(w, model.NewErrorResult(err.Error())) + return + } + } + WriteJson(w, model.NewNilResult()) +} + +func DeleteExtension(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + identity := vars["identity"] + if err := Downloader.DeleteExtension(identity); err != nil { + WriteJson(w, model.NewErrorResult(err.Error())) + return + } + WriteJson(w, model.NewNilResult()) +} + +func UpdateCheckExtension(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + identity := vars["identity"] + newVersion, err := Downloader.UpgradeCheckExtension(identity) + if err != nil { + WriteJson(w, model.NewErrorResult(err.Error())) + return + } + WriteJson(w, model.NewOkResult(&model.UpdateCheckExtensionResp{ + NewVersion: newVersion, + })) +} + +func UpdateExtension(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + identity := vars["identity"] + if err := Downloader.UpgradeExtension(identity); err != nil { + WriteJson(w, model.NewErrorResult(err.Error())) + return + } + WriteJson(w, model.NewNilResult()) +} + func DoProxy(w http.ResponseWriter, r *http.Request) { target := r.Header.Get("X-Target-Uri") if target == "" { @@ -191,18 +280,6 @@ func DoProxy(w http.ResponseWriter, r *http.Request) { return } w.Write(buf) - //var reader io.ReadCloser - //switch resp.Header.Get("Content-Encoding") { - //case "gzip": - // reader, err = gzip.NewReader(resp.Body) - // defer reader.Close() - //default: - // reader = resp.Body - //} - //if _, err := io.Copy(w, resp.Body); err != nil { - // writeError(w, err.Error()) - // return - //} } func writeError(w http.ResponseWriter, msg string) { diff --git a/pkg/rest/model/extension.go b/pkg/rest/model/extension.go new file mode 100644 index 00000000..2c67e567 --- /dev/null +++ b/pkg/rest/model/extension.go @@ -0,0 +1,18 @@ +package model + +type InstallExtension struct { + DevMode bool `json:"devMode"` + URL string `json:"url"` +} + +type UpdateExtensionSettings struct { + Settings map[string]any `json:"settings"` +} + +type SwitchExtension struct { + Status bool `json:"status"` +} + +type UpdateCheckExtensionResp struct { + NewVersion string `json:"newVersion"` +} diff --git a/pkg/rest/model/server.go b/pkg/rest/model/server.go index 6eb6e87d..9f8b363c 100644 --- a/pkg/rest/model/server.go +++ b/pkg/rest/model/server.go @@ -20,6 +20,8 @@ type StartConfig struct { StorageDir string `json:"storageDir"` ApiToken string `json:"apiToken"` + ProductionMode bool + WebEnable bool WebFS fs.FS WebBasicAuth *WebBasicAuth diff --git a/pkg/rest/model/task.go b/pkg/rest/model/task.go index f3d2081d..4cd9e656 100644 --- a/pkg/rest/model/task.go +++ b/pkg/rest/model/task.go @@ -3,7 +3,7 @@ package model import "github.com/GopeedLab/gopeed/pkg/base" type CreateTask struct { - Rid string `json:"rid"` - Req *base.Request `json:"req"` - Opts *base.Options `json:"opts"` + Rid string `json:"rid"` + Req *base.Request `json:"req"` + Opt *base.Options `json:"opt"` } diff --git a/pkg/rest/server.go b/pkg/rest/server.go index cbcaccf8..326a9e59 100644 --- a/pkg/rest/server.go +++ b/pkg/rest/server.go @@ -10,6 +10,10 @@ import ( "github.com/gorilla/mux" "net" "net/http" + "os" + "path/filepath" + "regexp" + "strings" ) var ( @@ -26,7 +30,6 @@ func Start(startCfg *model.StartConfig) (int, error) { go func() { if err := srv.Serve(listener); err != nil && err != http.ErrServerClosed { - // TODO log panic(err) } }() @@ -41,12 +44,12 @@ func Start(startCfg *model.StartConfig) (int, error) { func Stop() { if srv != nil { if err := srv.Shutdown(context.TODO()); err != nil { - // TODO log + Downloader.Logger.Warn().Err(err).Msg("shutdown server failed") } } if Downloader != nil { if err := Downloader.Close(); err != nil { - // TODO log + Downloader.Logger.Warn().Err(err).Msg("close downloader failed") } } } @@ -58,6 +61,7 @@ func BuildServer(startCfg *model.StartConfig) (*http.Server, net.Listener, error startCfg.Init() downloadCfg := &download.DownloaderConfig{ + ProductionMode: startCfg.ProductionMode, RefreshInterval: startCfg.RefreshInterval, } if startCfg.Storage == model.StorageBolt { @@ -93,6 +97,15 @@ func BuildServer(startCfg *model.StartConfig) (*http.Server, net.Listener, error r.Methods(http.MethodGet).Path("/api/v1/tasks").HandlerFunc(GetTasks) r.Methods(http.MethodGet).Path("/api/v1/config").HandlerFunc(GetConfig) r.Methods(http.MethodPut).Path("/api/v1/config").HandlerFunc(PutConfig) + r.Methods(http.MethodPost).Path("/api/v1/extensions").HandlerFunc(InstallExtension) + r.Methods(http.MethodGet).Path("/api/v1/extensions").HandlerFunc(GetExtensions) + r.Methods(http.MethodGet).Path("/api/v1/extensions/{identity}").HandlerFunc(GetExtension) + r.Methods(http.MethodPut).Path("/api/v1/extensions/{identity}/settings").HandlerFunc(UpdateExtensionSettings) + r.Methods(http.MethodPut).Path("/api/v1/extensions/{identity}/switch").HandlerFunc(SwitchExtension) + r.Methods(http.MethodDelete).Path("/api/v1/extensions/{identity}").HandlerFunc(DeleteExtension) + r.Methods(http.MethodGet).Path("/api/v1/extensions/{identity}/update").HandlerFunc(UpdateCheckExtension) + r.Methods(http.MethodPost).Path("/api/v1/extensions/{identity}/update").HandlerFunc(UpdateExtension) + r.PathPrefix("/fs/extensions").Handler(http.FileServer(new(extensionFileSystem))) r.Path("/api/v1/proxy").HandlerFunc(DoProxy) if startCfg.WebEnable { r.PathPrefix("/").Handler(http.FileServer(http.FS(startCfg.WebFS))) @@ -125,6 +138,32 @@ func BuildServer(startCfg *model.StartConfig) (*http.Server, net.Listener, error return srv, listener, nil } +// handle extension file resource +type extensionFileSystem struct { +} + +func (e *extensionFileSystem) Open(name string) (http.File, error) { + // remove prefix + path := strings.TrimPrefix(name, "/fs/extensions") + // match extension identity, eg: /fs/extensions/identity/xxx + reg := regexp.MustCompile(`^/([^/]+)/(.*)$`) + if !reg.MatchString(path) { + return nil, os.ErrNotExist + } + matched := reg.FindStringSubmatch(path) + if len(matched) != 3 { + return nil, os.ErrNotExist + } + // get extension identity + identity := matched[1] + extension, err := Downloader.GetExtension(identity) + if err != nil { + return nil, os.ErrNotExist + } + extensionPath := Downloader.ExtensionPath(extension) + return os.Open(filepath.Join(extensionPath, matched[2])) +} + func ReadJson(r *http.Request, w http.ResponseWriter, v any) bool { if err := json.NewDecoder(r.Body).Decode(v); err != nil { WriteJson(w, model.NewErrorResult(err.Error())) diff --git a/pkg/rest/server_test.go b/pkg/rest/server_test.go index 9235cdc0..b34fb346 100644 --- a/pkg/rest/server_test.go +++ b/pkg/rest/server_test.go @@ -33,7 +33,6 @@ var ( }, } taskRes = &base.Resource{ - Name: test.BuildName, Size: test.BuildSize, Range: true, Files: []*base.FileInfo{ @@ -44,15 +43,23 @@ var ( }, }, } + createOpts = &base.Options{ + Path: test.Dir, + Name: test.DownloadName, + Extra: map[string]any{ + "connections": 2, + }, + } createReq = &model.CreateTask{ Req: taskReq, - Opts: &base.Options{ - Path: test.Dir, - Name: test.DownloadName, - Extra: map[string]any{ - "connections": 2, - }, - }, + Opt: createOpts, + } + createResoledReq = &model.CreateTask{ + Req: taskReq, + Opt: createOpts, + } + installExtensionReq = &model.InstallExtension{ + URL: "https://github.com/GopeedLab/gopeed-extension-samples#github-contributor-avatars-sample", } ) @@ -66,6 +73,35 @@ func TestResolve(t *testing.T) { } func TestCreateTask(t *testing.T) { + doTest(func() { + resp := httpRequestCheckOk[*download.ResolveResult](http.MethodPost, "/api/v1/resolve", taskReq) + + var wg sync.WaitGroup + wg.Add(1) + Downloader.Listener(func(event *download.Event) { + if event.Key == download.EventKeyFinally { + wg.Done() + } + }) + + taskId := httpRequestCheckOk[string](http.MethodPost, "/api/v1/tasks", &model.CreateTask{ + Rid: resp.ID, + Opt: createOpts, + }) + if taskId == "" { + t.Fatal("create task failed") + } + + wg.Wait() + want := test.FileMd5(test.BuildFile) + got := test.FileMd5(test.DownloadFile) + if want != got { + t.Errorf("CreateTask() got = %v, want %v", got, want) + } + }) +} + +func TestCreateDirectTask(t *testing.T) { doTest(func() { var wg sync.WaitGroup wg.Add(1) @@ -84,32 +120,41 @@ func TestCreateTask(t *testing.T) { want := test.FileMd5(test.BuildFile) got := test.FileMd5(test.DownloadFile) if want != got { - t.Errorf("CreateTask() got = %v, want %v", got, want) + t.Errorf("CreateDirectTask() got = %v, want %v", got, want) + } + }) +} + +func TestCreateDirectTaskWithResoled(t *testing.T) { + doTest(func() { + var wg sync.WaitGroup + wg.Add(1) + Downloader.Listener(func(event *download.Event) { + if event.Key == download.EventKeyFinally { + wg.Done() + } + }) + + taskId := httpRequestCheckOk[string](http.MethodPost, "/api/v1/tasks", createResoledReq) + if taskId == "" { + t.Fatal("create task failed") + } + + wg.Wait() + want := test.FileMd5(test.BuildFile) + got := test.FileMd5(test.DownloadFile) + if want != got { + t.Errorf("CreateDirectTaskWithResoled() got = %v, want %v", got, want) } }) } func TestPauseAndContinueTask(t *testing.T) { doTest(func() { - type result struct { - pauseCount int - continueCount int - md5 string - } - - var got = result{ - pauseCount: 0, - continueCount: 0, - md5: "", - } var wg sync.WaitGroup wg.Add(1) Downloader.Listener(func(event *download.Event) { switch event.Key { - case download.EventKeyPause: - got.pauseCount++ - case download.EventKeyContinue: - got.continueCount++ case download.EventKeyFinally: wg.Done() } @@ -132,12 +177,8 @@ func TestPauseAndContinueTask(t *testing.T) { } wg.Wait() - want := result{ - pauseCount: 1, - continueCount: 1, - md5: test.FileMd5(test.BuildFile), - } - got.md5 = test.FileMd5(test.DownloadFile) + want := test.FileMd5(test.BuildFile) + got := test.FileMd5(test.DownloadFile) if !reflect.DeepEqual(got, want) { t.Errorf("PauseAndContinueTask() got = %v, want %v", got, want) } @@ -163,13 +204,15 @@ func TestPauseAllAndContinueALLTasks(t *testing.T) { // continue all httpRequestCheckOk[any](http.MethodPut, "/api/v1/tasks/continue", nil) + time.Sleep(time.Millisecond * 100) tasks := httpRequestCheckOk[[]*download.Task](http.MethodGet, fmt.Sprintf("/api/v1/tasks?status=%s", base.DownloadStatusRunning), nil) if len(tasks) != cfg.MaxRunning { t.Errorf("ContinueAllTasks() got = %v, want %v", len(tasks), cfg.MaxRunning) } // pause all httpRequestCheckOk[any](http.MethodPut, "/api/v1/tasks/pause", nil) - tasks = httpRequestCheckOk[[]*download.Task](http.MethodGet, fmt.Sprintf("/api/v1/tasks?status=%s,%s", base.DownloadStatusReady, base.DownloadStatusPause), nil) + time.Sleep(time.Millisecond * 500) + tasks = httpRequestCheckOk[[]*download.Task](http.MethodGet, fmt.Sprintf("/api/v1/tasks?status=%s", base.DownloadStatusPause), nil) if len(tasks) != total { t.Errorf("PauseAllTasks() got = %v, want %v", len(tasks), total) } @@ -210,7 +253,7 @@ func TestGetTasks(t *testing.T) { } }) - httpRequestCheckOk[string](http.MethodPost, fmt.Sprintf("/api/v1/tasks?status=%s,%s", + httpRequestCheckOk[string](http.MethodPost, fmt.Sprintf("/api/v1/tasks?status=%s&status=%s", base.DownloadStatusReady, base.DownloadStatusRunning), createReq) httpRequestCheckOk[[]*download.Task](http.MethodGet, "/api/v1/tasks", nil) @@ -248,6 +291,115 @@ func TestGetAndPutConfig(t *testing.T) { }) } +func TestInstallExtension(t *testing.T) { + doTest(func() { + identity := httpRequestCheckOk[string](http.MethodPost, "/api/v1/extensions", installExtensionReq) + if identity == "" { + t.Errorf("InstallExtension() got = %v, want %v", identity, "not empty") + } + + // not a valid extension repository + code, _ := httpRequest[string](http.MethodPost, "/api/v1/extensions", &model.InstallExtension{ + URL: "https://github.com/GopeedLab/gopeed", + }) + checkCode(code, model.CodeError) + + // not a git repository + code, _ = httpRequest[string](http.MethodPost, "/api/v1/extensions", &model.InstallExtension{ + URL: "https://github.com", + }) + checkCode(code, model.CodeError) + }) +} + +func TestGetExtensions(t *testing.T) { + doTest(func() { + httpRequestCheckOk[string](http.MethodPost, "/api/v1/extensions", installExtensionReq) + extensions := httpRequestCheckOk[[]*download.Extension](http.MethodGet, "/api/v1/extensions", nil) + if len(extensions) == 0 { + t.Errorf("GetExtensions() got = %v, want %v", len(extensions), "not empty") + } + }) +} + +func TestUpdateExtensionSettings(t *testing.T) { + doTest(func() { + identity := httpRequestCheckOk[string](http.MethodPost, "/api/v1/extensions", installExtensionReq) + + httpRequestCheckOk[any](http.MethodPut, "/api/v1/extensions/"+identity+"/settings", &model.UpdateExtensionSettings{ + Settings: map[string]any{ + "undefined": "test", + "ua": "test", + }, + }) + + settings := httpRequestCheckOk[*download.Extension](http.MethodGet, "/api/v1/extensions/"+identity, nil).Settings + if len(settings) != 1 { + t.Errorf("UpdateExtensionSettings() got = %v, want %v", len(settings), 1) + } + + if settings[0].Name != "ua" || settings[0].Value != "test" { + t.Errorf("UpdateExtensionSettings() got = %v, want %v", settings[0].Value, "test") + } + }) +} + +func TestSwitchExtension(t *testing.T) { + doTest(func() { + identity := httpRequestCheckOk[string](http.MethodPost, "/api/v1/extensions", installExtensionReq) + httpRequestCheckOk[any](http.MethodPut, "/api/v1/extensions/"+identity+"/switch", &model.SwitchExtension{ + Status: false, + }) + extensions := httpRequestCheckOk[[]*download.Extension](http.MethodGet, "/api/v1/extensions", nil) + if !extensions[0].Disabled { + t.Errorf("TestSwitchExtension() got = %v, want %v", extensions[0].Disabled, true) + } + }) +} + +func TestDeleteExtension(t *testing.T) { + doTest(func() { + identity := httpRequestCheckOk[string](http.MethodPost, "/api/v1/extensions", installExtensionReq) + httpRequestCheckOk[any](http.MethodDelete, "/api/v1/extensions/"+identity, nil) + extensions := httpRequestCheckOk[[]*download.Extension](http.MethodGet, "/api/v1/extensions", nil) + if len(extensions) != 0 { + t.Errorf("TestDeleteExtension() got = %v, want %v", len(extensions), 0) + } + }) +} + +func TestUpdateCheckExtension(t *testing.T) { + doTest(func() { + identity := httpRequestCheckOk[string](http.MethodPost, "/api/v1/extensions", installExtensionReq) + resp := httpRequestCheckOk[*model.UpdateCheckExtensionResp](http.MethodGet, "/api/v1/extensions/"+identity+"/update", nil) + // no new version + if resp.NewVersion != "" { + t.Errorf("UpdateCheckExtension() got = %v, want %v", resp.NewVersion, "") + } + // force update + httpRequestCheckOk[any](http.MethodPost, "/api/v1/extensions/"+identity+"/update", nil) + }) +} + +func TestFsExtension(t *testing.T) { + doTest(func() { + identity := httpRequestCheckOk[string](http.MethodPost, "/api/v1/extensions", installExtensionReq) + statusCode, _ := doHttpRequest0(http.MethodGet, "/fs/extensions/"+identity+"/icon.png", nil, nil) + if statusCode != http.StatusOK { + t.Errorf("FsExtension() got = %v, want %v", statusCode, http.StatusOK) + } + }) +} + +func TestFsExtensionFail(t *testing.T) { + doTest(func() { + statusCode, _ := doHttpRequest0(http.MethodGet, "/fs/extensions/not_exist/icon.png", nil, nil) + if statusCode != http.StatusNotFound { + t.Errorf("TestFsExtensionFail() got = %v, want %v", statusCode, http.StatusNotFound) + } + }) +} + func TestDoProxy(t *testing.T) { doTest(func() { code, respBody := doHttpRequest0(http.MethodGet, "/api/v1/proxy", map[string]string{ @@ -341,15 +493,18 @@ func doTest(handler func()) { var cfg = &model.StartConfig{} cfg.Init() cfg.Storage = storage + cfg.StorageDir = ".test_storage" fileListener := doStart(cfg) defer func() { if err := fileListener.Close(); err != nil { panic(err) } Stop() + Downloader.Clear() }() - defer Downloader.Clear() defer func() { + Downloader.PauseAll() + tasks := Downloader.GetTasks() taskIds := make([]string, len(tasks)) for i, task := range tasks { @@ -358,6 +513,7 @@ func doTest(handler func()) { for _, id := range taskIds { Downloader.Delete(id, true) } + os.RemoveAll(cfg.StorageDir) }() taskReq.URL = "http://" + fileListener.Addr().String() + "/" + test.BuildName handler() diff --git a/pkg/util/matcher.go b/pkg/util/matcher.go new file mode 100644 index 00000000..02f609c9 --- /dev/null +++ b/pkg/util/matcher.go @@ -0,0 +1,75 @@ +package util + +import ( + "net/url" + "regexp" + "strings" +) + +// Match url with pattern by chrome extension match pattern style +// https://developer.chrome.com/docs/extensions/mv3/match_patterns/ +func Match(pattern string, u string) bool { + scheme, host, path := parsePattern(pattern) + url, err := url.Parse(u) + if err != nil { + return false + } + if scheme != "*" && scheme != url.Scheme { + return false + } + if !matchHost(host, url.Hostname()) { + return false + } + if !matchPath(path, url.Path) { + return false + } + return true +} + +func parsePattern(pattern string) (scheme string, host string, path string) { + parts := strings.Split(pattern, "://") + if len(parts) == 2 { + scheme = parts[0] + pattern = parts[1] + } else { + scheme = "" + } + parts = strings.SplitN(pattern, "/", 2) + if len(parts) == 2 { + host = parts[0] + path = "/" + parts[1] + } else { + host = pattern + path = "/" + } + return +} + +func matchHost(pattern string, host string) bool { + if pattern == "*" { + return true + } + if strings.HasPrefix(pattern, "*.") { + return strings.HasSuffix(host, pattern[1:]) + } + return pattern == host +} + +func matchPath(pattern string, path string) bool { + if pattern == "*" { + return true + } + if !strings.HasSuffix(pattern, "*") && !strings.HasSuffix(pattern, "/") { + pattern += "/" + } + if !strings.HasSuffix(path, "/") { + path += "/" + } + + if strings.Contains(pattern, "*") { + pattern = strings.Replace(pattern, "*", ".*", -1) + matched, _ := regexp.MatchString("^"+pattern+"$", path) + return matched + } + return pattern == path +} diff --git a/pkg/util/matcher_test.go b/pkg/util/matcher_test.go new file mode 100644 index 00000000..5f24442e --- /dev/null +++ b/pkg/util/matcher_test.go @@ -0,0 +1,36 @@ +package util + +import "testing" + +func TestMatch(t *testing.T) { + tests := []struct { + pattern string + urls []string + want bool + }{ + {"*://*/*", []string{"https://www.google.com/", "http://example.org/foo/bar.html"}, true}, + {"https://*/*", []string{"https://www.google.com", "https://example.org/foo/bar.html"}, true}, + {"*://www.google.com", []string{"https://www.google.com/", "https://www.google.com"}, true}, + {"*://*.google.com/", []string{"https://a.www.google.com/", "https://c.www.google.com/", "https://www.google.com/"}, true}, + {"https://*/foo*", []string{"https://www.google.com/foo", "https://example.com/foo/bar.html"}, true}, + {"https://www.google.com/*/b/*", []string{"https://www.google.com/a/b", "https://www.google.com/a/b/c"}, true}, + {"https://*.google.com/foo*bar", []string{"https://www.google.com/foo/baz/bar", "https://docs.google.com/foobar"}, true}, + {"https://www.google.com/*abc*", []string{"https://www.google.com/abc", "https://www.google.com/123abc", "https://www.google.com/abc456", "https://www.google.com/123abc456"}, true}, + {"https://example.org/foo/bar.html", []string{"https://example.org/foo/bar.html"}, true}, + {"http://127.0.0.1/*", []string{"http://127.0.0.1/", "http://127.0.0.1/foo/bar.html"}, true}, + {"*://mail.google.com/*", []string{"http://mail.google.com/foo/baz/bar", "https://mail.google.com/foobar"}, true}, + {"https://www.google.com/", []string{"http://www.google.com/"}, false}, + {"www.google.com/", []string{"http://www.google.com/", "https://www.google.com/"}, false}, + {"www.google.com/*c", []string{"https://www.google.com/a", "https://www.google.com/b"}, false}, + {"https://*.example.org/*", []string{"https://www.google.com", "https://docs.google.com"}, false}, + } + for _, tt := range tests { + t.Run(tt.pattern, func(t *testing.T) { + for _, url := range tt.urls { + if got := Match(tt.pattern, url); got != tt.want { + t.Errorf("Match() = %v, want %v", got, tt.want) + } + } + }) + } +} diff --git a/pkg/util/path.go b/pkg/util/path.go index e0572f37..d7080694 100644 --- a/pkg/util/path.go +++ b/pkg/util/path.go @@ -3,8 +3,10 @@ package util import ( "errors" "fmt" + "io" "os" syspath "path" + "path/filepath" "strings" ) @@ -64,22 +66,105 @@ func CheckDuplicateAndRename(path string) (string, error) { } } -// GetSingleDir get the top level single folder name,if not exist, return empty string -func GetSingleDir(paths []string) string { - if len(paths) == 0 { - return "" +// CopyDir Copy all files to the target directory, if the file already exists, it will be overwritten. +// Remove target file if the source file is not exist. +func CopyDir(source string, target string, excludeDir ...string) error { + if err := os.MkdirAll(target, 0755); err != nil { + return err } - split := strings.Split(paths[0], "/") - if len(split) == 0 || split[0] == "" { - return "" - } - dir := split[0] - for i := 1; i < len(paths); i++ { - if !strings.HasPrefix(paths[i], dir) { - return "" + if err := filepath.Walk(source, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err } + if info.IsDir() { + if len(excludeDir) > 0 { + for _, dir := range excludeDir { + if info.IsDir() && info.Name() == dir { + return filepath.SkipDir + } + } + } + return nil + } + relPath, err := filepath.Rel(source, path) + if err != nil { + return err + } + targetPath := filepath.Join(target, relPath) + if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil { + return err + } + if err := copyForce(path, targetPath); err != nil { + return err + } + return nil + }); err != nil { + return err } - return dir + + if err := filepath.Walk(target, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() { + if len(excludeDir) > 0 { + for _, dir := range excludeDir { + if info.IsDir() && info.Name() == dir { + return filepath.SkipDir + } + } + } + return nil + } + relPath, err := filepath.Rel(target, path) + if err != nil { + return err + } + targetPath := filepath.Join(target, relPath) + sourcePath := filepath.Join(source, relPath) + // if source file is not exist, remove target file + if _, err := os.Stat(sourcePath); os.IsNotExist(err) { + if err := SafeRemove(targetPath); err != nil { + return err + } + } + return nil + }); err != nil { + return err + } + return nil +} + +// RmAndMkDirAll remove and create directory, if the directory already exists, it will be overwritten. +func RmAndMkDirAll(path string) error { + if err := os.RemoveAll(path); err != nil { + return err + } + if err := os.MkdirAll(path, 0755); err != nil { + return err + } + return nil +} + +// copy file, if the target file already exists, it will be overwritten. +func copyForce(source string, target string) error { + sourceFile, err := os.Open(source) + if err != nil { + return err + } + defer sourceFile.Close() + + targetFile, err := os.Create(target) + if err != nil { + return err + } + defer targetFile.Close() + + _, err = io.Copy(targetFile, sourceFile) + if err != nil { + return err + } + return nil } func CreateDirIfNotExist(dir string) error { @@ -92,7 +177,7 @@ func CreateDirIfNotExist(dir string) error { // IsExistsFile check file exists and is a file func IsExistsFile(path string) bool { info, err := os.Stat(path) - // 判断路径是否存在,并且是一个文件 + // if file exists and is a file if err == nil && !info.IsDir() { return true } diff --git a/pkg/util/path_test.go b/pkg/util/path_test.go index 91c013c7..0f8f4c37 100644 --- a/pkg/util/path_test.go +++ b/pkg/util/path_test.go @@ -78,7 +78,7 @@ func TestFilepath(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := Filepath(tt.args.path, tt.args.originName, tt.args.customName); got != tt.want { - t.Errorf("Filepath() = %v, want %v", got, tt.want) + t.Errorf("SingleFilepath() = %v, want %v", got, tt.want) } }) } @@ -105,67 +105,6 @@ func TestSafeRemove(t *testing.T) { } } -func TestGetSingleDir(t *testing.T) { - type args struct { - paths []string - } - tests := []struct { - name string - args args - want string - }{ - { - name: "empty", - args: args{ - paths: []string{}, - }, - want: "", - }, - { - name: "blank", - args: args{ - paths: []string{""}, - }, - want: "", - }, - { - name: "blank multi", - args: args{ - paths: []string{"", "a"}, - }, - want: "", - }, - { - name: "multi dir not single", - args: args{ - paths: []string{"a", "b"}, - }, - want: "", - }, - { - name: "single dir", - args: args{ - paths: []string{"a"}, - }, - want: "a", - }, - { - name: "multi dir single", - args: args{ - paths: []string{"a/b", "a/c"}, - }, - want: "a", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := GetSingleDir(tt.args.paths); got != tt.want { - t.Errorf("IsSingleDir() = %v, want %v", got, tt.want) - } - }) - } -} - func TestCheckDuplicateAndRename(t *testing.T) { doCheckDuplicateAndRename(t, []string{}, "a.txt", "a.txt") doCheckDuplicateAndRename(t, []string{"a.txt"}, "a.txt", "a (1).txt") diff --git a/ui/flutter/.gitignore b/ui/flutter/.gitignore index 74fa3cc9..b1f0661d 100644 --- a/ui/flutter/.gitignore +++ b/ui/flutter/.gitignore @@ -62,3 +62,5 @@ macos/Flutter/GeneratedPluginRegistrant.swift windows/flutter/generated_plugin_registrant.cc windows/flutter/generated_plugin_registrant.h windows/flutter/generated_plugins.cmake + +/extensions \ No newline at end of file diff --git a/ui/flutter/assets/extension/default_icon.png b/ui/flutter/assets/extension/default_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..d8b2dbc3d38dda9c8f1e031d53351fc9092c14f4 GIT binary patch literal 1054 zcmeAS@N?(olHy`uVBq!ia0vp^6(G#P3?%t>9eV(zSkfJR9T^xl_H+M9WCijSl0AZa z85pY67#JE_7#My5g&JNkFq9fFFuY1&V6d9Oz#v{QXIG#NP{J?3C&U##5jUHNu5wIc+*Jk!rXEih$B>MBZ)e}` zdt|`V798|lz^QO%V%q)xp5K$iuPjcMt?34;s6@w+>gaLCOqUU&Fc!kbU|6|x2M{inqpynpg` z7?Ye)o;R<|oZ<||V}F$WIq&S4tTCfT)c^Yq{E7$bbPVt7xZml19n=4)a1DRooJZX3HP5EZmPkkty&>`7 zgqEh!0r4=wuND>`P8~ab&~|-5oy49uygz0ooX=^KVO{^*exKZp@cgBD<{`DZ8@;%;hr81+vVUvdzC_ zn?K7jzmaQxD$in1{`PeVI~FAFn3S}=C3#0#%8rcGol)OnmU-8e>`k?L`1Rm^=J_AC zOK5MH4@|16C9V-ADTyViR>?)FK#IZ0z|cb1&`{UFEX2sb%EZ*l*i_rVz{%X8O-xS>N=;0uEIgSC%Crog Lu6{1-oD!M _parse( if (e.type == DioExceptionType.sendTimeout || e.type == DioExceptionType.receiveTimeout || e.type == DioExceptionType.connectionTimeout) { - throw Exception(Result(code: 1000, msg: "request timeout")); + throw TimeoutException("request timeout"); } throw Exception(Result(code: 1000, msg: e.message)); } @@ -105,8 +116,8 @@ Future createTask(CreateTask createTask) async { Future> getTasks(List statuses) async { return _parse>( - () => _client.dio - .get("/api/v1/tasks?status=${statuses.map((e) => e.name).join(",")}"), + () => _client.dio.get( + "/api/v1/tasks?${statuses.map((e) => "status=${e.name}").join("&")}"), (data) => (data as List).map((e) => Task.fromJson(e)).toList()); } @@ -140,6 +151,47 @@ Future putConfig(DownloaderConfig config) async { return _parse(() => _client.dio.put("/api/v1/config", data: config), null); } +Future installExtension(InstallExtension installExtension) async { + return _parse( + () => _client.dio.post("/api/v1/extensions", data: installExtension), + null); +} + +Future> getExtensions() async { + return _parse>(() => _client.dio.get("/api/v1/extensions"), + (data) => (data as List).map((e) => Extension.fromJson(e)).toList()); +} + +Future updateExtensionSettings( + String identity, UpdateExtensionSettings updateExtensionSettings) async { + return _parse( + () => _client.dio.put("/api/v1/extensions/$identity/settings", + data: updateExtensionSettings), + null); +} + +Future switchExtension( + String identity, SwitchExtension switchExtension) async { + return _parse( + () => _client.dio + .put("/api/v1/extensions/$identity/switch", data: switchExtension), + null); +} + +Future deleteExtension(String identity) async { + return _parse(() => _client.dio.delete("/api/v1/extensions/$identity"), null); +} + +Future upgradeCheckExtension(String identity) async { + return _parse(() => _client.dio.get("/api/v1/extensions/$identity/update"), + (data) => UpdateCheckExtensionResp.fromJson(data)); +} + +Future updateExtension(String identity) async { + return _parse( + () => _client.dio.post("/api/v1/extensions/$identity/update"), null); +} + Future> proxyRequest(String uri, {data, Options? options}) async { options ??= Options(); @@ -152,3 +204,7 @@ Future> proxyRequest(String uri, data: data, options: options); } + +String join(String path) { + return "${_client.dio.options.baseUrl}/${Util.cleanPath(path)}"; +} diff --git a/ui/flutter/lib/api/model/create_task.dart b/ui/flutter/lib/api/model/create_task.dart index 5ddbe999..92bcbcdf 100644 --- a/ui/flutter/lib/api/model/create_task.dart +++ b/ui/flutter/lib/api/model/create_task.dart @@ -9,12 +9,12 @@ part 'create_task.g.dart'; class CreateTask { String? rid; Request? req; - Options? opts; + Options? opt; CreateTask({ this.rid, this.req, - this.opts, + this.opt, }); factory CreateTask.fromJson( diff --git a/ui/flutter/lib/api/model/create_task.g.dart b/ui/flutter/lib/api/model/create_task.g.dart index f36e9432..3e147035 100644 --- a/ui/flutter/lib/api/model/create_task.g.dart +++ b/ui/flutter/lib/api/model/create_task.g.dart @@ -11,9 +11,9 @@ CreateTask _$CreateTaskFromJson(Map json) => CreateTask( req: json['req'] == null ? null : Request.fromJson(json['req'] as Map), - opts: json['opts'] == null + opt: json['opt'] == null ? null - : Options.fromJson(json['opts'] as Map), + : Options.fromJson(json['opt'] as Map), ); Map _$CreateTaskToJson(CreateTask instance) { @@ -27,6 +27,6 @@ Map _$CreateTaskToJson(CreateTask instance) { writeNotNull('rid', instance.rid); writeNotNull('req', instance.req?.toJson()); - writeNotNull('opts', instance.opts?.toJson()); + writeNotNull('opt', instance.opt?.toJson()); return val; } diff --git a/ui/flutter/lib/api/model/extension.dart b/ui/flutter/lib/api/model/extension.dart new file mode 100644 index 00000000..da13b403 --- /dev/null +++ b/ui/flutter/lib/api/model/extension.dart @@ -0,0 +1,95 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'extension.g.dart'; + +@JsonSerializable(explicitToJson: true) +class Extension { + String identity; + String name; + String author; + String title; + String description; + String icon; + String version; + String homepage; + Repository? repository; + List? settings; + bool disabled; + bool devMode; + + Extension({ + required this.identity, + required this.name, + required this.author, + required this.title, + required this.description, + required this.icon, + required this.version, + required this.homepage, + required this.repository, + required this.disabled, + required this.devMode, + }); + + factory Extension.fromJson(Map json) => + _$ExtensionFromJson(json); + Map toJson() => _$ExtensionToJson(this); +} + +@JsonSerializable() +class Repository { + String url; + String directory; + + Repository({ + required this.url, + required this.directory, + }); + + factory Repository.fromJson(Map json) => + _$RepositoryFromJson(json); + Map toJson() => _$RepositoryToJson(this); +} + +@JsonSerializable() +class Setting { + String name; + String title; + String description; + bool required; + SettingType type; + Object? value; + List