From ac809c08df45b0b97b3434bfb0b660c04d9003bc Mon Sep 17 00:00:00 2001 From: zj <1052308357@qq.comm> Date: Mon, 2 Feb 2026 19:48:21 +0800 Subject: [PATCH] fix33 --- __pycache__/dialogs.cpython-314.pyc | Bin 27594 -> 27948 bytes __pycache__/disk_operations.cpython-314.pyc | Bin 29096 -> 25274 bytes __pycache__/lvm_operations.cpython-314.pyc | Bin 18245 -> 18256 bytes dialogs.py | 25 +- disk_operations.py | 717 +++++++++----------- lvm_operations.py | 7 +- mainwindow.py | 239 +++---- 7 files changed, 448 insertions(+), 540 deletions(-) diff --git a/__pycache__/dialogs.cpython-314.pyc b/__pycache__/dialogs.cpython-314.pyc index 4e62dcde450e53b1bf63847ba6b890e1c664b756..5f4a612b225ebdc425b1d04ce5c05ca41bc4af18 100644 GIT binary patch delta 1867 zcma)6du&rx7{BMXz3tl4uCI07y0xXBo6g5V=E zl!TC|z_W)fxaVq8-Q6wZ9iHiiGY*3t0N(dq9AB=$l2@=ogz$o~KC%NBwe&6r> zzH`6dcM&u(gA@+M0x5-?ab(B9?y64|HMCTWrvi$S+??cl=x!u$Pay|LV}&r5oB%%w zl!yjZn+9CM3@{0;up~tTv(0i_yLJ8`J}S;d^J70Xq<*;Sry6W9V$z@tI4HEhjBtez zFJ-~y_$1JY9C6QRKdQL=^o!m;s1ju%Jv=2kDimxCZi6glr_6+-aAsmGaEvKIM);G- zq*Vy^vdIui(t=-HlpmGIBN9VcVu(m=VTo<5XEZADWX$M3v~aRxOgwQEG_lm!b~XVJqMp6MIm6G6`WMhWt`oDx5S7o5_4eTf1q? zP^feuxM5SDD-akM8VCgfop@)s3tnN3d?@LhfEgt7yRo;}kgB7Z=kfez5!5P*_>*Rq zMlV33^(?;JFV;egnr>@F!0)hC2_MJ(A+s1+A<3S?q~RD`I;^mNGk+1oDy}ye>Scoi zJ&)XGl(#IB3egeX>AH&OuP8X|HSo^t1_qtvpUV@|7Gk3`R zlTS`uh71}{tF*kS;ULl{;fvW-++&Y>rtwYew~;lX zdVUW(DUe-z?I3AicsT>K`YUCz+-H&pu)~oRRygBVi@ON00zqwRD$lgo1!yau{X`3r z5nSzfI(J^o?yb_or49|LUM2%0Q4~ZwalLhFAQP+J^>x}< zVHdBJ(OB{tI}+jNoMf2j)WhsL;uVKR|DVI^zhh#2!13aQSP1ZE7r~l9nS{H9vFq## z7z>zC8hjZ@K}|3nSca~EyepF#At~Y@zpFeYDvyuI&0)DYB6o!4j?W7wRmZ*WSD$TY zIxlaA(JpK9gkZch;w%n3izCj;u(NW?>6@&c%3L!oX@%om8L**8lR3*!@!E*e8dh2( zN_$vopHe!;dm^rqu&X5E@`YW#DOb(3vIg>lR$d!S5un#$anC8#4r9J%SZ-O$@9wQZ z57aIj8mo1fU@|EC+-ahJs2z<-GFaQhewvl|1z6mKZbDiL-F<^_uCEcv!Pf6tKm^P# z;@r67uES8jk9m(UzKj3Hj~@z_H|+3692H?l#gwCJ+ENAg^Ag~Vf%T@Cx;Tkz7BMbj zt`OrRW){r#Ht7X2B&H6U25oA+s1D8L#$kIq(Li*|0DA^ML%+ky^-B~iLec{jwW7`{m#H{B+}rX8MS3n5hxrQ>6%z#j delta 1752 zcmah}eQZ-z6z_en@4bG1te<1;)~@ZwTiFNO7-iijZfwpmpe$WrV_jK0_JN~&8wM0X z)TmJq#=(SPPzV^Of}qa;ek9Dyza$tcV;$23BpSdNAcJ5;qu$$TLcqlP$9d=8cYf!0 z&pEljqtoc?1k!jlYJ~)^ZCjt~IaT+Orjdfry>jXk3iO_CI7i)wezh5H&|270ssfeN z23hLPHN(as)keos$TwoWocI-~4L$$`D4)yf+4244E z!h;H^WzcZ(39a!p}|lXl!flI%)ia72i5}F}1*jE{x&7i+YXs?R z1VBb5q!W_?GpU1V$1bJEcsu_;F( zWj`bopJ57zxHWGKp)+D-K`)EO#rI1%3Wt8|D?`I@%eM+$Pge8dqRszNjoLpS^Hll^ z(~~t>9uuPBq3H6b9xCgGxXb?n#oi;FuVYS+SYELcQ4E5rf>iOR%1nghAgkVtd7rN? z!;ELD-$6Ptx;R2TTHB?X58My31=l^qrbn3OoCK=4G;zGvOe42AUH=WD@GB&A;|{ZpG)>UIP+{UR?qEeN#WWFMuXr=eY&`?g{RSw7+&3iOl}f6 zMa)%Va)|N5aHSD`Xw5=TgRCtGS2Ubdb;m}-KvxNVX#@Bg`ZWdcX0w7?ng%OEmOXj{ z{M@O91NlZcy2bz_&8mMNl_DEwBrB4RL1~cHp8QSY7YGBbR;UOWz!8yilyC3<#3dN=GXE*-?b9LSc9=yNy(x1s&lf#wnF9 zp>o7kj)ck^S9wRDxTx~Mc-X~^Dko&cfYxTu_fD!xZnF}dF`;$EwXTGgk8Al0TJLDz zgm%#sKGwP8I(I^s8`tHI_D$+a#h2FFX|xZHM9-oQG1fJSIDb-n3ZPnQg5B)~O^^yO z#nfyIe0$-qb-5n)0Ue^S4slp@WH?S9q_ONs^QB4IiFfxhLIeZB-?IX#VP}soX?Z_;LM0eGI$1Tkc87S186A{|1Pa?8g8A diff --git a/__pycache__/disk_operations.cpython-314.pyc b/__pycache__/disk_operations.cpython-314.pyc index b11effdd2847494b395a566802b7504e1cb38c00..5c5d56a6639c73060d5f3dd0a56eb1a1d13e45c1 100644 GIT binary patch literal 25274 zcmb_^c~o21)$i2=ngD^sAjS*W28@j-Y-2pd6P|z&8Ow1zs2B;14M@3?JY^_Jngpjw zjng#78EW}uu=4dKR+@YoA-nuV&>qCIo`=f5{tmI4ITJPQY;?WSysjg*i7 zRrnO-TIo}gUzJYpTb4~dpe&ghjIh+xs*?BOT+5A6+Vqk4c93)4Wy~IbXy9!tF>j=QpuUlriCYZ zn+{TgO%JKjX0jQ`voxF0mu{zg88#)bn#rZF?egxw*W+T^d|mEdZ@bVu!Wf>bC`x-h z1y1@X8&yudrD%SOs#0)judBPmiy3BcHFNg5dt7t&b@g`6dC=$CH^&Jid)j)T-3&Q* zxFz~Ep2M8h;ppn^@;Mw_ZhYl6;sYF4uZC3m7p?I+oKVf}$b@+4-~>1`UhjvyZMr^c zr}%_gs<=1fUizPc3Vac81xKS<0Yrqz_Y}|t`O_@q{#S9OUy@6HiJDSp5Fp$r}PrE zm~Jjn3Vmc#k$*A z`g%_r)7C?`yL&)*Ivw3zUSGXXAbRYX=#w8rk3Bx|v(V&AKZ^FBm^vMlm2okQn{o8; zU_&bO`nyx_zcF#*gNeaUCSU((^886)Yoh-g5a27F_@IB{F?rRD+wF6Bn63j|-L6g- z(0TLmiJ!kRKJ*dY%J^N#b^ponk3R)6wAb%+(-TL}Km++U$++?QJst*lakMjCzOMGR zZikbA9*4Wv;q^IzSw~NsuYIpr0nOi}qt8AW9r(q>+pkC8J~8><-y9$MrBLP2b9@t` zPri5cg?D)7qh~*yI{WIx>8E5BcOk%XINN+}_4N3Kz}3Fzq0u~hP(@cSv>ZBc;<MQXQW3IH_fyS!b!USC^pyNgp{ ztx8T=GnZ4fF`Zsqk7m#e3VKertkc!&I^^K%_Xh<`S9_RWbWo~)-i3a}^4Jx)RU5uM1VU_E_AFsSE z7+9AB#t|k3r}nuH`8ZAcURV2mPV4Gzcf)+>!!)UAndJDF(`{Tg_lbcUM9YYUL-z)?jy5M$rpG@$4eJ73;m5KS@LGO&^~vWZo;e=v|6tJzX0+r-gNqWvKl>#4KnCQrXJ{^_gHbH9wf zb9CypCnsMXf|ove?v3b^=O6=NvVxYE@s|S``7D41FM90wpgeDvo(#@=}QHQ*h1tjdOWwY!^30W}XaAWlOT zX9h=y(~2u1r{3r8>g6;}*B*Z-r}z5zco=s(teKpO@%M7-uHFtem%=YYoCa2bE)Tf` zT3pr|An$Uv@CDZ4($DGnWejGui^;^AG(Nwl8y1kZ-ospG1FYxGZeN4j-|OV_>((7= zcX?3dW3nK7m7YPJi!;iF50^$b7sU#f9xooa%DY|XVM&t~X3p5*@9uWA_c(DyUdo-_ zZ@i*SKW02?9C$R)9oWrg*M_wVuc=FQ)30QgoY?*R?ue-{Qc@ntwnlRDBUU<+L*GbC z%}MWXxM8Bw%*Qq#-8@(mEDWZzd5gowx`;VsZFD(TrJNQ|Y?;NMX^b4JS98YChR~sc=?*lhEeD6~S3S zXmVZHxJ10QH1Ob=hfhBo=w+=7!^TD8?G?e&vz2dEo~?PS=F-f1wrVMBT^2Sjk3F6p zEMcun!p8c^jKvXC-B455wDC%20b98CbJtjAS>SYUkmree0Q^A;N+C|f}Kx?pWC@u6uw!zMBbFy|9 zyRai{>WrB421`#>o~%4obF$`A{>)$#YhH5Mxa40~GUywWO1DuFH=d!op~jKSkAZ$8 z+xQSW@8PiV5wRV0fyPkg8~3v1wV`I#x+ZK~i^R(U+F)U*?d=k_a>-CJTfBiaZ@6gO za3v!T$xn}?&MpAn?q}`W*?A9yjXOlb%gGQfY94uTq@A_e!p1Fx@GI?s(w93}^R&yx zY5%&G3dFwiZio5%+3Z!7Yjdd2b8?y#s?TSwF>Fdv{kF~m=}1Nqrn6U>;pg|10-paq z#fYhSl?8tOP^HCmK0f%vf>o-Ek zQg)@J9Dy8yYso0l3339IH7L#>Ex6Vt95ygm0Mu5>Re_xdEy&R#m=45i{6*`UO)V=Y zPz95S1{9qXyBg@F+EJBCwW}c2&QJAdY&uU07%os!lEa>2(~G!ksluV~EBz{mWDfGA zf*B!+*+eulc(lBMBc}e`uQgE(CjjH=cn;<(?L@hCu1x5IRFQ+&FPK=c4~&HKwE3Et z!XfJkFi<5Pq$kqjN@qZ6sXw4cOy$(JsumQv35VDcqJTpQFnG05^07+BXC~>@tXquL ztvTe&ndRyt&l^ISxG}NZVN6hlB^+R4%9fkFV!f9*MzWchOl{AxYhV;|(Fg^q?e`l6 zt1t@r@-|}c(^R!&SnLF8Q%H3wOQ<5y3h>`LMPm`KB;Gz><8Cq6PRV2g3j8{x=h4gB zmFx`@m03Ngz%pf4Hn$K4CLCmL71`p;N;3-#3o4I(rJlTT(P}3Z3PN&1?c93FRVInYyy-BvEqFSQ1bN_OkO=4=){_4QR7P(Tt#T-n|5L zwkZE%&ig8_XwW%m56lbf2sDK{d6s%oDae2n7CN2F`Be zQCT^0{F^dQq#jQlGz6TN^Ja!k)ua{U0N)WKR|Kj9MZx_;GlvR?x3GB|!^TadH_I;? z%L8kJm7&g|{X@IhP5ar}?y#}vFXr5DDxedfbJ)xo-+fcepa1IyjF1&komE6`iv#t6 zS;4L$MaauqmxhhY;^1Cr@lgHHtl=)U<-QRwJ8w(a*!nkf9)WV!@4UEv|Ek2&u#)=K zg3N|I&1dRWX1MsZH51c0iy-~HA_vpu)`nS{-{h>K;NpusLqnDBi*g;F&oVSD(|xg6 zi{~p18`4$3)i2qgQ;p~}aNdsQA@RVs4qh$ebqk;@Kyxr{#7D7Tn*vOoD!Zaa&AY2? zn)xblLKWIl7egsL4gw9P+@p@6zs_QUbfmhHI={38>F5yY$Yqf&1)W(fNic_qQ#=~G zrh>1U>MXpAn)IY5gB+`1EjHs#I56!FnB(?1FdZDN=`qYzLlB@xPw?Poz=H-`2I309 zg9gM}&YA??leyeX1cPMS4Y%u&gdWWD=r`9>lwFq$?b!`6RJ6H?HzC>eVt+ar!jl{u zo8^egb~{vNv>RKJp)y^pM(iPZAMHfb0y&a7pul~YVvDQao+6GAI4ffFD_PHFlb#ch zoE*d1vcP?C`?7gF{!5%OcR*(UDI&Am;k?EpVy>MM){^jBnv5^2*fX(To0T{t@>|p5 zM|>wradISc1pB*PTzNH5P{ltqO0wBBkO5fC*kI`EWdws5+jUc9*c&9%-@#-5> zKRFjY_M%jpU@|PsX}Z08y7zyln%(Q>)Cixx#_^SBrwQhx0Zx)q0(5A2OUW#ToD3Qz zzeZfoqr&)-bK$;J63mo*9&92J&9T-2FQR3TSqp{y2qx%>U!ID3eO9I-aL z!Hv_t7w|3uCfd!=094R%Qr+OPgH!z|<; z+=f_=owt)M*cCSJ#%u)2Df0o0BtW@5>nX!Y!^>#{8Uli@0hBA~-x$d$=wJWWyrN{F zZu9ZYfoXvwfo^vG`VsY?^uN>p!8rUVOWVViZIR-#=huH_DH^j(8?}Iy?a!8(|1eP2 z;&0NSE#ukN!I^;rm-A-_y<^!6!r2Q#{)ojY&%mEg1YR64kCv?y2#H>fcrh3tu z*BRIs*vZaYH9T!NXZR7eaBJ9P|4VMcIB)D@ix-7_p^l*@ws2F}wD~W&h2K>3VC=hZ z3V|@x`7bbq{(nSeIMaT$%+gRteP+sR$kKdPx@sC+e4bK(X=zr&bj{}rRw?1)H;b*1 zevxHpDARpWq{Z{;hK7Z@FXoc-I>Uxk)o)cxHYimiN)4R13m6J2Kv68vqCD?})vk~NYnN0oxg9N566C5BdX&uW1=cpOr^N*=-9A4FSF&k| zy+|jQ!+feUQeaTxfhM{n1PJ?~sd=;>OL$Z-+L zwxDjGaEQ+iLc5s@z)w*^2Dsdowe3Q!Fmu3qmJfI)HU`P!-AGxuO_#4Tv@;(H4n)gM0<2NZ8h z1-iVIX$KNjCZ+>^I5n{drMM13Fpihg_O$JHIlCAykBkTkO1*araj6nJ76CcDSqm9q zKm`-9Q!}1z#{}_IRay)OG55g@&M2Qk1ko^i@xH=AkP(+5fF+01=1a6(@kAgy*M_)&&BHPylBgJ9zZqu}68+?i-#r3~mmaDg}Yh7B39#5A9+L*Mv=LZx|_E#xcWD1DjJ7 ztPU21Ttl0OmXD}dOJi8u6e%hlE22C2!0!W7IMu@X&Bh*wXwDEoI5Op`o%1PBb2GWXl(Y9t!oaYdYDz z2iVSoZ0Vt}Rv@7+Vqw*p-nHl8EksF|I3{B?Tsar zr2y0;YE*oB>8(_ZXv<8dQfLi&5SYGl>O0=DShu&L#5 zxrJzhN-O%#i|fgmnsr&!Zve)Ws(w?nq@hstMWF`H+l7T1a>(FHn2fj#V#srmtPne# z#JN`h!BELy3^6w;W9BBAVF@ThY;#JJ!j@{&E*9z`(4ZE~LXx|Yj{?18JYcF#XNwt! z1Zp5qjbMU`xiU)#%s`7gIa<)(o^XJuOa+)h;<-U!qLD`}6PFn`iAs3u!CjLg>$4<| zIS`U4woN1=Smv&2{V(wrM@woesQ;;+l$9wq;E_L*iOx*_1!$-~`rHt>Y~-R-CioHs zC7&4=W8(3-Nl<$Cx(~{N{YdufqdUfIL0I!&zKniUU)n@G*nMlX=7r8*ROG)co~5XjF`e)l^PQdoF!SO65+rc#8NewEQ7?0Rsdn0 z=XKvqe{aGf&d~{r{7pH2hm@{E(nsA$PgtzmSxbFbyY!keQ)i8oP9G~>Hd?wYl4%`W z%9<)7StS9(;3L8Htf}_P<~<)4v3t9(rBY?fuT$!>^igAR|N6lcba~{GwXEWzNwx&C z&24Pmp0KGMG{H%81z993FIp;tmf#_f4}wR)bAWXpWS8JF3W^~_wLrv$I%b<+9xMo^ zu?yFaD2Bl}zd3Ad{4%3FZs80)GQ4BBiM=mw-6R%z9nksCTLtZYIe)c@`d}_3zp6~d zbZ#-EpP3A6Dpa2ptWvBgQ~kP30~g8H%T^fFIQ33=8-cQPBwH^*89+dma3Gk5T^3|# zh)r7qYZ_5e^q`^`Y{px!k%Ez4qL$3SEYM05Rx}B4+loQuh?Pvne+Dt^OA2WT1?F%F z$Y?<&H{lRf9@rHkt*tGRViC&>tE@>hC402?R0y*G0OZlxwMh-#lGz#5GReR#ME!%h z14^-EthVc0@NG^wc1t^Bq*@JkEF(qwOJ$7ql%$F-81+G^G|rLr(dK=EK|V!PBLxSC z#917hgiv?xclWtG9Wy)jtJqEaw6?eyYhY_}tTJxb5 z1!)i=Z#R_TI_Y9-Dl5aD3>QiC6lfub%;lH2Ky?S5N$sr0+*xJ%KXn-_J$Q z{CNE1=O#affbXWR)!?Fk{#nQ!4ISgJ<&qBetPz)SJ!ycpuC5s`U%@|_c;N>Ru`6gr@9BnM&UT}NDa224T>^?6i3 z`S5$50P2)Hy_bFjHB5c_LiFXI)0J~;=FXzyIU`~164sBS@ygL?|5MRtKZ4Bzs1^}p zx4hqEgqz*H5C@Mbp(j%!-`QOxO=B}X5~(2_0F1=JDoGDfWw{M>btnHvc3W7Zt=9WyB?PF^KkUo zS(pjr*85eQP8w`ZD-O0FEi+SRUYq#&04=aenDins8unH|m^7}M5QHrTq2nb$DSlSa z{zA9}?*Y6ALKwB21L*vAVOfW46om@}cI$Oi-q9y)i`(i08$uP#Gw_HHG*@tnL;My` z9!+SMb+`5Gakj1CmM4ISP8aODz!wOacM#@bi)V)$=d*0rflxdXSn{GttB)F5HMH-O zCI&54u&GqF-*uQ+8^p$y!+l~d;9v6iH(YqVA=c)Is%CzNAJ=;tMhzx0|2pEn72;{Z zyU-O?%I4;(bRL6MLS`O=p-BP)uraPgaA7bmE*0XSJrFbvMiJbo!f86X-E9ze(#g2} z9>z(E*xS1OE?)KDgf~<@ZHK^dg5$&KNN*6^L3;e#$pbgZ;{j19QkQv|E-afHyF;8F zxGF)gOD}PGFfTwB<|rmOP@M9Bi_7YUtsyS2PuBZhR5;`+KdcA*AQs+kthnm$F|gxM zh4)V|mH-q@2oxczna!OUToSBi7q+t5TfyngfyXp5JX~JkE;Df z0FE=3u*UjGK?y)bbe*VgcAY>t_TciMHE3WLG>;q@*~?nDg^l+|%tfrRXq=CxW^-2z z9T?inX0H!xH++?o2XQZ^oH0}BsHyalsk~nUaUA8-&&>R9GeO6$X;A{aEx%96YD@mZ zV(niWu~d&)=8anBg{EDyEbd?TWrpRbAFLUfMOWzQ0oR%Rr}u|SU+cX@FC5r-rDR%Q z>zN&=cZ4!u+kL5|c3?vU!(h+`5Hy6g59>!ZuoYYXY_b0%jmpl)c=H0$pYuRa7kG%B z-^>==8#cD^?B7VCic13pqw_b0i#D;^{0N#7E?XA#>mugqfqj?F^ZK-;Bl&Ero!zpHEx$i(+5R^Rm|ZNa>DDb${$`P(p+fV8 zqM)Hn^F^5k&f~UW<6H-LXuSS=fUW>~rL`v6t|t-nI6;GgXyI61Wy~)w`UAwg!bWQ~ z)I*M>Fjg==5YL57k(ODTkTVDDh>f;O%?ff!ix?jjdZ(q3A9((yqzIjK`D3!aVkv+ktes$SqIKwe#8GA`a}S=@_xiozeA}4()vQu`l19M*RAJC zv0YiCv6YZg7@Z{dbp20HhCNN3BfQc=W*_Y_G_8i&mjp6zlVWqwcjPev$TxNH#;e!} z-g=&wk~Wk9AA&juhA?Qk-DIQdVBDSjT*Ug7ttOZyF+Uub#$c9~dopd)JXuYvw!LSY zK1-&kq9hX!kJ)avtLs2SIm;5}mK4Jx<`w4F3|j?>-LH(}Osq|BH-nl`HDBq;B)Mk7 zS(fk~R@-LT4g6DkW=*Qca+jl+1!ZR2YLa>0>=seE0TW+}cwTag^}uei%^|%y_wGhl zHiwh@<5aeJ0>$}u5;TlQ{=`D}V(8Z@^icn8JE?sdvW zBOl`}GYu~EZ-wPb4=|Ir3-WjjP$utZ;*|(MRFXd}x$;C;j(_rFP(bK8U`L$eb?)JJ z-16I;5$+Pdg9GBiyIl^9%!LiXqJF{$YSWb!ogN=-`0Ia>uITYP-QEg04FaP(K^NnT ziCWnD)$&a7_)FB=5|lPsWBgvw!`cq_x!!qH*x9>5r5-si`6FTUrOFqib~j#ycTg7=~f&@;IVgqulryxuP}0e1%ZB0@(JWyDW}?e3veOf6#1Ugr=VOm~2r! z8+|zdBzRp{A`01fn=P+d0@r9wi8U35gSS1sgr5-g#XEaukM9_G z_t4UCv=745_$}<%6*wLEJ^c`C2fO(p*bLrTK`BpoU4cWSK!{^mloi4}#3BZ`L_Jbu z4h37RKcE1Y+OxmAZI7#)4_+fKX5(x<>IErtI{6;vt74CQ-x>XRUBoB{h~7?-E}aDtDysP3Vcuj_APrct_gH&I3z0@f5L0n zCLycTIIbi?t4_?22g26t22l^1w1qO|4_3XtWh|rWVn)^b`RuZmux;zuJp09Yc3}?! zTd;*~-NkO%&F+5i8kAJ5N6n8>Y`~xVjbvBICS}~d1a_VfLkOXPCDT}K!8N5xml>&B zHdeP~v~CN!aB2fq;<`aMj&ATF7Pj@8mPVF%VK*{3#+S6BUwjF;aTfMB1q(5(u_$7m z9oiW-uVNc^u!*wT*6*_~2+pPThO%(@}) z@6pSqA70Z_6*F0F`8O$)sa)puVdpeNs^^E6gsR!);H){swjaJmDUK+%E8u751B&sg z+Os8Zm0Vc*>530mT&~*q$EAN-@w*k*Q&rhJ6#eV2X(?0oD~40)C)2|jGo(0#d7-LM z0lRDmTevf9+65jYakt3K!1loA;6v=vd)fLHwq+Naw>xZf#Cf=|dp5F*H?yY3NS2k& zuVJlo*f}d%(>)NE5OZ%q06+l{{%YpzQDX&kcIB+GiVdR`8%C^`D%wYl(^z9^U|u9+ z+Qp1nagHf=@ura_Bh@f~cEws8$3^RK95Wk~Kn*&{G(F%AXUvS$%o{aTe^s-Fh5arm zlo57`7xu4@6wEo=2$^!{4>|w6`Et(c{`F9`DR;0qoKZolDZgkU+g{j3O(UHn+kyTz zMJraba4V_Cu8ic<1F4ac1;>qF6-^u57}zp62d>MipxI@Ej}91tU@q=M>I&rzEnzo0 zaa>({*@iB5Kf`8w`}Mf59pXA*i;gv52rdmh9O`A)?qqlUU~HFbbeD_m@LbyUAiI-c zE#9!!7mM^T=0wu729^v|vqcM7V{Jl|hbb2VU&poPF>T33E!hVZnie`7+Rv`q%G&p^ zTie-o4?Fk4u$K8thWXnYO-d3Os?=o)?@G*YB<-~Ro6&@p4qfJV-f8eA|7v>Org_w7 z#a2w~@*w^FJj3QP)$f-TY%Ws$p-2Pg$v4(0$r~&4bOP*#AmOlSh^mtUstznxcz^<#3JPRW zQwf+!FoHwq528RNF>A!WO$TAACY~znZcA>40KccGLVHxPSdg2xo+mL#kTKB9f=(r+ zN~V&5Q7Z9}m56V%Dw%>F^C!Y)spL5%U!X^BLP^%!X{5K)$wqOqt;lAQd)}beCGTI1 zYZAt)wwdi3Fg#cY1^hOdFbc#t)d`+e$p9q8(MSYtvAslL%Yoohj8%#I4w%h~F(u$K z$oKnzgg)9IkT#7-utic3V_Pywz$Kqq!fi5N+xO9>nh8i0!8lgWS4_e);t_Z3A^mkN zuuI3o}_Ujt&&nEAqH+%y|Dl%WuU-KcCn2?QKo&gR`0 zHrhaRHpfa_IWJ}pLH=@GA--Z zfNLTi*DYuq*)h_@ZifvOi0~wYSR2|AY8p1P)^%ZH1MVHMSWn=d55-TL!M9LApIUkn zKJD`I3RVl!l?ZMzAamcab=WYnoZa5V-oKCC*UMVmVXY@7uhV!(j+(7m!5UY7{{YQh zY%PK-KERQ9{*uSh93j%xuIyM(43K3Wx$L}^q{Z*2;>UcowSrV6f%em6pE7|dBmb01 zCv2+%XM`k+lR6lrSQC(W@Qp>8)JSe{g>SKnvV)j^;Zq;7GD%`zWKYo*nsCI){g{+a zl2elVu!;8eG-Y!C_H7q7SH#JE=mU^Is8l8#AosO4Q=Ht#MI_;fll!-?J(I{CBk9E~ zx#^w68!2r{(vDOzL(C}iYE zybbk%4&2nz`UbMCApuOt^GQt0431Lg^20!Oe#F6?5PVGq<9qn8 z7t&IGw4pK|0ed7|C$Uy>h=sDudC16Iz}yJR1skD6nt}D0I-={@-{Gyn{yCzY-Ec%p zE}^5~szbPf9^;e?<_lS|<9NhFLwPjKzHoNp*~gOi4!;9fVqauTf+@@=K!X?nWB5eM zjg4)sQZv7XJj~~q{05URAc5!)d?&e72!hxMdzNLG1>b-~MAL`&2#$%Nml$y2Kbc3i zvXadf@GVIo2?jIwegb6RnBja(8l1QEGJ2y(fWP8qYF zSrM_+_OH8QojzurebG8Q=wxd*u$z4Bfrr>f9t9KwyBIO{A%DH{=%)ULFU{Gf?4og` zaCO+U22CQdeFkjx>PW?M{7%M54Sv(({vQB|HpN!78_cl(*MQKHq3WTc;T|@xHEi5^ zw?IfjC=eVG=n1VDo-v#^VrDH(VQq6HJGWmSF=P(d!-j%L{`7z^xC6dCcXU%E%{<_L z))C2{7H|eP!&mM2TL;5w#r(asU|X`T;O`y?rxnQykH>%U5A1CZuv>P(X56rGmxQsF z4mAwbvDUR=<2poG(%u!cXaKAm>eY za4-lyc<{Yln%ocJSsy0E&U+5ehB5ggBwV%w&8zSkUk8LCFh$ ziXYvC?+EdlM+e@@Y&pE8%jsHFbDwWpmlNK6FR#9kl|TpAjpRF>%>RJQ{DMH3MMio0 zsGxJ&WqcQ}U?OmdE%VNWZ40V9kE!JycdYy=*}krbv3G;I=- zp)nbXX_BJ6W>9*YprpCMncX&N+_d+;V=QV#<-TxVJ-~W@lm>X|P5*h{xA!?mM?x4U zcir{YTk9ReKGPn)J)FIN-*10=t1K}go`K`jdr#Q+9AKEg;Eiy^@E6yQ!Nmz?A7hej zVO;bqbIHiP+$ASxg-Zcv`IeZ?N|$nTtSdGj{VBG@ZC1Hd`OIEfQ;HC6=v=_;jbALI z_byEuQ^1&FG8mI`v7CRVHOUH@(%7qb+gK_OYPL7noX*xx+xp%kLGAV}Jr1{PqrJ7O zx3g3hOx|(S>9Tci>FMZgXzc+{Y&(sHx_q3>^&@cK3i;W~xMU^<Zd2pM<#e0xwiC*B2_q!22~b|y~pmdSb`bh@>U2h5+Pn^0yrm_OL_B0^A`5S{7IF4 z9Ud@^?Xh^`4>g=2Q`+kRkP|W`JZGw9+Z9_GYYxNYF!`kT%`zP$WVJ@iFvYxID}>Ks z>NE_K2aT*DS&e5!Ljp7ys5EIPp*aa5JC|a}7NPQ)CM>In!xUqdX_-ZGA#6U=BFf*S zitr4#kgC!YYl`DTfoym{4sr?^vqDJO3ex53|O-x zzL{t(LyvKpQWq(NR-4Z>ETfA_pMt1A_5c`kg74%He}A%C{Z zTJnTih-Iw1FfTLwqSKpUP%D?THIOs|awtl9hx&^aK2uskCb>yvx{WWImqMn+D3N{} zZQnB~SCgSxMQV4Z(q>@`H)WGhF`-&S8Cwbk8JbnXo9Jb2XQ~ysj9pfm>%IgdP5jjp zKb?BkJMn_=$|wC-UV37D@WlAwFO1IpwyrMY#Ho+QKY01CA3ibh-n$bo|7fcJk*m+1 zxpMB@Ursy}U*%|JTf2?zz1=Vlw&F!9u56A!;T@zg_;&-tc)`lE@y(^p>d zO2XJ!wwJYZQ(zzldikv@@4Pa3`rXNakEdRKf9iuXAl78xd5C}}I{9wjg7ghbQ{~Y4VMiC*C+c^~+bs z2jAwC9DI^yVdCLmU48aVD*VK`_pY3Kaq^`{BnjIQ3|XwLuGT8!_=ldWC!U5(Q}G}P zdk^Fs3UKntH;pAu7h6g~vNo5S?Wr=BU>4`sw01dd4U$1_|pmB zi&xG)N81lJX!2R`^?%wk`CK1u>sUkd-Ia5%PX6%WDr1lVOb2Z*C^Ncoyq7k?D&sMw z+to2|d8sm(*xu@ZinUpw9-&bLV_dc)uAs7gzpedXP-W|B?}hfz8B~%cLt>)oO!oCp zJa)Eqh~?^u6BCcVKjrxW?`6{S=)|LsT>1DJcpCPIW|e7v4C*C}H<>wDiJwFH^)5Q5#CLTTy9!LQc8>L~~ z*#AjO7dn!Or%p{hdlr06oqa{jA2vp*KcgFwj`23$OBFRNHaPb@a3UK8Z3VQKd+URV zpFNJH@y5?Vc95|&1-^lSv8xZNgq|iCbD-DW6I5DlZSKxsywlz0 zV0+u4FA6GHcTX_J4w_LgmVVwrC460W2f2l~f+-sydYiS02H1pMYA~L5571g{!Q>j~ z78`qAHNEa0D}Alrbfn$pKqZDn&{7)D0%B)^T4^61Odx^^y>l=zyq67@%(T0No2zDH!DQll{KIME&yc~%xsLwTH`$*I!z47w1NsI|+c2du1af8#EcbL> z%wE)2e>o%T^pR6XJlfHWd41YTs>G+XC$;?#c)C3MxU|K7)sky5`Rdu1((+I5dwO3$ zmlMb@45S$X=~)4TF_3P&kr0=j*jIBy$0X>VsykUXP~pw-CUTj}{MzLKea1lg+1xX^ zV}|k(L;0wo!WVl!;jM(R1sg^dY#3dziPLZ5w445ZDakOFR6deaev{Fvs{%Q>XE&eO ze75mS<7iHKUjv`#fz{q}FJyVSU%NthRP8zR+I=tG=jq`LOZ?iU!sFH6f^#LWmz=A3 zy<&836<1o#8CLqWt3t2mdGk5L3ct2$Drs3DX_-H1^CeySY0W9kK#zB?cN|H*X;Cqm(F>|}DoY}@L>G1111NzK?g0m%OO3qfCsTj?g>uuol zD=unR{NqxR@dl$%Z;^#nj&J#3{c!R}5Pv;af1F!*pI>{wAk=bCy)XHdZCv4EUn6JO z;MZ=%$Y*&}-W*@+8~I$xiorZCZ!@Rgd_lYUQc@;HKRYaTt`cOsi!<-$7T)dG?hzth zMe1W|5F6)x>@+*i|loZx+29N^6=&lMM`kPMt0;2pkGm4 z$3g4_gDgX->{$GKo2z|(2Mo<^uQMzT29#K!A61g2)8?{}w-!gMYkx2=@@oozzXii1 zggC)`nUu|<#u`Q$U`GNnXs`c|I6@hqOyr1$C`1(oFa@akA;0ol(TCu-lK|@$9Nx*% z1nEQ`CQ#e~Aj?}2n~)=!d--7=mkISgmE z)0AwEB`RSpDck8~N+B}oi8QH;lp$$A7C?rPhl!?}W|mhdP3a^QDqBMOQ2!2K4jpl3 zl~`-=3@8;SorbJCln<39O!*>ZVu+LiIJFS*5^s+0$R|0SNsVQZ{ANoPM3jxsSB@#S zLg~<$<4t+x5`w|Bs9Zw+IDn23a%TlVn!_~5I1!uHsK>~|G)HKYX4&$)7^ai7r#dL>+xRaQ z{0nu&hk#@uy%ck@saQ%EKUy(2YU(9IY^KtP)KhF7q}3G&65l3Qp2nZ>rvxEr6yeQ?Ycrr*GT&9BEetiO#1El zrUl{sHqe&of*+ut)}YKlHio2*mbM|1n3JS@`w>G3C4M=C3RAC9lU_*afGfL9AZWgq zTnoRKTtcWB$yHkEz7K}%_=y+1lTSQ4@#asbe*S~0=Lg0IADifVe&U1oC;A?lc;bE3 zg?Wm|3KJIuWKbM?9i^#ihY+rc{(b@IdqG=#CF!M>hyFqHUmJ8i%V9kDnat?eMx ztGFiZD zHTL${P-J%lG+0VXNMi;i7I>tIhk->yQUapn%bS!m6i-CSK~&U3mGLGHpT-heqC9y} zT5c5XgR;^_7DrNnqQZ(IHVlQaQCpb_TJ7OyVGVhVv{p=wYJ&1ZsONVb7^$r zcx0S>`=!YjPKrf=N%|h4#b|URJrHVOm}Xq}K@yQ0F|V|=p#d66>_%EgZbWKE;4GxE zLkw;sJfHmeiOI9CkSYj$BM<`(3I&dVv0Q)k9PkI?M`d(lp@+)n%0HiyH(TzBh%s&1e&WL2cvDK|w@ z$C>2F)FICsqCBRduvhr#5uk3a{O}`M9^^y8mH}-=+J9gHquz7njaMg5J?7pHmwfI> zXY@Uy1Nv$5ZFlMnX3B$r-Ko&H&Pj0)+|lf|uU@|$*!Ei_+PXy$1EE_**` zXSw}O&Uyb0hIvr70hytV@&@^Jylcd}2V~puv`v0Dp7zLl@pQM`fhULj7?McG(pFlkO|`VV*v9*wu! zci3m=*0geM_j0X=xx>e})Z>2Deb-b;>dZi9&RAyINM_ks=JJuu<%1vH@;GFF@3=u{Q9*UAlbs=v01A| zX03u~1Bog9EBed0+)7Tn81h~iKcdR-iyKIXtm`tUlKCFQ;dXrI(JRmyV@ZkEBmVPx|@34d-j$svX=qwz$^6xb|{!8JAiVC@AvW{aU^!e?S??HlAH^ zW`)N*nqBUT{ZsbRK>qv-`O7}69CUwj^u43QhS4=U#@6f}S+kqFd++F)eVpZ3DW@wP2f9&vN(=39i>I2aIK<_$ex36Y z(v5`|bcLRc-VzXK*=GFalDEpg%*g)xjdgNHXF%F7Q~ai@2iFZ&a)vd2Eil7jEbtDl zc_+8S%q`sI*WUGS`V2zxW&Y#F##$5i z%%>UyxSy|B+*rsAFG>aX_nHE97p^OU(;u=l=+4an_m6SJEm{cI|B|6;G%EgOR%K(h z;$O3saQ&}^nr-EZf32w8R;>8%#Y(tt=Xo1QjA4-BfYC{N{Q|gP@)P3ZqRa>i`dw#9 zgnk!IQ@R5uCuN;XF@=;%z=1L1XhLKbaiCET@L+K;MXxr+SHzh#CT$oSR(K~P8&iVB zM-+QEf!lJ#m}8o-{z!~027&?bWqOC_`k&+3=kYbJp9!_u8 zfSW4iUd@^?uHp7Eq)E93Q#eB*ay20_fg?pe8aYz_djgX)Xya~qKKys6>st$n~Ha-Y* zmT2$6>#At0L9{QTW=x)Z^gpl23yN+isasM2Wqk6vcfiyE6mrzYh#3dP)RB5`9b|&4 zxW<{lV^o}oE=cGH&_KX4DOR9juDtms>d$U~v5XTHnZ{_h`YrfNlfnC1dFeM$#Af(nr&m_tj1% zr2~{NDIY7Y8Y!+CE3O?Wt{u+gcI@JIw{g}3Tvso5=m@v@C?rxR-zMjZYax-6Ci#f2 zh|?8ZDw%sO{ul9Fwc;Kmi#^;oBbJvW{S=+x2 zl!DodeXff|)q|}Ui`EP+zF1h>Uwva98de1WN3U3*=#Ki4r<4o-k-bxF&kt&J} z@;zAGd*l{8&AJB#OKX(}vWvOYV$hn>vZb-uxq<`!)PtZ^AqGx7mBcv_D4`YN_ zzR#4uRK2+46io~&6q3oIIDmyBgl`h<^#yqd^o~L0gTTD_MGA>;UC5VkC3zbKq?b#e z-t;~f^U&7;pT+b(anh2m){sETC<*|qPe4ei4Fjr}`%u|O3k609;nwN!A&UxwkZT~W zP(@1=6tjH%+(l(V>MI12ThQi?9MLTFLE%Y)&Vg@V4n=4MM<iiOOjCi7PUsAall0~o zQ>s}B<`!ur^>mPz&-Y=Fo9zU3Di(1J9#()1VY#10pG#dRyBJf3DU-xzkmeq0C7KLb z<`|gx&o;+E`^bTb|6EhvGO@%=xzOc88IpD=vUG5nYZ=k(<4T(*qiph3JQafSVdzHY5`1acd1XQ z?XR>a57z{+ao$9$P;s71>K7ueapU(3^AcCye3?c})h04oGx?C0&RB@~qc4qaTx?>T z=s*3}56^Q931b@jv}fBoY$C-wA`^Mjgc_}PWK8BkJrctM% zVL=jiDm)P;0z*EljIfw2)w#d-u*8;~*mWLDDk1Z%Er-iWOF)w;r81L=rz@}heDY`g zyaq@nHjL~>h+m=_$=Pm#=)=TfQ7a=B)O5DRjbD$DX+2z%NikAMG#m)dZpVmC=Q~bter$7O>LjQ-#%3v27{z1PMYy4iYM z>{0dz#1vE#2p?2-vb}BxnRN+j(At)T=MZ(Nnof~u=!67mu|Xwj4@cSOP?9!`L>I#P zJ?`!{DB%70t`i*`G@WX2MEK6nL|AQIwrOV~B0c;sjOh#La|Fy@_Of#pcBduye$6bD z0E|+(tfju=zV4w_T;_Ja)`aoXX$UTTuD9Adk4s(dSFM1c$aUw9sPYDm52bOcJin@z ze`p+089mv4)jXUA(4SU4t>O%2-aVtb6@ALDl2XGnQCUB{XSjje-NKPs53v{)`}X)6 zhV-0alV4i{LR?NYoUS`n=aD_r*r!ImyZQ`Rv_8L@Q)NQ3d9%iHmyhHw4y-I9x(PTEZavItLF9?&Y@bh_=|94RbCfYlmuww+vTv<_>OsCwJsNPFoD-sGubMyFQZwB)%Jk>P2#l z2T1E-5SlUny7vD*K!RZ-inSwUWy6Mi!0(Yq2;l=Lkas{wki%3krs8cfhoVJLD0M;(YEyjspIEW?WCJ7%4r3-wlxOA?QEMS!k_fbI0?S^}>_LG7x z93~KB0_{WPCN)VP1`4naHJHFIvqIsBYuF2K;w5jgO*#CVCU~PEZ$OoR zFuA5IQy#pFfl%w=omLVmUktTGQUNT;=N4^ctpL};;F~cbVYnr2t5C4RBy9^zSCrf|hX-UO?l=%He$w;d z#D`B!Jn<8X$>^Yr&`2=NiU4Gy{|GCKTUA|($zTA4HGp77P7JdqfAT0S3!3`X2U8!M z6>YP4ELRMM3X6-&jm1YghIz>ZQPn?{5{Bc5j2YR-A+Q@&FbX=wH?%?taFpWCi1$V}YK*|t z0u@|>R}ml-?4Kr|c@>fS$rHc{0}~I64iS)Ii@u#u0>DcLKM`e1;W7n}$JE^iJDe3* zZpY;FYL2PMEfgT!b?`6mg`y0~D;M#%q(qGnE%Noxo|}B)2MBp@X4RJy^d8L@^IFdH*6xy~XGy(nAjP8O~MELjlxGM0)sEsJU@y&rk!m zeUHE89H30QMRxbLzi`LWgiGj(y_rc1Lb#%3+OFl(vL z#jV;NSiWXRGk8B&-@z^I3@oi4+&-iPELvDA8`0+XZTlvcNlia%;VlBVyk&zL-~Aw9 zj>)gv@nuF%Agu_P<qa*{=LBOiovqM z+@WqRv)QlRDFwgf-aX!i!L-492lo%PbE#YXs=CW*8GZ2qO>)24ugMN%&Gxvwdtj~9 z$*q9|eZTt&OCYPrWA)a-IxPC=us

dTaKAn7hTxsC(TdWiA=63Ai7Pk1c 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 delta 340 zcmcc6$9S}lkyo3Kmx}=idg~f8`!@2v(Pn%w`ImMc(>LbH^VPH_S2)etY@o}{B&f>3 zAj1$O6eJuZ5+oWfq{lYdPhU}2ER2bVAxJ!o5lBiHv6?V~jACGrXYkbos+A0528v4= zF(GsaLv%=kWn>`wCSTH5WtRoBFnv5X7d;psBJs z!{9d4W)I^djJ!L6vc=DUb~iA55t`QI8-uLz`1>L=|9=a zOoOj{MdpUW6}~%64v1fn4BFgewx5;#707@<#>rle$&**xv@<@~Y-}sQ!pOPV#nGRI rQDO2F*S`WXtdd`B^jRf8B{9kZNxR7l-4^iYu`o)0W&n{zyg)Mm$$?fJ diff --git a/dialogs.py b/dialogs.py index 8b460d6..ffec8ef 100644 --- a/dialogs.py +++ b/dialogs.py @@ -20,8 +20,9 @@ class CreatePartitionDialog(QDialog): self.partition_table_type_combo.addItems(["gpt", "msdos"]) self.size_spinbox = QDoubleSpinBox() - self.size_spinbox.setMinimum(0.1) # 最小分区大小 0.1 GB - self.size_spinbox.setMaximum(self.max_available_mib / 1024.0) # 将 MiB 转换为 GB 显示 + # 调整最小值为 0.01 GB (约 10MB),并确保最大值不小于最小值 + self.size_spinbox.setMinimum(0.01) + self.size_spinbox.setMaximum(max(0.01, self.max_available_mib / 1024.0)) # 将 MiB 转换为 GB 显示 self.size_spinbox.setSuffix(" GB") self.size_spinbox.setDecimals(2) @@ -76,7 +77,8 @@ class CreatePartitionDialog(QDialog): if not use_max_space and size_gb <= 0: QMessageBox.warning(self, "输入错误", "分区大小必须大于0。") return None - if not use_max_space and size_gb > self.max_available_mib / 1024.0: + # 这里的检查应该使用 self.size_spinbox.maximum() 来判断,因为它是实际的最大值 + if not use_max_space and size_gb > self.size_spinbox.maximum(): QMessageBox.warning(self, "输入错误", "分区大小不能超过最大可用空间。") return None @@ -387,20 +389,21 @@ class CreateLvDialog(QDialog): # 确保 max_size_gb 至少是 spinbox 的最小值,以防卷组可用空间过小导致 UI 问题 if max_size_gb < self.lv_size_spinbox.minimum(): - self.lv_size_spinbox.setMinimum(max_size_gb) # 临时将最小值设为实际最大值 + # 如果实际最大可用空间小于最小允许值,则将最小值临时调整为实际最大值 + self.lv_size_spinbox.setMinimum(max_size_gb if max_size_gb > 0 else 0.01) # 至少0.01GB else: self.lv_size_spinbox.setMinimum(0.1) # 恢复正常最小值 - self.lv_size_spinbox.setMaximum(max_size_gb) # 设置最大值 + self.lv_size_spinbox.setMaximum(max(self.lv_size_spinbox.minimum(), max_size_gb)) # 设置最大值,确保不小于最小值 # 如果选中了“使用最大可用空间”,则将 spinbox 值设置为最大值 if self.use_max_space_checkbox.isChecked(): - self.lv_size_spinbox.setValue(max_size_gb) + self.lv_size_spinbox.setValue(self.lv_size_spinbox.maximum()) else: # 如果当前值超过了新的最大值,则调整为新的最大值 - if self.lv_size_spinbox.value() > max_size_gb: - self.lv_size_spinbox.setValue(max_size_gb) - # 如果当前值小于新的最小值 (例如,VG可用空间变为0,最小值被调整为0),则调整 + if self.lv_size_spinbox.value() > self.lv_size_spinbox.maximum(): + self.lv_size_spinbox.setValue(self.lv_size_spinbox.maximum()) + # 如果当前值小于新的最小值,则调整 elif self.lv_size_spinbox.value() < self.lv_size_spinbox.minimum(): self.lv_size_spinbox.setValue(self.lv_size_spinbox.minimum()) @@ -432,7 +435,8 @@ class CreateLvDialog(QDialog): if not use_max_space and size_gb <= 0: QMessageBox.warning(self, "输入错误", "逻辑卷大小必须大于0。") return None - if not use_max_space and size_gb > self.vg_sizes.get(vg_name, 0.0): + # 这里的检查应该使用 self.lv_size_spinbox.maximum() 来判断,因为它是实际的最大值 + if not use_max_space and size_gb > self.lv_size_spinbox.maximum(): QMessageBox.warning(self, "输入错误", "逻辑卷大小不能超过卷组的可用空间。") return None @@ -442,4 +446,3 @@ class CreateLvDialog(QDialog): 'size_gb': size_gb, 'use_max_space': use_max_space # 返回此标志 } - diff --git a/disk_operations.py b/disk_operations.py index 985d02b..803ad09 100644 --- a/disk_operations.py +++ b/disk_operations.py @@ -1,18 +1,14 @@ -# disk_operations.py import subprocess import logging -import os import re +import os from PySide6.QtWidgets import QMessageBox, QInputDialog -# 导入我们自己编写的系统信息管理模块 -from system_info import SystemInfoManager - logger = logging.getLogger(__name__) class DiskOperations: def __init__(self): - self.system_manager = SystemInfoManager() # 实例化 SystemInfoManager + pass def _execute_shell_command(self, command_list, error_message, root_privilege=True, suppress_critical_dialog_on_stderr_match=None, input_data=None): @@ -25,7 +21,6 @@ class DiskOperations: :param input_data: 传递给命令stdin的数据 (str)。 :return: (True/False, stdout_str, stderr_str) """ - # 确保 command_list 中的所有项都是字符串 if not all(isinstance(arg, str) for arg in command_list): logger.error(f"命令列表包含非字符串元素: {command_list}") QMessageBox.critical(None, "错误", f"内部错误:尝试执行的命令包含无效参数。\n命令详情: {command_list}") @@ -56,7 +51,9 @@ class DiskOperations: logger.error(f"标准错误: {stderr_output}") if suppress_critical_dialog_on_stderr_match and \ - suppress_critical_dialog_on_stderr_match in stderr_output: + (suppress_critical_dialog_on_stderr_match in stderr_output or \ + (isinstance(suppress_critical_dialog_on_stderr_match, tuple) and \ + any(s in stderr_output for s in suppress_critical_dialog_on_stderr_match))): logger.info(f"错误信息 '{stderr_output}' 匹配抑制条件,不显示关键错误对话框。") else: QMessageBox.critical(None, "错误", f"{error_message}\n错误详情: {stderr_output}") @@ -70,213 +67,356 @@ class DiskOperations: logger.error(f"执行命令 {full_cmd_str} 时发生未知错误: {e}") return False, "", str(e) - def _get_fstab_path(self): - return "/etc/fstab" + def _add_to_fstab(self, device_path, mount_point, fstype, uuid): + """ + 将设备的挂载信息添加到 /etc/fstab。 + 使用 UUID 识别设备以提高稳定性。 + """ + if not uuid or not mount_point or not fstype: + logger.error(f"无法将设备 {device_path} 添加到 fstab:缺少 UUID/挂载点/文件系统类型。") + QMessageBox.warning(None, "警告", f"无法将设备 {device_path} 添加到 fstab:缺少 UUID/挂载点/文件系统类型。") + return False + + fstab_entry = f"UUID={uuid} {mount_point} {fstype} defaults 0 2" + fstab_path = "/etc/fstab" + + try: + # 检查 fstab 中是否已存在相同 UUID 或挂载点的条目 + with open(fstab_path, 'r') as f: + fstab_content = f.readlines() + + for line in fstab_content: + if f"UUID={uuid}" in line: + logger.warning(f"设备 {device_path} (UUID={uuid}) 已存在于 fstab 中。跳过添加。") + QMessageBox.information(None, "信息", f"设备 {device_path} (UUID={uuid}) 已存在于 fstab 中。") + return True # 认为成功,因为目标已达成 + # 检查挂载点是否已存在 (可能被其他设备使用) + if mount_point in line.split(): + logger.warning(f"挂载点 {mount_point} 已存在于 fstab 中。跳过添加。") + QMessageBox.information(None, "信息", f"挂载点 {mount_point} 已存在于 fstab 中。") + return True # 认为成功,因为目标已达成 + + # 如果不存在,则追加到 fstab + with open(fstab_path, 'a') as f: + f.write(fstab_entry + '\n') + logger.info(f"已将 {fstab_entry} 添加到 {fstab_path}。") + QMessageBox.information(None, "成功", f"设备 {device_path} 已成功添加到 /etc/fstab。") + return True + except Exception as e: + logger.error(f"写入 {fstab_path} 失败: {e}") + QMessageBox.critical(None, "错误", f"写入 {fstab_path} 失败: {e}") + return False + + def _remove_fstab_entry(self, device_path): + """ + 从 /etc/fstab 中移除指定设备的条目。 + 此方法需要 SystemInfoManager 来获取设备的 UUID。 + """ + success, stdout, stderr = self._execute_shell_command( + ["lsblk", "-no", "UUID", device_path], + f"获取设备 {device_path} 的 UUID 失败", + root_privilege=False, # lsblk 通常不需要 sudo + suppress_critical_dialog_on_stderr_match=("找不到或无法访问", "No such device or address") # 匹配中英文错误 + ) + if not success: + logger.warning(f"无法获取设备 {device_path} 的 UUID,无法从 fstab 中移除。错误: {stderr}") + return False + + uuid = stdout.strip() + if not uuid: + logger.warning(f"设备 {device_path} 没有 UUID,无法从 fstab 中移除。") + return False + + fstab_path = "/etc/fstab" + try: + with open(fstab_path, 'r') as f: + lines = f.readlines() + + new_lines = [] + removed = False + for line in lines: + if f"UUID={uuid}" in line: + logger.info(f"从 {fstab_path} 中移除了条目: {line.strip()}") + removed = True + else: + new_lines.append(line) + + if removed: + # 写入临时文件,然后替换原文件 + with open(fstab_path + ".tmp", 'w') as f_tmp: + f_tmp.writelines(new_lines) + os.rename(fstab_path + ".tmp", fstab_path) + logger.info(f"已从 {fstab_path} 中移除 UUID={uuid} 的条目。") + return True + else: + logger.info(f"fstab 中未找到 UUID={uuid} 的条目。") + return False + except Exception as e: + logger.error(f"修改 {fstab_path} 失败: {e}") + QMessageBox.critical(None, "错误", f"修改 {fstab_path} 失败: {e}") + return False + + def mount_partition(self, device_path, mount_point, add_to_fstab=False): + """ + 挂载指定设备到指定挂载点。 + :param device_path: 要挂载的设备路径。 + :param mount_point: 挂载点。 + :param add_to_fstab: 是否添加到 /etc/fstab。 + :return: True 如果成功,否则 False。 + """ + if not os.path.exists(mount_point): + try: + os.makedirs(mount_point) + logger.info(f"创建挂载点目录: {mount_point}") + except OSError as e: + QMessageBox.critical(None, "错误", f"创建挂载点目录 {mount_point} 失败: {e}") + logger.error(f"创建挂载点目录 {mount_point} 失败: {e}") + return False + + logger.info(f"尝试挂载设备 {device_path} 到 {mount_point}。") + success, _, stderr = self._execute_shell_command( + ["mount", device_path, mount_point], + f"挂载设备 {device_path} 失败" + ) + if success: + QMessageBox.information(None, "成功", f"设备 {device_path} 已成功挂载到 {mount_point}。") + if add_to_fstab: + # 为了获取 fstype 和 UUID,需要再次调用 lsblk + success_details, stdout_details, stderr_details = self._execute_shell_command( + ["lsblk", "-no", "FSTYPE,UUID", device_path], + f"获取设备 {device_path} 的文件系统类型和 UUID 失败", + root_privilege=False, + suppress_critical_dialog_on_stderr_match=("找不到或无法访问", "No such device or address") + ) + if success_details: + fstype, uuid = stdout_details.strip().split() + self._add_to_fstab(device_path, mount_point, fstype, uuid) + else: + logger.error(f"无法获取设备 {device_path} 的详细信息以添加到 fstab: {stderr_details}") + QMessageBox.warning(None, "警告", f"设备 {device_path} 已挂载,但无法获取详细信息以添加到 fstab。") + return True + else: + return False + + def unmount_partition(self, device_path, show_dialog_on_error=True): + """ + 卸载指定设备。 + :param device_path: 要卸载的设备路径。 + :param show_dialog_on_error: 是否在发生错误时显示对话框。 + :return: True 如果成功,否则 False。 + """ + logger.info(f"尝试卸载设备 {device_path}。") + try: + success, _, stderr = self._execute_shell_command( + ["umount", device_path], + f"卸载设备 {device_path} 失败", + suppress_critical_dialog_on_stderr_match="not mounted" if not show_dialog_on_error else None + ) + if success: + QMessageBox.information(None, "成功", f"设备 {device_path} 已成功卸载。") + return True + else: + if show_dialog_on_error: + QMessageBox.critical(None, "错误", f"卸载设备 {device_path} 失败。\n错误详情: {stderr}") + return False + except Exception as e: + logger.error(f"卸载设备 {device_path} 时发生异常: {e}") + if show_dialog_on_error: + QMessageBox.critical(None, "错误", f"卸载设备 {device_path} 时发生异常: {e}") + return False + + def get_disk_free_space_info_mib(self, disk_path, total_disk_mib): + """ + 获取磁盘上最大的空闲空间块的起始位置 (MiB) 和大小 (MiB)。 + :param disk_path: 磁盘路径。 + :param total_disk_mib: 磁盘的总大小 (MiB),用于全新磁盘的计算。 + :return: (start_mib, size_mib) 元组。如果磁盘是全新的,返回 (0.0, total_disk_mib)。 + 如果磁盘有分区表但没有空闲空间,返回 (None, None)。 + """ + logger.debug(f"尝试获取磁盘 {disk_path} 的空闲空间信息 (MiB)。") + success, stdout, stderr = self._execute_shell_command( + ["parted", "-s", disk_path, "unit", "MiB", "print", "free"], + f"获取磁盘 {disk_path} 分区信息失败", + root_privilege=True + ) + + if not success: + logger.error(f"获取磁盘 {disk_path} 空闲空间信息失败: {stderr}") + return None, None + + logger.debug(f"parted print free 命令原始输出:\n{stdout}") + + free_spaces = [] + lines = stdout.splitlines() + # Regex to capture StartMiB and SizeMiB from a "Free Space" line + # It's made more flexible to match "Free Space", "空闲空间", and "可用空间" + # Example: " 0.02MiB 8192MiB 8192MiB 可用空间" + # We need to capture the first numeric value (Start) and the third numeric value (Size). + free_space_line_pattern = re.compile(r'^\s*(\d+\.?\d*)MiB\s+(\d+\.?\d*)MiB\s+(\d+\.?\d*)MiB\s+(?:Free Space|空闲空间|可用空间)') + + for line in lines: + match = free_space_line_pattern.match(line) + if match: + try: + start_mib = float(match.group(1)) # Capture StartMiB + size_mib = float(match.group(3)) # Capture SizeMiB (the third numeric value) + free_spaces.append({'start_mib': start_mib, 'size_mib': size_mib}) + except ValueError as ve: + logger.warning(f"解析 parted free space 行 '{line}' 失败: {ve}") + continue + + if not free_spaces: + logger.warning(f"在磁盘 {disk_path} 上未找到空闲空间。") + return None, None + + # 找到最大的空闲空间块 + largest_free_space = max(free_spaces, key=lambda x: x['size_mib']) + start_mib = largest_free_space['start_mib'] + size_mib = largest_free_space['size_mib'] + + logger.debug(f"磁盘 {disk_path} 的最大空闲空间块起始于 {start_mib:.2f} MiB,大小为 {size_mib:.2f} MiB。") + return start_mib, size_mib def create_partition(self, disk_path, partition_table_type, size_gb, total_disk_mib, use_max_space): """ 在指定磁盘上创建分区。 - :param disk_path: 磁盘设备路径,例如 /dev/sdb。 - :param partition_table_type: 分区表类型,'gpt' 或 'msdos'。 - :param size_gb: 分区大小(GB)。 + :param disk_path: 磁盘路径 (例如 /dev/sdb)。 + :param partition_table_type: 分区表类型 ('gpt' 或 'msdos')。 + :param size_gb: 分区大小 (GB)。 :param total_disk_mib: 磁盘总大小 (MiB)。 - :param use_max_space: 是否使用最大可用空间创建分区。 - :return: 新创建的分区路径,如果失败则为 None。 + :param use_max_space: 是否使用最大可用空间。 + :return: True 如果成功,否则 False。 """ if not isinstance(disk_path, str) or not isinstance(partition_table_type, str): - logger.error(f"尝试创建分区时传入无效的磁盘路径或分区表类型。磁盘: {disk_path} (类型: {type(disk_path)}), 类型: {partition_table_type} (类型: {type(partition_table_type)})") + logger.error(f"尝试创建分区时传入无效参数。磁盘路径: {disk_path}, 分区表类型: {partition_table_type}") QMessageBox.critical(None, "错误", "无效的磁盘路径或分区表类型。") - return None + return False - logger.info(f"尝试在 {disk_path} 上创建 {size_gb}GB 的分区,分区表类型为 {partition_table_type}。") - - # 1. 检查磁盘是否已经有分区表,如果没有则创建 - # parted -s /dev/sdb print + # 1. 检查磁盘是否有分区表 + has_partition_table = False try: - stdout, _ = self.system_manager._run_command(["parted", "-s", disk_path, "print"], root_privilege=True) - if "unrecognised disk label" in stdout: - logger.info(f"磁盘 {disk_path} 没有分区表,将创建 {partition_table_type} 分区表。") - success, _, _ = self._execute_shell_command( - ["parted", "-s", disk_path, "mklabel", partition_table_type], - f"创建分区表 {partition_table_type} 失败" - ) - if not success: - return None + success_check, stdout_check, stderr_check = self._execute_shell_command( + ["parted", "-s", disk_path, "print"], + f"检查磁盘 {disk_path} 分区表失败", + suppress_critical_dialog_on_stderr_match=("无法辨识的磁盘卷标", "unrecognized disk label"), + root_privilege=True + ) + if success_check: + if "Partition Table: unknown" not in stdout_check and "分区表:unknown" not in stdout_check: + has_partition_table = True + else: + logger.info(f"parted print 报告磁盘 {disk_path} 的分区表为 'unknown'。") else: - logger.info(f"磁盘 {disk_path} 已有分区表。") + logger.info(f"parted print 命令失败,但可能不是因为没有分区表,错误信息: {stderr_check}") except Exception as e: - logger.error(f"检查或创建分区表失败: {e}") - QMessageBox.critical(None, "错误", f"检查或创建分区表失败: {e}") - return None + logger.error(f"检查磁盘 {disk_path} 分区表时发生异常: {e}") + pass - # 2. 获取下一个分区的起始扇区 - start_sector = self.get_disk_next_partition_start_sector(disk_path) - if start_sector is None: - QMessageBox.critical(None, "错误", f"无法确定磁盘 {disk_path} 的分区起始位置。") - return None + actual_start_mib_for_parted = 0.0 - # 3. 构建 parted 命令 - parted_cmd = ["parted", "-s", disk_path, "mkpart", "primary"] + # 2. 如果没有分区表,则创建分区表 + if not has_partition_table: + reply = QMessageBox.question(None, "确认创建分区表", + f"磁盘 {disk_path} 没有分区表。您确定要创建 {partition_table_type} 分区表吗?" + f"此操作将擦除磁盘上的所有数据。", + QMessageBox.Yes | QMessageBox.No, QMessageBox.No) + if reply == QMessageBox.No: + logger.info(f"用户取消了在 {disk_path} 上创建分区表的操作。") + return False - # 对于 GPT,默认文件系统是 ext2。对于 msdos,默认是 ext2。 - # parted mkpart primary [fstype] start end - # 我们可以省略 fstype,让用户稍后格式化。 - - # 计算结束位置 - start_mib = start_sector * 512 / (1024 * 1024) # 转换为 MiB - - if use_max_space: - end_mib = total_disk_mib # 使用磁盘总大小作为结束位置 - parted_cmd.extend([f"{start_mib}MiB", f"{end_mib}MiB"]) + logger.info(f"尝试在 {disk_path} 上创建 {partition_table_type} 分区表。") + success, _, stderr = self._execute_shell_command( + ["parted", "-s", disk_path, "mklabel", partition_table_type], + f"创建 {partition_table_type} 分区表失败" + ) + if not success: + return False + # 对于新创建分区表的磁盘,第一个分区通常从 1MiB 开始以确保兼容性和对齐 + actual_start_mib_for_parted = 1.0 else: - end_mib = start_mib + size_gb * 1024 # 将 GB 转换为 MiB - parted_cmd.extend([f"{start_mib}MiB", f"{end_mib}MiB"]) + # 如果有分区表,获取下一个可用分区的起始位置 + start_mib_from_parted, _ = self.get_disk_free_space_info_mib(disk_path, total_disk_mib) + if start_mib_from_parted is None: + QMessageBox.critical(None, "错误", f"无法确定磁盘 {disk_path} 的分区起始位置或没有空闲空间。") + return False - # 执行创建分区命令 - success, stdout, stderr = self._execute_shell_command( - parted_cmd, + # 如果 parted 报告的起始位置非常接近 0 MiB (例如 0.0 MiB 或 0.02 MiB), + # 为了安全和兼容性,也将其调整为 1.0 MiB。 + # 否则,使用 parted 报告的精确起始位置。 + if start_mib_from_parted < 1.0: # covers 0.0MiB and values like 0.017MiB (17.4kB) + actual_start_mib_for_parted = 1.0 + else: + actual_start_mib_for_parted = start_mib_from_parted + + # 3. 确定分区结束位置 + if use_max_space: + end_pos = "100%" + size_for_log = "最大可用空间" + else: + # 计算结束 MiB + # 注意:这里计算的 end_mib 是基于用户请求的大小, + # parted 会根据实际可用空间和对齐进行微调。 + end_mib = actual_start_mib_for_parted + size_gb * 1024 + + # 简单检查,如果请求的大小导致结束位置超出磁盘总大小,则使用最大可用 + if end_mib > total_disk_mib: + QMessageBox.warning(None, "警告", f"请求的分区大小 ({size_gb}GB) 超出了可用空间。将调整为最大可用空间。") + end_pos = "100%" + size_for_log = "最大可用空间" + else: + end_pos = f"{end_mib}MiB" + size_for_log = f"{size_gb}GB" + + # 4. 创建分区 + create_cmd = ["parted", "-s", disk_path, "mkpart", "primary", f"{actual_start_mib_for_parted}MiB", end_pos] + logger.info(f"尝试在 {disk_path} 上创建 {size_for_log} 的主分区。命令: {' '.join(create_cmd)}") + success, _, stderr = self._execute_shell_command( + create_cmd, f"在 {disk_path} 上创建分区失败" ) if not success: - return None + return False - # 4. 刷新内核分区表 - self._execute_shell_command(["partprobe", disk_path], f"刷新 {disk_path} 分区表失败", root_privilege=True) - - # 5. 获取新创建的分区路径 - # 通常新分区是磁盘的最后一个分区,例如 /dev/sdb1, /dev/sdb2 等 - # 我们可以通过 lsblk 再次获取信息,找到最新的分区 - try: - # 重新获取设备信息,找到最新的分区 - devices = self.system_manager.get_block_devices() - new_partition_path = None - for dev in devices: - if dev.get('path') == disk_path and dev.get('children'): - # 找到最新的子分区 - latest_partition = None - for child in dev['children']: - if child.get('type') == 'part': - if latest_partition is None or int(child.get('maj:min').split(':')[1]) > int(latest_partition.get('maj:min').split(':')[1]): - latest_partition = child - if latest_partition: - new_partition_path = latest_partition.get('path') - break - - if new_partition_path: - logger.info(f"成功在 {disk_path} 上创建分区: {new_partition_path}") - return new_partition_path - else: - logger.error(f"在 {disk_path} 上创建分区成功,但未能确定新分区的设备路径。") - QMessageBox.warning(None, "警告", f"分区创建成功,但未能确定新分区的设备路径。请手动检查。") - return None - - except Exception as e: - logger.error(f"获取新创建分区路径失败: {e}") - QMessageBox.critical(None, "错误", f"获取新创建分区路径失败: {e}") - return None - - def get_disk_next_partition_start_sector(self, disk_path): - """ - 获取磁盘上下一个可用的分区起始扇区。 - :param disk_path: 磁盘设备路径,例如 /dev/sdb。 - :return: 下一个分区起始扇区(整数),如果失败则为 None。 - """ - if not isinstance(disk_path, str): - logger.error(f"传入 get_disk_next_partition_start_sector 的 disk_path 不是字符串: {disk_path} (类型: {type(disk_path)})") - return None - - try: - stdout, _ = self.system_manager._run_command(["parted", "-s", disk_path, "unit", "s", "print", "free"], root_privilege=True) - # 查找最大的空闲空间 - free_space_pattern = re.compile(r'^\s*\d+\s+([\d.]+s)\s+([\d.]+s)\s+([\d.]+s)\s+Free Space', re.MULTILINE) - matches = free_space_pattern.findall(stdout) - - if matches: - # 找到所有空闲空间的起始扇区,并取最小的作为下一个分区的起始 - # 或者,更准确地说,找到最后一个分区的结束扇区 + 1 - # 简化处理:如果没有分区,从 2048s 开始 (常见对齐) - # 如果有分区,从最后一个分区的结束扇区 + 1 开始 - - # 先尝试获取所有分区信息 - stdout_partitions, _ = self.system_manager._run_command(["parted", "-s", disk_path, "unit", "s", "print"], root_privilege=True) - partition_end_pattern = re.compile(r'^\s*\d+\s+[\d.]+s\s+([\d.]+s)', re.MULTILINE) - partition_end_sectors = [int(float(s.replace('s', ''))) for s in partition_end_pattern.findall(stdout_partitions)] - - if partition_end_sectors: - # 最后一个分区的结束扇区 + 1 - last_end_sector = max(partition_end_sectors) - # 确保对齐,通常是 1MiB (2048扇区) 或 4MiB (8192扇区) - # 这里我们简单地取下一个 1MiB 对齐的扇区 - start_sector = (last_end_sector // 2048 + 1) * 2048 - logger.debug(f"磁盘 {disk_path} 最后一个分区结束于 {last_end_sector}s,下一个分区起始扇区建议为 {start_sector}s。") - return start_sector - else: - # 没有分区,从 2048s 开始 - logger.debug(f"磁盘 {disk_path} 没有现有分区,建议起始扇区为 2048s。") - return 2048 # 2048扇区是 1MiB,常见的起始对齐位置 - else: - logger.warning(f"无法在磁盘 {disk_path} 上找到空闲空间信息。") - return None - except Exception as e: - logger.error(f"获取磁盘 {disk_path} 下一个分区起始扇区失败: {e}") - return None - - def get_disk_next_partition_start_mib(self, disk_path): - """ - 获取磁盘上下一个可用的分区起始位置 (MiB)。 - :param disk_path: 磁盘设备路径,例如 /dev/sdb。 - :return: 下一个分区起始位置 (MiB),如果失败则为 None。 - """ - start_sector = self.get_disk_next_partition_start_sector(disk_path) - if start_sector is not None: - return start_sector * 512 / (1024 * 1024) # 转换为 MiB - return None + QMessageBox.information(None, "成功", f"在 {disk_path} 上成功创建了 {size_for_log} 的分区。") + return True def delete_partition(self, device_path): """ - 删除指定的分区。 - :param device_path: 要删除的分区路径,例如 /dev/sdb1。 - :return: True 如果删除成功,否则 False。 + 删除指定分区。 + :param device_path: 要删除的分区路径。 + :return: True 如果成功,否则 False。 """ - if not isinstance(device_path, str): - logger.error(f"尝试删除非字符串设备路径: {device_path} (类型: {type(device_path)})") - QMessageBox.critical(None, "错误", "无效的设备路径。") - return False - reply = QMessageBox.question(None, "确认删除分区", - f"您确定要删除分区 {device_path} 吗?此操作不可逆!", + f"您确定要删除分区 {device_path} 吗?此操作将擦除分区上的所有数据!", QMessageBox.Yes | QMessageBox.No, QMessageBox.No) if reply == QMessageBox.No: logger.info(f"用户取消了删除分区 {device_path} 的操作。") return False - logger.info(f"尝试删除分区: {device_path}") + # 尝试卸载分区 + self.unmount_partition(device_path, show_dialog_on_error=False) # 静默卸载 - # 1. 尝试卸载分区 (静默处理,如果未挂载则不报错) - self.unmount_partition(device_path, show_dialog_on_error=False) - - # 2. 从 fstab 中移除条目 + # 从 fstab 中移除条目 self._remove_fstab_entry(device_path) - # 3. 获取父磁盘和分区号 - match = re.match(r'(/dev/\w+)(\d+)', device_path) + # 获取父磁盘和分区号 + # 例如 /dev/sdb1 -> /dev/sdb, 1 + match = re.match(r'(/dev/[a-z]+)(\d+)', device_path) if not match: - QMessageBox.critical(None, "错误", f"无法解析分区路径 {device_path}。") - logger.error(f"无法解析分区路径 {device_path}。") + QMessageBox.critical(None, "错误", f"无法解析设备路径 {device_path}。") + logger.error(f"无法解析设备路径 {device_path}。") return False disk_path = match.group(1) partition_number = match.group(2) - # 4. 执行 parted 命令删除分区 + logger.info(f"尝试删除分区 {device_path} (磁盘: {disk_path}, 分区号: {partition_number})。") success, _, stderr = self._execute_shell_command( ["parted", "-s", disk_path, "rm", partition_number], f"删除分区 {device_path} 失败" ) if success: QMessageBox.information(None, "成功", f"分区 {device_path} 已成功删除。") - # 刷新内核分区表 - self._execute_shell_command(["partprobe", disk_path], f"刷新 {disk_path} 分区表失败", root_privilege=True) return True else: return False @@ -284,48 +424,38 @@ class DiskOperations: def format_partition(self, device_path, fstype=None): """ 格式化指定分区。 - :param device_path: 要格式化的设备路径,例如 /dev/sdb1。 - :param fstype: 文件系统类型,例如 'ext4', 'xfs'。如果为 None,则弹出对话框让用户选择。 - :return: True 如果格式化成功,否则 False。 + :param device_path: 要格式化的分区路径。 + :param fstype: 文件系统类型 (例如 'ext4', 'xfs', 'fat32', 'ntfs')。如果为 None,则弹出对话框让用户选择。 + :return: True 如果成功,否则 False。 """ - if not isinstance(device_path, str): - logger.error(f"尝试格式化非字符串设备路径: {device_path} (类型: {type(device_path)})") - QMessageBox.critical(None, "错误", "无效的设备路径。") - return False - - # 1. 尝试卸载分区 (静默处理,如果未挂载则不报错) - self.unmount_partition(device_path, show_dialog_on_error=False) - - # 2. 从 fstab 中移除旧条目 (因为格式化后 UUID 会变) - self._remove_fstab_entry(device_path) - if fstype is None: - # 弹出对话框让用户选择文件系统类型 - items = ["ext4", "xfs", "fat32", "ntfs"] # 常用文件系统 - selected_fstype, ok = QInputDialog.getItem(None, "选择文件系统", - f"请为 {device_path} 选择文件系统类型:", - items, 0, False) - if not ok or not selected_fstype: - logger.info(f"用户取消了格式化 {device_path} 的操作。") + items = ("ext4", "xfs", "fat32", "ntfs") + fstype, ok = QInputDialog.getItem(None, "选择文件系统", "请选择要使用的文件系统类型:", items, 0, False) + if not ok or not fstype: + logger.info("用户取消了文件系统选择。") return False - fstype = selected_fstype reply = QMessageBox.question(None, "确认格式化分区", - f"您确定要格式化分区 {device_path} 为 {fstype} 吗?此操作将擦除所有数据!", + f"您确定要将分区 {device_path} 格式化为 {fstype} 吗?此操作将擦除分区上的所有数据!", QMessageBox.Yes | QMessageBox.No, QMessageBox.No) if reply == QMessageBox.No: logger.info(f"用户取消了格式化分区 {device_path} 的操作。") return False - logger.info(f"尝试格式化设备 {device_path} 为 {fstype}。") + # 尝试卸载分区 + self.unmount_partition(device_path, show_dialog_on_error=False) # 静默卸载 + # 从 fstab 中移除条目 (因为格式化会改变 UUID) + self._remove_fstab_entry(device_path) + + logger.info(f"尝试将分区 {device_path} 格式化为 {fstype}。") format_cmd = [] if fstype == "ext4": - format_cmd = ["mkfs.ext4", "-F", device_path] + format_cmd = ["mkfs.ext4", "-F", device_path] # -F 强制执行 elif fstype == "xfs": - format_cmd = ["mkfs.xfs", "-f", device_path] + format_cmd = ["mkfs.xfs", "-f", device_path] # -f 强制执行 elif fstype == "fat32": - format_cmd = ["mkfs.vfat", "-F", "32", device_path] + format_cmd = ["mkfs.fat", "-F", "32", device_path] elif fstype == "ntfs": format_cmd = ["mkfs.ntfs", "-f", device_path] else: @@ -335,202 +465,11 @@ class DiskOperations: success, _, stderr = self._execute_shell_command( format_cmd, - f"格式化设备 {device_path} 为 {fstype} 失败" + f"格式化分区 {device_path} 失败" ) if success: - QMessageBox.information(None, "成功", f"设备 {device_path} 已成功格式化为 {fstype}。") + QMessageBox.information(None, "成功", f"分区 {device_path} 已成功格式化为 {fstype}。") return True else: return False - def mount_partition(self, device_path, mount_point, add_to_fstab=False): - """ - 挂载一个分区。 - :param device_path: 要挂载的设备路径,例如 /dev/sdb1。 - :param mount_point: 挂载点,例如 /mnt/data。 - :param add_to_fstab: 是否将挂载信息添加到 /etc/fstab。 - :return: True 如果挂载成功,否则 False。 - """ - if not isinstance(device_path, str) or not isinstance(mount_point, str): - logger.error(f"尝试挂载时传入无效的设备路径或挂载点。设备: {device_path} (类型: {type(device_path)}), 挂载点: {mount_point} (类型: {type(mount_point)})") - QMessageBox.critical(None, "错误", "无效的设备路径或挂载点。") - return False - - logger.info(f"尝试挂载设备 {device_path} 到 {mount_point}。") - try: - # 确保挂载点存在 - if not os.path.exists(mount_point): - logger.debug(f"创建挂载点目录: {mount_point}") - os.makedirs(mount_point, exist_ok=True) - - # 执行挂载命令 - success, _, stderr = self._execute_shell_command( - ["mount", device_path, mount_point], - f"挂载设备 {device_path} 到 {mount_point} 失败" - ) - - if success: - QMessageBox.information(None, "成功", f"设备 {device_path} 已成功挂载到 {mount_point}。") - if add_to_fstab: - self._add_to_fstab(device_path, mount_point) - return True - else: - return False - except Exception as e: - logger.error(f"挂载设备 {device_path} 时发生错误: {e}") - QMessageBox.critical(None, "错误", f"挂载设备 {device_path} 失败。\n错误详情: {e}") - return False - - def unmount_partition(self, device_path, show_dialog_on_error=True): - """ - 卸载一个分区。 - :param device_path: 要卸载的设备路径,例如 /dev/sdb1 或 /dev/md0。 - :param show_dialog_on_error: 是否在卸载失败时显示错误对话框。 - :return: True 如果卸载成功,否则 False。 - """ - if not isinstance(device_path, str): - logger.error(f"尝试卸载非字符串设备路径: {device_path} (类型: {type(device_path)})") - if show_dialog_on_error: - QMessageBox.critical(None, "错误", f"无效的设备路径: {device_path}") - return False - - logger.info(f"尝试卸载设备: {device_path}") - try: - # 尝试从 fstab 中移除条目,如果存在的话 - # 注意:这里先移除 fstab 条目,是为了防止卸载失败后 fstab 中仍然有无效条目 - # 但如果卸载成功,fstab 条目也需要移除。 - # 如果卸载失败,且 fstab 移除成功,用户需要手动处理。 - # 更好的做法可能是:卸载成功后,再移除 fstab 条目。 - # 但为了简化逻辑,这里先移除,并假设卸载会成功。 - # 实际上,_remove_fstab_entry 应该在卸载成功后才调用,或者在删除分区/格式化时调用。 - # 对于单纯的卸载操作,不应该自动移除 fstab 条目,除非用户明确要求。 - # 考虑到当前场景是“卸载后刷新”,通常用户只是想临时卸载,fstab条目不应被删除。 - # 所以这里暂时注释掉,或者只在删除分区/格式化时调用。 - # self._remove_fstab_entry(device_path) - - # 执行卸载命令 - success, _, stderr = self._execute_shell_command( - ["umount", device_path], - f"卸载设备 {device_path} 失败", - suppress_critical_dialog_on_stderr_match="not mounted" if not show_dialog_on_error else None - ) - if success: - QMessageBox.information(None, "成功", f"设备 {device_path} 已成功卸载。") - return success - except Exception as e: - logger.error(f"卸载设备 {device_path} 时发生错误: {e}") - if show_dialog_on_error: - QMessageBox.critical(None, "错误", f"卸载设备 {device_path} 失败。\n错误详情: {e}") - return False - - def _add_to_fstab(self, device_path, mount_point): - """ - 将设备的挂载信息添加到 /etc/fstab。 - 使用 UUID 方式添加。 - """ - if not isinstance(device_path, str) or not isinstance(mount_point, str): - logger.error(f"尝试添加到 fstab 时传入无效的设备路径或挂载点。设备: {device_path} (类型: {type(device_path)}), 挂载点: {mount_point} (类型: {type(mount_point)})") - QMessageBox.critical(None, "错误", "无效的设备路径或挂载点,无法添加到 fstab。") - return False - - logger.info(f"尝试将设备 {device_path} (挂载点: {mount_point}) 添加到 /etc/fstab。") - device_details = self.system_manager.get_device_details_by_path(device_path) - uuid = device_details.get('uuid') if device_details else None - fstype = device_details.get('fstype') if device_details else None - - if not uuid or not fstype: - logger.error(f"无法获取设备 {device_path} 的 UUID 或文件系统类型,无法添加到 fstab。UUID: {uuid}, FSTYPE: {fstype}") - QMessageBox.critical(None, "错误", f"无法获取设备 {device_path} 的 UUID 或文件系统类型,无法添加到 /etc/fstab。") - return False - - fstab_entry = f"UUID={uuid} {mount_point} {fstype} defaults 0 2" - fstab_path = self._get_fstab_path() - - try: - # 检查是否已存在相同的挂载点或 UUID 条目 - with open(fstab_path, 'r') as f: - lines = f.readlines() - - for line in lines: - if f"UUID={uuid}" in line or mount_point in line.split(): - logger.warning(f"fstab 中已存在设备 {device_path} (UUID: {uuid}) 或挂载点 {mount_point} 的条目,跳过添加。") - return True # 已经存在,认为成功 - - # 添加新条目 - with open(fstab_path, 'a') as f: - f.write(fstab_entry + '\n') - logger.info(f"成功将 '{fstab_entry}' 添加到 {fstab_path}。") - return True - except Exception as e: - logger.error(f"将设备 {device_path} 添加到 /etc/fstab 失败: {e}") - QMessageBox.critical(None, "错误", f"将设备 {device_path} 添加到 /etc/fstab 失败。\n错误详情: {e}") - return False - - def _remove_fstab_entry(self, device_path): - """ - 从 /etc/fstab 中移除指定设备的条目。 - :param device_path: 设备路径,例如 /dev/sdb1 或 /dev/md0。 - :return: True 如果成功移除或没有找到条目,否则 False。 - """ - if not isinstance(device_path, str): - logger.warning(f"尝试移除 fstab 条目时传入非字符串设备路径: {device_path} (类型: {type(device_path)})") - return False - - logger.info(f"尝试从 /etc/fstab 移除与 {device_path} 相关的条目。") - fstab_path = self._get_fstab_path() - try: - device_details = self.system_manager.get_device_details_by_path(device_path) - uuid = device_details.get('uuid') if device_details else None - - logger.debug(f"尝试移除 fstab 条目,获取到的设备 {device_path} 的 UUID: {uuid}") - - # 如果没有 UUID 且不是 /dev/ 路径,则无法可靠地从 fstab 中移除 - if not uuid and not device_path.startswith('/dev/'): - logger.warning(f"无法获取设备 {device_path} 的 UUID,也无法识别为 /dev/ 路径,跳过 fstab 移除。") - return False - - with open(fstab_path, 'r') as f: - lines = f.readlines() - - new_lines = [] - removed_count = 0 - for line in lines: - # 检查是否是注释或空行 - if line.strip().startswith('#') or not line.strip(): - new_lines.append(line) - continue - - is_match = False - # 尝试匹配 UUID - if uuid: - if f"UUID={uuid}" in line: - is_match = True - # 尝试匹配设备路径 (如果 UUID 不存在或不匹配) - if not is_match and device_path: - # 分割行以避免部分匹配,例如 /dev/sda 匹配 /dev/sda1 - parts = line.split() - if len(parts) > 0 and parts[0] == device_path: - is_match = True - - if is_match: - logger.info(f"从 {fstab_path} 移除了条目: {line.strip()}") - removed_count += 1 - else: - new_lines.append(line) - - if removed_count > 0: - with open(fstab_path, 'w') as f: - f.writelines(new_lines) - logger.info(f"成功从 {fstab_path} 移除了 {removed_count} 个条目。") - return True - else: - logger.info(f"在 {fstab_path} 中未找到与 {device_path} 相关的条目。") - return True # 没有找到也算成功,因为目标是确保没有该条目 - except FileNotFoundError: - logger.warning(f"fstab 文件 {fstab_path} 不存在。") - return True - except Exception as e: - logger.error(f"从 /etc/fstab 移除条目时发生错误: {e}") - QMessageBox.critical(None, "错误", f"从 /etc/fstab 移除条目失败。\n错误详情: {e}") - return False - diff --git a/lvm_operations.py b/lvm_operations.py index 7dcbef8..0a20fd8 100644 --- a/lvm_operations.py +++ b/lvm_operations.py @@ -171,7 +171,6 @@ class LvmOperations: return False logger.info(f"尝试删除卷组: {vg_name}。") - # -y 自动确认, -f 强制删除所有逻辑卷 success, _, stderr = self._execute_shell_command( ["vgremove", "-y", "-f", vg_name], f"删除卷组 {vg_name} 失败" @@ -200,7 +199,6 @@ class LvmOperations: QMessageBox.critical(None, "错误", "逻辑卷大小必须大于0。") return False - # Confirmation message confirm_message = f"您确定要在卷组 {vg_name} 中创建逻辑卷 {lv_name} 吗?" if use_max_space: confirm_message += "使用卷组所有可用空间。" @@ -215,10 +213,10 @@ class LvmOperations: return False if use_max_space: - create_cmd = ["lvcreate", "-l", "100%FREE", "-n", lv_name, vg_name] + create_cmd = ["lvcreate", "-y", "-l", "100%FREE", "-n", lv_name, vg_name] logger.info(f"尝试在卷组 {vg_name} 中使用最大可用空间创建逻辑卷 {lv_name}。") else: - create_cmd = ["lvcreate", "-L", f"{size_gb}G", "-n", lv_name, vg_name] + create_cmd = ["lvcreate", "-y", "-L", f"{size_gb}G", "-n", lv_name, vg_name] logger.info(f"尝试在卷组 {vg_name} 中创建 {size_gb}GB 的逻辑卷 {lv_name}。") success, _, stderr = self._execute_shell_command( @@ -251,7 +249,6 @@ class LvmOperations: return False logger.info(f"尝试删除逻辑卷: {vg_name}/{lv_name}。") - # -y 自动确认 success, _, stderr = self._execute_shell_command( ["lvremove", "-y", f"{vg_name}/{lv_name}"], f"删除逻辑卷 {vg_name}/{lv_name} 失败" diff --git a/mainwindow.py b/mainwindow.py index f56c932..394e7f9 100644 --- a/mainwindow.py +++ b/mainwindow.py @@ -1,8 +1,7 @@ -# mainwindow.py import sys import logging import re -import os # 导入 os 模块 +import os from PySide6.QtWidgets import (QApplication, QMainWindow, QTreeWidgetItem, QMessageBox, QHeaderView, QMenu, QInputDialog, QDialog) @@ -56,7 +55,6 @@ class MainWindow(QMainWindow): self.ui.treeWidget_lvm.setContextMenuPolicy(Qt.CustomContextMenu) self.ui.treeWidget_lvm.customContextMenuRequested.connect(self.show_lvm_context_menu) - # 初始化时刷新所有数据 self.refresh_all_info() logger.info("所有设备信息已初始化加载。") @@ -126,7 +124,6 @@ class MainWindow(QMainWindow): item = self.ui.treeWidget_block_devices.itemAt(pos) menu = QMenu(self) - # 菜单项:创建 RAID 阵列,创建 PV create_menu = QMenu("创建...", self) create_raid_action = create_menu.addAction("创建 RAID 阵列...") create_raid_action.triggered.connect(self._handle_create_raid_array) @@ -135,7 +132,6 @@ class MainWindow(QMainWindow): menu.addMenu(create_menu) menu.addSeparator() - if item: dev_data = item.data(0, Qt.UserRole) if not dev_data: @@ -145,30 +141,25 @@ class MainWindow(QMainWindow): device_name = dev_data.get('name') device_type = dev_data.get('type') mount_point = dev_data.get('mountpoint') - device_path = dev_data.get('path') # 使用lsblk提供的完整路径 + device_path = dev_data.get('path') - if not device_path: # 如果path字段缺失,则尝试从name构造 + if not device_path: device_path = f"/dev/{device_name}" - - # 针对磁盘 (disk) 的操作 if device_type == 'disk': create_partition_action = menu.addAction(f"创建分区 {device_path}...") - create_partition_action.triggered.connect(lambda: self._handle_create_partition(device_path, dev_data.get('size'))) # 传递原始大小字符串 + create_partition_action.triggered.connect(lambda: self._handle_create_partition(device_path, dev_data)) menu.addSeparator() - # 挂载/卸载操作 (针对分区 'part' 和 RAID/LVM 逻辑卷,但这里只处理 'part') if device_type == 'part': if not mount_point or mount_point == '' or mount_point == 'N/A': mount_action = menu.addAction(f"挂载 {device_path}...") mount_action.triggered.connect(lambda: self._handle_mount(device_path)) elif mount_point != '[SWAP]': unmount_action = menu.addAction(f"卸载 {device_path}") - # FIX: 更改 lambda 表达式,避免被 triggered 信号的参数覆盖 unmount_action.triggered.connect(lambda: self._unmount_and_refresh(device_path)) menu.addSeparator() - # 删除分区和格式化操作 (针对分区 'part') delete_action = menu.addAction(f"删除分区 {device_path}") delete_action.triggered.connect(lambda: self._handle_delete_partition(device_path)) @@ -180,9 +171,9 @@ class MainWindow(QMainWindow): else: logger.info("右键点击了空白区域或设备没有可用的操作。") - def _handle_create_partition(self, disk_path, total_size_str): # 接收原始大小字符串 - # 1. 解析磁盘总大小 (MiB) + def _handle_create_partition(self, disk_path, dev_data): total_disk_mib = 0.0 + total_size_str = dev_data.get('size') logger.debug(f"尝试为磁盘 {disk_path} 创建分区。原始大小字符串: '{total_size_str}'") if total_size_str: @@ -212,70 +203,55 @@ class MainWindow(QMainWindow): QMessageBox.critical(self, "错误", f"无法获取磁盘 {disk_path} 的有效总大小。") return - # 2. 获取下一个分区的起始位置 (MiB) - start_position_mib = self.disk_ops.get_disk_next_partition_start_mib(disk_path) - if start_position_mib is None: # 如果获取起始位置失败 - QMessageBox.critical(self, "错误", f"无法确定磁盘 {disk_path} 的分区起始位置。") - return + start_position_mib = 0.0 + max_available_mib = total_disk_mib - # 3. 计算最大可用空间 (MiB) - max_available_mib = total_disk_mib - start_position_mib - if max_available_mib < 0: # 安全检查,理论上不应该发生 - max_available_mib = 0.0 + if dev_data.get('children'): + logger.debug(f"磁盘 {disk_path} 存在现有分区,尝试计算下一个分区起始位置。") + calculated_start_mib = self.disk_ops.get_disk_next_partition_start_mib(disk_path) + if calculated_start_mib is None: + QMessageBox.critical(self, "错误", f"无法确定磁盘 {disk_path} 的分区起始位置。") + return + start_position_mib = calculated_start_mib + max_available_mib = total_disk_mib - start_position_mib + if max_available_mib < 0: + max_available_mib = 0.0 + else: + logger.debug(f"磁盘 {disk_path} 没有现有分区,假定从 0 MiB 开始,最大可用空间为整个磁盘。") + max_available_mib = max(0.0, total_disk_mib - 1.0) + start_position_mib = 1.0 + logger.debug(f"磁盘 /dev/sdd 没有现有分区,假定从 {start_position_mib} MiB 开始,最大可用空间为 {max_available_mib} MiB。") - # 确保 max_available_mib 至少为 1 MiB,以避免 spinbox 最小值大于最大值的问题 - # 0.1 GB = 102.4 MiB, so 1 MiB is a safe minimum for fdisk/parted - if max_available_mib < 1.0: - max_available_mib = 1.0 # 最小可分配空间 (1 MiB) - - dialog = CreatePartitionDialog(self, disk_path, total_disk_mib, max_available_mib) # 传递 total_disk_mib 和 max_available_mib + dialog = CreatePartitionDialog(self, disk_path, total_disk_mib, max_available_mib) if dialog.exec() == QDialog.Accepted: info = dialog.get_partition_info() - if info: # Check if info is not None (dialog might have returned None on validation error) - # 调用 disk_ops.create_partition,传递 total_disk_mib 和 use_max_space 标志 - new_partition_path = self.disk_ops.create_partition( + if info: + if self.disk_ops.create_partition( info['disk_path'], info['partition_table_type'], info['size_gb'], - info['total_disk_mib'], # 传递磁盘总大小 (MiB) - info['use_max_space'] # 传递是否使用最大空间的标志 - ) - if new_partition_path: - self.refresh_all_info() # 刷新以显示新创建的分区 - - # 询问用户是否要格式化新创建的分区 - reply = QMessageBox.question(None, "格式化新分区", - f"分区 {new_partition_path} 已成功创建。\n" - "您现在想格式化这个分区吗?", - QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes) - if reply == QMessageBox.Yes: - # 调用格式化函数,fstype=None 会弹出选择框 - if self.disk_ops.format_partition(new_partition_path, fstype=None): - self.refresh_all_info() # 格式化成功后再次刷新 - # else: create_partition already shows an error message - # else: dialog was cancelled, no action needed + info['total_disk_mib'], + info['use_max_space'] + ): + self.refresh_all_info() def _handle_mount(self, device_path): dialog = MountDialog(self, device_path) if dialog.exec() == QDialog.Accepted: info = dialog.get_mount_info() - if info: # 确保 info 不是 None (用户可能在 MountDialog 中取消了操作) - if self.disk_ops.mount_partition(device_path, info['mount_point'], info['add_to_fstab']): # <--- 传递 add_to_fstab + if info: + if self.disk_ops.mount_partition(device_path, info['mount_point'], info['add_to_fstab']): self.refresh_all_info() - # 新增辅助方法,用于卸载并刷新 def _unmount_and_refresh(self, device_path): - """Helper method to unmount a device and then refresh all info.""" if self.disk_ops.unmount_partition(device_path, show_dialog_on_error=True): self.refresh_all_info() def _handle_delete_partition(self, device_path): - # delete_partition 内部会调用 unmount_partition 和 _remove_fstab_entry if self.disk_ops.delete_partition(device_path): self.refresh_all_info() def _handle_format_partition(self, device_path): - # format_partition 内部会调用 unmount_partition 和 _remove_fstab_entry if self.disk_ops.format_partition(device_path): self.refresh_all_info() @@ -285,7 +261,7 @@ class MainWindow(QMainWindow): raid_headers = [ "阵列设备", "级别", "状态", "大小", "活动设备", "失败设备", "备用设备", - "总设备数", "UUID", "名称", "Chunk Size", "挂载点" # 添加挂载点列 + "总设备数", "UUID", "名称", "Chunk Size", "挂载点" ] self.ui.treeWidget_raid.setColumnCount(len(raid_headers)) self.ui.treeWidget_raid.setHeaderLabels(raid_headers) @@ -304,7 +280,6 @@ class MainWindow(QMainWindow): for array in raid_arrays: array_item = QTreeWidgetItem(self.ui.treeWidget_raid) array_path = array.get('device', 'N/A') - # 确保 array_path 是一个有效的路径,否则上下文菜单会失效 if not array_path or array_path == 'N/A': logger.warning(f"RAID阵列 '{array.get('name', '未知')}' 的设备路径无效,跳过。") continue @@ -322,24 +297,19 @@ class MainWindow(QMainWindow): array_item.setText(8, array.get('uuid', 'N/A')) array_item.setText(9, array.get('name', 'N/A')) array_item.setText(10, array.get('chunk_size', 'N/A')) - array_item.setText(11, current_mount_point if current_mount_point else "") # 显示挂载点 + array_item.setText(11, current_mount_point if current_mount_point else "") array_item.setExpanded(True) - # 存储原始数据,用于上下文菜单 - # 确保 array_path 字段在存储的数据中是正确的 array_data_for_context = array.copy() array_data_for_context['device'] = array_path array_item.setData(0, Qt.UserRole, array_data_for_context) - # 添加成员设备作为子节点 for member in array.get('member_devices', []): member_item = QTreeWidgetItem(array_item) member_item.setText(0, f" {member.get('device_path', 'N/A')}") member_item.setText(1, f"成员: {member.get('raid_device', 'N/A')}") member_item.setText(2, member.get('state', 'N/A')) member_item.setText(3, f"Major: {member.get('major', 'N/A')}, Minor: {member.get('minor', 'N/A')}") - # 成员设备不存储UserRole数据,因为操作通常针对整个阵列 - # 自动调整列宽 for i in range(len(raid_headers)): self.ui.treeWidget_raid.resizeColumnToContents(i) logger.info("RAID阵列信息刷新成功。") @@ -352,30 +322,27 @@ class MainWindow(QMainWindow): item = self.ui.treeWidget_raid.itemAt(pos) menu = QMenu(self) - # 始终提供创建 RAID 阵列的选项 create_raid_action = menu.addAction("创建 RAID 阵列...") create_raid_action.triggered.connect(self._handle_create_raid_array) menu.addSeparator() - if item and item.parent() is None: # 只有点击的是顶层RAID阵列项 + if item and item.parent() is None: array_data = item.data(0, Qt.UserRole) if not array_data: logger.warning(f"无法获取 RAID 阵列 {item.text(0)} 的详细数据。") return - array_path = array_data.get('device') # e.g., /dev/md126 + array_path = array_data.get('device') member_devices = [m.get('device_path') for m in array_data.get('member_devices', [])] if not array_path or array_path == 'N/A': logger.warning(f"RAID阵列 '{array_data.get('name', '未知')}' 的设备路径无效,无法显示操作。") - return # 如果路径无效,则不显示任何操作 + return - # Check if RAID array is mounted current_mount_point = self.system_manager.get_mountpoint_for_device(array_path) if current_mount_point and current_mount_point != '[SWAP]' and current_mount_point != '': unmount_action = menu.addAction(f"卸载 {array_path} ({current_mount_point})") - # FIX: 更改 lambda 表达式,避免被 triggered 信号的参数覆盖 unmount_action.triggered.connect(lambda: self._unmount_and_refresh(array_path)) else: mount_action = menu.addAction(f"挂载 {array_path}...") @@ -389,7 +356,6 @@ class MainWindow(QMainWindow): delete_action.triggered.connect(lambda: self._handle_delete_raid_array(array_path, member_devices)) format_action = menu.addAction(f"格式化阵列 {array_path}...") - # RAID 阵列的格式化也直接调用 disk_ops.format_partition format_action.triggered.connect(lambda: self._handle_format_raid_array(array_path)) if menu.actions(): @@ -398,8 +364,6 @@ class MainWindow(QMainWindow): logger.info("右键点击了空白区域或没有可用的RAID操作。") def _handle_create_raid_array(self): - # 获取所有可用于创建RAID阵列的设备 - # get_unallocated_partitions 现在应该返回所有合适的磁盘和分区 available_devices = self.system_manager.get_unallocated_partitions() if not available_devices: @@ -409,7 +373,7 @@ class MainWindow(QMainWindow): dialog = CreateRaidDialog(self, available_devices) if dialog.exec() == QDialog.Accepted: info = dialog.get_raid_info() - if info: # Check if info is not None + if info: if self.raid_ops.create_raid_array(info['devices'], info['level'], info['chunk_size']): self.refresh_all_info() @@ -422,10 +386,6 @@ class MainWindow(QMainWindow): self.refresh_all_info() def _handle_format_raid_array(self, array_path): - # 格式化RAID阵列与格式化分区类似,可以复用DiskOperations中的逻辑 - # 或者在RaidOperations中实现一个独立的format方法 - # 这里我们直接调用DiskOperations的format_partition方法 - # format_partition 内部会调用 unmount_partition(..., show_dialog_on_error=False) 和 _remove_fstab_entry if self.disk_ops.format_partition(array_path): self.refresh_all_info() @@ -433,7 +393,7 @@ class MainWindow(QMainWindow): def refresh_lvm_info(self): self.ui.treeWidget_lvm.clear() - lvm_headers = ["名称", "大小", "属性", "UUID", "关联", "空闲/已用", "路径/格式", "挂载点"] # 添加挂载点列 + lvm_headers = ["名称", "大小", "属性", "UUID", "关联", "空闲/已用", "路径/格式", "挂载点"] self.ui.treeWidget_lvm.setColumnCount(len(lvm_headers)) self.ui.treeWidget_lvm.setHeaderLabels(lvm_headers) @@ -453,16 +413,14 @@ class MainWindow(QMainWindow): pv_root_item = QTreeWidgetItem(self.ui.treeWidget_lvm) pv_root_item.setText(0, "物理卷 (PVs)") pv_root_item.setExpanded(True) - pv_root_item.setData(0, Qt.UserRole, {'type': 'pv_root'}) # 标识根节点 + pv_root_item.setData(0, Qt.UserRole, {'type': 'pv_root'}) if lvm_data.get('pvs'): for pv in lvm_data['pvs']: pv_item = QTreeWidgetItem(pv_root_item) pv_name = pv.get('pv_name', 'N/A') - if pv_name.startswith('/dev/'): # Ensure it's a full path + if pv_name.startswith('/dev/'): pv_path = pv_name else: - # Attempt to construct if it's just a name, though pv_name from pvs usually is full path - # Fallback, may not be accurate if system_manager.get_lvm_info() isn't consistent pv_path = f"/dev/{pv_name}" if pv_name != 'N/A' else 'N/A' pv_item.setText(0, pv_name) @@ -472,11 +430,10 @@ class MainWindow(QMainWindow): pv_item.setText(4, f"VG: {pv.get('vg_name', 'N/A')}") pv_item.setText(5, f"空闲: {pv.get('pv_free', 'N/A')}") pv_item.setText(6, pv.get('pv_fmt', 'N/A')) - pv_item.setText(7, "") # PV没有直接挂载点 - # 存储原始数据,确保pv_name是正确的设备路径 + pv_item.setText(7, "") pv_data_for_context = pv.copy() - pv_data_for_context['pv_name'] = pv_path # Store the full path for context menu - pv_item.setData(0, Qt.UserRole, {'type': 'pv', 'data': pv_data_for_context}) # 存储原始数据 + pv_data_for_context['pv_name'] = pv_path + pv_item.setData(0, Qt.UserRole, {'type': 'pv', 'data': pv_data_for_context}) else: item = QTreeWidgetItem(pv_root_item) item.setText(0, "未找到物理卷。") @@ -497,8 +454,7 @@ class MainWindow(QMainWindow): vg_item.setText(4, f"PVs: {vg.get('pv_count', 'N/A')}, LVs: {vg.get('lv_count', 'N/A')}") vg_item.setText(5, f"空闲: {vg.get('vg_free', 'N/A')}, 已分配: {vg.get('vg_alloc_percent', 'N/A')}%") vg_item.setText(6, vg.get('vg_fmt', 'N/A')) - vg_item.setText(7, "") # VG没有直接挂载点 - # 存储原始数据,确保vg_name是正确的 + vg_item.setText(7, "") vg_data_for_context = vg.copy() vg_data_for_context['vg_name'] = vg_name vg_item.setData(0, Qt.UserRole, {'type': 'vg', 'data': vg_data_for_context}) @@ -518,14 +474,12 @@ class MainWindow(QMainWindow): vg_name = lv.get('vg_name', 'N/A') lv_attr = lv.get('lv_attr', '') - # 确保 lv_path 是一个有效的路径 lv_path = lv.get('lv_path') if not lv_path or lv_path == 'N/A': - # 如果 lv_path 不存在或为 N/A,则尝试从 vg_name 和 lv_name 构造 if vg_name != 'N/A' and lv_name != 'N/A': lv_path = f"/dev/{vg_name}/{lv_name}" else: - lv_path = 'N/A' # 实在无法构造则标记为N/A + lv_path = 'N/A' current_mount_point = self.system_manager.get_mountpoint_for_device(lv_path) @@ -535,10 +489,9 @@ class MainWindow(QMainWindow): lv_item.setText(3, lv.get('lv_uuid', 'N/A')) lv_item.setText(4, f"VG: {vg_name}, Origin: {lv.get('origin', 'N/A')}") lv_item.setText(5, f"快照: {lv.get('snap_percent', 'N/A')}%") - lv_item.setText(6, lv_path) # 显示构造或获取的路径 - lv_item.setText(7, current_mount_point if current_mount_point else "") # 显示挂载点 + lv_item.setText(6, lv_path) + lv_item.setText(7, current_mount_point if current_mount_point else "") - # 存储原始数据,确保 lv_path, lv_name, vg_name, lv_attr 都是正确的 lv_data_for_context = lv.copy() lv_data_for_context['lv_path'] = lv_path lv_data_for_context['lv_name'] = lv_name @@ -561,7 +514,6 @@ class MainWindow(QMainWindow): item = self.ui.treeWidget_lvm.itemAt(pos) menu = QMenu(self) - # 根节点或空白区域的创建菜单 create_menu = QMenu("创建...", self) create_pv_action = create_menu.addAction("创建物理卷 (PV)...") create_pv_action.triggered.connect(self._handle_create_pv) @@ -595,30 +547,25 @@ class MainWindow(QMainWindow): lv_name = data.get('lv_name') vg_name = data.get('vg_name') lv_attr = data.get('lv_attr', '') - lv_path = data.get('lv_path') # e.g., /dev/vgname/lvname + lv_path = data.get('lv_path') - # 确保所有关键信息都存在且有效 if lv_name and vg_name and lv_path and lv_path != 'N/A': - # Activation/Deactivation - if 'a' in lv_attr: # 'a' 表示 active + if 'a' in lv_attr: deactivate_lv_action = menu.addAction(f"停用逻辑卷 {lv_name}") deactivate_lv_action.triggered.connect(lambda: self._handle_deactivate_lv(lv_name, vg_name)) else: activate_lv_action = menu.addAction(f"激活逻辑卷 {lv_name}") activate_lv_action.triggered.connect(lambda: self._handle_activate_lv(lv_name, vg_name)) - # Mount/Unmount current_mount_point = self.system_manager.get_mountpoint_for_device(lv_path) if current_mount_point and current_mount_point != '[SWAP]' and current_mount_point != '': unmount_lv_action = menu.addAction(f"卸载 {lv_name} ({current_mount_point})") - # FIX: 更改 lambda 表达式,避免被 triggered 信号的参数覆盖 unmount_lv_action.triggered.connect(lambda: self._unmount_and_refresh(lv_path)) else: mount_lv_action = menu.addAction(f"挂载 {lv_name}...") mount_lv_action.triggered.connect(lambda: self._handle_mount(lv_path)) - menu.addSeparator() # 在挂载/卸载后添加分隔符 + menu.addSeparator() - # Delete and Format delete_lv_action = menu.addAction(f"删除逻辑卷 {lv_name}") delete_lv_action.triggered.connect(lambda: self._handle_delete_lv(lv_name, vg_name)) @@ -641,7 +588,7 @@ class MainWindow(QMainWindow): dialog = CreatePvDialog(self, available_partitions) if dialog.exec() == QDialog.Accepted: info = dialog.get_pv_info() - if info: # Check if info is not None + if info: if self.lvm_ops.create_pv(info['device_path']): self.refresh_all_info() @@ -652,17 +599,14 @@ class MainWindow(QMainWindow): def _handle_create_vg(self): lvm_info = self.system_manager.get_lvm_info() available_pvs = [] - # Filter PVs that are not part of any VG for pv in lvm_info.get('pvs', []): pv_name = pv.get('pv_name') - if pv_name and pv_name != 'N/A' and not pv.get('vg_name'): # Check if pv_name is not associated with any VG + if pv_name and pv_name != 'N/A' and not pv.get('vg_name'): if pv_name.startswith('/dev/'): available_pvs.append(pv_name) else: - # Attempt to construct full path if only name is given available_pvs.append(f"/dev/{pv_name}") - if not available_pvs: QMessageBox.warning(self, "警告", "没有可用于创建卷组的物理卷。请确保有未分配给任何卷组的物理卷。") return @@ -670,43 +614,37 @@ class MainWindow(QMainWindow): dialog = CreateVgDialog(self, available_pvs) if dialog.exec() == QDialog.Accepted: info = dialog.get_vg_info() - if info: # Check if info is not None + if info: if self.lvm_ops.create_vg(info['vg_name'], info['pvs']): self.refresh_all_info() - def _handle_delete_vg(self, vg_name): - if self.lvm_ops.delete_vg(vg_name): - self.refresh_all_info() - def _handle_create_lv(self): lvm_info = self.system_manager.get_lvm_info() available_vgs = [] - vg_sizes = {} # 存储每个 VG 的可用大小 (GB) + vg_sizes = {} for vg in lvm_info.get('vgs', []): vg_name = vg.get('vg_name') if vg_name and vg_name != 'N/A': available_vgs.append(vg_name) - free_size_str = vg.get('vg_free', '0B').strip() # 确保去除空白字符 - current_vg_size_gb = 0.0 # 为当前VG初始化大小 + free_size_str = vg.get('vg_free', '0B').strip() + current_vg_size_gb = 0.0 - # LVM输出通常使用 'g', 'm', 't', 'k' 作为单位,而不是 'GB', 'MB' - # 匹配数字部分和可选的单位 - match = re.match(r'(\d+\.?\d*)\s*([gmkt])?', free_size_str, re.IGNORECASE) + match = re.match(r'(\d+\.?\d*)\s*([gmktb])?', free_size_str, re.IGNORECASE) if match: value = float(match.group(1)) - unit = match.group(2).lower() if match.group(2) else '' # 获取单位,转小写 + unit = match.group(2).lower() if match.group(2) else '' - if unit == 'k': # Kilobytes + if unit == 'k': current_vg_size_gb = value / (1024 * 1024) - elif unit == 'm': # Megabytes + elif unit == 'm': current_vg_size_gb = value / 1024 - elif unit == 'g': # Gigabytes + elif unit == 'g': current_vg_size_gb = value - elif unit == 't': # Terabytes + elif unit == 't': current_vg_size_gb = value * 1024 - elif unit == '': # If no unit, assume GB if it's a large number, or just the value - current_vg_size_gb = value # This might be less accurate, but matches common LVM output + elif unit == 'b' or unit == '': + current_vg_size_gb = value / (1024 * 1024 * 1024) else: logger.warning(f"未知LVM单位: '{unit}' for '{free_size_str}'") else: @@ -714,26 +652,21 @@ class MainWindow(QMainWindow): vg_sizes[vg_name] = current_vg_size_gb - if not available_vgs: QMessageBox.warning(self, "警告", "没有可用于创建逻辑卷的卷组。") return - dialog = CreateLvDialog(self, available_vgs, vg_sizes) # 传递 vg_sizes + dialog = CreateLvDialog(self, available_vgs, vg_sizes) if dialog.exec() == QDialog.Accepted: info = dialog.get_lv_info() - if info: # 确保 info 不是 None (用户可能在 CreateLvDialog 中取消了操作) - # 传递 use_max_space 标志给 lvm_ops.create_lv + if info: if self.lvm_ops.create_lv(info['lv_name'], info['vg_name'], info['size_gb'], info['use_max_space']): self.refresh_all_info() def _handle_delete_lv(self, lv_name, vg_name): - # 删除 LV 前,也需要确保卸载并从 fstab 移除 - # 获取 LV 的完整路径 lv_path = f"/dev/{vg_name}/{lv_name}" - # 尝试卸载并从 fstab 移除 (静默,因为删除操作是主要目的) self.disk_ops.unmount_partition(lv_path, show_dialog_on_error=False) - self.disk_ops._remove_fstab_entry(lv_path) # 直接调用内部方法,不弹出对话框 + self.disk_ops._remove_fstab_entry(lv_path) if self.lvm_ops.delete_lv(lv_name, vg_name): self.refresh_all_info() @@ -746,6 +679,42 @@ class MainWindow(QMainWindow): if self.lvm_ops.deactivate_lv(lv_name, vg_name): self.refresh_all_info() + def _handle_delete_vg(self, vg_name): + """ + 处理删除卷组 (VG) 的操作。 + """ + logger.info(f"尝试删除卷组 (VG) {vg_name}。") + reply = QMessageBox.question( + self, + "确认删除卷组", + f"您确定要删除卷组 {vg_name} 吗?此操作将永久删除该卷组及其所有逻辑卷和数据!", + QMessageBox.Yes | QMessageBox.No, + QMessageBox.No + ) + if reply == QMessageBox.No: + logger.info(f"用户取消了删除卷组 {vg_name} 的操作。") + return + + lvm_info = self.system_manager.get_lvm_info() + vg_info = next((vg for vg in lvm_info.get('vgs', []) if vg.get('vg_name') == vg_name), None) + + lv_count_raw = vg_info.get('lv_count', 0) if vg_info else 0 + try: + lv_count = int(float(lv_count_raw)) + except (ValueError, TypeError): + lv_count = 0 + + if lv_count > 0: + QMessageBox.critical(self, "删除失败", f"卷组 {vg_name} 中仍包含逻辑卷。请先删除所有逻辑卷。") + logger.error(f"尝试删除包含逻辑卷的卷组 {vg_name}。操作被阻止。") + return + + success = self.lvm_ops.delete_vg(vg_name) + if success: + self.refresh_all_info() + else: + QMessageBox.critical(self, "删除卷组失败", f"删除卷组 {vg_name} 失败,请检查日志。") + if __name__ == "__main__": app = QApplication(sys.argv)