From 3fec83cf73a77ec32c5a4f8c137fa7b7f4ca4799 Mon Sep 17 00:00:00 2001 From: Mikei386 <44135113+Mikei386@users.noreply.github.com> Date: Tue, 23 Jun 2026 06:52:25 +0200 Subject: [PATCH] Use background jobs for file transfers --- build-plugin.sh | 2 +- packages/u-navigator-2026.06.23.r003.tgz | Bin 12464 -> 0 bytes packages/u-navigator-2026.06.23.r004.tgz | Bin 0 -> 13696 bytes .../emhttp/plugins/u-navigator/api/common.php | 33 +++++ .../emhttp/plugins/u-navigator/api/job.php | 58 ++++----- .../plugins/u-navigator/api/job_worker.php | 119 ++++++++++++++++++ server/public/app.js | 26 ++-- server/public/styles.css | 6 + u-navigator.plg | 4 +- 9 files changed, 201 insertions(+), 47 deletions(-) delete mode 100644 packages/u-navigator-2026.06.23.r003.tgz create mode 100644 packages/u-navigator-2026.06.23.r004.tgz create mode 100644 plugin-root/usr/local/emhttp/plugins/u-navigator/api/job_worker.php diff --git a/build-plugin.sh b/build-plugin.sh index fb32826..13200c8 100755 --- a/build-plugin.sh +++ b/build-plugin.sh @@ -2,7 +2,7 @@ set -eu PLUGIN="u-navigator" -VERSION="2026.06.23.r003" +VERSION="2026.06.23.r004" PKG_DIR="packages" WORK_DIR=".plugin-build" PKG_NAME="${PLUGIN}-${VERSION}.tgz" diff --git a/packages/u-navigator-2026.06.23.r003.tgz b/packages/u-navigator-2026.06.23.r003.tgz deleted file mode 100644 index 589c2deb397bc32bbd3bbaf1b803c0d679525580..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12464 zcmX|HWl&vBki`k^?hxFa;O_43?(Xn#cS3Lo?k>S4z{54TI|O%q@HXFW)z;jaAAM)0 z?sVNbr+fO4#eacdM&q}KfW8be@*t8+Tiul6BQ7tzJRxQAfxLhqhkyc=+a?WX&nNaN zms;ygmQ>Nqk^3*60=;=9-8=&y!>K_2?S(AFKK*;3E5Ey)4wL<<+UqO(K-aHR`jR6v zah0>RwNw9E?b+K1Uu^$NYt_81qZdEwNB8)rQ|tPL$Wuso`Qz7iWPekyo+?`P<2%-ynTFe8Ke)dXa8Cxl!~=Yc;HhlY--Cu< zONOwHgKn>P`*A_fHZN!Z@E=Db()@zHLx6ca7!v&Pu9(rU{t7N4LOH*(#rAzvBksKy z=e_VZ0WFyTQa^v)i9~`Q!OnpZax5BZ{(F7k8IJ(tp3lFB1Dc;MJRG1MXBV!1*KGi$ zZ-VFSE1+Z#Sp#rN!u6}&VEQuuaytJ_*})+I>+8*vyv?NF-OG7NCR%?0XvpoOH@Dnv z3s?^?19*R_eD1)Is++!cQ7)`+^~uAseK>q9l_pGX0ImCWG$3>Ye`qALCxTN=INkvj zD4?C!#SpWu6HxNGN&lDPdZNH)_Tv>WPkTGR!iNimT@`riJOb39ZXdLZP$&Op@$mBec-x^&(+9sr@)DQ+Jv~Tf@Bdt}b8=#N z?2jxh`}Ao@-v^Yz20MOZ%K#G@5Pa^~JGTBeMcrYv8hqKx!>{nXBP1IKCRhO=a=Qk9 zH2m4LWqs_ID&2frVU-?ynLtZV<^_V@c8;}e9IC*~Uzs2K1M=NL4T}TGH1Nky;5Nd1 zX`6p9C~;o?ed6?tw?`F3@Du$gIPYAfoRqKs^H=BE1=D{k{(fvQNpL87pXqo1bBYHC zH(dvA%c(o&_;g|a-)(D-f%Q#ySu>#T&jEFHPyg%`0sF zWdPgE0t|95r~lWh%>`g*u6Bcs+{yc$fE4R}uu1E?$`Lqm#`*_%0~_4E2^IR`YE<8v?HY+k&wNv>Utb*v`aygDuY8y~?y} z>Mm%vR>{iHv?q?@JZ=VF8~6FvJQrg`I970z9xR$#L4INg99ow zs~0J3bfW;pIwh%iBA!?chJ$J@iN+r{G7Y(X3st>LZ%^;aop@fO60C~n0MdZl=~z6` zOFEX!%@#<-D^qTfwx#>(gM!D+#X39UNB~J~`R%HK5UbYk7pPC6H>nhaR+!)y9>|2e zrE@OMf6^3*Y9QxhQR(KLc1k`bsXm=V-gQCX){C#?Wcq{ z==NewB<;&DlD&X$%DwIg3i3EnAR!C)z!YfREfL~=(5K31L!S_fnN$Yg4fL9hHDLiw zEL*%6w59!|eV7wSPx}p;w7~*zg)HPpooL<&Tid~aL@DXA#`9UWz{6^z3TbBEP;u8x zJ;Qv{m4tGduyQX&!jR0AmjjonR-1QplHrl_MQ7D$G*c!N*-e*$E;4pwW>KzQfOnNe zMC7CBHs8oI}q#d2`j&L??NziiiZNMvR;Ha~2XT@&ogH-^3u+TadeK3dkqk*Gzt%LlGQVXI2?@CluL zoOCz)zuL+x72QoZ3LCo|ELX*RNyC9mvQU-Nh?qbUN;yt0 z5IM4ZgCU%EWF~?pTDc7y#uA8ddN!Tg(!Oc<@M@C!T;J$ZynhEAYlE-$!FJ3C{kY$M zd=>`eUtNK-jZhVjz?-i-))oZdwk%E8&z1EEG9m@|Ih(@cNAOhtf2YE_Vr%>goTzNk z31+2a2cr?Jq#KG%ooK`l=gri6LTN8lOS9GlFh-+kKaD>n^zNE+%eg?Y z68IW-ow>@ylDB*eL2r-mg?lxk^l<6fES+zC)r)lY{}hOxih|~ef_5*3%ZG*h&Oh@R z;!UGQohY5@tVpd2=S1duZvmJK{Hf#!APJ5yPCa-=m**v+wN0rP_9cF+x`qqh53f>d zSb1KN>6!FeI}wJWicW+gUU9S7se`?uR(?lQ1Mv`q-tKoC8p~@R#bh43mY7LgzrB3W z#h9~NJ9lW&TAhB=EMke>!py-;k`Flvq_1hD&q}l-s|;zJq59%CaBqs` zFR;GBW#Z-W@xtf(#UDiw+CSJd7p=GjP?3Q<*X)@OMDS9rm%y@Zy$AUJp{=sV>*s;_ zf;zZ+#m@NSn#JAp9}}QZn*E3CYmfcY`9N)tlD0*#@HfhSAaJg2K=16*XUKui! zmv*j$eJxmVc@$tGJ9ww4>D;n!^JjA4Q|=`_bmI9%o_hov93$lY-s{vb-n_{o?Q7w) zksCDs!)k%9?vmLjJvj||)7Z23e4wC7$p6}t{|0coRA%7ZRx<+!TDj89m+oCEo;z3g z_5Q?=1f&qXok^VJ8%OMR#+GavD-n>)+PF_w+ugq4*J)BQeV~e{StIQ=aarC_c26(e zZsd8YQBFM6OFYudZ(!eqZLP$5`#|BU;jf|H);j#wK-!?4Ka7&Dzds^OfzchU7R{!6 zwzPlBzvi{+M)++yIN?Y_q!vXGY!49*?cqH+Eq$;xQ1xxP4tOdQ;@{nTR~ThH>yv*+ z_rwM)mgcOc-U{V~0X=PInZ(Th&}DJQTCMNw9c@|U;AJ(t^yYYVL|A8bu~7JaQx>(f zb2_Qmw0Z@!I%uYKe|2|pZ0B7MTJxBvg**|fWg|m0D5R)8le}- zLOk&z;%237M*-fv9dVdH{x}C_o5=xQ0ZoHMSy%j<9D6sT56luTQn_@fbejTe?^DlQ zmJ*irz{XJpyX~DoD$j%{hiDqCvDl9Rrr*CriRC7Wh=z_6H0NB`pTwloD9(nHUn%Ij z%{?>!#+a{FnxV44xS+6}UMz5v#MWFQb8%S4cg@p>;1b8Sk^O|hf9WNaG5`L8PxC8Y zESzNd%?=ZnTm*rTH<{a){^aMu6FEI zwKV3I%S8*cGA*awsNR8tufKoP7qM#i=0gBfje<7NIcB8KlCT`u+@kEPsT0`yI-iLm z7AMftFacqu-!hdeNBz%Y^7CDwYiiySth3RtLsjSnsCxFkMaEuD2p?vV}|(}69Kv37a2 z&nzBY)R{k$MGXDv-e50J_UDF2i-Y%<7>C2aX-tcQXLHuny;0QLMO2iIorXFQq2y_# zzFF^exLSHwW@zS`E|kxAUoPl3hpsy-lhtCJ1)vhVmV>v;#PN0DUo;uDye=T2@{X#z zt2KX`Wt6}mE)^{1&TwhB^6q2Rwm?EByo1sqn%FA5@hww@yB1s$6tnUQ@izX2G$RZxzEq1HOBwn{XN_8u^BUSx`HyzGft5j)0eDxY3b=imPC0Uw$n|WEn%YAHMU&L9Tl=kxT5WviRagHaK~qh z>jpXcuCB7C>)C9hsf>sYW_>*n?$Q?*D^P?OE53f<8*RrHw>)I5^zEM_VsBa&5FD>k zmt?yqvL<2=9iw0oA^SqlqDH&UYa=feEUU2CP;Jon=NvzTAyfkE+SdvySHglADi!p% zw4d?+ zruv1HbLV#-nzaWXY8ZoF&MI^krOk=f-FwR2XGewo7rY&}EFdpKZUF8L262WQX^Wv2 zY18eOe*wy92}aHs8g&2rQ)Ik$6x|zp7fguw^}63#(ge;FqFJQ~dgT{=td?N^sUDU( z;Kb!NVj)A6<=4RKsk?aev<2RWFDkL+rzJK04=5ms{`6c5Aek7hFoe9G8oy|!BtjuC zv)Xke()WO{a?K{e__YzaAJwP2UImR$xx)r5CN;- zx9OLOmJjIU(}(r%O(!OwTzzpL!8>j{VUWgM zK|>JiW0WGtdIx#RxQx~9ePaxn4XAoE$}39U&=nUH@(k-7x+n5&XA-;e&+_`)jP6P( z5J68f9;r`6gjbokJ3U&3U?nDcgQ?dMwhKi>^n@>DWRp5HeTaYGA5Ojzsy`0C1`bv z#c~x9UZd>TY-h9zP`*~|l}{i+V6Yj{7a$lJlIC)x_lREPZyQxy!^P5e4@gt!doiKx{p<1z%)aSQ9o&)Ft zAa1!S3?G*j(C3uHTsQHvC+ zt-M4c>aM!~RH&$4d|yS-z4(q3T+)Y-if&wu^daQVgJ@z8(@j~5g(ybP6bZ$xV1uUh z*nG+&!tNxSnAIb?!p`ZkLOUfi(=NfvM(PR}qJoD-vF8&ZzidJEMC>M>tq`jBL|ST+ zeH|)_0u7$`?3Jh;N|Ai5I5tf4IEs2r1poy6UE1YJ<_~FjJ82}Ro}>{Ue_lZte~N19 z$t*Ih5Kw{F{NIz7thSn>IzRC}*vbmPyGLWhrc&+;RADxiCr5r6i4L2p0^j3^>Qd#> z(nKu}jo7mvL^UWdF}zU3>0>w3)TC@`cqRAWncA-h*Wp?Jc2Qk-d*Xjz8$Lx)f?L1* z-sHg7mk* zcw=uS(t0AvSVAOh5%P#aFvjTHYroBo=i#+^AG<5BmsYP2_7jYPf{n4Q@$FGrS~V4H z@&#wfN8uWVr|=rG>H$ZURDWnp2}U){6;6L8w$PvlF@`Z5;l4%eCJZW;00pHXEGKRw36^2#NP17ur2l#sux zE~jcIqz{KBBdp5aWp!jQI^Xr{Vo1+iu=(PKdHWirS0)Y}rm49Yo4uvw;KXR4EQw1D zt*eh7bXDN3YN{X++2s)a9YoAbyJ04(YNx#mIoPze9UHl|eH@(cDBKv={eCCFi$5ex z-EIX+Q_a@KdOtRS>gL`-m(C5mOVPo&@CJ*r65=`il>S&fhh~7UK`ZdHpmhycvwlNb zSbxxpxLb!0BiKrrWgkOmcOy`p!cjqBZ%{a9*9>eC!TeCuoUE=YuI#GJOCaqr>&*^w zgC>rietd!{A2Jz|mc?iPiFJo9q3;KtT9@%G%-3=DSPyMRwVUo>8@q_oEFb@f&84oL z`<|cX`}>$aMD0wHJA+^pvnJpdBsKdCKk?lAmb7`O?fEt z)I8qMh%?~=pI$QE|CBB?Cp3LGJvay^LuV>wci4yf`%L;xzk5FBRrl6i{^qZ7aJ0bFns5SR#pO?aDvv|` zpvA_s@Y5W>JH0_-g+U`m{0IHoi;bqxlCyeT68FV1FKR;%jJ7O8h^zv_F-E0iw*u66!kc61*0jMN0yZAUd6re1qNy(p@LB!j{HnaHAye0Snq$0;H-7b#pxU>GyVNJ_X``+PeQ1r40%p? z9V#faY%es9xw2Qw5q`z~sL%+Z(h>|cyq`3`$7jHa_*3YuOGURJXn%0#WIW;r<0Td$ zm>dv-4}UNSJsW>MLp8S-#Ok^I31a;TjG7sUHh;cDEcedfUlILjJcdmLSGs9qVeNv% z7IjrQrJZ>*`M9eYJRrFZ139f!PdyLO(vq0&=*(m~Hwjw>Sw(xpOIL#mq6 zM?zwdU)xj(KzmA0WfYRW1CCW^tLO-+K9{Cna4xNVqu=wSOx$L-HD{=-rSUevIC?*4 ztA+~^Me_C$s7kp-DaI1nTOV~YJ zuo5zFUu2>f^&MW~`>gKD)xj9Uik-v#JQJ}>wcJMFh0I)cF0KO|HBtzr0YEssEx!0D z^qZSMXBq%N1O~B+fJ+$Om)~CtQwh|6gg5S*tY%&ilwm5O*l`%QNMyD3o9&#OxI|T9<|qOuflZWZd&#F*m)i@T#F`u^l~4wNlMWRm>-dp>;{s z*TWRiQR&M14w-bFdfzTonA_56#rW~W1ju($V zn=r6vYjC9ub_kH-%Co%m>3lqOR)@rp1-=1iEK2CQ7+4ra}MVuZ;B4zqD8Yg(Q zslsdSJ`LMd@k_CWRwQcH3!Q7XvERq_^2&0}aQrzrz)TiHC<8Xh6TfZ}A=I4Q13E?g zpz#DW{tQJsh=oA*ePL0hE4+(<4)`VKTRE7fi%I24LK7uW6D8!S_0wGC#PrxldBW!6 zrPAIg_bcJ3k&0wQo(+be-I1Dy_)~{@Qby{E1S9!gd%UiFb;g>6Z@?<2nj!>Zf2fEV zl4s;d@!IezX`=bR%X~7T_;ADpc6JaP*a|%B86gOKCvChO3S9|0e=KDpNJe+^kV~(I zu{n|n(!cK8gMFZ|ZJAEtlyuh}f!f~J}H z$bG6>IIg6z%cXpGHf=r+bd?ueu7@Mc(h0NoogcWylsy-rQPXvIu3}9i*FGE4GX^Z3 z;0$omrB08uHL`$U_7`(uPR)qULn+Xg$j8Q+uS5bO-&URjT3-FOiJ)L6h^J)4(2W9w z-d|qX&kME@+w3X!S5Izk7-y5)PQ*gb@r#e+g{J$LrLEQZ>c z5)Y$V|A<&oLJZwG82Y)Eua9&vt_|!wu;Go5HHI%JAU!j zDyMlNEFuH!aE)QDD0ls|ksG$;fduyXjH z8tbsF)TRr^sbn?Ws>Y<{4I%q?L0rYAbgG9TQCV2Y*x6*JzfMsDuQ@b+M5nP963_O@ z>cKP{v4;z<$%LL&(3u4*5}G?TKWh$|Ppv9NK0yYW9K@rN?~|&3-%&WUays}!DQFdV z^=>pX$HaD%tenVS{zaA`t!BlS*}imMR`N8;7a{6AN>Q4;k%@ehliP>%DJXZtkTV#b zs%H`X8V0l&gIsGPZE>oS{M|CcvBw5ggJD6VhSfsoe#!6qNgM%k{?__EEkm%%J@PpT zrayO(*vlz7c&?EZjka*{FyKdx=KMkTD&4-+Z|LYJ z>*cSDomIZMLcN4xFgN18El{_tUE`jnviPfmR=il21nDYLLe^4$YV(^p_dSATfY9AW zj%e0k4pb|sH$}*y_bFnu0<|&43?D#}HU`-^SFNwAB748lor69y5s1WG8ZC*AYchd| z8zxH4I8b^tIW4h-Dc4ZLEv|egT+!)LRX4_#$4#r^6r^*p6vbG+Yndb&Nt!xhU9k-H zQ1`D|EOoFL<7c5;5u=th&YD|e$l)z%9HK?&0OWD$B?|UL0thVW;kUlp@LC@HA60?w}t-58& zRu|PKeg>%LdbGlHlL>{%QSf`SS6|7EwDY)5z`=>?hA(t_q($lr9bc_{Ga}rQbImt? zVteWY3syB7ml0AtVN$VQ+Xyaqm_iWxef;)VSCDj!^QdtH1a2n8TIUOs1KB0Xl}$ zK~#XEo_~v9_&)I6tTS};?L9l|%PTDtz`DE1*J zI*D!7GJ+=e4cN|DkGMx2CoLt9Hy*y83$@VBA+%o}$)yg&;N+mkYg;jzu2ID)W>*Gh zu`jk3uMi;BF=&nY6u5y?37Qbez4UZC6MeX`l=O^#BJT#XWIz?eNbBk;R`XBf;Fol2 zk|t{!uxWuTQ=BFpH0=f$xGYk=MsoRO1bqBEj;&qHNAxS)MAob4)!dr;%bAU za%h!@cNVHo=*0ZV8^gr7dPqbg4R~9F;wt3BbJd1>U2=qy63-Lhs=sL%;`G(y4=t^| zl?3Rbwp=@|8 z!FA9EPDjUrbym%Ts}aMHMVh}OW(VI`qN(@Ss`as%zNV*OjeTUIkCc*hI6HfCs7ubQ z%Y(ey5UO!$KID~NmF<<_zEl;*8Kcn}ZSv*0GeV>e!9$#3D^$AQO&>X&P)|5#GSfxowLJv~Om+p*H8QJy|P!+@L6{rHTyBi(Cp(88v(%Gr4sADO1 zM~h%g)B3Z~9tS&Ts<#Y}W^yzvP1#-k)E&bzTn*ebs5_K%Sdj6TS^Ai={PvrXX(s*V zdF0j2T?;f124^nQ^m2!WAn5>X1n#L3=$^b@5mE_uy27-D|7G#<$?HaE_vf_0MY=hrmBEv2N)?Y1R z{?rnPV_}|lkz$ivmmIV$jHxNcNt%o*XSlx(wI!LT!>g{ExT=7ov96pEri3HujJ2f@ zT@Yc_hZ2eZurb&?q`^0%w$;~fs`x$QuSTXy(4n5Y`Z#IF-9;ESJ<qm`DTD z9>P>62qaV}Z4;%BEL55+mfJ64hxlUcuc;}l^JIJtd6S^eJ$xEh5_Zv43N8uu`lM@w z%DCdRy%MzA;1hA5IlUf0jiu&eVkN)Cs}KZ*Q17PK;q{{PVfN!~vzY>~)H}o;G~Abz z^j-)PzG=VUz^?vo-9V7eE1I?{CM(=I#;~18JfHF~bT?rB5<9&L$>UBC#h=qPh4fnJV(Qd6OE!HCl{66u@osY1BI?SN%aLaJ=8T#kDG?rKC{A%uF z8(OV5=8F-UPoBPp;Y28gGv`W&&zz?AI{A*CXFsq3TtSNiYK|g$yleTmIHLW*C9jim8ie6@VuC77w zyB8c_J&4_D#BUa;9?l-3z-&}v)4u^|)aN(puMU&rceTI-TLINSB3Rv2-*SL@#b6}y@QeDCa=Mp=UklNnq=@4Gkp0Spe6O=Q>9`{KNxI- zVOL`I{;A?ovldEi5DanJqQZF(&i4WT1?_B31c7Z@nl{0BX-{ZoQaW?DSEJjo(%j$h z@RCXUdpcatOfw*NP)kz!teSr{bHJr_kYjx&*q!e)0AeY-8?ilDZg5534^fr}uWfB{ zZfJ3f;lK{F!P&=i;E`NBk7T##Yk;?Y9W|KspC$_YPe<_+Jj)FKum8oLSoYZ;qxlnj zJwSH(Pq_rX_%j6poNNIYz`q8d#;(A1b#+JoMsQ_*_TvV~VfU2aiOXg`Z@=Rk26}}W zpWm&I{2HyWp$2t(Cdt5p>dc;|dk%|FMy=K&D``s>bfZ1zR-C!2m1E8AKZX%X7Trjg z?UM~J9SQ=;QCM@IsTu{@@W1`-DheV$LGKqm>4F=SM9o_j1YAvD5W{PEQ|R_D>&PHJ*}ios^pUjiQ6KJFNVaJ=v6t z`IzJFw3mwf{HE*p9WsF&-8PPdCu_2*J-5x?^a}k`z}UNkTz!w(<+V&Kfl1i>yO|uy zys%(uJK`?86{VglSIPW8nE5(Yj8DAk!6VkuHByu6MIz@S+rOjc#qdW*t7{7{TZhdZ7tqh6@m@V~;LI+Bt5PuH#qtWmx^ z*AP@XcbfQDp>6Rlq$1yhza1p=6e9gWgglsmEEKxD8`!5A!O4LZ8i#e?pX&rnQMtfHT*RcQ7Sp3s`$qt5at-DY(Pe6ErbhGbiZ*9hQ`=`j0y=8!|C; zRzeY_6K0_vCbob4v2iC+;*cgutWI7-o^@G{i>!*neYm8pLsd~BS68Uso}tpYwhXCE zA8}w<=M$4V5Fv={Uh6VDMFjOX)G28+0aZ3x{zN_JHJkN*s!#qT9paQjAHkg5cW6pr)w4MA#^bt>~yFTW&(Ox$d#++fda?;~2&Xyq8js?3H5; zvqBCAqC)m{uVym#b&s26i~TbAgI1FbT;H3n@Ug)=q$k5xIK1GK)BUBEgY;+_)+92c zJi%}LuU9XUtpV2+KdPF_=By)3E`OgF&g66iIw>minrAac$cC>-g&Qez^WvuPU2qBy zFh!Xrlf;?~(*biZ8pcNB+V?R`M_$ z+j1vFxl9h?*SX=Ma+vR7Vt8rVw;`MpI5!Oo@+b8cYeaPNG4HN9!}Q8t71oA0$j_(37+*+-!yo=P5W-l4h4$Ql}F~t9*PN zRBqkoQCo^*&byt!hX}YC6dy&1%7}mun&XoJ0B>xd&-=mZi&) zN^XK$_0oG~#qLpI!Rex$WwBhS7^#^^ueGn(B_$fH!P|1 zQHngrLM!6TEuJ&P8!lD4;f-fp?d|P++G;K7GNf-@&38dzM7h0?4n2kr#KBp)kmZ54 zU*S!VVjdonA5JyQsikd83oL-rah5>YV~RA5saigGMG}phYbksi66+!_;28zi!KxUL zc1E1Yw=NN2f!#>smVi+aA}#23wx1coBzZ?YQ_VEq?pnkgFW5Cs0;lKw9pbw-QsJBh z)(A2?=8j{Uttpx1)WQwl;@PZN*2Or@XK?Sq)1aouDs7y#3FRPBXVX2`RQY|4hO0Ao zRv15YYO_yP)YI!sq{BGaXW)(Gl(hR=j~iIiMD*;LapKI;bNcn}AYT+wOu{>Y#V~~5 zJt~Z1y0e%3xI_;bt1K3k_$S5+I@`Era9;xdBAbM%!A%s&sZm;!wMYMQra`2hTj zI`032GS^SuU&I|luO=nE|KYtP1K+>uers<2Kl$DD|Hm57BE0-{7oR(hOa}nmG+}wr z$pqm$DZ?9~UMubU^lt_QYTS!XZ#Q?)UGzbwOw&k(&_Dpdh1TylC`b z23K_==RDY^HyMgCMdIuA*YyLw(4^JgiF!~HCK%XsMUvb(>0Qp^!IQnU$A!aIlH4=y z@>G1m_eSU8tbAcH$6vIA!EjJA`_1H?Q5yWvq&Jh4;duBrZboNQx1mv?9kgHB^*H&q syOz#Z6%tS5k&+Q$y_aWaD52I`cO%rzA|sFCTx)!u_s0{{NmTMJ ztg(Z20ypYaE3Mk}QQx*r0P7gakF>8I(-rL)uP)6xTlPz}nq7-FZGFuac?2xqBkERB z5YzKsN1i{ca=c~u=vOaP%|oF^696FK?Ry9C{i6SMqpr{2(6!p6d=-ZYWV)#Wf&kg~ z`vivouOE5l*7vX1*N{Lo>rbTDTS3am&lnk?{EmS}izL}xMeN{y&C!2 z9|mv(SO;{!B13R`I`N&maYrKPIt$Lqi$(ga3w+Wt`r^!bs;R4X&x0X6`WRdTs?Fez z{+Z{lw!dY&>}naEwQFfzv1#3$(K4=SvE}!3xqTh^3XAjOs_1Px;;J^IUmH3Sk5}b*q5myt$k$O$3fV;jHLE;TK30y^L^|oaUH0Qr~#~zN5uBi ze|;Z&t8?qlxAxAfj(a=ltGQWymigA6Y)b_8o+GXGC4vef5~b^ZTen!%qXIT&HocvVcT-` zKY>bDd3En`G|;n*z!vSTt)Fl9IQoD7DLjg_?{lrY=6V$x%kMbplj!N~xd&>`{;$)F z?qAli0YsC~r)fakt(X+xo>H&Po4tFPuJuZr`u(0c@T3-Cy8;( zx}A>ZI=9!(yuEK0+3ge6cKrpWIF=B3)MHfF?I3w6)aq~b>gZRg&{{()a=J*~?X(y* zm=Nr??AB>wE{7YzGx?khBZ-#T6KI9ZG!g$L%_)cbh$u9l?ZS={$v7e+^mX@nzJ^Q= z5cz7@i`nUK`y|HS%>8ImjxMl?;AyI!j~FYk&lxSa(pQXqO6$`I5vhQq86y2e>&B1C z{@Y8yJ%ak|W!-%FF37#}S|WENzR19iCm01_*5 zVWeM$NyZgdz@Sn>S?RlWixKtOmQ7Z}CE7JepIT*f`PCm+FtymL=+|$SEHh9|0AA+K zIM$#{XCPNF9N%Y;5bN#))AVsP5p@f6xxep90>f0fFrUli7bsy{9Vj5j-Ceb{1jx=# z5O9y<{PiH%NJdu;nYmhPE{OuJ zBN`;LVwOJXoT%_qZ)yof8gn3}79#?Ic2bKYq*1j$glx1Tf0#r~)Ewp>5&;VHQ5zaw zvRNpb@SETjGRFm73;kmMHAXQCb{fA)&+qiRC33xauS?LOhfq5iYKAnrotw-aY%vPD zH=j5}AJWsv>l9)Pn{t&uv&hK^^;%DUmDZH(lDZ0+s1*cq7?~Ev@U5_Ki#su;wNOxaQQ0{~>SPrdZXc_MA8OowfGO3j8DJ`n9G#qviw*1E_xhSp>|0Bqv^p z7hxMdw4fG0x7VE?KMx*S5GCmIDQc`A8+-fQT-nfPk-OVZA#p9$BR5+GQZqlzCIxLAfI zgL^8xFsKXufO_iM?Ek@W&^C^%X@^wMeB>Amck#X>*^AfJdM3e8j=N&@WZ*oUwViBB zNX*o@)j#A%&+Bp%X|8`*4?q~?T!2Z&nxcGAYG&~6CJ(}Mz2aS?x|PsF<{@Dns?lc7 zf=;l3l^&*X=< z6}$xZRVcMmFP1dRg3j{i&g_!q@O*rkchjU=1GBvX>(Yvh)wkuLkGOj3g3ek;SNM@d zYnj^Z_NC{b>-+Zj8a8|PAerMmRz@Y&!QIT|wB{NU!)YvT=nnS==BoyXM2_v_v1R<4 zDF=dbVBQ*M0wqI%XKv1}-+kHkB|zVWz3lfr5g&ugclir|_RP)78&IkD_g5V(*sEJp zN7Euu;KRQ3390H^)JLeS2B@_OY|^!^{S-{Qo1q0JSacZ>{o4~_PM>MNkDPi_K3~KE z50ikXzX26jeW~|AE1Q}>K)Ms_YqJSJR}9prPQ-r;W!C2yf_Bt1bKfWQ6aWM0NVCWH zpnpR>^~vYEeEI^ksQXvvLVE(B|HwJ_u5PsN^FMiezzwY(pyq6vEpW@$@bxz3>-GV7 zstJs``CeKD^r%*qJOlp=z%!*@+J_1u=u?@Kd#ky3*}e_?7x!lx65KG~t}6ptUkR%c z(2Lk5`$ug$g0Byfirys)HZM9$uGk#H;+Cte_~}KywK2@DuiaQ>b7?m${_cj395?-C z(}?9?#^zgg4|;_GJMgtHbnC2c61UZA@ZXHxoy}1(W*eJH7tS76bNV~#y9JNuq zjY?#}s`}(cSELjTEuyjF}ugQMLI;eSWx>>shy{aAp4UDQ@UA!J}1^cztZ@cqHoh{l}5oljJ zTDC%&WdbhIe*3u}Zz0&WktgJ9)=y@>=6O^VmbLnW+c%Vp8|Z9!v#YJ+2g-;|eJg1Z zMvXT$Q=6vp#rGGxgSz(I`AJ?!Gr1{^IRX$y9o$CzSn z7P@@iFJp&bk|YDJz^fw%bpRmEC{$;DRRdah8$E=GP{?X{rU)kXAX}B#yaUD{{i{ke zx^XADC+OIqR7%P)(C@n{t2C5c;~5ha_6$x;FT26 z!F{zmdVI)5s>Es;oNMHN%`$k?y|6DG2GJ*lBNucx$f$Q`ad5eDRT*+#HywS9*<7(f z>(U5E^Jj0x%oL##K@;h!F?3MV#a=PP$>g}3Mc1unI<7>+>F{!@uPE5v`tRQY1v?oOw$?-3CTp5$H-K^JFBfG+%@%V z>bhHHLC|(3OZ`{wnFFe!vieflL|?3duY#uU7BU}0)H40gWrIWb(%fSv3E=6jJ+cy+ zuvFw1Y~pE^iwO6s(y!}lA3P2HHA%K9e-C&csR@(=#5tNM3lt+Pn}{!xiCm99ExU;e zjQ;j-3w-(0&QY*$sldf0>tybFH_qaF4D`wL@ow_WUgyydNnz}*QGB^Q1tkYc z+(eU5-T@9WNTa=GsTaDr3+0Zsb7!Q@OerF;rkCtbMLAtnST3hxE?uW%_@KmE?vhsP z7B+-~--XL26s2d6Hw}{c!scoA+wCIWWQl;$S?YJwN*V4HJ&d*~*2a2i19kNb&HC$g z-&dS<6$U;bJv5=oSqz1f6WadL1v{t4hti)|b4$aCY}4fF{?P}c^*PIxHK}U7M#-oT zz8ITnY4H|p(Il|6y2$!wo2>|U!OknN#)}ZAUR4}XudbFawTr*~Qg1BW-8qd#m8R(A zQD!y#lwAx33RmTfu&kL3o#Est2 z4U9*nT&-gpG%25-mZ#$dqA2&d;;PPUHE_PJB+@sU34&I+7Fz_qqhBhB`6Fd%_Ts;l z3b*73PDFkZOUGGeFd@_mUB{l{7+QH!9GfzPjRqLOSh~`{Lv>@NBZe{Iry+y{oyyCZ z({E7sF_}Mg!@x+Xk)DmH*lVW0|4z%5&^Zp)7;il5bAM*lhP4eYUR^_Hske zW)>M^u8+Zl84{_^+Cr_~)}h=+SaudIo5G~W6F5~QPqq1}km_fNN%hs7EhC*yN^X0A zdJ;6pqHtc3DNLtvTC66`|2!%-A-MyPXC@W4~W&Z$zxCS@By%G+4>0tp7{I()-k?oPXnz51ep4O z>Qxc9eRZFG0$*u2z?EA2Hm#?X5BqD;@%%}ZYExAslVjl}!6yKM`Q@#x6Vu_V{c z;$(#DJBBX!fJc)bgT-7)TvcXe8LZx9)N}y5%Hy^@-#uiXS$yNR%K{b8TQ9@K%LX*L z_aTZ!^H@sOYgw5hnBZkNZU`@B+Lj9&LYgA+hE+A$`@ zfU40CjscW-ucI}0y~qeQ$>;o15)4DQ%6M@uF5S0EQM+qL|439kEjt`33&JUr;Y;dd;zNzi##WVI+<4q_+91+qP5S% zhGLYGK>Rz=*S4zxGFdAas257;BCffzV4y;M*WZd>rd6kb7(^T!TbX;LKLSo4dJ97T zCB;r<7;fy1tKpSSRyleBz~|w|;8yn^#nd8Qy}ycix!CnQYg>hXRc%ca7h3c__2yhn zQ1t)>sDr&K*8Atnbp_2kG6D$oL+twOH|&jGY!9BXT=LFZ;CVC|Di+_#t5gJd zO^WI}-L?rd#Gw_H7E4K-t5qs_f>kZ`rSZlxvz5Vx(n8(_Q{dTM9pZ)&K!+s}OfJ*S zl&Ux|vIk#cuh1fY&V9Jec@3S-hA=-6FY+<*IQ@XCi&m2>SkXwg8|8|G1mz7ia9iHH zbr&%D(3MT_uP(Lp9SWO#qojhka*rR2+##0CRETa1z3X@(zljY8TDH?XAP7Mq zs5hF6V4aOo&XIfLMJ;ukKiM)mnV@l1ROc7JXczvVV?0A6roTc~fQ{7~-;!4hl=ERj z*5e9r<`=*v)zdUeD!1oAE#I@EUS1nv1Mzd0#g7D~06o4UCgQ*`Mm#8d2t=2fA(xVk z!PSB*eqVJ-kDi@atFY+_&^Pr@-A<6%>_W0b@`ls#O3rE&a7vI7l*JFHd(JwM{@En#V-)p$ipK%*tSy;r0S_qHA4Col zWnG)Iz($_66DbOfMm?FtMY^H-^1XhApU?XqdqXZDM>bSV@qqI@ z3mNvbBy;{~p-Jymg>0Q-ASWJH#*7A{TobpeE)q-J`69ql*J}9Kqcb|gql?%G8pVd! zg^$c1X#Izq)R`c>0RA!v0d)j&z&vRGhuGs&kWg?QD zI@a>mPV#n|$B`*h=Y&!eU%2x zjlP&TD(vNO;B-~XX*+&~QvOvIRjzk;QIg$AffHxLO7fLwPIhk8h@1azsuSf0jsi`F z4txF6%UKkrm{JR=hF1bi5Jgs@6!`|7voFUO8{v+~83F1jZLoGdQz$Q3ng;Vi7||v{ z?+T>XL$Xn-h`@J`MZ59Hs6>xvu(KWmE`uWmw!>xu&oh)F>SX$)vV(Oga>N$aP`n|4 z|D6Q#81+_PCQ>5;McqwSb`Z^#T@%InOvQvWy?khxQ!{%sK6jxU4=R@`W81re)o4iy zOO^4wm%BtsM!k4cV}*57fNmJx0mmhu*_NsV#pHr@#vDxqhH%T{x2*qj6WqNV znt=sd0550+qIiSCWU83{Ifz@)>$-cG*w$XC5Ue;gpcI))H{j_(izIu8U4U6c4Mg`If0I; zDiF6WKymo0CtkUtfXlo8fwr#QMQW=PV)&RR8l6Me0}aa+YI;gEv|n@eyo3OFvw+3ujyeBo+E?=9)T7wiSSH`KB#6bE9V2Na!D&hdUGMKW<8{`vxV`S*6O zu#hK8O9FF;Df(g`Qo3>nvUYlG4dzfmNL=3pAE?UV4Rebg+sFA0-BG?wo)LEbhrvmb z9}zNLyJG=fv>jaEq5}U|srt_27b`62hOkB#4oEokAu7}Kyf;%|Y(@*eWc1AFkz{~; z+o|@p|J<{7Hi>Q7G8UbrT`4`TnLDx$YZU)fx-o4#EaXo3L&&?0jX_Th9>VdO_vHU1tF+B&%MFzF6ZD5PP9`h|8rfc&xwo<*CuVfr6DhaP>=!tPL^7lUZ& z;c6QziilA$1r95jBrPMiv$!R-+P@UxU=&sZDH$d+9!<&SmOoH1`HH-2e_+Q%kT=5z zX2QgGP}LVK(wHf-4}0gW%iS)rW}$+MP)gOP9A%*Z)D;O8Fqf^y8z-B*a4}$1jV9A6 z>tZc++zVxFPWGit5JJ z6g62FF1#=yMm$D>>M^?UZbFhL7Og?FrN`4UMokn6fO^;-D-f7w1Lw9R zmW@tfm&2_=UmqLpUUxKJl-rPHrqCJ}Ru8sl*4e2aLAhJ*IlGS0_S%9Hl7wM>e!7cR zL;8>7vsX$_R0gagU;wBzw5)XN!?AZ4<;YMopV~8qd5BNW_9)z+Y<&4d=A2}2FYX77 z!1pP^itgKN{clJt(P<@Q%uo2OM0zsyzylIl((OlO|5zc16Ox|6L^YhRVSjrKoX3tj z#=)#aJANiPMWrfad1r#wQ#x;aUGBBM&#C^X6|kimistp?9T`~LPFKr9uWM<9C!<)0 z*Q5NkEV104T)ikW%05(WnF*P4O!>-@RLjQHY!6N5u+AlRjEM2hhXgPAL>^Hs$_9&5 zt{Ay_)v&&rSt&@gbS-R*Us$T!+9G!be2Rz|OmENA&16V6Wed^5NSzfzJ8+_E67|Rr zp?xl^wnw%mE%_wsHOJN92-N)*1Mjl5LpKz2tiK<`Q#(MfnEPNX?t!27KT{@h*n&+Z znxe^ni+;{xIXM3=DWc>P8xY-%d66F*`h|>9FRU1^ms_{km!CF zXM_CHe{m6^Yqbhri0vnBQ!e$1tQj-eOhI$R6hdo)l`Z95KuRa6Z8Y(T2{%!XM5WML z5!t4pP3`LwvQDeU-s1hg=cNygcn+#K9s z7WBpzA*iJV2ts&76utdqM$FgCfGG;M#zthI<9Z0dTMixE)Gz~LY)*%NSkxeY`!kha z#VEllUQ^U$4p+i^)w`-+cYwAYi5HSO)b6icn$YGa4cup-tG1r9#@p9LQ&~T|qrUS8 z=g5nuC}_0wRA8%dK<136D~_bp2H89h3Vn81RyIY<*!;r6TPb233Ypl+~mI%`fM14vGF?- zlp)myr4#LA3WwMnaiZ%~WZHR6Ge6l>h?!wF0#1~Sjp76=8^NYA{Tk}T>}0~|d#_>) zjiQbxq9kJ!Sh9>^Zo9fZaT_3*Exi`=(LR2jwM)K-PKi^Q)*5Zcrb*?K;1vfO(-CPh z&|*t2E0DOsH@%B%r9lmvxh)yb!Pn;KxdAT(s$%uvO<9`T1Yma?gV;9!m2J%>sV!`x zY5bZP3vz(svCa<59Z^+iwx{{G2uW-lu0ww`zlL%5wqx)({{BuU{=|;ljG=pt6s~?}Gxp^>jtY z{O}fW$e6#JDH#d3ZAe@o*n`dXJ;DVWZF0h>_y$DacpBk(9WQVU`5ZxDn*jLEyNc*s zIetztE<{~3G>jim1WfKDN~Mm`bPSX6G*W~0y$gQ|SdkBvkhT?O0vb`~FFj*?^OmCYZbD9G>pk+u5W_ zhwq`6{s55t#`H%YCE)yk5#gD(Z%1JpV7BP zR0GHUJJxq`NrmxFCOJGP!GZ7T&JKF3MgtXe>NA^QhHW&*c(uK(yn%+QS#8LA(cl*1 zQ$-S-&YTD5yx|vIqts#ya|q2ze|bt8L-fVrwx{FF4nPhs7ilcxHjn4@l z98i^w3QJ+?EB|pXU5(E3gQC@7!)vEaa^I$Twh~R4<6a|CE8xX#Sg7a#@w}QQ;2GfJ z(CNTloj^gAxHmLTQ67~GcCMH4ozXapaUMeY-mm-68rkwi^hyW=mm< z6oF#d$AYf3q;Pko#%P>t~d8-?v1@c0T&LY~I%$hrl3T-IF8{OkgeW~4) z;5E4(?TFNlEJMEnF;pg_%F79!h6h$s&d^Ur5*ulgdl3!aHLH02^I<}5g2qA1UNhSU z`NrCn*{WMy-H7^cc53yzQ>Q}Ps9rdpo$sFzlRleDGqiqnrT-o_*qnpV1z9{#q8dL* z7M(agI*TsJ`V~0=YNDGqd6zinhAR()6iwqZj*ffKrT>r;!&({Jw7i;jQZ@UP@LP=| z>m}mY*rw~S5P27|JV87*4b?}YR^n!{d$}>4|3T6dNpCGhLPpl;lCq=_zpHEmYcfze z^o|IPHY@dswxBAd1o>^1ncfg({Y*Qfnf=9=oO8WymMqB+Q?T}%@= zQ%&`U{JrYwDs=h{<8FFOxQ|@PRMN=+kcE`gy;Up)Ea=)=9GSbNypidnf zlp{IHc)yVwV8c5T%Mi+3ohgS+{+0qc#)g9xGNV%2%2=2!;E04(Wcg-tfZ0h`@Ip|S6Q{k80UdNv5YvVTC9@?-M zvGkkfO&2?w8Rdv{sESKDcOpY-l$VMw7>OiOI?H!uTe-wJ2;cRzvApt2ZH4JVJ@O2G zLJ5|*ScguFKhp*F&?54TpES%JWSOQ^p`^HK>hIr>=!}`H*5dp~(sLC>-Xlvmih@LB z-kl0SfL)Ww!QA|a*ojIrVpxBVq`;Ua#%H>`>~*Oq zd>D49T)7r6(<83bSou#l4X|A+IFe=?5Ob6gy;;o<_)f?ws_{83YnHbg8rijJ3@r>! zEUnNO(a3%0hh=x^IN2VY99C;~x_=JfytvH_RqFhT1xp-PUDFVZG4ru@OHqP7l*THJ zi*2k$6t$9NhP1!Gxo|-v)wB*Gqchx*(>078(Y<_hHNBGBbyd&AG7KzY z@x3=O`<`hT$GbUO$T}^1fn|tDWB%=ZU(6-wAU0?>z-c^tu^bVzpR<`_M{#}nYnO$I zq&#|D>gk3&0Z5wwlsSPefQJXhWvA~_u&l*~qEt!^6k+^8;3GAMoF3q+)QH8EZNyNhN56zapMzd4$op$|87H8)| zFM>${&xei@Rcne~$PJ?bA)ojdc(4&&ONG)bQN-eqXj9QZrhUjHSGdWTh>T`N!AFK4 zre}7}wQeo(H;fDk9dXa3Xtfs=SG<;A6q+-O`bh*PBe4u9wE{%O=( zGn7Ek$fZFHH8d984Ef7Kk6jtP70Ni8;*TnlcKWG;eY#fDZ_4+KFI(II%Dr~E;MT+~ zt5~3&2$l!En1~zfaQxoJ4ibk0;tx7eP=nt>&E-&ZRzV!*@sfmN+YKrAZ!?KjZ1>yg zK8PbYvpvKhklNzFg3ZCP=l%K0UnLQ^ix$^Ur=#9#JonqRLA-J$41W@YklG@$BnLMf zEI~wTkU-S?8eAiQbwSexe9f;Zc#~yS9H?|^hy9gnxU#S)bRaT)Eq{7uX)V4ORCCYl z8xiKydtLVi0&CLjYHq1GSZZpfDf8+nsnjQ`-*$Q+zMFr_ZAL$&##BI=f|O}xO3d{M zw0G&XuXoc1$?(*R8Ue*y6XEoaRs!mTDL2p55#XSc4@X!I7$|fqf?Z`^AGqSBuR>gZ zzt61x@dL%uxxnY`q&7tisNE_Ji^tU`6`-E5O;#S|Oq|44U9u3eOHZEi1Em*{*+?wV zc%-Pf6Q7QlpQvNH#T7xvdm)z zP65EOja|FiLm=acb`S7D9Kc!!czT`N_A&T=0c_4l7(ac_vGiWQ0IX(K?P_*3{@HB& zvjK*<11+zJOfKKuoPhSNS5;pCoV@{*=~G-n%=UsM;WEamCC|U-;$_CF!b!odya+yC zQxJR7pea{xLuWb4@m+6->~kesodw3;)V5fhJY`gjeq5f8dR#rg!-a>Y*|s>sJU)Iu zC#Cj%I3(3;1+OwPI!I&URynR`#c?{j^@EkIht6^XD{u*EH~Gy?2k#R8E8G3Z7emhU zpFAnzgCkjABZxRd>)r1{CD@kk>4M@YS9o?O^si6&Y_Pn^KZGwP)s1S zlIfqdAMj~(n38xukjtuJax1}7Vd}0?qoGV18F_x@75q#s2c526HhG!+^YHr|1Kvld zE+REW_Htx>O!oX(ca7vqtKsh=;0St6V27r@W&OLmqXowd$pVW5|CCFoZe`K~L%%rr zH<{)gf^flKNxX_G1{eAf6e!u@6xOke5(A=sDiw2&JGn~(=E0A-Bb$2$t<&VogIX2{>O?_$w!6%H^Ey`=yiu`|!X#rw zpws;AE*g%kH2pyR&4eVvd|VFWanGJ65 zpa#%5#9L~26IDhNjbzr~z|{i@9Yqq(!+)WZE{TW%DF7;HVExOE$m`l8Pcd zo3k;{@%K=hCo*&(voiI(`zTJj)W+g^c|?jPc_2?DgnVFICZ9j`dslo?+xdphIIh#8 z@Y52nts2?rZl(GKcYB+q49wHsJT0&(9n$o|12mu@+?Z9Hrp=&t4Fe&iEYLIqZz>bG-&cG)%{n30G+`(Q1q}h0`sfiO|{|VLtTW4AIF}GH63c zx7}U6aPWB$g3`Q3%K1%h&Nrq%@;uoPnxMO=XHNpN!oRjw3byb2xMzXOU9HZ~%)qz3 z)n_+vQ{X?slLz=w*atZaJpD&x<^fx@VoonV|M8KtZ+m}MpVfLg{*_<%Wvl@QyMT{R z`g?X%FF-Z#e|f=^tgFwsjE9Ar*KAMh$nXtFI3Hn3F{ulK`fH;IZxWWE9NK=H=o5B6 z$d4AuzW393pt!|b$A^Z{xBZIRH(=@?y9vblq9+E<%|K(u1M#$f0{;=B8}|TT`0pLf z3E;nUD`s=^$CioYE6II`swIj?r$i-R7Jl-P)hs}v)EWwX4q{2(=rFCsRb zF9M+juS-&vfTQMl3f4(zG+$}qq+*QMD18H+ag1mn7+@5!Z57*v1F7ce1 z#bNXpHwioLugDd_%K1GdOvp3$6zhPCJ~Op}z!q zjY8XJh(k6#xGw8EP}bRd(#r-`4%NSdf6+#@&|HYTq@7Y@dA_xx2DH*$g|WDqfJUrF zWg50A@c0P3O!J`h8Fa^JeW6vIJHG+o^q9)OY@f2`sV(0O17VD*_M(UlqvZ2XsJuX+AAlD~o4#s~cR0{q_v z5BtYkM`>|$JdC;gyhCZ$oMC?5+)z14Y(h2bw|P6F{0TDt<#zM%y?YVwMx++-51-pd zZIPaTp-$ELen_i=>yF&*VZt-=u0x7AXb~@PL*l3hmd`}ISZ%}=@Y7=cjTI1pid(So zmTOQ5CDHN{kPfqN2>NGd+GjlRZHx%6#5(x@=&U5?v_`Lr;R$2Bw8^sYdRxXIKkZH!zJuwYMBSqB1T=C{!R?(-u z4E7t)aFv1|NoOUb4-p(o)Ux)G8F`?FC6yNc#J(^T2;Y1HA<`^{Fy*N~PblSsWvfw0 z0{7E^`JtuG8nSN?(g^N|eVR=_^J4aVj#s*HDD*A~}u2<9VC^6Q*-X z!PEp;J?eQQ60KMdB=1XJd5Y&Yw5!KA-j-$llby~55&WYiQ;(xARB(*@tSph2vg1G; z3RM}ZJHo>xJMrW<3#))*QTQRD1xe?0LP^6c%5)I~!6fx54Z_(l^WnsU7=w)L==DJ) zrECmJrNt_2CU%EZ0kQQD<>xyU{hc2OFIvkdc$)aD>krqETz* zsJY(KvTwdJ@DH5b%h4EeT1+A2?XB(N4Mr)lCW0-6%;)Ibp=SgekX4g|FI!=6RTc4x z{M|?uc7TOcTAUB^ItXj~%e=MV#s@n`a)5cXJt)~t?9g}){o$MsiH*|!_sVEZhQVX5 zpoZ*b!Xk{+gLP=L)buOZfti3>cN)4a9pEZ=o>wIzF#?^ z0}1|?!QZlR&geYqIV4c-^q3sLgU|MRs;+zInnvv#6^7o8b+IX=N!PgzDb(@h95DI;U#(Bx6{wc&JC2xI3k8Rp<*>Mw6>(cJ54BZ@0hJuNO31eeV9 zu!C*N#o$;qFK|xn%b;ug=?0BP1nrCQ;P4Mo!gNft80duEG|`bIx8$9>O;Ft7X=`YF zbPDH$>^QDO|A{&CZ5N<< z4YvMy2GrJWN%;bB?_GWZqrNe{fu;fi_5E)E+p1n3p#Er@^LJm*R<&)L+Sz{#{@e#6 zydE&u3+Vl`2e{U%{{8Ro#i<`qod56r;V}}Uyxbg*yxc6IfHyHir=PnMr7ihoKjR%L zyfdu&AvfTjd9KgLHi#cIFp$4$G7lnQkzZNFry#igy3ti{{}!XEOqQYjF!4e!)=KHQ zitJo|7V!Y*^y!P;l4#roFqV1Qfj$H3$AM(0w&S5IZcR28KS+|8=HEn6hSE?>Uw|UY zm!w%v+WB|EZ11aS3$0zusvKqnDFB+!)KXq2>-Z(H!y5E)@Rp?N*RfBe1`8k{cpDj{ z&De)BV*LjN5fZ#ONeiOF64 z{9(fMG2-!Q(D!do_nMj92RF2{NvrLzPCH*&csT%l_yc8BTSuK*tq)`5G-;?jzw#Jf b%jjk3$ECJ!mHm?`0Encl1xNz10|og%`a3~g literal 0 HcmV?d00001 diff --git a/plugin-root/usr/local/emhttp/plugins/u-navigator/api/common.php b/plugin-root/usr/local/emhttp/plugins/u-navigator/api/common.php index 53b86b3..afe2d8d 100644 --- a/plugin-root/usr/local/emhttp/plugins/u-navigator/api/common.php +++ b/plugin-root/usr/local/emhttp/plugins/u-navigator/api/common.php @@ -1,6 +1,7 @@ $_GET['id'] ?? '0', - 'type' => 'unknown', - 'status' => 'completed', - 'progress' => 100, - 'message' => 'Completed', - 'error' => null, - 'createdAt' => date(DATE_ATOM), - 'updatedAt' => date(DATE_ATOM), - ]); +if (!in_array($action, ['move', 'copy'], true)) { + unav_error(404, 'Unknown job action'); } $data = unav_request_json(); @@ -22,24 +17,29 @@ $source = unav_existing_path($data['source'] ?? null); $destination = unav_destination_path($data['destination'] ?? null); unav_guard_destination($source['path'], $destination['path']); -if ($action === 'move') { - if (!rename($source['path'], $destination['path'])) { - unav_copy_recursive($source['path'], $destination['path']); - unav_delete_recursive($source['path']); - } -} elseif ($action === 'copy') { - unav_copy_recursive($source['path'], $destination['path']); +$id = str_replace('.', '', uniqid($action . '-', true)); +$now = date(DATE_ATOM); +$job = [ + 'id' => $id, + 'type' => $action, + 'status' => 'queued', + 'progress' => 0, + 'message' => 'Queued', + 'error' => null, + 'source' => $source['path'], + 'destination' => $destination['path'], + 'createdAt' => $now, + 'updatedAt' => $now, +]; +unav_job_write($job); + +$worker = __DIR__ . '/job_worker.php'; +$php = is_executable('/usr/bin/php') ? '/usr/bin/php' : (PHP_BINARY ?: 'php'); +if (function_exists('exec') && is_file($worker)) { + exec(escapeshellarg($php) . ' ' . escapeshellarg($worker) . ' ' . escapeshellarg($id) . ' > /dev/null 2>&1 &'); } else { - unav_error(404, 'Unknown job action'); + require $worker; + unav_run_job($id); } -unav_json([ - 'id' => (string)time(), - 'type' => $action, - 'status' => 'completed', - 'progress' => 100, - 'message' => 'Completed', - 'error' => null, - 'createdAt' => date(DATE_ATOM), - 'updatedAt' => date(DATE_ATOM), -], 202); +unav_json($job, 202); diff --git a/plugin-root/usr/local/emhttp/plugins/u-navigator/api/job_worker.php b/plugin-root/usr/local/emhttp/plugins/u-navigator/api/job_worker.php new file mode 100644 index 0000000..cdda58f --- /dev/null +++ b/plugin-root/usr/local/emhttp/plugins/u-navigator/api/job_worker.php @@ -0,0 +1,119 @@ +getMessage(); + unav_worker_update($job, 'failed', 100, $error->getMessage()); + } +} + +function unav_worker_update(array &$job, string $status, int $progress, string $message): void { + $job['status'] = $status; + $job['progress'] = $progress; + $job['message'] = $message; + unav_job_write($job); +} + +function unav_worker_guard(string $source, string $destination): void { + $sourceReal = realpath($source); + if ($sourceReal === false) { + throw new RuntimeException('Source not found'); + } + unav_worker_assert_inside_root($sourceReal); + + $parentReal = realpath(dirname($destination)); + if ($parentReal === false) { + throw new RuntimeException('Destination parent not found'); + } + unav_worker_assert_inside_root($parentReal); + + $destinationPath = $parentReal . '/' . basename($destination); + if (file_exists($destinationPath)) { + throw new RuntimeException('Destination already exists'); + } + if ($sourceReal === $destinationPath || str_starts_with($destinationPath, rtrim($sourceReal, '/') . '/')) { + throw new RuntimeException('Destination cannot be inside source'); + } +} + +function unav_worker_assert_inside_root(string $path): void { + foreach (unav_roots() as $root) { + if (unav_is_inside($path, $root['path'])) { + return; + } + } + throw new RuntimeException('Path is outside allowed roots'); +} + +function unav_worker_move(string $source, string $destination, array &$job): void { + unav_worker_update($job, 'running', 15, 'Moving'); + if (@rename($source, $destination)) { + return; + } + unav_worker_copy($source, $destination, $job); + unav_worker_delete($source); +} + +function unav_worker_copy(string $source, string $destination, array &$job): void { + if (is_link($source)) { + throw new RuntimeException('Copying symlinks is not allowed'); + } + if (is_dir($source)) { + if (!mkdir($destination, 0777, true) && !is_dir($destination)) { + throw new RuntimeException('Could not create destination directory'); + } + $entries = array_values(array_filter(scandir($source) ?: [], fn($entry) => $entry !== '.' && $entry !== '..')); + $total = max(count($entries), 1); + foreach ($entries as $index => $entry) { + unav_worker_copy($source . '/' . $entry, $destination . '/' . $entry, $job); + unav_worker_update($job, 'running', min(95, 10 + (int)((($index + 1) / $total) * 80)), 'Copying directory'); + } + return; + } + if (!is_file($source)) { + throw new RuntimeException('Only files and directories can be copied'); + } + unav_worker_update($job, 'running', 35, 'Copying file'); + if (!copy($source, $destination)) { + throw new RuntimeException('Copy failed'); + } +} + +function unav_worker_delete(string $path): void { + if (is_link($path) || is_file($path)) { + if (!unlink($path)) { + throw new RuntimeException('Could not remove source after move'); + } + return; + } + if (is_dir($path)) { + foreach (scandir($path) ?: [] as $entry) { + if ($entry === '.' || $entry === '..') { + continue; + } + unav_worker_delete($path . '/' . $entry); + } + if (!rmdir($path)) { + throw new RuntimeException('Could not remove source directory after move'); + } + } +} diff --git a/server/public/app.js b/server/public/app.js index ef9df02..6880894 100644 --- a/server/public/app.js +++ b/server/public/app.js @@ -209,7 +209,7 @@ function renderExplorer(body, win) { dropZone.addEventListener('dragover', (event) => { if (hasDropPayload(event)) { event.preventDefault(); - setDropEffect(event, event.altKey ? 'copy' : 'move'); + setDropEffect(event, 'copy'); data.dragOver = true; dropZone.classList.add('drag-over'); } @@ -235,6 +235,9 @@ function renderExplorer(body, win) { for (const row of body.querySelectorAll('.file-row')) { const entry = data.entries.find((item) => item.path === row.dataset.path); + row.draggable = false; + row.addEventListener('dragstart', (event) => event.preventDefault()); + row.addEventListener('selectstart', (event) => event.preventDefault()); row.addEventListener('pointerdown', (event) => { lockExplorerScroll(win, body); startItemPointerDrag(event, win, entry); @@ -334,18 +337,6 @@ async function handleDrop(event, win, targetPath) { return; } - const internalPath = state.draggedEntry?.path || ''; - if (internalPath) { - const name = internalPath.split('/').filter(Boolean).at(-1); - const destination = `${targetPath.replace(/\/$/, '')}/${name}`; - if (internalPath === destination) { - throw new Error('Quelle und Ziel sind identisch.'); - } - await createJob(event.altKey ? 'copy' : 'move', internalPath, destination); - await reloadExplorerWindows([win.id, state.draggedEntry?.sourceWindowId].filter(Boolean)); - return; - } - const files = getDroppedFiles(event); if (files.length) { const form = new FormData(); @@ -378,7 +369,8 @@ async function reloadExplorerWindows(ids) { } function hasDropPayload(event) { - return Boolean(state.draggedEntry || getDroppedFiles(event).length); + const types = [...(event.dataTransfer?.types || [])]; + return types.includes('Files') || getDroppedFiles(event).length > 0; } function showError(error) { @@ -546,7 +538,7 @@ function updatePropertiesWindows() { } function startItemPointerDrag(event, win, entry) { - if (event.button !== 0) return; + if (event.button !== 0 || !entry) return; state.pointerDrag = { sourceWindowId: win.id, @@ -563,6 +555,9 @@ function startItemPointerDrag(event, win, entry) { const up = (upEvent) => finishItemPointerDrag(upEvent, move, up); document.addEventListener('pointermove', move); document.addEventListener('pointerup', up, { once: true }); + try { + event.currentTarget.setPointerCapture(event.pointerId); + } catch {} } function updateItemPointerDrag(event) { @@ -597,6 +592,7 @@ async function finishItemPointerDrag(event, move, up) { return; } + event.preventDefault(); state.suppressClickPath = drag.path; const target = findPointerDropTarget(event); if (!target) { diff --git a/server/public/styles.css b/server/public/styles.css index 635ed91..eee438f 100644 --- a/server/public/styles.css +++ b/server/public/styles.css @@ -225,6 +225,7 @@ background: var(--surface); color: var(--text); cursor: default; + -webkit-user-drag: none; } .u-nav .file-row:hover, @@ -242,6 +243,7 @@ } .u-nav .file-row { + -webkit-user-drag: none; user-select: none; } @@ -257,7 +259,9 @@ align-items: center; display: flex; gap: 8px; + -webkit-user-drag: none; min-width: 150px; + user-select: none; } .u-nav .badge { @@ -269,6 +273,8 @@ justify-content: center; min-width: 58px; padding: 2px 8px; + -webkit-user-drag: none; + user-select: none; } .u-nav .context-menu { diff --git a/u-navigator.plg b/u-navigator.plg index 7f371a8..0b08b0f 100644 --- a/u-navigator.plg +++ b/u-navigator.plg @@ -1,7 +1,7 @@ - + ]> @@ -14,7 +14,7 @@ https://git.casaderoll.de/michael/Unraid-Navigator/raw/branch/main/packages/&package; -c1ad74583e3a83dae808677aceb76d79 +da98c5f068d4f12dfddb0e7143a7aaa0