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/imh-python/lib/python3.9/site-packages/libcloud/storage/drivers
Viewing File: /opt/imh-python/lib/python3.9/site-packages/libcloud/storage/drivers/cloudfiles.py
# Licensed to the Apache Software Foundation (ASF) under one or more # contributor license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright ownership. # The ASF licenses this file to You under the Apache License, Version 2.0 # (the "License"); you may not use this file except in compliance with # the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import atexit import hmac import os from time import time from hashlib import sha1 from libcloud.utils.py3 import httplib from libcloud.utils.py3 import urlencode try: import simplejson as json except ImportError: import json # type: ignore from libcloud.utils.py3 import PY3 from libcloud.utils.py3 import b from libcloud.utils.py3 import urlquote if PY3: from io import FileIO as file from libcloud.utils.files import read_in_chunks from libcloud.common.types import MalformedResponseError, LibcloudError from libcloud.common.base import Response, RawResponse from libcloud.storage.providers import Provider from libcloud.storage.base import Object, Container, StorageDriver from libcloud.storage.types import ContainerAlreadyExistsError from libcloud.storage.types import ContainerDoesNotExistError from libcloud.storage.types import ContainerIsNotEmptyError from libcloud.storage.types import ObjectDoesNotExistError from libcloud.storage.types import ObjectHashMismatchError from libcloud.storage.types import InvalidContainerNameError from libcloud.common.openstack import OpenStackBaseConnection from libcloud.common.openstack import OpenStackDriverMixin from libcloud.common.rackspace import AUTH_URL CDN_HOST = 'cdn.clouddrive.com' API_VERSION = 'v1.0' # Keys which are used to select a correct endpoint from the service catalog. INTERNAL_ENDPOINT_KEY = 'internalURL' PUBLIC_ENDPOINT_KEY = 'publicURL' class CloudFilesResponse(Response): valid_response_codes = [httplib.NOT_FOUND, httplib.CONFLICT] def success(self): i = int(self.status) return 200 <= i <= 299 or i in self.valid_response_codes def parse_body(self): if not self.body: return None if 'content-type' in self.headers: key = 'content-type' elif 'Content-Type' in self.headers: key = 'Content-Type' else: raise LibcloudError('Missing content-type header') content_type = self.headers[key] if content_type.find(';') != -1: content_type = content_type.split(';')[0] if content_type == 'application/json': try: data = json.loads(self.body) except Exception: raise MalformedResponseError('Failed to parse JSON', body=self.body, driver=CloudFilesStorageDriver) elif content_type == 'text/plain': data = self.body else: data = self.body return data class CloudFilesRawResponse(CloudFilesResponse, RawResponse): pass class OpenStackSwiftConnection(OpenStackBaseConnection): """ Connection class for the OpenStack Swift endpoint. """ responseCls = CloudFilesResponse rawResponseCls = CloudFilesRawResponse auth_url = AUTH_URL _auth_version = '1.0' # TODO: Reverse the relationship - Swift -> CloudFiles def __init__(self, user_id, key, secure=True, **kwargs): # Ignore this for now kwargs.pop('use_internal_url', None) super(OpenStackSwiftConnection, self).__init__(user_id, key, secure=secure, **kwargs) self.api_version = API_VERSION self.accept_format = 'application/json' self._service_type = self._ex_force_service_type or 'object-store' self._service_name = self._ex_force_service_name or 'swift' if self._ex_force_service_region: self._service_region = self._ex_force_service_region else: self._service_region = None def get_endpoint(self, *args, **kwargs): if ('2.0' in self._auth_version) or ('3.x' in self._auth_version): endpoint = self.service_catalog.get_endpoint( service_type=self._service_type, name=self._service_name, region=self._service_region) elif ('1.1' in self._auth_version) or ('1.0' in self._auth_version): endpoint = self.service_catalog.get_endpoint( name=self._service_name, region=self._service_region) else: endpoint = None if endpoint: return endpoint.url else: raise LibcloudError('Could not find specified endpoint') def request(self, action, params=None, data='', headers=None, method='GET', raw=False, cdn_request=False): if not headers: headers = {} if not params: params = {} self.cdn_request = cdn_request params['format'] = 'json' if method in ['POST', 'PUT'] and 'Content-Type' not in headers: headers.update({'Content-Type': 'application/json; charset=UTF-8'}) return super(OpenStackSwiftConnection, self).request( action=action, params=params, data=data, method=method, headers=headers, raw=raw) class CloudFilesConnection(OpenStackSwiftConnection): """ Base connection class for the Cloudfiles driver. """ responseCls = CloudFilesResponse rawResponseCls = CloudFilesRawResponse auth_url = AUTH_URL _auth_version = '2.0' def __init__(self, user_id, key, secure=True, use_internal_url=False, **kwargs): super(CloudFilesConnection, self).__init__(user_id, key, secure=secure, **kwargs) self.api_version = API_VERSION self.accept_format = 'application/json' self.cdn_request = False self.use_internal_url = use_internal_url def get_endpoint(self): region = self._ex_force_service_region.upper() if self.use_internal_url: endpoint_type = 'internal' else: endpoint_type = 'external' if '2.0' in self._auth_version: ep = self.service_catalog.get_endpoint( service_type='object-store', name='cloudFiles', region=region, endpoint_type=endpoint_type) cdn_ep = self.service_catalog.get_endpoint( service_type='rax:object-cdn', name='cloudFilesCDN', region=region, endpoint_type=endpoint_type) else: raise LibcloudError( 'Auth version "%s" not supported' % (self._auth_version)) # if this is a CDN request, return the cdn url instead if self.cdn_request: ep = cdn_ep if not ep or not ep.url: raise LibcloudError('Could not find specified endpoint') return ep.url def request(self, action, params=None, data='', headers=None, method='GET', raw=False, cdn_request=False): if not headers: headers = {} if not params: params = {} self.cdn_request = cdn_request params['format'] = 'json' if method in ['POST', 'PUT'] and 'Content-Type' not in headers: headers.update({'Content-Type': 'application/json; charset=UTF-8'}) return super(CloudFilesConnection, self).request( action=action, params=params, data=data, method=method, headers=headers, raw=raw, cdn_request=cdn_request) class CloudFilesStorageDriver(StorageDriver, OpenStackDriverMixin): """ CloudFiles driver. """ name = 'CloudFiles' website = 'http://www.rackspace.com/' connectionCls = CloudFilesConnection hash_type = 'md5' supports_chunked_encoding = True def __init__(self, key, secret=None, secure=True, host=None, port=None, region='ord', use_internal_url=False, **kwargs): """ @inherits: :class:`StorageDriver.__init__` :param region: ID of the region which should be used. :type region: ``str`` """ # This is here for backard compatibility if 'ex_force_service_region' in kwargs: region = kwargs['ex_force_service_region'] self.use_internal_url = use_internal_url OpenStackDriverMixin.__init__(self, **kwargs) super(CloudFilesStorageDriver, self).__init__(key=key, secret=secret, secure=secure, host=host, port=port, region=region, **kwargs) @classmethod def list_regions(cls): return ['ord', 'dfw', 'iad', 'lon', 'hkg', 'syd'] def iterate_containers(self): response = self.connection.request('') if response.status == httplib.NO_CONTENT: return [] elif response.status == httplib.OK: return self._to_container_list(json.loads(response.body)) raise LibcloudError('Unexpected status code: %s' % (response.status)) def get_container(self, container_name): container_name_encoded = self._encode_container_name(container_name) response = self.connection.request('/%s' % (container_name_encoded), method='HEAD') if response.status == httplib.NO_CONTENT: container = self._headers_to_container( container_name, response.headers) return container elif response.status == httplib.NOT_FOUND: raise ContainerDoesNotExistError(None, self, container_name) raise LibcloudError('Unexpected status code: %s' % (response.status)) def get_object(self, container_name, object_name): container = self.get_container(container_name) container_name_encoded = self._encode_container_name(container_name) object_name_encoded = self._encode_object_name(object_name) response = self.connection.request('/%s/%s' % (container_name_encoded, object_name_encoded), method='HEAD') if response.status in [httplib.OK, httplib.NO_CONTENT]: obj = self._headers_to_object( object_name, container, response.headers) return obj elif response.status == httplib.NOT_FOUND: raise ObjectDoesNotExistError(None, self, object_name) raise LibcloudError('Unexpected status code: %s' % (response.status)) def get_container_cdn_url(self, container, ex_ssl_uri=False): # pylint: disable=unexpected-keyword-arg container_name_encoded = self._encode_container_name(container.name) response = self.connection.request('/%s' % (container_name_encoded), method='HEAD', cdn_request=True) if response.status == httplib.NO_CONTENT: if ex_ssl_uri: cdn_url = response.headers['x-cdn-ssl-uri'] else: cdn_url = response.headers['x-cdn-uri'] return cdn_url elif response.status == httplib.NOT_FOUND: raise ContainerDoesNotExistError(value='', container_name=container.name, driver=self) raise LibcloudError('Unexpected status code: %s' % (response.status)) def get_object_cdn_url(self, obj): container_cdn_url = self.get_container_cdn_url(container=obj.container) return '%s/%s' % (container_cdn_url, obj.name) def enable_container_cdn(self, container, ex_ttl=None): """ @inherits: :class:`StorageDriver.enable_container_cdn` :param ex_ttl: cache time to live :type ex_ttl: ``int`` """ container_name = self._encode_container_name(container.name) headers = {'X-CDN-Enabled': 'True'} if ex_ttl: headers['X-TTL'] = ex_ttl # pylint: disable=unexpected-keyword-arg response = self.connection.request('/%s' % (container_name), method='PUT', headers=headers, cdn_request=True) return response.status in [httplib.CREATED, httplib.ACCEPTED] def create_container(self, container_name): container_name_encoded = self._encode_container_name(container_name) response = self.connection.request( '/%s' % (container_name_encoded), method='PUT') if response.status == httplib.CREATED: # Accepted mean that container is not yet created but it will be # eventually extra = {'object_count': 0} container = Container(name=container_name, extra=extra, driver=self) return container elif response.status == httplib.ACCEPTED: error = ContainerAlreadyExistsError(None, self, container_name) raise error raise LibcloudError('Unexpected status code: %s' % (response.status)) def delete_container(self, container): name = self._encode_container_name(container.name) # Only empty container can be deleted response = self.connection.request('/%s' % (name), method='DELETE') if response.status == httplib.NO_CONTENT: return True elif response.status == httplib.NOT_FOUND: raise ContainerDoesNotExistError(value='', container_name=name, driver=self) elif response.status == httplib.CONFLICT: # @TODO: Add "delete_all_objects" parameter? raise ContainerIsNotEmptyError(value='', container_name=name, driver=self) def download_object(self, obj, destination_path, overwrite_existing=False, delete_on_failure=True): container_name = obj.container.name object_name = obj.name response = self.connection.request('/%s/%s' % (container_name, object_name), method='GET', raw=True) return self._get_object( obj=obj, callback=self._save_object, response=response, callback_kwargs={'obj': obj, 'response': response.response, 'destination_path': destination_path, 'overwrite_existing': overwrite_existing, 'delete_on_failure': delete_on_failure}, success_status_code=httplib.OK) def download_object_as_stream(self, obj, chunk_size=None): container_name = obj.container.name object_name = obj.name response = self.connection.request('/%s/%s' % (container_name, object_name), method='GET', raw=True) return self._get_object(obj=obj, callback=read_in_chunks, response=response, callback_kwargs={ 'iterator': response.iter_content( chunk_size ), 'chunk_size': chunk_size }, success_status_code=httplib.OK) def download_object_range(self, obj, destination_path, start_bytes, end_bytes=None, overwrite_existing=False, delete_on_failure=True): self._validate_start_and_end_bytes(start_bytes=start_bytes, end_bytes=end_bytes) container_name = obj.container.name object_name = obj.name headers = {'Range': self._get_standard_range_str(start_bytes, end_bytes)} response = self.connection.request('/%s/%s' % (container_name, object_name), method='GET', headers=headers, raw=True) return self._get_object( obj=obj, callback=self._save_object, response=response, callback_kwargs={'obj': obj, 'response': response.response, 'destination_path': destination_path, 'overwrite_existing': overwrite_existing, 'delete_on_failure': delete_on_failure, 'partial_download': True}, success_status_code=httplib.PARTIAL_CONTENT) def download_object_range_as_stream(self, obj, start_bytes, end_bytes=None, chunk_size=None): self._validate_start_and_end_bytes(start_bytes=start_bytes, end_bytes=end_bytes) container_name = obj.container.name object_name = obj.name headers = {'Range': self._get_standard_range_str(start_bytes, end_bytes)} response = self.connection.request('/%s/%s' % (container_name, object_name), headers=headers, method='GET', raw=True) return self._get_object( obj=obj, callback=read_in_chunks, response=response, callback_kwargs={'iterator': response.iter_content(chunk_size), 'chunk_size': chunk_size}, success_status_code=httplib.PARTIAL_CONTENT) def upload_object(self, file_path, container, object_name, extra=None, verify_hash=True, headers=None): """ Upload an object. Note: This will override file with a same name if it already exists. """ return self._put_object(container=container, object_name=object_name, extra=extra, file_path=file_path, verify_hash=verify_hash, headers=headers) def upload_object_via_stream(self, iterator, container, object_name, extra=None, headers=None): if isinstance(iterator, file): iterator = iter(iterator) return self._put_object(container=container, object_name=object_name, extra=extra, stream=iterator, headers=headers) def delete_object(self, obj): container_name = self._encode_container_name(obj.container.name) object_name = self._encode_object_name(obj.name) response = self.connection.request( '/%s/%s' % (container_name, object_name), method='DELETE') if response.status == httplib.NO_CONTENT: return True elif response.status == httplib.NOT_FOUND: raise ObjectDoesNotExistError(value='', object_name=object_name, driver=self) raise LibcloudError('Unexpected status code: %s' % (response.status)) def ex_purge_object_from_cdn(self, obj, email=None): """ Purge edge cache for the specified object. :param email: Email where a notification will be sent when the job completes. (optional) :type email: ``str`` """ container_name = self._encode_container_name(obj.container.name) object_name = self._encode_object_name(obj.name) headers = {'X-Purge-Email': email} if email else {} # pylint: disable=unexpected-keyword-arg response = self.connection.request('/%s/%s' % (container_name, object_name), method='DELETE', headers=headers, cdn_request=True) return response.status == httplib.NO_CONTENT def ex_get_meta_data(self): """ Get meta data :rtype: ``dict`` """ response = self.connection.request('', method='HEAD') if response.status == httplib.NO_CONTENT: container_count = response.headers.get( 'x-account-container-count', 'unknown') object_count = response.headers.get( 'x-account-object-count', 'unknown') bytes_used = response.headers.get( 'x-account-bytes-used', 'unknown') temp_url_key = response.headers.get( 'x-account-meta-temp-url-key', None) return {'container_count': int(container_count), 'object_count': int(object_count), 'bytes_used': int(bytes_used), 'temp_url_key': temp_url_key} raise LibcloudError('Unexpected status code: %s' % (response.status)) def ex_multipart_upload_object(self, file_path, container, object_name, chunk_size=33554432, extra=None, verify_hash=True): object_size = os.path.getsize(file_path) if object_size < chunk_size: return self.upload_object(file_path, container, object_name, extra=extra, verify_hash=verify_hash) iter_chunk_reader = FileChunkReader(file_path, chunk_size) for index, iterator in enumerate(iter_chunk_reader): self._upload_object_part(container=container, object_name=object_name, part_number=index, iterator=iterator, verify_hash=verify_hash) return self._upload_object_manifest(container=container, object_name=object_name, extra=extra, verify_hash=verify_hash) def ex_enable_static_website(self, container, index_file='index.html'): """ Enable serving a static website. :param container: Container instance :type container: :class:`Container` :param index_file: Name of the object which becomes an index page for every sub-directory in this container. :type index_file: ``str`` :rtype: ``bool`` """ container_name = container.name headers = {'X-Container-Meta-Web-Index': index_file} # pylint: disable=unexpected-keyword-arg response = self.connection.request('/%s' % (container_name), method='POST', headers=headers, cdn_request=False) return response.status in [httplib.CREATED, httplib.ACCEPTED] def ex_set_error_page(self, container, file_name='error.html'): """ Set a custom error page which is displayed if file is not found and serving of a static website is enabled. :param container: Container instance :type container: :class:`Container` :param file_name: Name of the object which becomes the error page. :type file_name: ``str`` :rtype: ``bool`` """ container_name = container.name headers = {'X-Container-Meta-Web-Error': file_name} # pylint: disable=unexpected-keyword-arg response = self.connection.request('/%s' % (container_name), method='POST', headers=headers, cdn_request=False) return response.status in [httplib.CREATED, httplib.ACCEPTED] def ex_set_account_metadata_temp_url_key(self, key): """ Set the metadata header X-Account-Meta-Temp-URL-Key on your Cloud Files account. :param key: X-Account-Meta-Temp-URL-Key :type key: ``str`` :rtype: ``bool`` """ headers = {'X-Account-Meta-Temp-URL-Key': key} # pylint: disable=unexpected-keyword-arg response = self.connection.request('', method='POST', headers=headers, cdn_request=False) return response.status in [httplib.OK, httplib.NO_CONTENT, httplib.CREATED, httplib.ACCEPTED] def ex_get_object_temp_url(self, obj, method='GET', timeout=60): """ Create a temporary URL to allow others to retrieve or put objects in your Cloud Files account for as long or as short a time as you wish. This method is specifically for allowing users to retrieve or update an object. :param obj: The object that you wish to make temporarily public :type obj: :class:`Object` :param method: Which method you would like to allow, 'PUT' or 'GET' :type method: ``str`` :param timeout: Time (in seconds) after which you want the TempURL to expire. :type timeout: ``int`` :rtype: ``bool`` """ # pylint: disable=no-member self.connection._populate_hosts_and_request_paths() expires = int(time() + timeout) path = '%s/%s/%s' % (self.connection.request_path, obj.container.name, obj.name) try: key = self.ex_get_meta_data()['temp_url_key'] assert key is not None except Exception: raise KeyError('You must first set the ' + 'X-Account-Meta-Temp-URL-Key header on your ' + 'Cloud Files account using ' + 'ex_set_account_metadata_temp_url_key before ' + 'you can use this method.') hmac_body = '%s\n%s\n%s' % (method, expires, path) sig = hmac.new(b(key), b(hmac_body), sha1).hexdigest() params = urlencode({'temp_url_sig': sig, 'temp_url_expires': expires}) temp_url = 'https://%s/%s/%s?%s' %\ (self.connection.host + self.connection.request_path, obj.container.name, obj.name, params) return temp_url def _upload_object_part(self, container, object_name, part_number, iterator, verify_hash=True): part_name = object_name + '/%08d' % part_number extra = {'content_type': 'application/octet-stream'} self._put_object(container=container, object_name=part_name, extra=extra, stream=iterator, verify_hash=verify_hash) def _upload_object_manifest(self, container, object_name, extra=None, verify_hash=True): extra = extra or {} meta_data = extra.get('meta_data') container_name_encoded = self._encode_container_name(container.name) object_name_encoded = self._encode_object_name(object_name) request_path = '/%s/%s' % (container_name_encoded, object_name_encoded) # pylint: disable=no-member headers = {'X-Auth-Token': self.connection.auth_token, 'X-Object-Manifest': '%s/%s/' % (container_name_encoded, object_name_encoded)} data = '' response = self.connection.request(request_path, method='PUT', data=data, headers=headers, raw=True) object_hash = None if verify_hash: hash_function = self._get_hash_function() hash_function.update(b(data)) data_hash = hash_function.hexdigest() object_hash = response.headers.get('etag') if object_hash != data_hash: raise ObjectHashMismatchError( value=('MD5 hash checksum does not match (expected=%s, ' + 'actual=%s)') % (data_hash, object_hash), object_name=object_name, driver=self) obj = Object(name=object_name, size=0, hash=object_hash, extra=None, meta_data=meta_data, container=container, driver=self) return obj def iterate_container_objects(self, container, prefix=None, ex_prefix=None): """ Return a generator of objects for the given container. :param container: Container instance :type container: :class:`Container` :param prefix: Only get objects with names starting with prefix :type prefix: ``str`` :param ex_prefix: (Deprecated.) Only get objects with names starting with ex_prefix :type ex_prefix: ``str`` :return: A generator of Object instances. :rtype: ``generator`` of :class:`Object` """ prefix = self._normalize_prefix_argument(prefix, ex_prefix) params = {} if prefix: params['prefix'] = prefix while True: container_name_encoded = \ self._encode_container_name(container.name) response = self.connection.request('/%s' % (container_name_encoded), params=params) if response.status == httplib.NO_CONTENT: # Empty or non-existent container break elif response.status == httplib.OK: objects = self._to_object_list(json.loads(response.body), container) if len(objects) == 0: break for obj in objects: yield obj params['marker'] = obj.name else: raise LibcloudError('Unexpected status code: %s' % (response.status)) def _put_object(self, container, object_name, extra=None, file_path=None, stream=None, verify_hash=True, headers=None): extra = extra or {} container_name_encoded = self._encode_container_name(container.name) object_name_encoded = self._encode_object_name(object_name) content_type = extra.get('content_type', None) meta_data = extra.get('meta_data', None) content_disposition = extra.get('content_disposition', None) headers = headers or {} if meta_data: for key, value in list(meta_data.items()): key = 'X-Object-Meta-%s' % (key) headers[key] = value if content_disposition is not None: headers['Content-Disposition'] = content_disposition request_path = '/%s/%s' % (container_name_encoded, object_name_encoded) result_dict = self._upload_object( object_name=object_name, content_type=content_type, request_path=request_path, request_method='PUT', headers=headers, file_path=file_path, stream=stream) response = result_dict['response'] bytes_transferred = result_dict['bytes_transferred'] server_hash = result_dict['response'].headers.get('etag', None) if response.status == httplib.EXPECTATION_FAILED: raise LibcloudError(value='Missing content-type header', driver=self) elif verify_hash and not server_hash: raise LibcloudError(value='Server didn\'t return etag', driver=self) elif (verify_hash and result_dict['data_hash'] != server_hash): raise ObjectHashMismatchError( value=('MD5 hash checksum does not match (expected=%s, ' + 'actual=%s)') % (result_dict['data_hash'], server_hash), object_name=object_name, driver=self) elif response.status == httplib.CREATED: obj = Object( name=object_name, size=bytes_transferred, hash=server_hash, extra=None, meta_data=meta_data, container=container, driver=self) return obj else: # @TODO: Add test case for this condition (probably 411) raise LibcloudError('status_code=%s' % (response.status), driver=self) def _encode_container_name(self, name): """ Encode container name so it can be used as part of the HTTP request. """ if name.startswith('/'): name = name[1:] name = urlquote(name) if name.find('/') != -1: raise InvalidContainerNameError(value='Container name cannot' ' contain slashes', container_name=name, driver=self) if len(name) > 256: raise InvalidContainerNameError( value='Container name cannot be longer than 256 bytes', container_name=name, driver=self) return name def _encode_object_name(self, name): name = urlquote(name) return name def _to_container_list(self, response): # @TODO: Handle more than 10k containers - use "lazy list"? for container in response: extra = {'object_count': int(container['count']), 'size': int(container['bytes'])} yield Container(name=container['name'], extra=extra, driver=self) def _to_object_list(self, response, container): objects = [] for obj in response: name = obj['name'] size = int(obj['bytes']) hash = obj['hash'] extra = {'content_type': obj['content_type'], 'last_modified': obj['last_modified']} objects.append(Object( name=name, size=size, hash=hash, extra=extra, meta_data=None, container=container, driver=self)) return objects def _headers_to_container(self, name, headers): size = int(headers.get('x-container-bytes-used', 0)) object_count = int(headers.get('x-container-object-count', 0)) extra = {'object_count': object_count, 'size': size} container = Container(name=name, extra=extra, driver=self) return container def _headers_to_object(self, name, container, headers): size = int(headers.pop('content-length', 0)) last_modified = headers.pop('last-modified', None) etag = headers.pop('etag', None) content_type = headers.pop('content-type', None) meta_data = {} for key, value in list(headers.items()): if key.find('x-object-meta-') != -1: key = key.replace('x-object-meta-', '') meta_data[key] = value extra = {'content_type': content_type, 'last_modified': last_modified} obj = Object(name=name, size=size, hash=etag, extra=extra, meta_data=meta_data, container=container, driver=self) return obj def _ex_connection_class_kwargs(self): kwargs = self.openstack_connection_kwargs() kwargs['ex_force_service_region'] = self.region kwargs['use_internal_url'] = self.use_internal_url return kwargs class OpenStackSwiftStorageDriver(CloudFilesStorageDriver): """ Storage driver for the OpenStack Swift. """ type = Provider.CLOUDFILES_SWIFT name = 'OpenStack Swift' connectionCls = OpenStackSwiftConnection # TODO: Reverse the relationship - Swift -> CloudFiles def __init__(self, key, secret=None, secure=True, host=None, port=None, region=None, **kwargs): super(OpenStackSwiftStorageDriver, self).__init__(key=key, secret=secret, secure=secure, host=host, port=port, region=region, **kwargs) class FileChunkReader(object): def __init__(self, file_path, chunk_size): self.file_path = file_path self.total = os.path.getsize(file_path) self.chunk_size = chunk_size self.bytes_read = 0 self.stop_iteration = False def __iter__(self): return self def next(self): if self.stop_iteration: raise StopIteration start_block = self.bytes_read end_block = start_block + self.chunk_size if end_block >= self.total: end_block = self.total self.stop_iteration = True self.bytes_read += end_block - start_block return ChunkStreamReader(file_path=self.file_path, start_block=start_block, end_block=end_block, chunk_size=8192) def __next__(self): return self.next() class ChunkStreamReader(object): def __init__(self, file_path, start_block, end_block, chunk_size): self.fd = open(file_path, 'rb') self.fd.seek(start_block) self.start_block = start_block self.end_block = end_block self.chunk_size = chunk_size self.bytes_read = 0 self.stop_iteration = False # Work around to make sure file description is closed even if the # iterator is never read from or if it's not fully exhausted def close_file(fd): try: fd.close() except Exception: pass atexit.register(close_file, self.fd) def __iter__(self): return self def next(self): if self.stop_iteration: self.fd.close() raise StopIteration block_size = self.chunk_size if self.bytes_read + block_size > \ self.end_block - self.start_block: block_size = self.end_block - self.start_block - self.bytes_read self.stop_iteration = True block = self.fd.read(block_size) self.bytes_read += block_size return block def __next__(self): return self.next()