PNG  IHDRX cHRMz&u0`:pQ<bKGD pHYsodtIME MeqIDATxw]Wug^Qd˶ 6`!N:!@xI~)%7%@Bh&`lnjVF29gΨ4E$|>cɚ{gk= %,a KX%,a KX%,a KX%,a KX%,a KX%,a KX%, b` ǟzeאfp]<!SJmɤY޲ڿ,%c ~ع9VH.!Ͳz&QynֺTkRR.BLHi٪:l;@(!MԴ=žI,:o&N'Kù\vRmJ雵֫AWic H@" !: Cé||]k-Ha oݜ:y F())u]aG7*JV@J415p=sZH!=!DRʯvɱh~V\}v/GKY$n]"X"}t@ xS76^[bw4dsce)2dU0 CkMa-U5tvLƀ~mlMwfGE/-]7XAƟ`׮g ewxwC4\[~7@O-Q( a*XGƒ{ ՟}$_y3tĐƤatgvێi|K=uVyrŲlLӪuܿzwk$m87k( `múcE)"@rK( z4$D; 2kW=Xb$V[Ru819קR~qloѱDyįݎ*mxw]y5e4K@ЃI0A D@"BDk_)N\8͜9dz"fK0zɿvM /.:2O{ Nb=M=7>??Zuo32 DLD@D| &+֎C #B8ַ`bOb $D#ͮҪtx]%`ES`Ru[=¾!@Od37LJ0!OIR4m]GZRJu$‡c=%~s@6SKy?CeIh:[vR@Lh | (BhAMy=݃  G"'wzn޺~8ԽSh ~T*A:xR[ܹ?X[uKL_=fDȊ؂p0}7=D$Ekq!/t.*2ʼnDbŞ}DijYaȲ(""6HA;:LzxQ‘(SQQ}*PL*fc\s `/d'QXW, e`#kPGZuŞuO{{wm[&NBTiiI0bukcA9<4@SӊH*؎4U/'2U5.(9JuDfrޱtycU%j(:RUbArLֺN)udA':uGQN"-"Is.*+k@ `Ojs@yU/ H:l;@yyTn}_yw!VkRJ4P)~y#)r,D =ě"Q]ci'%HI4ZL0"MJy 8A{ aN<8D"1#IJi >XjX֔#@>-{vN!8tRݻ^)N_╗FJEk]CT՟ YP:_|H1@ CBk]yKYp|og?*dGvzنzӴzjֺNkC~AbZƷ`.H)=!QͷVTT(| u78y֮}|[8-Vjp%2JPk[}ԉaH8Wpqhwr:vWª<}l77_~{s۴V+RCģ%WRZ\AqHifɤL36: #F:p]Bq/z{0CU6ݳEv_^k7'>sq*+kH%a`0ԣisqにtү04gVgW΂iJiS'3w.w}l6MC2uԯ|>JF5`fV5m`Y**Db1FKNttu]4ccsQNnex/87+}xaUW9y>ͯ骵G{䩓Գ3+vU}~jJ.NFRD7<aJDB1#ҳgSb,+CS?/ VG J?|?,2#M9}B)MiE+G`-wo߫V`fio(}S^4e~V4bHOYb"b#E)dda:'?}׮4繏`{7Z"uny-?ǹ;0MKx{:_pÚmFמ:F " .LFQLG)Q8qN q¯¯3wOvxDb\. BKD9_NN &L:4D{mm o^tֽ:q!ƥ}K+<"m78N< ywsard5+вz~mnG)=}lYݧNj'QJS{S :UYS-952?&O-:W}(!6Mk4+>A>j+i|<<|;ر^߉=HE|V#F)Emm#}/"y GII웻Jі94+v뾧xu~5C95~ūH>c@덉pʃ1/4-A2G%7>m;–Y,cyyaln" ?ƻ!ʪ<{~h~i y.zZB̃/,雋SiC/JFMmBH&&FAbϓO^tubbb_hZ{_QZ-sύodFgO(6]TJA˯#`۶ɟ( %$&+V'~hiYy>922 Wp74Zkq+Ovn錄c>8~GqܲcWꂎz@"1A.}T)uiW4="jJ2W7mU/N0gcqܗOO}?9/wìXžΏ0 >֩(V^Rh32!Hj5`;O28؇2#ݕf3 ?sJd8NJ@7O0 b־?lldщ̡&|9C.8RTWwxWy46ah嘦mh٤&l zCy!PY?: CJyв]dm4ǜҐR޻RլhX{FƯanшQI@x' ao(kUUuxW_Ñ줮[w8 FRJ(8˼)_mQ _!RJhm=!cVmm ?sFOnll6Qk}alY}; "baӌ~M0w,Ggw2W:G/k2%R,_=u`WU R.9T"v,<\Ik޽/2110Ӿxc0gyC&Ny޽JҢrV6N ``یeA16"J³+Rj*;BϜkZPJaÍ<Jyw:NP8/D$ 011z֊Ⱳ3ι֘k1V_"h!JPIΣ'ɜ* aEAd:ݺ>y<}Lp&PlRfTb1]o .2EW\ͮ]38؋rTJsǏP@芎sF\> P^+dYJLbJ C-xϐn> ι$nj,;Ǖa FU *择|h ~izť3ᤓ`K'-f tL7JK+vf2)V'-sFuB4i+m+@My=O҈0"|Yxoj,3]:cо3 $#uŘ%Y"y죯LebqtҢVzq¼X)~>4L׶m~[1_k?kxֺQ`\ |ٛY4Ѯr!)N9{56(iNq}O()Em]=F&u?$HypWUeB\k]JɩSع9 Zqg4ZĊo oMcjZBU]B\TUd34ݝ~:7ڶSUsB0Z3srx 7`:5xcx !qZA!;%͚7&P H<WL!džOb5kF)xor^aujƍ7 Ǡ8/p^(L>ὴ-B,{ۇWzֺ^k]3\EE@7>lYBȝR.oHnXO/}sB|.i@ɥDB4tcm,@ӣgdtJ!lH$_vN166L__'Z)y&kH;:,Y7=J 9cG) V\hjiE;gya~%ks_nC~Er er)muuMg2;֫R)Md) ,¶ 2-wr#F7<-BBn~_(o=KO㭇[Xv eN_SMgSҐ BS헃D%g_N:/pe -wkG*9yYSZS.9cREL !k}<4_Xs#FmҶ:7R$i,fi!~' # !6/S6y@kZkZcX)%5V4P]VGYq%H1!;e1MV<!ϐHO021Dp= HMs~~a)ަu7G^];git!Frl]H/L$=AeUvZE4P\.,xi {-~p?2b#amXAHq)MWǾI_r`S Hz&|{ +ʖ_= (YS(_g0a03M`I&'9vl?MM+m~}*xT۲(fY*V4x@29s{DaY"toGNTO+xCAO~4Ϳ;p`Ѫ:>Ҵ7K 3}+0 387x\)a"/E>qpWB=1 ¨"MP(\xp߫́A3+J] n[ʼnӼaTbZUWb={~2ooKױӰp(CS\S筐R*JغV&&"FA}J>G֐p1ٸbk7 ŘH$JoN <8s^yk_[;gy-;߉DV{c B yce% aJhDȶ 2IdйIB/^n0tNtџdcKj4϶v~- CBcgqx9= PJ) dMsjpYB] GD4RDWX +h{y`,3ꊕ$`zj*N^TP4L:Iz9~6s) Ga:?y*J~?OrMwP\](21sZUD ?ܟQ5Q%ggW6QdO+\@ ̪X'GxN @'4=ˋ+*VwN ne_|(/BDfj5(Dq<*tNt1х!MV.C0 32b#?n0pzj#!38}޴o1KovCJ`8ŗ_"]] rDUy޲@ Ȗ-;xџ'^Y`zEd?0„ DAL18IS]VGq\4o !swV7ˣι%4FѮ~}6)OgS[~Q vcYbL!wG3 7띸*E Pql8=jT\꘿I(z<[6OrR8ºC~ډ]=rNl[g|v TMTղb-o}OrP^Q]<98S¤!k)G(Vkwyqyr޽Nv`N/e p/~NAOk \I:G6]4+K;j$R:Mi #*[AȚT,ʰ,;N{HZTGMoּy) ]%dHء9Պ䠬|<45,\=[bƟ8QXeB3- &dҩ^{>/86bXmZ]]yޚN[(WAHL$YAgDKp=5GHjU&99v簪C0vygln*P)9^͞}lMuiH!̍#DoRBn9l@ xA/_v=ȺT{7Yt2N"4!YN`ae >Q<XMydEB`VU}u]嫇.%e^ánE87Mu\t`cP=AD/G)sI"@MP;)]%fH9'FNsj1pVhY&9=0pfuJ&gޤx+k:!r˭wkl03׼Ku C &ѓYt{.O.zҏ z}/tf_wEp2gvX)GN#I ݭ߽v/ .& и(ZF{e"=V!{zW`, ]+LGz"(UJp|j( #V4, 8B 0 9OkRrlɱl94)'VH9=9W|>PS['G(*I1==C<5"Pg+x'K5EMd؞Af8lG ?D FtoB[je?{k3zQ vZ;%Ɠ,]E>KZ+T/ EJxOZ1i #T<@ I}q9/t'zi(EMqw`mYkU6;[t4DPeckeM;H}_g pMww}k6#H㶏+b8雡Sxp)&C $@'b,fPߑt$RbJ'vznuS ~8='72_`{q纶|Q)Xk}cPz9p7O:'|G~8wx(a 0QCko|0ASD>Ip=4Q, d|F8RcU"/KM opKle M3#i0c%<7׿p&pZq[TR"BpqauIp$ 8~Ĩ!8Սx\ւdT>>Z40ks7 z2IQ}ItԀ<-%S⍤};zIb$I 5K}Q͙D8UguWE$Jh )cu4N tZl+[]M4k8֦Zeq֮M7uIqG 1==tLtR,ƜSrHYt&QP윯Lg' I,3@P'}'R˪e/%-Auv·ñ\> vDJzlӾNv5:|K/Jb6KI9)Zh*ZAi`?S {aiVDԲuy5W7pWeQJk֤#5&V<̺@/GH?^τZL|IJNvI:'P=Ϛt"¨=cud S Q.Ki0 !cJy;LJR;G{BJy޺[^8fK6)=yʊ+(k|&xQ2`L?Ȓ2@Mf 0C`6-%pKpm')c$׻K5[J*U[/#hH!6acB JA _|uMvDyk y)6OPYjœ50VT K}cǻP[ $:]4MEA.y)|B)cf-A?(e|lɉ#P9V)[9t.EiQPDѠ3ϴ;E:+Օ t ȥ~|_N2,ZJLt4! %ա]u {+=p.GhNcŞQI?Nd'yeh n7zi1DB)1S | S#ًZs2|Ɛy$F SxeX{7Vl.Src3E℃Q>b6G ўYCmtկ~=K0f(=LrAS GN'ɹ9<\!a`)֕y[uՍ[09` 9 +57ts6}b4{oqd+J5fa/,97J#6yν99mRWxJyѡyu_TJc`~W>l^q#Ts#2"nD1%fS)FU w{ܯ R{ ˎ󅃏џDsZSQS;LV;7 Od1&1n$ N /.q3~eNɪ]E#oM~}v֯FڦwyZ=<<>Xo稯lfMFV6p02|*=tV!c~]fa5Y^Q_WN|Vs 0ҘދU97OI'N2'8N֭fgg-}V%y]U4 峧p*91#9U kCac_AFңĪy뚇Y_AiuYyTTYЗ-(!JFLt›17uTozc. S;7A&&<ԋ5y;Ro+:' *eYJkWR[@F %SHWP 72k4 qLd'J "zB6{AC0ƁA6U.'F3:Ȅ(9ΜL;D]m8ڥ9}dU "v!;*13Rg^fJyShyy5auA?ɩGHRjo^]׽S)Fm\toy 4WQS@mE#%5ʈfFYDX ~D5Ϡ9tE9So_aU4?Ѽm%&c{n>.KW1Tlb}:j uGi(JgcYj0qn+>) %\!4{LaJso d||u//P_y7iRJ߬nHOy) l+@$($VFIQ9%EeKʈU. ia&FY̒mZ=)+qqoQn >L!qCiDB;Y<%} OgBxB!ØuG)WG9y(Ą{_yesuZmZZey'Wg#C~1Cev@0D $a@˲(.._GimA:uyw֬%;@!JkQVM_Ow:P.s\)ot- ˹"`B,e CRtaEUP<0'}r3[>?G8xU~Nqu;Wm8\RIkբ^5@k+5(By'L&'gBJ3ݶ!/㮻w҅ yqPWUg<e"Qy*167΃sJ\oz]T*UQ<\FԎ`HaNmڜ6DysCask8wP8y9``GJ9lF\G g's Nn͵MLN֪u$| /|7=]O)6s !ĴAKh]q_ap $HH'\1jB^s\|- W1:=6lJBqjY^LsPk""`]w)󭃈,(HC ?䔨Y$Sʣ{4Z+0NvQkhol6C.婧/u]FwiVjZka&%6\F*Ny#8O,22+|Db~d ~Çwc N:FuuCe&oZ(l;@ee-+Wn`44AMK➝2BRՈt7g*1gph9N) *"TF*R(#'88pm=}X]u[i7bEc|\~EMn}P瘊J)K.0i1M6=7'_\kaZ(Th{K*GJyytw"IO-PWJk)..axӝ47"89Cc7ĐBiZx 7m!fy|ϿF9CbȩV 9V-՛^pV̌ɄS#Bv4-@]Vxt-Z, &ֺ*diؠ2^VXbs֔Ìl.jQ]Y[47gj=幽ex)A0ip׳ W2[ᎇhuE^~q흙L} #-b۸oFJ_QP3r6jr+"nfzRJTUqoaۍ /$d8Mx'ݓ= OՃ| )$2mcM*cЙj}f };n YG w0Ia!1Q.oYfr]DyISaP}"dIӗթO67jqR ҊƐƈaɤGG|h;t]䗖oSv|iZqX)oalv;۩meEJ\!8=$4QU4Xo&VEĊ YS^E#d,yX_> ۘ-e\ "Wa6uLĜZi`aD9.% w~mB(02G[6y.773a7 /=o7D)$Z 66 $bY^\CuP. (x'"J60׿Y:Oi;F{w佩b+\Yi`TDWa~|VH)8q/=9!g߆2Y)?ND)%?Ǐ`k/sn:;O299yB=a[Ng 3˲N}vLNy;*?x?~L&=xyӴ~}q{qE*IQ^^ͧvü{Huu=R|>JyUlZV, B~/YF!Y\u_ݼF{_C)LD]m {H 0ihhadd nUkf3oٺCvE\)QJi+֥@tDJkB$1!Đr0XQ|q?d2) Ӣ_}qv-< FŊ߫%roppVBwü~JidY4:}L6M7f٬F "?71<2#?Jyy4뷢<_a7_=Q E=S1И/9{+93֮E{ǂw{))?maÆm(uLE#lïZ  ~d];+]h j?!|$F}*"4(v'8s<ŏUkm7^7no1w2ؗ}TrͿEk>p'8OB7d7R(A 9.*Mi^ͳ; eeUwS+C)uO@ =Sy]` }l8^ZzRXj[^iUɺ$tj))<sbDJfg=Pk_{xaKo1:-uyG0M ԃ\0Lvuy'ȱc2Ji AdyVgVh!{]/&}}ċJ#%d !+87<;qN޼Nفl|1N:8ya  8}k¾+-$4FiZYÔXk*I&'@iI99)HSh4+2G:tGhS^繿 Kتm0 вDk}֚+QT4;sC}rՅE,8CX-e~>G&'9xpW,%Fh,Ry56Y–hW-(v_,? ; qrBk4-V7HQ;ˇ^Gv1JVV%,ik;D_W!))+BoS4QsTM;gt+ndS-~:11Sgv!0qRVh!"Ȋ(̦Yl.]PQWgٳE'`%W1{ndΗBk|Ž7ʒR~,lnoa&:ü$ 3<a[CBݮwt"o\ePJ=Hz"_c^Z.#ˆ*x z̝grY]tdkP*:97YľXyBkD4N.C_[;F9`8& !AMO c `@BA& Ost\-\NX+Xp < !bj3C&QL+*&kAQ=04}cC!9~820G'PC9xa!w&bo_1 Sw"ܱ V )Yl3+ס2KoXOx]"`^WOy :3GO0g;%Yv㐫(R/r (s } u B &FeYZh0y> =2<Ϟc/ -u= c&׭,.0"g"7 6T!vl#sc>{u/Oh Bᾈ)۴74]x7 gMӒ"d]U)}" v4co[ ɡs 5Gg=XR14?5A}D "b{0$L .\4y{_fe:kVS\\O]c^W52LSBDM! C3Dhr̦RtArx4&agaN3Cf<Ԉp4~ B'"1@.b_/xQ} _߃҉/gٓ2Qkqp0շpZ2fԫYz< 4L.Cyυι1t@鎫Fe sYfsF}^ V}N<_`p)alٶ "(XEAVZ<)2},:Ir*#m_YӼ R%a||EƼIJ,,+f"96r/}0jE/)s)cjW#w'Sʯ5<66lj$a~3Kʛy 2:cZ:Yh))+a߭K::N,Q F'qB]={.]h85C9cr=}*rk?vwV렵ٸW Rs%}rNAkDv|uFLBkWY YkX מ|)1!$#3%y?pF<@<Rr0}: }\J [5FRxY<9"SQdE(Q*Qʻ)q1E0B_O24[U'],lOb ]~WjHޏTQ5Syu wq)xnw8~)c 쫬gٲߠ H% k5dƝk> kEj,0% b"vi2Wس_CuK)K{n|>t{P1򨾜j>'kEkƗBg*H%'_aY6Bn!TL&ɌOb{c`'d^{t\i^[uɐ[}q0lM˕G:‚4kb祔c^:?bpg… +37stH:0}en6x˟%/<]BL&* 5&fK9Mq)/iyqtA%kUe[ڛKN]Ě^,"`/ s[EQQm?|XJ߅92m]G.E΃ח U*Cn.j_)Tѧj̿30ڇ!A0=͜ar I3$C^-9#|pk!)?7.x9 @OO;WƝZBFU keZ75F6Tc6"ZȚs2y/1 ʵ:u4xa`C>6Rb/Yм)^=+~uRd`/|_8xbB0?Ft||Z\##|K 0>>zxv8۴吅q 8ĥ)"6>~\8:qM}#͚'ĉ#p\׶ l#bA?)|g g9|8jP(cr,BwV (WliVxxᡁ@0Okn;ɥh$_ckCgriv}>=wGzβ KkBɛ[˪ !J)h&k2%07δt}!d<9;I&0wV/ v 0<H}L&8ob%Hi|޶o&h1L|u֦y~󛱢8fٲUsւ)0oiFx2}X[zVYr_;N(w]_4B@OanC?gĦx>мgx>ΛToZoOMp>40>V Oy V9iq!4 LN,ˢu{jsz]|"R޻&'ƚ{53ўFu(<٪9:΋]B;)B>1::8;~)Yt|0(pw2N%&X,URBK)3\zz&}ax4;ǟ(tLNg{N|Ǽ\G#C9g$^\}p?556]/RP.90 k,U8/u776s ʪ_01چ|\N 0VV*3H鴃J7iI!wG_^ypl}r*jɤSR 5QN@ iZ#1ٰy;_\3\BQQ x:WJv츟ٯ$"@6 S#qe딇(/P( Dy~TOϻ<4:-+F`0||;Xl-"uw$Цi󼕝mKʩorz"mϺ$F:~E'ҐvD\y?Rr8_He@ e~O,T.(ފR*cY^m|cVR[8 JҡSm!ΆԨb)RHG{?MpqrmN>߶Y)\p,d#xۆWY*,l6]v0h15M˙MS8+EdI='LBJIH7_9{Caз*Lq,dt >+~ّeʏ?xԕ4bBAŚjﵫ!'\Ը$WNvKO}ӽmSşذqsOy?\[,d@'73'j%kOe`1.g2"e =YIzS2|zŐƄa\U,dP;jhhhaxǶ?КZ՚.q SE+XrbOu%\GتX(H,N^~]JyEZQKceTQ]VGYqnah;y$cQahT&QPZ*iZ8UQQM.qo/T\7X"u?Mttl2Xq(IoW{R^ ux*SYJ! 4S.Jy~ BROS[V|žKNɛP(L6V^|cR7i7nZW1Fd@ Ara{詑|(T*dN]Ko?s=@ |_EvF]׍kR)eBJc" MUUbY6`~V޴dJKß&~'d3i WWWWWW
Current Directory: /opt/saltstack/salt/lib/python3.10/site-packages/salt/utils
Viewing File: /opt/saltstack/salt/lib/python3.10/site-packages/salt/utils/find.py
""" Approximate the Unix find(1) command and return a list of paths that meet the specified criteria. The options include match criteria: name = file-glob # case sensitive iname = file-glob # case insensitive regex = file-regex # case sensitive iregex = file-regex # case insensitive type = file-types # match any listed type user = users # match any listed user group = groups # match any listed group size = [+-]number[size-unit] # default unit = byte mtime = interval # modified since date grep = regex # search file contents and/or actions: delete [= file-types] # default type = 'f' exec = command [arg ...] # where {} is replaced by pathname print [= print-opts] and/or depth criteria: maxdepth = maximum depth to transverse in path mindepth = minimum depth to transverse before checking files or directories The default action is 'print=path'. file-glob: * = match zero or more chars ? = match any char [abc] = match a, b, or c [!abc] or [^abc] = match anything except a, b, and c [x-y] = match chars x through y [!x-y] or [^x-y] = match anything except chars x through y {a,b,c} = match a or b or c file-regex: a Python re (regular expression) pattern file-types: a string of one or more of the following: a: all file types b: block device c: character device d: directory p: FIFO (named pipe) f: plain file l: symlink s: socket users: a space and/or comma separated list of user names and/or uids groups: a space and/or comma separated list of group names and/or gids size-unit: b: bytes k: kilobytes m: megabytes g: gigabytes t: terabytes interval: [<num>w] [<num>[d]] [<num>h] [<num>m] [<num>s] where: w: week d: day h: hour m: minute s: second print-opts: a comma and/or space separated list of one or more of the following: group: group name md5: MD5 digest of file contents mode: file permissions (as as integer) mtime: last modification time (as time_t) name: file basename path: file absolute path size: file size in bytes type: file type user: user name """ import logging import os import re import shutil import stat import sys import time from subprocess import PIPE, Popen import salt.defaults.exitcodes import salt.utils.args import salt.utils.hashutils import salt.utils.path import salt.utils.stringutils from salt.utils.filebuffer import BufferedReader try: import grp import pwd # TODO: grp and pwd are both used in the code, we better make sure that # that code never gets run if importing them does not succeed except ImportError: pass # Set up logger log = logging.getLogger(__name__) _REQUIRES_PATH = 1 _REQUIRES_STAT = 2 _REQUIRES_CONTENTS = 4 _FILE_TYPES = { "b": stat.S_IFBLK, "c": stat.S_IFCHR, "d": stat.S_IFDIR, "f": stat.S_IFREG, "l": stat.S_IFLNK, "p": stat.S_IFIFO, "s": stat.S_IFSOCK, stat.S_IFBLK: "b", stat.S_IFCHR: "c", stat.S_IFDIR: "d", stat.S_IFREG: "f", stat.S_IFLNK: "l", stat.S_IFIFO: "p", stat.S_IFSOCK: "s", } _INTERVAL_REGEX = re.compile( r""" ^\s* (?P<modifier>[+-]?) (?: (?P<week> \d+ (?:\.\d*)? ) \s* [wW] )? \s* (?: (?P<day> \d+ (?:\.\d*)? ) \s* [dD] )? \s* (?: (?P<hour> \d+ (?:\.\d*)? ) \s* [hH] )? \s* (?: (?P<minute> \d+ (?:\.\d*)? ) \s* [mM] )? \s* (?: (?P<second> \d+ (?:\.\d*)? ) \s* [sS] )? \s* $ """, flags=re.VERBOSE, ) _PATH_DEPTH_IGNORED = (os.path.sep, os.path.curdir, os.path.pardir) def _parse_interval(value): """ Convert an interval string like 1w3d6h into the number of seconds, time resolution (1 unit of the smallest specified time unit) and the modifier( '+', '-', or ''). w = week d = day h = hour m = minute s = second """ match = _INTERVAL_REGEX.match(str(value)) if match is None: raise ValueError(f"invalid time interval: '{value}'") result = 0 resolution = None for name, multiplier in [ ("second", 1), ("minute", 60), ("hour", 60 * 60), ("day", 60 * 60 * 24), ("week", 60 * 60 * 24 * 7), ]: if match.group(name) is not None: result += float(match.group(name)) * multiplier if resolution is None: resolution = multiplier return result, resolution, match.group("modifier") def _parse_size(value): scalar = value.strip() if scalar.startswith(("-", "+")): style = scalar[0] scalar = scalar[1:] else: style = "=" if scalar: multiplier = { "b": 2**0, "k": 2**10, "m": 2**20, "g": 2**30, "t": 2**40, }.get(scalar[-1].lower()) if multiplier: scalar = scalar[:-1].strip() else: multiplier = 1 else: multiplier = 1 try: num = int(scalar) * multiplier except ValueError: try: num = int(float(scalar) * multiplier) except ValueError: raise ValueError(f'invalid size: "{value}"') if style == "-": min_size = 0 max_size = num elif style == "+": min_size = num max_size = sys.maxsize else: min_size = num max_size = num + multiplier - 1 return min_size, max_size class Option: """ Abstract base class for all find options. """ def requires(self): return _REQUIRES_PATH class NameOption(Option): """ Match files with a case-sensitive glob filename pattern. Note: this is the 'basename' portion of a pathname. The option name is 'name', e.g. {'name' : '*.txt'}. """ def __init__(self, key, value): self.regex = re.compile( value.replace(".", "\\.").replace("?", ".?").replace("*", ".*") + "$" ) def match(self, dirname, filename, fstat): return self.regex.match(filename) class InameOption(Option): """ Match files with a case-insensitive glob filename pattern. Note: this is the 'basename' portion of a pathname. The option name is 'iname', e.g. {'iname' : '*.TXT'}. """ def __init__(self, key, value): self.regex = re.compile( value.replace(".", "\\.").replace("?", ".?").replace("*", ".*") + "$", re.IGNORECASE, ) def match(self, dirname, filename, fstat): return self.regex.match(filename) class RegexOption(Option): """ Match files with a case-sensitive regular expression. Note: this is the 'basename' portion of a pathname. The option name is 'regex', e.g. {'regex' : '.*\\.txt'}. """ def __init__(self, key, value): try: self.regex = re.compile(value) except re.error: raise ValueError(f'invalid regular expression: "{value}"') def match(self, dirname, filename, fstat): return self.regex.match(filename) class IregexOption(Option): """ Match files with a case-insensitive regular expression. Note: this is the 'basename' portion of a pathname. The option name is 'iregex', e.g. {'iregex' : '.*\\.txt'}. """ def __init__(self, key, value): try: self.regex = re.compile(value, re.IGNORECASE) except re.error: raise ValueError(f'invalid regular expression: "{value}"') def match(self, dirname, filename, fstat): return self.regex.match(filename) class TypeOption(Option): """ Match files by their file type(s). The file type(s) are specified as an optionally comma and/or space separated list of letters. b = block device c = character device d = directory f = regular (plain) file l = symbolic link p = FIFO (named pipe) s = socket The option name is 'type', e.g. {'type' : 'd'} or {'type' : 'bc'}. """ def __init__(self, key, value): # remove whitespace and commas value = "".join(value.strip().replace(",", "").split()) self.ftypes = set() for ftype in value: try: self.ftypes.add(_FILE_TYPES[ftype]) except KeyError: raise ValueError(f'invalid file type "{ftype}"') def requires(self): return _REQUIRES_STAT def match(self, dirname, filename, fstat): return stat.S_IFMT(fstat[stat.ST_MODE]) in self.ftypes class OwnerOption(Option): """ Match files by their owner name(s) and/or uid(s), e.g. 'root'. The names are a space and/or comma separated list of names and/or integers. A match occurs when the file's uid matches any user specified. The option name is 'owner', e.g. {'owner' : 'root'}. """ def __init__(self, key, value): self.uids = set() for name in value.replace(",", " ").split(): if name.isdigit(): self.uids.add(int(name)) else: try: self.uids.add(pwd.getpwnam(value).pw_uid) except KeyError: raise ValueError(f'no such user "{name}"') def requires(self): return _REQUIRES_STAT def match(self, dirname, filename, fstat): return fstat[stat.ST_UID] in self.uids class GroupOption(Option): """ Match files by their group name(s) and/or uid(s), e.g. 'admin'. The names are a space and/or comma separated list of names and/or integers. A match occurs when the file's gid matches any group specified. The option name is 'group', e.g. {'group' : 'admin'}. """ def __init__(self, key, value): self.gids = set() for name in value.replace(",", " ").split(): if name.isdigit(): self.gids.add(int(name)) else: try: self.gids.add(grp.getgrnam(name).gr_gid) except KeyError: raise ValueError(f'no such group "{name}"') def requires(self): return _REQUIRES_STAT def match(self, dirname, filename, fstat): return fstat[stat.ST_GID] in self.gids class SizeOption(Option): """ Match files by their size. Prefix the size with '-' to find files the specified size and smaller. Prefix the size with '+' to find files the specified size and larger. Without the +/- prefix, match the exact file size. The size can be suffixed with (case-insensitive) suffixes: b = bytes k = kilobytes m = megabytes g = gigabytes t = terabytes The option name is 'size', e.g. {'size' : '+1G'}. """ def __init__(self, key, value): self.min_size, self.max_size = _parse_size(value) def requires(self): return _REQUIRES_STAT def match(self, dirname, filename, fstat): return self.min_size <= fstat[stat.ST_SIZE] <= self.max_size class MtimeOption(Option): """ Match files modified since the specified time. The option name is 'mtime', e.g. {'mtime' : '3d'}. The value format is [<num>w] [<num>[d]] [<num>h] [<num>m] [<num>s] where num is an integer or float and the case-insensitive suffixes are: w = week d = day h = hour m = minute s = second Whitespace is ignored in the value. """ def __init__(self, key, value): secs, resolution, modifier = _parse_interval(value) self.mtime = time.time() - int(secs / resolution) * resolution self.modifier = modifier def requires(self): return _REQUIRES_STAT def match(self, dirname, filename, fstat): if self.modifier == "-": return fstat[stat.ST_MTIME] >= self.mtime else: return fstat[stat.ST_MTIME] <= self.mtime class GrepOption(Option): """Match files when a pattern occurs within the file. The option name is 'grep', e.g. {'grep' : '(foo)|(bar}'}. """ def __init__(self, key, value): try: self.regex = re.compile(value) except re.error: raise ValueError(f'invalid regular expression: "{value}"') def requires(self): return _REQUIRES_CONTENTS | _REQUIRES_STAT def match(self, dirname, filename, fstat): if not stat.S_ISREG(fstat[stat.ST_MODE]): return None dfilename = os.path.join(dirname, filename) with BufferedReader(dfilename, mode="rb") as bread: for chunk in bread: if self.regex.search(chunk): return dfilename return None class PrintOption(Option): """ Return information about a matched file. Print options are specified as a comma and/or space separated list of one or more of the following: group = group name md5 = MD5 digest of file contents mode = file mode (as integer) mtime = last modification time (as time_t) name = file basename path = file absolute path size = file size in bytes type = file type user = user name """ def __init__(self, key, value): self.need_stat = False self.print_title = False self.fmt = [] for arg in value.replace(",", " ").split(): self.fmt.append(arg) if arg not in ["name", "path"]: self.need_stat = True if not self.fmt: self.fmt.append("path") def requires(self): return _REQUIRES_STAT if self.need_stat else _REQUIRES_PATH def execute(self, fullpath, fstat, test=False): result = [] for arg in self.fmt: if arg == "path": result.append(fullpath) elif arg == "name": result.append(os.path.basename(fullpath)) elif arg == "size": result.append(fstat[stat.ST_SIZE]) elif arg == "type": result.append(_FILE_TYPES.get(stat.S_IFMT(fstat[stat.ST_MODE]), "?")) elif arg == "mode": # PY3 compatibility: Use radix value 8 on int type-cast explicitly result.append(int(oct(fstat[stat.ST_MODE])[-3:], 8)) elif arg == "mtime": result.append(fstat[stat.ST_MTIME]) elif arg == "user": uid = fstat[stat.ST_UID] try: result.append(pwd.getpwuid(uid).pw_name) except KeyError: result.append(uid) elif arg == "group": gid = fstat[stat.ST_GID] try: result.append(grp.getgrgid(gid).gr_name) except KeyError: result.append(gid) elif arg == "md5": if stat.S_ISREG(fstat[stat.ST_MODE]): md5digest = salt.utils.hashutils.get_hash(fullpath, "md5") result.append(md5digest) else: result.append("") if len(result) == 1: return result[0] else: return result class DeleteOption(TypeOption): """ Deletes matched file. Delete options are one or more of the following: a: all file types b: block device c: character device d: directory p: FIFO (named pipe) f: plain file l: symlink s: socket """ def __init__(self, key, value): if "a" in value: value = "bcdpfls" super().__init__(key, value) def execute(self, fullpath, fstat, test=False): if test: return fullpath try: if os.path.isfile(fullpath) or os.path.islink(fullpath): os.remove(fullpath) elif os.path.isdir(fullpath): shutil.rmtree(fullpath) except OSError as exc: return None return fullpath class ExecOption(Option): """ Execute the given command, {} replaced by filename. Quote the {} if commands might include whitespace. """ def __init__(self, key, value): self.command = value def execute(self, fullpath, fstat, test=False): try: command = self.command.replace("{}", fullpath) print(salt.utils.args.shlex_split(command)) p = Popen(salt.utils.args.shlex_split(command), stdout=PIPE, stderr=PIPE) (out, err) = p.communicate() if err: log.error( "Error running command: %s\n\n%s", command, salt.utils.stringutils.to_str(err), ) return f"{command}:\n{salt.utils.stringutils.to_str(out)}\n" except Exception as e: # pylint: disable=broad-except log.error('Exception while executing command "%s":\n\n%s', command, e) return f"{fullpath}: Failed" class Finder: def __init__(self, options): self.actions = [] self.maxdepth = None self.mindepth = 0 self.test = False criteria = { _REQUIRES_PATH: list(), _REQUIRES_STAT: list(), _REQUIRES_CONTENTS: list(), } if "mindepth" in options: self.mindepth = options["mindepth"] del options["mindepth"] if "maxdepth" in options: self.maxdepth = options["maxdepth"] del options["maxdepth"] if "test" in options: self.test = options["test"] del options["test"] for key, value in options.items(): if key.startswith("_"): # this is a passthrough object, continue continue if not value: raise ValueError(f'missing value for "{key}" option') try: obj = globals()[key.title() + "Option"](key, value) except KeyError: raise ValueError(f'invalid option "{key}"') if hasattr(obj, "match"): requires = obj.requires() if requires & _REQUIRES_CONTENTS: criteria[_REQUIRES_CONTENTS].append(obj) elif requires & _REQUIRES_STAT: criteria[_REQUIRES_STAT].append(obj) else: criteria[_REQUIRES_PATH].append(obj) if hasattr(obj, "execute"): self.actions.append(obj) if not self.actions: self.actions.append(PrintOption("print", "")) # order criteria so that least expensive checks are done first self.criteria = ( criteria[_REQUIRES_PATH] + criteria[_REQUIRES_STAT] + criteria[_REQUIRES_CONTENTS] ) def find(self, path): """ Generate filenames in path that satisfy criteria specified in the constructor. This method is a generator and should be repeatedly called until there are no more results. """ if self.mindepth < 1: dirpath, name = os.path.split(path) match, fstat = self._check_criteria(dirpath, name, path) if match: yield from self._perform_actions(path, fstat=fstat) for dirpath, dirs, files in salt.utils.path.os_walk(path): relpath = os.path.relpath(dirpath, path) depth = path_depth(relpath) + 1 if depth >= self.mindepth and ( self.maxdepth is None or self.maxdepth >= depth ): for name in dirs + files: fullpath = os.path.join(dirpath, name) match, fstat = self._check_criteria(dirpath, name, fullpath) if match: yield from self._perform_actions(fullpath, fstat=fstat) if self.maxdepth is not None and depth > self.maxdepth: dirs[:] = [] def _check_criteria(self, dirpath, name, fullpath, fstat=None): match = True for criterion in self.criteria: if fstat is None and criterion.requires() & _REQUIRES_STAT: try: fstat = os.stat(fullpath) except OSError: fstat = os.lstat(fullpath) if not criterion.match(dirpath, name, fstat): match = False break return match, fstat def _perform_actions(self, fullpath, fstat=None): for action in self.actions: if fstat is None and action.requires() & _REQUIRES_STAT: try: fstat = os.stat(fullpath) except OSError: fstat = os.lstat(fullpath) result = action.execute(fullpath, fstat, test=self.test) if result is not None: yield result def path_depth(path): depth = 0 head = path while True: head, tail = os.path.split(head) if not tail and (not head or head in _PATH_DEPTH_IGNORED): break if tail and tail not in _PATH_DEPTH_IGNORED: depth += 1 return depth def find(path, options): """ WRITEME """ finder = Finder(options) for path in finder.find(path): yield path def _main(): if len(sys.argv) < 2: sys.stderr.write(f"usage: {sys.argv[0]} path [options]\n") sys.exit(salt.defaults.exitcodes.EX_USAGE) path = sys.argv[1] criteria = {} for arg in sys.argv[2:]: key, value = arg.split("=") criteria[key] = value try: finder = Finder(criteria) except ValueError as ex: sys.stderr.write(f"error: {ex}\n") sys.exit(salt.defaults.exitcodes.EX_GENERIC) for result in finder.find(path): print(result) if __name__ == "__main__": _main()