From 760771f218e579afeec4119354791af1f795e496 Mon Sep 17 00:00:00 2001 From: Kyle | Guitar <26614720+TheGuitarleader@users.noreply.github.com> Date: Wed, 9 Jul 2025 14:58:31 -0500 Subject: [PATCH 01/20] Added projects to solution --- .gitignore | 1 + Parallel.Cli/Parallel.Cli.csproj | 11 ++++++++ Parallel.Cli/Program.cs | 10 +++++++ Parallel.Cli/parallel-red.ico | Bin 0 -> 180837 bytes Parallel.Core/Class1.cs | 7 +++++ Parallel.Core/Parallel.Core.csproj | 9 ++++++ Parallel.Service/Parallel.Service.csproj | 11 ++++++++ Parallel.Service/Program.cs | 10 +++++++ Parallel.Service/parallel-red.ico | Bin 0 -> 180837 bytes Parallel.sln | 34 +++++++++++++++++++++++ 10 files changed, 93 insertions(+) create mode 100644 Parallel.Cli/Parallel.Cli.csproj create mode 100644 Parallel.Cli/Program.cs create mode 100644 Parallel.Cli/parallel-red.ico create mode 100644 Parallel.Core/Class1.cs create mode 100644 Parallel.Core/Parallel.Core.csproj create mode 100644 Parallel.Service/Parallel.Service.csproj create mode 100644 Parallel.Service/Program.cs create mode 100644 Parallel.Service/parallel-red.ico create mode 100644 Parallel.sln diff --git a/.gitignore b/.gitignore index a4fe18b..1c422db 100644 --- a/.gitignore +++ b/.gitignore @@ -397,4 +397,5 @@ FodyWeavers.xsd *.msp # JetBrains Rider +.idea/ *.sln.iml diff --git a/Parallel.Cli/Parallel.Cli.csproj b/Parallel.Cli/Parallel.Cli.csproj new file mode 100644 index 0000000..30588aa --- /dev/null +++ b/Parallel.Cli/Parallel.Cli.csproj @@ -0,0 +1,11 @@ + + + + Exe + net9.0 + enable + enable + parallel-red.ico + + + diff --git a/Parallel.Cli/Program.cs b/Parallel.Cli/Program.cs new file mode 100644 index 0000000..f90d084 --- /dev/null +++ b/Parallel.Cli/Program.cs @@ -0,0 +1,10 @@ +namespace Parallel.Cli +{ + internal class Program + { + static void Main(string[] args) + { + Console.WriteLine("Hello, World!"); + } + } +} diff --git a/Parallel.Cli/parallel-red.ico b/Parallel.Cli/parallel-red.ico new file mode 100644 index 0000000000000000000000000000000000000000..27a395dd3aeaddc603993a067aef2d4f5e755748 GIT binary patch literal 180837 zcmeFa2bdkjnYP_$;yIJ&#K}nkA&?LV5XmNEVVj(6V-su)222Jcg8>)tgB)k|A&xV8w3q&P(%;}Xr=8}-pG^7f$jV{)7! z{EsL?Y&aLkL64jBEt&uMlM|or=S*52PZA%O;k^5BWORs+_Lzge~Bc1H8Cgg zU`lC})~T4ybzZ74cF~g+@x9zwyc()=>zuK)xeA`VWV^AUk#a*RQ+Jb`X=O*Giy46)Nw=8;$TTphjTUb8EEvfvm z39G7#yKR(kt5QiQ;VO;N78hUe$LgwDx28G?R{Og7`QzQ9qKn;duDTLycQr_A9q2G6`9CyV2-rZev3=&uWo30-p20ax`J2Cymk)Z*E=f?ru|K zJyh!^6F+bZ3m-;@JKVC;-%hm*%Dhx4B8W z?uYFQ!0K%9y8v7dvs7~^cujXpN+uGof%5h9zbLKaxcTAm!xQUj4{-;yh{sUk+(lko zioA#2!h+|}$5MX&BA(5F-}jL{ccRid&KlmLKc$L`?|!nr?l5=YfM$5?;8TXZClp$9cv0k$xfK=T{uqz_<_nST6{cra z=M7$(wWR6WPxTB{okMkI>L0_6IxoGRG2!Mpolw|u%BcsVcRNmOeqJbc=a@+RjK4)=jg;dG zsL~_Vfx$X4c&}%bZZzIZ9rp9T<-kL{3{UZ{oaN+{kLNo3jBxDCzf~r7bIX%QxY5Ke zZY6bHMe;D-iE8R4>dY<)+o+$zv9tarUX~|%7vJYNCG&PIsQAstF?5I~c|TJVsgr+f z>g90iHtOVIyx-Aw^>KRrtpAhL+p+inSG>g2@D44>&xy>KT@gKy_W)ay1KoneO>Q`G zsauvf#?4JkbITKl@GM5XmFSifuOfPYE1nL#9p{;S!;vwcB$KsniuyT~tab~dW8B=z z+sL=kZm8l;wXxKKE%o@APsH1C=8r2cKZ0v>u6G@!hDOlgOx|j!L+dm4~}dXddq71WDoyP?oy$UKtzavb3gxVbs^bG@JR_d#>e^@n^1QZ~Ke!8Z$@;vFu${mYu_ zX1BIh-=-RKt*IWsH)jUljp=S>#SgG^A>WrX;Bz|P$g7~=P?!FOdirzQ5ujUg9a<@E8}5oys?gZ*60Pp{B+O1K1HhPTZS)G@@7I~*izj2PUe&6Dv$)7Yf=v&r6oJQ|D0N(c^<3-BT zkHB~#b@O?ALu;TqkFdFZIL}^iBjNkuJ&jUG$DeGhn055P-2#oKZ8jo!R#B=W$= zjSaigR%j$n6W4m;odPZ!C_|T;_CYAL7I~{XkmB$Sru-j;?z>|510Rd0<-OWLHZsL5a0~$@6rS00}g(uNorn6F(+I#btrURq-xTmk*eF)6;<8%SyAe`FAZ_8Fg;To z#Z{Uh5An?Am!E8F&lS~7fXB2^;nb9Ak<<+z6(`T7|9OO~eqm*7XK1iTJDV^)Qyj%r z8l@EvzkMj)UHxoZahmD-79{ViE=WzgC7itelT!L1m9d?8pA!&mw`jR{)xA%C7~fgq zDvi>Lhj`gO@-)*P&7?ypHpBFvbK-X)f5sm9iOEZf5@*vlMXu>LtIg=QBfGoWxJoM? z;w7Hqt@f*AwfoT)48>jim>SRh?mUk|Y8Ur}H1<*BN`_=vnaB=3N|t0A+0LUmPVU3gib~I*UiQaU7%NJQ zaC2f4$+KzbvY#7{)7FU2rZ00A@n~ul+AHVW5+?{UD#a$(o0;2v5!dUh14krx}oqK`X_f%28Yv5QGXy1zbE&|l1#~VoTncxEj^WQZ33QZ z>wCoiWo192zwoG=UvMk${hss}rt|+?+VqE+zCd+VHSseMeFgOwd|v5ScuS^aheA)R zi&l=N9g~FAm+*=4FZy<&(0tll_alE_+Pya^4QYSI|MZ2h zFMOWWl_S%QAJ`XT-}m_L4&N)0pp6mTN`k+c~uKt65X@U_U7Zcm#3 z71o}f`-XYxixPg{db+4dZ1mXgnzcxzncD4s8(@%q-I6&1gvU0T!Hk8A7C(ch}mxbS>r zE~gCsoc^P*T>-ZDVb>tiX`I#e|7;&l{l`H(zn^$387sc118>QaOv#4Vd6CG}FB%$J zh^IcF`cXdV|3=Euc;c?1EZj`pcQksxi+wMXw;Ic&Z`T^XEnR8pjV4V|(G;FHK|XKk z+QK_KmHKA|_0OuIxw$WXR8xJR;nhT6vmG_nVz=JAHP|$tclkhMFXsOb(B(w>AGPh{ zlU`x5*qOvxP5=5C%E>U|XgpcG>pUJWek9rGz&Bv!wDPiRNY|Kw>ZoUxS1XXaly~xA z^5Amve==p`DEb${eyQ!BDy*jZ5X#Lv(A(x-W6?hU^73oYZ6)6y$zDM-Xw4}4yPwq7 z90WhFuS#FC8B%|>zJ5>M`BzA@hI0EO@^=*P!b#AH=#Ys{pnUxteV37D9rfTDhOc-_ zhGa>mmAw-AE74=kEhWX%&~XTHZC_UXSy6M-VDf7&yguf=yNdVVr{J;1@D~5hX(P|J zFh+BWu|vEiL$a()BOCeZFRrZy<0a9`G4$11AoT&8J!+zFdj)zgqs|*gUGg2i1GvTd`RXTbJTNEcz4bBd zZD|>b4(flm7+o}%U@857KfL`_eRTDsH3lG_;%#L}mShHh3RB-#{q@azFkZ2wq+}*# z?=Y`_u70`t=>xhU#j$afRy@Q@JS}g@kgQBUzdvtuAdfcRLcji#%8DDQYe!@I5O?sP zCPV&Qh=29N6-RNEMrp+Zf|qzw-5MJ+$@asP2R?1$z4&;P<9swNFZZpFii_`O{^SbY z=O2U1cd643Hl%xn>6zjvuF}}F;^FhtHJhJw^=TvbAH(C1L!n>tKG9}cyDm3(`Db}~ zZ+|J$y~6a&#!+0QQCgpmt{s1Wcbvw*()-y$w|}wYb*i#$TAR2Id+pRdIzPRTD~~QIV>N7cfq7KlBdt@6g|&uRPMd!t{(d zimNmeO2xyF&o|5QjEo9)64*BGqOq4rPEqQP5Hg2^t0r9%uA2NJ*FQ1N@`d`P#i`5O z5{+ZgPqlL@KQ{LY(=)|UT&3~S!einP@wDSIRz|ktGM%Vwg8lIo@hX7w7+(mdCJiSa zCNK{3vHGq>$;%lNIi7KvJ?P7{P@h*p%Yj z$ucs%`NOVs?CcqRTu0+VJ{2Y>CksJ?U#_mwr6<3(@hZlZ;&;l1eG8J)78N8XGVXIe_-UM&F{{?O$nM6C zI(x2h9E~X@3{UZv49SvA$(HUS>6FdSAMf(?&LQn7jFJ68;|-w$Hz+!jd0FOu&6wStdAafF7w0CXe_9y7f_&bE?||lKdpcIu z)A*G}mSjq{bjXR{$z1UaqnjfgGyN*=wqtLaXO7RvA^ydo*j=9&#xG{Bcp!Rfe%aSW z^U40be+|wt_w`=$)AUu4EghtbbTV_`8XuId=|3}fjcs}!oq38Ao%Lw}Ycrzp7RK#( zM_b1YnQJm*gbJ%S6p#C1JsCgjI&LB^8(LiHAYF`3(ybC5rEBn~F-K83c2(THFYA^5 zkY9PV!9e4KWz0hrGtMX~O`Sm-zKZe3qxkcQ+^B)8|oi80<11@1 zj=sLW4H~a3y+`AvNvG`IMLFenFh0E-{R_?Ev)(Crk{hm?0!?-!sp}ZS8^}6>?@;GW zHWW#XV=QsF7r*^FtW5L%Jqy!1uH9VeB;AaT(p5U^7iOGUW35_WRx;-*r}S>OG0bkkWNXx{=sf)C1&S zbe5YJ{T((nF#o|A7XOQKqvKesvIA-38Pcco>)rp-O*%?ftMgnM*YkJ@=3f#D-A~`> zAjS=lpSEU2VP%Xz*;!h>TXEHtrKgPtYOSinxV@+_{9CZDbBl@@ zZ}h@6pO69{$><#g<|zWQ+s`*781GRTn*W94JavKNJbPbm?mghUGktZ^GbfOVG!Ic) z@-ysy%usIbbaWlUIO;L{{|NLbV=4FY4IYW^%+)2eZi+QjU6JB=lwy5TCb{Wh(owof zXX)-Z&%INSKM9%5=Z?&E57XzWG@tCw_@&eENEi+>M?Zy+7xudFrhj5IfrOlpmp#`&K$ zR9=2EWqlCiIWzcwfuYipbHK6D=&!jJKW^981@N6iehz0G6dk+J!#qP*=`7tH=cT`E zjamCSBF*J=q?&5lg9WnN&I@T>n$}rIBGFzkMd{u%@-KW7W=Ah&k{ZoCQD4Bkqsh zdUxK0mXe=8BweOCM_(V!!)h%oZ83CroHy>v%Y6!5M`j^UJlK0tuDa!2L-~16QfKVU znA9o!{}%KPW$6XV)ZSU}lP)`vpPGjhzxSALzOtKi+QTx^mCn)~yJKm-EmE@85 z)ppO1b-0xk7q+I$%X^Oe9NC2~bUNbnVF0^4qMKcI%fnR5Q{BW(GwR6YJx0$@FN`0Sn9p`VQ zn{yvxUzWk=SXXO-yO-I(+~3XcSYs$$I1gP1@jcRep|G`->F3Ck5n16; zSF3qg<>zYXL+T{0OYW$L!la{gmCg{lzb_g>o_}0iJPCONNSkTCIT*&iH}xltyfu`i zS9uTjqAhd;dTSlXV(N!y`92*8ueSN+%zT-fZ)6@f{2=`7oNYJ*ytu5 zrK{Chx-Xah%nhxakdw0%9gfUGW<7PlXx>-V8SB7f6}X&4x@PisH|Ee(m+fZcwKUf; z$J!m$^1jdya@HFvEPR6e+zEb~^Y?A2H@Znj>1uVB?jp^#uNcnU@5e<&Gm+bIu3Pin zZPW<=UBOswh7E=)E18#Ne!RNsXvX5srvCYbx$54M#^^Fo>|FD_q~uPcyZGhizDa%_ z0KW!Lrm!DE`+~kUI$B+&v#-16t2J-C>Z(xaeagl;Mwd2ygSz{3_sYw=;kVgPaq%qD z44`fp=Y?6jeEWKG)w=c+>lC%--RhwIN#~Hy@56i5RaRH&EF1LGz2?1FB`MR3^70l@ zc1M$5_G^y4Bk5VR@=Ew^F;tNMG8pZ`x8WE3zX`H-_3pZbI`vxQYo5Hp=pvn@o7GXe zN@xFPbmt%C!H4@&hx{QNew6hrdwV?m^$Njj1Mk3bl%bb+XJ4T{eU>t>^-c#-p+ar~^MODw;z%--G#e;Ski_ z-nTRl26MfqBMs^Q0WAaDh565VcHN!#b{Mkvq|DDj_qCr|os4eiCtcHj{xjLIItP0% zBHiajMfbsHA8hc~OK6Qmkb-L?{P18{&+i8f)Ox4A(fvN@{yDl_Y;`iap`Uck^waY- z9<9wIE$P;O8V)}}eehlM_I2^~?C-1PFInNR`V;HHT{=h?UoYv{)lc!(dbEywSVvjf z_y_h<@NS+@`PhkdAnfrld7!m4eJAmm%K=`6<5Eg!rU=LmL-q zZBsGhhqblGq4y5ndXsF{zI3)Wrp53OFYy#_yMAipBCtoMFJD*b)RUjm1lN~rNMZwZ z@8*wla+cFynMM8cBi0g)Fza%(PR3uK)A?E+yN1V)tF+=FUg9aG~kPne!5j^Zkf(u#+8 zS)Sr88NNJUK0$r@`TSaKpuMz_Hs9t;_34e4HC zdZsvvt28#Pc!<|V@kGm@tUf87A+h+U(tuWpG8H<6Pr7U+B8O;~>~^8Xf1ax|Snd*FuNA zQ2cV+u4DB-2>Mt3`_J?nD`NuoE!O`5`?u(SaPe{Sar1HXarJTbai`!p2S8(?)^%D0 zWV4@)Bw7E=x-EaLRomJv`~pnU8m<_$H}nf=8uSvh0s0H{KM?D<4C!8BdZsvvt29a* z^LW|yTtQi^?fSZYtZCAkskU`%T4$CtYtCfzXy{gGDQnU`VQtz!Al7$HgzkWDg>G?+ zs%|o*dmE-_ilewnqqO2-c#UQ~Sj@<1UDJjvgktSeycTk-xYY_k`)0;wY}tD6M!{Ug8OF$w1cd z=&!szkNvkUt~(6Px;bwRo5f&?bQnY)+yHH|>n6y*iVBA4_W3={6 z>&vnst*c9#wBjLN;)&hf+6>8>bc1BG&cUpy^Vjl8$F1?xdN+UFpsvWPgf4*A*|ioE zSoe1s^U>ciddH%y_wv@$b-nH`n>Bf?^#DilmW;yG1aEzZWE&kSgStuAe*Kl))4JX; zH+iRU*c)0{kevEOL6Ubh`784JK=#V9kHM^slufd~KS?|*j10+=Ov$!7NS7&lOE=kW z>@S_&FAxOREW)<{It%(kesY>yn7p2KdH5iSv0lrp{X<9ApKV=|4B=33WJ?F>Vs$dQ z6=+?fDBHD-odIS1(XhtQTQ6Aw&s(AYo0piv8kh5^vlyRe?VzQtZFf3>y$>DxBDiIZkXQdwXxE{yl&(QDk+X;F`Q_#ksWq$?g96ru-pU%VuX3W}-7@x%Y8|K6tZGfCNNZQ^ zJ-+T(4{Fz|%J-d5%!&2;+RyIRibjpD(pkD28!BfO7@K69Z0zi(J?@HQXx3TmyZ^tT z%4zJSKHRede?{!!D^6Vrv3G{6_SkFPXd=!&(&TAg_!x67O|kD6ytX|CtjpBe0d&WP zzd04y;@c$~GySx7wgeJB<+JyO{@|3)a*LS9PQ>L$$>Ls~JjQR2a#cH>HMNPo&GXXK zPYKg{TKwnH{w+$~05(IswAuO|=#O~PHEDF04b~Q8leJN{27eK{{${N(;idOfvKDtC zYjW{Da2b2oS-;F$RqNMFYi{?Z4SENJjS%fG<8vXIV10Y?@YZnqd_3{@W35&9KEryp zc3A6HyCdFJbe9dX#n>dvY&Hk_Ge!JFK`Cl^U0;lA@FLF!q-N4>}_G^d5b1;5F zZot>W6lgLu)$Hrhn&fC=BzWBJJ=624#-C-|n&oWP6!&Kw)`??-Y>`dYHe)01ey~}; zd~dC<@F==(5VX!=Y`>)91ZAO7B`tfvL+vvlS>HE&xR4ll}cUgh;>%{hBx%(Z}d_Ed_t5o=&` zDp-eQ)}-5KMXaM~OOMTI>)5x&|7NW`w#i0gtF;;1=Zi+OR`~PW+-ZC_HVrx%{5enzu?%n40c83=W0KReHOug zBx_UR>~*oYcE@{bKF3E7Hp*6OvuuByvj6x}trf4R_#W#-y7$wgIMLYt)F zkUaLUv_5-`bzEbxA73+uHyF!*Q=cdsJ=6D0_)DI(Io|a?h4j4( zDds+T&z&bA?Y(%Mxn^&lMRg5xqVk#IKYIdfCi4CdM27ZdA4EAkrgQTArCY!0_)o7m z8agUKM}qNA@XF*XDqYWx|I1d{Y;Aw`CEb^oodUmfpH>~ywqeBhJbKA)yGKK7M72-j z`^;mUNKXw+F-c#7gXiUGg*Yj7Q9ov(KZphWat8wfnNofA$o39m9GG`6)X!5bi(k z$#ns1c<}3ipW$rv_2bD#*=qM_$oA*)BmXqxX9uHqc6&5@e`D3`ammlu`atdV5T4Id z?r$Prw4PLZV)!6O_>22fg14w^UJnMz7@5)a<}M1nn(slM@b)r#v_H zKws^>(OxC(ZBae4nDYM$_1_D;6W=kiB zP~h28_99%5oO-Auudc)DNlX8gy*R#{yu5kH7!I%Q_eHhw#+RSQFR)p*gD2mB#mpfc z$NKi}_9tc1xrTChIlSJ0guycEvS)bDM;SZqex|l(1&l$|l9z4zWa61AteW=EnednW zmO`PIC}W4hs|R{xqimJU*7nzE1H8_=dM4|f@oCu2KC6z;#qWonPO|e2uzihj|6^I> z%$XVG7ZCQQ3D;G7*qmY_)l1Y^VMEhS?|9(Kdf? zS65f|idB6JyHqc~4cWa~+RN40z_%u@Jv1s83KuQo`}+fQ?rJYtCVASQC13ukx8Cu1 zy-SFN@3+d4-^{wY9kSG0dlv^#Uri_d zoAiHV`y2Fs-})pk?-|-Adyr>Y?Ct8XmrOo4Z-eIwPq*dNdryMPaP%6;9s;eE*E3kD;%@?32I;VFKrXd7M*FR<*67q-bp zi=VL>+nEb+-Wg5V{5&u3S@K1GF+1*&>w52=&h6Tra;1G&+Gi-;RQL%O zVg7;lY!zbw=NS9D=Iif8^slNfw9imuE^Ts(il)2W?`c#$rLzAlY2Rb~Q#Q+X<^Q{s zgZJJLF0tq@dSSozl?>#)R{Ly?r`sCpnTH4;Or0~F`cdy(+q2S=+mNIEW4>+KUYp3n zI|95`Q`am&Mt6H6>x^wS?~Scq*?%AV4PCA>8ZLa0x;cH1p1)VGt7`+_di4d@LaU8_ z+V5CbyEpGZ6ZPXXPiK_@u)#NQW5ZCl{a(PX`1{Xf$N8QND<>!CEymKm1Fz2ZR5lo! zWSe~t-^W(pZteBde!b;`p-rYfiJ#g9FJF)L*B17WCKIQ)AydYc&uggQ^YA+Q97o_AMD@S0maNs(YiGLBA#*zUAk#TM zYBwz3-Fx23+M8l4To27pJsRZ$yc#_J*!=@4w7!`+0XGS{30t~ zv}dpus_{to3h#_0yCVt9hWe3|dG#yTdvZ3=rns5-nRF!Yu|+o7dPz1u;@RxmF8j6T zapfS0O)=X0eIfd__p98}Zm((j-c7yBoNZaC_Sr^Hw~dUEPRD-jw;sxO-~!s7S3_3? zNdIS|YiRGM(^$fw^6RL_UPH!)RymRId}QpHLFYzei)^yC$wt{~$~;&7^#3ZKE}))f z&&x~n;dCC4zt=V0-c0{D(-)eIoXwtYo2d5|@}BD~#T}^+A1C}HXj4CsZ1-s+c)U&> zxKEnC>=DHV*&>^;5!-w_WwZY?dCx!QStW57>YXY!dyaR`fN3V%<^P&9P+}J$OZhL_ z#2ISup~GS3Sy`Fdh{DEy-hWr^m#V5d(a5poRONAJV}t!x%BEG=CL32mLHmV+AFe%| zAM8z=WY|Zr3bL{~&*Rv=Cf74!g?Oo2a&$eTj{O0dL-)A0U zHH#?wFLPZ?fA3Q4Z{S;qAN0ZnHeAp8k+dJVvf?`QlrQ#XqqE(|zxr?3ge^haGTE*$ zKhA3IR~JCEi`RT5zx1ipDOqA5KMA`~_O$=lQgf5e^HQ179%Qvs+Mf02y*+b{`8Ra6 zzVz3AWovbD3H;u!mNt-yX3i>-qnJ`_@?l8@TktNplL?eX>Z0|&2u{9ssr z-fJJV{Op}-bhY1L=`I_5dt_TSKR^B&BCkpEEoYICFnE(T&N=RY0fVx@z_)i>T!rI6 zqnmW}+8^jF-DQJsPd2vuag6Px<`8*jv6V8)#2%K&tCENd-Jsvv0MC+(8G%~C&0=1zf+vtEUzFz*dGdubcF5XO+w4;smCpOZj z+DO0Bw)=I?OIg_z+M_>3?{85@3^jZH2M@CLm@8+F3=UG@nfwDNjnayTc!?)+A=7?F z*2WtkWT)#R9k=!`D03siP|Mjk7`mLYz&O(8&(+4|Tv7TS%UG*0pFSJw1M!7ijXia> zCs1#FpML3RL%LU(o+*yvDvk9ipfav@vUrNOWJuQkNG252g@3o%PqNcVx@?L;qao_y zO-s?8apo=mfK1+nLOMftHQ%aLv?p{%(+YF1Fg>&RtGG&|wBliSi6^`zBVDfk|GIv@ zE)+?p0|^V}7dHI@yQV=eK^U<4FW5!~e#E!h5cdi*&lHEaN@LQBM+V-$+`ifEf;8pV zb-&Pcf7kuHu8pp{Mb|>tU94-a>yFhmWTTS+#r3Oxb_w&JIqcU==5|U4gbtT`n=t@4CgygHA z&tAR{;pu2-Jk;tZ;+X#T69N0yUrs?wt1pLws>$*b5u`Bx4}^|@u835P=Ukuh_&^xH z1^O@Oe1 z5N9|~{L=Fk;rWb^?}!O$)ap;dk3$-zH9W*iJjJ`n#|A$Gt$H}P90yI_m z*Z`dgEr9;R?z_|8JK2nXjnp;fJl>Mz&+sX6622vlfWE_ij6*Exo-jRA9G%^(G)gNT z0e|j~Gv5^KwLhj@u+7=H{_hGZF;$euJvx>)_JZo1!A zzczmmoN<-3=UgGbn!FkVt>@nOkT7RPCC|ry!=czV5DYl)w-pCr!an;RQ4mMYMS!Px z7bC--C8e`UBwIR2m&u%4?&&B!`*1c^w%D?7&k=EUmM3p=K6)GrErY(2KaX(g7WTRQ zjJzIc@;!LwSI_Y14nN_g@|7~OBvZ1jPZ8;2bUKzi%ojG&+1Ec?dwUX3ej`M_3}n_q z6QED=wZZ82)aIKag~v72E26;lw(3eCJ3fUq4UReSBOvA2ZKS zhCSCUH8lk96QPfEj!!;*DT?E#uxF_GYsMuVf8INEl5Wz`o;f6)r=6IO4Sn??;$!3a z(eQnV^J1(gi0=d@l--Qv<0b6U<9ywCRvO)9gKV)jVVnH7 zs4P^>vi?cB!>IH@>&wIVa+$Rc=|9pw?Y!u9@E^uFo9u2j=fhPb4>RZ9RU~N_Cl4jO zf#=kfoDrn|E0TwJL>nw5?C{!PuP-ldb(+esV?|+w6XQY?aNjT^MBg$#+Wb zT<@&OdyXaUr}D27VI6)V!C6q?67(G>cPzaUN${2J+R|EAq@`8d@3 z_VxITX|KDr1)m<+=J}BM6#qEK8Xq#kz(1?96{mcA=A*)k=TzfwWf^`~-25`m*5s_# zj(%V$qtrRpkBt1qSf4SGt^UXV2sX-A*$l~ci-Yk!13)3Bbn^LTg~2HN zz#z^)T`zwv;nGX2E%+np=of}MC^>*LM1SYEOVu`org{EY_)aAErCmSC3)i#h#J|yv zC3k>dX8$%F7Q#f>=zWvV9)%S=c#Zrj

tYWpos4X*=^( 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 + + + net9.0 + enable + enable + + + diff --git a/Parallel.Service/Parallel.Service.csproj b/Parallel.Service/Parallel.Service.csproj new file mode 100644 index 0000000..30588aa --- /dev/null +++ b/Parallel.Service/Parallel.Service.csproj @@ -0,0 +1,11 @@ + + + + Exe + net9.0 + enable + enable + parallel-red.ico + + + diff --git a/Parallel.Service/Program.cs b/Parallel.Service/Program.cs new file mode 100644 index 0000000..0f7a01d --- /dev/null +++ b/Parallel.Service/Program.cs @@ -0,0 +1,10 @@ +namespace Parallel.Service +{ + internal class Program + { + static void Main(string[] args) + { + Console.WriteLine("Hello, World!"); + } + } +} diff --git a/Parallel.Service/parallel-red.ico b/Parallel.Service/parallel-red.ico new file mode 100644 index 0000000000000000000000000000000000000000..27a395dd3aeaddc603993a067aef2d4f5e755748 GIT binary patch literal 180837 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

tYWpos4X*=^( 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 Date: Wed, 9 Jul 2025 15:26:30 -0500 Subject: [PATCH 02/20] Add nuget packages --- Parallel.Cli/Parallel.Cli.csproj | 26 ++++++++++++++------ Parallel.Cli/Program.cs | 6 +++-- Parallel.Core/Class1.cs | 7 +++--- Parallel.Core/Parallel.Core.csproj | 26 ++++++++++++++++---- Parallel.Service/Parallel.Service.csproj | 30 ++++++++++++++++++------ Parallel.Service/Program.cs | 6 +++-- 6 files changed, 75 insertions(+), 26 deletions(-) diff --git a/Parallel.Cli/Parallel.Cli.csproj b/Parallel.Cli/Parallel.Cli.csproj index 30588aa..5c561cb 100644 --- a/Parallel.Cli/Parallel.Cli.csproj +++ b/Parallel.Cli/Parallel.Cli.csproj @@ -1,11 +1,23 @@  - - Exe - net9.0 - enable - enable - parallel-red.ico - + + Exe + net9.0 + parallel-red.ico + enable + enable + Parallel + 1.0.0 + Kyle Ebbinga + Copyright $(Company). All Rights Reserved. + $(AssemblyVersion) + $(VersionPrefix)$(AssemblyVersion) + $(Company) + + + + + + diff --git a/Parallel.Cli/Program.cs b/Parallel.Cli/Program.cs index f90d084..80f9ae3 100644 --- a/Parallel.Cli/Program.cs +++ b/Parallel.Cli/Program.cs @@ -1,4 +1,6 @@ -namespace Parallel.Cli +// Copyright 2025 Kyle Ebbinga + +namespace Parallel.Cli { internal class Program { @@ -7,4 +9,4 @@ static void Main(string[] args) Console.WriteLine("Hello, World!"); } } -} +} \ No newline at end of file diff --git a/Parallel.Core/Class1.cs b/Parallel.Core/Class1.cs index 0a753c3..a2f720b 100644 --- a/Parallel.Core/Class1.cs +++ b/Parallel.Core/Class1.cs @@ -1,7 +1,8 @@ -namespace Parallel.Core +// Copyright 2025 Kyle Ebbinga + +namespace Parallel.Core { public class Class1 { - } -} +} \ No newline at end of file diff --git a/Parallel.Core/Parallel.Core.csproj b/Parallel.Core/Parallel.Core.csproj index 125f4c9..4f18314 100644 --- a/Parallel.Core/Parallel.Core.csproj +++ b/Parallel.Core/Parallel.Core.csproj @@ -1,9 +1,25 @@  - - net9.0 - enable - enable - + + net9.0 + enable + enable + Parallel + 1.0.0 + Kyle Ebbinga + Copyright $(Company). All Rights Reserved. + $(AssemblyVersion) + $(VersionPrefix)$(AssemblyVersion) + $(Company) + + + + + + + + + + diff --git a/Parallel.Service/Parallel.Service.csproj b/Parallel.Service/Parallel.Service.csproj index 30588aa..1318e1b 100644 --- a/Parallel.Service/Parallel.Service.csproj +++ b/Parallel.Service/Parallel.Service.csproj @@ -1,11 +1,27 @@  - - Exe - net9.0 - enable - enable - parallel-red.ico - + + Exe + net9.0 + enable + parallel-red.ico + enable + Parallel + 1.0.0 + Kyle Ebbinga + Copyright $(Company). All Rights Reserved. + $(AssemblyVersion) + $(VersionPrefix)$(AssemblyVersion) + $(Company) + + + + + + + + + + diff --git a/Parallel.Service/Program.cs b/Parallel.Service/Program.cs index 0f7a01d..10f94dc 100644 --- a/Parallel.Service/Program.cs +++ b/Parallel.Service/Program.cs @@ -1,4 +1,6 @@ -namespace Parallel.Service +// Copyright 2025 Kyle Ebbinga + +namespace Parallel.Service { internal class Program { @@ -7,4 +9,4 @@ static void Main(string[] args) Console.WriteLine("Hello, World!"); } } -} +} \ No newline at end of file From 8880802a21a0124b40882ba853878044699adc83 Mon Sep 17 00:00:00 2001 From: Kyle | Guitar <26614720+TheGuitarleader@users.noreply.github.com> Date: Wed, 9 Jul 2025 19:32:18 -0500 Subject: [PATCH 03/20] Added core project files --- Parallel.Core/Class1.cs | 8 - Parallel.Core/Data/FileTypes.cs | 167 ++++++++ Parallel.Core/Database/DatabaseConnection.cs | 33 ++ Parallel.Core/Database/IDatabase.cs | 157 ++++++++ Parallel.Core/Database/SqliteDatabase.cs | 212 ++++++++++ Parallel.Core/Diagnostics/IProgress.cs | 32 ++ Parallel.Core/Diagnostics/ProgressDebug.cs | 38 ++ Parallel.Core/Events/LocalFileEventArgs.cs | 19 + .../Events/MessageRecievedEventArgs.cs | 14 + .../Events/TransferFailedEventArgs.cs | 20 + .../Events/TransferUpdateEventArgs.cs | 33 ++ .../Extensions/Json/UnixTimeConverter.cs | 57 +++ .../IO/FileSystem/DotNetFileSystem.cs | 164 ++++++++ .../IO/FileSystem/FileSystemManager.cs | 48 +++ Parallel.Core/IO/FileSystem/IFileSystem.cs | 83 ++++ Parallel.Core/IO/FileSystem/SftpFileSystem.cs | 202 ++++++++++ Parallel.Core/IO/PathBuilder.cs | 100 +++++ Parallel.Core/IO/Recovery/RecoveryManager.cs | 85 ++++ Parallel.Core/IO/Recovery/RecoveryPoint.cs | 55 +++ Parallel.Core/IO/Rsync/RsyncAlgorithm.cs | 61 +++ Parallel.Core/IO/Rsync/RsyncProgress.cs | 31 ++ Parallel.Core/IO/ScanFileSystemResult.cs | 25 ++ Parallel.Core/IO/Scanning/FileScanner.cs | 374 ++++++++++++++++++ Parallel.Core/Models/SystemFile.cs | 149 +++++++ Parallel.Core/Net/Communication.cs | 70 ++++ Parallel.Core/Net/MessageResult.cs | 8 + Parallel.Core/Parallel.Core.csproj | 50 ++- Parallel.Core/Settings/DatabaseCredentials.cs | 45 +++ .../Settings/FileSystemCredentials.cs | 27 ++ Parallel.Core/Settings/ParallelSettings.cs | 70 ++++ Parallel.Core/Settings/ProfileConfig.cs | 226 +++++++++++ Parallel.Core/Utils/Converter.cs | 63 +++ Parallel.Core/Utils/Encryption.cs | 39 ++ Parallel.Core/Utils/Formatter.cs | 78 ++++ Parallel.Core/Utils/HashGenerator.cs | 46 +++ Parallel.Core/Utils/ProcessMonitor.cs | 128 ++++++ Parallel.Core/Utils/UnixTime.cs | 305 ++++++++++++++ 37 files changed, 3298 insertions(+), 24 deletions(-) delete mode 100644 Parallel.Core/Class1.cs create mode 100644 Parallel.Core/Data/FileTypes.cs create mode 100644 Parallel.Core/Database/DatabaseConnection.cs create mode 100644 Parallel.Core/Database/IDatabase.cs create mode 100644 Parallel.Core/Database/SqliteDatabase.cs create mode 100644 Parallel.Core/Diagnostics/IProgress.cs create mode 100644 Parallel.Core/Diagnostics/ProgressDebug.cs create mode 100644 Parallel.Core/Events/LocalFileEventArgs.cs create mode 100644 Parallel.Core/Events/MessageRecievedEventArgs.cs create mode 100644 Parallel.Core/Events/TransferFailedEventArgs.cs create mode 100644 Parallel.Core/Events/TransferUpdateEventArgs.cs create mode 100644 Parallel.Core/Extensions/Json/UnixTimeConverter.cs create mode 100644 Parallel.Core/IO/FileSystem/DotNetFileSystem.cs create mode 100644 Parallel.Core/IO/FileSystem/FileSystemManager.cs create mode 100644 Parallel.Core/IO/FileSystem/IFileSystem.cs create mode 100644 Parallel.Core/IO/FileSystem/SftpFileSystem.cs create mode 100644 Parallel.Core/IO/PathBuilder.cs create mode 100644 Parallel.Core/IO/Recovery/RecoveryManager.cs create mode 100644 Parallel.Core/IO/Recovery/RecoveryPoint.cs create mode 100644 Parallel.Core/IO/Rsync/RsyncAlgorithm.cs create mode 100644 Parallel.Core/IO/Rsync/RsyncProgress.cs create mode 100644 Parallel.Core/IO/ScanFileSystemResult.cs create mode 100644 Parallel.Core/IO/Scanning/FileScanner.cs create mode 100644 Parallel.Core/Models/SystemFile.cs create mode 100644 Parallel.Core/Net/Communication.cs create mode 100644 Parallel.Core/Net/MessageResult.cs create mode 100644 Parallel.Core/Settings/DatabaseCredentials.cs create mode 100644 Parallel.Core/Settings/FileSystemCredentials.cs create mode 100644 Parallel.Core/Settings/ParallelSettings.cs create mode 100644 Parallel.Core/Settings/ProfileConfig.cs create mode 100644 Parallel.Core/Utils/Converter.cs create mode 100644 Parallel.Core/Utils/Encryption.cs create mode 100644 Parallel.Core/Utils/Formatter.cs create mode 100644 Parallel.Core/Utils/HashGenerator.cs create mode 100644 Parallel.Core/Utils/ProcessMonitor.cs create mode 100644 Parallel.Core/Utils/UnixTime.cs diff --git a/Parallel.Core/Class1.cs b/Parallel.Core/Class1.cs deleted file mode 100644 index a2f720b..0000000 --- a/Parallel.Core/Class1.cs +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright 2025 Kyle Ebbinga - -namespace Parallel.Core -{ - public class Class1 - { - } -} \ No newline at end of file diff --git a/Parallel.Core/Data/FileTypes.cs b/Parallel.Core/Data/FileTypes.cs new file mode 100644 index 0000000..cd6fd24 --- /dev/null +++ b/Parallel.Core/Data/FileTypes.cs @@ -0,0 +1,167 @@ +// Copyright 2025 Kyle Ebbinga + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Parallel.Core.Data +{ + ///

+ /// The type of file. + /// + public enum FileCategory + { + Document, + Photo, + Music, + Video, + Other + }; + + /// + /// Represents file types. This class cannot be inherited. + /// + public static class FileTypes + { + /// + /// Gets the category of a file based off the extension. + /// + /// + /// + public static FileCategory GetFileCategory(string extension) + { + return extension.ToLower() switch + { + // Documents + ".asp" => FileCategory.Document, + ".aspx" => FileCategory.Document, + ".bak" => FileCategory.Document, + ".c" => FileCategory.Document, + ".cab" => FileCategory.Document, + ".cer" => FileCategory.Document, + ".cfg" => FileCategory.Document, + ".cfm" => FileCategory.Document, + ".cgi" => FileCategory.Document, + ".class" => FileCategory.Document, + ".cpl" => FileCategory.Document, + ".cpp" => FileCategory.Document, + ".cs" => FileCategory.Document, + ".css" => FileCategory.Document, + ".csv" => FileCategory.Document, + ".cur" => FileCategory.Document, + ".dat" => FileCategory.Document, + ".db" => FileCategory.Document, + ".dbf" => FileCategory.Document, + ".dll" => FileCategory.Document, + ".dmp" => FileCategory.Document, + ".doc" => FileCategory.Document, + ".docx" => FileCategory.Document, + ".drv" => FileCategory.Document, + ".fnt" => FileCategory.Document, + ".fon" => FileCategory.Document, + ".h" => FileCategory.Document, + ".htm" => FileCategory.Document, + ".html" => FileCategory.Document, + ".icns" => FileCategory.Document, + ".ico" => FileCategory.Document, + ".ini" => FileCategory.Document, + ".lnk" => FileCategory.Document, + ".java" => FileCategory.Document, + ".jar" => FileCategory.Document, + ".js" => FileCategory.Document, + ".json" => FileCategory.Document, + ".jsp" => FileCategory.Document, + ".log" => FileCategory.Document, + ".mdb" => FileCategory.Document, + ".msi" => FileCategory.Document, + ".odt" => FileCategory.Document, + ".otf" => FileCategory.Document, + ".part" => FileCategory.Document, + ".pdf" => FileCategory.Document, + ".php" => FileCategory.Document, + ".pl" => FileCategory.Document, + ".ppt" => FileCategory.Document, + ".pptx" => FileCategory.Document, + ".py" => FileCategory.Document, + ".rss" => FileCategory.Document, + ".sav" => FileCategory.Document, + ".sh" => FileCategory.Document, + ".sql" => FileCategory.Document, + ".swift" => FileCategory.Document, + ".sys" => FileCategory.Document, + ".tar" => FileCategory.Document, + ".tmp" => FileCategory.Document, + ".ttf" => FileCategory.Document, + ".txt" => FileCategory.Document, + ".vb" => FileCategory.Document, + ".xhtml" => FileCategory.Document, + ".xls" => FileCategory.Document, + ".xlsx" => FileCategory.Document, + ".xml" => FileCategory.Document, + ".zip" => FileCategory.Document, + + // Photos + ".tif" => FileCategory.Photo, + ".tiff" => FileCategory.Photo, + ".bmp" => FileCategory.Photo, + ".jpg" => FileCategory.Photo, + ".jpeg" => FileCategory.Photo, + ".png" => FileCategory.Photo, + ".eps" => FileCategory.Photo, + ".raw" => FileCategory.Photo, + ".arw" => FileCategory.Photo, + ".svg" => FileCategory.Photo, + + // Music + ".m4a" => FileCategory.Music, + ".flac" => FileCategory.Music, + ".mp3" => FileCategory.Music, + ".wav" => FileCategory.Music, + ".wma" => FileCategory.Music, + ".aac" => FileCategory.Music, + + // Video + ".mp4" => FileCategory.Video, + ".mov" => FileCategory.Video, + ".wmv" => FileCategory.Video, + ".avi" => FileCategory.Video, + ".avchd" => FileCategory.Video, + ".flv" => FileCategory.Video, + ".f4v" => FileCategory.Video, + ".webm" => FileCategory.Video, + + _ => FileCategory.Other, + }; + } + + public static FileCategory FromString(string type) + { + return type switch + { + "Other" => FileCategory.Other, + "Document" => FileCategory.Document, + "Photo" => FileCategory.Photo, + "Music" => FileCategory.Music, + "Video" => FileCategory.Video, + + _ => FileCategory.Other, + }; + } + + public static string ToString(FileCategory type) + { + return type switch + { + FileCategory.Other => "Other", + FileCategory.Document => "Document", + FileCategory.Photo => "Photo", + FileCategory.Music => "Music", + FileCategory.Video => "Video", + + _ => "Other", + }; + } + } +} \ No newline at end of file diff --git a/Parallel.Core/Database/DatabaseConnection.cs b/Parallel.Core/Database/DatabaseConnection.cs new file mode 100644 index 0000000..692e249 --- /dev/null +++ b/Parallel.Core/Database/DatabaseConnection.cs @@ -0,0 +1,33 @@ +// Copyright 2025 Kyle Ebbinga + +using Parallel.Core.Settings; + +namespace Parallel.Core.Database +{ + /// + /// The supported file service types. + /// + public enum DatabaseProvider + { + Local + } + + /// + /// Represents a way to connect to different . + /// + public class DatabaseConnection + { + public static IDatabase CreateNew(ProfileConfig profile) + { + switch(profile.Database.Provider) + { + default: return null; + + case DatabaseProvider.Local: + IDatabase db = new SqliteDatabase(profile.Database, profile.Id); + if (!File.Exists(profile.Database.Address)) db.Initialize(); + return db; + } + } + } +} \ No newline at end of file diff --git a/Parallel.Core/Database/IDatabase.cs b/Parallel.Core/Database/IDatabase.cs new file mode 100644 index 0000000..96572c6 --- /dev/null +++ b/Parallel.Core/Database/IDatabase.cs @@ -0,0 +1,157 @@ +// Copyright 2025 Kyle Ebbinga + +using Parallel.Core.IO; +using System; +using System.Data; +using Parallel.Core.IO.FileSystem; +using Parallel.Core.Models; + +namespace Parallel.Core.Database +{ + /// + /// The type of history to log. + /// + public enum HistoryType + { + /// + /// A file that has been deleted locally but is still backed up. + /// + Archived, + + /// + /// A file that has been deleted locally. + /// + Cleaned, + + /// + /// A file that has been copied to another location. + /// + Cloned, + + /// + /// A file that has been deleted from the backup. + /// + Pruned, + + /// + /// A file that was deleted and has been restored. + /// + Restored, + + /// + /// A newly synced file. + /// + Synced + } + + /// + /// An interface for interacting with client data storage. + /// + public interface IDatabase + { + string ProfileId { get; } + + #region Base + + /// + /// Called when initializing a new instance. + /// Used for creating new table schemas. + /// + void Initialize(); + + /// + /// Runs a query in the assigned database language. + /// + /// The query to run. + /// The id of the last inserted row. -1 if none exist. + int RunQuery(string query); + + /// + /// Gets data from a query in the assigned database language and converts it to a . + /// + /// The query to send to the database. + /// A filled with returned data. + DataTable GetQuery(string query); + + /// + /// Pings the database server. + /// + /// The time, in milliseconds, of the database latency. -1 if disconnected. + long Ping(); + + #endregion + + #region Files + + /// + /// Adds a new file or updates an existing one. + /// + /// + /// The id of the last inserted row. -1 if none exist. + bool AddFile(SystemFile file); + + /// + /// Removes a file. + /// + /// + void RemoveFile(SystemFile file); + + /// + /// Gets a of all files in order of most recent. + /// + DataTable GetFiles(); + + /// + /// Gets a of files of either local files or deleted files in order of most recent. + /// + /// true if the table should only be deleted files, otherwise false + DataTable GetFiles(bool deleted); + + /// + /// Gets a of files of either local files or deleted files by a query in order of most recent. + /// + /// The query to search local file paths for. + /// true if the table should only be deleted files, otherwise false + DataTable GetFiles(string query, bool deleted); + + /// + /// Gets a of files by their last update milliseconds in order of oldest first. + /// + /// The lowest millisecond value to look for. + /// True if the table should only be deleted files, otherwise false + /// The total amount of entries to return. + DataTable GetFiles(long milliseconds, bool deleted, int limit); + + /// + /// Gets a of either local files or deleted files from a query and last update milliseconds in order of oldest first. + /// + /// The path to search local deleted files for. + /// The lowest millisecond value to look for. + /// true if the table should only be deleted files, otherwise false + /// The total amount of entries to return. + DataTable GetFiles(string query, long milliseconds, bool deleted, int limit); + + /// + /// Gets a specific file in the database. + /// + /// The file path. Either local or on in the backup. + /// + SystemFile GetFile(string query); + + long GetLocalSize(); + long GetRemoteSize(); + int GetTotalFiles(bool deleted); + + #endregion + + #region History + + int AddHistory(string path, HistoryType type); + + DataTable GetHistory(string path, int limit); + + DataTable GetHistory(string path, HistoryType type, int limit); + + #endregion + } +} \ No newline at end of file diff --git a/Parallel.Core/Database/SqliteDatabase.cs b/Parallel.Core/Database/SqliteDatabase.cs new file mode 100644 index 0000000..8d2cbcf --- /dev/null +++ b/Parallel.Core/Database/SqliteDatabase.cs @@ -0,0 +1,212 @@ +// Copyright 2025 Kyle Ebbinga + +using Microsoft.Data.Sqlite; +using System.Data; +using System.Diagnostics; +using Parallel.Core.Models; +using Parallel.Core.Settings; +using Parallel.Core.Utils; + +namespace Parallel.Core.Database +{ + /// + public class SqliteDatabase : IDatabase + { + public string FilePath { get; } + public string ProfileId { get; } + + /// + /// Initializes a new instance of the class. + /// + /// + /// + public SqliteDatabase(DatabaseCredentials credentials, string profileId) + { + FilePath = credentials.Address; + ProfileId = profileId; + } + + #region Base + + /// + public void Initialize() + { + Log.Information("Creating local database..."); + File.Create(FilePath).Close(); + File.SetAttributes(FilePath, File.GetAttributes(FilePath) | FileAttributes.Hidden); + + // Create tables + RunQuery( + "CREATE TABLE IF NOT EXISTS `files` (`profile` TEXT NOT NULL, `id` TEXT NOT NULL, `name` TEXT NOT NULL, `localpath` TEXT NOT NULL, `remotepath` TEXT NOT NULL, `lastwrite` LONG INTEGER NOT NULL, `lastupdate` LONG INTEGER NOT NULL, `LocalSize` LONG INTEGER NOT NULL, `RemoteSize` LONG INTEGER NOT NULL, `type` TEXT NOT NULL DEFAULT Other CHECK(`type` IN ('Document', 'Photo', 'Music', 'Video', 'Other')), `hidden` INTEGER NOT NULL DEFAULT 0, `readonly` INTEGER NOT NULL DEFAULT 0, `deleted` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`profile`, `id`));"); + + RunQuery("CREATE TABLE IF NOT EXISTS `history` (`profile` TEXT NOT NULL, `timestamp` LONG INTEGER NOT NULL, `id` TEXT NOT NULL, `path` TEXT NOT NULL, `type` TEXT NOT NULL, PRIMARY KEY(`profile`, `timestamp`));"); + } + + /// + public DataTable GetQuery(string query) + { + DataTable dt = new(); + Log.Debug($"Getting SQL query: {query}"); + using (SqliteConnection sqlite = new("Data Source=" + FilePath)) + using (SqliteCommand cmd = new(query, sqlite)) + { + sqlite.Open(); + using (SqliteDataReader reader = cmd.ExecuteReader()) + { + dt.Load(reader); + } + + sqlite.Close(); + sqlite.Dispose(); + } + + return dt; + } + + /// + public int RunQuery(string query) + { + Log.Debug($"Running SQL query: {query}"); + using (SqliteConnection sqlite = new("Data Source=" + FilePath)) + { + sqlite.Open(); + using (SqliteCommand cmd = sqlite.CreateCommand()) + { + cmd.CommandText = query; + cmd.CommandType = CommandType.Text; + return cmd.ExecuteNonQuery(); + } + } + } + + /// + public long Ping() + { + try + { + Stopwatch sw = Stopwatch.StartNew(); + using (SqliteConnection sqlite = new("Data Source=" + FilePath)) + { + sqlite.Open(); + using (SqliteCommand cmd = sqlite.CreateCommand()) + { + cmd.CommandText = "SELECT 1;"; + cmd.CommandType = CommandType.Text; + cmd.ExecuteNonQuery(); + } + + sqlite.Close(); + } + + return sw.ElapsedMilliseconds; + } + catch (Exception ex) + { + Log.Error(ex.GetBaseException().ToString()); + return -1; + } + } + + #endregion + + #region Files + + /// + public bool AddFile(SystemFile file) + { + return RunQuery($"INSERT OR REPLACE INTO files (profile, id, name, localpath, remotepath, lastwrite, lastupdate, LocalSize, RemoteSize, type, hidden, readonly, deleted) VALUES(\"{ProfileId}\", \"{file.Id}\", \"{file.Name}\", \"{file.LocalPath}\", \"{file.RemotePath}\", {file.LastWrite.TotalMilliseconds}, {file.LastUpdate.TotalMilliseconds}, {file.LocalSize}, {file.RemoteSize}, \"{file.Type.ToString()}\", {Converter.ToInt32(file.IsHidden)}, {Converter.ToInt32(file.IsReadOnly)}, {Converter.ToInt32(file.IsDeleted)});") > 0; + } + + /// + public void RemoveFile(SystemFile file) + { + RunQuery($"DELETE FROM files WHERE profile = \"{ProfileId}\" AND localpath = \"{file.LocalPath}\" AND remotepath = \"{file.RemotePath}\""); + } + + /// + public DataTable GetFiles() + { + return GetQuery($"SELECT * FROM files WHERE profile = \"{ProfileId}\" ORDER BY lastupdate DESC"); + } + + /// + public DataTable GetFiles(bool deleted) + { + return GetQuery($"SELECT * FROM files WHERE profile = \"{ProfileId}\" AND deleted = {Converter.ToInt32(deleted)} ORDER BY lastupdate DESC"); + } + + /// + public DataTable GetFiles(string query, bool deleted) + { + return GetQuery($"SELECT * FROM files WHERE profile = \"{ProfileId}\" AND localpath LIKE \"%{query}%\" AND deleted = {Converter.ToInt32(deleted)} ORDER BY lastupdate ASC"); + } + + /// + public DataTable GetFiles(long milliseconds, bool deleted, int limit) + { + return GetQuery($"SELECT * FROM files WHERE profile = \"{ProfileId}\" AND lastupdate <= {milliseconds} AND deleted = {Converter.ToInt32(deleted)} ORDER BY lastupdate ASC LIMIT {limit}"); + } + + /// + public DataTable GetFiles(string query, long milliseconds, bool deleted, int limit) + { + return GetQuery($"SELECT * FROM files WHERE profile = \"{ProfileId}\" AND localpath LIKE \"%{query}%\" AND lastupdate <= {milliseconds} AND deleted = {Converter.ToInt32(deleted)} ORDER BY lastupdate ASC LIMIT {limit}"); + } + + /// + public SystemFile GetFile(string query) + { + DataTable dt = GetQuery($"SELECT * FROM files WHERE profile = \"{ProfileId}\" AND localpath LIKE \"%{query}%\" OR remotepath LIKE \"%{query}%\""); + if (dt.Rows.Count == 0) return null; + return new SystemFile(dt.Rows[0]); + } + + /// + public int GetTotalFiles(bool deleted) + { + DataTable dt = GetQuery($"SELECT COUNT(*) as total FROM files WHERE profile = \"{ProfileId}\" AND deleted = {Converter.ToInt32(deleted)}"); + if (dt.Rows.Count == 0) return 0; + return Convert.ToInt32(dt.Rows[0].Field("total")); + } + + /// + public long GetLocalSize() + { + DataTable dt = GetQuery($"SELECT SUM(LocalSize) as LocalSize FROM files WHERE profile = \"{ProfileId}\""); + if (dt.Rows.Count == 0) return 0; + return Convert.ToInt64(dt.Rows[0].Field("LocalSize")); + } + + /// + public long GetRemoteSize() + { + DataTable dt = GetQuery($"SELECT SUM(RemoteSize) as RemoteSize FROM files WHERE profile = \"{ProfileId}\""); + if (dt.Rows.Count == 0) return 0; + return Convert.ToInt64(dt.Rows[0].Field("RemoteSize")); + } + + #endregion + + #region History + + /// + public int AddHistory(string path, HistoryType type) + { + return RunQuery($"INSERT OR REPLACE INTO history (profile, timestamp, name, path, type) VALUES(\"{ProfileId}\", {UnixTime.Now.TotalMilliseconds}, \"{Path.GetFileName(path)}\", \"{path}\", \"{type.ToString()}\");"); + } + + /// + public DataTable GetHistory(string query, int limit) + { + return GetQuery($"SELECT * FROM history WHERE profile = \"{ProfileId}\" AND path LIKE \"%{query}%\" ORDER BY timestamp DESC LIMIT {limit}"); + } + + /// + public DataTable GetHistory(string query, HistoryType type, int limit) + { + return GetQuery($"SELECT * FROM history WHERE profile = \"{ProfileId}\" AND path LIKE \"%{query}%\" AND type = \"{type.ToString()}\" ORDER BY timestamp DESC LIMIT {limit}"); + } + + #endregion + } +} \ No newline at end of file diff --git a/Parallel.Core/Diagnostics/IProgress.cs b/Parallel.Core/Diagnostics/IProgress.cs new file mode 100644 index 0000000..1b4ff15 --- /dev/null +++ b/Parallel.Core/Diagnostics/IProgress.cs @@ -0,0 +1,32 @@ +// Copyright 2025 Kyle Ebbinga + +using Parallel.Core.Models; + +namespace Parallel.Core.Diagnostics +{ + public enum ProgressOperation + { + Archiving, + Downloading, + Uploading, + Compressing, + Decompressing, + Syncing + } + + /// + /// Defines a provider for progress updates. + /// + public interface IProgress + { + /// + /// Reports a progress update. + /// + void Report(ProgressOperation operation, SystemFile file, int current, int total); + + /// + /// Reports a failed update. + /// + void Failed(Exception exception, SystemFile file); + } +} \ No newline at end of file diff --git a/Parallel.Core/Diagnostics/ProgressDebug.cs b/Parallel.Core/Diagnostics/ProgressDebug.cs new file mode 100644 index 0000000..d6ceb2e --- /dev/null +++ b/Parallel.Core/Diagnostics/ProgressDebug.cs @@ -0,0 +1,38 @@ +// Copyright 2025 Kyle Ebbinga + +using Parallel.Core.Models; + +namespace Parallel.Core.Diagnostics +{ + /// + /// Represents a basic progress report debugger. + /// + public class ProgressDebug : IProgress + { + private ProgressOperation currentOperation; + private int progressPercentage; + + /// + public void Report(ProgressOperation operation, SystemFile file, int current, int total) + { + int num = (int)(current / (double)total * 100.0 + 0.5); + if (currentOperation != operation) + { + progressPercentage = -1; + currentOperation = operation; + } + + if (progressPercentage != num && num % 10 == 0) + { + progressPercentage = num; + Log.Information($"{operation}: {current} out of {total} ({progressPercentage}%)"); + } + } + + /// + public void Failed(Exception exception, SystemFile file) + { + Log.Error($"{exception.GetType().FullName}: {exception.Message}. Failed to upload file: '{file.LocalPath}'"); + } + } +} \ No newline at end of file diff --git a/Parallel.Core/Events/LocalFileEventArgs.cs b/Parallel.Core/Events/LocalFileEventArgs.cs new file mode 100644 index 0000000..399768c --- /dev/null +++ b/Parallel.Core/Events/LocalFileEventArgs.cs @@ -0,0 +1,19 @@ +// Copyright 2025 Kyle Ebbinga + +using Parallel.Core.IO; +using System; +using System.IO; +using Parallel.Core.Models; + +namespace Parallel.Core.Events +{ + public class LocalFileEventArgs : EventArgs + { + public SystemFile SystemFile { get; } + + public LocalFileEventArgs(string file) + { + SystemFile = new SystemFile(new FileInfo(file)); + } + } +} \ No newline at end of file diff --git a/Parallel.Core/Events/MessageRecievedEventArgs.cs b/Parallel.Core/Events/MessageRecievedEventArgs.cs new file mode 100644 index 0000000..a8ae445 --- /dev/null +++ b/Parallel.Core/Events/MessageRecievedEventArgs.cs @@ -0,0 +1,14 @@ +// Copyright 2025 Kyle Ebbinga + +using System.Net.Sockets; +using System.Text; +using Parallel.Core.Utils; + +namespace Parallel.Core.Events +{ + public class MessageRecievedEventArgs(UdpReceiveResult result) + { + public DateTime TimeStamp { get; } = DateTime.Now; + public string Message { get; } = Encryption.Decode(Encoding.UTF8.GetString(result.Buffer)); + } +} \ No newline at end of file diff --git a/Parallel.Core/Events/TransferFailedEventArgs.cs b/Parallel.Core/Events/TransferFailedEventArgs.cs new file mode 100644 index 0000000..9ce7457 --- /dev/null +++ b/Parallel.Core/Events/TransferFailedEventArgs.cs @@ -0,0 +1,20 @@ +// Copyright 2025 Kyle Ebbinga + +using System.Security.Cryptography.X509Certificates; +using Parallel.Core.IO; +using Parallel.Core.Models; + +namespace Parallel.Core.Events +{ + public class TransferFailedEventArgs : EventArgs + { + public SystemFile File { get; } + public Exception Exception { get; } + + public TransferFailedEventArgs(SystemFile file, Exception exception) + { + File = file; + Exception = exception; + } + } +} \ No newline at end of file diff --git a/Parallel.Core/Events/TransferUpdateEventArgs.cs b/Parallel.Core/Events/TransferUpdateEventArgs.cs new file mode 100644 index 0000000..fab2c94 --- /dev/null +++ b/Parallel.Core/Events/TransferUpdateEventArgs.cs @@ -0,0 +1,33 @@ +// Copyright 2025 Kyle Ebbinga + +using System.Security.Cryptography.X509Certificates; +using Parallel.Core.IO; +using Parallel.Core.Models; + +namespace Parallel.Core.Events +{ + public class TransferUpdateEventArgs : EventArgs + { + /// + /// The recently transferred file. + /// + public SystemFile File { get; } + + /// + /// The amount of files already transferred. + /// + public int Finished { get; } + + /// + /// The total amount of files to transfer. + /// + public int Total { get; } + + public TransferUpdateEventArgs(SystemFile file, int finished, int total) + { + File = file; + Finished = finished; + Total = total; + } + } +} \ No newline at end of file diff --git a/Parallel.Core/Extensions/Json/UnixTimeConverter.cs b/Parallel.Core/Extensions/Json/UnixTimeConverter.cs new file mode 100644 index 0000000..a430ec0 --- /dev/null +++ b/Parallel.Core/Extensions/Json/UnixTimeConverter.cs @@ -0,0 +1,57 @@ +// Copyright 2025 Kyle Ebbinga + +using Newtonsoft.Json; +using Parallel.Core.Utils; + +namespace Parallel.Core.Extensions.Json +{ + public class UnixTimeConverter : JsonConverter + { + public override bool CanConvert(Type objectType) + { + return objectType.IsValueType; + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + UnixTime unix; + if (reader.TokenType == JsonToken.Integer) + { + unix = UnixTime.FromMilliseconds(Convert.ToInt64(reader.Value)); + } + else if (reader.TokenType == JsonToken.String) + { + unix = UnixTime.Parse((string)reader.Value); + } + else + { + throw new JsonSerializationException($"Unexpected token parsing date. Expected Integer or String, got {reader.TokenType}"); + } + + return unix; + } + + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) + { + long seconds; + //if(value is DateTime dateTime) + //{ + // seconds = (long)dateTime.ToUniversalTime().Subtract(new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalSeconds; + //} + //else if(value is DateTimeOffset dateTimeOffset) + //{ + // seconds = (long)dateTimeOffset.ToUniversalTime().Subtract(new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalSeconds; + //} + if (value is UnixTime unixTime) + { + seconds = unixTime.TotalMilliseconds; + } + else + { + throw new JsonSerializationException("Expected date object value."); + } + + writer.WriteValue(seconds); + } + } +} \ No newline at end of file diff --git a/Parallel.Core/IO/FileSystem/DotNetFileSystem.cs b/Parallel.Core/IO/FileSystem/DotNetFileSystem.cs new file mode 100644 index 0000000..554df93 --- /dev/null +++ b/Parallel.Core/IO/FileSystem/DotNetFileSystem.cs @@ -0,0 +1,164 @@ +// Copyright 2025 Kyle Ebbinga + +using System.Diagnostics; +using System.IO.Compression; +using Microsoft.VisualBasic.FileIO; +using Newtonsoft.Json.Linq; +using Parallel.Core.Diagnostics; +using Parallel.Core.Models; +using Parallel.Core.Settings; +using Parallel.Core.Utils; +using SearchOption = System.IO.SearchOption; + +namespace Parallel.Core.IO.FileSystem +{ + /// + /// Represents the wrapper for a default dotnet file system. + /// + public class DotNetFileSystem : IFileSystem + { + private readonly FileSystemCredentials _credentials; + + /// + /// Represents an for interacting with physical machine hardware. + /// + /// The credentials to log in with. + public DotNetFileSystem(FileSystemCredentials credentials) + { + _credentials = credentials; + } + + /// + public Task CreateDirectoryAsync(string path) + { + Directory.CreateDirectory(path); + return Task.CompletedTask; + } + + /// + public Task DeleteDirectoryAsync(string path) + { + //Directory.Delete(path); + return Task.CompletedTask; + } + + /// + public Task DeleteFileAsync(string path) + { + if (File.Exists(path)) + { + File.SetAttributes(path, ~FileAttributes.ReadOnly & File.GetAttributes(path)); + Microsoft.VisualBasic.FileIO.FileSystem.DeleteFile(path, UIOption.OnlyErrorDialogs, RecycleOption.SendToRecycleBin); + } + + return Task.CompletedTask; + } + + /// + public async Task DownloadFilesAsync(SystemFile[] files, IProgress progress) + { + if (!files.Any()) return; + for (int i = 0; i < files.Length; i++) + { + SystemFile file = files[i]; + Stopwatch sw = new Stopwatch(); + + progress.Report(ProgressOperation.Downloading, file, i, files.Length); + await using FileStream createStream = File.Create(file.LocalPath); + await using FileStream openStream = File.OpenRead(file.RemotePath); + await using GZipStream gzipStream = new GZipStream(openStream, CompressionMode.Decompress); + await gzipStream.CopyToAsync(createStream); + + Log.Debug($"Downloaded '{file.LocalPath}' in {sw.ElapsedMilliseconds}ms"); + } + } + + /// + public Task GetDirectoryNameAsync(string path) + { + return Task.FromResult(Path.GetDirectoryName(path)); + } + + /// + public Task> GetFilesAsync() + { + Dictionary files = new Dictionary(); + foreach (string file in Directory.GetFiles(PathBuilder.RootDirectory(_credentials), "*.gz", SearchOption.AllDirectories)) + { + FileInfo fi = new(file); + files.Add(fi.FullName, new SystemFile(file) + { + Name = fi.Name, + RemotePath = fi.FullName, + LastWrite = new UnixTime(fi.LastWriteTime), + RemoteSize = fi.Length + }); + } + + return Task.FromResult(files); + } + + /// + public Task GetFilesAsync(string path) + { + List list = new(); + foreach (string file in Directory.GetFiles(path, "*", SearchOption.AllDirectories)) + { + FileInfo fi = new(file); + list.Add(new SystemFile(file) + { + Name = fi.Name, + RemotePath = fi.FullName, + RemoteSize = fi.Length + }); + } + + return Task.FromResult(list.ToArray()); + } + + /// + public Task GetFileAsync(string path) + { + FileInfo fi = new(path); + return Task.FromResult(new SystemFile(path) + { + Name = fi.Name, + RemotePath = fi.FullName, + RemoteSize = fi.Length + }); + } + + /// + public Task PingAsync() + { + Stopwatch sw = Stopwatch.StartNew(); + if (!Directory.Exists(PathBuilder.RootDirectory(_credentials))) return Task.FromResult(-1); + return Task.FromResult(sw.ElapsedMilliseconds); + } + + /// + public async Task UploadFilesAsync(SystemFile[] files, IProgress progress) + { + if (!files.Any()) return; + for (int i = 0; i < files.Length; i++) + { + Stopwatch sw = new Stopwatch(); + SystemFile file = files[i]; + file.RemotePath = PathBuilder.Remote(file.LocalPath, _credentials); + + progress.Report(ProgressOperation.Uploading, file, i, files.Length); + if (File.Exists(file.RemotePath)) File.SetAttributes(file.RemotePath, ~FileAttributes.ReadOnly & File.GetAttributes(file.RemotePath)); + string parent = Path.GetDirectoryName(file.RemotePath); + if (!Directory.Exists(parent)) Directory.CreateDirectory(parent); + + await using FileStream createStream = File.Create(file.RemotePath); + await using FileStream openStream = File.OpenRead(file.LocalPath); + await using GZipStream gzipStream = new GZipStream(createStream, CompressionLevel.SmallestSize); + await openStream.CopyToAsync(gzipStream); + + Log.Debug($"Uploaded '{file.RemotePath}' in {sw.ElapsedMilliseconds}ms"); + File.SetAttributes(file.RemotePath, File.GetAttributes(file.RemotePath) | FileAttributes.ReadOnly); + } + } + } +} \ No newline at end of file diff --git a/Parallel.Core/IO/FileSystem/FileSystemManager.cs b/Parallel.Core/IO/FileSystem/FileSystemManager.cs new file mode 100644 index 0000000..97fa7b5 --- /dev/null +++ b/Parallel.Core/IO/FileSystem/FileSystemManager.cs @@ -0,0 +1,48 @@ +// Copyright 2025 Kyle Ebbinga + +using Parallel.Core.Settings; + +namespace Parallel.Core.IO.FileSystem +{ + /// + /// The supported file service types. + /// + public enum FileService + { + /// + /// A local file system either through external or networked drives. + /// + Local, + + /// + /// A remote file storage server through secure shell. + /// + Remote, + + /// + /// A cloud storage server hosted through Amazon simple storage service. + /// + Cloud, + }; + + /// + /// Represents the way to connect to different file system associations. This class cannot be inherited. + /// + public static class FileSystemManager + { + /// + /// Creates a new file system association. + /// + /// The credentials needed for the associated file system. + public static IFileSystem CreateNew(FileSystemCredentials credentials) + { + return credentials?.Service switch + { + FileService.Local => new DotNetFileSystem(credentials), + FileService.Remote => new SftpFileSystem(credentials), + //FileService.Cloud => new AmazonS3FileSystem(credentials), + _ => null + }; + } + } +} \ No newline at end of file diff --git a/Parallel.Core/IO/FileSystem/IFileSystem.cs b/Parallel.Core/IO/FileSystem/IFileSystem.cs new file mode 100644 index 0000000..360466b --- /dev/null +++ b/Parallel.Core/IO/FileSystem/IFileSystem.cs @@ -0,0 +1,83 @@ +// Copyright 2025 Kyle Ebbinga + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Parallel.Core.Diagnostics; +using Parallel.Core.Models; + +namespace Parallel.Core.IO.FileSystem +{ + /// + /// Defines the way for communicating with a file system. + /// + public interface IFileSystem + { + /// + /// Creates all directories and subdirectories in the specified path unless they already exist. + /// + /// + Task CreateDirectoryAsync(string path); + + /// + /// Deletes the specified directory. + /// + /// + Task DeleteDirectoryAsync(string path); + + /// + /// Deletes the specified file. + /// + /// + Task DeleteFileAsync(string path); + + /// + /// Downloads a file from the associated file system. + /// + /// + /// + Task DownloadFilesAsync(SystemFile[] files, IProgress progress); + + /// + /// Returns the parent directory name. + /// + /// + /// + Task GetDirectoryNameAsync(string path); + + /// + /// Gets all the files in the backup. + /// + /// A dictionary of s with the key being the backup path adn the value being the associated . + Task> GetFilesAsync(); + + /// + /// Gets all the files in the current directory. + /// + /// + /// A read-only collection of s. + Task GetFilesAsync(string path); + + /// + /// Gets a file on the associated file system. + /// + /// + /// + Task GetFileAsync(string path); + + /// + /// Pings the remote file system. + /// + /// The time, in milliseconds, of the database latency. -1 if disconnected. + Task PingAsync(); + + /// + /// Uploads a file to the associated file system. + /// + /// + /// + Task UploadFilesAsync(SystemFile[] files, IProgress progress); + } +} \ No newline at end of file diff --git a/Parallel.Core/IO/FileSystem/SftpFileSystem.cs b/Parallel.Core/IO/FileSystem/SftpFileSystem.cs new file mode 100644 index 0000000..86eeab2 --- /dev/null +++ b/Parallel.Core/IO/FileSystem/SftpFileSystem.cs @@ -0,0 +1,202 @@ +// Copyright 2025 Kyle Ebbinga + +using System.Diagnostics; +using Parallel.Core.Settings; +using Renci.SshNet; +using Renci.SshNet.Sftp; +using System.IO.Compression; +using Newtonsoft.Json.Linq; +using Parallel.Core.Diagnostics; +using Parallel.Core.Models; +using Parallel.Core.Utils; + +namespace Parallel.Core.IO.FileSystem +{ + /// + /// Represents the wrapper for an SFTP file system through SSH. + /// + public class SftpFileSystem : IFileSystem + { + private readonly ConnectionInfo _connectionInfo; + + /// + /// Represents an for interacting with an SSH server. + /// + /// The credentials to log in with. + public SftpFileSystem(FileSystemCredentials credentials) + { + Console.WriteLine(JObject.FromObject(credentials)); + _connectionInfo = new ConnectionInfo(credentials.Address, credentials.Username, new PasswordAuthenticationMethod(credentials.Username, Encryption.Decode(credentials.Password))); + } + + /// + public async Task CreateDirectoryAsync(string path) + { + using (SftpClient sftp = new SftpClient(_connectionInfo)) + { + sftp.Connect(); + if (sftp.IsConnected) + { + string parentDir = string.Empty; + foreach (string subPath in path.Split('/')) + { + parentDir += $"/{subPath}"; + if (!await sftp.ExistsAsync(parentDir)) + { + await sftp.CreateDirectoryAsync(parentDir); + } + } + } + + sftp.Disconnect(); + } + } + + /// + public async Task DeleteDirectoryAsync(string path) + { + using (SftpClient sftp = new SftpClient(_connectionInfo)) + { + sftp.Connect(); + if (sftp.IsConnected && await sftp.ExistsAsync(path)) + { + await sftp.DeleteDirectoryAsync(path); + } + + sftp.Disconnect(); + } + } + + /// + public async Task DeleteFileAsync(string path) + { + using (SftpClient sftp = new SftpClient(_connectionInfo)) + { + sftp.Connect(); + if (sftp.IsConnected && await sftp.ExistsAsync(path)) + { + await sftp.DeleteAsync(path); + } + + sftp.Disconnect(); + } + } + + public Task DownloadFilesAsync(SystemFile[] files, IProgress progress) + { + throw new NotImplementedException(); + } + + public Task GetDirectoryNameAsync(string path) + { + throw new NotImplementedException(); + } + + public Task> GetFilesAsync() + { + throw new NotImplementedException(); + } + + /// + public async Task GetFilesAsync(string path) + { + List list = new(); + using (SftpClient sftp = new SftpClient(_connectionInfo)) + { + sftp.Connect(); + if (sftp.IsConnected && await sftp.ExistsAsync(path)) + { + foreach (ISftpFile file in sftp.ListDirectory(path)) + { + list.Add(new SystemFile(file.FullName) + { + Name = file.Name, + RemotePath = file.FullName, + RemoteSize = file.Length + }); + } + } + + sftp.Disconnect(); + } + + return list.ToArray(); + } + + /// + public async Task GetFileAsync(string path) + { + SystemFile file = new SystemFile(path); + using (SftpClient sftp = new SftpClient(_connectionInfo)) + { + sftp.Connect(); + if (sftp.IsConnected && await sftp.ExistsAsync(path)) + { + ISftpFile sf = sftp.Get(path); + file = new SystemFile(sf.FullName) + { + Name = sf.Name, + RemotePath = sf.FullName, + RemoteSize = sf.Length, + }; + } + + sftp.Disconnect(); + } + + return file; + } + + /// + public async Task PingAsync() + { + CancellationTokenSource cts = new(); + Stopwatch sw = Stopwatch.StartNew(); + using (SftpClient sftp = new SftpClient(_connectionInfo)) + { + await sftp.ConnectAsync(cts.Token); + if (!sftp.IsConnected) return -1; + sftp.Disconnect(); + } + + return sw.ElapsedMilliseconds; + } + + /// + public async Task UploadFilesAsync(SystemFile[] files, IProgress progress) + { + using SftpClient sftp = new SftpClient(_connectionInfo); + sftp.Connect(); + if (sftp.IsConnected) + { + for (int i = 0; i < files.Length; i++) + { + SystemFile file = files[i]; + Stopwatch sw = new Stopwatch(); + progress.Report(ProgressOperation.Uploading, file, i, files.Length); + if (await sftp.ExistsAsync(file.RemotePath)) sftp.ChangePermissions(file.RemotePath, 644); + + string parentDir = string.Empty; + foreach (string subPath in file.RemotePath.Split('/')) + { + parentDir += $"/{subPath}"; + if (!await sftp.ExistsAsync(parentDir)) + { + await sftp.CreateDirectoryAsync(parentDir); + } + } + + await using SftpFileStream createStream = sftp.Create(file.RemotePath); + await using FileStream openStream = File.OpenRead(file.LocalPath); + await using GZipStream gzipStream = new GZipStream(createStream, CompressionLevel.SmallestSize); + await openStream.CopyToAsync(gzipStream); + sftp.ChangePermissions(file.RemotePath, 444); + + Log.Debug($"Uploaded '{file.RemotePath}' in {sw.ElapsedMilliseconds}ms"); + } + } + + sftp.Disconnect(); + } + } +} \ No newline at end of file diff --git a/Parallel.Core/IO/PathBuilder.cs b/Parallel.Core/IO/PathBuilder.cs new file mode 100644 index 0000000..93f07f4 --- /dev/null +++ b/Parallel.Core/IO/PathBuilder.cs @@ -0,0 +1,100 @@ +// Copyright 2025 Kyle Ebbinga + +using System.Runtime.InteropServices; +using Parallel.Core.IO.FileSystem; +using Parallel.Core.Models; +using Parallel.Core.Settings; +using Parallel.Core.Utils; + +namespace Parallel.Core.IO +{ + /// + /// Represents the way to build paths on different operating systems. + /// + public class PathBuilder + { + public static string TempDirectory() + { + string tempFolder = Path.Combine(Path.GetTempPath(), $"parallel_{UnixTime.Now.TotalMilliseconds}"); + if (!Directory.Exists(tempFolder)) Directory.CreateDirectory(tempFolder); + return tempFolder; + } + + /// + /// Gets the corresponding directory for program data based on the . + /// + public static string ProgramData + { + get + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Entex Interactive", "Parallel"); + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + return "/etc/Parallel"; + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support", "Entex Interactive", "Parallel"); + } + + throw new PlatformNotSupportedException("Unsupported OS detected."); + } + } + + /// + /// Builds the path for the local file system. + /// + /// + /// + /// + public static string Local(string path, FileSystemCredentials credentials) + { + string root = Path.Combine(credentials.RootDirectory, "Parallel", Environment.MachineName); + string main = path.Replace("/", "\\").Replace(root, string.Empty).Replace(".gz", string.Empty); + + Console.WriteLine(root); + Console.WriteLine(main); + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return main.Substring(1, main.Length - 1).Insert(1, ":"); + } + + return main.Replace(@"\", "/"); + } + + public static string RootDirectory(FileSystemCredentials credentials) + { + string root = Path.Combine(credentials.RootDirectory, "Parallel", Environment.MachineName); + Log.Debug($"Root directory: {root}"); + return credentials.Service switch + { + FileService.Local => root, + FileService.Remote => root.Replace('\\', '/'), + _ => null + }; + } + + /// + /// Builds the path on the remote . + /// + /// + /// + /// + public static string Remote(string path, FileSystemCredentials credentials) + { + string root = Path.Combine(credentials.RootDirectory, "Parallel", Environment.MachineName, path.Replace(":", string.Empty)) + ".gz"; + return credentials.Service switch + { + FileService.Local => root, + FileService.Remote => root.Replace('\\', '/'), + _ => null + }; + } + } +} \ No newline at end of file diff --git a/Parallel.Core/IO/Recovery/RecoveryManager.cs b/Parallel.Core/IO/Recovery/RecoveryManager.cs new file mode 100644 index 0000000..89777a3 --- /dev/null +++ b/Parallel.Core/IO/Recovery/RecoveryManager.cs @@ -0,0 +1,85 @@ +// Copyright 2025 Kyle Ebbinga + +using System.Data; +using Parallel.Core.Database; +using Parallel.Core.IO.FileSystem; +using Parallel.Core.Models; +using Parallel.Core.Settings; + +namespace Parallel.Core.IO.Recovery +{ + /// + /// Represents the way to manage recovery points on the system. + /// + public class RecoveryManager + { + private readonly string _dbPath = Path.Combine(PathBuilder.ProgramData, Environment.MachineName + ".db"); + + public IDatabase Database { get; set; } + public IFileSystem FileSystem { get; set; } + public ProfileConfig Profile { get; set; } + public string MachineName { get; } = Environment.MachineName; + public string RootFolder { get; set; } + + /// + /// Initializes a new instance of the class. + /// + /// + public RecoveryManager(ProfileConfig profile) + { + Profile = profile; + Database = DatabaseConnection.CreateNew(profile); + FileSystem = FileSystemManager.CreateNew(profile.FileSystem); + } + + public bool Initialize() + { + try + { + Database = DatabaseConnection.CreateNew(Profile); + bool fsInit = (FileSystem != null) && FileSystem.PingAsync().Result >= 0; + Profile.IgnoreDirectories.Add(Profile.FileSystem.RootDirectory); + if (Profile != null) Profile.SaveToFile(); + return fsInit; + } + catch (Exception ex) + { + Log.Error(ex.GetBaseException().ToString()); + return false; + } + } + + /// + /// Loads a to restore the + /// + /// + public void Load(RecoveryPoint recoveryPoint) + { + + } + + /// + /// Saves the current file system state as a . + /// + /// + public RecoveryPoint Save() + { + RecoveryPoint rp = new(Profile.BackupDirectories, Profile.IgnoreDirectories); + DataTable dt = Database.GetFiles(); + foreach (DataRow row in dt.Rows) + { + SystemFile lf = new SystemFile(row); + if (lf.IsDeleted) + { + rp.DeletedFiles.Add(lf); + } + else + { + rp.LocalFiles.Add(lf); + } + } + + return rp; + } + } +} \ No newline at end of file diff --git a/Parallel.Core/IO/Recovery/RecoveryPoint.cs b/Parallel.Core/IO/Recovery/RecoveryPoint.cs new file mode 100644 index 0000000..ea57c4d --- /dev/null +++ b/Parallel.Core/IO/Recovery/RecoveryPoint.cs @@ -0,0 +1,55 @@ +// Copyright 2025 Kyle Ebbinga + +using Parallel.Core.Models; + +namespace Parallel.Core.IO.Recovery +{ + /// + /// Represents a collection of files at an instance of time on the local machine. + /// + public class RecoveryPoint + { + /// + /// The unique identifier. + /// + public string Id { get; } + + /// + /// The time of creation. + /// + public DateTime CreatedAt { get; } + + /// + /// An array of folders to back up. + /// + public string[] BackupFolders { get; } + + /// + /// An array of folders to ignore. + /// + public string[] IgnoreFolders { get; } + + /// + /// A collection of files that exist in the local machine. + /// + public List LocalFiles { get; } + + /// + /// A collection of deleted files that don't exist on the local machine. + /// + public List DeletedFiles { get; } + + /// + /// Initializes a new instance of the class. + /// + public RecoveryPoint(IEnumerable backupFolders, IEnumerable ignoreFolders) + { + Id = Guid.NewGuid().ToString(); + CreatedAt = DateTime.Now; + BackupFolders = backupFolders.ToArray(); + IgnoreFolders = ignoreFolders.ToArray(); + LocalFiles = new List(); + DeletedFiles = new List(); + } + } +} \ No newline at end of file diff --git a/Parallel.Core/IO/Rsync/RsyncAlgorithm.cs b/Parallel.Core/IO/Rsync/RsyncAlgorithm.cs new file mode 100644 index 0000000..42fbbd9 --- /dev/null +++ b/Parallel.Core/IO/Rsync/RsyncAlgorithm.cs @@ -0,0 +1,61 @@ +// Copyright 2025 Kyle Ebbinga + +using FastRsync.Core; +using FastRsync.Delta; +using FastRsync.Diagnostics; +using FastRsync.Signature; + +namespace Parallel.Core.IO.Rsync +{ + /// + /// Represents the functions for using the rsync algorithm. + /// + public abstract class RsyncAlgorithm + { + private static readonly IProgress Logging = new RsyncProgress(); + private static int DeltaSize = 206; + + /// + /// Creates a new signature file. A signature file contains checksums for file changes. + /// + /// The path to the original version of the file. + /// The path to the signature of the original file. + public static async Task CreateSignatureAsync(string originalFilePath, string signatureFilePath) + { + SignatureBuilder signatureBuilder = new SignatureBuilder(); + await using FileStream originalStream = new FileStream(originalFilePath, FileMode.OpenOrCreate, FileAccess.Read, FileShare.Read); + await using FileStream signatureStream = new FileStream(signatureFilePath, FileMode.Create, FileAccess.Write, FileShare.Read); + await signatureBuilder.BuildAsync(originalStream, new SignatureWriter(signatureStream)); + } + + /// + /// Creates a new delta file. A delta file contains the data that changed in the file. + /// + /// The path to the new version of the file. + /// The path to the signature of the original file. + /// The path to the delta of the changed file data. + public static async Task CreateDeltaAsync(string newFilePath, string signatureFilePath, string deltaFilePath) + { + DeltaBuilder deltaBuilder = new DeltaBuilder(); + await using FileStream newFileStream = new FileStream(newFilePath, FileMode.Open, FileAccess.Read, FileShare.Read); + await using FileStream signatureStream = new FileStream(signatureFilePath, FileMode.Open, FileAccess.Read, FileShare.Read); + await using FileStream deltaStream = new FileStream(deltaFilePath, FileMode.Create, FileAccess.Write, FileShare.Read); + await deltaBuilder.BuildDeltaAsync(newFileStream, new SignatureReader(signatureStream, Logging), new AggregateCopyOperationsDecorator(new BinaryDeltaWriter(deltaStream))); + } + + /// + /// Applies a delta file to + /// + /// The path to the new version of the file. + /// The path to the original version of the file. + /// The path to the delta of the changed file data. + public static async Task ApplyDeltaAsync(string newFilePath, string originalFilePath, string deltaFilePath) + { + DeltaApplier deltaApplier = new DeltaApplier(); + await using FileStream originalStream = new FileStream(newFilePath, FileMode.Open, FileAccess.Read, FileShare.Read); + await using FileStream deltaStream = new FileStream(deltaFilePath, FileMode.Open, FileAccess.Read, FileShare.Read); + await using FileStream newFileStream = new FileStream(originalFilePath, FileMode.Create, FileAccess.ReadWrite, FileShare.Read); + await deltaApplier.ApplyAsync(originalStream, new BinaryDeltaReader(deltaStream, Logging), newFileStream); + } + } +} \ No newline at end of file diff --git a/Parallel.Core/IO/Rsync/RsyncProgress.cs b/Parallel.Core/IO/Rsync/RsyncProgress.cs new file mode 100644 index 0000000..29640cb --- /dev/null +++ b/Parallel.Core/IO/Rsync/RsyncProgress.cs @@ -0,0 +1,31 @@ +// Copyright 2025 Kyle Ebbinga + +using FastRsync.Diagnostics; + +namespace Parallel.Core.IO.Rsync +{ + /// + public class RsyncProgress : IProgress + { + private ProgressOperationType currentOperation; + private int progressPercentage; + + + /// + public void Report(ProgressReport progress) + { + int num = (int)(progress.CurrentPosition / (double)progress.Total * 100.0 + 0.5); + if (currentOperation != progress.Operation) + { + progressPercentage = -1; + currentOperation = progress.Operation; + } + + if (progressPercentage != num && num % 10 == 0) + { + progressPercentage = num; + Log.Information($"{progress.Operation}: {progress.CurrentPosition} out of {progress.Total} ({progressPercentage}%)"); + } + } + } +} \ No newline at end of file diff --git a/Parallel.Core/IO/ScanFileSystemResult.cs b/Parallel.Core/IO/ScanFileSystemResult.cs new file mode 100644 index 0000000..18623df --- /dev/null +++ b/Parallel.Core/IO/ScanFileSystemResult.cs @@ -0,0 +1,25 @@ +// Copyright 2025 Kyle Ebbinga + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Parallel.Core.Models; + +namespace Parallel.Core.IO +{ + public struct ScanFileSystemResult + { + public SystemFile[] BackupFiles { get; } + public SystemFile[] DeletedFiles { get; } + public SystemFile[] IgnoredFiles { get; } + + public ScanFileSystemResult(IEnumerable localFiles, IEnumerable deletedFiles, IEnumerable ignoredFiles) + { + BackupFiles = localFiles.ToArray(); + DeletedFiles = deletedFiles.ToArray(); + IgnoredFiles = ignoredFiles.ToArray(); + } + } +} \ No newline at end of file diff --git a/Parallel.Core/IO/Scanning/FileScanner.cs b/Parallel.Core/IO/Scanning/FileScanner.cs new file mode 100644 index 0000000..b6ccdd2 --- /dev/null +++ b/Parallel.Core/IO/Scanning/FileScanner.cs @@ -0,0 +1,374 @@ +// Copyright 2025 Kyle Ebbinga + +using System.Data; +using System.Diagnostics; +using Parallel.Core.Database; +using Parallel.Core.IO.Backup; +using Parallel.Core.Models; +using Parallel.Core.Settings; +using Parallel.Core.Utils; +using FileInfo = System.IO.FileInfo; + +namespace Parallel.Core.IO.Scanning +{ + /// + /// Represents file system scanning. + /// + public class FileScanner + { + private readonly ProfileConfig _profile; + private readonly IDatabase _db; + + public FileScanner(ProfileConfig profile, IDatabase database) + { + _profile = profile; + _db = database; + } + + public FileScanner(IBackupManager backup) + { + _profile = backup.Profile; + _db = backup.Database; + } + + /// + /// Scans all marked backup locations for file changes. + /// + /// + public SystemFile[] GetFileChanges() + { + List scannedFiles = new(); + foreach (string path in _profile.BackupDirectories.ToArray()) + { + SystemFile[] files = GetFileChanges(path, _profile.IgnoreDirectories.ToArray()); + scannedFiles.AddRange(files); + } + + Log.Information($"Backing up {scannedFiles.Where(x => x.IsDeleted.Equals(false)).Count()} files..."); + return scannedFiles.ToArray(); + } + + /// + /// Asynchronously scans for file changes in a directory. + /// + /// + /// + /// + public Task GetFileChangesAsync(string path, string[] ignoreFolders) + { + return Task.Run(() => GetFileChanges(path, ignoreFolders)); + } + + /// + /// Scans for file changes in a directory. + /// + /// + /// + /// A list of files that have changed since the last backup. + public SystemFile[] GetFileChanges(string path, string[] ignoreFolders) + { + if (!Directory.Exists(path)) return Array.Empty(); + + List scannedFiles = new(); + List systemFiles = FileScanner.GetFiles(path, ".", ignoreFolders).ToList(); + DataTable dataTable = _db.GetFiles(path, false); + Stopwatch sw = Stopwatch.StartNew(); + + foreach (DataRow row in dataTable.Rows) + { + SystemFile dlf = new(row); + + // Checks if the local file has a valid path and is part of a backup folder. + if (dlf.LocalPath != null && dlf.LocalPath.Contains(path)) + { + // Checks if a LocalFile exists on the current file system. + if (File.Exists(dlf.LocalPath) && dlf.RemotePath != null) + { + SystemFile lfi = new(new FileInfo(dlf.LocalPath)); + if (IsIgnored(lfi.LocalPath, ignoreFolders)) + { + Log.Debug($"Is ignored -> {lfi.LocalPath}"); + + lfi.IsDeleted = true; + scannedFiles.Add(lfi); + } + + if (dlf.LastWrite.TotalMilliseconds < lfi.LastWrite.TotalMilliseconds) + { + Log.Debug($"Changed -> {lfi.LocalPath}"); + + // Changed file + dlf.IsDeleted = false; + scannedFiles.Add(lfi); + } + + systemFiles.Remove(lfi.LocalPath); + } + else + { + // Adds deleted files + Log.Debug($"Deleted -> {dlf.LocalPath}"); + + dlf.IsDeleted = true; + scannedFiles.Add(dlf); + } + } + else + { + // Deletes ignored files + Log.Debug($"No contains Ignored -> {dlf.LocalPath}"); + + dlf.IsDeleted = true; + scannedFiles.Add(dlf); + } + } + + Log.Debug($"{systemFiles.Count} files are untracked! Adding..."); + if (systemFiles.Count > 0) + { + foreach (string file in systemFiles.ToArray()) + { + if (File.Exists(file) && !IsIgnored(file, ignoreFolders)) + { + Log.Debug($"Created -> {file}"); + scannedFiles.Add(new SystemFile(new FileInfo(file))); + systemFiles.Remove(file); + } + } + } + + Log.Debug($"{systemFiles.Count} files remaining."); + Log.Information($"Found {dataTable.Rows.Count.ToString("N0")} files in '{path}'. ({sw.ElapsedMilliseconds}ms)"); + return scannedFiles.ToArray(); + } + + /// + /// Gets the total size, in bytes, of a directory. + /// + /// The path of the directory. + public static long GetDirectorySize(string path) + { + long size = 0; + DirectoryInfo di = new(path); + EnumerationOptions options = new() + { + IgnoreInaccessible = true, + RecurseSubdirectories = true + }; + + foreach (FileInfo fi in di.EnumerateFiles("*", options)) + { + size += fi.Length; + } + + return size; + } + + /// + /// Gets an array of empty directories. + /// + /// The root directory to search. + /// If it should search recursively. + /// An array of empty directories. + public static DirectoryInfo[] GetEmptyDirectories(string path, bool recursive = true) + { + List list = new(); + DirectoryInfo directory = new(path); + EnumerationOptions options = new() + { + IgnoreInaccessible = true, + RecurseSubdirectories = recursive + }; + + foreach (DirectoryInfo di in directory.EnumerateDirectories("*", options)) + { + Log.Debug($"Checking -> {di.FullName}"); + if (!di.EnumerateFileSystemInfos().Any()) list.Add(di); + } + + Log.Debug($"Found {list.Count} empty directories"); + return list.ToArray(); + } + + /// + /// Gets an array of directories older than a specified time. + /// This includes all directories outside of Parallel's back up directories. + /// + /// The root directory to search. + /// The time, as a . + /// If it should search recursively. + /// An array of directories in order of oldest first. + public static DirectoryInfo[] GetCleanableDirectories(string path, UnixTime start, bool recursive = true) + { + Dictionary list = new(); + DirectoryInfo directory = new(path); + EnumerationOptions options = new() + { + IgnoreInaccessible = true, + RecurseSubdirectories = recursive + }; + + foreach (DirectoryInfo di in directory.EnumerateDirectories("*", options)) + { + DateTime compare = di.CreationTime > di.LastWriteTime ? di.CreationTime : di.LastWriteTime; + bool older = start.TotalMilliseconds >= new UnixTime(compare).TotalMilliseconds; + bool exists = list.Keys.Any(d => di.FullName.StartsWith(d.FullName)); + if (!exists && older) list.Add(di, compare); + } + + Log.Debug($"Found {list.Count} cleanable directories"); + return list.OrderBy(d => d.Value).ToDictionary().Keys.ToArray(); + } + + public static FileInfo[] GetCleanableFiles(string path, UnixTime start, bool recursive = true) + { + Dictionary list = new(); + Log.Debug($"Searching '{path}' for files older than {start.ToString("g")}"); + foreach (string file in GetFiles(path, "*")) + { + FileInfo fi = new FileInfo(file); + DateTime compare = fi.CreationTime > fi.LastWriteTime ? fi.CreationTime : fi.LastWriteTime; + bool older = start.TotalMilliseconds >= new UnixTime(compare).TotalMilliseconds; + if (older) list.Add(fi, compare); + } + + Log.Debug($"Found {list.Count} cleanable files"); + return list.OrderBy(d => d.Value).ToDictionary().Keys.ToArray(); + } + + public static IEnumerable GetFiles(string root, string searchPattern) + { + return GetFiles(root, searchPattern, Array.Empty()); + } + + public static IEnumerable GetFiles(string root, string searchPattern, string[] exempt) + { + Stack pending = new(); + pending.Push(root); + while (pending.Count != 0) + { + string path = pending.Pop(); + IEnumerable next = null; + try + { + if (!IsIgnored(path, exempt)) + { + //Log.Debug($"Searching -> {path}"); + next = Directory.EnumerateFiles(path, searchPattern); + } + // else + // { + // Log.Debug($"Ignored -> {path}"); + // } + } + catch + { + Log.Debug("No file access -> " + path); + } + + if (next != null && next.Count() != 0) + { + foreach (string file in next) yield return file; + } + + try + { + next = Directory.EnumerateDirectories(path); + foreach (string subdir in next) pending.Push(subdir); + } + catch + { + Log.Debug("No folder access -> " + path); + } + } + } + + /// + /// Scans a directory for duplicate files with the same name and size. + /// + /// + /// A array of duplicate files, in order of most duplicate entries, where the key refers to the filename, and the values are an array of s in order of oldest to newest. + public static Dictionary GetDuplicateFiles(string path) + { + Dictionary> dict = new(); + IEnumerable files = GetFiles(path, "*"); + foreach (string file in files) + { + SystemFile entry = new(new FileInfo(file)); + if (dict.TryGetValue(entry.Name, out List value)) + { + SystemFile key = value.FirstOrDefault(); + if (entry.LocalSize.Equals(key.LocalSize)) + { + value.Add(entry); + } + } + else + { + dict.Add(entry.Name, new List { entry }); + } + } + + return dict.Where(kv => kv.Value.Count > 1).OrderByDescending(kv => kv.Value.Count).ToDictionary(k => k.Key, v => v.Value.OrderBy(l => l.LastWrite.TotalMilliseconds).ToArray()); + } + + /// + /// Checks if the given path is a directory. + /// + /// + /// True if path is a directory, otherwise false. + public static bool IsDirectory(string path) + { + return Directory.Exists(path) && !File.Exists(path); + } + + /// + /// Checks if the given path is a file. + /// + /// + /// True if path is a file, otherwise false. + public static bool IsFile(string path) + { + return !Directory.Exists(path) && File.Exists(path); + } + + /// + /// Checks if the given path is set to be ignored. + /// + /// + /// + /// True if ignored, otherwise false. + public static bool IsIgnored(string path, string[] exempt) + { + foreach (string entry in exempt) + { + if (path.StartsWith(entry)) + { + return true; + } + + if (entry.EndsWith('/')) + { + string[] folders = path.Split('\\'); + foreach (string dir in folders) + { + if (dir.ToLower() == entry.Remove(entry.Length - 1, 1).ToLower()) + { + return true; + } + } + } + + if (entry.StartsWith('*')) + { + if (path.EndsWith(entry.Replace("*", string.Empty))) + { + return true; + } + } + } + + return false; + } + } +} \ No newline at end of file diff --git a/Parallel.Core/Models/SystemFile.cs b/Parallel.Core/Models/SystemFile.cs new file mode 100644 index 0000000..7c2c288 --- /dev/null +++ b/Parallel.Core/Models/SystemFile.cs @@ -0,0 +1,149 @@ +// Copyright 2025 Kyle Ebbinga + +using System.Data; +using Parallel.Core.Data; +using Parallel.Core.Utils; + +namespace Parallel.Core.Models +{ + /// + /// Represents a file managed by Parallel. + /// + public class SystemFile + { + /// + /// The unique identifier of the file. + /// + public string Id { get; } = string.Empty; + + /// + /// The name of the file. + /// + public string Name { get; set; } = string.Empty; + + /// + /// The path of the file on the local machine. + /// + public string LocalPath { get; set; } = string.Empty; + + /// + /// The path of the file in the backup file system. + /// + public string RemotePath { get; set; } = string.Empty; + + /// + /// The time the current file was last written to. + /// + public UnixTime LastWrite { get; set; } = UnixTime.Now; + + /// + /// The time the file was either last saved or deleted. + /// + public UnixTime LastUpdate { get; set; } = UnixTime.Now; + + /// + /// The size, in bytes, of the file on the local machine. + /// + public long LocalSize { get; set; } = 0; + + /// + /// The size, in bytes, of the file in the remote backup. + /// + public long RemoteSize { get; set; } = 0; + + /// + /// The category of the file. + /// + public FileCategory Type { get; set; } = FileCategory.Other; + + /// + /// If the file is currently hidden on the local machine. + /// + public bool IsHidden { get; set; } = false; + + /// + /// If the file is currently read-only on the local machine. + /// + public bool IsReadOnly { get; set; } = false; + + /// + /// If the file is currently deleted on the local machine. + /// + public bool IsDeleted { get; set; } = false; + + + /// + /// Initializes a new instance of the class with default properties. + /// + public SystemFile(string path) + { + Id = HashGenerator.CreateSHA1(path); + } + + /// + /// Initializes a new instance of the class from a . + /// + /// + public SystemFile(FileInfo fileInfo) + { + Id = HashGenerator.CreateSHA1(fileInfo.FullName); + Name = fileInfo.Name; + LocalPath = fileInfo.FullName; + LocalSize = fileInfo.Length; + RemoteSize = fileInfo.Length; + Type = FileTypes.GetFileCategory(Path.GetExtension(fileInfo.Name)); + LastWrite = new UnixTime(fileInfo.LastWriteTime); + LastUpdate = UnixTime.Now; + IsDeleted = !fileInfo.Exists; + + if (fileInfo.Attributes.HasFlag(FileAttributes.Hidden)) + { + IsHidden = true; + } + + if (fileInfo.Attributes.HasFlag(FileAttributes.ReadOnly)) + { + IsReadOnly = true; + } + } + + /// + /// Initializes a new instance of the class from a . + /// + /// + public SystemFile(DataRow row) + { + Id = row.Field("id"); + Name = row.Field("name"); + LocalPath = row.Field("localpath"); + RemotePath = row.Field("remotepath"); + LocalSize = Convert.ToInt64(row.Field("localsize")); + RemoteSize = Convert.ToInt64(row.Field("remotesize")); + LastWrite = UnixTime.FromMilliseconds(row.Field("lastwrite")); + LastUpdate = UnixTime.FromMilliseconds(row.Field("lastupdate")); + Type = (FileCategory)Enum.Parse(typeof(FileCategory), row.Field("type")); + IsHidden = Converter.ToBool(Convert.ToInt32(row.Field("hidden"))); + IsReadOnly = Converter.ToBool(Convert.ToInt32(row.Field("readonly"))); + IsDeleted = Converter.ToBool(Convert.ToInt32(row.Field("deleted"))); + } + + public bool Equals(SystemFile value) + { + bool?[] results = + [ + this?.Id != null && value?.Id != null ? this.Id.Equals(value.Id) : (bool?)null, + this?.Name != null && value?.Name != null ? this.Name.Equals(value.Name) : (bool?)null, + this?.LocalPath != null && value?.LocalPath != null ? this.LocalPath.Equals(value.LocalPath) : (bool?)null, + this?.RemotePath != null && value?.RemotePath != null ? this.RemotePath.Equals(value.RemotePath) : (bool?)null, + value?.LocalSize != null ? this.LocalSize.Equals(value.LocalSize) : (bool?)null, + value?.RemoteSize != null ? this.RemoteSize.Equals(value.RemoteSize) : (bool?)null, + value?.Type != null ? this.Type.Equals(value.Type) : (bool?)null, + value?.IsHidden != null ? this.IsHidden.Equals(value.IsHidden) : (bool?)null, + value?.IsReadOnly != null ? this.IsReadOnly.Equals(value.IsReadOnly) : (bool?)null, + value?.IsDeleted != null ? this.IsDeleted.Equals(value.IsDeleted) : (bool?)null + ]; + + return results.All(b => b != null && (bool)b); + } + } +} \ No newline at end of file diff --git a/Parallel.Core/Net/Communication.cs b/Parallel.Core/Net/Communication.cs new file mode 100644 index 0000000..eb9f480 --- /dev/null +++ b/Parallel.Core/Net/Communication.cs @@ -0,0 +1,70 @@ +// Copyright 2025 Kyle Ebbinga + +using System.Net; +using System.Net.Sockets; +using System.Text; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Parallel.Core.Events; +using Parallel.Core.Utils; + +namespace Parallel.Core.Net +{ + /// + /// Represents UDP communication between services. + /// + public class Communication + { + private readonly CancellationTokenSource _exit = new(); + private bool _active; + + /// + /// The primary client for network communication. + /// + public UdpClient Client { get; } = new UdpClient(); + + public event EventHandler RecievedMessage; + + public Communication() + { + Client = new UdpClient(); + } + + public Communication(int port) + { + Client = new UdpClient(new IPEndPoint(IPAddress.Any, port)); + } + + /// + /// Starts listening for messages. + /// + public async Task Start() + { + _active = true; + while (_active && !_exit.IsCancellationRequested) + { + UdpReceiveResult result = await Client.ReceiveAsync(_exit.Token); + RecievedMessage?.Invoke(this, new MessageRecievedEventArgs(result)); + } + } + + /// + /// Stops listening for messages. + /// + public void Stop() + { + _active = false; + _exit.Cancel(); + } + + /// + /// Sends a message to a specified port on a specified remote host. + /// + /// + /// + public void Send(string message, IPEndPoint endPoint) + { + Client.Send(Encoding.UTF8.GetBytes(Encryption.Encode(message)), endPoint); + } + } +} \ No newline at end of file diff --git a/Parallel.Core/Net/MessageResult.cs b/Parallel.Core/Net/MessageResult.cs new file mode 100644 index 0000000..0bd428a --- /dev/null +++ b/Parallel.Core/Net/MessageResult.cs @@ -0,0 +1,8 @@ +// Copyright 2025 Kyle Ebbinga + +namespace Parallel.Core.Net +{ + public struct MessageResult + { + } +} \ No newline at end of file diff --git a/Parallel.Core/Parallel.Core.csproj b/Parallel.Core/Parallel.Core.csproj index 4f18314..5d2cb54 100644 --- a/Parallel.Core/Parallel.Core.csproj +++ b/Parallel.Core/Parallel.Core.csproj @@ -1,25 +1,43 @@  - net9.0 - enable - enable - Parallel - 1.0.0 - Kyle Ebbinga - Copyright $(Company). All Rights Reserved. - $(AssemblyVersion) - $(VersionPrefix)$(AssemblyVersion) - $(Company) + net8.0 + enable + disable + 1.0.1.0 + Entex Interactive, LLC + Copyright Entex Interactive, LLC. All Rights Reserved. + $(AssemblyVersion) + $(VersionPrefix)$(AssemblyVersion) + $(Company) + Parallel.Core + True + Parallel.Core - - - - - - + + + + + + + + + + ..\EntexSharp.dll + + + C:\Users\kebbi\.nuget\packages\newtonsoft.json\13.0.3\lib\net6.0\Newtonsoft.Json.dll + + + + + + + + + diff --git a/Parallel.Core/Settings/DatabaseCredentials.cs b/Parallel.Core/Settings/DatabaseCredentials.cs new file mode 100644 index 0000000..a8f7914 --- /dev/null +++ b/Parallel.Core/Settings/DatabaseCredentials.cs @@ -0,0 +1,45 @@ +// Copyright 2025 Kyle Ebbinga + +using Parallel.Core.Database; +using Parallel.Core.IO; + +namespace Parallel.Core.Settings +{ + /// + /// Represents credentials used to gain access to various s. + /// + public class DatabaseCredentials + { + /// + /// The associated provider of this database. + /// + public DatabaseProvider Provider { get; set; } = DatabaseProvider.Local; + + /// + /// The hostname or address of the database. + /// If using a , this will be a file path. + /// + public string Address { get; set; } = string.Empty; + + /// + /// The username of the database. + /// + public string Username { get; set; } = string.Empty; + + /// + /// The password of the database. + /// + public string Password { get; set; } = string.Empty; + + /// + /// The database name. + /// + public string Name { get; set; } = string.Empty; + + public static DatabaseCredentials Local => new() + { + Provider = DatabaseProvider.Local, + Address = Path.Combine(PathBuilder.ProgramData, $"{Environment.MachineName}.db") + }; + } +} \ No newline at end of file diff --git a/Parallel.Core/Settings/FileSystemCredentials.cs b/Parallel.Core/Settings/FileSystemCredentials.cs new file mode 100644 index 0000000..042bb1a --- /dev/null +++ b/Parallel.Core/Settings/FileSystemCredentials.cs @@ -0,0 +1,27 @@ +// Copyright 2025 Kyle Ebbinga + +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Parallel.Core.IO.FileSystem; + +namespace Parallel.Core.Settings +{ + /// + /// Represents credentials used to gain access to various s. + /// + public class FileSystemCredentials + { + public FileService Service { get; set; } = FileService.Local; + public string RootDirectory { get; set; } = string.Empty; + public string Address { get; set; } = string.Empty; + public string Username { get; set; } = string.Empty; + public string Password { get; set; } = string.Empty; + + public FileSystemCredentials() { } + + public FileSystemCredentials(string root) + { + RootDirectory = root; + } + } +} \ No newline at end of file diff --git a/Parallel.Core/Settings/ParallelSettings.cs b/Parallel.Core/Settings/ParallelSettings.cs new file mode 100644 index 0000000..412a8b5 --- /dev/null +++ b/Parallel.Core/Settings/ParallelSettings.cs @@ -0,0 +1,70 @@ +// Copyright 2025 Kyle Ebbinga + +using Newtonsoft.Json; +using Org.BouncyCastle.Math.EC; +using Parallel.Core.IO; + +namespace Parallel.Core.Settings +{ + /// + /// + /// + public class ParallelSettings + { + /// + /// The location to the application configuration file. + /// + private static string ConfigFile { get; } = Path.Combine(PathBuilder.ProgramData, "settings.json"); + + /// + /// The location of files for different file system credentials./>. + /// + public static string ProfilesDir { get; } = Path.Combine(PathBuilder.ProgramData, "Profiles"); + + /// + /// The address that will accept incoming commands. + /// Default: 127.0.0.1 + /// + public string Address { get; set; } = "127.0.0.1"; + + /// + /// The port number to listen for commands on. + /// Default: 8192 + /// + public int ListenerPort { get; set; } = 8192; + + /// + /// The profiles to use. + /// The CLI defaults to the first in the list. + /// + public HashSet Profiles { get; } = new HashSet(); + + + /// + /// Loads settings from a file. + /// + public static ParallelSettings Load() + { + Log.Debug($"Loading config file: {ConfigFile}"); + if (File.Exists(ConfigFile)) + { + string json = File.ReadAllText(ConfigFile); + return JsonConvert.DeserializeObject(json); + } + else + { + return new ParallelSettings(); + } + } + + /// + /// Saves settings to a file. + /// + public void Save() + { + Log.Debug($"Saving config file: {ConfigFile}"); + if (!Directory.Exists(PathBuilder.ProgramData)) Directory.CreateDirectory(PathBuilder.ProgramData); + File.WriteAllText(ConfigFile, JsonConvert.SerializeObject(this, Formatting.Indented)); + } + } +} \ No newline at end of file diff --git a/Parallel.Core/Settings/ProfileConfig.cs b/Parallel.Core/Settings/ProfileConfig.cs new file mode 100644 index 0000000..93c2d17 --- /dev/null +++ b/Parallel.Core/Settings/ProfileConfig.cs @@ -0,0 +1,226 @@ +// Copyright 2025 Kyle Ebbinga + +using System.Runtime.InteropServices; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Parallel.Core.Database; +using Parallel.Core.IO.FileSystem; +using Parallel.Core.Utils; + +namespace Parallel.Core.Settings +{ + /// + /// Represents a back-up connection. + /// + public class ProfileConfig + { + /// + /// A unique hash used to identify the profile. + /// + public string Id { get; } = HashGenerator.GenerateHash(12, true); + + /// + /// The name of the profile. + /// + public string Name { get; set; } = "Default"; + + /// + /// The credentials needed to log in to the associated . + /// + public DatabaseCredentials Database { get; } + + /// + /// The credentials needed to log in to the associated . + /// + public FileSystemCredentials FileSystem { get; } + + /// + /// The amount of time, in minutes, between backup cycles. + /// Default: 60 minutes + /// + public int BackupInterval { get; set; } = 60; + + /// + /// The amount of time, in days, to hold a file before it can be cleaned. + /// Default: 90 days + /// + public int RetentionPeriod { get; set; } = 90; + + /// + /// The amount of time, in days, to hold a file before it can be pruned. + /// Default: 180 days (6 months) + /// + public int PrunePeriod { get; set; } = 180; + + /// + /// A collection of directories to be backed up. + /// Default: Empty + /// + public HashSet BackupDirectories { get; } = CreateBackupDirectories(); + + /// + /// A collection of directories to be ignored when archiving or cleaning. + /// Default: Empty + /// + public HashSet IgnoreDirectories { get; } = CreateIgnoreDirectories(); + + /// + /// A collection of directories to be cleaned on the machine. + /// It's important to note that when using the service host this will delete any available file. + /// Default: Empty + /// + public HashSet CleanDirectories { get; } = CreateCleanDirectories(); + + /// + /// A collection of deleted directories allowed to be pruned. + /// Recommended when using a cloud-based to save on storage costs. + /// Default: Empty + /// + public HashSet PruneDirectories { get; } = new HashSet(); + + + /// + /// Initializes a new instance of the class. + /// + /// + /// + /// + /// + [JsonConstructor] + public ProfileConfig(string id, string name, DatabaseCredentials database, FileSystemCredentials fileSystem) + { + Id = id; + Name = name; + Database = database; + FileSystem = fileSystem; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// + /// + public ProfileConfig(string name, DatabaseCredentials database, FileSystemCredentials fileSystem) + { + Id = HashGenerator.GenerateHash(12, true); + Name = name; + Database = database; + FileSystem = fileSystem; + } + + /// + /// Loads settings from a file. + /// + public static ProfileConfig Load(string name) + { + ArgumentException.ThrowIfNullOrEmpty(name); + string path = Path.Combine(ParallelSettings.ProfilesDir, name + ".json"); + if (File.Exists(path)) + { + string json = File.ReadAllText(path); + return JsonConvert.DeserializeObject(json); + } + else + { + return new ProfileConfig(name, new DatabaseCredentials(), new FileSystemCredentials()); + } + } + + /// + /// Loads credentials from the app configuration. + /// + /// A instance. + public static ProfileConfig Load(ParallelSettings settings, string name) + { + ProfileConfig profile = Load(Path.GetFileNameWithoutExtension(settings.Profiles.FirstOrDefault())); + if (!string.IsNullOrEmpty(name)) + { + string path = Path.Combine(ParallelSettings.ProfilesDir, name + ".json"); + if (!File.Exists(path)) return null; + profile = Load(Path.GetFileNameWithoutExtension(path)); + } + + return profile; + } + + /// + /// Saves credentials to a file. + /// + /// The current profile to save. + public static void Save(ProfileConfig profile) + { + if (!Directory.Exists(ParallelSettings.ProfilesDir)) Directory.CreateDirectory(ParallelSettings.ProfilesDir); + string path = Path.Combine(ParallelSettings.ProfilesDir, profile.Name + ".json"); + Log.Debug($"Saving profile file: {path}"); + if (!File.Exists(path)) + { + Log.Debug("Creating file -> " + path); + File.Create(path).Close(); + } + + File.WriteAllText(path, JsonConvert.SerializeObject(profile, Formatting.Indented)); + } + + /// + /// Saves the current instance to a file. + /// + public void SaveToFile() + { + Save(this); + } + + #region Privates + + private static HashSet CreateBackupDirectories() + { + return + [ + Environment.GetFolderPath(Environment.SpecialFolder.Desktop), + Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), + Environment.GetFolderPath(Environment.SpecialFolder.MyPictures), + Environment.GetFolderPath(Environment.SpecialFolder.MyMusic), + Environment.GetFolderPath(Environment.SpecialFolder.MyVideos) + ]; + } + + private static HashSet CreateIgnoreDirectories() + { + HashSet list = new HashSet(); + + // Ignore folders on Windows machines + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + list.Add("$RECYCLE.BIN/"); // For NTFS file systems + list.Add("*.lnk"); // Shortcuts to other paths + } + + // Ignore folders on Linux machines + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + list.Add("lost+found/"); // File system recovery directory + list.Add(".Trash/"); // User's trash folder + list.Add("*.desktop"); // Linux shortcuts + } + + // Ignore folders on Apple machines + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + list.Add(".Trash/"); // User's trash folder + list.Add("*.DS_Store"); // macOS Finder metadata + } + + return list; + } + + private static HashSet CreateCleanDirectories() + { + return + [ + Path.GetTempPath(), + ]; + } + + #endregion + } +} \ No newline at end of file diff --git a/Parallel.Core/Utils/Converter.cs b/Parallel.Core/Utils/Converter.cs new file mode 100644 index 0000000..4295054 --- /dev/null +++ b/Parallel.Core/Utils/Converter.cs @@ -0,0 +1,63 @@ +// Copyright 2025 Kyle Ebbinga + +using System.Net; + +namespace Parallel.Core.Utils +{ + /// + /// Converts a data type to another data type. This class cannot be inherited. + /// + public static class Converter + { + /// + /// Converts a int32 into a boolean. + /// + /// The int32 value. + /// If 1 true, false otherwise. + public static bool ToBool(double value) + { + if (value.Equals(1)) + { + return true; + } + + return false; + } + + /// + /// Converts a boolean into an int32. + /// + /// The int32 value. + /// If 1 true, false otherwise. + public static int ToInt32(bool value) + { + if (value) + { + return 1; + } + + return 0; + } + + /// + /// Converts two double values into a percent. + /// + /// + /// + public static double ToPercent(double part, double whole) + { + return part * 100 / whole; + } + + /// + /// Converts an to + /// + /// + /// + public static string ToString(EndPoint endPoint) + { + string value = endPoint.ToString(); + return value[..value.IndexOf(':')]; + } + } +} \ No newline at end of file diff --git a/Parallel.Core/Utils/Encryption.cs b/Parallel.Core/Utils/Encryption.cs new file mode 100644 index 0000000..1571534 --- /dev/null +++ b/Parallel.Core/Utils/Encryption.cs @@ -0,0 +1,39 @@ +// Copyright 2025 Kyle Ebbinga + +using System.Text; + +namespace Parallel.Core.Utils +{ + /// + /// Provides functionality for encryption. This class cannot be inherited. + /// + public static class Encryption + { + /// + /// Converts a string of UTF-8 characters to a base64 string. + /// + /// The string to be encoded. + /// A base64 encoded string. + public static string Encode(string value) + { + if (string.IsNullOrEmpty(value)) return string.Empty; + + byte[] data = Encoding.UTF8.GetBytes(value); + string encoded = Convert.ToBase64String(data); + return encoded; + } + + /// + /// Converts a base64 string to a string of UTF-8 characters. + /// + /// The base64 string to decode. + public static string Decode(string value) + { + if (string.IsNullOrEmpty(value)) return string.Empty; + + byte[] data = Convert.FromBase64String(value); + string decoded = Encoding.UTF8.GetString(data); + return decoded; + } + } +} \ No newline at end of file diff --git a/Parallel.Core/Utils/Formatter.cs b/Parallel.Core/Utils/Formatter.cs new file mode 100644 index 0000000..411d22d --- /dev/null +++ b/Parallel.Core/Utils/Formatter.cs @@ -0,0 +1,78 @@ +// Copyright 2025 Kyle Ebbinga + +namespace Parallel.Core.Utils +{ + /// + /// Converts a data type to a formatted string. This class cannot be inherited. + /// + public class Formatter + { + /// + /// Formats a file size with the corresponding data volume. + /// + /// The bytes to convert. + /// The bytes formatted as a string. + public static string FromBytes(long bytes) + { + string[] sizeSuffixes = { "Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB" }; + int sizeIndex = 0; + double size = bytes; + + while (size >= 1000 && sizeIndex < sizeSuffixes.Length - 1) + { + sizeIndex++; + size /= 1000; + } + + return $"{size:N2} {sizeSuffixes[sizeIndex]}"; + } + + /// + /// Formats a with the corresponding data volume. + /// + /// The bytes to convert. + /// A formatted as a MM/DD/YYYY HH:MM TT. + public static string FromDateTime(DateTime dateTime) + { + return dateTime.ToLocalTime().ToString("g"); + } + + public static string FromTimeSpan(TimeSpan timeSpan) + { + if (timeSpan.TotalDays > 365) + { + int years = (int)Math.Floor(timeSpan.TotalDays / 365); + return years == 1 ? "1 year ago" : $"{years} years ago"; + } + else if (timeSpan.TotalDays > 30) + { + int months = (int)Math.Floor(timeSpan.TotalDays / 30); + return months == 1 ? "1 month ago" : $"{months} months ago"; + } + else if (timeSpan.TotalHours > 24) + { + int days = (int)timeSpan.TotalDays; + return days == 1 ? "1 day ago" : $"{days} days ago"; + } + else if (timeSpan.TotalMinutes > 60) + { + int hours = (int)timeSpan.TotalHours; + return hours == 1 ? "1 hour ago" : $"{hours} hours ago"; + } + else if (timeSpan.TotalSeconds > 60) + { + int minutes = (int)timeSpan.TotalMinutes; + return minutes == 1 ? "1 minute ago" : $"{minutes} minutes ago"; + } + else + { + return "Just now"; + } + } + + public static string FromTimeSpan(DateTime dateTime) + { + return dateTime.ToString("dd:HH:mm:ss.fff"); + } + } +} \ No newline at end of file diff --git a/Parallel.Core/Utils/HashGenerator.cs b/Parallel.Core/Utils/HashGenerator.cs new file mode 100644 index 0000000..3466323 --- /dev/null +++ b/Parallel.Core/Utils/HashGenerator.cs @@ -0,0 +1,46 @@ +// Copyright 2025 Kyle Ebbinga + +using System.Security.Cryptography; +using System.Text; + +namespace Parallel.Core.Utils +{ + /// + /// Provides functionality for generating random hashes. This class cannot be inherited. + /// + public static class HashGenerator + { + /// + /// Generates a random hash. + /// + /// + /// + /// + public static string GenerateHash(int length, bool lowercase = false) + { + return RandomNumberGenerator.GetHexString(length, lowercase); + } + + /// + /// Computes a SHA1 hash from a string. + /// + /// The string to hash. + /// A hash as a string. + public static string CreateSHA1(string value) + { + ArgumentException.ThrowIfNullOrEmpty(value); + return Convert.ToHexString(SHA1.HashData(Encoding.ASCII.GetBytes(value))).ToLower(); + } + + /// + /// Computes a SHA256 hash from a string. + /// + /// The string to hash. + /// A hash as a string. + public static string CreateSHA256(string value) + { + ArgumentException.ThrowIfNullOrEmpty(value); + return Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(value))).ToLower(); + } + } +} \ No newline at end of file diff --git a/Parallel.Core/Utils/ProcessMonitor.cs b/Parallel.Core/Utils/ProcessMonitor.cs new file mode 100644 index 0000000..6afa3e2 --- /dev/null +++ b/Parallel.Core/Utils/ProcessMonitor.cs @@ -0,0 +1,128 @@ +// Copyright 2025 Kyle Ebbinga + +using System.Diagnostics; +using System.Timers; + +namespace Parallel.Core.Utils +{ + /// + /// Allows for easy use of monitoring system processes. + /// + public class ProcessMonitor : IDisposable + { + private readonly Process _process; + private System.Timers.Timer? _timer; + private DateTime _oldTime = DateTime.UtcNow; + private TimeSpan _oldUsage; + + /// + /// The timestamp of when the associated process started running. + /// + public DateTime StartTime { get; private set; } + + /// + /// The duration of time that the associated process has been running. + /// + public TimeSpan Uptime { get; private set; } + + /// + /// The percent of cpu currently being utilized by the associated process. + /// + public double CpuUsage { get; private set; } + + /// + /// The highest recorded percent of cpu utilized by the associated process. + /// + public double PeakCpuUsage { get; private set; } + + /// + /// The amount of memory, in bytes, being used by the associated process. + /// + public double RamUsage { get; private set; } + + /// + /// The highest recorded amount of memory, in bytes, being used by the associated process. + /// + public double PeakRamUsage { get; private set; } + + /// + /// Initializes new instance of the class with the current process. + /// + public ProcessMonitor() + { + _process = Process.GetCurrentProcess(); + StartTime = _process.StartTime; + } + + /// + /// Initializes new instance of the class with a provided process. + /// + /// The process to monitor. + public ProcessMonitor(Process process) + { + _process = process; + StartTime = process.StartTime; + } + + + private void Timer_Elapsed(object sender, ElapsedEventArgs e) + { + Refresh(); + } + + /// + /// Starts refreshing the system process information of the associated with an interval between updates. + /// + /// The time, in milliseconds, in which to request a system update. + public void Start(double interval = 5000) + { + _oldTime = DateTime.UtcNow; + _oldUsage = _process.TotalProcessorTime; + _timer = new System.Timers.Timer(); + _timer.Elapsed += Timer_Elapsed; + _timer.Interval = interval; + _timer.Start(); + } + + /// + /// Stops refreshing the system process information. + /// + public void Stop() + { + _timer?.Stop(); + } + + /// + /// Refreshes the information cached in the associated process. + /// Its important to note that CPU utilization works by calculating the amount of time spent processing in a certain timespan. + /// Calling this function more frequently will provide a more accurate CPU utilization result. + /// + public void Refresh() + { + _process.Refresh(); + Uptime = DateTime.Now - _process.StartTime; + + DateTime endTime = DateTime.UtcNow; + TimeSpan endUsage = _process.TotalProcessorTime; + + double timeMs = (endTime - _oldTime).TotalMilliseconds; + double usageMs = (endUsage - _oldUsage).TotalMilliseconds; + + _oldTime = endTime; + _oldUsage = endUsage; + + CpuUsage = (usageMs / (Environment.ProcessorCount * timeMs)) * 100; + RamUsage = _process.PrivateMemorySize64; + + if (CpuUsage > PeakCpuUsage) PeakCpuUsage = CpuUsage; + if (RamUsage > PeakRamUsage) PeakRamUsage = RamUsage; + } + + /// + public void Dispose() + { + _process?.Dispose(); + GC.SuppressFinalize(this); + } + } +} \ No newline at end of file diff --git a/Parallel.Core/Utils/UnixTime.cs b/Parallel.Core/Utils/UnixTime.cs new file mode 100644 index 0000000..2b99d7c --- /dev/null +++ b/Parallel.Core/Utils/UnixTime.cs @@ -0,0 +1,305 @@ +// Copyright 2025 Kyle Ebbinga + +using Newtonsoft.Json; +using Parallel.Core.Extensions.Json; + +namespace Parallel.Core.Utils +{ + /// + /// Represents an instant in time, expressed as either the seconds, or milliseconds since the Unix epoch. + /// + [JsonConverter(typeof(UnixTimeConverter))] + public readonly struct UnixTime + { + private readonly DateTime _timestamp; + + #region Constants + + /// + /// Represents the largest possible value of . + /// + public const long MaxValue = 9223372036854775807; + + /// + /// Represents the smallest possible value of . + /// + public const long MinValue = -9223372036854775808; + + /// + /// A year in the corresponding milliseconds. + /// + public const long Year = 31104000000; + + /// + /// A month in the corresponding milliseconds. + /// + public const long Month = 2592000000; + + /// + /// A week in the corresponding milliseconds. + /// + public const long Week = 604800000; + + /// + /// A day in the corresponding milliseconds. + /// + public const long Day = 86400000; + + /// + /// An hour in the corresponding milliseconds. + /// + public const long Hour = 3600000; + + /// + /// A minute in the corresponding milliseconds. + /// + public const long Minute = 60000; + + /// + /// A second in the corresponding milliseconds. + /// + public const long Second = 1000; + + #endregion + + #region Properties + + /// + /// Gets a object that is set to current time since epoch. + /// + public static UnixTime Now => new(DateTime.Now); + + /// + /// Gets the value of the current structure expressed as whole milliseconds. + /// + public long TotalMilliseconds => (long)_timestamp.ToUniversalTime().Subtract(new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalMilliseconds; + + /// + /// Gets the value of the current structure expressed as whole and fractional seconds. + /// + public double TotalSeconds => _timestamp.ToUniversalTime().Subtract(new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalSeconds; + + + /// + /// Gets the value of the current structure expressed as whole and fractional minutes. + /// + public double TotalMinutes => _timestamp.ToUniversalTime().Subtract(new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalMinutes; + + /// + /// Gets the value of the current structure expressed as whole and fractional hours. + /// + public double TotalHours => _timestamp.ToUniversalTime().Subtract(new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalHours; + + /// + /// Gets the value of the current structure expressed as whole and fractional days. + /// + public double TotalDays => _timestamp.ToUniversalTime().Subtract(new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalDays; + + #endregion + + #region Constructors + + /// + /// Initializes new instance of the structure with epoch time. + /// + public UnixTime() + { + _timestamp = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); + } + + /// + /// Initializes new instance of the structure to the specified . + /// + /// The specified . + public UnixTime(DateTime datetime) + { + _timestamp = datetime.ToUniversalTime(); + } + + /// + /// Converts a string representation of a date and time to its equivalent. + /// + /// + /// An object equivalent to the date and time of the string. + /// + /// + public static UnixTime Parse(string s) + { + ArgumentNullException.ThrowIfNullOrEmpty(s); + return new UnixTime(DateTime.Parse(s).ToUniversalTime()); + } + + /// + /// Returns a that represents the milliseconds since epoch. + /// + /// + public static UnixTime FromTicks(long value) + { + return new UnixTime(new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddTicks(value)); + } + + /// + /// Returns a that represents the milliseconds since epoch. + /// + /// + public static UnixTime FromMilliseconds(double value) + { + return new UnixTime(new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddMilliseconds(value)); + } + + /// + /// Returns a that represents the seconds since epoch. + /// + /// + public static UnixTime FromSeconds(double value) + { + return new UnixTime(new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(value)); + } + + /// + /// Returns a that represents the minutes since epoch. + /// + /// + public static UnixTime FromMinutes(double value) + { + return new UnixTime(new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddMinutes(value)); + } + + /// + /// Returns a that represents the hours since epoch. + /// + /// + public static UnixTime FromHours(double value) + { + return new UnixTime(new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddHours(value)); + } + + /// + /// Returns a that represents the days since epoch. + /// + /// + public static UnixTime FromDays(double value) + { + return new UnixTime(new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddDays(value)); + } + + /// + /// Returns a that represents the months since epoch. + /// + /// + public static UnixTime FromMonths(int value) + { + return new UnixTime(new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddMonths(value)); + } + + /// + /// Returns a that represents the years since epoch. + /// + /// + public static UnixTime FromYears(int value) + { + return new UnixTime(new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddYears(value)); + } + + #endregion + + #region Methods + + /// + /// Returns a new that adds the specified to the value of this instance. + /// + /// + /// An object whose value is equal to the Unix time represented by this instance. + public UnixTime Add(TimeSpan time) + { + return new UnixTime(_timestamp.Add(time)); + } + + /// + /// Returns a new that adds the specified to the value of this instance. + /// + /// + /// An object whose value is equal to the Unix time represented by this instance. + public UnixTime Add(UnixTime time) + { + return Add(TimeSpan.FromMilliseconds(time.TotalMilliseconds)); + } + + /// + /// Returns a new that subtracts the specified to the value of this instance. + /// + /// + /// An object whose value is equal to the Unix time represented by this instance. + public UnixTime Subtract(TimeSpan time) + { + return new UnixTime(_timestamp.Subtract(time)); + } + + /// + /// Returns a new that subtracts the specified to the value of this instance. + /// + /// + /// An object whose value is equal to the Unix time represented by this instance. + public UnixTime Subtract(UnixTime time) + { + return Subtract(TimeSpan.FromMilliseconds(time.TotalMilliseconds)); + } + + /// + /// Converts the value of the current to a . + /// + /// A equivalent whose property is set to . + public DateTime ToUniversalTime() + { + return _timestamp.ToUniversalTime(); + } + + /// + /// Converts the value of the current to a . + /// + /// A equivalent whose property is set to . + public DateTime ToLocalTime() + { + return _timestamp.ToLocalTime(); + } + + /// + /// Converts the value of the current to a since January 1st, 1970. + /// + /// A structure equivalent to the current . + public TimeSpan ToTimeSpan() + { + return TimeSpan.FromMilliseconds(TotalMilliseconds); + } + + /// + /// Converts the current to the equivalent milliseconds since epoch. + /// + /// The string representation of milliseconds since epoch. + public override string ToString() + { + return TotalMilliseconds.ToString(); + } + + /// + /// Converts the current to the equivalent representation specified by format. + /// + /// + /// The string representation specified by the . + public string ToString(string format) + { + return _timestamp.ToString(format); + } + + /// + /// Converts the current to its equivalent representation specified by the ISO 8601 format. + /// + /// The string representation specified by the ISO 8601 format. + public string ToISOString() + { + return _timestamp.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ"); + } + + #endregion + } +} \ No newline at end of file From 65b4064dff3528bddef0eb24daafba6f2117cf18 Mon Sep 17 00:00:00 2001 From: Kyle | Guitar <26614720+TheGuitarleader@users.noreply.github.com> Date: Wed, 9 Jul 2025 19:32:44 -0500 Subject: [PATCH 04/20] Added service host project files --- Parallel.Service/Parallel.Service.csproj | 9 +++ Parallel.Service/Program.cs | 58 ++++++++++++++++++- Parallel.Service/Requests/IServiceRequest.cs | 9 +++ .../Responses/IServiceResponse.cs | 9 +++ .../Services/FileBackupService.cs | 14 +++++ .../Services/FileCleanupService.cs | 14 +++++ .../Services/TcpRequestService.cs | 14 +++++ 7 files changed, 125 insertions(+), 2 deletions(-) create mode 100644 Parallel.Service/Requests/IServiceRequest.cs create mode 100644 Parallel.Service/Responses/IServiceResponse.cs create mode 100644 Parallel.Service/Services/FileBackupService.cs create mode 100644 Parallel.Service/Services/FileCleanupService.cs create mode 100644 Parallel.Service/Services/TcpRequestService.cs diff --git a/Parallel.Service/Parallel.Service.csproj b/Parallel.Service/Parallel.Service.csproj index 1318e1b..df2f223 100644 --- a/Parallel.Service/Parallel.Service.csproj +++ b/Parallel.Service/Parallel.Service.csproj @@ -24,4 +24,13 @@ + + + + + + + + + diff --git a/Parallel.Service/Program.cs b/Parallel.Service/Program.cs index 10f94dc..b486d32 100644 --- a/Parallel.Service/Program.cs +++ b/Parallel.Service/Program.cs @@ -1,12 +1,66 @@ // Copyright 2025 Kyle Ebbinga +using System.Reflection; +using System.Runtime.InteropServices; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Parallel.Core.IO; +using Parallel.Core.Settings; +using Parallel.Core.Utils; +using Parallel.Service.Services; + namespace Parallel.Service { internal class Program { - static void Main(string[] args) + private static string _logFile = Path.Combine(PathBuilder.ProgramData, "Logs", "latest.txt"); + + static async Task Main(string[] args) { - Console.WriteLine("Hello, World!"); + HostApplicationBuilder builder = Host.CreateApplicationBuilder(args); + + // Logging + Log.Logger = new LoggerConfiguration().MinimumLevel.Debug().WriteTo.Console().WriteTo.File(_logFile).CreateLogger(); + builder.Logging.ClearProviders(); + builder.Logging.AddSerilog(); + + AssemblyName assembly = Assembly.GetExecutingAssembly().GetName(); + Log.Information($"{assembly.Name} v{assembly.Version}"); + + // Add Windows services + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + builder.Services.AddWindowsService(); + } + + // Add Linux systemd + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + builder.Services.AddSystemd(); + } + + // Background services + builder.Services.AddHostedService(); + builder.Services.AddHostedService(); + builder.Services.AddHostedService(); + + // Other services + builder.Services.AddSingleton(ParallelSettings.Load()); + builder.Services.AddSingleton(); + + IHost host = builder.Build(); + IHostApplicationLifetime lifetime = host.Services.GetRequiredService(); + ParallelSettings settings = host.Services.GetRequiredService(); + lifetime.ApplicationStopped.Register(() => + { + settings.Save(); + Log.CloseAndFlush(); + File.Move(_logFile, Path.Combine(PathBuilder.ProgramData, "Logs", $"{DateTime.Now:MM-dd-yyyy hh-mm-ss}.log")); + }); + + // Starts the application + await host.RunAsync(); } } } \ No newline at end of file diff --git a/Parallel.Service/Requests/IServiceRequest.cs b/Parallel.Service/Requests/IServiceRequest.cs new file mode 100644 index 0000000..e9b3154 --- /dev/null +++ b/Parallel.Service/Requests/IServiceRequest.cs @@ -0,0 +1,9 @@ +// Copyright 2025 Kyle Ebbinga + +namespace Parallel.Service.Requests +{ + public interface IServiceRequest + { + + } +} \ No newline at end of file diff --git a/Parallel.Service/Responses/IServiceResponse.cs b/Parallel.Service/Responses/IServiceResponse.cs new file mode 100644 index 0000000..6841756 --- /dev/null +++ b/Parallel.Service/Responses/IServiceResponse.cs @@ -0,0 +1,9 @@ +// Copyright 2025 Kyle Ebbinga + +namespace Parallel.Service.Responses +{ + public interface IServiceResponse + { + + } +} \ No newline at end of file diff --git a/Parallel.Service/Services/FileBackupService.cs b/Parallel.Service/Services/FileBackupService.cs new file mode 100644 index 0000000..7c0c0c1 --- /dev/null +++ b/Parallel.Service/Services/FileBackupService.cs @@ -0,0 +1,14 @@ +// Copyright 2025 Kyle Ebbinga + +using Microsoft.Extensions.Hosting; + +namespace Parallel.Service.Services +{ + public class FileBackupService : BackgroundService + { + protected override Task ExecuteAsync(CancellationToken stoppingToken) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/Parallel.Service/Services/FileCleanupService.cs b/Parallel.Service/Services/FileCleanupService.cs new file mode 100644 index 0000000..10d1363 --- /dev/null +++ b/Parallel.Service/Services/FileCleanupService.cs @@ -0,0 +1,14 @@ +// Copyright 2025 Kyle Ebbinga + +using Microsoft.Extensions.Hosting; + +namespace Parallel.Service.Services +{ + public class FileCleanupService : BackgroundService + { + protected override Task ExecuteAsync(CancellationToken stoppingToken) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/Parallel.Service/Services/TcpRequestService.cs b/Parallel.Service/Services/TcpRequestService.cs new file mode 100644 index 0000000..a53ed11 --- /dev/null +++ b/Parallel.Service/Services/TcpRequestService.cs @@ -0,0 +1,14 @@ +// Copyright 2025 Kyle Ebbinga + +using Microsoft.Extensions.Hosting; + +namespace Parallel.Service.Services +{ + public class TcpRequestService : BackgroundService + { + protected override Task ExecuteAsync(CancellationToken stoppingToken) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file From e7c0c69be753b953373e4612c227be914e226c37 Mon Sep 17 00:00:00 2001 From: Kyle | Guitar <26614720+TheGuitarleader@users.noreply.github.com> Date: Wed, 9 Jul 2025 19:40:42 -0500 Subject: [PATCH 05/20] Somehow these were ignored --- .gitignore | 9 +-- Parallel.Core/IO/Backup/BackupManager.cs | 28 +++++++ Parallel.Core/IO/Backup/BaseFileManager.cs | 66 +++++++++++++++++ Parallel.Core/IO/Backup/DeltaBackupManager.cs | 32 ++++++++ Parallel.Core/IO/Backup/FileBackupManager.cs | 74 +++++++++++++++++++ Parallel.Core/IO/Backup/IBackupManager.cs | 63 ++++++++++++++++ 6 files changed, 267 insertions(+), 5 deletions(-) create mode 100644 Parallel.Core/IO/Backup/BackupManager.cs create mode 100644 Parallel.Core/IO/Backup/BaseFileManager.cs create mode 100644 Parallel.Core/IO/Backup/DeltaBackupManager.cs create mode 100644 Parallel.Core/IO/Backup/FileBackupManager.cs create mode 100644 Parallel.Core/IO/Backup/IBackupManager.cs diff --git a/.gitignore b/.gitignore index 1c422db..9a9d1e7 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ *.user *.userosscache *.sln.docstates +*.sln # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs @@ -34,6 +35,7 @@ bld/ # Visual Studio 2015/2017 cache/options directory .vs/ + # Uncomment if you have tasks that create the project's static files in wwwroot #wwwroot/ @@ -82,8 +84,6 @@ StyleCopReport.xml *.pgc *.pgd *.rsp -# but not Directory.Build.rsp, as it configures directory-level build defaults -!Directory.Build.rsp *.sbr *.tlb *.tli @@ -95,7 +95,7 @@ StyleCopReport.xml *.tlog *.vspscc *.vssscc -.builds +.build *.pidb *.svclog *.scc @@ -257,7 +257,6 @@ Generated_Code/ # to a newer Visual Studio version. Backup files are not needed, # because we have git ;-) _UpgradeReport_Files/ -Backup*/ UpgradeLog*.XML UpgradeLog*.htm ServiceFabricBackup/ @@ -397,5 +396,5 @@ FodyWeavers.xsd *.msp # JetBrains Rider -.idea/ *.sln.iml +.idea/ \ No newline at end of file diff --git a/Parallel.Core/IO/Backup/BackupManager.cs b/Parallel.Core/IO/Backup/BackupManager.cs new file mode 100644 index 0000000..875c06f --- /dev/null +++ b/Parallel.Core/IO/Backup/BackupManager.cs @@ -0,0 +1,28 @@ +// Copyright 2025 Kyle Ebbinga + +using Newtonsoft.Json.Linq; +using Parallel.Core.Settings; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Parallel.Core.IO.Backup +{ + /// + /// Represents the way manage s. + /// + public static class BackupManager + { + /// + /// Creates a new instance of an . + /// + /// + /// + public static IBackupManager CreateNew(ProfileConfig profile) + { + return new FileBackupManager(profile); + } + } +} \ No newline at end of file diff --git a/Parallel.Core/IO/Backup/BaseFileManager.cs b/Parallel.Core/IO/Backup/BaseFileManager.cs new file mode 100644 index 0000000..a3c10af --- /dev/null +++ b/Parallel.Core/IO/Backup/BaseFileManager.cs @@ -0,0 +1,66 @@ +// Copyright 2025 Kyle Ebbinga + +using Parallel.Core.Database; +using Parallel.Core.Diagnostics; +using Parallel.Core.Events; +using Parallel.Core.IO.FileSystem; +using Parallel.Core.Models; +using Parallel.Core.Settings; + +namespace Parallel.Core.IO.Backup +{ + /// + /// Represents the base way of backing up files to an associated file system. + /// + public abstract class BaseFileManager : IBackupManager + { + /// + public ProfileConfig Profile { get; } + + /// + public IDatabase Database { get; set; } + + /// + public IFileSystem FileSystem { get; set; } + + /// + public string MachineName { get; } = Environment.MachineName; + + /// + public string RootFolder { get; set; } + + /// + /// + /// + /// + public BaseFileManager(ProfileConfig profile) + { + FileSystem = FileSystemManager.CreateNew(profile.FileSystem); + Profile = profile; + } + + /// + public virtual bool Initialize() + { + try + { + Database = DatabaseConnection.CreateNew(Profile); + bool fsInit = (FileSystem != null) && FileSystem.PingAsync().Result >= 0; + Profile.IgnoreDirectories.Add(Profile.FileSystem.RootDirectory); + if (Profile != null) Profile.SaveToFile(); + return fsInit; + } + catch (Exception ex) + { + Log.Error(ex.GetBaseException().ToString()); + return false; + } + } + + /// + public abstract Task BackupFilesAsync(SystemFile[] files, IProgress progress); + + /// + public abstract Task RestoreFilesAsync(SystemFile[] files, IProgress progress); + } +} \ No newline at end of file diff --git a/Parallel.Core/IO/Backup/DeltaBackupManager.cs b/Parallel.Core/IO/Backup/DeltaBackupManager.cs new file mode 100644 index 0000000..56b8754 --- /dev/null +++ b/Parallel.Core/IO/Backup/DeltaBackupManager.cs @@ -0,0 +1,32 @@ +// Copyright 2025 Kyle Ebbinga + +using Parallel.Core.Diagnostics; +using Parallel.Core.Models; +using Parallel.Core.Settings; + +namespace Parallel.Core.IO.Backup +{ + /// + /// Represents the way to clone files to an associated file system using file deltas. + /// + public class DeltaBackupManager : BaseFileManager + { + /// + /// Initializes a new instance of the class. + /// + /// + public DeltaBackupManager(ProfileConfig profile) : base(profile) { } + + /// + public override Task BackupFilesAsync(SystemFile[] systemFiles, IProgress progress) + { + throw new NotImplementedException(); + } + + /// + public override Task RestoreFilesAsync(SystemFile[] systemFiles, IProgress progress) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/Parallel.Core/IO/Backup/FileBackupManager.cs b/Parallel.Core/IO/Backup/FileBackupManager.cs new file mode 100644 index 0000000..32bad08 --- /dev/null +++ b/Parallel.Core/IO/Backup/FileBackupManager.cs @@ -0,0 +1,74 @@ +// Copyright 2025 Kyle Ebbinga + +using Newtonsoft.Json.Linq; +using Parallel.Core.Database; +using Parallel.Core.Diagnostics; +using Parallel.Core.Events; +using Parallel.Core.IO.FileSystem; +using Parallel.Core.Models; +using Parallel.Core.Settings; + +namespace Parallel.Core.IO.Backup +{ + /// + /// Represents the way to archive files to an associated file system. + /// + public class FileBackupManager : BaseFileManager + { + private List _tasks = new List(); + private int _totalFiles; + + /// + /// Initializes a new instance of the class. + /// + /// + public FileBackupManager(ProfileConfig profile) : base(profile) { } + + /// + public override async Task BackupFilesAsync(SystemFile[] files, IProgress progress) + { + if (!files.Any()) return; + SystemFile[] backupFiles = files.Where(f => !f.IsDeleted).ToArray(); + Log.Information($"Backing up {backupFiles.Length} files..."); + await FileSystem.UploadFilesAsync(backupFiles, progress); + for (int i = 0; i < files.Length; i++) + { + SystemFile file = files.ElementAt(i); + if (file.IsDeleted) + { + Database.AddHistory(file.LocalPath, HistoryType.Archived); + Database.AddFile(file); + progress.Report(ProgressOperation.Archiving, file, i, files.Length); + } + else + { + progress.Report(ProgressOperation.Syncing, file, i, files.Length); + SystemFile remote = await FileSystem.GetFileAsync(file.RemotePath); + if (remote is not null) + { + file.RemoteSize = remote.RemoteSize; + Database.AddHistory(file.LocalPath, HistoryType.Synced); + Database.AddFile(file); + } + } + } + } + + /// + public override async Task RestoreFilesAsync(SystemFile[] files, IProgress progress) + { + SystemFile[] restoreFiles = files.Where(f => f.IsDeleted).ToArray(); + + if (!restoreFiles.Any()) return; + await FileSystem.DownloadFilesAsync(restoreFiles, progress); + + + for (int i = 0; i < files.Length; i++) + { + SystemFile file = files[i]; + Log.Information($"Restoring file: {file.LocalPath}..."); + file.RemotePath = PathBuilder.Remote(file.LocalPath, Profile.FileSystem); + } + } + } +} \ No newline at end of file diff --git a/Parallel.Core/IO/Backup/IBackupManager.cs b/Parallel.Core/IO/Backup/IBackupManager.cs new file mode 100644 index 0000000..ad6f6b5 --- /dev/null +++ b/Parallel.Core/IO/Backup/IBackupManager.cs @@ -0,0 +1,63 @@ +// Copyright 2025 Kyle Ebbinga + +using Parallel.Core.Database; +using Parallel.Core.Diagnostics; +using Parallel.Core.Events; +using Parallel.Core.IO.FileSystem; +using Parallel.Core.Models; +using Parallel.Core.Settings; + +namespace Parallel.Core.IO.Backup +{ + /// + /// Defines the methods needed for backing up a file system. + /// + public interface IBackupManager + { + /// + /// The back-up connection profile. + /// + public ProfileConfig Profile { get; } + + /// + /// The associated database connection. + /// + IDatabase Database { get; set; } + + /// + /// The associated file system connection. + /// + IFileSystem FileSystem { get; set; } + + /// + /// The current machine name. + /// + string MachineName { get; } + + /// + /// The root directory of the back-up. + /// + string RootFolder { get; set; } + + + /// + /// Initializes the backup manager by logging into the and + /// + /// + bool Initialize(); + + /// + /// Backs up an array of files as fast as possible. + /// + /// + /// + Task BackupFilesAsync(SystemFile[] files, IProgress progress); + + /// + /// Restores an array of files as fast as possible. + /// + /// + /// + Task RestoreFilesAsync(SystemFile[] files, IProgress progress); + } +} \ No newline at end of file From 101ba5998a8620ee19834824ba828b9dd5d20b23 Mon Sep 17 00:00:00 2001 From: Kyle | Guitar <26614720+TheGuitarleader@users.noreply.github.com> Date: Thu, 10 Jul 2025 14:59:35 -0500 Subject: [PATCH 06/20] Created new Net library for cleaning up unused code in the CLI --- .../Communication.cs | 0 .../MessageResult.cs | 0 Parallel.Core.Net/Parallel.Core.Net.csproj | 13 ++++++ Parallel.Core.Net/ServerRequest.cs | 22 ++++++++++ Parallel.Core.Net/Sockets/ISocketHandler.cs | 31 ++++++++++++++ Parallel.Core.Net/Sockets/TcpSocketHandler.cs | 41 +++++++++++++++++++ 6 files changed, 107 insertions(+) rename {Parallel.Core/Net => Parallel.Core.Net}/Communication.cs (100%) rename {Parallel.Core/Net => Parallel.Core.Net}/MessageResult.cs (100%) create mode 100644 Parallel.Core.Net/Parallel.Core.Net.csproj create mode 100644 Parallel.Core.Net/ServerRequest.cs create mode 100644 Parallel.Core.Net/Sockets/ISocketHandler.cs create mode 100644 Parallel.Core.Net/Sockets/TcpSocketHandler.cs diff --git a/Parallel.Core/Net/Communication.cs b/Parallel.Core.Net/Communication.cs similarity index 100% rename from Parallel.Core/Net/Communication.cs rename to Parallel.Core.Net/Communication.cs diff --git a/Parallel.Core/Net/MessageResult.cs b/Parallel.Core.Net/MessageResult.cs similarity index 100% rename from Parallel.Core/Net/MessageResult.cs rename to Parallel.Core.Net/MessageResult.cs diff --git a/Parallel.Core.Net/Parallel.Core.Net.csproj b/Parallel.Core.Net/Parallel.Core.Net.csproj new file mode 100644 index 0000000..9d41196 --- /dev/null +++ b/Parallel.Core.Net/Parallel.Core.Net.csproj @@ -0,0 +1,13 @@ + + + + net9.0 + enable + enable + + + + + + + diff --git a/Parallel.Core.Net/ServerRequest.cs b/Parallel.Core.Net/ServerRequest.cs new file mode 100644 index 0000000..817e237 --- /dev/null +++ b/Parallel.Core.Net/ServerRequest.cs @@ -0,0 +1,22 @@ +// Copyright 2025 Kyle Ebbinga + +namespace Parallel.Core.Net +{ + public class ServerRequest + { + /// + /// The request name. + /// + public string Name { get; } + + /// + /// The request parameters. + /// + public Dictionary Parameters { get; } + + /// + /// The current session id. + /// + public string Session { get; } = string.Empty; + } +} \ No newline at end of file diff --git a/Parallel.Core.Net/Sockets/ISocketHandler.cs b/Parallel.Core.Net/Sockets/ISocketHandler.cs new file mode 100644 index 0000000..8000d4f --- /dev/null +++ b/Parallel.Core.Net/Sockets/ISocketHandler.cs @@ -0,0 +1,31 @@ +// Copyright 2025 Kyle Ebbinga + +using System.Net; +using Parallel.Core.Utils; + +namespace Parallel.Core.Net.Sockets +{ + public interface ISocketHandler + { + /// + /// The time the socket was received. + /// + UnixTime ReceivedAt { get; } + + /// + /// The raw string of incoming data decrypted. + /// + string RawData { get; set; } + + /// + /// The remote client that sent the request. + /// + IPEndPoint RemoteEndPoint { get; } + + /// + /// Reads the incoming encrypted data as formatted JSON string. + /// + /// + ServerRequest ReceiveRequest(); + } +} \ No newline at end of file diff --git a/Parallel.Core.Net/Sockets/TcpSocketHandler.cs b/Parallel.Core.Net/Sockets/TcpSocketHandler.cs new file mode 100644 index 0000000..755c20b --- /dev/null +++ b/Parallel.Core.Net/Sockets/TcpSocketHandler.cs @@ -0,0 +1,41 @@ +// Copyright 2025 Kyle Ebbinga + +using System.Net; +using System.Net.Sockets; +using Parallel.Core.Utils; + +namespace Parallel.Core.Net.Sockets +{ + public class TcpSocketHandler : ISocketHandler + { + /// + /// + /// + public Socket Socket { get; } + + /// + public UnixTime ReceivedAt { get; } + + /// + public string RawData { get; set; } + + /// + public IPEndPoint RemoteEndPoint { get; } + + /// + /// Initializes a new instance of the class for the specified socket. + /// + /// The socket to handle. + public TcpSocketHandler(Socket socket) + { + ReceivedAt = UnixTime.Now; + Socket = socket; + RemoteEndPoint = (IPEndPoint)socket.RemoteEndPoint; + } + + public ServerRequest ReceiveRequest() + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file From 4441ae0d1f3860153ef08e582b72c3eb1dff0ec1 Mon Sep 17 00:00:00 2001 From: Kyle | Guitar <26614720+TheGuitarleader@users.noreply.github.com> Date: Thu, 10 Jul 2025 17:21:34 -0500 Subject: [PATCH 07/20] Started work on TCP socket connections --- Parallel.Core.Net/Connections/IConnection.cs | 9 ++ .../Connections/TcpConnection.cs | 91 +++++++++++++++++++ Parallel.Core.Net/ServerRequest.cs | 13 ++- Parallel.Core.Net/ServerResponse.cs | 30 ++++++ Parallel.Core.Net/Sockets/ISocketHandler.cs | 13 ++- Parallel.Core.Net/Sockets/TcpSocketHandler.cs | 53 ++++++++++- 6 files changed, 203 insertions(+), 6 deletions(-) create mode 100644 Parallel.Core.Net/Connections/IConnection.cs create mode 100644 Parallel.Core.Net/Connections/TcpConnection.cs create mode 100644 Parallel.Core.Net/ServerResponse.cs diff --git a/Parallel.Core.Net/Connections/IConnection.cs b/Parallel.Core.Net/Connections/IConnection.cs new file mode 100644 index 0000000..40b26fc --- /dev/null +++ b/Parallel.Core.Net/Connections/IConnection.cs @@ -0,0 +1,9 @@ +// Copyright 2025 Kyle Ebbinga + +namespace Parallel.Core.Net.Connections +{ + public interface IConnection + { + Task SendRequestAsync(ServerRequest request); + } +} \ No newline at end of file diff --git a/Parallel.Core.Net/Connections/TcpConnection.cs b/Parallel.Core.Net/Connections/TcpConnection.cs new file mode 100644 index 0000000..aa1ce43 --- /dev/null +++ b/Parallel.Core.Net/Connections/TcpConnection.cs @@ -0,0 +1,91 @@ +// Copyright 2025 Kyle Ebbinga + +using System.Net.Sockets; +using System.Text; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Parallel.Core.Utils; +using Serilog; +using Serilog.Core; + +namespace Parallel.Core.Net.Connections +{ + public class TcpConnection : IConnection + { + private readonly string _address; + private readonly int _port; + + /// + /// Initializes a new instance of the class with the saved settings. + /// + public TcpConnection() + { + _address = "127.0.0.1"; + _port = 8192; + } + + /// + /// Initializes a new instance of the class with a address and port. + /// + public TcpConnection(string address, int port) + { + _address = address; + _port = port; + } + + public async Task SendRequestAsync(ServerRequest request) + { + Socket socket = new(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); + socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.DontLinger, true); + ServerResponse response = new(request); + + try + { + socket.Connect(_address, _port); + if (socket.Connected) + { + Console.WriteLine($"Connected: {socket.Connected}"); + Console.WriteLine($"Available: {socket.Available}"); + + + // Sends an encrypted json request to the server. + string rawJson = JsonConvert.SerializeObject(request) + ";"; + Log.Debug($"Sending request: '{rawJson}'"); + socket.Send(Encoding.UTF8.GetBytes(rawJson)); + + // The encrypted returned json + string returnedData = string.Empty; + using (NetworkStream ns = new(socket)) + { + while (!returnedData.EndsWith(';')) + { + byte[] buffer = new byte[socket.ReceiveBufferSize]; + int bytesRead = ns.Read(buffer, 0, socket.ReceiveBufferSize); + returnedData += Encoding.UTF8.GetString(buffer, 0, bytesRead); + } + } + + Log.Debug($"Response: {returnedData}"); + JToken? json = JToken.Parse(returnedData.TrimEnd(';')); + response = ServerResponse.Parse(request, json); + + // Closes the socket. + socket.Shutdown(SocketShutdown.Both); + socket.Close(); + return response; + } + else + { + Log.Warning($"Failed to connect to server '{_address}:{_port}'"); + return response; + } + } + catch (Exception ex) + { + Log.Warning(ex.Message); + return response; + } + } + } +} \ No newline at end of file diff --git a/Parallel.Core.Net/ServerRequest.cs b/Parallel.Core.Net/ServerRequest.cs index 817e237..932865c 100644 --- a/Parallel.Core.Net/ServerRequest.cs +++ b/Parallel.Core.Net/ServerRequest.cs @@ -1,5 +1,7 @@ // Copyright 2025 Kyle Ebbinga +using Newtonsoft.Json; + namespace Parallel.Core.Net { public class ServerRequest @@ -15,8 +17,15 @@ public class ServerRequest public Dictionary Parameters { get; } /// - /// The current session id. + /// Initializes new instance of the class with a request name and a of parameters. /// - public string Session { get; } = string.Empty; + /// The request name. + /// A collection of parameter keys and values. + [JsonConstructor] + public ServerRequest(string name, Dictionary parameters) + { + Name = name.ToLower(); + Parameters = parameters; + } } } \ No newline at end of file diff --git a/Parallel.Core.Net/ServerResponse.cs b/Parallel.Core.Net/ServerResponse.cs new file mode 100644 index 0000000..78e90a9 --- /dev/null +++ b/Parallel.Core.Net/ServerResponse.cs @@ -0,0 +1,30 @@ +// Copyright 2025 Kyle Ebbinga + +using Newtonsoft.Json.Linq; + +namespace Parallel.Core.Net.Connections +{ + public class ServerResponse + { + public ServerRequest Request { get; } + public bool IsSuccess { get; } = false; + public JToken? Data { get; } + + public ServerResponse(ServerRequest request) + { + Request = request; + } + + private ServerResponse(ServerRequest request, JToken? data, bool isSuccess) + { + Request = request; + IsSuccess = isSuccess; + Data = data; + } + + public static ServerResponse Parse(ServerRequest request, JToken? json) + { + return new ServerResponse(request, json, json != null); + } + } +} \ No newline at end of file diff --git a/Parallel.Core.Net/Sockets/ISocketHandler.cs b/Parallel.Core.Net/Sockets/ISocketHandler.cs index 8000d4f..08398c3 100644 --- a/Parallel.Core.Net/Sockets/ISocketHandler.cs +++ b/Parallel.Core.Net/Sockets/ISocketHandler.cs @@ -22,10 +22,21 @@ public interface ISocketHandler /// IPEndPoint RemoteEndPoint { get; } + /// + /// Shuts down the , closes the connection, and releases all resources. + /// + void Close(); + /// /// Reads the incoming encrypted data as formatted JSON string. /// /// - ServerRequest ReceiveRequest(); + ServerRequest? Parse(); + + /// + /// Responds to the current request. + /// + /// + Task RespondAsync(object? data); } } \ No newline at end of file diff --git a/Parallel.Core.Net/Sockets/TcpSocketHandler.cs b/Parallel.Core.Net/Sockets/TcpSocketHandler.cs index 755c20b..c792447 100644 --- a/Parallel.Core.Net/Sockets/TcpSocketHandler.cs +++ b/Parallel.Core.Net/Sockets/TcpSocketHandler.cs @@ -2,7 +2,11 @@ using System.Net; using System.Net.Sockets; +using System.Text; +using Newtonsoft.Json; using Parallel.Core.Utils; +using Serilog; +using Serilog.Core; namespace Parallel.Core.Net.Sockets { @@ -17,7 +21,7 @@ public class TcpSocketHandler : ISocketHandler public UnixTime ReceivedAt { get; } /// - public string RawData { get; set; } + public string RawData { get; set; } = string.Empty; /// public IPEndPoint RemoteEndPoint { get; } @@ -33,9 +37,52 @@ public TcpSocketHandler(Socket socket) RemoteEndPoint = (IPEndPoint)socket.RemoteEndPoint; } - public ServerRequest ReceiveRequest() + public void Close() { - throw new NotImplementedException(); + Socket.Shutdown(SocketShutdown.Both); + Socket.Close(); + } + + public ServerRequest? Parse() + { + using (NetworkStream ns = new(Socket)) + { + Console.WriteLine("1"); + while (!RawData.EndsWith(';')) + { + Console.WriteLine("2"); + byte[] buffer = new byte[Socket.ReceiveBufferSize]; + int bytesRead = ns.Read(buffer, 0, Socket.ReceiveBufferSize); + RawData += Encoding.UTF8.GetString(buffer, 0, bytesRead); + Log.Debug(RawData); + Console.WriteLine("3"); + } + + Console.WriteLine("4"); + } + + return JsonConvert.DeserializeObject(RawData.TrimEnd(';')); + } + + public Task RespondAsync(object? data) + { + try + { + string json = JsonConvert.SerializeObject(data, Formatting.Indented); + byte[] bytes = Encoding.ASCII.GetBytes(Encryption.Encode(json)); + Socket?.Send(bytes); + Close(); + return Task.CompletedTask; + } + catch (ObjectDisposedException) + { + throw; + } + catch (Exception ex) + { + Log.Error(ex.Message); + return Task.CompletedTask; + } } } } \ No newline at end of file From 8a88f18f1cfa345d78a5e652225bbd94e93b20ea Mon Sep 17 00:00:00 2001 From: Kyle | Guitar <26614720+TheGuitarleader@users.noreply.github.com> Date: Thu, 10 Jul 2025 17:22:21 -0500 Subject: [PATCH 08/20] Renamed to Parallel Server --- .../Attributes/DescriptionAttribute.cs | 12 +++ .../Attributes/RequiredAttribute.cs | 7 ++ .../Parallel.Server.csproj | 6 +- .../Program.cs | 6 +- Parallel.Server/RequestHandler.cs | 71 ++++++++++++++ Parallel.Server/Requests/BaseRequest.cs | 28 ++++++ Parallel.Server/Requests/HelpRequest.cs | 17 ++++ Parallel.Server/Requests/IRequest.cs | 19 ++++ Parallel.Server/Requests/LoginRequest.cs | 24 +++++ Parallel.Server/Requests/PingRequest.cs | 15 +++ Parallel.Server/Responses/IResponse.cs | 8 ++ Parallel.Server/Responses/ObjectResponse.cs | 14 +++ .../Services/FileBackupService.cs | 4 +- .../Services/FileCleanupService.cs | 4 +- Parallel.Server/Services/TcpRequestService.cs | 88 ++++++++++++++++++ .../parallel-red.ico | Bin Parallel.Service/Requests/IServiceRequest.cs | 9 -- .../Responses/IServiceResponse.cs | 9 -- .../Services/TcpRequestService.cs | 14 --- Parallel.sln | 10 +- 20 files changed, 324 insertions(+), 41 deletions(-) create mode 100644 Parallel.Server/Attributes/DescriptionAttribute.cs create mode 100644 Parallel.Server/Attributes/RequiredAttribute.cs rename Parallel.Service/Parallel.Service.csproj => Parallel.Server/Parallel.Server.csproj (76%) rename {Parallel.Service => Parallel.Server}/Program.cs (93%) create mode 100644 Parallel.Server/RequestHandler.cs create mode 100644 Parallel.Server/Requests/BaseRequest.cs create mode 100644 Parallel.Server/Requests/HelpRequest.cs create mode 100644 Parallel.Server/Requests/IRequest.cs create mode 100644 Parallel.Server/Requests/LoginRequest.cs create mode 100644 Parallel.Server/Requests/PingRequest.cs create mode 100644 Parallel.Server/Responses/IResponse.cs create mode 100644 Parallel.Server/Responses/ObjectResponse.cs rename {Parallel.Service => Parallel.Server}/Services/FileBackupService.cs (73%) rename {Parallel.Service => Parallel.Server}/Services/FileCleanupService.cs (73%) create mode 100644 Parallel.Server/Services/TcpRequestService.cs rename {Parallel.Service => Parallel.Server}/parallel-red.ico (100%) delete mode 100644 Parallel.Service/Requests/IServiceRequest.cs delete mode 100644 Parallel.Service/Responses/IServiceResponse.cs delete mode 100644 Parallel.Service/Services/TcpRequestService.cs diff --git a/Parallel.Server/Attributes/DescriptionAttribute.cs b/Parallel.Server/Attributes/DescriptionAttribute.cs new file mode 100644 index 0000000..dc4fddd --- /dev/null +++ b/Parallel.Server/Attributes/DescriptionAttribute.cs @@ -0,0 +1,12 @@ +// Copyright 2025 Kyle Ebbinga + +namespace Parallel.Server.Attributes +{ + [AttributeUsage(AttributeTargets.Class)] + public class DescriptionAttribute : Attribute + { + public string Text { get; } + + public DescriptionAttribute(string text) => Text = text; + } +} \ No newline at end of file diff --git a/Parallel.Server/Attributes/RequiredAttribute.cs b/Parallel.Server/Attributes/RequiredAttribute.cs new file mode 100644 index 0000000..017283f --- /dev/null +++ b/Parallel.Server/Attributes/RequiredAttribute.cs @@ -0,0 +1,7 @@ +// Copyright 2025 Kyle Ebbinga + +namespace Parallel.Server.Attributes +{ + [AttributeUsage(AttributeTargets.Property)] + public class RequiredAttribute : Attribute { } +} \ No newline at end of file diff --git a/Parallel.Service/Parallel.Service.csproj b/Parallel.Server/Parallel.Server.csproj similarity index 76% rename from Parallel.Service/Parallel.Service.csproj rename to Parallel.Server/Parallel.Server.csproj index df2f223..b43b230 100644 --- a/Parallel.Service/Parallel.Service.csproj +++ b/Parallel.Server/Parallel.Server.csproj @@ -6,7 +6,7 @@ enable parallel-red.ico enable - Parallel + ParallelServer 1.0.0 Kyle Ebbinga Copyright $(Company). All Rights Reserved. @@ -16,6 +16,9 @@ + + + @@ -30,6 +33,7 @@ + diff --git a/Parallel.Service/Program.cs b/Parallel.Server/Program.cs similarity index 93% rename from Parallel.Service/Program.cs rename to Parallel.Server/Program.cs index b486d32..8cd23fe 100644 --- a/Parallel.Service/Program.cs +++ b/Parallel.Server/Program.cs @@ -8,9 +8,10 @@ using Parallel.Core.IO; using Parallel.Core.Settings; using Parallel.Core.Utils; -using Parallel.Service.Services; +using Parallel.Server.Requests; +using Parallel.Server.Services; -namespace Parallel.Service +namespace Parallel.Server { internal class Program { @@ -47,6 +48,7 @@ static async Task Main(string[] args) // Other services builder.Services.AddSingleton(ParallelSettings.Load()); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); IHost host = builder.Build(); diff --git a/Parallel.Server/RequestHandler.cs b/Parallel.Server/RequestHandler.cs new file mode 100644 index 0000000..402db85 --- /dev/null +++ b/Parallel.Server/RequestHandler.cs @@ -0,0 +1,71 @@ +// Copyright 2025 Kyle Ebbinga + +using System.ComponentModel.DataAnnotations; +using Parallel.Core.Net; +using Parallel.Server.Requests; + +namespace Parallel.Server +{ + public class RequestHandler + { + private readonly Dictionary _requests; + + public RequestHandler() + { + Type[] types = AppDomain.CurrentDomain.GetAssemblies().SelectMany(a => a.GetTypes()).Where(t => typeof(BaseRequest).IsAssignableFrom(t) && !t.IsAbstract).ToArray(); + _requests = types.ToDictionary(t => t.Name.Replace("Request", ""), t => t, StringComparer.OrdinalIgnoreCase); + + // Logs if all requests registered. + if (_requests.Count == types.Length) + { + Log.Information($"Successfully registered all {types.Length} requests"); + } + else + { + int remaining = types.Length - _requests.Count; + Log.Warning($"Failed to register {remaining} requests"); + } + } + + /// + /// Creates an to be handled. + /// + /// The name of the request. + /// The corresponding . If none was found a help request will be returned. + public IRequest CreateNew(ServerRequest request) + { + if (!_requests.TryGetValue(request.Name, out Type? requestType)) + { + Log.Warning($"Unknown command: {request.Name}"); + throw new InvalidOperationException($"Unknown command: {request.Name}"); + } + + // Instantiate the request object + object? instance = Activator.CreateInstance(requestType); + if (instance is not IRequest requestInstance) + throw new InvalidOperationException($"Type '{requestType.Name}' does not implement IRequest."); + + // Map parameters to object properties + foreach (var prop in requestType.GetProperties()) + { + if (request.Parameters.TryGetValue(prop.Name, out var value)) + { + var converted = Convert.ChangeType(value, prop.PropertyType); + prop.SetValue(instance, converted); + } + } + + // Validate required properties + var validationResults = new List(); + var context = new ValidationContext(instance, serviceProvider: null, items: null); + if (!Validator.TryValidateObject(instance, context, validationResults, validateAllProperties: true)) + { + var errors = string.Join("; ", validationResults.Select(r => r.ErrorMessage)); + Log.Warning($"Validation failed for '{request.Name}': {errors}"); + throw new InvalidOperationException($"Validation failed: {errors}"); + } + + return requestInstance; + } + } +} \ No newline at end of file diff --git a/Parallel.Server/Requests/BaseRequest.cs b/Parallel.Server/Requests/BaseRequest.cs new file mode 100644 index 0000000..ff088bc --- /dev/null +++ b/Parallel.Server/Requests/BaseRequest.cs @@ -0,0 +1,28 @@ +// Copyright 2025 Kyle Ebbinga + +using Parallel.Core.Net.Sockets; +using Parallel.Server.Responses; + +namespace Parallel.Server.Requests +{ + /// + /// The base implementation for an + /// + public abstract class BaseRequest : IRequest + { + protected ISocketHandler Handler { get; } + + public abstract Task ExecuteAsync(); + + public virtual void Dispose() + { + Handler.Close(); + GC.SuppressFinalize(this); + } + + protected ObjectResponse Success() + { + return new ObjectResponse("Success"); + } + } +} \ No newline at end of file diff --git a/Parallel.Server/Requests/HelpRequest.cs b/Parallel.Server/Requests/HelpRequest.cs new file mode 100644 index 0000000..3fc8ea8 --- /dev/null +++ b/Parallel.Server/Requests/HelpRequest.cs @@ -0,0 +1,17 @@ +// Copyright 2025 Kyle Ebbinga + +using System.ComponentModel; +using Parallel.Core.Net.Sockets; +using Parallel.Server.Responses; + +namespace Parallel.Server.Requests +{ + [Description("Lists all avalible requests to the server.")] + public class HelpRequest : BaseRequest + { + public override Task ExecuteAsync() + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/Parallel.Server/Requests/IRequest.cs b/Parallel.Server/Requests/IRequest.cs new file mode 100644 index 0000000..f6550dc --- /dev/null +++ b/Parallel.Server/Requests/IRequest.cs @@ -0,0 +1,19 @@ +// Copyright 2025 Kyle Ebbinga + +using Parallel.Core.Net; +using Parallel.Core.Net.Sockets; +using Parallel.Server.Responses; + +namespace Parallel.Server.Requests +{ + /// + /// Defines the request class. + /// + public interface IRequest : IDisposable + { + /// + /// Executes a request and responds with an . + /// + Task ExecuteAsync(); + } +} \ No newline at end of file diff --git a/Parallel.Server/Requests/LoginRequest.cs b/Parallel.Server/Requests/LoginRequest.cs new file mode 100644 index 0000000..84cb834 --- /dev/null +++ b/Parallel.Server/Requests/LoginRequest.cs @@ -0,0 +1,24 @@ +// Copyright 2025 Kyle Ebbinga + +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using Parallel.Core.Net.Sockets; +using Parallel.Server.Responses; + +namespace Parallel.Server.Requests +{ + [Description("Logins into the server.")] + public class LoginRequest : BaseRequest + { + [Required] + public string Username { get; set; } + + [Required] + public string Password { get; set; } + + public override Task ExecuteAsync() + { + return Task.FromResult(Success()); + } + } +} \ No newline at end of file diff --git a/Parallel.Server/Requests/PingRequest.cs b/Parallel.Server/Requests/PingRequest.cs new file mode 100644 index 0000000..41f3146 --- /dev/null +++ b/Parallel.Server/Requests/PingRequest.cs @@ -0,0 +1,15 @@ +// Copyright 2025 Kyle Ebbinga + +using Parallel.Core.Net.Sockets; +using Parallel.Server.Responses; + +namespace Parallel.Server.Requests +{ + public class PingRequest : BaseRequest + { + public override Task ExecuteAsync() + { + return Task.FromResult(Success()); + } + } +} \ No newline at end of file diff --git a/Parallel.Server/Responses/IResponse.cs b/Parallel.Server/Responses/IResponse.cs new file mode 100644 index 0000000..dcfff2c --- /dev/null +++ b/Parallel.Server/Responses/IResponse.cs @@ -0,0 +1,8 @@ +// Copyright 2025 Kyle Ebbinga + +namespace Parallel.Server.Responses +{ + public interface IResponse + { + } +} \ No newline at end of file diff --git a/Parallel.Server/Responses/ObjectResponse.cs b/Parallel.Server/Responses/ObjectResponse.cs new file mode 100644 index 0000000..4573ff5 --- /dev/null +++ b/Parallel.Server/Responses/ObjectResponse.cs @@ -0,0 +1,14 @@ +// Copyright 2025 Kyle Ebbinga + +namespace Parallel.Server.Responses +{ + public sealed class ObjectResponse : IResponse + { + public object? Data { get; } + + public ObjectResponse(object? data) + { + Data = data; + } + } +} \ No newline at end of file diff --git a/Parallel.Service/Services/FileBackupService.cs b/Parallel.Server/Services/FileBackupService.cs similarity index 73% rename from Parallel.Service/Services/FileBackupService.cs rename to Parallel.Server/Services/FileBackupService.cs index 7c0c0c1..80ed8ba 100644 --- a/Parallel.Service/Services/FileBackupService.cs +++ b/Parallel.Server/Services/FileBackupService.cs @@ -2,13 +2,13 @@ using Microsoft.Extensions.Hosting; -namespace Parallel.Service.Services +namespace Parallel.Server.Services { public class FileBackupService : BackgroundService { protected override Task ExecuteAsync(CancellationToken stoppingToken) { - throw new NotImplementedException(); + return Task.CompletedTask; } } } \ No newline at end of file diff --git a/Parallel.Service/Services/FileCleanupService.cs b/Parallel.Server/Services/FileCleanupService.cs similarity index 73% rename from Parallel.Service/Services/FileCleanupService.cs rename to Parallel.Server/Services/FileCleanupService.cs index 10d1363..7dcf375 100644 --- a/Parallel.Service/Services/FileCleanupService.cs +++ b/Parallel.Server/Services/FileCleanupService.cs @@ -2,13 +2,13 @@ using Microsoft.Extensions.Hosting; -namespace Parallel.Service.Services +namespace Parallel.Server.Services { public class FileCleanupService : BackgroundService { protected override Task ExecuteAsync(CancellationToken stoppingToken) { - throw new NotImplementedException(); + return Task.CompletedTask; } } } \ No newline at end of file diff --git a/Parallel.Server/Services/TcpRequestService.cs b/Parallel.Server/Services/TcpRequestService.cs new file mode 100644 index 0000000..5f86665 --- /dev/null +++ b/Parallel.Server/Services/TcpRequestService.cs @@ -0,0 +1,88 @@ +// Copyright 2025 Kyle Ebbinga + +using System.Net; +using System.Net.Sockets; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Parallel.Core.Net; +using Parallel.Core.Net.Sockets; +using Parallel.Core.Settings; +using Parallel.Server.Requests; +using Parallel.Server.Responses; + +namespace Parallel.Server.Services +{ + public class TcpRequestService : BackgroundService + { + // Privates + private readonly CancellationTokenSource _exit = new(); + private readonly ILogger _logger; + private readonly Socket _listener = new(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + private readonly ParallelSettings _settings; + private readonly RequestHandler _requests; + private readonly List _requestPool = new List(); + + public TcpRequestService(ILogger logger, ParallelSettings settings, RequestHandler requests) + { + _logger = logger; + _settings = settings; + _requests = requests; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + try + { + // Starts listening for requests over the TCP socket. + IPAddress address = string.IsNullOrEmpty(_settings.Address) ? IPAddress.Any : IPAddress.Parse( _settings.Address); + _listener.Bind(new IPEndPoint(address, _settings.ListenerPort)); + _listener.Listen(5); + } + catch + { + _logger.LogError("Failed to start server! This usually means either the port is already in use or another instance of Parallel is currently running."); + Environment.Exit(1); + } + + // Starts listening for connections + _logger.LogInformation($"Listening for commands on: {_listener.LocalEndPoint}"); + while (!stoppingToken.IsCancellationRequested && !_exit.IsCancellationRequested) + { + _requestPool.RemoveAll(c => c.IsCompleted); + Socket requestSocket = await _listener.AcceptAsync(_exit.Token); + StartHandlingRequests(requestSocket, stoppingToken); + } + } + + private void StartHandlingRequests(Socket socket, CancellationToken token) + { + TcpSocketHandler handler = new(socket); + Task handlerTask = Task.Run(() => AcceptRequestAsync(handler).ContinueWith(t => { t.Dispose(); }, token), token); + _requestPool.Add(handlerTask); + } + + private async Task AcceptRequestAsync(ISocketHandler handler) + { + ServerRequest request = handler.Parse(); + Log.Debug($"Received request '{handler.RawData}' from '{handler.RemoteEndPoint}' ({_requestPool.Count} active request{(_requestPool.Count == 1 ? string.Empty : "s")})"); + IRequest requestInstance = _requests.CreateNew(request); + + IResponse response = await requestInstance.ExecuteAsync(); + await handler.RespondAsync(response); + Log.Debug($"Responding to '{handler.RemoteEndPoint}' with '{JsonConvert.SerializeObject(response)}' ({_requestPool.Count} active request{(_requestPool.Count == 1 ? string.Empty : "s")})"); + } + + public override async Task StopAsync(CancellationToken cancellationToken) + { + // Stops listening for requests. + await _exit.CancelAsync(); + + // Checks if any requests are still being processed. + _requestPool.RemoveAll(c => c.IsCompleted); + if (_requestPool.Count > 0) _logger.LogInformation($"Shutdown received. Still processing {_requestPool.Count} request{(_requestPool.Count == 1 ? string.Empty : "s")}!"); + await Task.WhenAll(_requestPool); + + return base.StopAsync(cancellationToken); + } + } +} \ No newline at end of file diff --git a/Parallel.Service/parallel-red.ico b/Parallel.Server/parallel-red.ico similarity index 100% rename from Parallel.Service/parallel-red.ico rename to Parallel.Server/parallel-red.ico diff --git a/Parallel.Service/Requests/IServiceRequest.cs b/Parallel.Service/Requests/IServiceRequest.cs deleted file mode 100644 index e9b3154..0000000 --- a/Parallel.Service/Requests/IServiceRequest.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright 2025 Kyle Ebbinga - -namespace Parallel.Service.Requests -{ - public interface IServiceRequest - { - - } -} \ No newline at end of file diff --git a/Parallel.Service/Responses/IServiceResponse.cs b/Parallel.Service/Responses/IServiceResponse.cs deleted file mode 100644 index 6841756..0000000 --- a/Parallel.Service/Responses/IServiceResponse.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright 2025 Kyle Ebbinga - -namespace Parallel.Service.Responses -{ - public interface IServiceResponse - { - - } -} \ No newline at end of file diff --git a/Parallel.Service/Services/TcpRequestService.cs b/Parallel.Service/Services/TcpRequestService.cs deleted file mode 100644 index a53ed11..0000000 --- a/Parallel.Service/Services/TcpRequestService.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2025 Kyle Ebbinga - -using Microsoft.Extensions.Hosting; - -namespace Parallel.Service.Services -{ - public class TcpRequestService : BackgroundService - { - protected override Task ExecuteAsync(CancellationToken stoppingToken) - { - throw new NotImplementedException(); - } - } -} \ No newline at end of file diff --git a/Parallel.sln b/Parallel.sln index a81af5f..7601968 100644 --- a/Parallel.sln +++ b/Parallel.sln @@ -1,13 +1,15 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 -VisualStudioVersion = 17.12.35527.113 d17.12 +VisualStudioVersion = 17.12.35527.113 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Parallel.Core", "Parallel.Core\Parallel.Core.csproj", "{1391ED00-10D5-417A-982B-0A9A6A2295D4}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Parallel.Cli", "Parallel.Cli\Parallel.Cli.csproj", "{4BFE65E9-9534-4C85-B59F-1F64A998C76D}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Parallel.Service", "Parallel.Service\Parallel.Service.csproj", "{09C88AEF-288B-4F22-A3CE-04F79F5110F8}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Parallel.Server", "Parallel.Server\Parallel.Server.csproj", "{09C88AEF-288B-4F22-A3CE-04F79F5110F8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Parallel.Core.Net", "Parallel.Core.Net\Parallel.Core.Net.csproj", "{157A0A8F-A393-4577-AD3B-DF5FB49A7331}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -27,6 +29,10 @@ Global {09C88AEF-288B-4F22-A3CE-04F79F5110F8}.Debug|Any CPU.Build.0 = Debug|Any CPU {09C88AEF-288B-4F22-A3CE-04F79F5110F8}.Release|Any CPU.ActiveCfg = Release|Any CPU {09C88AEF-288B-4F22-A3CE-04F79F5110F8}.Release|Any CPU.Build.0 = Release|Any CPU + {157A0A8F-A393-4577-AD3B-DF5FB49A7331}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {157A0A8F-A393-4577-AD3B-DF5FB49A7331}.Debug|Any CPU.Build.0 = Debug|Any CPU + {157A0A8F-A393-4577-AD3B-DF5FB49A7331}.Release|Any CPU.ActiveCfg = Release|Any CPU + {157A0A8F-A393-4577-AD3B-DF5FB49A7331}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 132adb409e4457cc7c13c193fdd6aef89d3d7b9b Mon Sep 17 00:00:00 2001 From: Kyle | Guitar <26614720+TheGuitarleader@users.noreply.github.com> Date: Thu, 10 Jul 2025 17:22:46 -0500 Subject: [PATCH 09/20] Added a temp ping command to debug server connections --- Parallel.Cli/Commands/PingCommand.cs | 25 +++++++++++++++++++++++++ Parallel.Cli/Parallel.Cli.csproj | 12 ++++++++++++ Parallel.Cli/Program.cs | 24 ++++++++++++++++++++++-- 3 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 Parallel.Cli/Commands/PingCommand.cs diff --git a/Parallel.Cli/Commands/PingCommand.cs b/Parallel.Cli/Commands/PingCommand.cs new file mode 100644 index 0000000..47491fc --- /dev/null +++ b/Parallel.Cli/Commands/PingCommand.cs @@ -0,0 +1,25 @@ +// Copyright 2025 Kyle Ebbinga + +using System.CommandLine; +using Parallel.Core.Net; +using Parallel.Core.Net.Connections; + +namespace Parallel.Cli.Commands +{ + public class PingCommand : Command + { + private Argument Address = new("address", "The server address."); + private Argument Port = new("port", "The server port."); + + public PingCommand() : base("ping", "Tests the connection to Parallel.") + { + this.AddArgument(Address); + this.AddArgument(Port); + this.SetHandler(async (address, port) => + { + IConnection connection = new TcpConnection(address, port); + await connection.SendRequestAsync(new ServerRequest("ping", new Dictionary())); + }, Address, Port); + } + } +} \ No newline at end of file diff --git a/Parallel.Cli/Parallel.Cli.csproj b/Parallel.Cli/Parallel.Cli.csproj index 5c561cb..3337169 100644 --- a/Parallel.Cli/Parallel.Cli.csproj +++ b/Parallel.Cli/Parallel.Cli.csproj @@ -16,8 +16,20 @@ + + + + + + + + + + + + diff --git a/Parallel.Cli/Program.cs b/Parallel.Cli/Program.cs index 80f9ae3..2a66d13 100644 --- a/Parallel.Cli/Program.cs +++ b/Parallel.Cli/Program.cs @@ -1,12 +1,32 @@ // Copyright 2025 Kyle Ebbinga +using System.CommandLine; +using System.Reflection; +using Parallel.Core.IO; +using Parallel.Core.Settings; + namespace Parallel.Cli { internal class Program { - static void Main(string[] args) + internal static ParallelSettings Settings = new ParallelSettings(); + + public static async Task Main(string[] args) { - Console.WriteLine("Hello, World!"); + Settings = ParallelSettings.Load(); + string logFile = Path.Combine(PathBuilder.ProgramData, "Logs", $"{DateTime.Now:MM-dd-yyyy hh-mm-ss}.log"); + //Log.Logger = new LoggerConfiguration().MinimumLevel.Debug().WriteTo.File(logFile).CreateLogger(); + Log.Logger = new LoggerConfiguration().MinimumLevel.Debug().WriteTo.Console().CreateLogger(); + + AssemblyName assembly = Assembly.GetExecutingAssembly().GetName(); + Log.Information($"{assembly.Name} [Version {assembly.Version}]"); + + RootCommand rootCommand = new("Parallel file manager - Easily back up and sync massive amounts of files, and save on drive space in the process."); + Type[] types = Assembly.GetExecutingAssembly().GetTypes().Where(t => t.IsSubclassOf(typeof(Command)) && t.IsClass).ToArray(); + foreach (var type in types) rootCommand.AddCommand((Command)Activator.CreateInstance(type)!); + + await rootCommand.InvokeAsync(args); + Settings.Save(); } } } \ No newline at end of file From adef8ddeb0ea2ac7324cb8d4ab149025e9393058 Mon Sep 17 00:00:00 2001 From: Kyle | Guitar <26614720+TheGuitarleader@users.noreply.github.com> Date: Thu, 10 Jul 2025 17:23:01 -0500 Subject: [PATCH 10/20] Fixed formatting --- Parallel.Core/Database/SqliteDatabase.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Parallel.Core/Database/SqliteDatabase.cs b/Parallel.Core/Database/SqliteDatabase.cs index 8d2cbcf..a19e390 100644 --- a/Parallel.Core/Database/SqliteDatabase.cs +++ b/Parallel.Core/Database/SqliteDatabase.cs @@ -36,9 +36,7 @@ public void Initialize() File.SetAttributes(FilePath, File.GetAttributes(FilePath) | FileAttributes.Hidden); // Create tables - RunQuery( - "CREATE TABLE IF NOT EXISTS `files` (`profile` TEXT NOT NULL, `id` TEXT NOT NULL, `name` TEXT NOT NULL, `localpath` TEXT NOT NULL, `remotepath` TEXT NOT NULL, `lastwrite` LONG INTEGER NOT NULL, `lastupdate` LONG INTEGER NOT NULL, `LocalSize` LONG INTEGER NOT NULL, `RemoteSize` LONG INTEGER NOT NULL, `type` TEXT NOT NULL DEFAULT Other CHECK(`type` IN ('Document', 'Photo', 'Music', 'Video', 'Other')), `hidden` INTEGER NOT NULL DEFAULT 0, `readonly` INTEGER NOT NULL DEFAULT 0, `deleted` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`profile`, `id`));"); - + RunQuery("CREATE TABLE IF NOT EXISTS `files` (`profile` TEXT NOT NULL, `id` TEXT NOT NULL, `name` TEXT NOT NULL, `localpath` TEXT NOT NULL, `remotepath` TEXT NOT NULL, `lastwrite` LONG INTEGER NOT NULL, `lastupdate` LONG INTEGER NOT NULL, `LocalSize` LONG INTEGER NOT NULL, `RemoteSize` LONG INTEGER NOT NULL, `type` TEXT NOT NULL DEFAULT Other CHECK(`type` IN ('Document', 'Photo', 'Music', 'Video', 'Other')), `hidden` INTEGER NOT NULL DEFAULT 0, `readonly` INTEGER NOT NULL DEFAULT 0, `deleted` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`profile`, `id`));"); RunQuery("CREATE TABLE IF NOT EXISTS `history` (`profile` TEXT NOT NULL, `timestamp` LONG INTEGER NOT NULL, `id` TEXT NOT NULL, `path` TEXT NOT NULL, `type` TEXT NOT NULL, PRIMARY KEY(`profile`, `timestamp`));"); } @@ -114,7 +112,9 @@ public long Ping() /// public bool AddFile(SystemFile file) { - return RunQuery($"INSERT OR REPLACE INTO files (profile, id, name, localpath, remotepath, lastwrite, lastupdate, LocalSize, RemoteSize, type, hidden, readonly, deleted) VALUES(\"{ProfileId}\", \"{file.Id}\", \"{file.Name}\", \"{file.LocalPath}\", \"{file.RemotePath}\", {file.LastWrite.TotalMilliseconds}, {file.LastUpdate.TotalMilliseconds}, {file.LocalSize}, {file.RemoteSize}, \"{file.Type.ToString()}\", {Converter.ToInt32(file.IsHidden)}, {Converter.ToInt32(file.IsReadOnly)}, {Converter.ToInt32(file.IsDeleted)});") > 0; + return RunQuery( + $"INSERT OR REPLACE INTO files (profile, id, name, localpath, remotepath, lastwrite, lastupdate, LocalSize, RemoteSize, type, hidden, readonly, deleted) VALUES(\"{ProfileId}\", \"{file.Id}\", \"{file.Name}\", \"{file.LocalPath}\", \"{file.RemotePath}\", {file.LastWrite.TotalMilliseconds}, {file.LastUpdate.TotalMilliseconds}, {file.LocalSize}, {file.RemoteSize}, \"{file.Type.ToString()}\", {Converter.ToInt32(file.IsHidden)}, {Converter.ToInt32(file.IsReadOnly)}, {Converter.ToInt32(file.IsDeleted)});") > + 0; } /// From 7dfae428111d6947969af8210ba8183b8d85bf86 Mon Sep 17 00:00:00 2001 From: Kyle | Guitar <26614720+TheGuitarleader@users.noreply.github.com> Date: Wed, 23 Jul 2025 22:06:03 -0500 Subject: [PATCH 11/20] Renamed Parallel Server to Parallel Service --- Parallel.Service/Parallel.Service.csproj | 40 ++++++++ Parallel.Service/Program.cs | 68 ++++++++++++++ Parallel.Service/RequestHandler.cs | 71 ++++++++++++++ Parallel.Service/Requests/BaseRequest.cs | 28 ++++++ Parallel.Service/Requests/HelpRequest.cs | 17 ++++ Parallel.Service/Requests/IRequest.cs | 19 ++++ Parallel.Service/Requests/LoginRequest.cs | 24 +++++ Parallel.Service/Requests/PingRequest.cs | 15 +++ Parallel.Service/Responses/IResponse.cs | 8 ++ Parallel.Service/Responses/ObjectResponse.cs | 14 +++ .../Services/FileBackupService.cs | 14 +++ .../Services/FileCleanupService.cs | 14 +++ .../Services/TcpRequestService.cs | 88 ++++++++++++++++++ Parallel.Service/parallel-red.ico | Bin 0 -> 180837 bytes 14 files changed, 420 insertions(+) create mode 100644 Parallel.Service/Parallel.Service.csproj create mode 100644 Parallel.Service/Program.cs create mode 100644 Parallel.Service/RequestHandler.cs create mode 100644 Parallel.Service/Requests/BaseRequest.cs create mode 100644 Parallel.Service/Requests/HelpRequest.cs create mode 100644 Parallel.Service/Requests/IRequest.cs create mode 100644 Parallel.Service/Requests/LoginRequest.cs create mode 100644 Parallel.Service/Requests/PingRequest.cs create mode 100644 Parallel.Service/Responses/IResponse.cs create mode 100644 Parallel.Service/Responses/ObjectResponse.cs create mode 100644 Parallel.Service/Services/FileBackupService.cs create mode 100644 Parallel.Service/Services/FileCleanupService.cs create mode 100644 Parallel.Service/Services/TcpRequestService.cs create mode 100644 Parallel.Service/parallel-red.ico diff --git a/Parallel.Service/Parallel.Service.csproj b/Parallel.Service/Parallel.Service.csproj new file mode 100644 index 0000000..bb63011 --- /dev/null +++ b/Parallel.Service/Parallel.Service.csproj @@ -0,0 +1,40 @@ + + + + Exe + net9.0 + enable + parallel-red.ico + enable + ParallelService + 1.0.0 + Kyle Ebbinga + Copyright $(Company). All Rights Reserved. + $(AssemblyVersion) + $(VersionPrefix)$(AssemblyVersion) + $(Company) + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Parallel.Service/Program.cs b/Parallel.Service/Program.cs new file mode 100644 index 0000000..003d2c9 --- /dev/null +++ b/Parallel.Service/Program.cs @@ -0,0 +1,68 @@ +// Copyright 2025 Kyle Ebbinga + +using System.Reflection; +using System.Runtime.InteropServices; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Parallel.Core.IO; +using Parallel.Core.Settings; +using Parallel.Core.Utils; +using Parallel.Service.Requests; +using Parallel.Service.Services; + +namespace Parallel.Service +{ + internal class Program + { + private static string _logFile = Path.Combine(PathBuilder.ProgramData, "Logs", "latest.txt"); + + static async Task Main(string[] args) + { + HostApplicationBuilder builder = Host.CreateApplicationBuilder(args); + + // Logging + Log.Logger = new LoggerConfiguration().MinimumLevel.Debug().WriteTo.Console().WriteTo.File(_logFile).CreateLogger(); + builder.Logging.ClearProviders(); + builder.Logging.AddSerilog(); + + AssemblyName assembly = Assembly.GetExecutingAssembly().GetName(); + Log.Information($"{assembly.Name} v{assembly.Version}"); + + // Add Windows services + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + builder.Services.AddWindowsService(); + } + + // Add Linux systemd + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + builder.Services.AddSystemd(); + } + + // Background services + builder.Services.AddHostedService(); + builder.Services.AddHostedService(); + builder.Services.AddHostedService(); + + // Other services + builder.Services.AddSingleton(ParallelSettings.Load()); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + + IHost host = builder.Build(); + IHostApplicationLifetime lifetime = host.Services.GetRequiredService(); + ParallelSettings settings = host.Services.GetRequiredService(); + lifetime.ApplicationStopped.Register(() => + { + settings.Save(); + Log.CloseAndFlush(); + File.Move(_logFile, Path.Combine(PathBuilder.ProgramData, "Logs", $"{DateTime.Now:MM-dd-yyyy hh-mm-ss}.log")); + }); + + // Starts the application + await host.RunAsync(); + } + } +} \ No newline at end of file diff --git a/Parallel.Service/RequestHandler.cs b/Parallel.Service/RequestHandler.cs new file mode 100644 index 0000000..9a08f61 --- /dev/null +++ b/Parallel.Service/RequestHandler.cs @@ -0,0 +1,71 @@ +// Copyright 2025 Kyle Ebbinga + +using System.ComponentModel.DataAnnotations; +using Parallel.Core.Net; +using Parallel.Service.Requests; + +namespace Parallel.Service +{ + public class RequestHandler + { + private readonly Dictionary _requests; + + public RequestHandler() + { + Type[] types = AppDomain.CurrentDomain.GetAssemblies().SelectMany(a => a.GetTypes()).Where(t => typeof(BaseRequest).IsAssignableFrom(t) && !t.IsAbstract).ToArray(); + _requests = types.ToDictionary(t => t.Name.Replace("Request", ""), t => t, StringComparer.OrdinalIgnoreCase); + + // Logs if all requests registered. + if (_requests.Count == types.Length) + { + Log.Information($"Successfully registered all {types.Length} requests"); + } + else + { + int remaining = types.Length - _requests.Count; + Log.Warning($"Failed to register {remaining} requests"); + } + } + + /// + /// Creates an to be handled. + /// + /// The name of the request. + /// The corresponding . If none was found a help request will be returned. + public IRequest CreateNew(ServerRequest request) + { + if (!_requests.TryGetValue(request.Name, out Type? requestType)) + { + Log.Warning($"Unknown command: {request.Name}"); + throw new InvalidOperationException($"Unknown command: {request.Name}"); + } + + // Instantiate the request object + object? instance = Activator.CreateInstance(requestType); + if (instance is not IRequest requestInstance) + throw new InvalidOperationException($"Type '{requestType.Name}' does not implement IRequest."); + + // Map parameters to object properties + foreach (var prop in requestType.GetProperties()) + { + if (request.Parameters.TryGetValue(prop.Name, out var value)) + { + var converted = Convert.ChangeType(value, prop.PropertyType); + prop.SetValue(instance, converted); + } + } + + // Validate required properties + var validationResults = new List(); + var context = new ValidationContext(instance, serviceProvider: null, items: null); + if (!Validator.TryValidateObject(instance, context, validationResults, validateAllProperties: true)) + { + var errors = string.Join("; ", validationResults.Select(r => r.ErrorMessage)); + Log.Warning($"Validation failed for '{request.Name}': {errors}"); + throw new InvalidOperationException($"Validation failed: {errors}"); + } + + return requestInstance; + } + } +} \ No newline at end of file diff --git a/Parallel.Service/Requests/BaseRequest.cs b/Parallel.Service/Requests/BaseRequest.cs new file mode 100644 index 0000000..52dfe05 --- /dev/null +++ b/Parallel.Service/Requests/BaseRequest.cs @@ -0,0 +1,28 @@ +// Copyright 2025 Kyle Ebbinga + +using Parallel.Core.Net.Sockets; +using Parallel.Service.Responses; + +namespace Parallel.Service.Requests +{ + /// + /// The base implementation for an + /// + public abstract class BaseRequest : IRequest + { + protected ISocketHandler Handler { get; } + + public abstract Task ExecuteAsync(); + + public virtual void Dispose() + { + Handler.Close(); + GC.SuppressFinalize(this); + } + + protected ObjectResponse Success() + { + return new ObjectResponse("Success"); + } + } +} \ No newline at end of file diff --git a/Parallel.Service/Requests/HelpRequest.cs b/Parallel.Service/Requests/HelpRequest.cs new file mode 100644 index 0000000..18c28f4 --- /dev/null +++ b/Parallel.Service/Requests/HelpRequest.cs @@ -0,0 +1,17 @@ +// Copyright 2025 Kyle Ebbinga + +using System.ComponentModel; +using Parallel.Core.Net.Sockets; +using Parallel.Service.Responses; + +namespace Parallel.Service.Requests +{ + [Description("Lists all avalible requests to the server.")] + public class HelpRequest : BaseRequest + { + public override Task ExecuteAsync() + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/Parallel.Service/Requests/IRequest.cs b/Parallel.Service/Requests/IRequest.cs new file mode 100644 index 0000000..88e6286 --- /dev/null +++ b/Parallel.Service/Requests/IRequest.cs @@ -0,0 +1,19 @@ +// Copyright 2025 Kyle Ebbinga + +using Parallel.Core.Net; +using Parallel.Core.Net.Sockets; +using Parallel.Service.Responses; + +namespace Parallel.Service.Requests +{ + /// + /// Defines the request class. + /// + public interface IRequest : IDisposable + { + /// + /// Executes a request and responds with an . + /// + Task ExecuteAsync(); + } +} \ No newline at end of file diff --git a/Parallel.Service/Requests/LoginRequest.cs b/Parallel.Service/Requests/LoginRequest.cs new file mode 100644 index 0000000..8972719 --- /dev/null +++ b/Parallel.Service/Requests/LoginRequest.cs @@ -0,0 +1,24 @@ +// Copyright 2025 Kyle Ebbinga + +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using Parallel.Core.Net.Sockets; +using Parallel.Service.Responses; + +namespace Parallel.Service.Requests +{ + [Description("Logins into the server.")] + public class LoginRequest : BaseRequest + { + [Required] + public string Username { get; set; } + + [Required] + public string Password { get; set; } + + public override Task ExecuteAsync() + { + return Task.FromResult(Success()); + } + } +} \ No newline at end of file diff --git a/Parallel.Service/Requests/PingRequest.cs b/Parallel.Service/Requests/PingRequest.cs new file mode 100644 index 0000000..643ef2a --- /dev/null +++ b/Parallel.Service/Requests/PingRequest.cs @@ -0,0 +1,15 @@ +// Copyright 2025 Kyle Ebbinga + +using Parallel.Core.Net.Sockets; +using Parallel.Service.Responses; + +namespace Parallel.Service.Requests +{ + public class PingRequest : BaseRequest + { + public override Task ExecuteAsync() + { + return Task.FromResult(Success()); + } + } +} \ No newline at end of file diff --git a/Parallel.Service/Responses/IResponse.cs b/Parallel.Service/Responses/IResponse.cs new file mode 100644 index 0000000..84473ba --- /dev/null +++ b/Parallel.Service/Responses/IResponse.cs @@ -0,0 +1,8 @@ +// Copyright 2025 Kyle Ebbinga + +namespace Parallel.Service.Responses +{ + public interface IResponse + { + } +} \ No newline at end of file diff --git a/Parallel.Service/Responses/ObjectResponse.cs b/Parallel.Service/Responses/ObjectResponse.cs new file mode 100644 index 0000000..20ff8ce --- /dev/null +++ b/Parallel.Service/Responses/ObjectResponse.cs @@ -0,0 +1,14 @@ +// Copyright 2025 Kyle Ebbinga + +namespace Parallel.Service.Responses +{ + public sealed class ObjectResponse : IResponse + { + public object? Data { get; } + + public ObjectResponse(object? data) + { + Data = data; + } + } +} \ No newline at end of file diff --git a/Parallel.Service/Services/FileBackupService.cs b/Parallel.Service/Services/FileBackupService.cs new file mode 100644 index 0000000..2c91ca5 --- /dev/null +++ b/Parallel.Service/Services/FileBackupService.cs @@ -0,0 +1,14 @@ +// Copyright 2025 Kyle Ebbinga + +using Microsoft.Extensions.Hosting; + +namespace Parallel.Service.Services +{ + public class FileBackupService : BackgroundService + { + protected override Task ExecuteAsync(CancellationToken stoppingToken) + { + return Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/Parallel.Service/Services/FileCleanupService.cs b/Parallel.Service/Services/FileCleanupService.cs new file mode 100644 index 0000000..e1abcc1 --- /dev/null +++ b/Parallel.Service/Services/FileCleanupService.cs @@ -0,0 +1,14 @@ +// Copyright 2025 Kyle Ebbinga + +using Microsoft.Extensions.Hosting; + +namespace Parallel.Service.Services +{ + public class FileCleanupService : BackgroundService + { + protected override Task ExecuteAsync(CancellationToken stoppingToken) + { + return Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/Parallel.Service/Services/TcpRequestService.cs b/Parallel.Service/Services/TcpRequestService.cs new file mode 100644 index 0000000..a69ca91 --- /dev/null +++ b/Parallel.Service/Services/TcpRequestService.cs @@ -0,0 +1,88 @@ +// Copyright 2025 Kyle Ebbinga + +using System.Net; +using System.Net.Sockets; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Parallel.Core.Net; +using Parallel.Core.Net.Sockets; +using Parallel.Core.Settings; +using Parallel.Service.Requests; +using Parallel.Service.Responses; + +namespace Parallel.Service.Services +{ + public class TcpRequestService : BackgroundService + { + // Privates + private readonly CancellationTokenSource _exit = new(); + private readonly ILogger _logger; + private readonly Socket _listener = new(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + private readonly ParallelSettings _settings; + private readonly RequestHandler _requests; + private readonly List _requestPool = new List(); + + public TcpRequestService(ILogger logger, ParallelSettings settings, RequestHandler requests) + { + _logger = logger; + _settings = settings; + _requests = requests; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + try + { + // Starts listening for requests over the TCP socket. + IPAddress address = string.IsNullOrEmpty(_settings.Address) ? IPAddress.Any : IPAddress.Parse( _settings.Address); + _listener.Bind(new IPEndPoint(address, _settings.ListenerPort)); + _listener.Listen(5); + } + catch + { + _logger.LogError("Failed to start server! This usually means either the port is already in use or another instance of Parallel is currently running."); + Environment.Exit(1); + } + + // Starts listening for connections + _logger.LogInformation($"Listening for commands on: {_listener.LocalEndPoint}"); + while (!stoppingToken.IsCancellationRequested && !_exit.IsCancellationRequested) + { + _requestPool.RemoveAll(c => c.IsCompleted); + Socket requestSocket = await _listener.AcceptAsync(_exit.Token); + StartHandlingRequests(requestSocket, stoppingToken); + } + } + + private void StartHandlingRequests(Socket socket, CancellationToken token) + { + TcpSocketHandler handler = new(socket); + Task handlerTask = Task.Run(() => AcceptRequestAsync(handler).ContinueWith(t => { t.Dispose(); }, token), token); + _requestPool.Add(handlerTask); + } + + private async Task AcceptRequestAsync(ISocketHandler handler) + { + ServerRequest request = handler.Parse(); + Log.Debug($"Received request '{handler.RawData}' from '{handler.RemoteEndPoint}' ({_requestPool.Count} active request{(_requestPool.Count == 1 ? string.Empty : "s")})"); + IRequest requestInstance = _requests.CreateNew(request); + + IResponse response = await requestInstance.ExecuteAsync(); + await handler.RespondAsync(response); + Log.Debug($"Responding to '{handler.RemoteEndPoint}' with '{JsonConvert.SerializeObject(response)}' ({_requestPool.Count} active request{(_requestPool.Count == 1 ? string.Empty : "s")})"); + } + + public override async Task StopAsync(CancellationToken cancellationToken) + { + // Stops listening for requests. + await _exit.CancelAsync(); + + // Checks if any requests are still being processed. + _requestPool.RemoveAll(c => c.IsCompleted); + if (_requestPool.Count > 0) _logger.LogInformation($"Shutdown received. Still processing {_requestPool.Count} request{(_requestPool.Count == 1 ? string.Empty : "s")}!"); + await Task.WhenAll(_requestPool); + + return base.StopAsync(cancellationToken); + } + } +} \ No newline at end of file diff --git a/Parallel.Service/parallel-red.ico b/Parallel.Service/parallel-red.ico new file mode 100644 index 0000000000000000000000000000000000000000..27a395dd3aeaddc603993a067aef2d4f5e755748 GIT binary patch literal 180837 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

tYWpos4X*=^( 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 Date: Wed, 23 Jul 2025 22:06:44 -0500 Subject: [PATCH 12/20] Can now send and receive requests --- Parallel.Cli/Commands/PingCommand.cs | 9 +- .../Connections/TcpConnection.cs | 1 + Parallel.Core.Net/Parallel.Core.Net.csproj | 5 + Parallel.Core.Net/Sockets/TcpSocketHandler.cs | 8 +- Parallel.Core/Parallel.Core.csproj | 15 +-- .../Attributes/DescriptionAttribute.cs | 12 --- .../Attributes/RequiredAttribute.cs | 7 -- Parallel.Server/Parallel.Server.csproj | 40 -------- Parallel.Server/Program.cs | 68 -------------- Parallel.Server/RequestHandler.cs | 71 -------------- Parallel.Server/Requests/BaseRequest.cs | 28 ------ Parallel.Server/Requests/HelpRequest.cs | 17 ---- Parallel.Server/Requests/IRequest.cs | 19 ---- Parallel.Server/Requests/LoginRequest.cs | 24 ----- Parallel.Server/Requests/PingRequest.cs | 15 --- Parallel.Server/Responses/IResponse.cs | 8 -- Parallel.Server/Responses/ObjectResponse.cs | 14 --- Parallel.Server/Services/FileBackupService.cs | 14 --- .../Services/FileCleanupService.cs | 14 --- Parallel.Server/Services/TcpRequestService.cs | 88 ------------------ Parallel.Server/parallel-red.ico | Bin 180837 -> 0 bytes Parallel.sln | 12 +-- 22 files changed, 26 insertions(+), 463 deletions(-) delete mode 100644 Parallel.Server/Attributes/DescriptionAttribute.cs delete mode 100644 Parallel.Server/Attributes/RequiredAttribute.cs delete mode 100644 Parallel.Server/Parallel.Server.csproj delete mode 100644 Parallel.Server/Program.cs delete mode 100644 Parallel.Server/RequestHandler.cs delete mode 100644 Parallel.Server/Requests/BaseRequest.cs delete mode 100644 Parallel.Server/Requests/HelpRequest.cs delete mode 100644 Parallel.Server/Requests/IRequest.cs delete mode 100644 Parallel.Server/Requests/LoginRequest.cs delete mode 100644 Parallel.Server/Requests/PingRequest.cs delete mode 100644 Parallel.Server/Responses/IResponse.cs delete mode 100644 Parallel.Server/Responses/ObjectResponse.cs delete mode 100644 Parallel.Server/Services/FileBackupService.cs delete mode 100644 Parallel.Server/Services/FileCleanupService.cs delete mode 100644 Parallel.Server/Services/TcpRequestService.cs delete mode 100644 Parallel.Server/parallel-red.ico diff --git a/Parallel.Cli/Commands/PingCommand.cs b/Parallel.Cli/Commands/PingCommand.cs index 47491fc..d64a50d 100644 --- a/Parallel.Cli/Commands/PingCommand.cs +++ b/Parallel.Cli/Commands/PingCommand.cs @@ -3,18 +3,19 @@ using System.CommandLine; using Parallel.Core.Net; using Parallel.Core.Net.Connections; +using Parallel.Core.Settings; namespace Parallel.Cli.Commands { public class PingCommand : Command { - private Argument Address = new("address", "The server address."); - private Argument Port = new("port", "The server port."); + private Option Address = new(["--host", "-h"], "The server address."); + private Option Port = new(["--port", "-p"], "The server port."); public PingCommand() : base("ping", "Tests the connection to Parallel.") { - this.AddArgument(Address); - this.AddArgument(Port); + this.AddOption(Address); + this.AddOption(Port); this.SetHandler(async (address, port) => { IConnection connection = new TcpConnection(address, port); diff --git a/Parallel.Core.Net/Connections/TcpConnection.cs b/Parallel.Core.Net/Connections/TcpConnection.cs index aa1ce43..1407803 100644 --- a/Parallel.Core.Net/Connections/TcpConnection.cs +++ b/Parallel.Core.Net/Connections/TcpConnection.cs @@ -60,6 +60,7 @@ public async Task SendRequestAsync(ServerRequest request) { while (!returnedData.EndsWith(';')) { + Console.WriteLine("Waiting for response..."); byte[] buffer = new byte[socket.ReceiveBufferSize]; int bytesRead = ns.Read(buffer, 0, socket.ReceiveBufferSize); returnedData += Encoding.UTF8.GetString(buffer, 0, bytesRead); diff --git a/Parallel.Core.Net/Parallel.Core.Net.csproj b/Parallel.Core.Net/Parallel.Core.Net.csproj index 9d41196..bf55236 100644 --- a/Parallel.Core.Net/Parallel.Core.Net.csproj +++ b/Parallel.Core.Net/Parallel.Core.Net.csproj @@ -10,4 +10,9 @@ + + + + + diff --git a/Parallel.Core.Net/Sockets/TcpSocketHandler.cs b/Parallel.Core.Net/Sockets/TcpSocketHandler.cs index c792447..de68a70 100644 --- a/Parallel.Core.Net/Sockets/TcpSocketHandler.cs +++ b/Parallel.Core.Net/Sockets/TcpSocketHandler.cs @@ -47,18 +47,13 @@ public void Close() { using (NetworkStream ns = new(Socket)) { - Console.WriteLine("1"); while (!RawData.EndsWith(';')) { - Console.WriteLine("2"); byte[] buffer = new byte[Socket.ReceiveBufferSize]; int bytesRead = ns.Read(buffer, 0, Socket.ReceiveBufferSize); RawData += Encoding.UTF8.GetString(buffer, 0, bytesRead); Log.Debug(RawData); - Console.WriteLine("3"); } - - Console.WriteLine("4"); } return JsonConvert.DeserializeObject(RawData.TrimEnd(';')); @@ -69,8 +64,7 @@ public Task RespondAsync(object? data) try { string json = JsonConvert.SerializeObject(data, Formatting.Indented); - byte[] bytes = Encoding.ASCII.GetBytes(Encryption.Encode(json)); - Socket?.Send(bytes); + Socket?.Send(Encoding.UTF8.GetBytes(json + ";")); Close(); return Task.CompletedTask; } diff --git a/Parallel.Core/Parallel.Core.csproj b/Parallel.Core/Parallel.Core.csproj index 5d2cb54..aece14c 100644 --- a/Parallel.Core/Parallel.Core.csproj +++ b/Parallel.Core/Parallel.Core.csproj @@ -16,11 +16,11 @@ - - - - - + + + + + @@ -33,11 +33,12 @@ - + + - + diff --git a/Parallel.Server/Attributes/DescriptionAttribute.cs b/Parallel.Server/Attributes/DescriptionAttribute.cs deleted file mode 100644 index dc4fddd..0000000 --- a/Parallel.Server/Attributes/DescriptionAttribute.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright 2025 Kyle Ebbinga - -namespace Parallel.Server.Attributes -{ - [AttributeUsage(AttributeTargets.Class)] - public class DescriptionAttribute : Attribute - { - public string Text { get; } - - public DescriptionAttribute(string text) => Text = text; - } -} \ No newline at end of file diff --git a/Parallel.Server/Attributes/RequiredAttribute.cs b/Parallel.Server/Attributes/RequiredAttribute.cs deleted file mode 100644 index 017283f..0000000 --- a/Parallel.Server/Attributes/RequiredAttribute.cs +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright 2025 Kyle Ebbinga - -namespace Parallel.Server.Attributes -{ - [AttributeUsage(AttributeTargets.Property)] - public class RequiredAttribute : Attribute { } -} \ No newline at end of file diff --git a/Parallel.Server/Parallel.Server.csproj b/Parallel.Server/Parallel.Server.csproj deleted file mode 100644 index b43b230..0000000 --- a/Parallel.Server/Parallel.Server.csproj +++ /dev/null @@ -1,40 +0,0 @@ - - - - Exe - net9.0 - enable - parallel-red.ico - enable - ParallelServer - 1.0.0 - Kyle Ebbinga - Copyright $(Company). All Rights Reserved. - $(AssemblyVersion) - $(VersionPrefix)$(AssemblyVersion) - $(Company) - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Parallel.Server/Program.cs b/Parallel.Server/Program.cs deleted file mode 100644 index 8cd23fe..0000000 --- a/Parallel.Server/Program.cs +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright 2025 Kyle Ebbinga - -using System.Reflection; -using System.Runtime.InteropServices; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using Parallel.Core.IO; -using Parallel.Core.Settings; -using Parallel.Core.Utils; -using Parallel.Server.Requests; -using Parallel.Server.Services; - -namespace Parallel.Server -{ - internal class Program - { - private static string _logFile = Path.Combine(PathBuilder.ProgramData, "Logs", "latest.txt"); - - static async Task Main(string[] args) - { - HostApplicationBuilder builder = Host.CreateApplicationBuilder(args); - - // Logging - Log.Logger = new LoggerConfiguration().MinimumLevel.Debug().WriteTo.Console().WriteTo.File(_logFile).CreateLogger(); - builder.Logging.ClearProviders(); - builder.Logging.AddSerilog(); - - AssemblyName assembly = Assembly.GetExecutingAssembly().GetName(); - Log.Information($"{assembly.Name} v{assembly.Version}"); - - // Add Windows services - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - builder.Services.AddWindowsService(); - } - - // Add Linux systemd - if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - { - builder.Services.AddSystemd(); - } - - // Background services - builder.Services.AddHostedService(); - builder.Services.AddHostedService(); - builder.Services.AddHostedService(); - - // Other services - builder.Services.AddSingleton(ParallelSettings.Load()); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - - IHost host = builder.Build(); - IHostApplicationLifetime lifetime = host.Services.GetRequiredService(); - ParallelSettings settings = host.Services.GetRequiredService(); - lifetime.ApplicationStopped.Register(() => - { - settings.Save(); - Log.CloseAndFlush(); - File.Move(_logFile, Path.Combine(PathBuilder.ProgramData, "Logs", $"{DateTime.Now:MM-dd-yyyy hh-mm-ss}.log")); - }); - - // Starts the application - await host.RunAsync(); - } - } -} \ No newline at end of file diff --git a/Parallel.Server/RequestHandler.cs b/Parallel.Server/RequestHandler.cs deleted file mode 100644 index 402db85..0000000 --- a/Parallel.Server/RequestHandler.cs +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright 2025 Kyle Ebbinga - -using System.ComponentModel.DataAnnotations; -using Parallel.Core.Net; -using Parallel.Server.Requests; - -namespace Parallel.Server -{ - public class RequestHandler - { - private readonly Dictionary _requests; - - public RequestHandler() - { - Type[] types = AppDomain.CurrentDomain.GetAssemblies().SelectMany(a => a.GetTypes()).Where(t => typeof(BaseRequest).IsAssignableFrom(t) && !t.IsAbstract).ToArray(); - _requests = types.ToDictionary(t => t.Name.Replace("Request", ""), t => t, StringComparer.OrdinalIgnoreCase); - - // Logs if all requests registered. - if (_requests.Count == types.Length) - { - Log.Information($"Successfully registered all {types.Length} requests"); - } - else - { - int remaining = types.Length - _requests.Count; - Log.Warning($"Failed to register {remaining} requests"); - } - } - - ///

- /// Creates an to be handled. - /// - /// The name of the request. - /// The corresponding . If none was found a help request will be returned. - public IRequest CreateNew(ServerRequest request) - { - if (!_requests.TryGetValue(request.Name, out Type? requestType)) - { - Log.Warning($"Unknown command: {request.Name}"); - throw new InvalidOperationException($"Unknown command: {request.Name}"); - } - - // Instantiate the request object - object? instance = Activator.CreateInstance(requestType); - if (instance is not IRequest requestInstance) - throw new InvalidOperationException($"Type '{requestType.Name}' does not implement IRequest."); - - // Map parameters to object properties - foreach (var prop in requestType.GetProperties()) - { - if (request.Parameters.TryGetValue(prop.Name, out var value)) - { - var converted = Convert.ChangeType(value, prop.PropertyType); - prop.SetValue(instance, converted); - } - } - - // Validate required properties - var validationResults = new List(); - var context = new ValidationContext(instance, serviceProvider: null, items: null); - if (!Validator.TryValidateObject(instance, context, validationResults, validateAllProperties: true)) - { - var errors = string.Join("; ", validationResults.Select(r => r.ErrorMessage)); - Log.Warning($"Validation failed for '{request.Name}': {errors}"); - throw new InvalidOperationException($"Validation failed: {errors}"); - } - - return requestInstance; - } - } -} \ No newline at end of file diff --git a/Parallel.Server/Requests/BaseRequest.cs b/Parallel.Server/Requests/BaseRequest.cs deleted file mode 100644 index ff088bc..0000000 --- a/Parallel.Server/Requests/BaseRequest.cs +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright 2025 Kyle Ebbinga - -using Parallel.Core.Net.Sockets; -using Parallel.Server.Responses; - -namespace Parallel.Server.Requests -{ - /// - /// The base implementation for an - /// - public abstract class BaseRequest : IRequest - { - protected ISocketHandler Handler { get; } - - public abstract Task ExecuteAsync(); - - public virtual void Dispose() - { - Handler.Close(); - GC.SuppressFinalize(this); - } - - protected ObjectResponse Success() - { - return new ObjectResponse("Success"); - } - } -} \ No newline at end of file diff --git a/Parallel.Server/Requests/HelpRequest.cs b/Parallel.Server/Requests/HelpRequest.cs deleted file mode 100644 index 3fc8ea8..0000000 --- a/Parallel.Server/Requests/HelpRequest.cs +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2025 Kyle Ebbinga - -using System.ComponentModel; -using Parallel.Core.Net.Sockets; -using Parallel.Server.Responses; - -namespace Parallel.Server.Requests -{ - [Description("Lists all avalible requests to the server.")] - public class HelpRequest : BaseRequest - { - public override Task ExecuteAsync() - { - throw new NotImplementedException(); - } - } -} \ No newline at end of file diff --git a/Parallel.Server/Requests/IRequest.cs b/Parallel.Server/Requests/IRequest.cs deleted file mode 100644 index f6550dc..0000000 --- a/Parallel.Server/Requests/IRequest.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright 2025 Kyle Ebbinga - -using Parallel.Core.Net; -using Parallel.Core.Net.Sockets; -using Parallel.Server.Responses; - -namespace Parallel.Server.Requests -{ - /// - /// Defines the request class. - /// - public interface IRequest : IDisposable - { - /// - /// Executes a request and responds with an . - /// - Task ExecuteAsync(); - } -} \ No newline at end of file diff --git a/Parallel.Server/Requests/LoginRequest.cs b/Parallel.Server/Requests/LoginRequest.cs deleted file mode 100644 index 84cb834..0000000 --- a/Parallel.Server/Requests/LoginRequest.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2025 Kyle Ebbinga - -using System.ComponentModel; -using System.ComponentModel.DataAnnotations; -using Parallel.Core.Net.Sockets; -using Parallel.Server.Responses; - -namespace Parallel.Server.Requests -{ - [Description("Logins into the server.")] - public class LoginRequest : BaseRequest - { - [Required] - public string Username { get; set; } - - [Required] - public string Password { get; set; } - - public override Task ExecuteAsync() - { - return Task.FromResult(Success()); - } - } -} \ No newline at end of file diff --git a/Parallel.Server/Requests/PingRequest.cs b/Parallel.Server/Requests/PingRequest.cs deleted file mode 100644 index 41f3146..0000000 --- a/Parallel.Server/Requests/PingRequest.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright 2025 Kyle Ebbinga - -using Parallel.Core.Net.Sockets; -using Parallel.Server.Responses; - -namespace Parallel.Server.Requests -{ - public class PingRequest : BaseRequest - { - public override Task ExecuteAsync() - { - return Task.FromResult(Success()); - } - } -} \ No newline at end of file diff --git a/Parallel.Server/Responses/IResponse.cs b/Parallel.Server/Responses/IResponse.cs deleted file mode 100644 index dcfff2c..0000000 --- a/Parallel.Server/Responses/IResponse.cs +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright 2025 Kyle Ebbinga - -namespace Parallel.Server.Responses -{ - public interface IResponse - { - } -} \ No newline at end of file diff --git a/Parallel.Server/Responses/ObjectResponse.cs b/Parallel.Server/Responses/ObjectResponse.cs deleted file mode 100644 index 4573ff5..0000000 --- a/Parallel.Server/Responses/ObjectResponse.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2025 Kyle Ebbinga - -namespace Parallel.Server.Responses -{ - public sealed class ObjectResponse : IResponse - { - public object? Data { get; } - - public ObjectResponse(object? data) - { - Data = data; - } - } -} \ No newline at end of file diff --git a/Parallel.Server/Services/FileBackupService.cs b/Parallel.Server/Services/FileBackupService.cs deleted file mode 100644 index 80ed8ba..0000000 --- a/Parallel.Server/Services/FileBackupService.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2025 Kyle Ebbinga - -using Microsoft.Extensions.Hosting; - -namespace Parallel.Server.Services -{ - public class FileBackupService : BackgroundService - { - protected override Task ExecuteAsync(CancellationToken stoppingToken) - { - return Task.CompletedTask; - } - } -} \ No newline at end of file diff --git a/Parallel.Server/Services/FileCleanupService.cs b/Parallel.Server/Services/FileCleanupService.cs deleted file mode 100644 index 7dcf375..0000000 --- a/Parallel.Server/Services/FileCleanupService.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2025 Kyle Ebbinga - -using Microsoft.Extensions.Hosting; - -namespace Parallel.Server.Services -{ - public class FileCleanupService : BackgroundService - { - protected override Task ExecuteAsync(CancellationToken stoppingToken) - { - return Task.CompletedTask; - } - } -} \ No newline at end of file diff --git a/Parallel.Server/Services/TcpRequestService.cs b/Parallel.Server/Services/TcpRequestService.cs deleted file mode 100644 index 5f86665..0000000 --- a/Parallel.Server/Services/TcpRequestService.cs +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright 2025 Kyle Ebbinga - -using System.Net; -using System.Net.Sockets; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using Parallel.Core.Net; -using Parallel.Core.Net.Sockets; -using Parallel.Core.Settings; -using Parallel.Server.Requests; -using Parallel.Server.Responses; - -namespace Parallel.Server.Services -{ - public class TcpRequestService : BackgroundService - { - // Privates - private readonly CancellationTokenSource _exit = new(); - private readonly ILogger _logger; - private readonly Socket _listener = new(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); - private readonly ParallelSettings _settings; - private readonly RequestHandler _requests; - private readonly List _requestPool = new List(); - - public TcpRequestService(ILogger logger, ParallelSettings settings, RequestHandler requests) - { - _logger = logger; - _settings = settings; - _requests = requests; - } - - protected override async Task ExecuteAsync(CancellationToken stoppingToken) - { - try - { - // Starts listening for requests over the TCP socket. - IPAddress address = string.IsNullOrEmpty(_settings.Address) ? IPAddress.Any : IPAddress.Parse( _settings.Address); - _listener.Bind(new IPEndPoint(address, _settings.ListenerPort)); - _listener.Listen(5); - } - catch - { - _logger.LogError("Failed to start server! This usually means either the port is already in use or another instance of Parallel is currently running."); - Environment.Exit(1); - } - - // Starts listening for connections - _logger.LogInformation($"Listening for commands on: {_listener.LocalEndPoint}"); - while (!stoppingToken.IsCancellationRequested && !_exit.IsCancellationRequested) - { - _requestPool.RemoveAll(c => c.IsCompleted); - Socket requestSocket = await _listener.AcceptAsync(_exit.Token); - StartHandlingRequests(requestSocket, stoppingToken); - } - } - - private void StartHandlingRequests(Socket socket, CancellationToken token) - { - TcpSocketHandler handler = new(socket); - Task handlerTask = Task.Run(() => AcceptRequestAsync(handler).ContinueWith(t => { t.Dispose(); }, token), token); - _requestPool.Add(handlerTask); - } - - private async Task AcceptRequestAsync(ISocketHandler handler) - { - ServerRequest request = handler.Parse(); - Log.Debug($"Received request '{handler.RawData}' from '{handler.RemoteEndPoint}' ({_requestPool.Count} active request{(_requestPool.Count == 1 ? string.Empty : "s")})"); - IRequest requestInstance = _requests.CreateNew(request); - - IResponse response = await requestInstance.ExecuteAsync(); - await handler.RespondAsync(response); - Log.Debug($"Responding to '{handler.RemoteEndPoint}' with '{JsonConvert.SerializeObject(response)}' ({_requestPool.Count} active request{(_requestPool.Count == 1 ? string.Empty : "s")})"); - } - - public override async Task StopAsync(CancellationToken cancellationToken) - { - // Stops listening for requests. - await _exit.CancelAsync(); - - // Checks if any requests are still being processed. - _requestPool.RemoveAll(c => c.IsCompleted); - if (_requestPool.Count > 0) _logger.LogInformation($"Shutdown received. Still processing {_requestPool.Count} request{(_requestPool.Count == 1 ? string.Empty : "s")}!"); - await Task.WhenAll(_requestPool); - - return base.StopAsync(cancellationToken); - } - } -} \ No newline at end of file diff --git a/Parallel.Server/parallel-red.ico b/Parallel.Server/parallel-red.ico deleted file mode 100644 index 27a395dd3aeaddc603993a067aef2d4f5e755748..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 180837 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

tYWpos4X*=^( zBOUS85x#juVzcL)<4X?sV{inGYO49AB6^_p*F~e5^s>z}22R zyC3PJjKdsG7}&Kwzt_!Ytzt6S#PjZa?f7!G%~dwbcIzWWSj-2L`Bc!4h<1nm1B_Uc za4fpVv%p6_o*M801W)^Y)Hl+Q&b6&h=^W^hZcZutBfM~3b4sTYzUy{}kM*N;Ecnb5 zMqptu0h>of7t04mZth+5p@y+`LwTE>KcAxVv+)^3y@tOaOAoq{vWrNs{n`WE{F0lk zPodW5bKDAiO!P!Xwz~8w9yZH%VPO5D2oquRco=$IzCgm^-(Z*aj%VYuMsX5udF4^m z`S?__bw7U0=DPWjo2j2#z<_$Ml;Ow2%9GcLM+~S zf_Li&#^#>j6H{L=kGjYB9Ad8r*POhWYr{SItT+{AxtbbPhWvgmoM|k^AzWY z_tZB~wa&pdWm0=ow7*dFID75Rf|t(u*4fUq0gSIBJ$nrK52DAn>w}M=4Gc|56=k zIANVolK4v3j)s!2+VAcA?CI%M`Mo3gN1F~z9LHcoXKW#7pxy@W?9PL3FQ0R^OHYTN z)?J!%$@MArRb2-^{AL+{ST~xmw&zcwAi;&Ut#2IgJC@BZa>h`OP!73j@(Y_Q*`6 zYz@Gc8nC+3gxP0)oZeg)vVQ#{bjo}NcXr`b1`B&;_;Xe5OqaILR9{n2Qr|b6~-xnyy57PD;fh|4pnH8`F z41|T%RoJ}9Qoa}2!*w#ca>jWUXO(x1!#V2m%W2NB7e*rOGn+t$jTM&amJQ=?o-S6-^5xY<3Gyq1Dltj zmppuW>Zi-+DL>Tm)uQ@!5%jXT=H@7RYXR_gXCR)sz`*D;n?cWoh?0i-Q zT2;V(F>l! zKv>v(7B-8igO-5#+1Swoe@SV6s_N<*(2KsP@%6OC)A=>(x(f{M)*sj-gumuJdzJF` zJmv17KFF@GuuPe8XO&jdxiu`33o& zi+)QX`8^U2%V!ojv$w#nxl{S$4m)!tA{FmWxslKk~B)?CIxO~Ov{SgMDH+bjf)5aMFU&$Rq*qedp zZ_?KIZLj1R9qVhU+wmWVAL90WU!z?8kUc2Ey$7}!+uQkL1Dn@qLvv1MVbG6MX8%)N z`>ImEN4{j#PL@9%;UK?ZFO!!Cv%fk;U8HZE%9nj6zgvZcvngAHutk2QdLtKmif@4D z+wJ(4m0r;+p2p7P- z-S{KRHU7xt??cbt;oW$ScJwIZ_QuDX@WgLg`whz$cgiCe2n%82 zVZ->w8=vLmyhOX|5bPiy@eS2Gs^cBH2Hx*O?|Sq;KHh!@Ki^-O{DHJR4~5=fp6xi~ zf=@5xDqV`X+#G!0waI+Xl;fJ3k?_pk9@#DogoQ8xBVjWTS`V?;_e_INBYO6Z@WC8A zwG|hOgM9zoNBuJxo(=R>Z{_(4NEnHh(_WQNyWaRmlb&_dLAz4V>Ahaw%2&R)QpxZ2 zkZ-NQKv;N~fX$nXA->62;yZkw$5URL;n`b!c= z=X&9KCR&AD`Mm4BT=|5PZ#e6-!sn~~=I8bZAL$?rgoVXK*u3pPj77ggnOwm8w-^4x zvhW#}(WjXFcglzFPQ{bt0NdD9q&o}$}^~~ZF$CsY`=4IABlbm3f@}6&_@t>!B5zi0#|5e`WZ^N@Y zzjvxn1{>S$J1;B@CSbDyl0U8Y&ZNHlGAHLX+N9qnufXj`zR(BLkaqP6rjui{zP{?cTt>MC0&3Il_MS8jyO za-XhI{g%u3r@HEJ_{v9GcX*^fSGnJTvZ`^J^^o$@;*^O04182B2Z5o+J@#y+hWg!* z)!YktzI@dm`ZoIVbY0K)=x*cY&{y(Fno6FG+@APx^ZlDK zr^S9Oo0lp>qK%ZPhtP2ccs3f|<@nw!FTXL3ZsLEaES-p}_#NmL>aJT%cuC0=@X@#3 zm${z*-=|;r{Z8y?FnW0YSHH5h3j<*xO#EK~Hws%Rss^WJs^1C=9|QY6I^peOk?AV` za)zV657aLSggSx6t-vb-f*D`Gp zY?kf9Kv)Qq;7{Rd3kknf$MB8(RNu++vKz_sfoZ&hax&f1CTrLQ*$a>@4201}z9%2> zt{(?JYG>;kr|+e{(wR4;LFc21*e!@n+(mu$CCfIKz zZQsPtu_+rrd2RmlTAByYFTE4_A3<$pBo@02Uh;RlH+6&hAzPyiwaJtGF#cr@!XVk2W@W?K5mNwgznP3?GH- z-BTG7Rv+#IkB{_yE+~+1>_d8hQ4?ikKi>TdnQyt+Bc0>+6R?rr#%A<9j_^x}b5R?$ z#`V+m{hElM&-=ayzI!xRa)`0TYja?mZ1l@TXYUnIwEhe({I?p?{pu5;{}jK%!Y632 z9E2RbYw~y49mzjjV;Z%kwSL^vpKrbYrMqnKu)!wTW^D9q)qOUgzJsYA9 z!zY))=um@E*S-pUKXTi{wTXN^)aY*8MzTdV8QTK3a+~$f$43~g$%WSi5Pa8DKA)pM zcnlc%ei*k03^M6p^^>mB*|v*hgKUva#y0Get-bmA>DQ8YtuO-fb(G8X)NijbrgIkW z*x>E7Ea^QSY;?3fk=16D?y|wyf=#}Cy}`y$W3UlE&_>?1b-c6dsMj_y=DUKn?l{`$ zqwsU)`-GGavrKupEyAp9=^~w^n{>48Md>WvWkb*=-_E{V#c!QQbWxmj)cNcAE~=fn zp7;70+I#Y$x})bWF@l#M(2PXvZ+6RK0f|+J?*3Qw5!(h zK5wL-vx>IOU3@=}LdU_zN8X@;^7+``7^L%SF|s66vZaG`vF&l`W^_bXUvK}~pV;`k z#9QUZrwz2THyjJmciBk)4!_#D@3L-XD*Z3@BX;u2lYB&`+tt_qt=~z>kSxiRY};0s zF4D>9hK|0zy2^&F`SWFLKu9Z9b3X=*c&N7hX48)@E_#Ui@jU8+Z&RjrAfKB&yi~4w zQnvi~^4+LBa^gR*t6_u2TYyoM_ctxb$m zs_(UdZ{1SH=VsIAxtMv1Bk2q664yci@k8= z=@#!kJ>NSkC&985EMvj4r?RM@uI^afIkv1WHuts+Z!!0_YzNDH@LjNsGQ2xVY~QIN z+?@t3hFblQj^Da|NDA)k@!Q>JNXZ z?R?6%{$DjQiLEC=^Pqo)Jm0j|pOE!YD-3VMm(G>=8T}_hWE809<#%*K0j%a1#)|pNvn_a<18s z{GTcfd*I+9Ug9azd^=`9a@^+sE7I)u&v45SE!XDZ59`ps| z*)5;6oQshBHMpDzM)FIm`E~rXfpP!&r?viQEl>G8t~0VEQ?jK)*!ah_<-pTXx}HZK zq%zsGy=4UcVjWAK-(l-RI)d&eK(AuQKOp>a`96|wHvVQ+UqqwzU@f1%KIYx|_wDO* z<%eYH{iru}mVDoa@uTMX2hi%Dm6H#S7JwNK@2^B5nrj+4CC!(aZf$&1wFVd#Ai z^qG&jYzwDua!c@2qfyQ2Dshul&@u<@)IT>>d|E?`hD)>UXbLwnz)E}d=zUw z5)d}xt9Sb*>4<+8be8V2A&?KB%SNbPHv9JLy3Kw$WyZ&~FI)K!?FlV{Jm1*UC_gvi z)9Xn5L2LcyHv#wbav@*V?EAq6+0y1O(AX$jWpisD;3p4%U}al98{{9__|Zllb-Vn6 z9|^66@XIHfijVS(X`heq$~krHc3XB-`5TG&Ub841~Qf5jI=xN8XCR zkAGf#n*5HR4*eTs{2=Bf@K+l@8N3^qr>KCU|0JaMx53yZ8$F-p@&PH^Pd8Y2_?tY~ z3h#k@ev1tL3FFuN9P<3nkns=0`tz*OB-v{Wt5kbf%jdV~+ykUKV@G20p?wQ@X9$ z#!FAPJ^imd*wbL(`PG-NS7CFc&N4KAL2EgP^vYaAAkG4k_TWQEaYR?Vgp9P zO7({3$%LV>?CDQ_?TtVGS>Qjb9$Ex(Ho)EZz@I=Jct9)mi8%d6_FR|XyS^lKsdxPq z*I(h!evI)+ZgFJ%D>0Jr3!zKW=u)>lc_KWM-?%&w7Q)1yogj=1R`up=2_MIv;9qtR zX9djm&r^tye{{I-#t-n_Zm8l;wsG{K;guG<7y{RJMix zaL!=3iD!Bqq{-yfmAn`1(P-r>-rGCry~gJ>mXRTY!gV+B^w0UzT#8SV%$Wr7L^Elq_wmE8cf!tv_;KttQl+yT zMwoc*`3GffpZCH<*nl@QTO`bW3Rx_LX*NHd6VY~F#xU}2t@ZPKuUk;@Yt9DH9-_80 zGwj(A)c@dHl)8%X!`5+h?lsm=8Rz`BD87&F85y*plRwWGPv;Kme3tFwEDiEG?d*%L z|JS(*O$Hl=)LgFQ1R9tM+0{ zOK1K0W7Ph2Dsxgg9|Ax98S?m>5+2Ij>b$*Jx;G?)8aT8;NcLv7^&Hwg{k{u171m{ph%y{DTw0QXPWPD3!b#yP!m%BY) zV+J!}Ck#C-&*N(GO~+qnTI4^-(mC>=@PRt$J;-$m@tI$GK3G(H7~jqc@PpfgXhp*U?D&OWJ} zq&|2W>8!?U!L`NkOmJ3AespZG&)yneeR=g4EdJTZ-CpB>2N>$y3UiKy!4`~#wU2w- zHO%=v^B`AeHiW~MQwQVos7L1wagHTttK?PU1E=DiU|*bj&V#I&1g^WmQ)eWpzldL- z4)HY3F_W>y?|AXRwnsW_U-5;Zur%jj2wP$7%nP^SKi|)b5rqA#;^`cSit_K^b0NF) zfP!Z#rBc;yxbhy@+d;-fvj3C?Ofj%T!!Tc7)J?{$mm%Ln;>bMG0O z1WRFR&ms}V^Mp0H``GI`{}TKBQT(IaPWyWx_V?y2DR7Pc!0UU^cP`e-r4o$%!ukGlSf!^q`J*7z+Oxw58p$#dFU?czP)h9Y;&Bd8^x#U3_m6e_3;hJzODi7!UMBb6KI7(Z@nKIQ?)qU2bueS70 z`YMC1J*!Aq3v*%rWDI&0Qoiu5KFY(UFZi>bxco@kT3~O^71Y^+K0SiJ!Am?`H5W{O zA(!^CIj78zqw6DXe!(r2o0fkP{3&Z-D~v7H26M1~inh>GAM2c?oSX;2W0ZF$8}9(; zh4qevE9d9s(4I8s!^!`vPml2)-bx)cz{t@xKYtw0RWJH+bbXZcvzQATnWhirLg&YI zN!gq$*By`E+TK|@9}~4hpN53Fuz&h^=)ZLaRes)`;Iae$)lyLV^Tm4hKVufDL0)~Q z{8qclr^mn*pA_-kks+LP4wigM>YeoC*mhVbG!I`3I?vCZi>5Qq?74HhaP5wCo^}^x z&Z*N`bsedyNU_E0%VqhLz7yxFL=QD(-QuM>&!=a>m2)1e!4-L_)Xw;| zVGPvZ>c`Rb8Nz4qd?(_mUZwnyAKv>L;1^8O*L&*W{AtJ>Vr2Fmhai?IgK^t=d&2%X zoXI_hEerAa{X_f*_U^2|j%N&NoS8FGIG5bMU(b5D>AC=1XVSOD2bJ=>TIY$~V05t9 zw#CiKd4zBI{+?b{=uf*gaNc5i{J#IcfbY{h>bm{<5exrJM9#8ZdEd%UMxM@F6s||1gUY?~Do8I-XO1C`&KS~pf2?`cnb77;Nb&6}1!ej7oQqm~ zDe`o_V_(W(dRm=zrLjr^Sog*5F+|-H8h3s`Xyq{&VZr0$hdR7=1?5TeiVdFx+`G?@^P-n5~ zJfr>~)nhte^LNO41yUaPv>2Jc2UpJWH8QJd8>qiv^QJAH;xDEzaV|Qk-1moGK93ss z{?hPoFVD&>FF&h)Wn!l=7S>kY%hW%M_y!1z=c${%i@$7a?8W)0ouv`K3eKgq_$v>B zw8YqN&l}|&ORY)aj7@unXfWQZl+#v2{V0=jZ^2kt+x%WE?CF0k zG5=Rqo`S62o*CPjjWvAdcBQ|iG0mlr?DT0VxIXCNs=B3#a()NTmj&V|uFjr)j&J#a z{j8tbmjmge%s@uEZj!lxKIFIhQD!ym!Bdc>_sd`|?CAsR{x!zI&g;j#Zp)9l+5z-$ zry%1ENOt%-vMjuzz$LcQ#e?N|YeT_cC)+ir!`1QRSdFi?>rJTOX7|IVhBc#8| zWR1btmOo)G>|dR2-#hs#9*ZpREMlF>+mBS%;S6G7ESq0vZuAt+9ML(l+Lw2nvD-dV z96#QhjM-h?@A^q*G^+2u@>%-$GId2}>96|{hkAptm1XL>+2jS|&#y5S%9+;T$&~*V zaLD%D+ic_Md{&*~_`0!4cD2#l*m$)kQ+1Wjy;lGHtw0>bf17t`CUsO_&P2|nug=vw znDaJ@YxU zT$Qn^)VIM^-{5x)mi9c%x4GAu$Fpc-SX}EkSG%zAUK8FHNAYyd>a%<+_B8KF|CJGq zYYfBo`9?-ZnNK2<^M?D3iRA2TFt+#$bAvr|U`v?`vvqM*YJYpSG4)VGzmd*p9!h)S zA#8llvs3vXdKX+5c(^LxIjgwjx(xB&G4D?T--Zo3)43mYtFP1fx)YH3zNb^0ZaI7d zl8Nv2T_)!&3tN5fJp93YDcCP#4tN>!@3tj^&iZWY`bNgI#vyw-^lpIO z1J|Y4c(jo#+0p13JQoJatM+j#DlY7I{Uo!ZLf`$4@+7~q@|-@)lni03@16IZ0dryh zMh?VU2;u)0b@5naH9$HWwm+#BJI+Oh%2-FLthm6~C_SrEhfz-SeNtWpo2&tPll50`vL@(l>WBxa zD|SX!T_5G8>%|%DNu6b@c7<>a(n=54CL_15b_Dh0lf+rsAznejqv$uRpY^My{?pmF z8gpIIL1tm$!+bl3_CcoV-<=J%_WL5N4d!6~mWZ`*Z?PusZSZ*ooH)OoGnwsspm(7^ zNM|+cjBK@SgsbfIiL=CoD`$`^-*s+!QPEVMuL{Ib+?9O8m(WH!2>nz(`a>`KuGiP^ zKs#VgCo&f?{{8LV%LGedDr_zO!rEXC_HT<=3;H(eZ`ICtmp;N3oWr8?e*2di8V1q7 zorR1KAmxEitH71BiK`Dnrp^(@XKlshJQx0c9DCJSdjsAz-=Rd}2gp-d>`y!E!1%}TSKY5X2-0eB9qY+cU9~Uu zvBrTv><}*!xre%Ea4+%b%$_>xHI2WjAKX!9QPEw<9M}syuo*1vx81a_gt4&p%Dt}d ziP#hK9-A^0XE}4OlX@wyGy4>-H8lsAx`wk2Jv(KqXbp8!8?FuYJ5nF3zpy4P-fF%f zFYrCvv!C%-Io*YF`cwv)YiJK}j%(e>9?Mjo2}}E430s4)uvVE5(EHrKe-gw|9?O~I zeueTd+U?z0@ondmw?ErlXGiZ$|5ob@*FwqzpJXGra(29AHgRToS?SF@Uk51?xySudr=ZvX5+4) zy?PTmwIH)IJbM$~%sATk47jer#&;QyKMHwu#?D0Ibjtcm)I%?~BZVd6KkXqF+31SD z`^w7#dF|)9eFx*QvwI}7sj0bcKE4S_h>$jv5BJ>QkRdDpZCV0}O@>9>LJ(R{v%L$i`A%!Hk7 zKbwAzFcr2Q)siN~uQ}HZkn+H% z4SXA(z{X+7124&H8bJQ5t z>bD3>gDKbwW0m#vpYlW4>-owQ#Q2cXen7kAUUajVw17$9Nab(``nMWm-Uz9j`Lu!e zbs07uXlzs-q>`K&!v4w5>3*JdL6HZTWA2O|-0C&<_PzZu%Jji^nZpl-U4R$hT_ja_0Ptc02EFABqzU@1(6tuXHF zXK?4C;;o86_dxLZz_dSVYDOcsp-b7>y&J;y6m-Btjp^G9J7EZxKCYbtA9$d+ewq(X=l*jDzN;7qxenQbIip(V)HnB%^j#ee zuIg)k1O@9AY|MWdTlVy1w6G>(LWX$3G~4QaGkvM|85{X7x-|xDSN%KKU?YqyRt7V$ z3t}07xXW8p_Psp#kay=p=4UiUu$nsKUB(~JLv~9*esA(&0PSh@%Rlz8RX&Ke&~EsE z_xNbf#zy*D7Z9%Uzr~B=$KBRf$=}3y&N-fLzTGVb6FXkmI<6wjz%Gbom!PLT^wWJv z_=ok-BAW+ms5h3<4)`%T^yM93FC%_H3k#pmKnv*ypWKRnZSCRaUCA6bQ~IqKR`7eq zNiRV+ANwYQg)j+>RS7F$=9OV#+0&oFokV9rt3`W4)Mv`ywTuhAO#klKKIMVl*`~%_ zGLXJ8Lt1TYX;%MFw4s@@uM(a|aG+hFbm~A??pT&cnWSd{&r%jll@4eB8M0^H02lzi4$X z*K>H^{$k%b<-ua=>r=pfz(09DsEiFTw#w$f_|;!6CI%ZHC$9bZ^YN#8bJmduYn2D& zx!%3?e3RdxopL^P&<_8kd7yf32V}4~y3xgmgTMKueZ;TtsTNk7~ z`G1ksO=E={7z25ocJgJkBS*M{1~x+jzG0-Xl@Z1U*<#0@{P9rPx=uC+Fc2njXj}b) zvUR_n{>J*L(C0QEHkk3Xq9U!SK8bf>$TuV(ROW{mouzx*7@@&lw#i0pO}Bqr;U1J@ za3^3d;V+`Uz5b^Vg~7fD>aT0AtE_Aa^N6aG25+bNAlwHV9i^+CpVPRK##jyZvPrhd zMr>7|*4WJT>-_mT_*do02FCn0+yi}P_1AdiCVX+c>6Vv|XN|*=*t(-TcwiHG(egFc zOD#qR=^~w^n{>3}fzsKGZ_5VRf=zyT^KI32d-(bKY(SY-qMU3v0eTf;{!O;ne8|mx zmo>$+mk}Vyii*!oET{=s5*&tg2w)ysM57-BF2nxO2al2D#XxuPAiWp*X7|UtAs)>h&w)mUWJ#uE zONX|(HR)z_l&;bl-F-WP*PzYYZzeNPsoeGgM?!iWb$pGNh==WC7$9f%q7cWF4?N%4eruOx=BZL zH9DiWsZTu{xO`)N!aXA;vH2wEVTiSm!MPJVo*xRSKm8v5Iq;EE^bqy(MD~tc&79@= z)E7VHUHKvXi{toK9&JeX3ez*iQCy`_TJaDs@wD>`k`a_?bdWCSv=QAh>imsCy`!R? z*|Ui?kefz87efmm*2Hi60`l-t*%Vfq^APF(jo`S>)ESziQ8~8sfeF(y#j$bybmHNc zbHiIQBug?STRNaiXL@$D=$jd`3AH;V*1~V1eX;3OXez{aZ_~%ne?wnFU?>uf*^nRK zq!kbG5>N4#49SwrE_C{4qTIdKUD>Q>n{yyK*_$bsn~#UCgl0o8Lu;TvL14Q1f1$5D z(!Ij;OmP%fX_Qtx#7jKk-JSgIE$Gh}uIv6){K~oEw^;YN_FlJ+WA1rn_1jZ{A^m;+H zFe0b$oXXPc;==v%((A#6`-Rf$g}eV-`!_Nmt+n*JxX{@magtNGUs-xxI`*VvQhHrF zhNah~V_v!~@J&!4KpPSB6{9svuNPD-)9wl?aAyh|720e;1z6=PVB5IF8@BCBthS@tRJ-f-(T?z zs2D+a9z|n-+Lk5I6<7{j0;oNHMXv?;jKIH#p4RF3KY_VG1P~6)k$~1|AALs0(6O@R z`E!(5FWE%4k&R@lCG?gn5(B({SpN6NF@X1|LwACd{INN+X5gLe6B%{G5qNjFAKv@g74Oe%%V>$3;XQTkKz*P#;35I7(>{0h7&?~9 zP+2+$or}&%F+z24by0oA*~0E4Tghg!oqSOL!as@M|EqCOs5`GeEf-N7=mB29M)39k zD28X^ei6H0)hG=51VXF6(5wwKtjp-=SHZj5tQcKAWBle1y{l3e&;hi+6Lc(<5wYi> zbJ02J+*AkEMRii$oPA_ViSh7&^@Z~ZzLAg8K6C5;D{-I<|2ZraC>hPi`a%}qL^P0Ak7zn z-`U_f0DOC*ZQDa{{N`#Ay?<8cA1yB(5BNQL*hIFGjbtkq2V}dnFYG$`NO91ad@qiZ ze_ISF87>wm8A@x;t}DQIcW|%~cn&ayScLV(0Qy{p!HkAMPqej~&>X)v`L}9bQjAE) zf+=e&*-W;R51cRLleCYl&+wf%SVUY%^5P5d$4IDyebZq?PdgXVaz`7g=kCF%F8_ftIRv$m5DHpC6j6TkeY z#(<*X5&?ntL$Xvx9`J&%X~3s1b*J_oh&e)pUAhO`Jf;U zh>JAh20VeI!Z_f0{3pc$Z?p6gK3M`0z*B%J#1eL0#W09%@0{*@d;BMAUYyOGKja(b z1M-#gpYy$t3zi$=$Wod+e*K>i0}2L_=+`f1+dkQ?U>GUH(gQvn06rD^O1>{)lnuk! z>$|RgMa(Y&clzLpUDEo$4V-V}EArfE~~rjHpZARjzC-xC&Z0R z9K0Y70phZNU!L#ZAs5Jrma^@gw@DHQHn4Xs@Bv^rL}9svQ85@NY4hmQ|EqJotkxxM zQ!dbFIU#O{BLxHEisy~xkhrvw=9qFUasPMzQT@^&n_#0ouv^N02HWsw0i!1JMZDuT z9iY1i*Y^J)%>m^C{5~`|BCa^j3Sxk^cLVmk?$W&f9Wfx(UnUUh7wXSyFN$);@+jF} zh^35zC>%MiE#e&Ssrpa*uf#p&0uz=i;!NIPsT7akl(^+ME@*e80clazf1LiySUYGP z@mL!;0WgJF#wdzrGFo^Kwn$$j@mn9u|4=dqloLcOXT+VHXqgnBEVsmQZ3*YB_GPZW zezZUU@G*d;Gvcs7dm&;#2)h+U!Hl+k?f>-tf5tm;rzi?yxg<{cc12usoKp@)+n4B3 zCOJTFg+QQ3`lD_=wtjoqa0Flq5zQ#-PiAyP_@1!{@hn1n{}2BU4)j|V5 zp}P&IJv1S0NTbsD(?x92xe0nKM5uQyz!YLRqk!*-k-bIRJ>`IJp|~9JU+&=YCKm(5 zEx4EBo8=xFEQb~nZCH&;ArI(~{sl3hiu%HU4}ip`6^w%3RA}#n-?GsI^jU~RM$n$# zzsSQ3>jb=)gM|gVZ6FdAl%aKzh~8;V@3k!%1|mbLGF*Lpz5l)?#Q@&-3XTPOD-`jXqT#7`-DcM)vx^1S&4H3q`%&BA(jJx7XZUyB_ppp8-2Yx(4$a$(m>as5u;%= zTmsC;GYXJkB$kBHjHXe4=xifZ)|k;Y^ulLU&c8(H7`8we>A3m##{h9mTnltpO1Nh= zc+P6VX~Stndi{z&ZCqCfx{?^sS?PiJ$_5w?D;YW61-M^nTufWkgZ>_vH%aqqX)Z3! zp_Ro#M$I^k(KDzirN05Ah2N)E#T>uFIlTN_UVga_)Jy8UbnXhC~|Hl1^&CC-0=W#$1TmBDzTQ4r2gzn#XHafPO;TZ`9A*@o!t|v)q#g0xi6Aqd*r{qfXf7v?C2k%OCyI zSi_a0@|vq{aK5y{I#x4s`0aKb{knLcpFSY^3iOfNTT14?(R`;Ob08(#G{1K`a(R`4 z^BEWzW1jsWd`|NLiz`F>7UP(H(BJ4Q^?%K_fBI+O78;NiGBJ?GYQ}3%TK>qNTC6~z zF413OwF2U2CeHT>U^uK~)OEXpbA#{TopO0iwBtDvEeqznVVhqkjps4Ch7E8&y@GRU z8+MdBhCfz%xdb_40`#{=nQz76KV9pR>^EREpoKsa*MS0kpcQGxX~$_vnttye9ZNZb zVnBV3yFg>jU4UVs9?K|cO~Cc%dpW0EUJi2~r^wE8!gj%&Cw5)S(5pauiUVB(SIm)` z_r-CAgd&%@J;RiTcGQ%DZUpmRCaEQ%zURv95V#Rnn+>*^FwewP&Sj8fIPO?gDihloWuHr(k5SX^2d<63 zL-(RQY8rHwp3_pydKBgcO<{8d-=8qRy@RU37#v$}i8CH}kCtdgd);eh< zP{#GTKsQ#yUfAaJCyk5yliwf^&}T|4^sw9mFdWu1ifS{_&#=SqK#Bl^9|ZaUhL()7 zD6qKa*hQXes~h;@+(we~;~p3H4)UV8MUE+)XTZ%lkQdEo$AOHHH-6lxknA_MF&#&>! zs>pnSqH_n7MB~toEhTkP+?2z!@8GYN!+aQgFTdSS+Q-}ZPhX0lRY&NC98jPit0ig5 z>HCHLn*;(m#02e^-=Ho~SiccqSjcZ=6qRTHMBC^2#GE!!Ydp8an03K3WB%DOzr^oV z>xH4;U$sC>Lqk(WMSqy=Gf$awtYB322cnEIz}NS`x|HMqXeNMm1v-+Zq%CPoS_`0g z!5`Lz_^DpnFSnsS@Bm;~$ggKKG<$$o!=D8DCk8IubE~4yE~TGiGD2P4UmCcfF3L4j zC&o8KopB6eq~Lio`^-~ndnKc&HyOFYhCTl0ZT(qa|Nio%nE=`q=*VeG+HzXU>Mwx) zf=zOWgINH>VG|>-un=>StKt3aXirA)>;DY{^wBCX3cAx}pIJ#izry+oR`0_4M2H7> zJZFrQIgXw`@>qpu;`7kQbo>Q&{)~^mOIi9pT{TuafpC*VOIFiatj3(ylGeY4bmw7- zb^Jy>%COjs_wx(^r^fi5b;F;Ak&zjrq8EVYRTxJuc!tP7OXQcU8C5-h)M+ZIkK)Fj zk=Ma}jW(V=6AOe}%7q_c=&2(oP`QEYXtHl(Z#{C3=(ggv~tg zTkV_QaAh{*H^^h~UhW3Jlv548?ii4KmR~J{GNV1-a|&mZAIQ#i9Uo2-U$i~jKRe(0rig%lbTZ)fhxw9R57+(EaDh-Z9nLU zeg|7G`ZM^gMmaU~0aY;`Swa~(wPhFst@n#<{#CyJ9p|BbAb5k8q$#IuJg)Do*3g}_ zCv20$avpaK9k(6t)0qmbtiUDW=$B~E`)|ysYYoKsB=j#Fqm)_4`(-pHO4W~NJGggH zRGWq4a8FZgIXYh~qov!i6l~^g|EDdfy;-qZlBR|FlGd!|q&-0d9070)VJmXKH{SbW z2pIihAeZa5WrPLeQ_xLO78@7^l|PZos}|T`jC%)FjZruj;}Yz+mTVcs1wK>L918tS zezDEJnuD@DALiddOHO-UUsh|<9NKRuI0N^&7}-oaQZBE^C@SH8UWj|sg0Uuk%!yxafZkoD zY%sw64DKD|Fm_OMOhf7%G+q^n`O%J2<$e{9|J3u~8UQUxQ-L4>pcr7aCe5LJ0=cr#=jW2hTulum@)BDpZmcDJ8S10T9$&c4+5anSCl0^8*uA87j)MQk@=^O*&^pFaCOSmLq7Jm zbah%w*<#4(=(K|$biF7%uIT!9N&lr1${9(xHo-e$J29#%lYvPkfIj;YDk@VLf~w4t z-_I?BxUz^lioKFC5%zb0O(qh%WOXX7ZTfDW3#%<@%xTSP4(*eC0gU_5{@skC!cz1X zDob%!8aB~2$p*PR6m?Rcgzb|SyVTSeAjK8=fqMrzJO?Q3%gU6a`ibZAe-{!N73Jy35!R*5udHS?pQ<^HIX*MjYrVq1E-6KyXT_E<^mp*}!GMoDo#j-wnVeJsD8fc&r(?cGa? zW4hin#eUpdu=VlBmi2Nc`malI|KlKK+ppsD>*pa&Nn6sG)0)*B+V76xVuIpg4|3OJ z^m)t~Q&Z%qQiTz6p%*lvJkDN^idiNiA4Q=LXb)T9AM7zPvA}c92{^XkI#z5s4fPQ? z9@kpd2h3N;+yDgy%p3V?xsvSoRr?c>n>J$nqY>=FwW(D3Evg@BO4^dfg?f|r$the+ zP+aU`G&BOr#4a=VZ-93$$;nY4u;7}6b<&kb63dqe?`5Lni7zw6fwmU)Wl0nMIN9Yc z#CjNF#g;vu%FsIP<6>X-__Ewiz&q4?v38ZE{!|xf%jr*AvmouyaqAQpdobqGPr?~- zU7DadC6$rO7o%RvJ#_sk4m**{yGz+*g!a=EaY6Tc#T`%kcB8GY!ZpuPsvNF2tp8uD ztEBV&%Kf_;b+tg`fAdo2S5`Y?R$ESg(wf!(JhpFh`}VT?bhLZIE~7HB&6IMvDxL)v z%}K0&g*hsjQCA-ed(0#@(Hu|8<(MzT`apWhBAM+2=<7F=^2G@E5}mQ1@`mj3Wx2fv z;~ERlr?G==#uA@owJMEmTyLQ*#~EqOws&a1_bGtwz2IgS#?HK?I4cc1q|ZsY9CN~1 zJqlt{Vk4iU_9Acp3BJk~@MVfzuA(xVwOJC!GG(}PX>0ivl;N&7G!9PpjwzC|oZr0v zziwHQFXYo+jBBjN{cmk4+oU-ut#!mdv?YzD{Dk%?xc4JIQt(+4`pIn>Gc&}cnQ0jS z@9@%ZfqJPQUXm6m;42ROw_1gEAs1+BjQC0!I@dn*6NAuau$1^=f_u&?h#UN_F8G(1 zUsp<4&+=HZKh(|=!E^gUf69n!(v~#l_2#tS_kp+~&}aJktr+t%(%;<71bzD&$mRI1 z0&4>ohf?-Q?Ai&tJPYh1`}7K8xwta4Z$I+vQsj-wD2F!1a*VlQ#x$3?W~sr@urBh| ze763geiXG+OLLgSrm|>frYv`)EvG-HH#jHlKXcsgXFn6^w`7ks2VbQR8V4(fHaQo3 z9bmQQY$>p>V4s%OASrw3ey%>+FZBWU7aUWx?0)uhL;QvS#Y>?tCX#L1R~j(ESo_GY z*hRh_z`fWk|>>T392 zpg>$g8SXrqnxjz03@8hZIi4x!$jj3+%93moqaT`p@sGx3$TjJYIkeE2*PGM+Ao_nC zR}{1Q`t2EWbIM7?Ss4Iz8RB<;=Fw1p_(H>&}q(9k2 z9Q&{wTY#&wf_VX7M_~hLSsad_pAZO}EF^ZJof+4_^8w8L6O>f$2;w6daniDkeL`;1 z834ODos0M98156I(5I;g+e{@sl_uvVthQXfB)ix)5A6?O{Fk;lXloBHBYUKM<@qAr zNz3Apid?=Jd{vOxNBJDTD~5T(0zqmCWw^M|(dl2tK2dIJiP)n)U~#_CzGKKuTQLS+ z2ez32(tMQ0x+$JJ(A=3qtvT%v;~4rHZo?VH&Q3c17K4sw#^)t)k9)AH_|ChcL`lhYzFR`mMZKJ*%`XtbnYb(6otoE1# za0K%|XdmU+C9oYi)&llefU7bA@l^qQErJav0Mb$xTwLhr3@Nb9(t^GtPWKM<{I;ZW zC)m%8ja(TzzHIy<-YoFEZ7l2}yJdahw@+YA7Wh;;_Hq1@wp=W-`jh6+{wU@E z9mRbBU6WFApS=!k#Q|KI0p?{0;%hu?ARURb;&2kVJPhr{T4EQT37X+IB9&us{K*o^ zoIsvEj6B-~WlYP^AH45GzZ>c%u1oUiB<>T}mjU1O?9_qPmeZf@laS`n{+I`V`JZ&X zOqH~i24&0@#hEGYPrHLJvW>JX4yVA^TKHm*Kwi^Or@WS4LK*Hn+FE1EPyq&c)t#T?*N%>U-DNkZX4Tw^&aH~v z6brjfmuQ<%n1=guA1Qx|~%}INF2Ou?x+fUb}RNT7`EkmBjHw(5t zGc$YewH7v<0VoE(1o5S>-%E-wQ;Z+dy#w_DiRUlN(D@VBhQu$b?`Jvvc6kd6 zYviVxu&HE!$YxD^AKuc!vJ`DHV>Ol5SfV+!Pm=@iU7*4~iMskM^hazMOH1%nhM+n% zaD5jD&Puc-J!NqQ{e+3I3w*J*(OlJ<(3j?smQ?Nx8y`k^4}qm6+JBk(M7e1Y?4n#G z>j%Gm7JcsJ=mS=QU5Ketu#I>nO=bGXq&c)dE&zN0eE*aBT+}CtL+)~5tYD87>?u2# zgRdd5f!Z*&nc|Rv`-G*)<#rPLs2!F^Kbrcs86}jV^Q0rs4k%-v%y4a|+(h@=6r08S zqo9z8+*Aj)nUw<9=B$>asZ<}CwDti=djh@-eEdFuV@|O5*+zJ`TItyHwRj@F+Ms^w zGn1CYnJmtsttX%lSgVMA+S+vQAbYIz_Bo6n&p;XLQsB5GpJ{zPD0O}3+_WmYtkIrpCE{UBi!e7%n4jChQpq+ZvS+jQj67BAUF>@ zC^p;Jfg?)E zE^}5(X>BE1ljhL=BmvifllV;#{+{a;a%>d1stB7(itWQluzVKaj6E}nne6QI5dcb{cq+=#KVq8wmZHn8IA(bk^Ga#F|{ z&mF(KfVup0&{wj7eJEo^&jju9-MV!scM;%^lU|-jemIHW5a?3IKA9so^+4UkE5)X? zP5k-=-1lrk9}s;m`0~s6hIXW(%sn+}Oj=88Px{*e_-!ywf64{Ay8etc?6C&NWe4J` z7xzkqG`3IUw?R+i_u=T?ld9)z>CcWgkhY|8aoUsq7Qh(*_HZ$vr8$+g z|JT^`eg0Tk+Tva<95#^t95=-+b+rGG;tKPq%xXg4onI+;33>Jyu8A#Bj?Ww4XVcI5 z&*vt(-t#da>(^!UdzYh6QxUfPifgV7e)C`|>Z36SUQbqAOKg+Yk{G~3a29~i3x7fZxFS-oFm)Aw?1fw3BY)KAxK;;T4Z!F|N-r8p;%hEZh&rA#YScIm`m6rXHBKLS@8|5Y1a~!A&A5Bb z0{ux-(iWl{u`TR==g%^>_ z2g5E4faBT1ybjv;EQ}81Jpqgrabr|MA&t3?EJMz~mqBl-T+YXV^fD9ow;Pbl z?O|704n*lVKc4fu03sN@76T7h1o@er{&k5dQEvP5K<)UMG!6D_%E=hNLAl z<@GI2d)mi;&gzfn1Lu$f3dbmw6nCP3>MG%y+D~Z$-`h8B1U~3lAn{e)wLBA_i(!+S zj7`K7#l!CdTN@hJt%~+d;~H5d_;ZbocSFNoj17+eF@Dj?+UL+E|w|tiYE8_}YScub04ACi=`7_%45U z*kV~=57}p7LEo#W{|&g}^Y29i^x&~N0&&uN_cF#6o>i}3xuHvZ^wHrC}Z zhA|KRkp7edio-QrmjYprm4q{jn;IA!z*7G)DgF`QTngWY0*;{^UlQcx zC^r}92fyz+Vq`hmSVbwHzSY*~H)`9n+Hh^3*NfFo3;IEEUe~XRFZqy;FMvDN?-s_u zMT}Fe0>_R&VBfdwFX?kD^zVlwC(^Z%+6>Q;^zsJ!`Afl_M4SL67b0L9Ml zgp&N=eY%cuf@F-{yL>0V6z`6#Cfpclf%c@CyOfUKBfO0M7qERn0SE;?aj|s)dYna@ z8w>kvfs(ZQ75fp_CNvI256q1z^7>pz zlW0cTN$?}u)3JQHi&$sjh2>K&$I!Ej%jlb}m&#K%u=iJk?k_6gdbkSpm7L4z`e0x% zh$&}FeE`^irMX#CJaeb{j1)J&Q~v)RTW;WY@xtLtIe@e;j#sX2kOth?XuSqkyaAzNW-7`C+5bswI z_r0Y2Dddp2CeFFMU+^qB8=62H(u>!M)_=v{C7guEMIqJ)pbm^J@XzF~fdBpIt9OLG z#pSC)+l$8X_w1)!=YU+d5Ob)Kkkjd%0J2EL{m({R^P5ZYWraSWC-Uf)Vvd(B|2u9c zpiTM7@M+C*Ok8u%LwNp43)18QX(Of8uh5=92Y+6Qt;?!F81SJm2Cg#7cph(N)Lg&jYsb;b-x!_>lfwx-RMc|KxRRmQ!WrZL;m2xF-!r z3tktAR#^PW0Y+yC%W9MEsXHfK34=D9Jr zzYGmXivmr4x#~qvC>sM;>{0iTFJpl6Kuc)J+W=|b654M^JEt-5B6A>!b8ugl z+Vhb>u~r1m*gnD8$UoD*#*WLHntDmv|9^4t{c*r?McmQYHuvnC=ar2EaD9b1m(hXO zr!1Cy-B%?xTy=r|GBJ?JK6^7Z>VrN&h5vHFcU=e2PbpUc_kO-Ccib~CY3*5#YjfK3 z+>;h%{U@zI*}(!ac@=p{+TKjutDeBU_!x{^R7DI}OY*^g{emy^0gdfcWx0}P$FAv_ zpfr~(x5P2eHAPA0}P&SOk_F2-f+{1*eo<^dYB zuETOfTygE5=Z{^2QC+^cwC3M>!hz{CFz$Y=7MIMCOdz;aSJ$A#yM<&HQcE~U8T`IeI4w^)_7kzH4X<`|dE z%;vNw{ka$r3NPdP5cFGKgCI}_G?0ZZ(|v*}FCPmuPP=0jv+j?X^yd&eUm)`kAW)4wSWIDg1D z@{xSyIN*Gj;)A#$j)<#bIQ*?d{)REd*`I}Z30at%l7;z6S;z&_bFgk857O9iBIfeY z_e|6-xcn#N1hSE?@AUhG9;^>4@P&Nh=B~*4%W^eG_VWIOr5`9eNP`^frCzPkg&g_QRE`VUb2k2sX~OL}=7-%q&S8At|R1K>#- z+?+3Y`LmegwTTgl#^bv^Ex~iOzd079HLu3nM7EKQWGkmR*)Hu1yG}lmujDg)m*#?B z|3`@V|1^^Kk(SiArR8-3?s2d81c)1q8OZWR`GEMO@dLaEKy5v8>mPVN))9RL7s)-O zEn`=Ka>K9A51iKAxoOO4TUo7>o5B;F)$c@+WJ0!9^@^L`(dU5}V@Bb+)@5WruDE3-wYT}J~fGybmV|iN# zj4f4S?8;XF${{wm_#hoi29BX)QHILWIjH|e=cIE}pM>h7I;n2XKC*?&Q(xN4`ar&r zPvl#1KL1ZBUc8ef@o^o$RdJ&}fZwdSfgh{5@dg0TvLN1xe{P<)9L7=<6w;v&z5jbN zv|5VauL*;06Y-sp0Q|1ZKPs14An_>lMQ4G*+jOHjbHKM z`&jtzsIPIzenxo#{iqwxfDhUl#?5Xf0T+RX0CL35j{xH0r$FbRbJ02J+*AkZqB_~S z$p*6JE1J`1|NZ^{8Tb~r^mz_6xTyt{13Umr{7)IoK9k4=K#{*R%*U@}w4uF^jQJpH-zJMy?~ZGKzKZOil9 z+@?Sv*_IS9IDw?NKp;6m4u8BdS0I?*mRP`VN={gCI>`z13qQA_6BL|YQbA7P=WGSR zh1+Zeg_~>zh=9VspaSQ)eePdwOI0rrSQV+BH6W+(Yytrc5KC`^GTH55>Ca)H^d=c7 zvt7hofgnhV9XirocDsnVFj#h5iajc?ExVmVdkg=Ra|+jKO<7!YTV8fMxajA?Z*3Pn z{J*XL82EQF5M0!qZ?*?<(c`~pUpYlTSC-wDZO^jXviU%ETQ;A_Zp-E)*?jfIcCfU; z$aiAt?PBs{j`Ziqr$rpV6{U#5$j3ztEGB;!F*t}RnqaZXT@1NO+3SUr0qpfe$^iC) zCS`yXm6tNWowlVIU^fdZU{B951r@NTm-0|@y(~DPJ6FEogpy63!h-9u#6n5&0*mLcMyTn69Ro%3_i+aXXCl&3Sfgb`7DQWLB2^UVB zG$8ft8^4=78l+`~54;y~y6>q8egjSgM>x6J?>dyKQeMHhe8_Z_U1U zdVC<~s=oO`v5KRbhP?ctTK!`LmBd=l?#s#5@u*OzOGt$rwe2dWgf$P1bf|ML_+3tt zT;NEb_m5VrylUQVTIR#*8M!U2$3@uWw9Ih~P-xt|dB`;Ja9`Ux>!RC0cwF&0@vz`TWa`q)7UzA_VUV4>_;hWm(QQF@a$*(yvGC9-_&-MPZlO? zB`Y=l=oe+N|3XmBLoGDbjqL)=0~G=*1-{xjVp^QL|EOCv$Gwk!Yg2Es)7S;`_Ff=xmyhA0PraP+)YS!3rQ)HbXdYHK>n`lD@tN*$$- z)_IL`x*O|e4}W^5fxJCZ^tu00=WhmLsIuPu1b-!M+eC&nXn_F|&y6kiLkUhZ0Y1N4V!xvc^ zIk`+b+t>K;$nf-07NZ7ipYWH4uRbhpAWjddE!f@n`sk%+hn5SOnLAnT;PQ^07WM5s zv`Q~LSvt(-_3cM zpwdHO#`F4mlOn4P(2-lNbU<9mfvn zqm?vn^aiiZYM=G>lJpwN_lU7}n`W!=-mUSRTVbg4L}X_O#mT6_bwy zw$NE(+(OmwKw9FR?Po{Ud2q2#ugy+(_j)F8oo{+oujMU`JZ-U$oR8nMTaE)2&K)-q zD^1$=E^VQBb8u~8^&?Gg#x-h@C%0wDX~nF2VI9NLTK8XB#dPYyVb?mnUUqzI`HzRz z-kDNIb!96@Pr2|`9bU{BdA9$mI{}GZ?sUJT*0t+q&lCsw13s&!g&QBT4baZ(lk?KJ z_MLivo$_r^&)?!{rDZydb6&li)8aWTD&L#ACP}?l!myQ93??MIipQt-Z!4P9YnQ+N zqHUs9{Z<^vj^D7^;!w?N;^fDp)^@p5cd^=)u3hT(hYt68UcF!EqetA~HdWJm z897di7&px&r|OKa8t zfQ#!b-l@{aMW(Bq3NnSj_a>Ty@`=!Apm3HVB=3a7x?g4Sj4ivb865 z@vZwYtycX{IrXO>@Drq6VFvdJ|07q0dAIQ>ItXw(k>Nu!>-wVrDQ6rObHMd|nCh!zrZ+tD{lsP=K8F=v4pXz~(7kWucAQ&)O#GgI;DbM%vs zqW8408zW3^H939N#mCI_-q!hjVAsYf5zor0PgyTsWY=-g(fgT0X7t=@|NQ*j3U88! ze`2Di%TLnEZyesE{lTWyYffF+Yj@hJ&*z-;#^o4Idt0sC@)-49r+b$xcfD?n!(n|@ zBYW-673CX`KRv(4;;e1GZY`BS7?X}&8uD7b;_evxmIO)?WFLC{l@tfM1zH@Gt#=OXL405lRsvp;}ZGP|Rf9m$? z9seTpn4S7{D37y_!J0>zzB#*ttp@0i*&Nb8{#|O0 zPEc)^)N*%g)QTH*IJaYdRL+{mRSc%|>l0!WZ(sAT57GS<-16o{y?*sN?}&FFx!z|5 zE;^qDd83WzGsg$m9g?@SG|m~3S}{2&;LlcV^Bc%pRhkoU=cJnZxOaM2wr%aZV{lTA zLSBlPNqyxh*t<%1OfC7V9v_vRrZX>%n`N9oQZ;ATYVp9-)IFp7O`ni{QuSc-TkXFxS>qL*W*xFYwe2TJHl){7Ib{UH`a(1i_dTymK#meu&P}SARL2vUMSDBjK z59_#bV#V^!(^bMgJsPsOwtcUA2CZ8^-Jm#L{$R+)e!ULnCe{BG;2-n&@&~`i2lqv& zK6>ZxIr-BPtNg%?N7L-aJ%7F0X49VJ?D2Ua3skCmd*55{aC&r`B(-6!Oas;YjPhqi zwFrp$5arR;DLCyiQ&;KZ#GGv3diTmV^3xl8z_G^3?C^P`x3(WtDPewWwvJ)-YiQh8 z@22m2anklk^g^Yrf*^&DMy9qZDPgK(PG@F&e4Oc|v~<;>z)3y+XtDEgyS&$jR$t${ zDt2y0&%E1<`%igvKJIiYdCNQ_;|~{3R0hb=fhU~NcsImYn94V4F4b{Ws9g=UB9?ioJlZV%1n zyZt}^F=^`Ad2_d~8PoP--M+Wvr`>+J=~&f|NAI?NGWN!4$B>!9KEh`j?hnS;W$e(% zIT!l2X?v}Yb(&pi^(uGf47-C39K}p#trO$Z@1B`*)!br6`&U72;|`xad-nPDOPAg+ z8zQV29OM(+?xLx~gg0&7J1OYBzQ5vd%G9pH);aOUJA#7Fk5vm;eEVf=?7pBOwc8oY zS(o~$t$X{qTN@f%jXJdJ-Q^tXBL~i&>k;3%uZi;Tm$&rttcCi4xj56Qo#RB7y_tc! zN7Sb*G;B3;rqjouT5g91HgDej@}A?X%2`bEY`Uhtq0ze&>5(oAy|&L9+3sLu=8L(-=&MU$saV_*KX3Rnd$xAoa>$#f3jLr;r-(~ z2CD@0jyrsCW%S+mt>QXIdno3g6~$EiH2O~e^ww8x<6Fpg&Ut&Rh1{jQBW;Z0 zP8rvC5IR5jsF;)8$97ZS@t@mQ|M2RnvDLEWeI3r`r0*rVbDaSbKuFJMC z_9)k@XZ)`7&8qa1OBe4C-Zy7Of4Tap0+(5zM;u%GGS%+&^o5_FhpB!Z($}a~+&J%< zC(i9wxs=iTq@yY@#NL*|HUA9^yW{xOAd(}k|5dgQt)x`(K~SQ?(vOlT07 zfAy$?o0Hpo{h+yXgXLV~0=M)^^_o4fYnAwm(bfH;#9nT$TO6C^t?-r0IwP>u@}4$m zb62IaldYdE`!k;z)pzPa6SK4vVeZY9ZR{(*?Y1$|#LiJ+Uz4RDLn`(e7$x3y{@jtr zjB9A+ZKCL_`m;Y-MMvf6*L{Bffv-w?om*)}4llhT9Xbp-T|ZAJZ-8ln%C_v(+T_~4hxqRuzAVza@x>9M8X@!Ac(&KOrKuj-^V;AbB|r82 zdBGog=hV&0)986%N~7D_N`KU}v$8#RWBipo$K2@5(~Dys53eoOyVZPauUn~?7LQB0 zczNXgJjd~xn$6u(wD%1fJL8IjcSFB1ryU30*jGg~wBDdQlU-J(Jb!8ayuG+-*1c^= z(Y{&pN85>K<-R;~ZO-+C#EU1FB~N{>_Ibj7yF-TV>APA84)wbperx!SquWA0C@H<$ z*?Xb=xDJ2L^Ig_%->?Z`kz2P8->ccV+z|2pY14KnY;;nrI^4%Q(<8rK#b+Bn{H2g> zDa!NR6TW`I*`61dxo6cn@#p=~7ap}bzt{NCTAiSIp$qQx46D{(pt**z@r-FVtd>>8 ziANuDE{cryI_?U({xa%?t9j-#uX%F!szu*9cD0jAD_hst9*tFAc#a-cmHH!F;+rUK za+=Z9txm0DImb-4ZtJulO58ys{-b@sADcUeREyoAblXaN^zA{{hjUI{bzw$dDPJ*V zd%${MwN`ES4&R^USzlSa&);c8chxmTxr2 z%dg5dtg${RZ;sOYyK~|~7B)SkdEIWjc16R_54$aD64+&^s?CnPV6Pc+m!fVo70w)5 zE3^jA>M}E6XT1?dnF*_|H*h_bmX_9YzNM1FO^Xbj?X^68tJcn{>{)kPxt6w()a#hrIiM2WA0s@vb>6@+xE=p$wIfL zj}FT7ds(RwwDS>^b*3D|vdiOIXc&dD@ne#Q8yr?;2s*|AEshO3c#+kqI-`c0? z-0=1tX5$V>p<&WHzoEXDcjf0DRhVco^I?8=N54T2H;di-e{x;gQlK5be}&R#`R(-| zU3WZmU*GSJYp*K18_ZuJIOOb*ysk@Jlu*BZ=Sm4{V}lHJqsH}4b=@|!^Sa?PoCH0< zLFU_+p>Or_hi^QECcV+FwSJ0|bCS{Jb3KOi+a#BNZ_iDKyxHYDynTk0?Vez$pLk(S z?`5&(517H7qn34a?O=cT)BDq@=fyRLbXL1z?=-fuC}68^O(T4M*>l#}dEo(XGXko< zoj60B^(owMca1&*o8}$!YUCKT)cG{esP)HVN9!zhnDcyulF+WfJo&bNdXyS3i$V7gM zL9AR8w?hjqf4r1mXJFQt*fF+m_G;;S?+tWS>{aRNsq#mcv{a;5x=p!<0$*~aEiPWrAIkRh(Q zImuHl_{=pI^?Gh{0p~|wdwTCZGp@04bB~d0w(k+dbcqQWp`rC+#;y5X+6HOnHM246 zZEavNCHjxV>{{aL4aZa{uQ_R3x7!Q-{4Un`E2e(LR25~5ib?HmDz3CQ9I1EjUW?Ox zJYEQ5R*0ATmrsljjuovkH@h?OSd-2%!K;&oPSVg^*Dy;XrjM_B;7sw@k?)@_xIAgn zq~)V~NByO*c&NOgBK7HlwTcjx@#2kxuTH1Pt_V}|II^?U#eydaSobLA5Ul?>E!S6_GMbGFV z>qA4U)$gRfEq?ihz-Mi(x{Hk-rE?4CJJN*<27!L6pRSDVo+EB(SF!62cJiS4WI zOlF|>lO(w{;XYUPsja!#<5Z~Jqg6}0R-1b8RxNwa^G}u@8F}SEtlI48Ee16QyI8%> z&HU@#N6jmP!f#z{_0UDLccMd2BX_IRG`iN@-?}xS_7uNs5v%uq2)U~qazDvwK#v*s zYKbjIRtywpN6dZRZ^u=yquR|r*L!mDk=wg*kx|;qs~Gk@(%QgrVE2y)y;2_EA9U(t z=Aq3qa#ovqN9#8$_q>~mgIfK4%@o>IeiA%u@s^55Bc@xfe5|x>rCM$uxwS4emZj!C zahc%%@yzM1ugzAD{uFrclJ-4EQKWYLS~u&~j;s<;<&?m+>!8(F$F~|3UhP3{v$Xuk zm!n%ck5&#FF4@O2XKWT_ zS2x^Pp+$?W9hX-Yjgt4BlV@Naf2mS`>(#xt`=80`8Ozi@Vl;E+gy$z~xa;_M^iyd( z%}3#0)_ggaV*<-w%>Bn<=NgK(uW?>wYH?@57kAf70lN%Z}a~Djgfxyjk5{druDf zST9Aj@xq|J*Jg{Vxu2Cc9&QtQVc!ged(RiIwy|t8cf_+Ei<%~H>eBktB)7azedk|t z*g7*~W1YV)8mC(~nRqjy&Q47waYTkVHcrpS#I#ktOG-Cw9sMt4_-#4nUCoy zM<>6mNyGakuDAVcs{8&g?YJ!hh1C6H_O?Gh`a{i7Ij#Cy(@ZuDs=4cQTs1X8t47uS zlsl|)G%6*0o#`z|K_hS7SD;8!ruWRw-Mtn=*h}4Wh=^Z^KruCu72qP1eLMEe0x^A%#8vhRepin}y?=X}4;i*NV58?x2m;AORhMH#)~ z>U6gsHdxSJ&o#VfUjOuT&opJ_{VI)PgRVWQ*Rr1=drjMqM~t=fvv%BGx!R_!;UDAQ zDlZg_S?qP>jClW;HQk(_#m!>W1N@UtZaa0RMpR$dYKPyps(>y^w`UK1D&qX>I))Cq z(mHsz^=khL&g(i}Y|yyz$Na;Mu5YZM5%D>GYTO9n*%7yHhN&D~9jS7C`PvUbBL^j~ zuYXC!(faT$-|;QWdGC!1)lHhNm!oVRX_K9X*Vl?W?d&~lb#$BRIl+GA6BDas?(6U0 zT`prlj?l-{boepDfYBbCtJy3OP8#|&KH{NwJ=3wnqmBn8%p4n}q8HpNvO(MTH~M!U zy3+G%Z}h5mc8TvQ7-HnMZdsdcrUT6S_}L1cYD^v1R&+Z$ahQ>FKti`+6;=-yHQwy9 zCHB_w56#x}dB3&dx})PCyk%Zwv~QRD+2&1(R?5CttYR}7O;XLX$-eKQwD-yE%dQb=sV!W9H3F z7?ktpor|MiWX(8izcsx_?bA!-U*35-(qf!)|JV!00(3O?`L9oIJ>DZYEL8N=dGxML zZ6cakJyh9v`Qiop7pFsn!+Uzqow!Thcv%~J1D$uhvNgOF->B@fY1^(xm)R+yp&QqX zJ22{qp`~s67c(|*PP%9itWd`F2$ojf->Z1=4#y&Vrc(TYlo{~YnAernU` zC6}rVIo&<|=JSC=TsIl_=&7}KL6ldv^>*dUgZ7x%ngpl3d2?5+IdaZpj69T!>*zCP zlgVq>H+vp7ebK71V$6me3HHJ%dwq9z9K7!Fy!MT%4iv3vu&SYIp1roBYsV@hU1N3| zU-!tEpA}JeV`cedt46V24L#R&So`>_;xiNTA#rO4Uuo>vrmktz7vr=XmBcm2O&T(y z<@CmL7gmW<3}={)jhx2WyiyycyLIcO8kM7`B((QE@o39fi!PTB3=F9B@U6XKM~_2? zGse^`ztQE2VE56Hu{CGNWk0zUey~|3--C~9{2B7u-7hOl=z&q1PbP_io}FheoZP;- zN3d@8z*W&JFV&?U@zJN^#TJ>@&p3Aqo$dR1x9^}jdTmU^3x|#fQmG>vKkb9El3wfX zuhoX-EHz0I)X!?9nzw)Mk$xxja@3-d{0Drxti92*=pq}+HNub{pyhd#3~elp>T-T*h_SCiii)DisU`!T(3 zBSFTcwz!zfxkji3*tHn&P}y8fFZleXL8F&kI%gKGeZ9V;aaMD^o#D@PjQ?nRRJC2o z`vXb4k*7ozKU-BdoUo?bldCOH>NP7r{@SWT&pKXOR&maAw|8q)nh$OJXiY?Fr2ol? z*Y9)tg|AEU3|}`;eTGH4V3?v>;F@lK1$qQIX*{)?8WsFu+4YKZCtug{Z2IYfGKN^6 zygRy}*^|+|n$-xXUTx6naEtxr)E!No&D+#p5-VzFsNTRTrb1NA#>OVjgI|Z#95=9e zo6z}d-)h;l%JCBWcVK#@37*BBsTEow!(xrns-43Q^lp2x;U7wBJ5v^Ux!e_a*-gGY zJ6Ble?%pSF&QCOR7<&4eTcz4|QFiekZoF#KV{@zL`={GgTQPM+q;k~G+no(UtkecA zn^^l}^;U-`8RqTVaHF|!-s0H}BUiO`Uf6HmsA;VoB2G7UT)D7wylpvw!R&HvtBrM< zlQc!X%U|90%OCT;QeF@@qq5L)L9_d3qEm<2q3jdyu-AqSv>RW@9yV@XJ=5glrxiLh zIh`Y?t)bEWaWB=%UMhBi2fj+ZC-mM>J8IU{W{+;A4nFwyVWW`6ZW)ZhIzbCVp$F1` z)6Mmx2DCBh;i=Llt8qq`Nq6dXt0+`3U)y5p8OIUVTszv2tugwPD0Tl|PR~9(ZgT9j zt;TJ%%K5c*J)PzpNvbuXaY~-!rD45H>%D(fsa3;;mp#inr-)k_ueqTZxVBs0n+cDr z_$drmbhB3R=EV;LruE*CM?U4cT69$tLvEE_IJi4pKR$9tLv#4vvbC?g~P3?E`D^#Ti0^# z8u4kf6=xrJnW=n7tRAb>AxdSJ`mP(bt~*szxX^oCm1%O;mtuDNENbCk-22Teuk`S1 zuNof|n$>wwXQO6sn7Uo%)U2ob^9(*RtVhJY0dcx z?>lr+G4oven(;}nZ(G4RYS5Me(F6AN5D(jXC~}2TpIL7@tY7lS8P}W>eOFgqI-qG~ z%j%04xNIEi=tp2 zohnrY6`Z2Ig9UyoyKWOKQtI>e?r_g&L(dh#i}M@VEEGmu=`c_*=iXgA+o{tUF%KSE zEAFtejuOOdt~tcY=&dlwb< z_RKYLn@BUa3@&FNnD? zL$3ASi7VFiF?$)AW~8xSt?}b|@>hL&e@Lr2OO=VQl(f!k-2IQkOk0_J3aTXk_WYXu zBW*l02aT)Qe?j+zjQ(98oeJF=Hejgo8pMSC$#oy1^n5#CI@V#~Nyicz z;^+LX-E)=p2`e?f9+`ef?wQeTBk`L!Zy&e#DHAexb>BW_jLLSgcUphjX`!)m`Y8F% zW4auO5p;2t)uPj%?^;zuv+0zlIrTJq8I5gf+9ktBY4%*fMF^7-dE=S+(8Htr_TCSwiiF~}sNu!G*2LBA-EC;f zr3lAy0>6)Kw)gBkvwM{(m4x97+PmhBQ6AgknwE;=-91l6?!UOVf9J|=uOT;2RcUUv zHp2PN3I|cQXVX+X%)DHx&H3{c9@_McR20-|jKeCX1Z}e!^m3rGer;9r;o}A*Pg{`` z5tR60{f4oWmOs6Eem7m!?VY1=P`zoxU6NuHlC^seK0VxE^*QJG>4rBSukHOH>`@no z&@M;b&Fa>Arjv6}_u7*j4Qtiy9G9FC_iX;S)ghbBy?h+vCk{?c@SFT%YRlR;7tRoF zpVullcjSfEgM6dzg;|Cz>tsB5-KM^u-2D{HraVw`l?!ijWBVqN(bL4cl~dF!=xWs& zZkLk2bn5Aj8uE^xl$MXZo0{3scuTG9ib9Wx`-YWIyyP*=@Q=nr_8L0|M?JWtJM~Xr z3(uRE8ok^#($`fmr-8L)(riazCI7Z=#`B+aY81Fpu~+BT)%}B8)_yiK`=s35%ga6Q z4RdMO=&{}OJ~@$hYRY%^csXe5p74}g71H}AX@|G6IUMydOrz1Lsy$wIT5Y_i#gTqX zwnw*F_Pke4lhrzA{#UD3>X+eC?PHrWBE8-Nd(Y?`-?Q@IDWdJeLK3D<8#&NB!G632GC2 zMc&dFb4@Y*_L{{_?HUejtYg<;+sax??+reZy3z86m#~tm+fZk-nzg;eYc-$9*{wOF z=eb~diy%jVFtoucc}?31DS06m#)S;=3~3p0j%jVXF+4&|U(sb~_Zwy>_o|w2?eFK@ z`{FY7U6;IEy&OX(s?Kv3K1n#1FW)@&%!`oLnXOI_PEmAA6+|`loKo$<$vb+dUFU@C zwwW_z*FgX1OQDnUyk{tytFE#0s(Cu5rTVKg%T)uq_iWkmbfpl5?N)<&{8jyl-|j)L zX9rv~eUmcW?p9oStI>xN%H3#fCpdoJBPM-a_h5&-PCJ6L?lcf6T#y^)qcr-SU#G-N zmdkfmU9xV|*k1l8^tQK?|1e864}CC$OYRyTerld;;{^%<=XUi~cSd9D7KQA%xyR_6 z&*lkYMvka9q=u$>TwsmWtw-(L7Nw!bol5?J2u-s;wSd_3(>(iv!2Dm%rc= zDs18w)Oq7x?`zt|&nLa9e{EJ|JHy*%o+8IN*KGHH4sxm_y!rUxBBi+(J+l%MEthpq z@0fS`)&~1O@&2^5C{x75@TiVS^Oaqz#yKG`S00OM4UJfaFIu5(( zMe;Cu@@#@gKH+&xrpy2Y!)#6l>BhZotr_vWOe8c9I8iLbHfiZ*v`D+7nusKz5UX0@ z_u>pSL9rpcGz~)W@>0z1BA8|tqZu%pMXfp82i2Md*yozIX=x=iw}l!F-3(tD+D*WS zEv6c9QwYXT;@AJ`U4#a}{Z9I>ygor~HmxEf04-pzn?9K(WH$3w?{{iczya4`AS0%E z-cH-?v2)>LXg5Jp8n%IfxEs|}e`Ek`bjhU^FHhM89HRCAvTsU&XHv|fP-Ip0z5%e1 zQ48>}#NBL<8UVqpixzA`1FbQDJ_!Tf@3@&Xm|W69eYuz$jdX>h0*ksU0|h~D*(6fj4d$$iHHIWZ{qYx4xok6$?Xh8^2_z4oaQ`0fN5`ix^cT> zEo)t`0oTjFlK_($%o!Nz;Y`7%)}sbMSWmk#b5*i2?&wIkik=IU*a!hi83h0-OiX@K z0MNAS)^*vm$t*KH=*rp%!PICR$zaY_wC0T6I~9PMK`^wNfPyVCo+C5S{UHI2c|KEa zx~7~E>@K!x&JZ>KrUI}D07^UFS-ZfMK41VA#sHXBLIcwSu-Bs|8)-LYzO+q3p9YpaBu~IoYHepDEa}%%&G0m?o`_;n{EZR z7_U{+6)PCaa<3}^VN;H@BDr8nfG;G}W833q<84^cnln9^%(4-JsQ|nyV47**1PMSM zy*+0jXd#JVdNv6XRj(6&m|wmpg9<(SFfQ7( zDcWI|=F@#3H7Q!6UEdaPfXJremh{3BiiTiXfK9pScNf!@#SV0@W(7b^zk!2JHpwjK zY@4N+XhSgh27`Wg2XZJ8uB}xwX#sXgYc9vuXzU$W$$cgr8>j+9&HgJGA7K}-otCi2 zbs5NDcUcX5@=+Op0AoP_P}*q2klDC#>Bb`b*y19`Di!=L&JC8c#M24Lq6GMP%f>C6|pzURc+WWh0aX z<;F5F04`{YS<6(jjfys$@La|-$ZhBqR2upw^tuF~p}wib{PwFhuoQQrlK4G4S!S0s z_2sfi0J>mXu~k?s5%yMH5YBG=M8}2^Ohv>mtV34 zuEd=&Ac7Xb>tY=;3ZZ)`i>4fn9cH640Ec~_y%`h$S?f0t?QPJQ-1#gb=XwEQ08og-JP2SZj+mFduiufCvSVo-CbI6k(s6zA1+SW^If4g#7Y-K?UHnV=XC_ z$ZSS}*oNMA;|SypU2Yr(iS2mN%dvw8(#2H^QV2mlIE7O7WGj&e!?Gd6|+_%ulhT4Mk{ zLT~9##!Bun=J}~(@?FgC66KQ@V8Hj;@1`p^7J4dXY@6kn3P1`|+n}5>x1m?CRspED z)^HR|@|2&X+WNqZEpQGaAvHBAmwcZ=zxNzT3R9DRP|~^N1mlxGXIuD4WB|~JpT3)o zV1Bg|g#u1YyY`|8(;1b{?iF0Lti-6vUc#X7Q;d%{@qn{7a268{__T1~Fm!b|1HW~r zL1T(?WK{tusRh_>&&#BkMK#d>MKbAb+$-=fMOu5Y1z0nL>TQAqPzBD}0&^Ho zK-2Qz3gav9rKt<8!i!u4-3fS*Blrvxe3zo$NE1ua*xL&=vNxw6I&_#RB zw)>)QLio%f&Bpxxi@;py1-Jwph`QOh=l9e~5@6W#>-ptr;A09*R+Mf!N1;V_f6xLv zi)5Astz#`a`yGd2ulJ<3cCn~Um*#$&ka5pXVgQ$bc^e}E%toE1Fa<1nt?K+f*JUJS z01$u|v;Yg;OU>>z5JUiya;&v}1JNy?tl-%!@&F((zsY>iq@`bMA{s-p3C}wV%-I4J z;8fIE0*v_{#UK`-SFp!TpI;8fCx1Sn0#Kv3Aq_lC3UE}v0SMBK^8lba!T_L353SuK z6h#0T}Y-Uw53(q}9=;`AxNvxAeKAg22Fi0?D(`ONn~9ChfEpU%ii ze!;d`jtMWdp`HMgMLVmD2?0QRU6&Eh!(~k+2_qdhzJ~vgyI<6`aVlZseZ$wrYc~Er z>YB6&YcwQ9P4*H(`_^c@!C5dgU#|st0+SRp+U(4d2veTN$wF)kfv$k50OT>2GEFvI z)MsrwvI9#PNR^eh;eu_mqT>UAF4#7o9vQ_|rr@KmsMm%IuWLrw7J{o3wg?^yi?!g8 zjp?%*!AD^s^>aMTc&;*^hjj{kwPD=gGfPaxX*+ne8oukY)_5udCLmDNil_RZacU!` zJTFPTy@H|nIs-TfED+Huydc7E&)wR~S5t~}1v}WaqwmxT(Wr#icCKl5k9MvJ|L*-(CtCvx zL#qizd`@C$zRnyMf#XqU3&yonlx`<~qt5JY5Xsv;pfLcb`MhX^y+HzypK`2atvPGk zeG|%>qdkNft_iE;I6)ZzZ367ZPUU*9trTIUU{ zZwYo5c#;SPprGE-wi~OehB5hj-E3m=7qqTd0FIIXraX^w2GNFVYr8(rv*XI=uK=^Y?w20yj+unDo4Q39W8} z0298)gzp&!K>LTMOu&s3697~aVUM>KZP>O4AQ)?*2cj@@Y#ofJ&Pg(GyTE<7vc@1b zMvOd!@efWn8=x%0Giwfy6iYk(yqkss_IfTQ1kvs9qzb5^IBmx_m>Bgvc6;fh4TaJF zXJVL5jvQdt$}?+DlFB(pU;qrE0e+PFj&nlbc$`1_YNtl23*!Mau+MX_GYDo{u{84u z2|yFR$1pyA3c3|cJL}W7oj3Q$5sQ)tS35NzR-!x}&^SLDT(?n~zl>1>nnHjBj>GN@ z`m}E+1Q-;bJ)T$Jk=s2`Fl$*{($Ne+0S8>iSYuO;wW1YsmerMNSB~w0j|u-h;3TT& z_hD$fj&&+PaPe1wcMwq{aMbs?x>n;$o_sbOe)Lt@v{@c?ip>Mk01cCD6_Y|v26YjKMVkxZqWbJ9CMbHoh^y-GM;EZiYGMgMZ!g!OT$>RS51Q~!0w7y;i z2&~#Q;7c3MTQ}gC=kd&%!?}c3Se=3*!nE(AG7!lZw8fmVV?%QHdM*Rvr&c#=N^xG> z_|ktVtvOnQV66XPeTx`;38-ycw(Hha!+O036EfHoHgH|7niO)_5fETVe5U>MB00+F zbu$!>O2;BnwV;8o4;<^xyN zj2dvZ!;OIg>5I^?$1nS#B&x4gs#M)fi7MIQvf+nr@_VAVReaf(E`QFdG$Apn&IAD-3qH8c@J6 zfG`0lO;L^&rI^+663-rS8746wJMnOVb2^&Ys7-^3D51QF zuumN0ZAfVVT+|kqd!!FTj_jd<8>&?X0tr0g5a90xmhG7PJPn~cL5%Luv};f}$n~{q zCRJ~@0s>5nYYc#p>hOWH+NKs005l*zQ{t_gpLUN6%4W4snpx!790b_qd0bzsCZ+zK z3TQY|m2Aow8g6(TxC?kw)OmYcm+Na)?j9KExW#G#yCp~fO8MogC}y;&iyU>UUE(o@ zmns1W0tTQhYD9+{=jdOA8}FXv;-7aB6#I>e5ojs_{A4(g!V$%%L{W_rS1sWAmCiH< z77+G{Tesq;5y_XdW;Q`2pOS3CjVns!`hSk52#tTwuT)HF`{;F44QSVaHJaXtR@C<| zvmuAL5x#;WuEW(%XSoE!0HBGKB6)~8Ww8`9T^6uU-1-@5##>gJiz=^-pX2EeOiKO{ z*GZBzAHvXXEF!m=p9DT;f>~xTbwh88I=75)Q>DUVg(6;uC<3BjDiF!ji2or`o>w*% znpfo&poB0jCfqn}s`PxwY&waf=I96%0$M90Netjqz>ETZGG-mcO8}w6U-4-aXX1in zu5)S}aXY6@fD)LLAOR@ly&M!5Rdi7f0Y*b+lfietplnt<+P9KpC*T7|T$k&dT9TXj zOhB`Vn@Fsp*|2jy4EzJ|)~4rSBH&(7t?*<}Dr=`<0sAG0$xm?~1+1dDpzPTD>>_;y zATR+y7nRlMiOG=zFl+h+)e6Is*r4Yh2z>vNzyxBqnV$|;p(tEY{G}+a#2K${aH{O} z5-pS4f)YNHq%X=Tz@pMD#7vdK=>G*I)i?mFXwIoZN6cZ4&VgXEOmA?iNk-%=0j;{Q zM#E9qjGXy36!*lb0Tbf!!dhj=g;yVwlfNs}|5F|_%4SK$v;aex##8`GJJuDYSx`GY zgE=}48hBx?!h|HQ>3e{$ZRWnQ_-PLlQlwZC_;Z13+%9&kR_O>SU>7a}q-)9<1^~@s zSQJZ0K)VTOZL3*3>+>kS5hn#q zc7PP1fdk?O299)Fat+P71pR+pd`87jJ(iDRCWhJM=ud>Z|77y+{|(?D>ld|n??n8J zW6l)C9PsBvG!F^!0;j_M9jqK&M3}}yDTSmsP>L0$V-tV?rg14M!B07!qcmq#UIIMF z8hlJSyB9c>q*GnY?mvePXto8kzEKA*K~a206!*u~v6~Q&n` zxjALA7?VtFA?(3Rs>qTE1+1W$uSXkh|J>zA=N$!vn`D#aaG%zKp)$31|YSh;i5RJ@(MsX3s3Tm?Qd0KQ@#cpva0V5r%-K5&)q zaHCV{fm@^#+w@ zIoJOC9zL+&cX_$fQaadYgQIQQYKsB9Y1O=nVp$aL5yizggWx0{FLPH@$I2%i3Yb*s zU2*^}iWxBmz^L&?@fprQ{P%$iSoLyaJMVnTkDWRS2rqM2O=M}j;qPKVd&{s!lbc2A zfW1Hm`AEV!9P=tX$E$6b6dZCLc#c=&m{)0K zQ~78>d#TVi19;Plc~uGz#rsh_66X;pBfQdGW=giMfanmyu8hd&GVvLJHn2x|sX3?u zmeI_TR{-i~2&RC|E8S(Ysl=;{kDz#;QpRZ%(qcPEwSM2cTn||}F7O@!C(bKy(094T zUCBg5Z#IFG%41MTs&HI$z$}_7&AlpusQ|Pm(Ernpm6UFrqxWG0x45eu3~XgFYJwy1 z9t_R#=H+zbX0qx6ZuLSUOOl*ukw~D;gf}5KoH9K#wC{luArHdXjamg0F;J_ z35MCsqBEI?{OGPj5K8#G%3J23xW?xneF%yVc#3;O8Jf!inJS(^9os(v?v6Xpcm=Aw z&}$=dAp#SDNS=jLegVx&j6hyMn8pu;_Vy2jVg=2daU^`; zYTx12-ZEv8=r+U&M?r)=Dt${51*|H?jOf_$CJ~cvoN_!DxQK51x$~p@4iPAe@M>?F ztNoVhW&9)XzJR7O-Tk4BWS~|dFbn)Pa6O76ap&GB6)yNTck6+6#$i+mV|DRA4I2=y zh~|pK2LSEG3(9u1KbAtMpt&fy|G%H0fEW7}Zj{PedVLgx6Toi)vs)!0X1@B~vJgC! zF`|DR#qR;Jk;)cuo4>*_NvwR*WR74QpJA1j_>EcaBAV5h1fUYaE)^OZ(~Y(uip87( z^c9XtmD~K4R^mQaQT$#&LkNbpvc{IIzPE;7)ZG2Um~!Hgl97)ot^HbWiL1nkO~BcB zC}3Lo#MkX@4L)#DbloT4Y`k$LOl0J4oNe$nbbxf4A6s~M7^B~7y(QB@D&c%g@!b!H z6#CXJAb!{)1_1EZ%jQ*JRp56}+?|wuJE45;kY%PfQP86R>`{Iu4^qSE|8ZlF;Rgvo z>EL5L0B*|rRyPML)S};qRz(6o#3!B9dpd zYv;cI-i2W5Q~ajyxYpoaTV97gfn zI6X)K2ZMrKCshHjjbNE5gJZM9qxG^%8N?gq#NWdZOr-1WvcfB5)eu?og7_$k|3@ja z=l`p1Ab!{;1_1E-wVI$+;68!p0awLcqk%)1Mx&=xA*l6}BS+`L5I@`^E8HrpttO1! zso5ZlH8KD`=90cJuLfo}sh1Jg-gu#pLbEjV-J=rjmHVGmj1Hd$@8 zT>L}8F9MH+=I4JkzkT0%Tdh!^UWM0%!G4PKC>}>~D~ho=@yJ$i7*i=FX977o5h3xv zLRPq))mA%GokZ~t6kiMP@vpa$_+gtF0KjJgNdN`n^Q2Oo5x9kj-U8voSQsWCsGJGp z=qRia|0=i9^3H!z!LNwV->P6CZ#VJ74ln=+n7|;Qpo;ad4VOXP0;(LxE^Q1GP{1i= zw`Ca2kt0il2ESXS!mX@QBn(N9Jr+RyS`F(XmNIAW1o6WTF#s@3;B|v44B>qcr~oem zigDM4Oh5yt$eBQnokQsAyNy+Dq0;KkzYP2)@TV5ygW_#-JNFDb8i^7z0nA)}6XT7$ znS=l(;~nrQ0%x#sawd>tn-K>2ypmO3My1tUlnU^>z#kEWoV;yrXJbKkJQOWp0Ei*} zZ=e(?^-`3wgZE7jt>Bv!B%oME@lq6} zB=5?g60TMzU}y%@+B;_gInqEdeJx+As=QjSGS+(Q`9&1J3H*1UO0X6?W#zA)s$$V{Cg?@r-6;MHG1YC} zKI`rD<21$A*8^x5A=L_=#{d63tV_Uuf%pQWp}2OVsxg7j=cUYv;4yPXkfRlZM1BO9 zSE&kDD;HCryCs|ee$~SJYhQvS;0|7R9sJb{KCpXDOA*xV7?J9RR<6|;yTF&F#Qjoi zxvoHt{zVwvbEB&9a#b~T`&+;KA>daOa%cF#+b?$J&L5)VYycbaw=2Sr3|R!c>Pg^h zz_Wp)N!JRd8R^x^Wdh+sP=qsQ0y&yNfJuDrP!(=gRfaZK`|(SdtiG>>y6Z1?IPpUl zFaUtRTk-i9Yi`2xz!x$0falOcUBj@J-x1}pf?^&kPbSJyN0`iWy>fV+u5hh#H{(q9 zfWH8K33x30@E0$3N4eLoU;ywmCa@q%-HB2@NoT22`jHO)Q1Qw_gt?W{)Mh`opLF#sd|qmfd314FR&6ag?Drj@k5s} z0D!++_5+&$U>Nv=z`ejVBzOoKF{*@TDUWgGa~aJNIu9P|1BAcvKIQRBUFB9?Yp2o6 z-M~A5zons+3jDc3eg`9Y0^LdYA-aC8zkg2wg&-&dH2}N`_`lhNG!T{q zKPeVpks`;%rp6X`P06us@G(Uuu2C*Gs~U$@*xYqfI05_~@L`3lhHI{yi644^0fczp zo^^FZUX1BK@WX6^julWv@t9bALyDXd3%8dhsm~Bv_YWzL7pWT8Dv#3U+q(If7Tv!M z{B!u>J7;P;{dk$6r`GlcCNKrOGdL1Mh#R4RWfb4_ZSIvK=SAmk!!Ckgxc;>Ad6BB| z9PMt_ryt@X@V?-f4L|G^;)fnw)9aXkpha;zigyFAV3X1Sz-7@qBsLHD1uo`{pra8I z{%+-SOgr45T*dFWf-Z^X zJ7V#mUtmTw9=VO6Q{ZFVS$mYv^R&YcD6ekWZ)4ZL0DJ)W6GSdF5=gHRKlCUA2=U(O zbp?eKrs(Le0!Vaq6v`$i%-DY{;hurDTb2NAmM z1`rMhJN`A=V~Dm1AK(#8*XoY~3*m>oO!y)CYQ1h?1lIuX5cpX(X(WWT7=j9lGoty9 zZ*yF1E{jnZbEzO(gmXHsglm+`4``PI%BLI{_u3@09E5d<_Edp6T+&kEVei1>OSONQ%;FA)&j#m4F$1Uo1|EW?nQk)|O(w{1_oI zD~zbMOZgm9KF2w%#aG8KU8wBzRO~qVWzDsOjf_AHBX~U~ zzu-l*(5NI1A(49+FTw@UoD_?bzUCsDWg%#N+FJ~vk&tbKAqBhfIjnsSD~~RwXvzD7HIpMd0oB?bCf4sNc?BhMa%P~g7TY!UX;k-5y=^B9>iJ&In1n;XT zsuC~+z!8OuVGMr5wXjCy8bjzdP*7rab%att_(h-)FoGhWkovVr%q1fTrVz(n7^B{o zX{f~);+N0ogx@N12G9nYF#;c5P{=jFD}g&OZp2MT7^cBar~uo9ix@ZFXMryW>bS;# zS|yI0@HdZ~0i?tq?QL3pY!J8}xE;d~j$tH)9IaypcmVhUa3}CECW3Dr{>}N-o&KI( zjhq2wga}3eFbq5k<2|?qcmc+E*jBV*52J=L=6w?*;(rBr9M&83Lga*>4RQvsMTlSs zMc@!dBKSezg&5{AMHg0RP6-cFW#@5BRp+l_cKlNqX0Q%9!EX(62C&75kqp4}GB^r6 z7h^McE^rk_T`1FAs9?nUCxLHcboy@tPXU*sF3JgiYmqa69YBJ*upfAa7}w%;n6%}? zm^ua{bpCH9z!gkh{Ygxk?IX&#?7k12B^T>E2A#f`dLD5Sf)84msKY6Y>Tm>jM(}zl z_1Y-%an=b Date: Fri, 1 Aug 2025 00:57:26 -0500 Subject: [PATCH 13/20] Various changes related to communicating commands --- Parallel.Cli/Commands/BackupCommand.cs | 80 +++++++++++++++++ Parallel.Cli/Commands/PingCommand.cs | 26 ------ Parallel.Cli/Commands/UnzipCommand.cs | 74 ++++++++++++++++ Parallel.Cli/Commands/ZipCommand.cs | 76 ++++++++++++++++ Parallel.Cli/Parallel.Cli.csproj | 5 +- Parallel.Cli/Utils/CommandLine.cs | 87 +++++++++++++++++++ Parallel.Cli/Utils/Formatter.cs | 32 +++++++ Parallel.Cli/Utils/ProgressReport.cs | 21 +++++ Parallel.Cli/Utils/TextWriter.cs | 52 +++++++++++ Parallel.Core.Net/Connections/IConnection.cs | 3 +- .../Connections/TcpConnection.cs | 10 +-- Parallel.Core/IO/Backup/DeltaBackupManager.cs | 4 +- Parallel.Core/IO/Backup/FileBackupManager.cs | 3 +- Parallel.Core/IO/Backup/IBackupManager.cs | 4 +- .../Net}/Communication.cs | 0 .../Net}/MessageResult.cs | 0 Parallel.Core/Parallel.Core.csproj | 4 - Parallel.Service/Responses/MessageResponse.cs | 14 +++ 18 files changed, 450 insertions(+), 45 deletions(-) create mode 100644 Parallel.Cli/Commands/BackupCommand.cs delete mode 100644 Parallel.Cli/Commands/PingCommand.cs create mode 100644 Parallel.Cli/Commands/UnzipCommand.cs create mode 100644 Parallel.Cli/Commands/ZipCommand.cs create mode 100644 Parallel.Cli/Utils/CommandLine.cs create mode 100644 Parallel.Cli/Utils/Formatter.cs create mode 100644 Parallel.Cli/Utils/ProgressReport.cs create mode 100644 Parallel.Cli/Utils/TextWriter.cs rename {Parallel.Core.Net => Parallel.Core/Net}/Communication.cs (100%) rename {Parallel.Core.Net => Parallel.Core/Net}/MessageResult.cs (100%) create mode 100644 Parallel.Service/Responses/MessageResponse.cs diff --git a/Parallel.Cli/Commands/BackupCommand.cs b/Parallel.Cli/Commands/BackupCommand.cs new file mode 100644 index 0000000..5085fe5 --- /dev/null +++ b/Parallel.Cli/Commands/BackupCommand.cs @@ -0,0 +1,80 @@ +// Copyright 2025 Kyle Ebbinga + +using System.CommandLine; +using System.Diagnostics; +using Parallel.Cli.Utils; +using Parallel.Core.Diagnostics; +using Parallel.Core.Settings; + +namespace Parallel.Cli.Commands +{ + public class BackupCommand : Command + { + private Stopwatch _sw = new Stopwatch(); + private IProgress _progress = new ProgressReport(); + + private Command addCmd = new("add", "Adds a new directory to the backup list."); + private Command listCmd = new("list", "Shows all directories in the backup list."); + private Command removeCmd = new("remove", "Removes a directory from the backup list."); + + public Argument pathArg = new("path", "The folder path to backup."); + public Option pathOpt = new(["--path", "-p"], "The folder path to backup."); + + public BackupCommand() : base("backup", "Manages backup folders and files.") + { + this.SetHandler(async (path) => + { + _sw = Stopwatch.StartNew(); + + ProfileConfig profile = ProfileConfig.Load(Program.Settings, config); + if (string.IsNullOrEmpty(path)) + { + await BackupSystemAsync(profile); + } + else + { + await BackupDirectoryAsync(path, profile); + } + }, pathOpt); + + this.AddCommand(addCmd); + addCmd.AddArgument(pathArg); + addCmd.SetHandler((path, config) => + { + ProfileConfig profile = ProfileConfig.Load(Program.Settings, config); + if (!profile.BackupDirectories.Add(path)) + { + CommandLine.WriteLine($"Failed to add '{path}' to the backup list!", ConsoleColor.Red); + return; + } + + CommandLine.WriteLine($"Successfully added '{path}' to the backup list.", ConsoleColor.Green); + }, pathArg); + + this.AddCommand(listCmd); + listCmd.SetHandler(() => + { + ProfileConfig profile = ProfileConfig.Load(Program.Settings, config); + CommandLine.WriteLine($"Backup list for '{profile.Name}' ({profile.Id}):"); + foreach (string folder in profile.BackupDirectories) + { + CommandLine.WriteLine(folder); + } + }); + + this.AddCommand(removeCmd); + removeCmd.AddArgument(pathArg); + removeCmd.SetHandler((path) => + { + ProfileConfig profile = ProfileConfig.Load(Program.Settings, config); + if (!profile.BackupDirectories.Remove(path)) + { + CommandLine.WriteLine($"Failed to remove '{path}' to the backup list!", ConsoleColor.Red); + return; + } + + CommandLine.WriteLine($"Removed path '{path}' from the backup list.", ConsoleColor.Green); + }, pathArg); + } + } +} \ No newline at end of file diff --git a/Parallel.Cli/Commands/PingCommand.cs b/Parallel.Cli/Commands/PingCommand.cs deleted file mode 100644 index d64a50d..0000000 --- a/Parallel.Cli/Commands/PingCommand.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright 2025 Kyle Ebbinga - -using System.CommandLine; -using Parallel.Core.Net; -using Parallel.Core.Net.Connections; -using Parallel.Core.Settings; - -namespace Parallel.Cli.Commands -{ - public class PingCommand : Command - { - private Option Address = new(["--host", "-h"], "The server address."); - private Option Port = new(["--port", "-p"], "The server port."); - - public PingCommand() : base("ping", "Tests the connection to Parallel.") - { - this.AddOption(Address); - this.AddOption(Port); - this.SetHandler(async (address, port) => - { - IConnection connection = new TcpConnection(address, port); - await connection.SendRequestAsync(new ServerRequest("ping", new Dictionary())); - }, Address, Port); - } - } -} \ No newline at end of file diff --git a/Parallel.Cli/Commands/UnzipCommand.cs b/Parallel.Cli/Commands/UnzipCommand.cs new file mode 100644 index 0000000..4248712 --- /dev/null +++ b/Parallel.Cli/Commands/UnzipCommand.cs @@ -0,0 +1,74 @@ +// Copyright 2025 Entex Interactive, LLC + +using System.CommandLine; +using System.Diagnostics; +using System.IO.Compression; +using Parallel.Cli.Utils; + +namespace Parallel.Cli.Commands +{ + public class UnzipCommand : Command + { + private Argument sourceArg = new("path", "The source path of files to unzip."); + private Option keepOpt = new(["--keep", "-k"], "If the original files should be kept."); + + private Stopwatch _sw; + private List _tasks = new List(); + private int _totalTasks = 0; + + public UnzipCommand() : base("unzip", "Unzips files in a directory.") + { + this.AddArgument(sourceArg); + this.AddOption(keepOpt); + this.SetHandler(async (path, keep) => + { + _sw = Stopwatch.StartNew(); + CommandLine.WriteLine($"Scanning for files in {path}...", ConsoleColor.DarkGray); + string[] files = Directory.GetFiles(path, $"*.gz", SearchOption.AllDirectories); + if (files.Length == 0) + { + CommandLine.WriteLine("No files found to unzip!", ConsoleColor.Yellow); + return; + } + + CommandLine.WriteLine($"Unzipping {files.Length.ToString("N0")} files...", ConsoleColor.DarkGray); + _totalTasks = files.Length; + foreach (var file in files) + { + StartDecompressFile(file, keep); + } + + await Task.WhenAll(_tasks); + CommandLine.WriteLine($"Successfully unzipped {files.Length.ToString("N0")} files in {_sw.Elapsed}.", ConsoleColor.Green); + }, sourceArg, keepOpt); + } + + private void StartDecompressFile(string path, bool keep) + { + Task decompTask = Task.Run(() => { DecompressFile(path, keep); }); + decompTask.ContinueWith(t => t.Dispose()); + _tasks.Add(decompTask); + } + + private void DecompressFile(string path, bool keep) + { + if (File.Exists(path)) + { + using (FileStream openFile = File.OpenRead(path)) + using (FileStream createFile = new FileStream(path.Replace(".gz", string.Empty), FileMode.OpenOrCreate)) + using (GZipStream gZip = new GZipStream(openFile, CompressionMode.Decompress)) + { + gZip.CopyTo(createFile); + } + + if (!keep) + { + File.SetAttributes(path, ~FileAttributes.ReadOnly & File.GetAttributes(path)); + File.Delete(path); + } + } + + CommandLine.ProgressBar(_tasks.Count(t => t.IsCompleted), _totalTasks, _sw.Elapsed, ConsoleColor.DarkGray); + } + } +} \ No newline at end of file diff --git a/Parallel.Cli/Commands/ZipCommand.cs b/Parallel.Cli/Commands/ZipCommand.cs new file mode 100644 index 0000000..00a8a42 --- /dev/null +++ b/Parallel.Cli/Commands/ZipCommand.cs @@ -0,0 +1,76 @@ +// Copyright 2025 Entex Interactive, LLC + +using System.CommandLine; +using System.Diagnostics; +using System.IO.Compression; +using System.Runtime.CompilerServices; +using Parallel.Cli.Utils; +using SQLitePCL; + +namespace Parallel.Cli.Commands +{ + public class ZipCommand : Command + { + private Argument sourceArg = new("path", "The source path of files to zip."); + private Option keepOpt = new(["--keep", "-k"], "If the original files should be kept."); + + private Stopwatch _sw = new Stopwatch(); + private List _tasks = new List(); + private int _totalTasks = 0; + + public ZipCommand() : base("zip", "Zips files in a directory.") + { + this.AddArgument(sourceArg); + this.AddOption(keepOpt); + this.SetHandler(async (path, keep) => + { + _sw = Stopwatch.StartNew(); + CommandLine.WriteLine($"Scanning for files in {path}...", ConsoleColor.DarkGray); + string[] files = Directory.EnumerateFiles(path, $"*", SearchOption.AllDirectories).Where(f => !f.EndsWith(".gz")).ToArray(); + if (files.Length == 0) + { + CommandLine.WriteLine("No files found to zip!", ConsoleColor.Yellow); + return; + } + + CommandLine.WriteLine($"Zipping {files.Length.ToString("N0")} files...", ConsoleColor.DarkGray); + _totalTasks = files.Length; + foreach (var file in files) + { + StartCompressFile(file, keep); + } + + await Task.WhenAll(_tasks); + CommandLine.WriteLine($"Successfully zipped {files.Length.ToString("N0")} files in {_sw.Elapsed}.", ConsoleColor.Green); + }, sourceArg, keepOpt); + } + + private void StartCompressFile(string path, bool keep) + { + Task compTask = Task.Run(() => { CompressFile(path, keep); }); + compTask.ContinueWith(t => t.Dispose()); + _tasks.Add(compTask); + } + + private void CompressFile(string path, bool keep) + { + if (File.Exists(path)) + { + using (FileStream openFile = File.OpenRead(path)) + using (FileStream createFile = new FileStream($"{path}.gz", FileMode.OpenOrCreate)) + using (GZipStream gZip = new GZipStream(createFile, CompressionLevel.SmallestSize)) + { + openFile.CopyTo(gZip); + } + + if (!keep) + { + File.SetAttributes(path, ~FileAttributes.ReadOnly & File.GetAttributes(path)); + File.Delete(path); + } + } + + CommandLine.ProgressBar(_tasks.Count(t => t.IsCompleted), _totalTasks, _sw.Elapsed, ConsoleColor.DarkGray); + } + } +} \ No newline at end of file diff --git a/Parallel.Cli/Parallel.Cli.csproj b/Parallel.Cli/Parallel.Cli.csproj index 3337169..fb16134 100644 --- a/Parallel.Cli/Parallel.Cli.csproj +++ b/Parallel.Cli/Parallel.Cli.csproj @@ -28,8 +28,11 @@ - + + + + diff --git a/Parallel.Cli/Utils/CommandLine.cs b/Parallel.Cli/Utils/CommandLine.cs new file mode 100644 index 0000000..39c5f9a --- /dev/null +++ b/Parallel.Cli/Utils/CommandLine.cs @@ -0,0 +1,87 @@ +// Copyright 2025 Entex Interactive, LLC + +using System.Text; +using Parallel.Core.Utils; + +namespace Parallel.Cli.Utils +{ + public class CommandLine + { + public static string? ReadLine(object value, ConsoleColor color = ConsoleColor.Gray) + { + Console.ForegroundColor = color; + Console.Write($"> {value}: "); + Console.ResetColor(); + return Console.ReadLine(); + } + + public static string? ReadPassword(object value, ConsoleColor color = ConsoleColor.Gray) + { + string password = string.Empty; + Console.ForegroundColor = color; + Console.Write($"> {value}: "); + Console.ResetColor(); + ConsoleKeyInfo key; + + do + { + key = Console.ReadKey(true); + if (key.Key != ConsoleKey.Enter && key.Key != ConsoleKey.Backspace) + { + password += key.KeyChar; + } + else if (key.Key == ConsoleKey.Backspace && password.Length > 0) + { + password = password.Substring(0, password.Length - 1); + } + } while (key.Key != ConsoleKey.Enter); + + Console.WriteLine(); + return Encryption.Encode(password); + } + + public static void Write(object value, ConsoleColor color = ConsoleColor.Gray) + { + Console.ForegroundColor = color; + Console.Write("\r" + value?.ToString()?.PadRight(Console.WindowWidth)); + Console.ResetColor(); + } + + public static void WriteLine(object value, ConsoleColor color = ConsoleColor.Gray) + { + Console.ForegroundColor = color; + Console.WriteLine($"> {value}"); + Console.ResetColor(); + } + + public static void ProgressBar(double part, double total, TimeSpan elapsed, ConsoleColor color = ConsoleColor.Gray) + { + double percent = part / total; + string percentStr = $"> Progress: {Convert.ToInt32(percent * 100).ToString("D2")}%"; + + TimeSpan remaining; + double remainingMs = elapsed.TotalMilliseconds * (total - part) / part; + if (remainingMs <= TimeSpan.MaxValue.TotalMilliseconds) + remaining = TimeSpan.FromMilliseconds(remainingMs); + else + remaining = TimeSpan.MaxValue; + + string remainingStr = $"{remaining.Hours:00}:{remaining.Minutes:00}:{remaining.Seconds:00} remaining"; + int barWidth = Console.WindowWidth - percentStr.Length - remainingStr.Length - 4; + int filledWidth = Convert.ToInt32(percent * barWidth); + + StringBuilder progressBar = new StringBuilder(barWidth); + for (int i = 0; i < filledWidth; i++) + { + progressBar.Append('#'); + } + + for (int i = filledWidth; i < barWidth; i++) + { + progressBar.Append('.'); + } + + Console.Write($"\r{percentStr} [{progressBar.ToString()}] {remainingStr}"); + } + } +} \ No newline at end of file diff --git a/Parallel.Cli/Utils/Formatter.cs b/Parallel.Cli/Utils/Formatter.cs new file mode 100644 index 0000000..f2ca3d3 --- /dev/null +++ b/Parallel.Cli/Utils/Formatter.cs @@ -0,0 +1,32 @@ +// Copyright 2025 Entex Interactive, LLC + +namespace Parallel.Cli.Utils +{ + public class Formatter + { + public static string FromBytes(long bytes) + { + string[] sizeSuffixes = { "Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB" }; + int sizeIndex = 0; + double size = bytes; + + while (size >= 1000 && sizeIndex < sizeSuffixes.Length - 1) + { + sizeIndex++; + size /= 1000; + } + + return $"{size:N2} {sizeSuffixes[sizeIndex]}"; + } + + public static string FromDateTime(DateTime dateTime) + { + return dateTime.ToLocalTime().ToString("MM/dd/yyyy hh:mmtt"); + } + + public static string FromTimeSpan(TimeSpan timeSpan) + { + return $"{timeSpan.Hours:00}:{timeSpan.Minutes:00}:{timeSpan.Seconds:00}.{timeSpan.Milliseconds:N2}"; + } + } +} \ No newline at end of file diff --git a/Parallel.Cli/Utils/ProgressReport.cs b/Parallel.Cli/Utils/ProgressReport.cs new file mode 100644 index 0000000..c314b69 --- /dev/null +++ b/Parallel.Cli/Utils/ProgressReport.cs @@ -0,0 +1,21 @@ +// Copyright 2025 Entex Interactive, LLC + +using Parallel.Core.Diagnostics; +using Parallel.Core.Models; + +namespace Parallel.Cli.Utils +{ + public class ProgressReport : IProgress + { + public void Report(ProgressOperation operation, SystemFile file, int current, int total) + { + int percent = current * 100 / total; + CommandLine.WriteLine($"[{percent}%] {operation}: {file.LocalPath}"); + } + + public void Failed(Exception exception, SystemFile file) + { + CommandLine.WriteLine($"Failed to upload file: '{file.LocalPath}'", ConsoleColor.Red); + } + } +} \ No newline at end of file diff --git a/Parallel.Cli/Utils/TextWriter.cs b/Parallel.Cli/Utils/TextWriter.cs new file mode 100644 index 0000000..c86d8b4 --- /dev/null +++ b/Parallel.Cli/Utils/TextWriter.cs @@ -0,0 +1,52 @@ +// Copyright 2025 Entex Interactive, LLC + +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Parallel.Cli.Utils +{ + public class TextWriter + { + public static string CreateTxtFile(string text) + { + string parentDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "Parallel"); + if (!Directory.Exists(parentDir)) Directory.CreateDirectory(parentDir); + + string fileName = Path.Combine(parentDir, DateTime.Now.ToString("MM-dd-yyyy hh-mm-ss") + ".json"); + File.WriteAllText(fileName, text); + return fileName; + } + + public static string CreateTxtFile(params string[] lines) + { + string parentDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "Parallel"); + if (!Directory.Exists(parentDir)) Directory.CreateDirectory(parentDir); + + string fileName = Path.Combine(parentDir, DateTime.Now.ToString("MM-dd-yyyy hh-mm-ss") + ".txt"); + File.WriteAllLines(fileName, lines); + return fileName; + } + + public static string CreateJsonFile(params string[] lines) + { + string parentDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "Parallel"); + if (!Directory.Exists(parentDir)) Directory.CreateDirectory(parentDir); + + string fileName = Path.Combine(parentDir, DateTime.Now.ToString("MM-dd-yyyy hh-mm-ss") + ".json"); + JArray json = JArray.FromObject(lines); + + File.WriteAllText(fileName, JsonConvert.SerializeObject(json, Newtonsoft.Json.Formatting.Indented)); + return fileName; + } + + public static string CreateJsonFile(JArray json) + { + string parentDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "Parallel"); + if (!Directory.Exists(parentDir)) Directory.CreateDirectory(parentDir); + + string fileName = Path.Combine(parentDir, DateTime.Now.ToString("MM-dd-yyyy hh-mm-ss") + ".json"); + File.WriteAllText(fileName, JsonConvert.SerializeObject(json, Newtonsoft.Json.Formatting.Indented)); + return fileName; + } + } +} \ No newline at end of file diff --git a/Parallel.Core.Net/Connections/IConnection.cs b/Parallel.Core.Net/Connections/IConnection.cs index 40b26fc..b87b4e4 100644 --- a/Parallel.Core.Net/Connections/IConnection.cs +++ b/Parallel.Core.Net/Connections/IConnection.cs @@ -4,6 +4,7 @@ namespace Parallel.Core.Net.Connections { public interface IConnection { - Task SendRequestAsync(ServerRequest request); + ServerResponse SendRequest(ServerRequest request); + //Task SendRequestAsync(ServerRequest request); } } \ No newline at end of file diff --git a/Parallel.Core.Net/Connections/TcpConnection.cs b/Parallel.Core.Net/Connections/TcpConnection.cs index 1407803..7e75d9c 100644 --- a/Parallel.Core.Net/Connections/TcpConnection.cs +++ b/Parallel.Core.Net/Connections/TcpConnection.cs @@ -16,7 +16,7 @@ public class TcpConnection : IConnection private readonly int _port; ///

- /// Initializes a new instance of the class with the saved settings. + /// Initializes a new instance of the class with the saved settings. /// public TcpConnection() { @@ -25,7 +25,7 @@ public TcpConnection() } /// - /// Initializes a new instance of the class with a address and port. + /// Initializes a new instance of the class with a address and port. /// public TcpConnection(string address, int port) { @@ -33,7 +33,7 @@ public TcpConnection(string address, int port) _port = port; } - public async Task SendRequestAsync(ServerRequest request) + public ServerResponse SendRequest(ServerRequest request) { Socket socket = new(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); @@ -45,10 +45,6 @@ public async Task SendRequestAsync(ServerRequest request) socket.Connect(_address, _port); if (socket.Connected) { - Console.WriteLine($"Connected: {socket.Connected}"); - Console.WriteLine($"Available: {socket.Available}"); - - // Sends an encrypted json request to the server. string rawJson = JsonConvert.SerializeObject(request) + ";"; Log.Debug($"Sending request: '{rawJson}'"); diff --git a/Parallel.Core/IO/Backup/DeltaBackupManager.cs b/Parallel.Core/IO/Backup/DeltaBackupManager.cs index 56b8754..ccf03bb 100644 --- a/Parallel.Core/IO/Backup/DeltaBackupManager.cs +++ b/Parallel.Core/IO/Backup/DeltaBackupManager.cs @@ -18,13 +18,13 @@ public class DeltaBackupManager : BaseFileManager public DeltaBackupManager(ProfileConfig profile) : base(profile) { } /// - public override Task BackupFilesAsync(SystemFile[] systemFiles, IProgress progress) + public override Task BackupFilesAsync(SystemFile[] files, IProgress progress) { throw new NotImplementedException(); } /// - public override Task RestoreFilesAsync(SystemFile[] systemFiles, IProgress progress) + public override Task RestoreFilesAsync(SystemFile[] files, IProgress progress) { throw new NotImplementedException(); } diff --git a/Parallel.Core/IO/Backup/FileBackupManager.cs b/Parallel.Core/IO/Backup/FileBackupManager.cs index 32bad08..0bc1c6b 100644 --- a/Parallel.Core/IO/Backup/FileBackupManager.cs +++ b/Parallel.Core/IO/Backup/FileBackupManager.cs @@ -36,9 +36,9 @@ public override async Task BackupFilesAsync(SystemFile[] files, IProgress progre SystemFile file = files.ElementAt(i); if (file.IsDeleted) { + progress.Report(ProgressOperation.Archiving, file, i, files.Length); Database.AddHistory(file.LocalPath, HistoryType.Archived); Database.AddFile(file); - progress.Report(ProgressOperation.Archiving, file, i, files.Length); } else { @@ -62,7 +62,6 @@ public override async Task RestoreFilesAsync(SystemFile[] files, IProgress progr if (!restoreFiles.Any()) return; await FileSystem.DownloadFilesAsync(restoreFiles, progress); - for (int i = 0; i < files.Length; i++) { SystemFile file = files[i]; diff --git a/Parallel.Core/IO/Backup/IBackupManager.cs b/Parallel.Core/IO/Backup/IBackupManager.cs index ad6f6b5..7dd71d8 100644 --- a/Parallel.Core/IO/Backup/IBackupManager.cs +++ b/Parallel.Core/IO/Backup/IBackupManager.cs @@ -47,14 +47,14 @@ public interface IBackupManager bool Initialize(); /// - /// Backs up an array of files as fast as possible. + /// Backs up a path. Can be either a file or directory. /// /// /// Task BackupFilesAsync(SystemFile[] files, IProgress progress); /// - /// Restores an array of files as fast as possible. + /// Restores a path. Can be either a file or directory. /// /// /// diff --git a/Parallel.Core.Net/Communication.cs b/Parallel.Core/Net/Communication.cs similarity index 100% rename from Parallel.Core.Net/Communication.cs rename to Parallel.Core/Net/Communication.cs diff --git a/Parallel.Core.Net/MessageResult.cs b/Parallel.Core/Net/MessageResult.cs similarity index 100% rename from Parallel.Core.Net/MessageResult.cs rename to Parallel.Core/Net/MessageResult.cs diff --git a/Parallel.Core/Parallel.Core.csproj b/Parallel.Core/Parallel.Core.csproj index aece14c..da23636 100644 --- a/Parallel.Core/Parallel.Core.csproj +++ b/Parallel.Core/Parallel.Core.csproj @@ -37,8 +37,4 @@ - - - - diff --git a/Parallel.Service/Responses/MessageResponse.cs b/Parallel.Service/Responses/MessageResponse.cs new file mode 100644 index 0000000..5f6c383 --- /dev/null +++ b/Parallel.Service/Responses/MessageResponse.cs @@ -0,0 +1,14 @@ +// Copyright 2025 Kyle Ebbinga + +namespace Parallel.Service.Responses +{ + public class MessageResponse + { + public string Message { get; set; } + + public MessageResponse(string message) + { + Message = message; + } + } +} \ No newline at end of file From 4c5a0d663c8ce0fb38408b5aa0d219a88cd3288c Mon Sep 17 00:00:00 2001 From: Kyle | Guitar <26614720+TheGuitarleader@users.noreply.github.com> Date: Fri, 1 Aug 2025 01:39:29 -0500 Subject: [PATCH 14/20] Reformatted code. --- Parallel.Cli/Commands/BackupCommand.cs | 80 ------------------- Parallel.Cli/Commands/UnzipCommand.cs | 26 +++--- Parallel.Cli/Commands/ZipCommand.cs | 24 +++--- Parallel.Cli/Parallel.Cli.csproj | 40 +++++----- Parallel.Cli/Program.cs | 2 +- Parallel.Cli/Utils/CommandLine.cs | 10 +-- Parallel.Cli/Utils/Formatter.cs | 4 +- Parallel.Cli/Utils/ProgressReport.cs | 2 +- Parallel.Cli/Utils/TextWriter.cs | 2 +- Parallel.Core.Net/Parallel.Core.Net.csproj | 6 +- Parallel.Core/Data/FileTypes.cs | 8 +- Parallel.Core/Database/SqliteDatabase.cs | 4 +- Parallel.Core/Parallel.Core.csproj | 14 ++-- Parallel.Service/Parallel.Service.csproj | 50 ++++++------ Parallel.Service/RequestHandler.cs | 13 +-- Parallel.Service/Requests/LoginRequest.cs | 6 +- .../Services/TcpRequestService.cs | 8 +- 17 files changed, 115 insertions(+), 184 deletions(-) delete mode 100644 Parallel.Cli/Commands/BackupCommand.cs diff --git a/Parallel.Cli/Commands/BackupCommand.cs b/Parallel.Cli/Commands/BackupCommand.cs deleted file mode 100644 index 5085fe5..0000000 --- a/Parallel.Cli/Commands/BackupCommand.cs +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright 2025 Kyle Ebbinga - -using System.CommandLine; -using System.Diagnostics; -using Parallel.Cli.Utils; -using Parallel.Core.Diagnostics; -using Parallel.Core.Settings; - -namespace Parallel.Cli.Commands -{ - public class BackupCommand : Command - { - private Stopwatch _sw = new Stopwatch(); - private IProgress _progress = new ProgressReport(); - - private Command addCmd = new("add", "Adds a new directory to the backup list."); - private Command listCmd = new("list", "Shows all directories in the backup list."); - private Command removeCmd = new("remove", "Removes a directory from the backup list."); - - public Argument pathArg = new("path", "The folder path to backup."); - public Option pathOpt = new(["--path", "-p"], "The folder path to backup."); - - public BackupCommand() : base("backup", "Manages backup folders and files.") - { - this.SetHandler(async (path) => - { - _sw = Stopwatch.StartNew(); - - ProfileConfig profile = ProfileConfig.Load(Program.Settings, config); - if (string.IsNullOrEmpty(path)) - { - await BackupSystemAsync(profile); - } - else - { - await BackupDirectoryAsync(path, profile); - } - }, pathOpt); - - this.AddCommand(addCmd); - addCmd.AddArgument(pathArg); - addCmd.SetHandler((path, config) => - { - ProfileConfig profile = ProfileConfig.Load(Program.Settings, config); - if (!profile.BackupDirectories.Add(path)) - { - CommandLine.WriteLine($"Failed to add '{path}' to the backup list!", ConsoleColor.Red); - return; - } - - CommandLine.WriteLine($"Successfully added '{path}' to the backup list.", ConsoleColor.Green); - }, pathArg); - - this.AddCommand(listCmd); - listCmd.SetHandler(() => - { - ProfileConfig profile = ProfileConfig.Load(Program.Settings, config); - CommandLine.WriteLine($"Backup list for '{profile.Name}' ({profile.Id}):"); - foreach (string folder in profile.BackupDirectories) - { - CommandLine.WriteLine(folder); - } - }); - - this.AddCommand(removeCmd); - removeCmd.AddArgument(pathArg); - removeCmd.SetHandler((path) => - { - ProfileConfig profile = ProfileConfig.Load(Program.Settings, config); - if (!profile.BackupDirectories.Remove(path)) - { - CommandLine.WriteLine($"Failed to remove '{path}' to the backup list!", ConsoleColor.Red); - return; - } - - CommandLine.WriteLine($"Removed path '{path}' from the backup list.", ConsoleColor.Green); - }, pathArg); - } - } -} \ No newline at end of file diff --git a/Parallel.Cli/Commands/UnzipCommand.cs b/Parallel.Cli/Commands/UnzipCommand.cs index 4248712..385bad6 100644 --- a/Parallel.Cli/Commands/UnzipCommand.cs +++ b/Parallel.Cli/Commands/UnzipCommand.cs @@ -1,4 +1,4 @@ -// Copyright 2025 Entex Interactive, LLC +// Copyright 2025 Kyle Ebbinga using System.CommandLine; using System.Diagnostics; @@ -9,13 +9,13 @@ namespace Parallel.Cli.Commands { public class UnzipCommand : Command { - private Argument sourceArg = new("path", "The source path of files to unzip."); - private Option keepOpt = new(["--keep", "-k"], "If the original files should be kept."); - + private readonly Argument sourceArg = new("path", "The source path of files to unzip."); + private readonly Option keepOpt = new(["--keep", "-k"], "If the original files should be kept."); + private Stopwatch _sw; - private List _tasks = new List(); + private readonly List _tasks = new List(); private int _totalTasks = 0; - + public UnzipCommand() : base("unzip", "Unzips files in a directory.") { this.AddArgument(sourceArg); @@ -30,14 +30,14 @@ public UnzipCommand() : base("unzip", "Unzips files in a directory.") CommandLine.WriteLine("No files found to unzip!", ConsoleColor.Yellow); return; } - + CommandLine.WriteLine($"Unzipping {files.Length.ToString("N0")} files...", ConsoleColor.DarkGray); _totalTasks = files.Length; - foreach (var file in files) + foreach (string file in files) { StartDecompressFile(file, keep); } - + await Task.WhenAll(_tasks); CommandLine.WriteLine($"Successfully unzipped {files.Length.ToString("N0")} files in {_sw.Elapsed}.", ConsoleColor.Green); }, sourceArg, keepOpt); @@ -45,7 +45,11 @@ public UnzipCommand() : base("unzip", "Unzips files in a directory.") private void StartDecompressFile(string path, bool keep) { - Task decompTask = Task.Run(() => { DecompressFile(path, keep); }); + Task decompTask = Task.Run(() => + { + DecompressFile(path, keep); + }); + decompTask.ContinueWith(t => t.Dispose()); _tasks.Add(decompTask); } @@ -67,7 +71,7 @@ private void DecompressFile(string path, bool keep) File.Delete(path); } } - + CommandLine.ProgressBar(_tasks.Count(t => t.IsCompleted), _totalTasks, _sw.Elapsed, ConsoleColor.DarkGray); } } diff --git a/Parallel.Cli/Commands/ZipCommand.cs b/Parallel.Cli/Commands/ZipCommand.cs index 00a8a42..3dd1a5d 100644 --- a/Parallel.Cli/Commands/ZipCommand.cs +++ b/Parallel.Cli/Commands/ZipCommand.cs @@ -1,4 +1,4 @@ -// Copyright 2025 Entex Interactive, LLC +// Copyright 2025 Kyle Ebbinga using System.CommandLine; using System.Diagnostics; @@ -11,13 +11,13 @@ namespace Parallel.Cli.Commands { public class ZipCommand : Command { - private Argument sourceArg = new("path", "The source path of files to zip."); - private Option keepOpt = new(["--keep", "-k"], "If the original files should be kept."); + private readonly Argument sourceArg = new("path", "The source path of files to zip."); + private readonly Option keepOpt = new(["--keep", "-k"], "If the original files should be kept."); private Stopwatch _sw = new Stopwatch(); - private List _tasks = new List(); + private readonly List _tasks = new List(); private int _totalTasks = 0; - + public ZipCommand() : base("zip", "Zips files in a directory.") { this.AddArgument(sourceArg); @@ -32,14 +32,14 @@ public ZipCommand() : base("zip", "Zips files in a directory.") CommandLine.WriteLine("No files found to zip!", ConsoleColor.Yellow); return; } - + CommandLine.WriteLine($"Zipping {files.Length.ToString("N0")} files...", ConsoleColor.DarkGray); _totalTasks = files.Length; - foreach (var file in files) + foreach (string file in files) { StartCompressFile(file, keep); } - + await Task.WhenAll(_tasks); CommandLine.WriteLine($"Successfully zipped {files.Length.ToString("N0")} files in {_sw.Elapsed}.", ConsoleColor.Green); }, sourceArg, keepOpt); @@ -47,7 +47,11 @@ public ZipCommand() : base("zip", "Zips files in a directory.") private void StartCompressFile(string path, bool keep) { - Task compTask = Task.Run(() => { CompressFile(path, keep); }); + Task compTask = Task.Run(() => + { + CompressFile(path, keep); + }); + compTask.ContinueWith(t => t.Dispose()); _tasks.Add(compTask); } @@ -69,7 +73,7 @@ private void CompressFile(string path, bool keep) File.Delete(path); } } - + CommandLine.ProgressBar(_tasks.Count(t => t.IsCompleted), _totalTasks, _sw.Elapsed, ConsoleColor.DarkGray); } } diff --git a/Parallel.Cli/Parallel.Cli.csproj b/Parallel.Cli/Parallel.Cli.csproj index fb16134..d042abc 100644 --- a/Parallel.Cli/Parallel.Cli.csproj +++ b/Parallel.Cli/Parallel.Cli.csproj @@ -1,38 +1,38 @@  - Exe - net9.0 - parallel-red.ico - enable - enable - Parallel - 1.0.0 - Kyle Ebbinga - Copyright $(Company). All Rights Reserved. - $(AssemblyVersion) - $(VersionPrefix)$(AssemblyVersion) - $(Company) + Exe + net9.0 + parallel-red.ico + enable + enable + Parallel + 1.0.0 + Kyle Ebbinga + Copyright $(Company). All Rights Reserved. + $(AssemblyVersion) + $(VersionPrefix)$(AssemblyVersion) + $(Company) - - - - + + + + - - + + - + - + diff --git a/Parallel.Cli/Program.cs b/Parallel.Cli/Program.cs index 2a66d13..c459355 100644 --- a/Parallel.Cli/Program.cs +++ b/Parallel.Cli/Program.cs @@ -23,7 +23,7 @@ public static async Task Main(string[] args) RootCommand rootCommand = new("Parallel file manager - Easily back up and sync massive amounts of files, and save on drive space in the process."); Type[] types = Assembly.GetExecutingAssembly().GetTypes().Where(t => t.IsSubclassOf(typeof(Command)) && t.IsClass).ToArray(); - foreach (var type in types) rootCommand.AddCommand((Command)Activator.CreateInstance(type)!); + foreach (Type? type in types) rootCommand.AddCommand((Command)Activator.CreateInstance(type)!); await rootCommand.InvokeAsync(args); Settings.Save(); diff --git a/Parallel.Cli/Utils/CommandLine.cs b/Parallel.Cli/Utils/CommandLine.cs index 39c5f9a..2cd26ff 100644 --- a/Parallel.Cli/Utils/CommandLine.cs +++ b/Parallel.Cli/Utils/CommandLine.cs @@ -1,4 +1,4 @@ -// Copyright 2025 Entex Interactive, LLC +// Copyright 2025 Kyle Ebbinga using System.Text; using Parallel.Core.Utils; @@ -14,7 +14,7 @@ public class CommandLine Console.ResetColor(); return Console.ReadLine(); } - + public static string? ReadPassword(object value, ConsoleColor color = ConsoleColor.Gray) { string password = string.Empty; @@ -35,7 +35,7 @@ public class CommandLine password = password.Substring(0, password.Length - 1); } } while (key.Key != ConsoleKey.Enter); - + Console.WriteLine(); return Encryption.Encode(password); } @@ -69,13 +69,13 @@ public static void ProgressBar(double part, double total, TimeSpan elapsed, Cons string remainingStr = $"{remaining.Hours:00}:{remaining.Minutes:00}:{remaining.Seconds:00} remaining"; int barWidth = Console.WindowWidth - percentStr.Length - remainingStr.Length - 4; int filledWidth = Convert.ToInt32(percent * barWidth); - + StringBuilder progressBar = new StringBuilder(barWidth); for (int i = 0; i < filledWidth; i++) { progressBar.Append('#'); } - + for (int i = filledWidth; i < barWidth; i++) { progressBar.Append('.'); diff --git a/Parallel.Cli/Utils/Formatter.cs b/Parallel.Cli/Utils/Formatter.cs index f2ca3d3..f00ee78 100644 --- a/Parallel.Cli/Utils/Formatter.cs +++ b/Parallel.Cli/Utils/Formatter.cs @@ -1,4 +1,4 @@ -// Copyright 2025 Entex Interactive, LLC +// Copyright 2025 Kyle Ebbinga namespace Parallel.Cli.Utils { @@ -23,7 +23,7 @@ public static string FromDateTime(DateTime dateTime) { return dateTime.ToLocalTime().ToString("MM/dd/yyyy hh:mmtt"); } - + public static string FromTimeSpan(TimeSpan timeSpan) { return $"{timeSpan.Hours:00}:{timeSpan.Minutes:00}:{timeSpan.Seconds:00}.{timeSpan.Milliseconds:N2}"; diff --git a/Parallel.Cli/Utils/ProgressReport.cs b/Parallel.Cli/Utils/ProgressReport.cs index c314b69..a00cd16 100644 --- a/Parallel.Cli/Utils/ProgressReport.cs +++ b/Parallel.Cli/Utils/ProgressReport.cs @@ -1,4 +1,4 @@ -// Copyright 2025 Entex Interactive, LLC +// Copyright 2025 Kyle Ebbinga using Parallel.Core.Diagnostics; using Parallel.Core.Models; diff --git a/Parallel.Cli/Utils/TextWriter.cs b/Parallel.Cli/Utils/TextWriter.cs index c86d8b4..01848bc 100644 --- a/Parallel.Cli/Utils/TextWriter.cs +++ b/Parallel.Cli/Utils/TextWriter.cs @@ -1,4 +1,4 @@ -// Copyright 2025 Entex Interactive, LLC +// Copyright 2025 Kyle Ebbinga using Newtonsoft.Json; using Newtonsoft.Json.Linq; diff --git a/Parallel.Core.Net/Parallel.Core.Net.csproj b/Parallel.Core.Net/Parallel.Core.Net.csproj index bf55236..be6daf4 100644 --- a/Parallel.Core.Net/Parallel.Core.Net.csproj +++ b/Parallel.Core.Net/Parallel.Core.Net.csproj @@ -7,12 +7,12 @@ - + - - + + diff --git a/Parallel.Core/Data/FileTypes.cs b/Parallel.Core/Data/FileTypes.cs index cd6fd24..919047f 100644 --- a/Parallel.Core/Data/FileTypes.cs +++ b/Parallel.Core/Data/FileTypes.cs @@ -132,7 +132,7 @@ public static FileCategory GetFileCategory(string extension) ".f4v" => FileCategory.Video, ".webm" => FileCategory.Video, - _ => FileCategory.Other, + _ => FileCategory.Other }; } @@ -140,13 +140,12 @@ public static FileCategory FromString(string type) { return type switch { - "Other" => FileCategory.Other, "Document" => FileCategory.Document, "Photo" => FileCategory.Photo, "Music" => FileCategory.Music, "Video" => FileCategory.Video, - _ => FileCategory.Other, + _ => FileCategory.Other }; } @@ -154,13 +153,12 @@ public static string ToString(FileCategory type) { return type switch { - FileCategory.Other => "Other", FileCategory.Document => "Document", FileCategory.Photo => "Photo", FileCategory.Music => "Music", FileCategory.Video => "Video", - _ => "Other", + _ => "Other" }; } } diff --git a/Parallel.Core/Database/SqliteDatabase.cs b/Parallel.Core/Database/SqliteDatabase.cs index a19e390..a8932b6 100644 --- a/Parallel.Core/Database/SqliteDatabase.cs +++ b/Parallel.Core/Database/SqliteDatabase.cs @@ -36,7 +36,9 @@ public void Initialize() File.SetAttributes(FilePath, File.GetAttributes(FilePath) | FileAttributes.Hidden); // Create tables - RunQuery("CREATE TABLE IF NOT EXISTS `files` (`profile` TEXT NOT NULL, `id` TEXT NOT NULL, `name` TEXT NOT NULL, `localpath` TEXT NOT NULL, `remotepath` TEXT NOT NULL, `lastwrite` LONG INTEGER NOT NULL, `lastupdate` LONG INTEGER NOT NULL, `LocalSize` LONG INTEGER NOT NULL, `RemoteSize` LONG INTEGER NOT NULL, `type` TEXT NOT NULL DEFAULT Other CHECK(`type` IN ('Document', 'Photo', 'Music', 'Video', 'Other')), `hidden` INTEGER NOT NULL DEFAULT 0, `readonly` INTEGER NOT NULL DEFAULT 0, `deleted` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`profile`, `id`));"); + RunQuery( + "CREATE TABLE IF NOT EXISTS `files` (`profile` TEXT NOT NULL, `id` TEXT NOT NULL, `name` TEXT NOT NULL, `localpath` TEXT NOT NULL, `remotepath` TEXT NOT NULL, `lastwrite` LONG INTEGER NOT NULL, `lastupdate` LONG INTEGER NOT NULL, `LocalSize` LONG INTEGER NOT NULL, `RemoteSize` LONG INTEGER NOT NULL, `type` TEXT NOT NULL DEFAULT Other CHECK(`type` IN ('Document', 'Photo', 'Music', 'Video', 'Other')), `hidden` INTEGER NOT NULL DEFAULT 0, `readonly` INTEGER NOT NULL DEFAULT 0, `deleted` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`profile`, `id`));"); + RunQuery("CREATE TABLE IF NOT EXISTS `history` (`profile` TEXT NOT NULL, `timestamp` LONG INTEGER NOT NULL, `id` TEXT NOT NULL, `path` TEXT NOT NULL, `type` TEXT NOT NULL, PRIMARY KEY(`profile`, `timestamp`));"); } diff --git a/Parallel.Core/Parallel.Core.csproj b/Parallel.Core/Parallel.Core.csproj index da23636..7d6a8eb 100644 --- a/Parallel.Core/Parallel.Core.csproj +++ b/Parallel.Core/Parallel.Core.csproj @@ -16,11 +16,11 @@ - - - - - + + + + + @@ -33,8 +33,8 @@ - - + + diff --git a/Parallel.Service/Parallel.Service.csproj b/Parallel.Service/Parallel.Service.csproj index bb63011..bfde18a 100644 --- a/Parallel.Service/Parallel.Service.csproj +++ b/Parallel.Service/Parallel.Service.csproj @@ -1,40 +1,40 @@  - Exe - net9.0 - enable - parallel-red.ico - enable - ParallelService - 1.0.0 - Kyle Ebbinga - Copyright $(Company). All Rights Reserved. - $(AssemblyVersion) - $(VersionPrefix)$(AssemblyVersion) - $(Company) + Exe + net9.0 + enable + parallel-red.ico + enable + ParallelService + 1.0.0 + Kyle Ebbinga + Copyright $(Company). All Rights Reserved. + $(AssemblyVersion) + $(VersionPrefix)$(AssemblyVersion) + $(Company) - - - - - - - - - + + + + + + + + + - - + + - - + + diff --git a/Parallel.Service/RequestHandler.cs b/Parallel.Service/RequestHandler.cs index 9a08f61..118dcf5 100644 --- a/Parallel.Service/RequestHandler.cs +++ b/Parallel.Service/RequestHandler.cs @@ -1,6 +1,7 @@ // Copyright 2025 Kyle Ebbinga using System.ComponentModel.DataAnnotations; +using System.Reflection; using Parallel.Core.Net; using Parallel.Service.Requests; @@ -46,21 +47,21 @@ public IRequest CreateNew(ServerRequest request) throw new InvalidOperationException($"Type '{requestType.Name}' does not implement IRequest."); // Map parameters to object properties - foreach (var prop in requestType.GetProperties()) + foreach (PropertyInfo? prop in requestType.GetProperties()) { - if (request.Parameters.TryGetValue(prop.Name, out var value)) + if (request.Parameters.TryGetValue(prop.Name, out string? value)) { - var converted = Convert.ChangeType(value, prop.PropertyType); + object? converted = Convert.ChangeType(value, prop.PropertyType); prop.SetValue(instance, converted); } } // Validate required properties - var validationResults = new List(); - var context = new ValidationContext(instance, serviceProvider: null, items: null); + List? validationResults = new List(); + ValidationContext? context = new ValidationContext(instance, serviceProvider: null, items: null); if (!Validator.TryValidateObject(instance, context, validationResults, validateAllProperties: true)) { - var errors = string.Join("; ", validationResults.Select(r => r.ErrorMessage)); + string? errors = string.Join("; ", validationResults.Select(r => r.ErrorMessage)); Log.Warning($"Validation failed for '{request.Name}': {errors}"); throw new InvalidOperationException($"Validation failed: {errors}"); } diff --git a/Parallel.Service/Requests/LoginRequest.cs b/Parallel.Service/Requests/LoginRequest.cs index 8972719..20e4f75 100644 --- a/Parallel.Service/Requests/LoginRequest.cs +++ b/Parallel.Service/Requests/LoginRequest.cs @@ -10,11 +10,9 @@ namespace Parallel.Service.Requests [Description("Logins into the server.")] public class LoginRequest : BaseRequest { - [Required] - public string Username { get; set; } + [Required] public string Username { get; set; } - [Required] - public string Password { get; set; } + [Required] public string Password { get; set; } public override Task ExecuteAsync() { diff --git a/Parallel.Service/Services/TcpRequestService.cs b/Parallel.Service/Services/TcpRequestService.cs index a69ca91..ceabb94 100644 --- a/Parallel.Service/Services/TcpRequestService.cs +++ b/Parallel.Service/Services/TcpRequestService.cs @@ -34,7 +34,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) try { // Starts listening for requests over the TCP socket. - IPAddress address = string.IsNullOrEmpty(_settings.Address) ? IPAddress.Any : IPAddress.Parse( _settings.Address); + IPAddress address = string.IsNullOrEmpty(_settings.Address) ? IPAddress.Any : IPAddress.Parse(_settings.Address); _listener.Bind(new IPEndPoint(address, _settings.ListenerPort)); _listener.Listen(5); } @@ -57,7 +57,11 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) private void StartHandlingRequests(Socket socket, CancellationToken token) { TcpSocketHandler handler = new(socket); - Task handlerTask = Task.Run(() => AcceptRequestAsync(handler).ContinueWith(t => { t.Dispose(); }, token), token); + Task handlerTask = Task.Run(() => AcceptRequestAsync(handler).ContinueWith(t => + { + t.Dispose(); + }, token), token); + _requestPool.Add(handlerTask); } From 5dcb39da90fd8060107820fccd5bb62450b41f81 Mon Sep 17 00:00:00 2001 From: Kyle | Guitar <26614720+TheGuitarleader@users.noreply.github.com> Date: Fri, 1 Aug 2025 01:41:34 -0500 Subject: [PATCH 15/20] Created service to manage system host log files --- Parallel.Service/Program.cs | 6 ++--- Parallel.Service/Services/LoggingService.cs | 28 +++++++++++++++++++++ 2 files changed, 30 insertions(+), 4 deletions(-) create mode 100644 Parallel.Service/Services/LoggingService.cs diff --git a/Parallel.Service/Program.cs b/Parallel.Service/Program.cs index 003d2c9..8950aa8 100644 --- a/Parallel.Service/Program.cs +++ b/Parallel.Service/Program.cs @@ -15,14 +15,14 @@ namespace Parallel.Service { internal class Program { - private static string _logFile = Path.Combine(PathBuilder.ProgramData, "Logs", "latest.txt"); + internal static readonly string LogFile = Path.Combine(PathBuilder.ProgramData, "Logs", "latest.txt"); static async Task Main(string[] args) { HostApplicationBuilder builder = Host.CreateApplicationBuilder(args); // Logging - Log.Logger = new LoggerConfiguration().MinimumLevel.Debug().WriteTo.Console().WriteTo.File(_logFile).CreateLogger(); + Log.Logger = new LoggerConfiguration().MinimumLevel.Debug().WriteTo.Console().WriteTo.File(LogFile).CreateLogger(); builder.Logging.ClearProviders(); builder.Logging.AddSerilog(); @@ -57,8 +57,6 @@ static async Task Main(string[] args) lifetime.ApplicationStopped.Register(() => { settings.Save(); - Log.CloseAndFlush(); - File.Move(_logFile, Path.Combine(PathBuilder.ProgramData, "Logs", $"{DateTime.Now:MM-dd-yyyy hh-mm-ss}.log")); }); // Starts the application diff --git a/Parallel.Service/Services/LoggingService.cs b/Parallel.Service/Services/LoggingService.cs new file mode 100644 index 0000000..2e1c644 --- /dev/null +++ b/Parallel.Service/Services/LoggingService.cs @@ -0,0 +1,28 @@ +// Copyright 2025 Kyle Ebbinga + +using Microsoft.Extensions.Hosting; +using Parallel.Core.IO; + +namespace Parallel.Service.Services +{ + public class LoggingService : BackgroundService + { + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + await Task.Delay(GetTimeUntilNextDay(), stoppingToken); + await Log.CloseAndFlushAsync(); + + File.Move(Program.LogFile, Path.Combine(PathBuilder.ProgramData, "Logs", $"{DateTime.Now:MM-dd-yyyy hh-mm-ss}.log")); + } + } + + private static TimeSpan GetTimeUntilNextDay() + { + DateTime current = DateTime.Now; + DateTime nextMidnight = current.AddDays(1); + return nextMidnight - current; + } + } +} \ No newline at end of file From 48ddf8f70618dd3dcf3b03f79d9dd41d0cf781fc Mon Sep 17 00:00:00 2001 From: Kyle | Guitar <26614720+TheGuitarleader@users.noreply.github.com> Date: Fri, 1 Aug 2025 04:18:44 -0500 Subject: [PATCH 16/20] Added README --- README.md | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..08546b7 --- /dev/null +++ b/README.md @@ -0,0 +1,55 @@ +# Parallel + +Your files under your control. + +## What is Parallel? + +Parallel is a modular, cross-platform file backup and synchronization tool built for people who want full control of their files. It ditches the cloud-first assumptions and gives you full transparency, and control over the how, when, and where your files move. No contracts, no vendor lock-ins, no silent overwrites. Just clean, dependable syncing on your terms. + +Parallel was originally built to handle **terabytes of data** because Dropbox simply couldn’t. When commercial cloud services hit their limits, Parallel stepped in to offer **unbounded scale**, **local-first logic**, and **configurable workflows** that respect your storage, your bandwidth, and your rules. + +## Why Parallel? + +Parallel is completely free and open source. You provide the storage, and Parallel handles the sync. Whether it’s an external drive, a NAS, a remote SSH server, or an S3-compatible cloud like [Storj](https://www.storj.io/) or [Wasabi](https://wasabi.com/), Parallel adapts to what you own. + +Your computer already gives you enough to fight with — your files don't have to be one of them. Parallel keeps backups simple, transparent, and under your control with no cloud drama. + +| Feature | **Parallel** | **Dropbox** | **OneDrive** | **iCloud** | **File History (Windows)** | +|-------------------------------|--------------|-------------|--------------|------------|-----------------------------| +| **Open Source** | ✅ Yes | ❌ No | ❌ No | ❌ No | ❌ No +| **Local-first** | ✅ Always | ❌ Cloud-first | ❌ Cloud-first | ⚠️ Hybrid (Apple ecosystem) | ✅ Yes +| **Modular storage options** | ✅ Any (NAS, SSH, S3) | ❌ Vendor-locked | ❌ Vendor-locked | ❌ Vendor-locked | ❌ Local only +| **Compression** | ✅ Optional | ❌ No | ❌ No | ❌ No | ❌ No +| **Encryption** | ✅ Optional | ❌ Vendor-controlled | ❌ Vendor-controlled | ❌ Vendor-controlled | ⚠️ Depends on drive encryption +| **Cross-platform** | ✅ Yes | ✅ Yes | ✅ Yes | ⚠️ Apple-centric | ❌ Windows only +| **Free to use** | ✅ Fully | ⚠️ 2GB free | ⚠️ 5GB free | ⚠️ 5GB free | ✅ Yes +| **Max storage** | ✅ Unlimited | ⚠️ 2GB (free), 3TB (personal), 15TB (enterprise) | ⚠️ 5TB (personal), 25TB (enterprise) | ⚠️ 5GB–12TB (paid tiers) | ⚠️ Limited by drive size + + + +## 📦 Installation + +> Coming soon — Parallel is currently in active development. Stay tuned for install instructions, binaries, and package manager support. + +## 🧪 Status + +Parallel is currently in early development. Expect rapid iteration, breaking changes, and lots of modular experimentation. Contributions, feedback, and testing are welcome! + +## 🤝 Contributing + +Want to help shape the future of file storage? Open an issue, submit a pull request, or reach out with ideas. Parallel is built for transparency — and that includes its development. + +## 📄 License + +Parallel is licensed under the **Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0)** license. + +* ✅ You can **use, modify, and share** this code freely. +* ✅ You can **build on it**, fork it, and remix it — as long as you credit the original. +* 🚫 You **cannot sell** this code or use it in commercial products. +* 🚫 You **cannot claim it as your own** or strip attribution. +* 🔁 If you make changes and share them, you must use the same license + +## 💬 Contact + +For questions and ideas, reach out via our [GitHub Issues](https://github.com/TheGuitarleader/Parallel/issues). + From 699f1342a4e3a3065c145d41b639d48724d4a027 Mon Sep 17 00:00:00 2001 From: Kyle | Guitar <26614720+TheGuitarleader@users.noreply.github.com> Date: Fri, 1 Aug 2025 04:19:02 -0500 Subject: [PATCH 17/20] Updated LICENSE --- LICENSE.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/LICENSE.md b/LICENSE.md index cc23bb0..b75b631 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1 +1,2 @@ -Parallel © 2025 by [Entex Interactive, LLC](https://www.entexinteractive.com/) is licensed under [Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International](https://creativecommons.org/licenses/by-nc-sa/4.0/) +Parallel © 2025 by [Kyle Ebbinga](https://github.com/TheGuitarleader/Parallel) is licensed under [Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International](https://creativecommons.org/licenses/by-nc-sa/4.0/) + From 95ead69c174109d3ba2dc644b0e69e826051bd6d Mon Sep 17 00:00:00 2001 From: Kyle Ebbinga <26614720+TheGuitarleader@users.noreply.github.com> Date: Mon, 18 Aug 2025 22:35:39 -0500 Subject: [PATCH 18/20] Create dotnet.yml --- .github/workflows/dotnet.yml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 .github/workflows/dotnet.yml diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml new file mode 100644 index 0000000..0574582 --- /dev/null +++ b/.github/workflows/dotnet.yml @@ -0,0 +1,27 @@ +# This workflow will build a .NET project +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net + +name: .NET + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 9.0.x + - name: Restore dependencies + run: dotnet restore + - name: Build + run: dotnet build --no-restore + - name: Test + run: dotnet test --no-build --verbosity normal From 65bc37bcb08491eb39da615466721fed9218fcd7 Mon Sep 17 00:00:00 2001 From: Kyle | Guitar <26614720+TheGuitarleader@users.noreply.github.com> Date: Mon, 18 Aug 2025 23:00:14 -0500 Subject: [PATCH 19/20] Imported code from original project --- Parallel.Cli/Commands/ConfigCommand.cs | 84 +++++++++++++++++++ Parallel.Cli/Commands/DecryptCommand.cs | 14 ++++ Parallel.Cli/Commands/EncryptCommand.cs | 16 ++++ Parallel.Cli/Program.cs | 3 +- Parallel.Cli/Utils/CommandLine.cs | 26 +++++- Parallel.Cli/Utils/ProgressReport.cs | 2 +- Parallel.Core/Database/SqliteDatabase.cs | 2 +- .../{IProgress.cs => IProgressReporter.cs} | 2 +- Parallel.Core/Diagnostics/ProgressDebug.cs | 10 +-- Parallel.Core/IO/Backup/BaseFileManager.cs | 4 +- Parallel.Core/IO/Backup/DeltaBackupManager.cs | 4 +- Parallel.Core/IO/Backup/FileBackupManager.cs | 10 +-- Parallel.Core/IO/Backup/IBackupManager.cs | 4 +- .../IO/FileSystem/DotNetFileSystem.cs | 4 +- Parallel.Core/IO/FileSystem/IFileSystem.cs | 4 +- Parallel.Core/IO/FileSystem/SftpFileSystem.cs | 4 +- Parallel.Core/IO/PathBuilder.cs | 15 ++-- Parallel.Core/IO/Recovery/RecoveryManager.cs | 2 +- Parallel.Core/IO/Scanning/FileScanner.cs | 10 +-- Parallel.Core/Models/SystemFile.cs | 42 +++++++--- Parallel.Core/Parallel.Core.csproj | 2 +- .../Settings/FileSystemCredentials.cs | 10 +++ Parallel.Core/Utils/Encryption.cs | 49 +++++++++++ Parallel.Core/Utils/HashGenerator.cs | 34 ++++++++ Parallel.Service/Utils/UdpReporting.cs | 24 ++++++ 25 files changed, 327 insertions(+), 54 deletions(-) create mode 100644 Parallel.Cli/Commands/ConfigCommand.cs create mode 100644 Parallel.Cli/Commands/DecryptCommand.cs create mode 100644 Parallel.Cli/Commands/EncryptCommand.cs rename Parallel.Core/Diagnostics/{IProgress.cs => IProgressReporter.cs} (94%) create mode 100644 Parallel.Service/Utils/UdpReporting.cs diff --git a/Parallel.Cli/Commands/ConfigCommand.cs b/Parallel.Cli/Commands/ConfigCommand.cs new file mode 100644 index 0000000..6a42351 --- /dev/null +++ b/Parallel.Cli/Commands/ConfigCommand.cs @@ -0,0 +1,84 @@ +// Copyright 2025 Kyle Ebbinga + +using System.CommandLine; +using Parallel.Cli.Utils; +using Parallel.Core.Database; +using Parallel.Core.IO.FileSystem; +using Parallel.Core.Settings; +using Parallel.Core.Utils; + +namespace Parallel.Cli.Commands +{ + public class ConfigCommand : Command + { + private Command addCmd = new("add", "Adds a new profile configuration."); + private Command editCmd = new("edit", "Edits a profile configuration."); + private Command viewCmd = new("view", "Shows the profile configuration."); + private Command setCmd = new("set", "Sets a new profile configuration."); + private Command delCmd = new("delete", "Deletes a profile configuration."); + + public ConfigCommand() : base("config", "View or edit the profile configurations.") + { + this.SetHandler(() => + { + //ProfileConfig profile = ProfileConfig.Load(); + CommandLine.WriteLine($"Current profile: '{Program.Settings.Profiles.FirstOrDefault()}'"); + }); + + this.AddCommand(addCmd); + addCmd.SetHandler(() => + { + CommandLine.WriteLine("Creating new database credentials...", ConsoleColor.DarkGray); + DatabaseCredentials dbc = new DatabaseCredentials(); + dbc.Provider = Enum.Parse(CommandLine.ReadString($"Provider ({string.Join(", ", Enum.GetNames(typeof(DatabaseProvider)))})"), true); + if (dbc.Provider == DatabaseProvider.Local) + { + dbc = DatabaseCredentials.Local; + } + else + { + dbc.Address = CommandLine.ReadString("Address"); + dbc.Username = CommandLine.ReadString("Username"); + dbc.Password = CommandLine.ReadPassword("Password"); + dbc.Name = CommandLine.ReadString("Name"); + } + + CommandLine.WriteLine("Creating new file system credentials...", ConsoleColor.DarkGray); + FileSystemCredentials fsc = new FileSystemCredentials(); + fsc.Service = Enum.Parse(CommandLine.ReadString($"Service ({string.Join(", ", Enum.GetNames(typeof(FileService)))})"), true); + if (fsc.Service == FileService.Local) + { + fsc.RootDirectory = CommandLine.ReadString("Root"); + } + else if (fsc.Service == FileService.Cloud) + { + fsc.Address = CommandLine.ReadString("Bucket Name"); + fsc.Username = CommandLine.ReadString("Access Key"); + fsc.Password = CommandLine.ReadPassword("Secret Key"); + } + else + { + fsc.RootDirectory = CommandLine.ReadString("Root"); + fsc.Address = CommandLine.ReadString("Address"); + fsc.Username = CommandLine.ReadString("Username"); + fsc.Password = CommandLine.ReadPassword("Password"); + } + + fsc.Encrypt = CommandLine.ReadBool("Encrypt files? (y/n)", false); + fsc.EncryptionKey = HashGenerator.GenerateHash(32, true); + + string profileName = CommandLine.ReadString("Profile Name"); + ProfileConfig profile = new ProfileConfig(profileName, dbc, fsc); + profile.SaveToFile(); + + CommandLine.WriteLine($"Saved new connection profile: '{profile.Name}'"); + }); + + this.AddCommand(setCmd); + setCmd.SetHandler(() => + { + + }); + } + } +} \ No newline at end of file diff --git a/Parallel.Cli/Commands/DecryptCommand.cs b/Parallel.Cli/Commands/DecryptCommand.cs new file mode 100644 index 0000000..e031679 --- /dev/null +++ b/Parallel.Cli/Commands/DecryptCommand.cs @@ -0,0 +1,14 @@ +// Copyright 2025 Kyle Ebbinga + +using System.CommandLine; + +namespace Parallel.Cli.Commands +{ + public class DecryptCommand : Command + { + public DecryptCommand() : base("decrypt", "Decrypts a file or directory.") + { + + } + } +} \ No newline at end of file diff --git a/Parallel.Cli/Commands/EncryptCommand.cs b/Parallel.Cli/Commands/EncryptCommand.cs new file mode 100644 index 0000000..f4787b5 --- /dev/null +++ b/Parallel.Cli/Commands/EncryptCommand.cs @@ -0,0 +1,16 @@ +// Copyright 2025 Kyle Ebbinga + +using System.CommandLine; + +namespace Parallel.Cli.Commands +{ + public class EncryptCommand : Command + { + private readonly Argument sourceArg = new("path", "The source path to encrypt."); + + public EncryptCommand() : base("encrypt", "Encrypts a file or directory.") + { + + } + } +} \ No newline at end of file diff --git a/Parallel.Cli/Program.cs b/Parallel.Cli/Program.cs index c459355..9d020b4 100644 --- a/Parallel.Cli/Program.cs +++ b/Parallel.Cli/Program.cs @@ -21,10 +21,9 @@ public static async Task Main(string[] args) AssemblyName assembly = Assembly.GetExecutingAssembly().GetName(); Log.Information($"{assembly.Name} [Version {assembly.Version}]"); - RootCommand rootCommand = new("Parallel file manager - Easily back up and sync massive amounts of files, and save on drive space in the process."); + RootCommand rootCommand = new("Parallel file manager - Easily back up and synchronize massive amounts of files, and free up drive space."); Type[] types = Assembly.GetExecutingAssembly().GetTypes().Where(t => t.IsSubclassOf(typeof(Command)) && t.IsClass).ToArray(); foreach (Type? type in types) rootCommand.AddCommand((Command)Activator.CreateInstance(type)!); - await rootCommand.InvokeAsync(args); Settings.Save(); } diff --git a/Parallel.Cli/Utils/CommandLine.cs b/Parallel.Cli/Utils/CommandLine.cs index 2cd26ff..ac5bdf6 100644 --- a/Parallel.Cli/Utils/CommandLine.cs +++ b/Parallel.Cli/Utils/CommandLine.cs @@ -7,7 +7,7 @@ namespace Parallel.Cli.Utils { public class CommandLine { - public static string? ReadLine(object value, ConsoleColor color = ConsoleColor.Gray) + public static string? ReadString(object value, ConsoleColor color = ConsoleColor.Gray) { Console.ForegroundColor = color; Console.Write($"> {value}: "); @@ -15,6 +15,30 @@ public class CommandLine return Console.ReadLine(); } + public static bool ReadBool(object value, bool defaultValue, ConsoleColor color = ConsoleColor.Gray) + { + HashSet trueValues = new HashSet(StringComparer.OrdinalIgnoreCase) { "y", "yes", "true", "1" }; + HashSet falseValues = new HashSet(StringComparer.OrdinalIgnoreCase) { "n", "no", "false", "0" }; + + Console.ForegroundColor = color; + Console.Write($"> {value}: "); + + bool result = defaultValue; + string? input = Console.ReadLine()?.Trim(); + if (!string.IsNullOrEmpty(input) && trueValues.Contains(input)) + { + result = true; + } + + if (!string.IsNullOrEmpty(input) && falseValues.Contains(input)) + { + result = false; + } + + Console.ResetColor(); + return result; + } + public static string? ReadPassword(object value, ConsoleColor color = ConsoleColor.Gray) { string password = string.Empty; diff --git a/Parallel.Cli/Utils/ProgressReport.cs b/Parallel.Cli/Utils/ProgressReport.cs index a00cd16..a5e1d49 100644 --- a/Parallel.Cli/Utils/ProgressReport.cs +++ b/Parallel.Cli/Utils/ProgressReport.cs @@ -5,7 +5,7 @@ namespace Parallel.Cli.Utils { - public class ProgressReport : IProgress + public class ProgressReport : IProgressReporter { public void Report(ProgressOperation operation, SystemFile file, int current, int total) { diff --git a/Parallel.Core/Database/SqliteDatabase.cs b/Parallel.Core/Database/SqliteDatabase.cs index a8932b6..15738ec 100644 --- a/Parallel.Core/Database/SqliteDatabase.cs +++ b/Parallel.Core/Database/SqliteDatabase.cs @@ -115,7 +115,7 @@ public long Ping() public bool AddFile(SystemFile file) { return RunQuery( - $"INSERT OR REPLACE INTO files (profile, id, name, localpath, remotepath, lastwrite, lastupdate, LocalSize, RemoteSize, type, hidden, readonly, deleted) VALUES(\"{ProfileId}\", \"{file.Id}\", \"{file.Name}\", \"{file.LocalPath}\", \"{file.RemotePath}\", {file.LastWrite.TotalMilliseconds}, {file.LastUpdate.TotalMilliseconds}, {file.LocalSize}, {file.RemoteSize}, \"{file.Type.ToString()}\", {Converter.ToInt32(file.IsHidden)}, {Converter.ToInt32(file.IsReadOnly)}, {Converter.ToInt32(file.IsDeleted)});") > + $"INSERT OR REPLACE INTO files (profile, id, name, localpath, remotepath, lastwrite, lastupdate, LocalSize, RemoteSize, type, hidden, readonly, deleted) VALUES(\"{ProfileId}\", \"{file.Id}\", \"{file.Name}\", \"{file.LocalPath}\", \"{file.RemotePath}\", {file.LastWrite.TotalMilliseconds}, {file.LastUpdate.TotalMilliseconds}, {file.LocalSize}, {file.RemoteSize}, \"{file.Type.ToString()}\", {Converter.ToInt32(file.Hidden)}, {Converter.ToInt32(file.ReadOnly)}, {Converter.ToInt32(file.Deleted)});") > 0; } diff --git a/Parallel.Core/Diagnostics/IProgress.cs b/Parallel.Core/Diagnostics/IProgressReporter.cs similarity index 94% rename from Parallel.Core/Diagnostics/IProgress.cs rename to Parallel.Core/Diagnostics/IProgressReporter.cs index 1b4ff15..a774bd1 100644 --- a/Parallel.Core/Diagnostics/IProgress.cs +++ b/Parallel.Core/Diagnostics/IProgressReporter.cs @@ -17,7 +17,7 @@ public enum ProgressOperation /// /// Defines a provider for progress updates. /// - public interface IProgress + public interface IProgressReporter { /// /// Reports a progress update. diff --git a/Parallel.Core/Diagnostics/ProgressDebug.cs b/Parallel.Core/Diagnostics/ProgressDebug.cs index d6ceb2e..9ca0b6e 100644 --- a/Parallel.Core/Diagnostics/ProgressDebug.cs +++ b/Parallel.Core/Diagnostics/ProgressDebug.cs @@ -7,7 +7,7 @@ namespace Parallel.Core.Diagnostics /// /// Represents a basic progress report debugger. /// - public class ProgressDebug : IProgress + public class ProgressDebug : IProgressReporter { private ProgressOperation currentOperation; private int progressPercentage; @@ -22,11 +22,9 @@ public void Report(ProgressOperation operation, SystemFile file, int current, in currentOperation = operation; } - if (progressPercentage != num && num % 10 == 0) - { - progressPercentage = num; - Log.Information($"{operation}: {current} out of {total} ({progressPercentage}%)"); - } + if (progressPercentage == num || num % 10 != 0) return; + Log.Information($"{operation}: {current} out of {total} ({progressPercentage}%)"); + progressPercentage = num; } /// diff --git a/Parallel.Core/IO/Backup/BaseFileManager.cs b/Parallel.Core/IO/Backup/BaseFileManager.cs index a3c10af..53f9b7e 100644 --- a/Parallel.Core/IO/Backup/BaseFileManager.cs +++ b/Parallel.Core/IO/Backup/BaseFileManager.cs @@ -58,9 +58,9 @@ public virtual bool Initialize() } /// - public abstract Task BackupFilesAsync(SystemFile[] files, IProgress progress); + public abstract Task BackupFilesAsync(SystemFile[] files, IProgressReporter progress); /// - public abstract Task RestoreFilesAsync(SystemFile[] files, IProgress progress); + public abstract Task RestoreFilesAsync(SystemFile[] files, IProgressReporter progress); } } \ No newline at end of file diff --git a/Parallel.Core/IO/Backup/DeltaBackupManager.cs b/Parallel.Core/IO/Backup/DeltaBackupManager.cs index ccf03bb..20c2084 100644 --- a/Parallel.Core/IO/Backup/DeltaBackupManager.cs +++ b/Parallel.Core/IO/Backup/DeltaBackupManager.cs @@ -18,13 +18,13 @@ public class DeltaBackupManager : BaseFileManager public DeltaBackupManager(ProfileConfig profile) : base(profile) { } /// - public override Task BackupFilesAsync(SystemFile[] files, IProgress progress) + public override Task BackupFilesAsync(SystemFile[] files, IProgressReporter progress) { throw new NotImplementedException(); } /// - public override Task RestoreFilesAsync(SystemFile[] files, IProgress progress) + public override Task RestoreFilesAsync(SystemFile[] files, IProgressReporter progress) { throw new NotImplementedException(); } diff --git a/Parallel.Core/IO/Backup/FileBackupManager.cs b/Parallel.Core/IO/Backup/FileBackupManager.cs index 0bc1c6b..1c30a8b 100644 --- a/Parallel.Core/IO/Backup/FileBackupManager.cs +++ b/Parallel.Core/IO/Backup/FileBackupManager.cs @@ -25,16 +25,16 @@ public class FileBackupManager : BaseFileManager public FileBackupManager(ProfileConfig profile) : base(profile) { } /// - public override async Task BackupFilesAsync(SystemFile[] files, IProgress progress) + public override async Task BackupFilesAsync(SystemFile[] files, IProgressReporter progress) { if (!files.Any()) return; - SystemFile[] backupFiles = files.Where(f => !f.IsDeleted).ToArray(); + SystemFile[] backupFiles = files.Where(f => !f.Deleted).ToArray(); Log.Information($"Backing up {backupFiles.Length} files..."); await FileSystem.UploadFilesAsync(backupFiles, progress); for (int i = 0; i < files.Length; i++) { SystemFile file = files.ElementAt(i); - if (file.IsDeleted) + if (file.Deleted) { progress.Report(ProgressOperation.Archiving, file, i, files.Length); Database.AddHistory(file.LocalPath, HistoryType.Archived); @@ -55,9 +55,9 @@ public override async Task BackupFilesAsync(SystemFile[] files, IProgress progre } /// - public override async Task RestoreFilesAsync(SystemFile[] files, IProgress progress) + public override async Task RestoreFilesAsync(SystemFile[] files, IProgressReporter progress) { - SystemFile[] restoreFiles = files.Where(f => f.IsDeleted).ToArray(); + SystemFile[] restoreFiles = files.Where(f => f.Deleted).ToArray(); if (!restoreFiles.Any()) return; await FileSystem.DownloadFilesAsync(restoreFiles, progress); diff --git a/Parallel.Core/IO/Backup/IBackupManager.cs b/Parallel.Core/IO/Backup/IBackupManager.cs index 7dd71d8..d819857 100644 --- a/Parallel.Core/IO/Backup/IBackupManager.cs +++ b/Parallel.Core/IO/Backup/IBackupManager.cs @@ -51,13 +51,13 @@ public interface IBackupManager /// /// /// - Task BackupFilesAsync(SystemFile[] files, IProgress progress); + Task BackupFilesAsync(SystemFile[] files, IProgressReporter progress); /// /// Restores a path. Can be either a file or directory. /// /// /// - Task RestoreFilesAsync(SystemFile[] files, IProgress progress); + Task RestoreFilesAsync(SystemFile[] files, IProgressReporter progress); } } \ No newline at end of file diff --git a/Parallel.Core/IO/FileSystem/DotNetFileSystem.cs b/Parallel.Core/IO/FileSystem/DotNetFileSystem.cs index 554df93..9268564 100644 --- a/Parallel.Core/IO/FileSystem/DotNetFileSystem.cs +++ b/Parallel.Core/IO/FileSystem/DotNetFileSystem.cs @@ -55,7 +55,7 @@ public Task DeleteFileAsync(string path) } /// - public async Task DownloadFilesAsync(SystemFile[] files, IProgress progress) + public async Task DownloadFilesAsync(SystemFile[] files, IProgressReporter progress) { if (!files.Any()) return; for (int i = 0; i < files.Length; i++) @@ -137,7 +137,7 @@ public Task PingAsync() } /// - public async Task UploadFilesAsync(SystemFile[] files, IProgress progress) + public async Task UploadFilesAsync(SystemFile[] files, IProgressReporter progress) { if (!files.Any()) return; for (int i = 0; i < files.Length; i++) diff --git a/Parallel.Core/IO/FileSystem/IFileSystem.cs b/Parallel.Core/IO/FileSystem/IFileSystem.cs index 360466b..7f043da 100644 --- a/Parallel.Core/IO/FileSystem/IFileSystem.cs +++ b/Parallel.Core/IO/FileSystem/IFileSystem.cs @@ -38,7 +38,7 @@ public interface IFileSystem /// /// /// - Task DownloadFilesAsync(SystemFile[] files, IProgress progress); + Task DownloadFilesAsync(SystemFile[] files, IProgressReporter progress); /// /// Returns the parent directory name. @@ -78,6 +78,6 @@ public interface IFileSystem /// /// /// - Task UploadFilesAsync(SystemFile[] files, IProgress progress); + Task UploadFilesAsync(SystemFile[] files, IProgressReporter progress); } } \ No newline at end of file diff --git a/Parallel.Core/IO/FileSystem/SftpFileSystem.cs b/Parallel.Core/IO/FileSystem/SftpFileSystem.cs index 86eeab2..58ec6cd 100644 --- a/Parallel.Core/IO/FileSystem/SftpFileSystem.cs +++ b/Parallel.Core/IO/FileSystem/SftpFileSystem.cs @@ -82,7 +82,7 @@ public async Task DeleteFileAsync(string path) } } - public Task DownloadFilesAsync(SystemFile[] files, IProgress progress) + public Task DownloadFilesAsync(SystemFile[] files, IProgressReporter progress) { throw new NotImplementedException(); } @@ -163,7 +163,7 @@ public async Task PingAsync() } /// - public async Task UploadFilesAsync(SystemFile[] files, IProgress progress) + public async Task UploadFilesAsync(SystemFile[] files, IProgressReporter progress) { using SftpClient sftp = new SftpClient(_connectionInfo); sftp.Connect(); diff --git a/Parallel.Core/IO/PathBuilder.cs b/Parallel.Core/IO/PathBuilder.cs index 93f07f4..e2b4bf9 100644 --- a/Parallel.Core/IO/PathBuilder.cs +++ b/Parallel.Core/IO/PathBuilder.cs @@ -13,11 +13,14 @@ namespace Parallel.Core.IO /// public class PathBuilder { - public static string TempDirectory() + public static string TempDirectory { - string tempFolder = Path.Combine(Path.GetTempPath(), $"parallel_{UnixTime.Now.TotalMilliseconds}"); - if (!Directory.Exists(tempFolder)) Directory.CreateDirectory(tempFolder); - return tempFolder; + get + { + string tempFolder = Path.Combine(Path.GetTempPath(), $"parallel_{UnixTime.Now.TotalMilliseconds}"); + if (!Directory.Exists(tempFolder)) Directory.CreateDirectory(tempFolder); + return tempFolder; + } } /// @@ -29,7 +32,7 @@ public static string ProgramData { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Entex Interactive", "Parallel"); + return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Parallel"); } if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) @@ -39,7 +42,7 @@ public static string ProgramData if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { - return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support", "Entex Interactive", "Parallel"); + return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support", "Parallel"); } throw new PlatformNotSupportedException("Unsupported OS detected."); diff --git a/Parallel.Core/IO/Recovery/RecoveryManager.cs b/Parallel.Core/IO/Recovery/RecoveryManager.cs index 89777a3..df396f9 100644 --- a/Parallel.Core/IO/Recovery/RecoveryManager.cs +++ b/Parallel.Core/IO/Recovery/RecoveryManager.cs @@ -69,7 +69,7 @@ public RecoveryPoint Save() foreach (DataRow row in dt.Rows) { SystemFile lf = new SystemFile(row); - if (lf.IsDeleted) + if (lf.Deleted) { rp.DeletedFiles.Add(lf); } diff --git a/Parallel.Core/IO/Scanning/FileScanner.cs b/Parallel.Core/IO/Scanning/FileScanner.cs index b6ccdd2..5ab6cb0 100644 --- a/Parallel.Core/IO/Scanning/FileScanner.cs +++ b/Parallel.Core/IO/Scanning/FileScanner.cs @@ -44,7 +44,7 @@ public SystemFile[] GetFileChanges() scannedFiles.AddRange(files); } - Log.Information($"Backing up {scannedFiles.Where(x => x.IsDeleted.Equals(false)).Count()} files..."); + Log.Information($"Backing up {scannedFiles.Where(x => !x.Deleted).Count()} files..."); return scannedFiles.ToArray(); } @@ -89,7 +89,7 @@ public SystemFile[] GetFileChanges(string path, string[] ignoreFolders) { Log.Debug($"Is ignored -> {lfi.LocalPath}"); - lfi.IsDeleted = true; + lfi.Deleted = true; scannedFiles.Add(lfi); } @@ -98,7 +98,7 @@ public SystemFile[] GetFileChanges(string path, string[] ignoreFolders) Log.Debug($"Changed -> {lfi.LocalPath}"); // Changed file - dlf.IsDeleted = false; + dlf.Deleted = false; scannedFiles.Add(lfi); } @@ -109,7 +109,7 @@ public SystemFile[] GetFileChanges(string path, string[] ignoreFolders) // Adds deleted files Log.Debug($"Deleted -> {dlf.LocalPath}"); - dlf.IsDeleted = true; + dlf.Deleted = true; scannedFiles.Add(dlf); } } @@ -118,7 +118,7 @@ public SystemFile[] GetFileChanges(string path, string[] ignoreFolders) // Deletes ignored files Log.Debug($"No contains Ignored -> {dlf.LocalPath}"); - dlf.IsDeleted = true; + dlf.Deleted = true; scannedFiles.Add(dlf); } } diff --git a/Parallel.Core/Models/SystemFile.cs b/Parallel.Core/Models/SystemFile.cs index 7c2c288..02a7ace 100644 --- a/Parallel.Core/Models/SystemFile.cs +++ b/Parallel.Core/Models/SystemFile.cs @@ -59,17 +59,32 @@ public class SystemFile /// /// If the file is currently hidden on the local machine. /// - public bool IsHidden { get; set; } = false; + public bool Hidden { get; set; } = false; /// /// If the file is currently read-only on the local machine. /// - public bool IsReadOnly { get; set; } = false; + public bool ReadOnly { get; set; } = false; /// /// If the file is currently deleted on the local machine. /// - public bool IsDeleted { get; set; } = false; + public bool Deleted { get; set; } = false; + + /// + /// If the file is encrypted in the backup. + /// + public bool Encrypted { get; set; } = false; + + /// + /// The salt used to encrypt the file. + /// + public byte[] Salt { get; set; } = Array.Empty(); + + /// + /// The initialization vector used to encrypt the file. + /// + public byte[] IV { get; set; } = Array.Empty(); /// @@ -94,16 +109,16 @@ public SystemFile(FileInfo fileInfo) Type = FileTypes.GetFileCategory(Path.GetExtension(fileInfo.Name)); LastWrite = new UnixTime(fileInfo.LastWriteTime); LastUpdate = UnixTime.Now; - IsDeleted = !fileInfo.Exists; + Deleted = !fileInfo.Exists; if (fileInfo.Attributes.HasFlag(FileAttributes.Hidden)) { - IsHidden = true; + Hidden = true; } if (fileInfo.Attributes.HasFlag(FileAttributes.ReadOnly)) { - IsReadOnly = true; + ReadOnly = true; } } @@ -122,9 +137,9 @@ public SystemFile(DataRow row) LastWrite = UnixTime.FromMilliseconds(row.Field("lastwrite")); LastUpdate = UnixTime.FromMilliseconds(row.Field("lastupdate")); Type = (FileCategory)Enum.Parse(typeof(FileCategory), row.Field("type")); - IsHidden = Converter.ToBool(Convert.ToInt32(row.Field("hidden"))); - IsReadOnly = Converter.ToBool(Convert.ToInt32(row.Field("readonly"))); - IsDeleted = Converter.ToBool(Convert.ToInt32(row.Field("deleted"))); + Hidden = Converter.ToBool(Convert.ToInt32(row.Field("hidden"))); + ReadOnly = Converter.ToBool(Convert.ToInt32(row.Field("readonly"))); + Deleted = Converter.ToBool(Convert.ToInt32(row.Field("deleted"))); } public bool Equals(SystemFile value) @@ -138,9 +153,12 @@ public bool Equals(SystemFile value) value?.LocalSize != null ? this.LocalSize.Equals(value.LocalSize) : (bool?)null, value?.RemoteSize != null ? this.RemoteSize.Equals(value.RemoteSize) : (bool?)null, value?.Type != null ? this.Type.Equals(value.Type) : (bool?)null, - value?.IsHidden != null ? this.IsHidden.Equals(value.IsHidden) : (bool?)null, - value?.IsReadOnly != null ? this.IsReadOnly.Equals(value.IsReadOnly) : (bool?)null, - value?.IsDeleted != null ? this.IsDeleted.Equals(value.IsDeleted) : (bool?)null + value?.Hidden != null ? this.Hidden.Equals(value.Hidden) : (bool?)null, + value?.ReadOnly != null ? this.ReadOnly.Equals(value.ReadOnly) : (bool?)null, + value?.Deleted != null ? this.Deleted.Equals(value.Deleted) : (bool?)null, + value?.Encrypted != null ? this.Encrypted.Equals(value.Encrypted) : (bool?)null, + this?.Salt != null && value?.Salt != null ? this.Salt.SequenceEqual(value.Salt) : (bool?)null, + this?.IV != null && value?.IV != null ? this.IV.SequenceEqual(value.IV) : (bool?)null, ]; return results.All(b => b != null && (bool)b); diff --git a/Parallel.Core/Parallel.Core.csproj b/Parallel.Core/Parallel.Core.csproj index 7d6a8eb..2093d47 100644 --- a/Parallel.Core/Parallel.Core.csproj +++ b/Parallel.Core/Parallel.Core.csproj @@ -1,7 +1,7 @@  - net8.0 + net9.0 enable disable 1.0.1.0 diff --git a/Parallel.Core/Settings/FileSystemCredentials.cs b/Parallel.Core/Settings/FileSystemCredentials.cs index 042bb1a..db02bf1 100644 --- a/Parallel.Core/Settings/FileSystemCredentials.cs +++ b/Parallel.Core/Settings/FileSystemCredentials.cs @@ -17,6 +17,16 @@ public class FileSystemCredentials public string Username { get; set; } = string.Empty; public string Password { get; set; } = string.Empty; + /// + /// If the file system is encrypting files. + /// + public bool Encrypt { get; set; } = false; + + /// + /// The master key used for encryption. + /// + public string? EncryptionKey { get; set; } = null; + public FileSystemCredentials() { } public FileSystemCredentials(string root) diff --git a/Parallel.Core/Utils/Encryption.cs b/Parallel.Core/Utils/Encryption.cs index 1571534..97a5db0 100644 --- a/Parallel.Core/Utils/Encryption.cs +++ b/Parallel.Core/Utils/Encryption.cs @@ -1,5 +1,6 @@ // Copyright 2025 Kyle Ebbinga +using System.Security.Cryptography; using System.Text; namespace Parallel.Core.Utils @@ -35,5 +36,53 @@ public static string Decode(string value) string decoded = Encoding.UTF8.GetString(data); return decoded; } + + /// + /// Encrypts a . + /// + /// The input stream. + /// The output stream. + /// + /// + public static void EncryptStream(Stream input, Stream output, string masterKey, UnixTime timestamp) + { + byte[] salt = HashGenerator.RandomBytes(16); + byte[] iv = HashGenerator.RandomBytes(16); + byte[] derivedKey = HashGenerator.HKDF(masterKey, salt, timestamp.ToISOString(), 32); // 256-bit key + using (Aes aes = Aes.Create()) + { + aes.Key = derivedKey; + aes.IV = iv; + aes.Mode = CipherMode.CBC; + using (CryptoStream cryptoStream = new CryptoStream(output, aes.CreateEncryptor(), CryptoStreamMode.Write)) + { + input.CopyTo(cryptoStream); + } + } + } + + /// + /// Decrypts a . + /// + /// The input stream. + /// The output stream. + /// + /// + /// + /// + public static void DecryptStream(Stream input, Stream output, string masterKey, UnixTime timestamp, byte[] salt, byte[] iv) + { + byte[] derivedKey = HashGenerator.HKDF(masterKey, salt, timestamp.ToISOString(), 32); // 256-bit key + using (Aes aes = Aes.Create()) + { + aes.Key = derivedKey; + aes.IV = iv; + aes.Mode = CipherMode.CBC; + using (CryptoStream cryptoStream = new CryptoStream(input, aes.CreateDecryptor(), CryptoStreamMode.Read)) + { + cryptoStream.CopyTo(output); + } + } + } } } \ No newline at end of file diff --git a/Parallel.Core/Utils/HashGenerator.cs b/Parallel.Core/Utils/HashGenerator.cs index 3466323..43614f4 100644 --- a/Parallel.Core/Utils/HashGenerator.cs +++ b/Parallel.Core/Utils/HashGenerator.cs @@ -10,6 +10,40 @@ namespace Parallel.Core.Utils /// public static class HashGenerator { + public static byte[] RandomBytes(int length) + { + byte[] bytes = new byte[length]; + using RandomNumberGenerator rng = RandomNumberGenerator.Create(); + rng.GetBytes(bytes); + return bytes; + } + + public static byte[] HKDF(string masterKey, byte[] salt, string info, int length) + { + using (HMACSHA256 hmac = new HMACSHA256(Encoding.UTF8.GetBytes(masterKey))) + { + byte[] prk = hmac.ComputeHash(salt); + byte[] infoBytes = Encoding.UTF8.GetBytes(info); + byte[] output = new byte[length]; + byte[] previous = new byte[0]; + int iterations = (int)Math.Ceiling((double)length / hmac.HashSize * 8); + + for (int i = 0; i < iterations; i++) + { + byte[] input = new byte[previous.Length + infoBytes.Length + 1]; + Buffer.BlockCopy(previous, 0, input, 0, previous.Length); + Buffer.BlockCopy(infoBytes, 0, input, previous.Length, infoBytes.Length); + input[input.Length - 1] = (byte)(i + 1); + + previous = hmac.ComputeHash(input); + Buffer.BlockCopy(previous, 0, output, i * previous.Length, previous.Length); + } + + Array.Resize(ref output, length); + return output; + } + } + /// /// Generates a random hash. /// diff --git a/Parallel.Service/Utils/UdpReporting.cs b/Parallel.Service/Utils/UdpReporting.cs new file mode 100644 index 0000000..3d10d00 --- /dev/null +++ b/Parallel.Service/Utils/UdpReporting.cs @@ -0,0 +1,24 @@ +// Copyright 2025 Kyle Ebbinga + +using Parallel.Core.Diagnostics; +using Parallel.Core.Models; +using Parallel.Core.Net; + +namespace Parallel.Service.Utils +{ + public class UdpReporting : IProgressReporter + { + private readonly Communication _comms = new Communication(); + + public void Report(ProgressOperation operation, SystemFile file, int current, int total) + { + int percent = current * 100 / total; + //_comms.Send($"[{percent}%] {operation}: {file.LocalPath}"); + } + + public void Failed(Exception exception, SystemFile file) + { + //_comms.Send($"Failed to upload file: '{file.LocalPath}'"); + } + } +} \ No newline at end of file From 97ba8227b23417e6fe69eff07060d183a511e09f Mon Sep 17 00:00:00 2001 From: Kyle Ebbinga <26614720+TheGuitarleader@users.noreply.github.com> Date: Mon, 18 Aug 2025 23:35:53 -0500 Subject: [PATCH 20/20] Update README.md --- README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 08546b7..c398c58 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # Parallel +[![.NET](https://img.shields.io/github/actions/workflow/status/TheGuitarleader/Parallel/dotnet.yml?&style=for-the-badge)](https://github.com/TheGuitarleader/Parallel/actions/workflows/dotnet.yml) Your files under your control. @@ -19,10 +20,10 @@ Your computer already gives you enough to fight with — your files don't have t | **Open Source** | ✅ Yes | ❌ No | ❌ No | ❌ No | ❌ No | **Local-first** | ✅ Always | ❌ Cloud-first | ❌ Cloud-first | ⚠️ Hybrid (Apple ecosystem) | ✅ Yes | **Modular storage options** | ✅ Any (NAS, SSH, S3) | ❌ Vendor-locked | ❌ Vendor-locked | ❌ Vendor-locked | ❌ Local only -| **Compression** | ✅ Optional | ❌ No | ❌ No | ❌ No | ❌ No +| **Compression** | ✅ Always | ❌ No | ❌ No | ❌ No | ❌ No | **Encryption** | ✅ Optional | ❌ Vendor-controlled | ❌ Vendor-controlled | ❌ Vendor-controlled | ⚠️ Depends on drive encryption | **Cross-platform** | ✅ Yes | ✅ Yes | ✅ Yes | ⚠️ Apple-centric | ❌ Windows only -| **Free to use** | ✅ Fully | ⚠️ 2GB free | ⚠️ 5GB free | ⚠️ 5GB free | ✅ Yes +| **Free to use** | ✅ Always | ⚠️ 2GB free | ⚠️ 5GB free | ⚠️ 5GB free | ✅ Yes | **Max storage** | ✅ Unlimited | ⚠️ 2GB (free), 3TB (personal), 15TB (enterprise) | ⚠️ 5TB (personal), 25TB (enterprise) | ⚠️ 5GB–12TB (paid tiers) | ⚠️ Limited by drive size @@ -45,8 +46,8 @@ Parallel is licensed under the **Creative Commons Attribution-NonCommercial-Shar * ✅ You can **use, modify, and share** this code freely. * ✅ You can **build on it**, fork it, and remix it — as long as you credit the original. -* 🚫 You **cannot sell** this code or use it in commercial products. -* 🚫 You **cannot claim it as your own** or strip attribution. +* ❌ You **cannot sell** this code or use it in commercial products. +* ❌ You **cannot claim it as your own** or strip attribution. * 🔁 If you make changes and share them, you must use the same license ## 💬 Contact