JFIF     "" $(4,$&1'-=-157:::#+?D?8C49:7 7%%77777777777777777777777777777777777777777777777777"H !1AQ"2aqB#R3b$Cr4Ss%Tt&c$!1AQ"a#2B ? }XuAo)8^ IƟ`vUp9jY0Ǧ w)E허2jU`SEKw5]kSno!]:?jc\غV7/9N+{t#8zd/޲3F/=ź3GNquV"/4:{z%ۣI'D@ %88^f}VV)S_2ed^Mx"͟?UC62Q%чmO͓ cq0rŖJ\Õ_Sݶ'|G.q޾D U]nP%EF>˲E"d&'f2s6H]4w IS˶4VbaQ+9]XtNx:M0JNxϙ⟟"{nr;|{%vo\z-wc,*|k}-m55o4W9ؓw߱Yzk .=/oϡȴ^9ҧʹamtQԬZ]4?egjrQ}+)MleE]MPEn!`IK2RUEwVIoͷcp;lśe7΄uN ;rПV8|e\׹9Y-V_G.)XԢOv<;_"ڜ]ߙEr݊'K{KuBJ}KI}24|"v)/ʻo5)6-Tjd7.C]Q&lU,Yk1P4~UKZs|$kX6+屷CUq+N(jlGrpG&UB3#k3\9qfg7O8Kim(AJOO~C#e`i0wĦij$cWh<dtQߺ"NOtG+ZǪ]b5%]v5$)u|qZ柡s-rۖu$MKڎCmN_V'/1u,21pvlc>қeNnֺ|bkl=lǷNOʣlz*]»vȎ[)j[fs[]:s#m6Qt6*Q+`};ßj[F_jcv`r#w}|k<ڞ/r53N8>Kh q_-_??@enſEܥ\D\YAEo+ ޟd}IcY7+t{=ɩ>}i\\JfxzVdSzᔢ]Q^CJի\iceitMM5hڦg')^ et#ۯ"ÿfF->4iؤ2ݷ6#p6^-R̫gETj^I.kӽUp~D9[:/>h> \gJ|ۿؘ>ml9jMK =+*2i=0RiͶۗV{"u]IH`9J_˹KƼK$X-|=ve/ bjxw.9i%NqVJcFYKcTtO,F;%67vYb8֝qq0tUt=DvawsS~~Edzr^F-v{c++ݔ\|9Iy #nOavOY=3690Tcrilwa\˓m$?箵S6U c(.~R7suMhqcMOnKoc*ȣȩEd'J ܜk*_q}%M/7c.|;trddbsdcJev85̤iW Ę 8C# .딖e$sk80^\J众2)Nm~|Idj_ O+6ǻ#(MIz4Qo:օY,:q]̌"lK}{F]ζ)h>ʶ ^ue78_G#rqv$wkk[Q c+վ+ĸZΝFB]VzoiJRke&Kgom_7Wef_7,osJɽE%lzBt>mRs)v8'P0ֲtrOg4p_2`GlhYڦDF/ӚKmtm'P2kqU765fJY:y؊.ox%8V_ִ̌ܞjpqwЮQ;iUcNoOoٸcY w*4soӵkqf$?-jy~0{>?DaL8XL/ɞo+'8 {ʸxգj#Dy)wk̘e۩+%}~;ڼ5xek|y-%ڱ-ʜe:EEScÚ5z|r'&I&яF*F7|[nRF =(4ۖ@. n7@xx:N^8Bg%u/ny6&dR{?8U_Q6Z߯-oh.NR]} qi6~H(j7*uF&l&o8ts]/P89:jW*$w׹Ӌ FxpsCJi.7N q4WU_}7*M#qWiصnk'4ݍl*t^ c<'d:~͗enFQRz9v~ddoTZ̚k7X(wUswO̙fոҁՕ[$IAI>WW~ĪEѢNoeutYߑ-Eixιpxq{FnyfRrjqU᫤]>wPU8)Y-7Wbq㛋w:7ܣ].j%K:y4] %9$I%pT(󨪙VqiYٓ4y~5S/XTDZM2lȪ; S~Kx:(Mn0';-{*qV&|W3S+\֔a{R{s=lYmN9Fn&o'}Vi( ?*qV5ѼCNsM饏zߴ$^O69@ ,$y|jE;gW/u|M?3+ZՕN86յw%|QO㏏S\E#ddsgl+Scl3~~CԕQľ?5_ z߿t11OĶ0>oB9E/SOSk+b&Yn>$툧eg) "!܉(1 uBoJ)/t/,:=7M+1ܺ#CmS^Nz 6[u&]+|Dfj:uZ5-Z^TjMtm>cȳ NdT_,M#Ex;pt۴ͮ#!N iKl!zPծ~$1SiO} HI&g Bf)b%Ko̧kumEnص;V?j>nltOMVۆl>.WueYaw2+qK,?uHiqqSM}~gu3xbcWSy/Xc{%sZ]uaUM;7:cb5G97'7þյW,;$ܛyVjl޻y7S;o6gf.Tг[7/i1Z^rE cUF'P1-?%u&q{fw~27ޡ ^w$?SwP[=R3Y73 4x(Kk&rLȫMKn:RjcI?3Al`vض[POĖSYujj6v+-[xҵ=~zNN>\ɲQ/uufo*e6l;31붏.>w6=7#7dFDc%ƶTbd;2/=?Asr! ~ZSS~I"9y]Hn,ĊJ7S}cK"amCg3yP=RQɤW}t;-{F+v+RɔڎB?º{SV묖kۏmK~%.Q;OfEf_Y/F-V-MdD)m.ZՍ8Y*h[g/6ydmCc[rdfʾ䖗gd$^֍^ʅѻL|<[݉\߯RiJUo';œN?B smS ܹkس,mRE^ѣlJ&.ċ԰YO:޼f\Z'HCѯU[ʩ1ff4S-٥YxTIGLiыr }L)edׂ*l|ٚuoxӿnWkTbbVm zT_'"x5Vިxo1ج^Fq6Sd3ws'/ڞ6m?}1OsRGݝ+,~ڬ%^p1ef5c25vq~﹉ă[r-eq] 8+/ESj}?mUE.xYK3"oƔ^Y9I]I ޑ" &*4.Jâ}ټQbXKJ񽼀ncg`+riܭ_'Bֽp%bX'7cB}WPm|zHָLJhj~E>i~Z$297|_hyΕ&s}ZϷ *j]:v.HK<SP8`Pƣ)r ,}8Wk[ArHgn=о7:J]TTP>OOj J_KyB\Ԥrm嬷ȫr{ݙ5R(FRЪ6q}KLmR'eޖz6[YތesYYL5Tr7s\^rؙV͸컬j5d?yk'b S }kra^ߚRH)[sg.fLM\u= vJQ]rVkZuoN}#G?yjO%|i2fKoӰღC P_Ϳ6Zr{e/m$i}9 G2')YG9KY>|1ӫ +v+i;h\Q@˿Lӭn˖ 7ck>Vr.D0)hC<˄4"0[eԬݭe+l2s3ss oX]1r]+VK vI;mZ')R6e5=/i@]H^Z۬՝EW.jƆf{8mXMV~_̝z^VR}T63}}k3+k3:j1Phlpi{欍BȽ}6w73GtUZv>4eUj$ xz$$D/߇ߟI"uk̜aƪ*ke/F:dһ_PE1ݡkp(5ʏ-ɮ{Yllԧg!ܝ g]i-umεŸxOê^=PR ##XeMy%2L~󜺶Hm ݙ2t_ƶz7'\Z4T<"AM-&xaC]a5.huQ۫$cMμ|h;.J.o߸sE-zU{d];|YLSMvSEneNKr1B[]NeonNߪ$4̘FPrkxޱ=0lr7Q%=$KQ;0r*XKdGۃ*]w-npᬶ\tt4>Dc[Ouo3/)-WҴ xs71eԤm*ٖ웗H''.Cnmy]݊Kra[9)Y#2U6d7tf.[R.GdE>#O_.+-K`{KonR_ÕM/)?:F,Xo1ƽRmz8C]lD %(x+d2Ah+\CCLJ!D65x\ȼv)\Nrp*[YُfL*PyVΚuWA K4hyYdwihNIy#ub?4NDϐ'4 :nFe(o%ve@@xl-k%QƭRP&kεMŪ-Ys2u ]T!}8*TQnZ}v =~mԧyDM&8K>2|Bnugܷ.wvCs̼5F^ubES7ݢM&4Ź-~mKx1((sr!M5uy\q)oy|a)ˣ,A?w"T휳2\F}PR-<2%`~4Z5\W"(USkGpT(~Qj>ɰ쏳ǓSKKx's]nEf'.iݙL>Moƹk7ݭ[.г6lk<;?)#E]xFU7'>vF%R;t:Җs}NSBWX=Y8ث}~G)S^^ƽwR[)/Fm-ڞTK~˓Z]U;RQ=M/"NԝP[-Y9t_8V+}P?Ue{M/O&WWKvc#r'KM'p[±vtpRC/W|7K2Rfm;ljm%Z]^T[6}6iTC }L[uxg7(Z}. SRI)jҞzȶ쳢oYRw$ŷ"J\ǭw{u'R taF{;3hHB\RP(*ZQ]y;;k٥nWbGKv-V?NDҞkd9@z LJ}Kc9C*?V-*[*۸-0.|󲝳ߗZK#%_OFGF$kC$[NNJ7Yn[k~Xzc+Sʲuhsw^^4+nElbƮKD,}YLV=i=|p|_=b5mȵ(~,em#Xƥ.sVoEaWXc.lY uG\m';'*\ӆ}|˯UfQBvo}/"zw + qvMrQ[[AdU2ٽCGgjؖS~Ev%9">$_2Sߚ%ѽ7jX(t#21r{̬F]b()?r[Rı)W[O/6]XL9 vuLh-Ȃ9"'7f!Փ䮿Bf}[lag֧]?Pc#D9EmfK7o*})+n!]qIo^FrNVNo!Eƃd#OP?%ۋ(mPu93ۣ{}2&$%cZ߯LҚY);U afԶd,*'6_?B:R~}^̬~mJ+vC}Ѩe"MY+mi :s쥸;iJeYvBddeK|#5/mzR]F2 JHUU )/S{Ic$=: W)>} @0#URsR=w"L{+ɞ)d|*qq2>[nƨDۋ-G[6½J|{Ѿ4MwyG-Σ Ze{ug>2|'zΤ2%xՑ*<Q̥T')uLkjn(zF-JOR}wn~FV5zq2m'^VS=7Y^RdfeO)>EpX붚w*r*w˿^kڴ{J;K۔sRŶU]p\zn@dx6[+yeH[_m_/I&mv|M5&&-G"v۴^{vg8Y(K_~h0e AxfrzڬkhS/Vy1ϯdW3'͹}{'V-:MW(V/ͷ*E7s\EmEW}bUr'k,P{9?B֫ #[uNrB,wo^{fdF(5tRf.2J-/:~ t0M"d_/c^32*q]yLl^2[ݥZc*vtm213r'tSuM-Խ#o/HF+2VEpmǦޟS?Rs+t:u G8n,Ԛf,hY8SX*rKf>+cpruɬ=DMrXgϸ:~ɲ ~]'5'kElw\=ڞAG&')G9R\_̝1K;nPg&T(ի[^Jҟ"qoӸ.W}3mF>'$<\U6-~?x?B~{^xkpv-vlߣe빹j\(ښsuu6lH(qoaYt?x8}Ie '@b%TݲygV.+O9/W4MsCMuFjYzG.{ds.k(>G~K?ni-=R r}r ?s̥%l5Ϛ9IN6~۩RĢWNʾE[|nb.HY—קWkr1ҺշMNDp)^¸R:w;u1 12]T/Uiʹd%2OC2K*r5S]g凫5 UQ.ȫ– /i91njFkQxuJ1rn%XDžy?s˗վuMGƋ/m^J*RsF))uF,'l{=|nFm9:N\%u#tnXE->e2Y0PũjUȨEŭ|'eʹ[o{Ցms%CGg/}t|snzrvm\g}cÊ94Pvg'L}ّg궮ԱߢO^f.W-sT]M˔ېе<^Н'KuNn_Vl8*Kж^ xsuW51-ᅱFzƉT-kY/9wzDޯ/XlW)gypǚjDɨ~{ݤHCim.[>rqE_Uرx/>|L64%aj;fxӱF(K֓J9՞ -K> I_5Enn´&=Oc%o̟IJZF$۲5I9Wݚ n.WTuѲӏ[4U/9.2zX5\j3ĎEsMq4%9.d[7јc9eNa+sjE';%s#ɤ`ףS=WI쫢.Mv:j/[3:rTF_zt:.z%udW%]xܮVz$Vŗ49[^y.խN~M&mx+wGR~_4KC[ʻ:v>03߶v9x-Mȧ$c:lrCWjeg%ֹ_Nh՝Qɏj^ϛr^.>WhlE5yֵ6\W^确]*гc&^NI[oCDn.ߑ!,m&M_/'Mn$s\r^8|uSZZ1|LV<(zq׮xmٚZƏ%.Ԁs^2𱸒O#&,s[mײ9kޖCoSq&俙qxP.N] 2UǎsM2iN.f r[mcQZmFُE{#[TbҔ*sfaSrn^8N<\_'MarJ6 EQғ|F[S'[~q~kmn[_x?B f5Q١X=g(~[Cx}GO ĺo'e)~dq(Ot`sN=~heu ::m'Cjj>~5V柙cyQD%uqEc{[l^U O]b~eŦۑ'W3&' 2V.^D%G S6\wYNO$. O+^ŵG~haEs^=1*bICzFF4O#,Wu3허ekB\I'tWMߩOG3iFz{rgeM9g r] i3gk&u1r/1kVgR-ɿuF .^;3;?3큦bN̂r4ovMkڞ}[:,IVG<};*-2",>K%bK2Ƨ[w!)ˤ;d?4%Ul2ږec4#ōIw^R_/TFX+*FM[F|a'ߚ2SIMeVGn ~&Y Ym(?ԛ],=|сG4yjk"Q^~ԗ^c,qqrg^-:Uc[E8>>k|nS..LBIc>3i|ZEZXAqm nuOm<; X~mrK=~ ƱrSN<U!F΋WS/|t?K)zd} ,C"ovx?bբs3mX3桭X֖˦kFddhg}$ggSo5jL*NdJis$ EQ\v=0HxzyW~FT_Ƶccg,&=_V(%kq+_÷O'[_[Uڽv F $Ξ9n5EN/4Yy/%*} .jΔ`V_6\VͲohzfOgޯzpj}y}v:34WH;+x7ӻu<ݦ"mJ/=>eoD֣c4kXW-[}٬6;t[Na_• _5i5˗sٴ]+e;Joj㼶ۙyLumo5&F)F\ {(sm_M>gzcr)KU̠Ħ=VDd'h;-aŤ9KٰqQܫަazMp4bk9 UX.ͮ]KeS5Uq[¹X0ɦ6]roFjʧ2׏6/C6eQE5KӰmsFnIz&`z팡-ٯ.ixyك?c2//z6M4W[]_"?Õ[? Vfvӳq]I5(d|MʝzcC*mN>B2gD+><e:Gh %UkW%zJ8k_ˠ=KFRfw{sŖ^q\/{v[Ω}gLjT[t_ޕg6G~rkkMcSRKբ54?SAûO1o%[>5/R~CioNdNʛćh>f6H8c/<1xd[ŦCEk.9"ej?w&O6^ژR[vrQ.z㎩f6:V8}hi2z~ s-w]+|I9s_C~>-S&9ZFVLf7-d'pՠplJ#mm؎s(?Ʋ?/A%_sXuGNnR}_dq>1ʍ|У3]NXYZʷ/&ܛ彖LS? 6]"_t5qP5Kq]^m91jW暹U6-5WU澦M0˵f2ӪǮ.P~? _nEJTcTei)ٳrۣ%x %gs}7l9'tb~dXst# r?}Weaq>=+to)7،E*vn\e_,\NFxcivz]tM˼?Oԝ2Zrλs-ĺEtonIIfm/9^[^EBUjOnr6vI& l]%0")2䒶-+R*zyX<> -X9GUo^xYQ8ιvixٔa\t)hv}ьոVU~tK,=_wLLa?TYIo]$`N6cbi?#7;MRt<.~Q-mob\\g5췍 ڌ_?8nfJN/Y͢n3?_sϩ{HiְPo'yS??_jߡWi5q? MWȲ)8a]lLˏ--b[TXlΫRy;o5뜾$HW.mm?շG[Ƀ seo5Q}Le%*،«~uU{R$t\^%!weX:G('6WupTS&~8=jo?2_PϖE[nf6Tٯ;GLW)NM[o*\j%.gb|䭹noOX:1R)UTj74˓]D_bʝkzNI.9|^G`KeQ{mOjX/sR7evdgi7qm}ތW&4=~|YY)?7Oj}xXkF×4c.l?i|b[5Ή5j-[Y\z<茲Z$Ff&o;gErǩݦ̪/q[&[/9uuzi;PS^_/?]=ΕqK~ӛ5'NM[m_Ϲc'[oӯE#g߂vvGNRo϶o5Ǩ[ɉtov2~i<7iSȜN(G5+/ٛMTܣukj鷣/$1˒!Mxr\ߤs1ZuMQȌ^]c$CXrj#N/˦Ķ9]Nzê5zi;W,v!ŧD6zğ7uR5^MW}>igl2U2nXo{}_w]&vte\Z3 MEEe/ 2s㗼S_bIղTI}|[Ye/c]*̪9u/DmyNxSDgi `Z?.RFj۪'~.[KVb޺o濡to?E#[.^y=q4F8ڎ/GX\.YW!Z.ѕtt:?gYYyU%Uw~ri>ȦKhg,5/=>V?TrN4aWO,oӕ7-SRi*"dܽpuaVQÞd-#J2Nr:#``ѧWR-F?I-T -cOT2pr?þזgE\Ij~L9%EMoџUؙt8_eYΧWjU}e9y9z/#TT-2dLt3H=ڼcKb'"uIٓ'[[߱F~\2]r%C]^VCLjm[cJNryf}ջ.[DEoRՒb'>fVy_c6[K4Na5>{ɳaw/Uj.Զ_K~?IeJ7OQx3IgFc*جɊǽ-o3Ӭp / ]7V*ENܜ[r/tOJΉw*ʨ*JFN^.WZeLgUwKi/M9y8dkOᛊHxGĶM*&#h/U|6D(uFyE5hYxiSEVm^D|,ۿCj;<*ouOkYpΔ2{x-L] !k2ا#IM'a7:M}M1Y儭Mnk[/;4Uwkkɫ%aɔoXVV$m;2Z4i9:>\Yů= ?[{t6,~!c`Un+dW.gKyIB]l+3kض(\MZ\}>k\C~閹l[ů]VNtƸr몮X+U>v'nv{y7s[г̭9Ctvt% GqT8=wa(6\Rd柮YWv^Fd^\+緉,+=-^S"k:NVu o[_TIѝ椯bF/G㿏dΙ?T}K-T)W>s?3M)V*,;P\,}B u{rDexڥVFfw}47׋w}]Դ 1dmk1V%/'T:Fǒ_TEe[l/l/ٯc{Ƀ[~`zj⾥r}Vܪ{M8Qv]$mU]8J2MngcxY?鑞.9HjxSy.fS(|]MgcK2$(jRQ3XO|<f:Jq4& fw|$N )A8ת99 mFNM*Dϒ NoIa9i9y?:D⻧߇\7ɧ]mu"-˥5/w̨_ 7DK['[2"(%xzT\*GT"+<,yX.lEJrfo?.4N;l>jmZߣ5FdB3\r,t,./S]Q{tm5lӕT~A [fv7Iہc: ΪN7I]2(|o$NLW"#~Dͭ=v-Mv{-lqn{I3xn'6.=DƟܖަ~deQV;k2Ei\[bӴ1_]OhZl朠&t3xkei+c\'ZԪ'hK梿X@cTԫ#emIz6e^i?8 NBc̆f+MׇdC]YSd%lώ8-c7eι/}_con/no\핍~[WNReXMo+اn ?#Ͷ-AUFN1V4!y,{1a$S﹑;Ǚr"__[o) xk}7EI/riwؙ7mR}`|yrEVdo/B# uٳiNQKQkᑑ^d@/=ˑɒ768fsuor9=7ףܹճpMr-$1uySOZN?đrqզ9F q=.!T?ػ bf{¯q=$^:!ES߿ Fu\OS,8e^UוS^hF4BQƺȪw-kF39@X06 Fv=Q^|ƞ5}2tnmG_|Λ(|%](-5>KȁN$=6lq).12 V6m$ׇlOcҫܸ K{;ľ>+Q?Rx-Keu uMy$i B}G*h$Q -W[-&a"[i\}~Ek$<~c{MffS eS.#\^lMiytު]9S{u4 {DFޅSź}R ]R$y;r/P̙3niXMt;&!rxw\ZFmQ"w\L{^۔K&/gr:m=2%5bwE"^e[\$ɟPi!U_rdS2d?=[!(I.rC QZEim%}|YmzZ_ά<ۡLQM|` ybPȏ}?]Eu[`kҫgFb~F}Q8NP>5lӳ^-K%Q}$sx7SvnfTƸ|Kzd'_ⰽח$4L Y?qy32t j2e ȜrJ{mبhۍUU'p#8y'ѝ=i+Tĩo7WYyČkL5؝M=%"Nt}eXW)N.~sv5pɮ sSQ[+-/}kVk'FEɩ9SE&T=&\緵 --tf.9Ѳ4_##_ɱTFV؞~YTddS&s=䟚Fb1._5}~gM'p#,U hs--XG wtԹTi7M:GYK5'^W?C>_Gq/S&d| k_gO ӊiJeHU G_ Êg#),}-:5>V1emq}t}q?meKU:BqJeiPɗ#\$sI} Z生ƫoo=V=pVcUg"%wEm叡vIdhrȔ~F]p58_.,O|'Ɇ^L!c6OWӷ{x9?Fp?ceOuT+Uɵݹ&gx9i퓃sxGIm}_3Īr#:ԣ?4בc[jö#B7KʌWNo)=+c }YvP{lv^r+5Vxx_:~=̌Q}CTy+Wh鸚f$101뢊F[#--Y\i@l)W8/E>8nlj/ktOľ,q*[sE[]:?ZeQvŔɺ|j(Wx,LW=:S?κq%81c)jJvODLiW,{96vr-2}-EH,}%3k#l5gl~x__W Sڎ 8YJQvA=QIWju6-X9$kWЩCI4UWd'&O/Cf=Pi/#+>n$KYst܅y4ʷD^~%~myj,s_4Q}΍Cή;SW:h=Ff{.B/inȇo=-T͸OY2}hlK}.m7-z?,f-/^b\QWs/_͔/3In[6M;l ygؼ!WUË_)D9YL4_>f}ϵ3hV5Oѣ(l8?L4蹥[-Э=7V{&ʢPʼ*3cMz>u4@[oM gKS[jy"Lھzɵfx)GE`ֿ.=kJ>/iˢ[j-qץQC B@o V(ʯG?Bܻ\I>=K-].(vOE.5׮=/Pf^&$caY9{3މ%YOxZ~6Z;;ԗ.NJzş/YϖĜ%ѿO^tY$ν4|e}2ɶU9A؜h˺LrIm%J.|I]kG|DzU k4'(T\9߱^!z -:mW^ <= <^2*;Seq(6ªsHf5ʸO{Ilr~G uJY^k5X_y;5'59O@ƣ̶>pnCOvNwX4oUUf]Џe%MV9Xm9]x'Q=82z)c/~1\~LSow>ﺍƻUql~Sqo羘sk}VjG71kYؽ]b4qnMӡ; w@̇IL㿗[43)]=v*)EH'a񖳋ҎTkxuXGK& ZIR(M8?:ixJp-dmckpu*%N^-7E3='ceE&';_J'Mw𶥏Y9+d9+>!e_Sn|VX -TZu]Ģ/6\ckr /ޗ/z[y.N:*k$ }Yǭ}GUm^-%dm;K_#ctBsg2:8rz-VE|T w.}w9NEPGnoCe8/&3qT}MJ̙Mۗ~哳,-WI_Bsh+~͛vN{ZdYKݲkr%+lo*re-ه?:vYqFfCsqMXRķ{yqgrx.oǓ\xdڗ_ZC9WomX|KmV_%UJܷr$drȳL~MoKyYLic Jq<1$UuٯTד374s<ĕ96춉r9 pGc9=p^:)ZJb&VӝXٽ 0/X& ۳*_ԙƏ.5J 6<$$6B0d_d?hqd>XCe- wO@pg:.>$.Ϣ~L޲|,{-ɪ2.u/Ds-[ُiVIWK5M#Fܭ3?x.)ۣ,wJ)Ȳڣ-#fbdq&Tͧ8Q,YqQ)/R­?\k˔[p_+ogzP[6r^o}_kT}JiJ;<ivEH8wI@MOPʊ\#+$%PDF-1.7 GIF89;
ANDA PELER
Server IP : 182.253.108.180  /  Your IP : 3.144.90.203
Web Server : Apache
System : Linux sma1wiradesa.sch.id 4.15.0-213-generic #224-Ubuntu SMP Mon Jun 19 13:30:12 UTC 2023 x86_64
User : wijaya ( 1017)
PHP Version : 7.3.33-10+ubuntu18.04.1+deb.sury.org+1
Disable Function : pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,
MySQL : OFF  |  cURL : ON  |  WGET : ON  |  Perl : ON  |  Python : ON  |  Sudo : ON  |  Pkexec : ON
Directory :  /usr/sbin/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Command :


[ HOME ]     

Current File : /usr/sbin/ftpquota
#!/usr/bin/env perl
# -------------------------------------------------------------------------
# Copyright (C) 2000-2013 TJ Saunders <tj@castaglia.org>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335, USA.
#
#  $Id: ftpquota,v 1.6 2013-04-23 23:25:20 castaglia Exp $
# -------------------------------------------------------------------------

use strict;

use Fcntl qw(:flock);
use File::Basename qw(basename);
use Getopt::Long;
use IO::Seekable;

my $program = basename($0);
my $verbose = 0;

# These formats are highly dependent on the quota record definitions in
# mod_quotatab.h. Should those definitions change, these format strings will
# need to be adjusted accordingly.
my $limit_format = "Z81I1C1I1d3L3";
my $tally_format = "Z81I1d3L3";

# These sizes are determined by the sizes of the quota_limit_t and
# quota_tally_t structs (NOT using C's sizeof(), though -- alignment issues)
my $limit_reclen = 126;
my $tally_reclen = 121;

# the values for these variables is similarly dependent on the actual
# values defined in mod_quotatab.h
my $ALL_QUOTA = 10;
my $USER_QUOTA = 20;
my $GROUP_QUOTA = 30;
my $CLASS_QUOTA = 40;

my $HARD_LIMIT = 1;
my $SOFT_LIMIT = 2;

my $LIMIT_TABLE = 100;
my $TALLY_TABLE = 101;

my $LIMIT_MAGIC = hex(7626);
my $TALLY_MAGIC = hex(7644);

my $default_limit_table = "./ftpquota.limittab";
my $default_tally_table = "./ftpquota.tallytab";
my $default_name = "";
my $default_quota_type = $ALL_QUOTA;
my $default_limit_type = $HARD_LIMIT;
my $default_per_session = 0;
my $default_bytes_in = -1.0;
my $default_bytes_out = -1.0;
my $default_bytes_xfer = -1.0;
my $default_files_in = 0;
my $default_files_out = 0;
my $default_files_xfer = 0;

my $byte_units = "byte";

my %opts = ();
GetOptions(\%opts, 'Bu=n', 'bytes-upload=n', 'Bd=n', 'bytes-download=n',
  'Bt=n', 'bytes-xfer=n', 'Fu=n', 'files-upload=n', 'Fd=n', 'files-download=n',
  'Ft=n', 'files-xfer', 'L|limit-type=s', 'N|name=s', 'P|per-session',
  'Q|quota-type=s', 'help', 'table-path=s', 'units=s', 'verbose', 'type=s',
  'add-record', 'create-table', 'delete-record', 'show-records',
  'update-record');

usage() if (defined($opts{'help'}));

my ($table, $table_type);
my %record = ();

parse_options();

if (defined($opts{'add-record'})) {
  open_table();
  wlock_table();
  add_record();
  unlock_table();
  close_table();
  exit 0;
}

if (defined($opts{'create-table'})) {
  open_table();
  close_table();
  exit 0;
}

if (defined($opts{'delete-record'})) {
  open_table();
  wlock_table();
  delete_record();
  unlock_table();
  close_table();
  exit 0;
}

if (defined($opts{'show-records'})) {
  open_table();
  rlock_table();
  print_table();
  unlock_table();
  close_table();
  exit 0;
}

if (defined($opts{'update-record'})) {
  open_table();
  wlock_table();
  update_record();
  unlock_table();
  close_table();
  exit 0;
}

# if no operations have been specified, note this and exit
print STDOUT "$program: no operations to perform, exiting\n";

exit;

# -------------------------------------------------------------------------
sub add_record {
  print STDOUT "$program: adding record\n" if $verbose;

  my $add_quota_type;

  # first, find the matching record.  Note that this option requires
  # both --name and --quota-type

  if (!defined($opts{'Q'})) {
    print STDOUT "$program: --add-record requires --quota-type option\n";
    exit(1);

  } else {
    $add_quota_type = get_quota_type(quota_type => $opts{'Q'});
  }

  if ($add_quota_type != $ALL_QUOTA) {
    if (!defined($opts{'N'})) {
      print STDOUT "$program: --add-record requires --name option\n";
      exit(1);
    }
  }

  # now, see if there is a matching record already in the table
  if (find_record(name => $opts{'N'}, quota_type => $add_quota_type)) {
    print STDOUT "$program: unable to add record: matching record already exists\n";
    exit(1);
  }

  # move to the end of the table, and write the new record
  set_table_position(0, SEEK_END);

  my $formatted_record;

  if ($table_type == $LIMIT_TABLE) {
    $formatted_record = pack($limit_format,
      $record{'name'}, $record{'quota_type'},
      $record{'per_session'}, $record{'limit_type'},
      $record{'bytes_in'}, $record{'bytes_out'}, $record{'bytes_xfer'},
      $record{'files_in'}, $record{'files_out'}, $record{'files_xfer'});

  } elsif ($table_type == $TALLY_TABLE) {

    $formatted_record = pack($tally_format,
      $record{'name'}, $record{'quota_type'},
      $record{'bytes_in'}, $record{'bytes_out'}, $record{'bytes_xfer'},
      $record{'files_in'}, $record{'files_out'}, $record{'files_xfer'});
  }

  write_record(record => $formatted_record);
}

# -------------------------------------------------------------------------
sub check_table {
  my $data;

  print STDOUT "$program: checking table '$table'\n" if $verbose;

  sysread TABLE, $data, 4;

  # unpack what was read in
  my ($magic) = unpack("L", $data);

  if ($table_type == $LIMIT_TABLE) {

    if ($magic != $LIMIT_MAGIC) {
      print STDOUT "$program: bad header: $magic != $LIMIT_MAGIC\n" if
        $verbose;
      die "$program: mismatched table header, exiting\n";

    } else {
      print STDOUT "$program: table has correct header\n" if $verbose;
    }

  } elsif ($table_type == $TALLY_TABLE) {

    if ($magic != $TALLY_MAGIC) {
      print STDOUT "$program: bad header magics: $magic != $TALLY_MAGIC\n" if
        $verbose;
      die "$program: mismatched table header, exiting\n";

    } else {
     print STDOUT "$program: table has correct header\n" if $verbose;
    }
  }
}

# -------------------------------------------------------------------------
sub close_table {
  print STDOUT "$program: closing table '$table'\n" if $verbose;

  close(TABLE);
}

# -------------------------------------------------------------------------
sub delete_record {
  print STDOUT "$program: deleting record\n" if $verbose;

  my $delete_quota_type;

  # first, find the matching record.  Note that this option requires
  # both --name and --quota-type

  if (!defined($opts{'Q'})) {
    print STDOUT "$program: --delete-record requires --quota-type option\n";
    exit(1);

  } else {
    $delete_quota_type = get_quota_type(quota_type => $opts{'Q'});
  }

  if ($delete_quota_type != $ALL_QUOTA) {
    if (!defined($opts{'N'})) {
      print STDOUT "$program: --delete-record requires --name option\n";
      exit(1);
    }
  }

  # now, find the matching record in the table
  unless (find_record(name => $opts{'N'}, quota_type => $delete_quota_type)) {
    print STDOUT "$program: unable to delete record: no match found\n";
    exit(1);
  }
  rewind_record();

  print STDOUT "$program: modifying table '$table'\n" if $verbose;

  # first, record the current file position
  my $position = get_table_position();

  # next, advance past this record to be deleted, and read in the
  # rest of the file

  seek_record(1);

  my ($nrecords, @records) = read_table();

  # Rewind the previous position, and "delete" that record by simply
  # overwriting it.  Note that this approach works for file tables --
  # tables in other formats, such as SQL, will have a much easier time
  # deleting a quota record.

  set_table_position($position, SEEK_SET);

  if ($nrecords > 0) {
    for (my $i = 0; $i < $nrecords; $i++) {
      write_record(record => $records[$i]);
    }

    # Truncate the table
    truncate_table(len => get_table_position() - 4);

  } else {

    # Truncate the table
    truncate_table(len => $position - 4);
  }

  # done
  exit(0);
}

# -------------------------------------------------------------------------
sub find_record {
  my %args = @_;
  my $nrecords = 0;
 
  my $search_name = $args{'name'};
  my $search_quota_type = $args{'quota_type'};

  while (my $record = read_record()) {
    $nrecords++;

    my ($name, $quota_type, @junk);

    # unpack the binary record structure
    if ($table_type == $LIMIT_TABLE) {
      ($name, $quota_type, @junk) = unpack($limit_format, $record);

    } elsif ($table_type == $TALLY_TABLE) {
      ($name, $quota_type, @junk) = unpack($tally_format, $record);
    }

    # remove any NULs from $name
    $name =~ s/(\0*)?//;

    if ($search_quota_type == $ALL_QUOTA and
        $quota_type == $ALL_QUOTA) {
      return $record;

    } elsif ($name eq $search_name and
             $quota_type == $search_quota_type) {
      return $record;
    }
  }

  return undef;
}

# -------------------------------------------------------------------------
sub get_display_bytes {
  my %args = @_;

  my $bytes = $args{'bytes'};

  if ($byte_units eq "byte") {
    # no calculation necessary

    unless ($bytes eq "unlimited" || $bytes <= 0) {
      return sprintf "bytes:\t%.2f", $bytes;
    } else {
      return sprintf "bytes:\tunlimited";
    }

  } elsif ($byte_units eq "kilo") {
    # divide by 1024.0
    my $adj_bytes = $bytes / 1024.0;

    unless ($bytes eq "unlimited" || $bytes <= 0) {
      return sprintf "Kb:\t%.2f", $adj_bytes;
    } else {
      return sprintf "Kb:\tunlimited";
    }

  } elsif ($byte_units eq "mega") {
    # divide by 1024.0 * 1024.0
    my $adj_bytes = $bytes / (1024.0 * 1024.0);

    unless ($bytes eq "unlimited" || $bytes <= 0) {
      return sprintf "Mb:\t%.2f", $adj_bytes;
    } else {
      return sprintf "Mb:\tunlimited";
    }

  } elsif ($byte_units eq "giga") {
    # divide by 1024.0 * 1024.0 * 1024.0
    my $adj_bytes = $bytes / (1024.0 * 1024.0 * 1024.0);

    unless ($bytes eq "unlimited" || $bytes <= 0) {
      return sprintf "Gb:\t%.2f", $adj_bytes;
    } else {
      return sprintf "Gb:\tunlimited";
    }
  }
}

# -------------------------------------------------------------------------
sub get_quota_type {
  my %args = @_;

  my $quota_type = $args{'quota_type'};

  # given the command-line --quote-type option value, return the
  # corresponding numeric value

  if ($quota_type eq "all") {
    return $ALL_QUOTA;

  } elsif ($quota_type eq "user") {
    return $USER_QUOTA;

  } elsif ($quota_type eq "group") {
    return $GROUP_QUOTA;

  } elsif ($quota_type eq "class") {
    return $CLASS_QUOTA;

  } else {
    print STDERR "$program: unknown quota type: $quota_type\n";
    exit(1);
  }
}

# -------------------------------------------------------------------------
sub get_table_position {
  return sysseek(TABLE, 0, 1);
}

# -------------------------------------------------------------------------
sub open_table {

  # if the table doesn't exist, create it if requested
  if (defined($opts{'create-table'})) {
    die "$program: cannot create existing table\n" if (-e $table);

    print STDOUT "$program: creating new table\n" if $verbose;

    open(TABLE, "> $table") or
      die "$program: unable to create $table: $!\n";

    print STDOUT "$program: writing header for new table\n" if $verbose;

    # write out the identifying header for the table type
    if ($table_type == $LIMIT_TABLE) {
      syswrite TABLE, pack("L", $LIMIT_MAGIC);

    } elsif ($table_type == $TALLY_TABLE) {
      syswrite TABLE, pack("L", $TALLY_MAGIC);
    }

  } else {

    print STDOUT "$program: opening table '$table'\n" if $verbose;

    if (defined($opts{'show-records'})) {
      open(TABLE, "< $table") or
        die "$program: unable to open $table: $!\n";

    } else {
      open(TABLE, "+< $table") or
        die "$program: unable to open $table: $!\n";
    }

    check_table();
  }
}

# -------------------------------------------------------------------------
sub parse_options {

  $verbose = 1 if (defined($opts{'verbose'}));

  if (defined($opts{'type'})) {

    die "$program: unknown table type: $opts{'type'}\n" if
      ($opts{'type'} ne 'limit' and $opts{'type'} ne 'tally');

    if ($opts{'type'} eq 'limit') {
      $table_type = $LIMIT_TABLE;
      $table = $default_limit_table;
    }

    if ($opts{'type'} eq 'tally') {
      $table_type = $TALLY_TABLE;
      $table = $default_tally_table;
    }

  } else {
    die "$program: missing required --type parameter\n";
  }

  $table = $opts{'table-path'} if (defined($opts{'table-path'}));

  if (defined($opts{'N'})) {
    $record{'name'} = $opts{'N'};

    # make sure the name is not too long
    die "$program: name is too long, must be less than 80 characters\n"
      if (length($record{'name'}) >= 80);

  } else {
    $record{'name'} = $default_name;
  }

  if (defined($opts{'P'})) {
    $record{'per_session'} = 1;

  } else {
    $record{'per_session'} = $default_per_session;
  }

  if (defined($opts{'Q'})) {
    if ($opts{'Q'} eq "all") {
      $record{'quota_type'} = $ALL_QUOTA;

    } elsif ($opts{'Q'} eq "user") {
      $record{'quota_type'} = $USER_QUOTA;

      # it's an error if there's no -N option present as well
      unless (defined($opts{'N'})) {
        print STDOUT "$program: quota type \"user\" requires --name option\n";
        exit(1);
      }

    } elsif ($opts{'Q'} eq "group") {
      $record{'quota_type'} = $GROUP_QUOTA;

      # it's an error if there's no -N option present as well
      unless (defined($opts{'N'})) {
        print STDOUT "$program: quota type \"group\" requires --name option\n";
        exit(1);
      }

    } elsif ($opts{'Q'} eq "class") {
      $record{'quota_type'} = $CLASS_QUOTA;

      # it's an error if there's no -N option present as well
      unless (defined($opts{'N'})) {
        print STDOUT "$program: quota type \"class\" requires --name option\n";
        exit(1);
      }

    } else {
      print STDERR "$program: unknown quota type: $opts{'Q'}\n";
      exit(1);
    }

  } else {
    $record{'quota_type'} = $ALL_QUOTA;
  }

  if (defined($opts{'L'})) {
    if ($opts{'L'} eq "hard") {
      $record{'limit_type'} = $HARD_LIMIT;

    } elsif ($opts{'L'} eq "soft") {
      $record{'limit_type'} = $SOFT_LIMIT;
 
    } else {
      print STDERR "$program: unknown limit type: $opts{'L'}\n";
      exit(1);
    }

  } else {
    $record{'limit_type'} = $HARD_LIMIT;
  }

  # process the --units option, if present
  if (defined($opts{'units'})) {

    if ($opts{'units'} eq "B" or $opts{'units'} eq "byte") {
      $byte_units = "byte";

    } elsif ($opts{'units'} eq "Kb" or $opts{'units'} eq "kilo") {
      $byte_units = "kilo";

    } elsif ($opts{'units'} eq "Mb" or $opts{'units'} eq "mega") {
      $byte_units = "mega";

    } elsif ($opts{'units'} eq "Gb" or $opts{'units'} eq "giga") {
      $byte_units = "giga";

    } else {
      print STDERR "$program: unknown unit type: $opts{'units'}\n";
      exit(1);
    }
  }

  if (defined($opts{'Bu'}) || defined($opts{'bytes-upload'})) {
    if (defined($opts{'Bu'}) && defined($opts{'bytes-upload'})) {
      print STDOUT "$program: do not use both --Bu and --bytes-upload options\n";
      exit(1);
    }

    $record{'bytes_in'} = $opts{'Bu'} if (defined($opts{'Bu'}));
    $record{'bytes_in'} = $opts{'bytes-upload'} if
      (defined($opts{'bytes-upload'}));

    if ($record{'bytes_in'} <= 0.0) {
      print STDERR "$program: notice: upload bytes value '", $record{'bytes_in'}, "' means 'unlimited'\n";
    }

    if ($record{'bytes_in'} != $default_bytes_in) {
      if ($byte_units eq "kilo") {
        $record{'bytes_in'} = $record{'bytes_in'} * 1024.0;

      } elsif ($byte_units eq "mega") {
        $record{'bytes_in'} = $record{'bytes_in'} * 1024.0 * 1024.0;

      } elsif ($byte_units eq "giga") {
        $record{'bytes_in'} = $record{'bytes_in'} * 1024.0 * 1024.0 * 1024.0;
      }
    }

  } else {

    $record{'bytes_in'} = $default_bytes_in if ($table_type == $LIMIT_TABLE);
    $record{'bytes_in'} = 0.0 if ($table_type == $TALLY_TABLE);
  }

  if (defined($opts{'Bd'}) || defined($opts{'bytes-download'})) {
    if (defined($opts{'Bd'}) && defined($opts{'bytes-download'})) {
      print STDOUT "$program: do not use both --Bd and --bytes-download options\n";
      exit(1);
    }

    $record{'bytes_out'} = $opts{'Bd'} if (defined($opts{'Bd'}));
    $record{'bytes_out'} = $opts{'bytes-download'} if
      (defined($opts{'bytes-download'}));

    if ($record{'bytes_out'} <= 0.0) {
      print STDERR "$program: notice: download bytes value '", $record{'bytes_out'}, "' means 'unlimited'\n";
    }

    if ($record{'bytes_out'} != $default_bytes_out) {
      if ($byte_units eq "kilo") {
        $record{'bytes_out'} = $record{'bytes_out'} * 1024.0;

      } elsif ($byte_units eq "mega") {
        $record{'bytes_out'} = $record{'bytes_out'} * 1024.0 * 1024.0;

      } elsif ($byte_units eq "giga") {
        $record{'bytes_out'} = $record{'bytes_out'} * 1024.0 * 1024.0 * 1024.0;
      }
    }

  } else {

    $record{'bytes_out'} = $default_bytes_out if ($table_type == $LIMIT_TABLE);
    $record{'bytes_out'} = 0.0 if ($table_type == $TALLY_TABLE);
  }

  if (defined($opts{'Bx'}) || defined($opts{'bytes-xfer'})) {
    if (defined($opts{'Bx'}) && defined($opts{'bytes-xfer'})) {
      print STDOUT "$program: do not use both --Bx and --bytes-xfer options\n";
      exit(1);
    }

    $record{'bytes_xfer'} = $opts{'Bx'} if (defined($opts{'Bx'}));
    $record{'bytes_xfer'} = $opts{'bytes-xfer'} if
      (defined($opts{'bytes-xfer'}));

    if ($record{'bytes_xfer'} <= 0.0) {
      print STDERR "$program: notice: transfer bytes value '", $record{'bytes_xfer'}, "' means 'unlimited'\n";
    }

    if ($record{'bytes_xfer'} != $default_bytes_xfer) {
      if ($byte_units eq "kilo") {
        $record{'bytes_xfer'} = $record{'bytes_xfer'} * 1024.0;

      } elsif ($byte_units eq "mega") {
        $record{'bytes_xfer'} = $record{'bytes_xfer'} * 1024.0 * 1024.0;

      } elsif ($byte_units eq "giga") {
        $record{'bytes_xfer'} =
          $record{'bytes_xfer'} * 1024.0 * 1024.0 * 1024.0;
      }
    }

  } else {

    $record{'bytes_xfer'} = $default_bytes_xfer if
      ($table_type == $LIMIT_TABLE);
    $record{'bytes_xfer'} = 0.0 if ($table_type == $TALLY_TABLE);
  }

  if (defined($opts{'Fu'}) || defined($opts{'files-upload'})) {
    if (defined($opts{'Fu'}) && defined($opts{'files-upload'})) {
      print STDOUT "$program: do not use both --Fu and --files-upload options\n";
      exit(1);
    }

    $record{'files_in'} = $opts{'Fu'} if (defined($opts{'Fu'}));
    $record{'files_in'} = $opts{'files-upload'} if
      (defined($opts{'files-upload'}));

    if ($record{'files_in'} <= 0) {
      print STDERR "$program: notice: upload files value '", $record{'files_in'}, "' means 'unlimited'\n";
    }

  } else {

    $record{'files_in'} = $default_files_in if ($table_type == $LIMIT_TABLE);
    $record{'files_in'} = 0 if ($table_type == $TALLY_TABLE);
  }

  if (defined($opts{'Fd'}) || defined($opts{'files-download'})) {
    if (defined($opts{'Fd'}) && defined($opts{'files-download'})) {
      print STDOUT "$program: do not use both --Fd and --files-download options\n";
      exit(1);
    }

    $record{'files_out'} = $opts{'Fd'} if (defined($opts{'Fd'}));
    $record{'files_out'} = $opts{'files-download'} if
      (defined($opts{'files-download'}));

    if ($record{'files_out'} <= 0) {
      print STDERR "$program: notice: download files value '", $record{'files_out'}, "' means 'unlimited'\n";
    }

  } else {

    $record{'files_out'} = $default_files_out if ($table_type == $LIMIT_TABLE);
    $record{'files_out'} = 0 if ($table_type == $TALLY_TABLE);
  }

  if (defined($opts{'Fx'}) || defined($opts{'files-xfer'})) {
    if (defined($opts{'Fx'}) && defined($opts{'files-xfer'})) {
      print STDOUT "$program: do not use both --Fx and --files-xfer options\n";
      exit(1);
    }

    $record{'files_xfer'} = $opts{'Fx'} if (defined($opts{'Fx'}));
    $record{'files_xfer'} = $opts{'files-xfer'} if
      (defined($opts{'files-xfer'}));

    if ($record{'files_xfer'} <= 0) {
      print STDERR "$program: notice: transfer files value '", $record{'files_xfer'}, "' means 'unlimited'\n";
    }

  } else {

    $record{'files_xfer'} = $default_files_xfer if
      ($table_type == $LIMIT_TABLE);
    $record{'files_xfer'} = 0 if ($table_type == $TALLY_TABLE);
  }
}

# -------------------------------------------------------------------------
sub print_record {
  my %args = @_;

  my $record = $args{'record'};

  my ($name, $quota_type, $per_session, $limit_type, $bytes_in, $bytes_out,
    $bytes_xfer, $files_in, $files_out, $files_xfer);

  if ($table_type == $LIMIT_TABLE) {
    ($name, $quota_type, $per_session, $limit_type, $bytes_in, $bytes_out,
     $bytes_xfer, $files_in, $files_out, $files_xfer) = unpack($limit_format,
     $record);

  } elsif ($table_type == $TALLY_TABLE) {

    ($name, $quota_type, $bytes_in, $bytes_out, $bytes_xfer,
     $files_in, $files_out, $files_xfer) = unpack($tally_format, $record);
  }


  # make the symbolic values legible
  $quota_type = "All" if ($quota_type == $ALL_QUOTA);
  $quota_type = "User" if ($quota_type == $USER_QUOTA);
  $quota_type = "Group" if ($quota_type == $GROUP_QUOTA);
  $quota_type = "Class" if ($quota_type == $CLASS_QUOTA);

  if ($table_type == $LIMIT_TABLE) {
    $limit_type = "Hard" if ($limit_type == $HARD_LIMIT);
    $limit_type = "Soft" if ($limit_type == $SOFT_LIMIT);

    if ($per_session eq 1) {
      $per_session = "True";

    } else {
      $per_session = "False";
    }
  }

  # print "unlimited" if quota is unlimited, but only for limit tables
  if ($table_type == $LIMIT_TABLE) {
    $bytes_in = "unlimited" if ($bytes_in <= 0.0);
    $bytes_out = "unlimited" if ($bytes_out <= 0.0);
    $bytes_xfer = "unlimited" if ($bytes_xfer <= 0.0);

    $files_in = "unlimited" if ($files_in <= 0);
    $files_out = "unlimited" if ($files_out <= 0);
    $files_xfer = "unlimited" if ($files_xfer <= 0);
  }

  print STDOUT "-------------------------------------------\n";
  print STDOUT "  Name: $name\n";
  print STDOUT "  Quota Type: $quota_type\n";

  if ($table_type == $LIMIT_TABLE) {
    print STDOUT "  Per Session: $per_session\n";
    print STDOUT "  Limit Type: $limit_type\n";
  }

  print STDOUT "    Uploaded ", get_display_bytes('bytes' => $bytes_in), "\n";
  print STDOUT "    Downloaded ", get_display_bytes('bytes' => $bytes_out),
    "\n";
  print STDOUT "    Transferred ", get_display_bytes('bytes' => $bytes_xfer),
    "\n";

  print STDOUT "    Uploaded files:\t$files_in\n";
  print STDOUT "    Downloaded files:\t$files_out\n";
  print STDOUT "    Transferred files:\t$files_xfer\n";
}

# -------------------------------------------------------------------------
sub print_table {
  my $have_records = 0;

  while (my $record = read_record()) {
    $have_records = 1;
    print_record(record => $record);
  }
  print STDOUT "$program: (empty table)\n" unless ($have_records);

  # done
  exit(0);
}

# -------------------------------------------------------------------------
sub read_record {
  my $record;
  my $bread;

  if ($table_type == $LIMIT_TABLE) {
    $bread = sysread TABLE, $record, $limit_reclen;

  } elsif ($table_type == $TALLY_TABLE) {
    $bread = sysread TABLE, $record, $tally_reclen;
  }

  if ($bread) {
    return $record;

  } else {
    return undef;
  }
}

# -------------------------------------------------------------------------
sub read_table {
  my @records = ();
  my $nrecords_read = 0;

  # read from TABLE
  while (my $record = read_record()) {
    $nrecords_read++;
    push(@records, $record);
  }

  return ($nrecords_read, @records);
}

# -------------------------------------------------------------------------
sub rewind_record {
  if ($table_type == $LIMIT_TABLE) {
    sysseek TABLE, -$limit_reclen, 1;

  } elsif ($table_type == $TALLY_TABLE) {
    sysseek TABLE, -$tally_reclen, 1;
  }
}

# -------------------------------------------------------------------------
sub rlock_table {
  print STDOUT "$program: read-locking table '$table'\n" if $verbose;

  flock(TABLE, LOCK_SH);
}

# -------------------------------------------------------------------------
sub seek_record {
  my ($n) = @_;

  if ($table_type == $LIMIT_TABLE) {
    sysseek TABLE, $limit_reclen, $n;
  
  } elsif ($table_type == $TALLY_TABLE) {
    sysseek TABLE, $tally_reclen, $n;
  }
}

# -------------------------------------------------------------------------
sub set_table_position {
  my ($position, $whence) = @_;

  my $result = sysseek(TABLE, $position, $whence);

  print STDOUT "$program: set table position to $result\n" if $verbose;

  return $result;
}

# -------------------------------------------------------------------------
sub truncate_table {
  my %args = @_;

  my $length = $args{'len'};

  # don't forget about the header (4 bytes)
  truncate TABLE, $length + 4;
}

# -------------------------------------------------------------------------
sub unlock_table {
  print STDOUT "$program: unlocking table '$table'\n" if $verbose;

  flock(TABLE, LOCK_UN);
}

# -------------------------------------------------------------------------
sub update_record {
  my $current_record;
  my $search_quota_type;

  # first, find the matching record.  Note that this option requires
  # both --name and --quota-type

  if (!defined($opts{'Q'})) {
    print STDOUT "$program: --update-record requires --quota-type option\n";
    exit(1);

  } else {
    $search_quota_type = get_quota_type(quota_type => $opts{'Q'});
  }

  if ($search_quota_type != $ALL_QUOTA) {
    if (!defined($opts{'N'})) {
      print STDOUT "$program: --update-record requires --name option\n";
      exit(1);
    }
  }

  # now, find the matching record in the table
  open_table();
  unless ($current_record = find_record(name => $opts{'N'},
      quota_type => $search_quota_type)) {
    print STDOUT "$program: unable to update record: no match found\n";
    exit(1);
  }
  rewind_record();

  print STDOUT "$program: updating table '$table'\n" if $verbose;

  # now, adjust the current record with the updated values in the requested
  # record

  my ($name, $quota_type, $per_session, $limit_type, @junk);

  if ($table_type == $LIMIT_TABLE) { 
    ($name, $quota_type, $per_session, $limit_type,
      @junk) = unpack($limit_format, $current_record);

  } elsif ($table_type == $TALLY_TABLE) {

    ($name, $quota_type, @junk) = unpack($tally_format, $current_record);
  }

  my $formatted_record;

  if ($table_type == $LIMIT_TABLE) {
    $formatted_record = pack($limit_format, $name, $quota_type,
      $record{'per_session'}, $record{'limit_type'},
      $record{'bytes_in'}, $record{'bytes_out'}, $record{'bytes_xfer'},
      $record{'files_in'}, $record{'files_out'}, $record{'files_xfer'});

  } elsif ($table_type == $TALLY_TABLE) {

    $formatted_record = pack($tally_format, $name, $quota_type,
      $record{'bytes_in'}, $record{'bytes_out'}, $record{'bytes_xfer'},
      $record{'files_in'}, $record{'files_out'}, $record{'files_xfer'});
  }

  write_record(record => $formatted_record);

  # done
  exit(0);
}

# -------------------------------------------------------------------------
sub wlock_table {
  print STDOUT "$program: write-locking table '$table'\n" if $verbose;

  flock(TABLE, LOCK_EX);
}

# -------------------------------------------------------------------------
sub write_record {
  my %args = @_;
  my $record = $args{'record'};

  print STDOUT "$program: writing record\n" if $verbose;

  die "$program: error writing table: $!\n" unless
    syswrite TABLE, $record;
}

# -------------------------------------------------------------------------
sub usage {

  print STDOUT <<END_OF_USAGE;

usage: $program [options]

 The following options describe the type of operation to be performed:

  --add-record         Create a new record with the specified limits.  Any
                       limits left unspecified with have their default 
                       values.  This option requires the --name and 
                       --quota-type options.

  --create-table       Create the table if not present.  Used to initialize
                       a table.  The default limit table path is
                       "$default_limit_table".  The default tally table path is
                       "$default_tally_table".

  --delete-record      Deletes a quota record from the table.  This option
                       requires the --name and --quote-type options.

  --show-records       Prints out all of the quota records in the table in
                       a legible format.

  --update-record      Updates a quota record with the specified limits.  Any
                       limits left unspecified will be updated with their
                       default value.  This option requires the --name and
                       --quota-type options.

 The following option describes the type of table on which to operate:

  --type               Specifies a table type to use.  The allowable options
                       are: "limit" or "tally".  This is required.

 The following options are used to specify specific quota limits:

  --Bu                 Specifies the limit of the number of bytes that may be
  --bytes-upload       uploaded.  Defaults to -1 (unlimited).

  --Bd                 Specifies the limit of the number of bytes that may be
  --bytes-download     downloaded.  Defaults to -1 (unlimited).

  --Bx                 Specifies the limit of the number of bytes that may be
  --bytes-xfer         transferred.  Note that this total includes uploads,
                       downloads, AND directory listings.  Defaults to
                       -1 (unlimited).

  --Fu                 Specifies the limit of the number of files that may be
  --files-upload       uploaded.  Defaults to -1 (unlimited).

  --Fd                 Specifies the limit of the number of files that may be
  --files-download     downloaded.  Defaults to -1 (unlimited).

  --Fx                 Specifies the limit of the number of files that may be
  --files-xfer         transferred, including uploads and downloads.  Defaults
                       to -1 (unlimited).

  -L                   Specifies the type of limit, "hard" or "soft", of
  --limit-type         the bytes limits.  If "hard", any uploaded files that
                       push the bytes used counter past the limit will be
                       automatically deleted; if "soft", those extra bytes
                       will be allowed, but future uploads will be denied.
                       This option only makes sense if --type is "limit".

  -N                   Specifies a name for the quota record.  This name
  --name               will be the user/login name, group name, or class
                       name, depending on the quota type.  This option
                       is ignored if the quota type specified is "all".

  -P
  --per-session        Specifies that the quota limit is to be applied only
                       to each session, rather than persisting across
                       sessions.  By default, quotas are persistent.

  -Q                   Specifies a "quota type" for this record, where
  --quota-type         the type means to which category of FTP users this
                       quota will apply.  The quota type must be one of:
                       "user", "group", "class", or "all".

 The following options are miscellaneous:

  --help               Displays this message.

  --table-path         Specifies the path to a quota table file to use.

  --units              Specifies whether to treats bytes as is, in kilobytes,
                       megabytes, or gigabytes.  Allowable options are:
                       "B" or "byte", "Kb" or "kilo", "Mb" or "mega",
                       and "Gb" or "giga".  Defaults to "byte".

  --verbose            Toggles more verbose information about the doings of
                       the tool as it works.

END_OF_USAGE

  exit 0;
}

# -------------------------------------------------------------------------


Anon7 - 2022
SCDN GOK