Merge lp:~gholt/swift/stwork into lp:~hudson-openstack/swift/trunk
- stwork
- Merge into trunk
Status: | Merged | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Approved by: | Chuck Thier | ||||||||||||
Approved revision: | 136 | ||||||||||||
Merged at revision: | 131 | ||||||||||||
Proposed branch: | lp:~gholt/swift/stwork | ||||||||||||
Merge into: | lp:~hudson-openstack/swift/trunk | ||||||||||||
Diff against target: |
2218 lines (+981/-927) 2 files modified
bin/st (+975/-925) swift/common/client.py (+6/-2) |
||||||||||||
To merge this branch: | bzr merge lp:~gholt/swift/stwork | ||||||||||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Chuck Thier (community) | Approve | ||
clayg | Approve | ||
Review via email: mp+41214@code.launchpad.net |
Commit message
Description of the change
Cleaned up st command line parsing; always use included client.py as well
Jay Payne (letterj) wrote : | # |
Awesome work!
When I run:
./st -A http://
I get an error even though it states: Usage: st [options] <command> [options] [args]
st: error: no such option: -o
Maybe for clarity we could change the Usage to read "st [connection options] <command> [options] [args]
Jay Payne (letterj) wrote : | # |
> Awesome work!
>
>
> When I run:
> ./st -A http://
> logtest test
>
> I get an error even though it states: Usage: st [options] <command>
> [options] [args]
> st: error: no such option: -o
>
> Maybe for clarity we could change the Usage to read "st [connection options]
> <command> [options] [args]
How about
Maybe for clarity we could change the Usage to read "st [options] <command> [command options] [args]
gholt (gholt) wrote : | # |
But what are "connection options"? I'll just change it to st <command> [options] [args] and let the advanced user know better.
Chuck Thier (cthier) wrote : | # |
Much cleaner now, thanks!
Preview Diff
1 | === modified file 'bin/st' |
2 | --- bin/st 2010-11-03 22:58:18 +0000 |
3 | +++ bin/st 2010-11-18 21:42:22 +0000 |
4 | @@ -14,807 +14,6 @@ |
5 | # See the License for the specific language governing permissions and |
6 | # limitations under the License. |
7 | |
8 | -try: |
9 | - # Try to use installed swift.common.client... |
10 | - from swift.common.client import get_auth, ClientException, Connection |
11 | -except: |
12 | - # But if not installed, use an included copy. |
13 | - # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # |
14 | - # Inclusion of swift.common.client |
15 | - |
16 | - """ |
17 | - Cloud Files client library used internally |
18 | - """ |
19 | - import socket |
20 | - from cStringIO import StringIO |
21 | - from httplib import HTTPConnection, HTTPException, HTTPSConnection |
22 | - from re import compile, DOTALL |
23 | - from tokenize import generate_tokens, STRING, NAME, OP |
24 | - from urllib import quote as _quote, unquote |
25 | - from urlparse import urlparse, urlunparse |
26 | - |
27 | - try: |
28 | - from eventlet import sleep |
29 | - except: |
30 | - from time import sleep |
31 | - |
32 | - |
33 | - def quote(value, safe='/'): |
34 | - """ |
35 | - Patched version of urllib.quote that encodes utf8 strings before quoting |
36 | - """ |
37 | - if isinstance(value, unicode): |
38 | - value = value.encode('utf8') |
39 | - return _quote(value, safe) |
40 | - |
41 | - |
42 | - # look for a real json parser first |
43 | - try: |
44 | - # simplejson is popular and pretty good |
45 | - from simplejson import loads as json_loads |
46 | - except ImportError: |
47 | - try: |
48 | - # 2.6 will have a json module in the stdlib |
49 | - from json import loads as json_loads |
50 | - except ImportError: |
51 | - # fall back on local parser otherwise |
52 | - comments = compile(r'/\*.*\*/|//[^\r\n]*', DOTALL) |
53 | - |
54 | - def json_loads(string): |
55 | - ''' |
56 | - Fairly competent json parser exploiting the python tokenizer and |
57 | - eval(). -- From python-cloudfiles |
58 | - |
59 | - _loads(serialized_json) -> object |
60 | - ''' |
61 | - try: |
62 | - res = [] |
63 | - consts = {'true': True, 'false': False, 'null': None} |
64 | - string = '(' + comments.sub('', string) + ')' |
65 | - for type, val, _, _, _ in \ |
66 | - generate_tokens(StringIO(string).readline): |
67 | - if (type == OP and val not in '[]{}:,()-') or \ |
68 | - (type == NAME and val not in consts): |
69 | - raise AttributeError() |
70 | - elif type == STRING: |
71 | - res.append('u') |
72 | - res.append(val.replace('\\/', '/')) |
73 | - else: |
74 | - res.append(val) |
75 | - return eval(''.join(res), {}, consts) |
76 | - except: |
77 | - raise AttributeError() |
78 | - |
79 | - |
80 | - class ClientException(Exception): |
81 | - |
82 | - def __init__(self, msg, http_scheme='', http_host='', http_port='', |
83 | - http_path='', http_query='', http_status=0, http_reason='', |
84 | - http_device=''): |
85 | - Exception.__init__(self, msg) |
86 | - self.msg = msg |
87 | - self.http_scheme = http_scheme |
88 | - self.http_host = http_host |
89 | - self.http_port = http_port |
90 | - self.http_path = http_path |
91 | - self.http_query = http_query |
92 | - self.http_status = http_status |
93 | - self.http_reason = http_reason |
94 | - self.http_device = http_device |
95 | - |
96 | - def __str__(self): |
97 | - a = self.msg |
98 | - b = '' |
99 | - if self.http_scheme: |
100 | - b += '%s://' % self.http_scheme |
101 | - if self.http_host: |
102 | - b += self.http_host |
103 | - if self.http_port: |
104 | - b += ':%s' % self.http_port |
105 | - if self.http_path: |
106 | - b += self.http_path |
107 | - if self.http_query: |
108 | - b += '?%s' % self.http_query |
109 | - if self.http_status: |
110 | - if b: |
111 | - b = '%s %s' % (b, self.http_status) |
112 | - else: |
113 | - b = str(self.http_status) |
114 | - if self.http_reason: |
115 | - if b: |
116 | - b = '%s %s' % (b, self.http_reason) |
117 | - else: |
118 | - b = '- %s' % self.http_reason |
119 | - if self.http_device: |
120 | - if b: |
121 | - b = '%s: device %s' % (b, self.http_device) |
122 | - else: |
123 | - b = 'device %s' % self.http_device |
124 | - return b and '%s: %s' % (a, b) or a |
125 | - |
126 | - |
127 | - def http_connection(url): |
128 | - """ |
129 | - Make an HTTPConnection or HTTPSConnection |
130 | - |
131 | - :param url: url to connect to |
132 | - :returns: tuple of (parsed url, connection object) |
133 | - :raises ClientException: Unable to handle protocol scheme |
134 | - """ |
135 | - parsed = urlparse(url) |
136 | - if parsed.scheme == 'http': |
137 | - conn = HTTPConnection(parsed.netloc) |
138 | - elif parsed.scheme == 'https': |
139 | - conn = HTTPSConnection(parsed.netloc) |
140 | - else: |
141 | - raise ClientException('Cannot handle protocol scheme %s for url %s' % |
142 | - (parsed.scheme, repr(url))) |
143 | - return parsed, conn |
144 | - |
145 | - |
146 | - def get_auth(url, user, key, snet=False): |
147 | - """ |
148 | - Get authentication/authorization credentials. |
149 | - |
150 | - The snet parameter is used for Rackspace's ServiceNet internal network |
151 | - implementation. In this function, it simply adds *snet-* to the beginning |
152 | - of the host name for the returned storage URL. With Rackspace Cloud Files, |
153 | - use of this network path causes no bandwidth charges but requires the |
154 | - client to be running on Rackspace's ServiceNet network. |
155 | - |
156 | - :param url: authentication/authorization URL |
157 | - :param user: user to authenticate as |
158 | - :param key: key or password for authorization |
159 | - :param snet: use SERVICENET internal network (see above), default is False |
160 | - :returns: tuple of (storage URL, auth token) |
161 | - :raises ClientException: HTTP GET request to auth URL failed |
162 | - """ |
163 | - parsed, conn = http_connection(url) |
164 | - conn.request('GET', parsed.path, '', |
165 | - {'X-Auth-User': user, 'X-Auth-Key': key}) |
166 | - resp = conn.getresponse() |
167 | - resp.read() |
168 | - if resp.status < 200 or resp.status >= 300: |
169 | - raise ClientException('Auth GET failed', http_scheme=parsed.scheme, |
170 | - http_host=conn.host, http_port=conn.port, |
171 | - http_path=parsed.path, http_status=resp.status, |
172 | - http_reason=resp.reason) |
173 | - url = resp.getheader('x-storage-url') |
174 | - if snet: |
175 | - parsed = list(urlparse(url)) |
176 | - # Second item in the list is the netloc |
177 | - parsed[1] = 'snet-' + parsed[1] |
178 | - url = urlunparse(parsed) |
179 | - return url, resp.getheader('x-storage-token', |
180 | - resp.getheader('x-auth-token')) |
181 | - |
182 | - |
183 | - def get_account(url, token, marker=None, limit=None, prefix=None, |
184 | - http_conn=None, full_listing=False): |
185 | - """ |
186 | - Get a listing of containers for the account. |
187 | - |
188 | - :param url: storage URL |
189 | - :param token: auth token |
190 | - :param marker: marker query |
191 | - :param limit: limit query |
192 | - :param prefix: prefix query |
193 | - :param http_conn: HTTP connection object (If None, it will create the |
194 | - conn object) |
195 | - :param full_listing: if True, return a full listing, else returns a max |
196 | - of 10000 listings |
197 | - :returns: a tuple of (response headers, a list of containers) The response |
198 | - headers will be a dict and all header names will be lowercase. |
199 | - :raises ClientException: HTTP GET request failed |
200 | - """ |
201 | - if not http_conn: |
202 | - http_conn = http_connection(url) |
203 | - if full_listing: |
204 | - rv = get_account(url, token, marker, limit, prefix, http_conn) |
205 | - listing = rv[1] |
206 | - while listing: |
207 | - marker = listing[-1]['name'] |
208 | - listing = \ |
209 | - get_account(url, token, marker, limit, prefix, http_conn)[1] |
210 | - if listing: |
211 | - rv.extend(listing) |
212 | - return rv |
213 | - parsed, conn = http_conn |
214 | - qs = 'format=json' |
215 | - if marker: |
216 | - qs += '&marker=%s' % quote(marker) |
217 | - if limit: |
218 | - qs += '&limit=%d' % limit |
219 | - if prefix: |
220 | - qs += '&prefix=%s' % quote(prefix) |
221 | - conn.request('GET', '%s?%s' % (parsed.path, qs), '', |
222 | - {'X-Auth-Token': token}) |
223 | - resp = conn.getresponse() |
224 | - resp_headers = {} |
225 | - for header, value in resp.getheaders(): |
226 | - resp_headers[header.lower()] = value |
227 | - if resp.status < 200 or resp.status >= 300: |
228 | - resp.read() |
229 | - raise ClientException('Account GET failed', http_scheme=parsed.scheme, |
230 | - http_host=conn.host, http_port=conn.port, |
231 | - http_path=parsed.path, http_query=qs, http_status=resp.status, |
232 | - http_reason=resp.reason) |
233 | - if resp.status == 204: |
234 | - resp.read() |
235 | - return resp_headers, [] |
236 | - return resp_headers, json_loads(resp.read()) |
237 | - |
238 | - |
239 | - def head_account(url, token, http_conn=None): |
240 | - """ |
241 | - Get account stats. |
242 | - |
243 | - :param url: storage URL |
244 | - :param token: auth token |
245 | - :param http_conn: HTTP connection object (If None, it will create the |
246 | - conn object) |
247 | - :returns: a dict containing the response's headers (all header names will |
248 | - be lowercase) |
249 | - :raises ClientException: HTTP HEAD request failed |
250 | - """ |
251 | - if http_conn: |
252 | - parsed, conn = http_conn |
253 | - else: |
254 | - parsed, conn = http_connection(url) |
255 | - conn.request('HEAD', parsed.path, '', {'X-Auth-Token': token}) |
256 | - resp = conn.getresponse() |
257 | - resp.read() |
258 | - if resp.status < 200 or resp.status >= 300: |
259 | - raise ClientException('Account HEAD failed', http_scheme=parsed.scheme, |
260 | - http_host=conn.host, http_port=conn.port, |
261 | - http_path=parsed.path, http_status=resp.status, |
262 | - http_reason=resp.reason) |
263 | - resp_headers = {} |
264 | - for header, value in resp.getheaders(): |
265 | - resp_headers[header.lower()] = value |
266 | - return resp_headers |
267 | - |
268 | - |
269 | - def post_account(url, token, headers, http_conn=None): |
270 | - """ |
271 | - Update an account's metadata. |
272 | - |
273 | - :param url: storage URL |
274 | - :param token: auth token |
275 | - :param headers: additional headers to include in the request |
276 | - :param http_conn: HTTP connection object (If None, it will create the |
277 | - conn object) |
278 | - :raises ClientException: HTTP POST request failed |
279 | - """ |
280 | - if http_conn: |
281 | - parsed, conn = http_conn |
282 | - else: |
283 | - parsed, conn = http_connection(url) |
284 | - headers['X-Auth-Token'] = token |
285 | - conn.request('POST', parsed.path, '', headers) |
286 | - resp = conn.getresponse() |
287 | - resp.read() |
288 | - if resp.status < 200 or resp.status >= 300: |
289 | - raise ClientException('Account POST failed', |
290 | - http_scheme=parsed.scheme, http_host=conn.host, |
291 | - http_port=conn.port, http_path=path, http_status=resp.status, |
292 | - http_reason=resp.reason) |
293 | - |
294 | - |
295 | - def get_container(url, token, container, marker=None, limit=None, |
296 | - prefix=None, delimiter=None, http_conn=None, |
297 | - full_listing=False): |
298 | - """ |
299 | - Get a listing of objects for the container. |
300 | - |
301 | - :param url: storage URL |
302 | - :param token: auth token |
303 | - :param container: container name to get a listing for |
304 | - :param marker: marker query |
305 | - :param limit: limit query |
306 | - :param prefix: prefix query |
307 | - :param delimeter: string to delimit the queries on |
308 | - :param http_conn: HTTP connection object (If None, it will create the |
309 | - conn object) |
310 | - :param full_listing: if True, return a full listing, else returns a max |
311 | - of 10000 listings |
312 | - :returns: a tuple of (response headers, a list of objects) The response |
313 | - headers will be a dict and all header names will be lowercase. |
314 | - :raises ClientException: HTTP GET request failed |
315 | - """ |
316 | - if not http_conn: |
317 | - http_conn = http_connection(url) |
318 | - if full_listing: |
319 | - rv = get_container(url, token, container, marker, limit, prefix, |
320 | - delimiter, http_conn) |
321 | - listing = rv[1] |
322 | - while listing: |
323 | - if not delimiter: |
324 | - marker = listing[-1]['name'] |
325 | - else: |
326 | - marker = listing[-1].get('name', listing[-1].get('subdir')) |
327 | - listing = get_container(url, token, container, marker, limit, |
328 | - prefix, delimiter, http_conn)[1] |
329 | - if listing: |
330 | - rv[1].extend(listing) |
331 | - return rv |
332 | - parsed, conn = http_conn |
333 | - path = '%s/%s' % (parsed.path, quote(container)) |
334 | - qs = 'format=json' |
335 | - if marker: |
336 | - qs += '&marker=%s' % quote(marker) |
337 | - if limit: |
338 | - qs += '&limit=%d' % limit |
339 | - if prefix: |
340 | - qs += '&prefix=%s' % quote(prefix) |
341 | - if delimiter: |
342 | - qs += '&delimiter=%s' % quote(delimiter) |
343 | - conn.request('GET', '%s?%s' % (path, qs), '', {'X-Auth-Token': token}) |
344 | - resp = conn.getresponse() |
345 | - if resp.status < 200 or resp.status >= 300: |
346 | - resp.read() |
347 | - raise ClientException('Container GET failed', |
348 | - http_scheme=parsed.scheme, http_host=conn.host, |
349 | - http_port=conn.port, http_path=path, http_query=qs, |
350 | - http_status=resp.status, http_reason=resp.reason) |
351 | - resp_headers = {} |
352 | - for header, value in resp.getheaders(): |
353 | - resp_headers[header.lower()] = value |
354 | - if resp.status == 204: |
355 | - resp.read() |
356 | - return resp_headers, [] |
357 | - return resp_headers, json_loads(resp.read()) |
358 | - |
359 | - |
360 | - def head_container(url, token, container, http_conn=None): |
361 | - """ |
362 | - Get container stats. |
363 | - |
364 | - :param url: storage URL |
365 | - :param token: auth token |
366 | - :param container: container name to get stats for |
367 | - :param http_conn: HTTP connection object (If None, it will create the |
368 | - conn object) |
369 | - :returns: a dict containing the response's headers (all header names will |
370 | - be lowercase) |
371 | - :raises ClientException: HTTP HEAD request failed |
372 | - """ |
373 | - if http_conn: |
374 | - parsed, conn = http_conn |
375 | - else: |
376 | - parsed, conn = http_connection(url) |
377 | - path = '%s/%s' % (parsed.path, quote(container)) |
378 | - conn.request('HEAD', path, '', {'X-Auth-Token': token}) |
379 | - resp = conn.getresponse() |
380 | - resp.read() |
381 | - if resp.status < 200 or resp.status >= 300: |
382 | - raise ClientException('Container HEAD failed', |
383 | - http_scheme=parsed.scheme, http_host=conn.host, |
384 | - http_port=conn.port, http_path=path, http_status=resp.status, |
385 | - http_reason=resp.reason) |
386 | - resp_headers = {} |
387 | - for header, value in resp.getheaders(): |
388 | - resp_headers[header.lower()] = value |
389 | - return resp_headers |
390 | - |
391 | - |
392 | - def put_container(url, token, container, headers=None, http_conn=None): |
393 | - """ |
394 | - Create a container |
395 | - |
396 | - :param url: storage URL |
397 | - :param token: auth token |
398 | - :param container: container name to create |
399 | - :param headers: additional headers to include in the request |
400 | - :param http_conn: HTTP connection object (If None, it will create the |
401 | - conn object) |
402 | - :raises ClientException: HTTP PUT request failed |
403 | - """ |
404 | - if http_conn: |
405 | - parsed, conn = http_conn |
406 | - else: |
407 | - parsed, conn = http_connection(url) |
408 | - path = '%s/%s' % (parsed.path, quote(container)) |
409 | - if not headers: |
410 | - headers = {} |
411 | - headers['X-Auth-Token'] = token |
412 | - conn.request('PUT', path, '', headers) |
413 | - resp = conn.getresponse() |
414 | - resp.read() |
415 | - if resp.status < 200 or resp.status >= 300: |
416 | - raise ClientException('Container PUT failed', |
417 | - http_scheme=parsed.scheme, http_host=conn.host, |
418 | - http_port=conn.port, http_path=path, http_status=resp.status, |
419 | - http_reason=resp.reason) |
420 | - |
421 | - |
422 | - def post_container(url, token, container, headers, http_conn=None): |
423 | - """ |
424 | - Update a container's metadata. |
425 | - |
426 | - :param url: storage URL |
427 | - :param token: auth token |
428 | - :param container: container name to update |
429 | - :param headers: additional headers to include in the request |
430 | - :param http_conn: HTTP connection object (If None, it will create the |
431 | - conn object) |
432 | - :raises ClientException: HTTP POST request failed |
433 | - """ |
434 | - if http_conn: |
435 | - parsed, conn = http_conn |
436 | - else: |
437 | - parsed, conn = http_connection(url) |
438 | - path = '%s/%s' % (parsed.path, quote(container)) |
439 | - headers['X-Auth-Token'] = token |
440 | - conn.request('POST', path, '', headers) |
441 | - resp = conn.getresponse() |
442 | - resp.read() |
443 | - if resp.status < 200 or resp.status >= 300: |
444 | - raise ClientException('Container POST failed', |
445 | - http_scheme=parsed.scheme, http_host=conn.host, |
446 | - http_port=conn.port, http_path=path, http_status=resp.status, |
447 | - http_reason=resp.reason) |
448 | - |
449 | - |
450 | - def delete_container(url, token, container, http_conn=None): |
451 | - """ |
452 | - Delete a container |
453 | - |
454 | - :param url: storage URL |
455 | - :param token: auth token |
456 | - :param container: container name to delete |
457 | - :param http_conn: HTTP connection object (If None, it will create the |
458 | - conn object) |
459 | - :raises ClientException: HTTP DELETE request failed |
460 | - """ |
461 | - if http_conn: |
462 | - parsed, conn = http_conn |
463 | - else: |
464 | - parsed, conn = http_connection(url) |
465 | - path = '%s/%s' % (parsed.path, quote(container)) |
466 | - conn.request('DELETE', path, '', {'X-Auth-Token': token}) |
467 | - resp = conn.getresponse() |
468 | - resp.read() |
469 | - if resp.status < 200 or resp.status >= 300: |
470 | - raise ClientException('Container DELETE failed', |
471 | - http_scheme=parsed.scheme, http_host=conn.host, |
472 | - http_port=conn.port, http_path=path, http_status=resp.status, |
473 | - http_reason=resp.reason) |
474 | - |
475 | - |
476 | - def get_object(url, token, container, name, http_conn=None, |
477 | - resp_chunk_size=None): |
478 | - """ |
479 | - Get an object |
480 | - |
481 | - :param url: storage URL |
482 | - :param token: auth token |
483 | - :param container: container name that the object is in |
484 | - :param name: object name to get |
485 | - :param http_conn: HTTP connection object (If None, it will create the |
486 | - conn object) |
487 | - :param resp_chunk_size: if defined, chunk size of data to read. NOTE: If |
488 | - you specify a resp_chunk_size you must fully read |
489 | - the object's contents before making another |
490 | - request. |
491 | - :returns: a tuple of (response headers, the object's contents) The response |
492 | - headers will be a dict and all header names will be lowercase. |
493 | - :raises ClientException: HTTP GET request failed |
494 | - """ |
495 | - if http_conn: |
496 | - parsed, conn = http_conn |
497 | - else: |
498 | - parsed, conn = http_connection(url) |
499 | - path = '%s/%s/%s' % (parsed.path, quote(container), quote(name)) |
500 | - conn.request('GET', path, '', {'X-Auth-Token': token}) |
501 | - resp = conn.getresponse() |
502 | - if resp.status < 200 or resp.status >= 300: |
503 | - resp.read() |
504 | - raise ClientException('Object GET failed', http_scheme=parsed.scheme, |
505 | - http_host=conn.host, http_port=conn.port, http_path=path, |
506 | - http_status=resp.status, http_reason=resp.reason) |
507 | - if resp_chunk_size: |
508 | - |
509 | - def _object_body(): |
510 | - buf = resp.read(resp_chunk_size) |
511 | - while buf: |
512 | - yield buf |
513 | - buf = resp.read(resp_chunk_size) |
514 | - object_body = _object_body() |
515 | - else: |
516 | - object_body = resp.read() |
517 | - resp_headers = {} |
518 | - for header, value in resp.getheaders(): |
519 | - resp_headers[header.lower()] = value |
520 | - return resp_headers, object_body |
521 | - |
522 | - |
523 | - def head_object(url, token, container, name, http_conn=None): |
524 | - """ |
525 | - Get object info |
526 | - |
527 | - :param url: storage URL |
528 | - :param token: auth token |
529 | - :param container: container name that the object is in |
530 | - :param name: object name to get info for |
531 | - :param http_conn: HTTP connection object (If None, it will create the |
532 | - conn object) |
533 | - :returns: a dict containing the response's headers (all header names will |
534 | - be lowercase) |
535 | - :raises ClientException: HTTP HEAD request failed |
536 | - """ |
537 | - if http_conn: |
538 | - parsed, conn = http_conn |
539 | - else: |
540 | - parsed, conn = http_connection(url) |
541 | - path = '%s/%s/%s' % (parsed.path, quote(container), quote(name)) |
542 | - conn.request('HEAD', path, '', {'X-Auth-Token': token}) |
543 | - resp = conn.getresponse() |
544 | - resp.read() |
545 | - if resp.status < 200 or resp.status >= 300: |
546 | - raise ClientException('Object HEAD failed', http_scheme=parsed.scheme, |
547 | - http_host=conn.host, http_port=conn.port, http_path=path, |
548 | - http_status=resp.status, http_reason=resp.reason) |
549 | - resp_headers = {} |
550 | - for header, value in resp.getheaders(): |
551 | - resp_headers[header.lower()] = value |
552 | - return resp_headers |
553 | - |
554 | - |
555 | - def put_object(url, token, container, name, contents, content_length=None, |
556 | - etag=None, chunk_size=65536, content_type=None, headers=None, |
557 | - http_conn=None): |
558 | - """ |
559 | - Put an object |
560 | - |
561 | - :param url: storage URL |
562 | - :param token: auth token |
563 | - :param container: container name that the object is in |
564 | - :param name: object name to put |
565 | - :param contents: a string or a file like object to read object data from |
566 | - :param content_length: value to send as content-length header |
567 | - :param etag: etag of contents |
568 | - :param chunk_size: chunk size of data to write |
569 | - :param content_type: value to send as content-type header |
570 | - :param headers: additional headers to include in the request |
571 | - :param http_conn: HTTP connection object (If None, it will create the |
572 | - conn object) |
573 | - :returns: etag from server response |
574 | - :raises ClientException: HTTP PUT request failed |
575 | - """ |
576 | - if http_conn: |
577 | - parsed, conn = http_conn |
578 | - else: |
579 | - parsed, conn = http_connection(url) |
580 | - path = '%s/%s/%s' % (parsed.path, quote(container), quote(name)) |
581 | - if not headers: |
582 | - headers = {} |
583 | - headers['X-Auth-Token'] = token |
584 | - if etag: |
585 | - headers['ETag'] = etag.strip('"') |
586 | - if content_length is not None: |
587 | - headers['Content-Length'] = str(content_length) |
588 | - if content_type is not None: |
589 | - headers['Content-Type'] = content_type |
590 | - if not contents: |
591 | - headers['Content-Length'] = '0' |
592 | - if hasattr(contents, 'read'): |
593 | - conn.putrequest('PUT', path) |
594 | - for header, value in headers.iteritems(): |
595 | - conn.putheader(header, value) |
596 | - if not content_length: |
597 | - conn.putheader('Transfer-Encoding', 'chunked') |
598 | - conn.endheaders() |
599 | - chunk = contents.read(chunk_size) |
600 | - while chunk: |
601 | - if not content_length: |
602 | - conn.send('%x\r\n%s\r\n' % (len(chunk), chunk)) |
603 | - else: |
604 | - conn.send(chunk) |
605 | - chunk = contents.read(chunk_size) |
606 | - if not content_length: |
607 | - conn.send('0\r\n\r\n') |
608 | - else: |
609 | - conn.request('PUT', path, contents, headers) |
610 | - resp = conn.getresponse() |
611 | - resp.read() |
612 | - if resp.status < 200 or resp.status >= 300: |
613 | - raise ClientException('Object PUT failed', http_scheme=parsed.scheme, |
614 | - http_host=conn.host, http_port=conn.port, http_path=path, |
615 | - http_status=resp.status, http_reason=resp.reason) |
616 | - return resp.getheader('etag').strip('"') |
617 | - |
618 | - |
619 | - def post_object(url, token, container, name, headers, http_conn=None): |
620 | - """ |
621 | - Update object metadata |
622 | - |
623 | - :param url: storage URL |
624 | - :param token: auth token |
625 | - :param container: container name that the object is in |
626 | - :param name: name of the object to update |
627 | - :param headers: additional headers to include in the request |
628 | - :param http_conn: HTTP connection object (If None, it will create the |
629 | - conn object) |
630 | - :raises ClientException: HTTP POST request failed |
631 | - """ |
632 | - if http_conn: |
633 | - parsed, conn = http_conn |
634 | - else: |
635 | - parsed, conn = http_connection(url) |
636 | - path = '%s/%s/%s' % (parsed.path, quote(container), quote(name)) |
637 | - headers['X-Auth-Token'] = token |
638 | - conn.request('POST', path, '', headers) |
639 | - resp = conn.getresponse() |
640 | - resp.read() |
641 | - if resp.status < 200 or resp.status >= 300: |
642 | - raise ClientException('Object POST failed', http_scheme=parsed.scheme, |
643 | - http_host=conn.host, http_port=conn.port, http_path=path, |
644 | - http_status=resp.status, http_reason=resp.reason) |
645 | - |
646 | - |
647 | - def delete_object(url, token, container, name, http_conn=None): |
648 | - """ |
649 | - Delete object |
650 | - |
651 | - :param url: storage URL |
652 | - :param token: auth token |
653 | - :param container: container name that the object is in |
654 | - :param name: object name to delete |
655 | - :param http_conn: HTTP connection object (If None, it will create the |
656 | - conn object) |
657 | - :raises ClientException: HTTP DELETE request failed |
658 | - """ |
659 | - if http_conn: |
660 | - parsed, conn = http_conn |
661 | - else: |
662 | - parsed, conn = http_connection(url) |
663 | - path = '%s/%s/%s' % (parsed.path, quote(container), quote(name)) |
664 | - conn.request('DELETE', path, '', {'X-Auth-Token': token}) |
665 | - resp = conn.getresponse() |
666 | - resp.read() |
667 | - if resp.status < 200 or resp.status >= 300: |
668 | - raise ClientException('Object DELETE failed', |
669 | - http_scheme=parsed.scheme, http_host=conn.host, |
670 | - http_port=conn.port, http_path=path, http_status=resp.status, |
671 | - http_reason=resp.reason) |
672 | - |
673 | - |
674 | - class Connection(object): |
675 | - """Convenience class to make requests that will also retry the request""" |
676 | - |
677 | - def __init__(self, authurl, user, key, retries=5, preauthurl=None, |
678 | - preauthtoken=None, snet=False): |
679 | - """ |
680 | - :param authurl: authenitcation URL |
681 | - :param user: user name to authenticate as |
682 | - :param key: key/password to authenticate with |
683 | - :param retries: Number of times to retry the request before failing |
684 | - :param preauthurl: storage URL (if you have already authenticated) |
685 | - :param preauthtoken: authentication token (if you have already |
686 | - authenticated) |
687 | - :param snet: use SERVICENET internal network default is False |
688 | - """ |
689 | - self.authurl = authurl |
690 | - self.user = user |
691 | - self.key = key |
692 | - self.retries = retries |
693 | - self.http_conn = None |
694 | - self.url = preauthurl |
695 | - self.token = preauthtoken |
696 | - self.attempts = 0 |
697 | - self.snet = snet |
698 | - |
699 | - def get_auth(self): |
700 | - return get_auth(self.authurl, self.user, self.key, snet=self.snet) |
701 | - |
702 | - def http_connection(self): |
703 | - return http_connection(self.url) |
704 | - |
705 | - def _retry(self, func, *args, **kwargs): |
706 | - self.attempts = 0 |
707 | - backoff = 1 |
708 | - while self.attempts <= self.retries: |
709 | - self.attempts += 1 |
710 | - try: |
711 | - if not self.url or not self.token: |
712 | - self.url, self.token = self.get_auth() |
713 | - self.http_conn = None |
714 | - if not self.http_conn: |
715 | - self.http_conn = self.http_connection() |
716 | - kwargs['http_conn'] = self.http_conn |
717 | - rv = func(self.url, self.token, *args, **kwargs) |
718 | - return rv |
719 | - except (socket.error, HTTPException): |
720 | - if self.attempts > self.retries: |
721 | - raise |
722 | - self.http_conn = None |
723 | - except ClientException, err: |
724 | - if self.attempts > self.retries: |
725 | - raise |
726 | - if err.http_status == 401: |
727 | - self.url = self.token = None |
728 | - if self.attempts > 1: |
729 | - raise |
730 | - elif 500 <= err.http_status <= 599: |
731 | - pass |
732 | - else: |
733 | - raise |
734 | - sleep(backoff) |
735 | - backoff *= 2 |
736 | - |
737 | - def head_account(self): |
738 | - """Wrapper for :func:`head_account`""" |
739 | - return self._retry(head_account) |
740 | - |
741 | - def get_account(self, marker=None, limit=None, prefix=None, |
742 | - full_listing=False): |
743 | - """Wrapper for :func:`get_account`""" |
744 | - # TODO(unknown): With full_listing=True this will restart the entire |
745 | - # listing with each retry. Need to make a better version that just |
746 | - # retries where it left off. |
747 | - return self._retry(get_account, marker=marker, limit=limit, |
748 | - prefix=prefix, full_listing=full_listing) |
749 | - |
750 | - def post_account(self, headers): |
751 | - """Wrapper for :func:`post_account`""" |
752 | - return self._retry(post_account, headers) |
753 | - |
754 | - def head_container(self, container): |
755 | - """Wrapper for :func:`head_container`""" |
756 | - return self._retry(head_container, container) |
757 | - |
758 | - def get_container(self, container, marker=None, limit=None, prefix=None, |
759 | - delimiter=None, full_listing=False): |
760 | - """Wrapper for :func:`get_container`""" |
761 | - # TODO(unknown): With full_listing=True this will restart the entire |
762 | - # listing with each retry. Need to make a better version that just |
763 | - # retries where it left off. |
764 | - return self._retry(get_container, container, marker=marker, |
765 | - limit=limit, prefix=prefix, delimiter=delimiter, |
766 | - full_listing=full_listing) |
767 | - |
768 | - def put_container(self, container, headers=None): |
769 | - """Wrapper for :func:`put_container`""" |
770 | - return self._retry(put_container, container, headers=headers) |
771 | - |
772 | - def post_container(self, container, headers): |
773 | - """Wrapper for :func:`post_container`""" |
774 | - return self._retry(post_container, container, headers) |
775 | - |
776 | - def delete_container(self, container): |
777 | - """Wrapper for :func:`delete_container`""" |
778 | - return self._retry(delete_container, container) |
779 | - |
780 | - def head_object(self, container, obj): |
781 | - """Wrapper for :func:`head_object`""" |
782 | - return self._retry(head_object, container, obj) |
783 | - |
784 | - def get_object(self, container, obj, resp_chunk_size=None): |
785 | - """Wrapper for :func:`get_object`""" |
786 | - return self._retry(get_object, container, obj, |
787 | - resp_chunk_size=resp_chunk_size) |
788 | - |
789 | - def put_object(self, container, obj, contents, content_length=None, |
790 | - etag=None, chunk_size=65536, content_type=None, |
791 | - headers=None): |
792 | - """Wrapper for :func:`put_object`""" |
793 | - return self._retry(put_object, container, obj, contents, |
794 | - content_length=content_length, etag=etag, chunk_size=chunk_size, |
795 | - content_type=content_type, headers=headers) |
796 | - |
797 | - def post_object(self, container, obj, headers): |
798 | - """Wrapper for :func:`post_object`""" |
799 | - return self._retry(post_object, container, obj, headers) |
800 | - |
801 | - def delete_object(self, container, obj): |
802 | - """Wrapper for :func:`delete_object`""" |
803 | - return self._retry(delete_object, container, obj) |
804 | - |
805 | - # End inclusion of swift.common.client |
806 | - # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # |
807 | - |
808 | - |
809 | from errno import EEXIST, ENOENT |
810 | from hashlib import md5 |
811 | from optparse import OptionParser |
812 | @@ -826,6 +25,805 @@ |
813 | from time import sleep |
814 | |
815 | |
816 | +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # |
817 | +# Inclusion of swift.common.client for convenience of single file distribution |
818 | + |
819 | +import socket |
820 | +from cStringIO import StringIO |
821 | +from httplib import HTTPException, HTTPSConnection |
822 | +from re import compile, DOTALL |
823 | +from tokenize import generate_tokens, STRING, NAME, OP |
824 | +from urllib import quote as _quote, unquote |
825 | +from urlparse import urlparse, urlunparse |
826 | + |
827 | +try: |
828 | + from eventlet import sleep |
829 | +except: |
830 | + from time import sleep |
831 | + |
832 | +try: |
833 | + from swift.common.bufferedhttp \ |
834 | + import BufferedHTTPConnection as HTTPConnection |
835 | +except: |
836 | + from httplib import HTTPConnection |
837 | + |
838 | + |
839 | +def quote(value, safe='/'): |
840 | + """ |
841 | + Patched version of urllib.quote that encodes utf8 strings before quoting |
842 | + """ |
843 | + if isinstance(value, unicode): |
844 | + value = value.encode('utf8') |
845 | + return _quote(value, safe) |
846 | + |
847 | + |
848 | +# look for a real json parser first |
849 | +try: |
850 | + # simplejson is popular and pretty good |
851 | + from simplejson import loads as json_loads |
852 | +except ImportError: |
853 | + try: |
854 | + # 2.6 will have a json module in the stdlib |
855 | + from json import loads as json_loads |
856 | + except ImportError: |
857 | + # fall back on local parser otherwise |
858 | + comments = compile(r'/\*.*\*/|//[^\r\n]*', DOTALL) |
859 | + |
860 | + def json_loads(string): |
861 | + ''' |
862 | + Fairly competent json parser exploiting the python tokenizer and |
863 | + eval(). -- From python-cloudfiles |
864 | + |
865 | + _loads(serialized_json) -> object |
866 | + ''' |
867 | + try: |
868 | + res = [] |
869 | + consts = {'true': True, 'false': False, 'null': None} |
870 | + string = '(' + comments.sub('', string) + ')' |
871 | + for type, val, _, _, _ in \ |
872 | + generate_tokens(StringIO(string).readline): |
873 | + if (type == OP and val not in '[]{}:,()-') or \ |
874 | + (type == NAME and val not in consts): |
875 | + raise AttributeError() |
876 | + elif type == STRING: |
877 | + res.append('u') |
878 | + res.append(val.replace('\\/', '/')) |
879 | + else: |
880 | + res.append(val) |
881 | + return eval(''.join(res), {}, consts) |
882 | + except: |
883 | + raise AttributeError() |
884 | + |
885 | + |
886 | +class ClientException(Exception): |
887 | + |
888 | + def __init__(self, msg, http_scheme='', http_host='', http_port='', |
889 | + http_path='', http_query='', http_status=0, http_reason='', |
890 | + http_device=''): |
891 | + Exception.__init__(self, msg) |
892 | + self.msg = msg |
893 | + self.http_scheme = http_scheme |
894 | + self.http_host = http_host |
895 | + self.http_port = http_port |
896 | + self.http_path = http_path |
897 | + self.http_query = http_query |
898 | + self.http_status = http_status |
899 | + self.http_reason = http_reason |
900 | + self.http_device = http_device |
901 | + |
902 | + def __str__(self): |
903 | + a = self.msg |
904 | + b = '' |
905 | + if self.http_scheme: |
906 | + b += '%s://' % self.http_scheme |
907 | + if self.http_host: |
908 | + b += self.http_host |
909 | + if self.http_port: |
910 | + b += ':%s' % self.http_port |
911 | + if self.http_path: |
912 | + b += self.http_path |
913 | + if self.http_query: |
914 | + b += '?%s' % self.http_query |
915 | + if self.http_status: |
916 | + if b: |
917 | + b = '%s %s' % (b, self.http_status) |
918 | + else: |
919 | + b = str(self.http_status) |
920 | + if self.http_reason: |
921 | + if b: |
922 | + b = '%s %s' % (b, self.http_reason) |
923 | + else: |
924 | + b = '- %s' % self.http_reason |
925 | + if self.http_device: |
926 | + if b: |
927 | + b = '%s: device %s' % (b, self.http_device) |
928 | + else: |
929 | + b = 'device %s' % self.http_device |
930 | + return b and '%s: %s' % (a, b) or a |
931 | + |
932 | + |
933 | +def http_connection(url): |
934 | + """ |
935 | + Make an HTTPConnection or HTTPSConnection |
936 | + |
937 | + :param url: url to connect to |
938 | + :returns: tuple of (parsed url, connection object) |
939 | + :raises ClientException: Unable to handle protocol scheme |
940 | + """ |
941 | + parsed = urlparse(url) |
942 | + if parsed.scheme == 'http': |
943 | + conn = HTTPConnection(parsed.netloc) |
944 | + elif parsed.scheme == 'https': |
945 | + conn = HTTPSConnection(parsed.netloc) |
946 | + else: |
947 | + raise ClientException('Cannot handle protocol scheme %s for url %s' % |
948 | + (parsed.scheme, repr(url))) |
949 | + return parsed, conn |
950 | + |
951 | + |
952 | +def get_auth(url, user, key, snet=False): |
953 | + """ |
954 | + Get authentication/authorization credentials. |
955 | + |
956 | + The snet parameter is used for Rackspace's ServiceNet internal network |
957 | + implementation. In this function, it simply adds *snet-* to the beginning |
958 | + of the host name for the returned storage URL. With Rackspace Cloud Files, |
959 | + use of this network path causes no bandwidth charges but requires the |
960 | + client to be running on Rackspace's ServiceNet network. |
961 | + |
962 | + :param url: authentication/authorization URL |
963 | + :param user: user to authenticate as |
964 | + :param key: key or password for authorization |
965 | + :param snet: use SERVICENET internal network (see above), default is False |
966 | + :returns: tuple of (storage URL, auth token) |
967 | + :raises ClientException: HTTP GET request to auth URL failed |
968 | + """ |
969 | + parsed, conn = http_connection(url) |
970 | + conn.request('GET', parsed.path, '', |
971 | + {'X-Auth-User': user, 'X-Auth-Key': key}) |
972 | + resp = conn.getresponse() |
973 | + resp.read() |
974 | + if resp.status < 200 or resp.status >= 300: |
975 | + raise ClientException('Auth GET failed', http_scheme=parsed.scheme, |
976 | + http_host=conn.host, http_port=conn.port, |
977 | + http_path=parsed.path, http_status=resp.status, |
978 | + http_reason=resp.reason) |
979 | + url = resp.getheader('x-storage-url') |
980 | + if snet: |
981 | + parsed = list(urlparse(url)) |
982 | + # Second item in the list is the netloc |
983 | + parsed[1] = 'snet-' + parsed[1] |
984 | + url = urlunparse(parsed) |
985 | + return url, resp.getheader('x-storage-token', |
986 | + resp.getheader('x-auth-token')) |
987 | + |
988 | + |
989 | +def get_account(url, token, marker=None, limit=None, prefix=None, |
990 | + http_conn=None, full_listing=False): |
991 | + """ |
992 | + Get a listing of containers for the account. |
993 | + |
994 | + :param url: storage URL |
995 | + :param token: auth token |
996 | + :param marker: marker query |
997 | + :param limit: limit query |
998 | + :param prefix: prefix query |
999 | + :param http_conn: HTTP connection object (If None, it will create the |
1000 | + conn object) |
1001 | + :param full_listing: if True, return a full listing, else returns a max |
1002 | + of 10000 listings |
1003 | + :returns: a tuple of (response headers, a list of containers) The response |
1004 | + headers will be a dict and all header names will be lowercase. |
1005 | + :raises ClientException: HTTP GET request failed |
1006 | + """ |
1007 | + if not http_conn: |
1008 | + http_conn = http_connection(url) |
1009 | + if full_listing: |
1010 | + rv = get_account(url, token, marker, limit, prefix, http_conn) |
1011 | + listing = rv[1] |
1012 | + while listing: |
1013 | + marker = listing[-1]['name'] |
1014 | + listing = \ |
1015 | + get_account(url, token, marker, limit, prefix, http_conn)[1] |
1016 | + if listing: |
1017 | + rv.extend(listing) |
1018 | + return rv |
1019 | + parsed, conn = http_conn |
1020 | + qs = 'format=json' |
1021 | + if marker: |
1022 | + qs += '&marker=%s' % quote(marker) |
1023 | + if limit: |
1024 | + qs += '&limit=%d' % limit |
1025 | + if prefix: |
1026 | + qs += '&prefix=%s' % quote(prefix) |
1027 | + conn.request('GET', '%s?%s' % (parsed.path, qs), '', |
1028 | + {'X-Auth-Token': token}) |
1029 | + resp = conn.getresponse() |
1030 | + resp_headers = {} |
1031 | + for header, value in resp.getheaders(): |
1032 | + resp_headers[header.lower()] = value |
1033 | + if resp.status < 200 or resp.status >= 300: |
1034 | + resp.read() |
1035 | + raise ClientException('Account GET failed', http_scheme=parsed.scheme, |
1036 | + http_host=conn.host, http_port=conn.port, |
1037 | + http_path=parsed.path, http_query=qs, http_status=resp.status, |
1038 | + http_reason=resp.reason) |
1039 | + if resp.status == 204: |
1040 | + resp.read() |
1041 | + return resp_headers, [] |
1042 | + return resp_headers, json_loads(resp.read()) |
1043 | + |
1044 | + |
1045 | +def head_account(url, token, http_conn=None): |
1046 | + """ |
1047 | + Get account stats. |
1048 | + |
1049 | + :param url: storage URL |
1050 | + :param token: auth token |
1051 | + :param http_conn: HTTP connection object (If None, it will create the |
1052 | + conn object) |
1053 | + :returns: a dict containing the response's headers (all header names will |
1054 | + be lowercase) |
1055 | + :raises ClientException: HTTP HEAD request failed |
1056 | + """ |
1057 | + if http_conn: |
1058 | + parsed, conn = http_conn |
1059 | + else: |
1060 | + parsed, conn = http_connection(url) |
1061 | + conn.request('HEAD', parsed.path, '', {'X-Auth-Token': token}) |
1062 | + resp = conn.getresponse() |
1063 | + resp.read() |
1064 | + if resp.status < 200 or resp.status >= 300: |
1065 | + raise ClientException('Account HEAD failed', http_scheme=parsed.scheme, |
1066 | + http_host=conn.host, http_port=conn.port, |
1067 | + http_path=parsed.path, http_status=resp.status, |
1068 | + http_reason=resp.reason) |
1069 | + resp_headers = {} |
1070 | + for header, value in resp.getheaders(): |
1071 | + resp_headers[header.lower()] = value |
1072 | + return resp_headers |
1073 | + |
1074 | + |
1075 | +def post_account(url, token, headers, http_conn=None): |
1076 | + """ |
1077 | + Update an account's metadata. |
1078 | + |
1079 | + :param url: storage URL |
1080 | + :param token: auth token |
1081 | + :param headers: additional headers to include in the request |
1082 | + :param http_conn: HTTP connection object (If None, it will create the |
1083 | + conn object) |
1084 | + :raises ClientException: HTTP POST request failed |
1085 | + """ |
1086 | + if http_conn: |
1087 | + parsed, conn = http_conn |
1088 | + else: |
1089 | + parsed, conn = http_connection(url) |
1090 | + headers['X-Auth-Token'] = token |
1091 | + conn.request('POST', parsed.path, '', headers) |
1092 | + resp = conn.getresponse() |
1093 | + resp.read() |
1094 | + if resp.status < 200 or resp.status >= 300: |
1095 | + raise ClientException('Account POST failed', |
1096 | + http_scheme=parsed.scheme, http_host=conn.host, |
1097 | + http_port=conn.port, http_path=path, http_status=resp.status, |
1098 | + http_reason=resp.reason) |
1099 | + |
1100 | + |
1101 | +def get_container(url, token, container, marker=None, limit=None, |
1102 | + prefix=None, delimiter=None, http_conn=None, |
1103 | + full_listing=False): |
1104 | + """ |
1105 | + Get a listing of objects for the container. |
1106 | + |
1107 | + :param url: storage URL |
1108 | + :param token: auth token |
1109 | + :param container: container name to get a listing for |
1110 | + :param marker: marker query |
1111 | + :param limit: limit query |
1112 | + :param prefix: prefix query |
1113 | + :param delimeter: string to delimit the queries on |
1114 | + :param http_conn: HTTP connection object (If None, it will create the |
1115 | + conn object) |
1116 | + :param full_listing: if True, return a full listing, else returns a max |
1117 | + of 10000 listings |
1118 | + :returns: a tuple of (response headers, a list of objects) The response |
1119 | + headers will be a dict and all header names will be lowercase. |
1120 | + :raises ClientException: HTTP GET request failed |
1121 | + """ |
1122 | + if not http_conn: |
1123 | + http_conn = http_connection(url) |
1124 | + if full_listing: |
1125 | + rv = get_container(url, token, container, marker, limit, prefix, |
1126 | + delimiter, http_conn) |
1127 | + listing = rv[1] |
1128 | + while listing: |
1129 | + if not delimiter: |
1130 | + marker = listing[-1]['name'] |
1131 | + else: |
1132 | + marker = listing[-1].get('name', listing[-1].get('subdir')) |
1133 | + listing = get_container(url, token, container, marker, limit, |
1134 | + prefix, delimiter, http_conn)[1] |
1135 | + if listing: |
1136 | + rv[1].extend(listing) |
1137 | + return rv |
1138 | + parsed, conn = http_conn |
1139 | + path = '%s/%s' % (parsed.path, quote(container)) |
1140 | + qs = 'format=json' |
1141 | + if marker: |
1142 | + qs += '&marker=%s' % quote(marker) |
1143 | + if limit: |
1144 | + qs += '&limit=%d' % limit |
1145 | + if prefix: |
1146 | + qs += '&prefix=%s' % quote(prefix) |
1147 | + if delimiter: |
1148 | + qs += '&delimiter=%s' % quote(delimiter) |
1149 | + conn.request('GET', '%s?%s' % (path, qs), '', {'X-Auth-Token': token}) |
1150 | + resp = conn.getresponse() |
1151 | + if resp.status < 200 or resp.status >= 300: |
1152 | + resp.read() |
1153 | + raise ClientException('Container GET failed', |
1154 | + http_scheme=parsed.scheme, http_host=conn.host, |
1155 | + http_port=conn.port, http_path=path, http_query=qs, |
1156 | + http_status=resp.status, http_reason=resp.reason) |
1157 | + resp_headers = {} |
1158 | + for header, value in resp.getheaders(): |
1159 | + resp_headers[header.lower()] = value |
1160 | + if resp.status == 204: |
1161 | + resp.read() |
1162 | + return resp_headers, [] |
1163 | + return resp_headers, json_loads(resp.read()) |
1164 | + |
1165 | + |
1166 | +def head_container(url, token, container, http_conn=None): |
1167 | + """ |
1168 | + Get container stats. |
1169 | + |
1170 | + :param url: storage URL |
1171 | + :param token: auth token |
1172 | + :param container: container name to get stats for |
1173 | + :param http_conn: HTTP connection object (If None, it will create the |
1174 | + conn object) |
1175 | + :returns: a dict containing the response's headers (all header names will |
1176 | + be lowercase) |
1177 | + :raises ClientException: HTTP HEAD request failed |
1178 | + """ |
1179 | + if http_conn: |
1180 | + parsed, conn = http_conn |
1181 | + else: |
1182 | + parsed, conn = http_connection(url) |
1183 | + path = '%s/%s' % (parsed.path, quote(container)) |
1184 | + conn.request('HEAD', path, '', {'X-Auth-Token': token}) |
1185 | + resp = conn.getresponse() |
1186 | + resp.read() |
1187 | + if resp.status < 200 or resp.status >= 300: |
1188 | + raise ClientException('Container HEAD failed', |
1189 | + http_scheme=parsed.scheme, http_host=conn.host, |
1190 | + http_port=conn.port, http_path=path, http_status=resp.status, |
1191 | + http_reason=resp.reason) |
1192 | + resp_headers = {} |
1193 | + for header, value in resp.getheaders(): |
1194 | + resp_headers[header.lower()] = value |
1195 | + return resp_headers |
1196 | + |
1197 | + |
1198 | +def put_container(url, token, container, headers=None, http_conn=None): |
1199 | + """ |
1200 | + Create a container |
1201 | + |
1202 | + :param url: storage URL |
1203 | + :param token: auth token |
1204 | + :param container: container name to create |
1205 | + :param headers: additional headers to include in the request |
1206 | + :param http_conn: HTTP connection object (If None, it will create the |
1207 | + conn object) |
1208 | + :raises ClientException: HTTP PUT request failed |
1209 | + """ |
1210 | + if http_conn: |
1211 | + parsed, conn = http_conn |
1212 | + else: |
1213 | + parsed, conn = http_connection(url) |
1214 | + path = '%s/%s' % (parsed.path, quote(container)) |
1215 | + if not headers: |
1216 | + headers = {} |
1217 | + headers['X-Auth-Token'] = token |
1218 | + conn.request('PUT', path, '', headers) |
1219 | + resp = conn.getresponse() |
1220 | + resp.read() |
1221 | + if resp.status < 200 or resp.status >= 300: |
1222 | + raise ClientException('Container PUT failed', |
1223 | + http_scheme=parsed.scheme, http_host=conn.host, |
1224 | + http_port=conn.port, http_path=path, http_status=resp.status, |
1225 | + http_reason=resp.reason) |
1226 | + |
1227 | + |
1228 | +def post_container(url, token, container, headers, http_conn=None): |
1229 | + """ |
1230 | + Update a container's metadata. |
1231 | + |
1232 | + :param url: storage URL |
1233 | + :param token: auth token |
1234 | + :param container: container name to update |
1235 | + :param headers: additional headers to include in the request |
1236 | + :param http_conn: HTTP connection object (If None, it will create the |
1237 | + conn object) |
1238 | + :raises ClientException: HTTP POST request failed |
1239 | + """ |
1240 | + if http_conn: |
1241 | + parsed, conn = http_conn |
1242 | + else: |
1243 | + parsed, conn = http_connection(url) |
1244 | + path = '%s/%s' % (parsed.path, quote(container)) |
1245 | + headers['X-Auth-Token'] = token |
1246 | + conn.request('POST', path, '', headers) |
1247 | + resp = conn.getresponse() |
1248 | + resp.read() |
1249 | + if resp.status < 200 or resp.status >= 300: |
1250 | + raise ClientException('Container POST failed', |
1251 | + http_scheme=parsed.scheme, http_host=conn.host, |
1252 | + http_port=conn.port, http_path=path, http_status=resp.status, |
1253 | + http_reason=resp.reason) |
1254 | + |
1255 | + |
1256 | +def delete_container(url, token, container, http_conn=None): |
1257 | + """ |
1258 | + Delete a container |
1259 | + |
1260 | + :param url: storage URL |
1261 | + :param token: auth token |
1262 | + :param container: container name to delete |
1263 | + :param http_conn: HTTP connection object (If None, it will create the |
1264 | + conn object) |
1265 | + :raises ClientException: HTTP DELETE request failed |
1266 | + """ |
1267 | + if http_conn: |
1268 | + parsed, conn = http_conn |
1269 | + else: |
1270 | + parsed, conn = http_connection(url) |
1271 | + path = '%s/%s' % (parsed.path, quote(container)) |
1272 | + conn.request('DELETE', path, '', {'X-Auth-Token': token}) |
1273 | + resp = conn.getresponse() |
1274 | + resp.read() |
1275 | + if resp.status < 200 or resp.status >= 300: |
1276 | + raise ClientException('Container DELETE failed', |
1277 | + http_scheme=parsed.scheme, http_host=conn.host, |
1278 | + http_port=conn.port, http_path=path, http_status=resp.status, |
1279 | + http_reason=resp.reason) |
1280 | + |
1281 | + |
1282 | +def get_object(url, token, container, name, http_conn=None, |
1283 | + resp_chunk_size=None): |
1284 | + """ |
1285 | + Get an object |
1286 | + |
1287 | + :param url: storage URL |
1288 | + :param token: auth token |
1289 | + :param container: container name that the object is in |
1290 | + :param name: object name to get |
1291 | + :param http_conn: HTTP connection object (If None, it will create the |
1292 | + conn object) |
1293 | + :param resp_chunk_size: if defined, chunk size of data to read. NOTE: If |
1294 | + you specify a resp_chunk_size you must fully read |
1295 | + the object's contents before making another |
1296 | + request. |
1297 | + :returns: a tuple of (response headers, the object's contents) The response |
1298 | + headers will be a dict and all header names will be lowercase. |
1299 | + :raises ClientException: HTTP GET request failed |
1300 | + """ |
1301 | + if http_conn: |
1302 | + parsed, conn = http_conn |
1303 | + else: |
1304 | + parsed, conn = http_connection(url) |
1305 | + path = '%s/%s/%s' % (parsed.path, quote(container), quote(name)) |
1306 | + conn.request('GET', path, '', {'X-Auth-Token': token}) |
1307 | + resp = conn.getresponse() |
1308 | + if resp.status < 200 or resp.status >= 300: |
1309 | + resp.read() |
1310 | + raise ClientException('Object GET failed', http_scheme=parsed.scheme, |
1311 | + http_host=conn.host, http_port=conn.port, http_path=path, |
1312 | + http_status=resp.status, http_reason=resp.reason) |
1313 | + if resp_chunk_size: |
1314 | + |
1315 | + def _object_body(): |
1316 | + buf = resp.read(resp_chunk_size) |
1317 | + while buf: |
1318 | + yield buf |
1319 | + buf = resp.read(resp_chunk_size) |
1320 | + object_body = _object_body() |
1321 | + else: |
1322 | + object_body = resp.read() |
1323 | + resp_headers = {} |
1324 | + for header, value in resp.getheaders(): |
1325 | + resp_headers[header.lower()] = value |
1326 | + return resp_headers, object_body |
1327 | + |
1328 | + |
1329 | +def head_object(url, token, container, name, http_conn=None): |
1330 | + """ |
1331 | + Get object info |
1332 | + |
1333 | + :param url: storage URL |
1334 | + :param token: auth token |
1335 | + :param container: container name that the object is in |
1336 | + :param name: object name to get info for |
1337 | + :param http_conn: HTTP connection object (If None, it will create the |
1338 | + conn object) |
1339 | + :returns: a dict containing the response's headers (all header names will |
1340 | + be lowercase) |
1341 | + :raises ClientException: HTTP HEAD request failed |
1342 | + """ |
1343 | + if http_conn: |
1344 | + parsed, conn = http_conn |
1345 | + else: |
1346 | + parsed, conn = http_connection(url) |
1347 | + path = '%s/%s/%s' % (parsed.path, quote(container), quote(name)) |
1348 | + conn.request('HEAD', path, '', {'X-Auth-Token': token}) |
1349 | + resp = conn.getresponse() |
1350 | + resp.read() |
1351 | + if resp.status < 200 or resp.status >= 300: |
1352 | + raise ClientException('Object HEAD failed', http_scheme=parsed.scheme, |
1353 | + http_host=conn.host, http_port=conn.port, http_path=path, |
1354 | + http_status=resp.status, http_reason=resp.reason) |
1355 | + resp_headers = {} |
1356 | + for header, value in resp.getheaders(): |
1357 | + resp_headers[header.lower()] = value |
1358 | + return resp_headers |
1359 | + |
1360 | + |
1361 | +def put_object(url, token, container, name, contents, content_length=None, |
1362 | + etag=None, chunk_size=65536, content_type=None, headers=None, |
1363 | + http_conn=None): |
1364 | + """ |
1365 | + Put an object |
1366 | + |
1367 | + :param url: storage URL |
1368 | + :param token: auth token |
1369 | + :param container: container name that the object is in |
1370 | + :param name: object name to put |
1371 | + :param contents: a string or a file like object to read object data from |
1372 | + :param content_length: value to send as content-length header |
1373 | + :param etag: etag of contents |
1374 | + :param chunk_size: chunk size of data to write |
1375 | + :param content_type: value to send as content-type header |
1376 | + :param headers: additional headers to include in the request |
1377 | + :param http_conn: HTTP connection object (If None, it will create the |
1378 | + conn object) |
1379 | + :returns: etag from server response |
1380 | + :raises ClientException: HTTP PUT request failed |
1381 | + """ |
1382 | + if http_conn: |
1383 | + parsed, conn = http_conn |
1384 | + else: |
1385 | + parsed, conn = http_connection(url) |
1386 | + path = '%s/%s/%s' % (parsed.path, quote(container), quote(name)) |
1387 | + if not headers: |
1388 | + headers = {} |
1389 | + headers['X-Auth-Token'] = token |
1390 | + if etag: |
1391 | + headers['ETag'] = etag.strip('"') |
1392 | + if content_length is not None: |
1393 | + headers['Content-Length'] = str(content_length) |
1394 | + if content_type is not None: |
1395 | + headers['Content-Type'] = content_type |
1396 | + if not contents: |
1397 | + headers['Content-Length'] = '0' |
1398 | + if hasattr(contents, 'read'): |
1399 | + conn.putrequest('PUT', path) |
1400 | + for header, value in headers.iteritems(): |
1401 | + conn.putheader(header, value) |
1402 | + if not content_length: |
1403 | + conn.putheader('Transfer-Encoding', 'chunked') |
1404 | + conn.endheaders() |
1405 | + chunk = contents.read(chunk_size) |
1406 | + while chunk: |
1407 | + if not content_length: |
1408 | + conn.send('%x\r\n%s\r\n' % (len(chunk), chunk)) |
1409 | + else: |
1410 | + conn.send(chunk) |
1411 | + chunk = contents.read(chunk_size) |
1412 | + if not content_length: |
1413 | + conn.send('0\r\n\r\n') |
1414 | + else: |
1415 | + conn.request('PUT', path, contents, headers) |
1416 | + resp = conn.getresponse() |
1417 | + resp.read() |
1418 | + if resp.status < 200 or resp.status >= 300: |
1419 | + raise ClientException('Object PUT failed', http_scheme=parsed.scheme, |
1420 | + http_host=conn.host, http_port=conn.port, http_path=path, |
1421 | + http_status=resp.status, http_reason=resp.reason) |
1422 | + return resp.getheader('etag').strip('"') |
1423 | + |
1424 | + |
1425 | +def post_object(url, token, container, name, headers, http_conn=None): |
1426 | + """ |
1427 | + Update object metadata |
1428 | + |
1429 | + :param url: storage URL |
1430 | + :param token: auth token |
1431 | + :param container: container name that the object is in |
1432 | + :param name: name of the object to update |
1433 | + :param headers: additional headers to include in the request |
1434 | + :param http_conn: HTTP connection object (If None, it will create the |
1435 | + conn object) |
1436 | + :raises ClientException: HTTP POST request failed |
1437 | + """ |
1438 | + if http_conn: |
1439 | + parsed, conn = http_conn |
1440 | + else: |
1441 | + parsed, conn = http_connection(url) |
1442 | + path = '%s/%s/%s' % (parsed.path, quote(container), quote(name)) |
1443 | + headers['X-Auth-Token'] = token |
1444 | + conn.request('POST', path, '', headers) |
1445 | + resp = conn.getresponse() |
1446 | + resp.read() |
1447 | + if resp.status < 200 or resp.status >= 300: |
1448 | + raise ClientException('Object POST failed', http_scheme=parsed.scheme, |
1449 | + http_host=conn.host, http_port=conn.port, http_path=path, |
1450 | + http_status=resp.status, http_reason=resp.reason) |
1451 | + |
1452 | + |
1453 | +def delete_object(url, token, container, name, http_conn=None): |
1454 | + """ |
1455 | + Delete object |
1456 | + |
1457 | + :param url: storage URL |
1458 | + :param token: auth token |
1459 | + :param container: container name that the object is in |
1460 | + :param name: object name to delete |
1461 | + :param http_conn: HTTP connection object (If None, it will create the |
1462 | + conn object) |
1463 | + :raises ClientException: HTTP DELETE request failed |
1464 | + """ |
1465 | + if http_conn: |
1466 | + parsed, conn = http_conn |
1467 | + else: |
1468 | + parsed, conn = http_connection(url) |
1469 | + path = '%s/%s/%s' % (parsed.path, quote(container), quote(name)) |
1470 | + conn.request('DELETE', path, '', {'X-Auth-Token': token}) |
1471 | + resp = conn.getresponse() |
1472 | + resp.read() |
1473 | + if resp.status < 200 or resp.status >= 300: |
1474 | + raise ClientException('Object DELETE failed', |
1475 | + http_scheme=parsed.scheme, http_host=conn.host, |
1476 | + http_port=conn.port, http_path=path, http_status=resp.status, |
1477 | + http_reason=resp.reason) |
1478 | + |
1479 | + |
1480 | +class Connection(object): |
1481 | + """Convenience class to make requests that will also retry the request""" |
1482 | + |
1483 | + def __init__(self, authurl, user, key, retries=5, preauthurl=None, |
1484 | + preauthtoken=None, snet=False): |
1485 | + """ |
1486 | + :param authurl: authenitcation URL |
1487 | + :param user: user name to authenticate as |
1488 | + :param key: key/password to authenticate with |
1489 | + :param retries: Number of times to retry the request before failing |
1490 | + :param preauthurl: storage URL (if you have already authenticated) |
1491 | + :param preauthtoken: authentication token (if you have already |
1492 | + authenticated) |
1493 | + :param snet: use SERVICENET internal network default is False |
1494 | + """ |
1495 | + self.authurl = authurl |
1496 | + self.user = user |
1497 | + self.key = key |
1498 | + self.retries = retries |
1499 | + self.http_conn = None |
1500 | + self.url = preauthurl |
1501 | + self.token = preauthtoken |
1502 | + self.attempts = 0 |
1503 | + self.snet = snet |
1504 | + |
1505 | + def get_auth(self): |
1506 | + return get_auth(self.authurl, self.user, self.key, snet=self.snet) |
1507 | + |
1508 | + def http_connection(self): |
1509 | + return http_connection(self.url) |
1510 | + |
1511 | + def _retry(self, func, *args, **kwargs): |
1512 | + self.attempts = 0 |
1513 | + backoff = 1 |
1514 | + while self.attempts <= self.retries: |
1515 | + self.attempts += 1 |
1516 | + try: |
1517 | + if not self.url or not self.token: |
1518 | + self.url, self.token = self.get_auth() |
1519 | + self.http_conn = None |
1520 | + if not self.http_conn: |
1521 | + self.http_conn = self.http_connection() |
1522 | + kwargs['http_conn'] = self.http_conn |
1523 | + rv = func(self.url, self.token, *args, **kwargs) |
1524 | + return rv |
1525 | + except (socket.error, HTTPException): |
1526 | + if self.attempts > self.retries: |
1527 | + raise |
1528 | + self.http_conn = None |
1529 | + except ClientException, err: |
1530 | + if self.attempts > self.retries: |
1531 | + raise |
1532 | + if err.http_status == 401: |
1533 | + self.url = self.token = None |
1534 | + if self.attempts > 1: |
1535 | + raise |
1536 | + elif 500 <= err.http_status <= 599: |
1537 | + pass |
1538 | + else: |
1539 | + raise |
1540 | + sleep(backoff) |
1541 | + backoff *= 2 |
1542 | + |
1543 | + def head_account(self): |
1544 | + """Wrapper for :func:`head_account`""" |
1545 | + return self._retry(head_account) |
1546 | + |
1547 | + def get_account(self, marker=None, limit=None, prefix=None, |
1548 | + full_listing=False): |
1549 | + """Wrapper for :func:`get_account`""" |
1550 | + # TODO(unknown): With full_listing=True this will restart the entire |
1551 | + # listing with each retry. Need to make a better version that just |
1552 | + # retries where it left off. |
1553 | + return self._retry(get_account, marker=marker, limit=limit, |
1554 | + prefix=prefix, full_listing=full_listing) |
1555 | + |
1556 | + def post_account(self, headers): |
1557 | + """Wrapper for :func:`post_account`""" |
1558 | + return self._retry(post_account, headers) |
1559 | + |
1560 | + def head_container(self, container): |
1561 | + """Wrapper for :func:`head_container`""" |
1562 | + return self._retry(head_container, container) |
1563 | + |
1564 | + def get_container(self, container, marker=None, limit=None, prefix=None, |
1565 | + delimiter=None, full_listing=False): |
1566 | + """Wrapper for :func:`get_container`""" |
1567 | + # TODO(unknown): With full_listing=True this will restart the entire |
1568 | + # listing with each retry. Need to make a better version that just |
1569 | + # retries where it left off. |
1570 | + return self._retry(get_container, container, marker=marker, |
1571 | + limit=limit, prefix=prefix, delimiter=delimiter, |
1572 | + full_listing=full_listing) |
1573 | + |
1574 | + def put_container(self, container, headers=None): |
1575 | + """Wrapper for :func:`put_container`""" |
1576 | + return self._retry(put_container, container, headers=headers) |
1577 | + |
1578 | + def post_container(self, container, headers): |
1579 | + """Wrapper for :func:`post_container`""" |
1580 | + return self._retry(post_container, container, headers) |
1581 | + |
1582 | + def delete_container(self, container): |
1583 | + """Wrapper for :func:`delete_container`""" |
1584 | + return self._retry(delete_container, container) |
1585 | + |
1586 | + def head_object(self, container, obj): |
1587 | + """Wrapper for :func:`head_object`""" |
1588 | + return self._retry(head_object, container, obj) |
1589 | + |
1590 | + def get_object(self, container, obj, resp_chunk_size=None): |
1591 | + """Wrapper for :func:`get_object`""" |
1592 | + return self._retry(get_object, container, obj, |
1593 | + resp_chunk_size=resp_chunk_size) |
1594 | + |
1595 | + def put_object(self, container, obj, contents, content_length=None, |
1596 | + etag=None, chunk_size=65536, content_type=None, |
1597 | + headers=None): |
1598 | + """Wrapper for :func:`put_object`""" |
1599 | + return self._retry(put_object, container, obj, contents, |
1600 | + content_length=content_length, etag=etag, chunk_size=chunk_size, |
1601 | + content_type=content_type, headers=headers) |
1602 | + |
1603 | + def post_object(self, container, obj, headers): |
1604 | + """Wrapper for :func:`post_object`""" |
1605 | + return self._retry(post_object, container, obj, headers) |
1606 | + |
1607 | + def delete_object(self, container, obj): |
1608 | + """Wrapper for :func:`delete_object`""" |
1609 | + return self._retry(delete_object, container, obj) |
1610 | + |
1611 | +# End inclusion of swift.common.client |
1612 | +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # |
1613 | + |
1614 | + |
1615 | def mkdirs(path): |
1616 | try: |
1617 | makedirs(path) |
1618 | @@ -865,12 +863,21 @@ |
1619 | delete --all OR delete container [object] [object] ... |
1620 | Deletes everything in the account (with --all), or everything in a |
1621 | container, or a list of objects depending on the args given.'''.strip('\n') |
1622 | -def st_delete(options, args): |
1623 | + |
1624 | + |
1625 | +def st_delete(parser, args, print_queue, error_queue): |
1626 | + parser.add_option('-a', '--all', action='store_true', dest='yes_all', |
1627 | + default=False, help='Indicates that you really want to delete ' |
1628 | + 'everything in the account') |
1629 | + (options, args) = parse_args(parser, args) |
1630 | + args = args[1:] |
1631 | if (not args and not options.yes_all) or (args and options.yes_all): |
1632 | - options.error_queue.put('Usage: %s [options] %s' % |
1633 | - (basename(argv[0]), st_delete_help)) |
1634 | + error_queue.put('Usage: %s [options] %s' % |
1635 | + (basename(argv[0]), st_delete_help)) |
1636 | return |
1637 | + |
1638 | object_queue = Queue(10000) |
1639 | + |
1640 | def _delete_object((container, obj), conn): |
1641 | try: |
1642 | conn.delete_object(container, obj) |
1643 | @@ -878,13 +885,14 @@ |
1644 | path = options.yes_all and join(container, obj) or obj |
1645 | if path[:1] in ('/', '\\'): |
1646 | path = path[1:] |
1647 | - options.print_queue.put(path) |
1648 | + print_queue.put(path) |
1649 | except ClientException, err: |
1650 | if err.http_status != 404: |
1651 | raise |
1652 | - options.error_queue.put('Object %s not found' % |
1653 | - repr('%s/%s' % (container, obj))) |
1654 | + error_queue.put('Object %s not found' % |
1655 | + repr('%s/%s' % (container, obj))) |
1656 | container_queue = Queue(10000) |
1657 | + |
1658 | def _delete_container(container, conn): |
1659 | try: |
1660 | marker = '' |
1661 | @@ -913,11 +921,12 @@ |
1662 | except ClientException, err: |
1663 | if err.http_status != 404: |
1664 | raise |
1665 | - options.error_queue.put('Container %s not found' % repr(container)) |
1666 | - url, token = get_auth(options.auth, options.user, options.key, snet=options.snet) |
1667 | + error_queue.put('Container %s not found' % repr(container)) |
1668 | + |
1669 | + url, token = get_auth(options.auth, options.user, options.key, |
1670 | + snet=options.snet) |
1671 | create_connection = lambda: Connection(options.auth, options.user, |
1672 | - options.key, preauthurl=url, |
1673 | - preauthtoken=token, snet=options.snet) |
1674 | + options.key, preauthurl=url, preauthtoken=token, snet=options.snet) |
1675 | object_threads = [QueueFunctionThread(object_queue, _delete_object, |
1676 | create_connection()) for _ in xrange(10)] |
1677 | for thread in object_threads: |
1678 | @@ -945,7 +954,7 @@ |
1679 | except ClientException, err: |
1680 | if err.http_status != 404: |
1681 | raise |
1682 | - options.error_queue.put('Account not found') |
1683 | + error_queue.put('Account not found') |
1684 | elif len(args) == 1: |
1685 | conn = create_connection() |
1686 | _delete_container(args[0], conn) |
1687 | @@ -969,23 +978,39 @@ |
1688 | st_download_help = ''' |
1689 | download --all OR download container [object] [object] ... |
1690 | Downloads everything in the account (with --all), or everything in a |
1691 | - container, or a list of objects depending on the args given. Use |
1692 | - the -o [--output] <filename> option to redirect the output to a file |
1693 | - or if "-" then the just redirect to stdout. '''.strip('\n') |
1694 | -def st_download(options, args): |
1695 | + container, or a list of objects depending on the args given. For a single |
1696 | + object download, you may use the -o [--output] <filename> option to |
1697 | + redirect the output to a specific file or if "-" then just redirect to |
1698 | + stdout.'''.strip('\n') |
1699 | + |
1700 | + |
1701 | +def st_download(options, args, print_queue, error_queue): |
1702 | + parser.add_option('-a', '--all', action='store_true', dest='yes_all', |
1703 | + default=False, help='Indicates that you really want to download ' |
1704 | + 'everything in the account') |
1705 | + parser.add_option('-o', '--output', dest='out_file', help='For a single ' |
1706 | + 'file download, stream the output to an alternate location ') |
1707 | + (options, args) = parse_args(parser, args) |
1708 | + args = args[1:] |
1709 | + if options.out_file == '-': |
1710 | + options.verbose = 0 |
1711 | + if options.out_file and len(args) != 2: |
1712 | + exit('-o option only allowed for single file downloads') |
1713 | if (not args and not options.yes_all) or (args and options.yes_all): |
1714 | - options.error_queue.put('Usage: %s [options] %s' % |
1715 | - (basename(argv[0]), st_download_help)) |
1716 | + error_queue.put('Usage: %s [options] %s' % |
1717 | + (basename(argv[0]), st_download_help)) |
1718 | return |
1719 | + |
1720 | object_queue = Queue(10000) |
1721 | + |
1722 | def _download_object(queue_arg, conn): |
1723 | - if len(queue_arg) == 2: |
1724 | + if len(queue_arg) == 2: |
1725 | container, obj = queue_arg |
1726 | out_file = None |
1727 | elif len(queue_arg) == 3: |
1728 | container, obj, out_file = queue_arg |
1729 | else: |
1730 | - raise Exception("Invalid queue_arg length of %s" % len(queue_arg)) |
1731 | + raise Exception("Invalid queue_arg length of %s" % len(queue_arg)) |
1732 | try: |
1733 | headers, body = \ |
1734 | conn.get_object(container, obj, resp_chunk_size=65536) |
1735 | @@ -1015,29 +1040,30 @@ |
1736 | fp = open(path, 'wb') |
1737 | read_length = 0 |
1738 | md5sum = md5() |
1739 | - for chunk in body : |
1740 | + for chunk in body: |
1741 | fp.write(chunk) |
1742 | read_length += len(chunk) |
1743 | md5sum.update(chunk) |
1744 | fp.close() |
1745 | if md5sum.hexdigest() != etag: |
1746 | - options.error_queue.put('%s: md5sum != etag, %s != %s' % |
1747 | - (path, md5sum.hexdigest(), etag)) |
1748 | + error_queue.put('%s: md5sum != etag, %s != %s' % |
1749 | + (path, md5sum.hexdigest(), etag)) |
1750 | if read_length != content_length: |
1751 | - options.error_queue.put( |
1752 | - '%s: read_length != content_length, %d != %d' % |
1753 | - (path, read_length, content_length)) |
1754 | + error_queue.put('%s: read_length != content_length, %d != %d' % |
1755 | + (path, read_length, content_length)) |
1756 | if 'x-object-meta-mtime' in headers and not options.out_file: |
1757 | mtime = float(headers['x-object-meta-mtime']) |
1758 | utime(path, (mtime, mtime)) |
1759 | if options.verbose: |
1760 | - options.print_queue.put(path) |
1761 | + print_queue.put(path) |
1762 | except ClientException, err: |
1763 | if err.http_status != 404: |
1764 | raise |
1765 | - options.error_queue.put('Object %s not found' % |
1766 | - repr('%s/%s' % (container, obj))) |
1767 | + error_queue.put('Object %s not found' % |
1768 | + repr('%s/%s' % (container, obj))) |
1769 | + |
1770 | container_queue = Queue(10000) |
1771 | + |
1772 | def _download_container(container, conn): |
1773 | try: |
1774 | marker = '' |
1775 | @@ -1052,11 +1078,12 @@ |
1776 | except ClientException, err: |
1777 | if err.http_status != 404: |
1778 | raise |
1779 | - options.error_queue.put('Container %s not found' % repr(container)) |
1780 | - url, token = get_auth(options.auth, options.user, options.key, snet=options.snet) |
1781 | + error_queue.put('Container %s not found' % repr(container)) |
1782 | + |
1783 | + url, token = get_auth(options.auth, options.user, options.key, |
1784 | + snet=options.snet) |
1785 | create_connection = lambda: Connection(options.auth, options.user, |
1786 | - options.key, preauthurl=url, |
1787 | - preauthtoken=token, snet=options.snet) |
1788 | + options.key, preauthurl=url, preauthtoken=token, snet=options.snet) |
1789 | object_threads = [QueueFunctionThread(object_queue, _download_object, |
1790 | create_connection()) for _ in xrange(10)] |
1791 | for thread in object_threads: |
1792 | @@ -1080,7 +1107,7 @@ |
1793 | except ClientException, err: |
1794 | if err.http_status != 404: |
1795 | raise |
1796 | - options.error_queue.put('Account not found') |
1797 | + error_queue.put('Account not found') |
1798 | elif len(args) == 1: |
1799 | _download_container(args[0], create_connection()) |
1800 | else: |
1801 | @@ -1112,12 +1139,24 @@ |
1802 | items with the given delimiter (see Cloud Files general documentation for |
1803 | what this means). |
1804 | '''.strip('\n') |
1805 | -def st_list(options, args): |
1806 | + |
1807 | + |
1808 | +def st_list(options, args, print_queue, error_queue): |
1809 | + parser.add_option('-p', '--prefix', dest='prefix', help='Will only list ' |
1810 | + 'items beginning with the prefix') |
1811 | + parser.add_option('-d', '--delimiter', dest='delimiter', help='Will roll ' |
1812 | + 'up items with the given delimiter (see Cloud Files general ' |
1813 | + 'documentation for what this means)') |
1814 | + (options, args) = parse_args(parser, args) |
1815 | + args = args[1:] |
1816 | + if options.delimiter and not args: |
1817 | + exit('-d option only allowed for container listings') |
1818 | if len(args) > 1: |
1819 | - options.error_queue.put('Usage: %s [options] %s' % |
1820 | - (basename(argv[0]), st_list_help)) |
1821 | + error_queue.put('Usage: %s [options] %s' % |
1822 | + (basename(argv[0]), st_list_help)) |
1823 | return |
1824 | - conn = Connection(options.auth, options.user, options.key, snet=options.snet) |
1825 | + conn = Connection(options.auth, options.user, options.key, |
1826 | + snet=options.snet) |
1827 | try: |
1828 | marker = '' |
1829 | while True: |
1830 | @@ -1130,35 +1169,39 @@ |
1831 | if not items: |
1832 | break |
1833 | for item in items: |
1834 | - options.print_queue.put(item.get('name', item.get('subdir'))) |
1835 | + print_queue.put(item.get('name', item.get('subdir'))) |
1836 | marker = items[-1].get('name', items[-1].get('subdir')) |
1837 | except ClientException, err: |
1838 | if err.http_status != 404: |
1839 | raise |
1840 | if not args: |
1841 | - options.error_queue.put('Account not found') |
1842 | + error_queue.put('Account not found') |
1843 | else: |
1844 | - options.error_queue.put('Container %s not found' % repr(args[0])) |
1845 | + error_queue.put('Container %s not found' % repr(args[0])) |
1846 | |
1847 | |
1848 | st_stat_help = ''' |
1849 | stat [container] [object] |
1850 | Displays information for the account, container, or object depending on the |
1851 | args given (if any).'''.strip('\n') |
1852 | -def st_stat(options, args): |
1853 | + |
1854 | + |
1855 | +def st_stat(options, args, print_queue, error_queue): |
1856 | + (options, args) = parse_args(parser, args) |
1857 | + args = args[1:] |
1858 | conn = Connection(options.auth, options.user, options.key) |
1859 | if not args: |
1860 | try: |
1861 | headers = conn.head_account() |
1862 | if options.verbose > 1: |
1863 | - options.print_queue.put(''' |
1864 | + print_queue.put(''' |
1865 | StorageURL: %s |
1866 | Auth Token: %s |
1867 | '''.strip('\n') % (conn.url, conn.token)) |
1868 | container_count = int(headers.get('x-account-container-count', 0)) |
1869 | object_count = int(headers.get('x-account-object-count', 0)) |
1870 | bytes_used = int(headers.get('x-account-bytes-used', 0)) |
1871 | - options.print_queue.put(''' |
1872 | + print_queue.put(''' |
1873 | Account: %s |
1874 | Containers: %d |
1875 | Objects: %d |
1876 | @@ -1166,24 +1209,24 @@ |
1877 | object_count, bytes_used)) |
1878 | for key, value in headers.items(): |
1879 | if key.startswith('x-account-meta-'): |
1880 | - options.print_queue.put('%10s: %s' % ('Meta %s' % |
1881 | + print_queue.put('%10s: %s' % ('Meta %s' % |
1882 | key[len('x-account-meta-'):].title(), value)) |
1883 | for key, value in headers.items(): |
1884 | if not key.startswith('x-account-meta-') and key not in ( |
1885 | 'content-length', 'date', 'x-account-container-count', |
1886 | 'x-account-object-count', 'x-account-bytes-used'): |
1887 | - options.print_queue.put( |
1888 | + print_queue.put( |
1889 | '%10s: %s' % (key.title(), value)) |
1890 | except ClientException, err: |
1891 | if err.http_status != 404: |
1892 | raise |
1893 | - options.error_queue.put('Account not found') |
1894 | + error_queue.put('Account not found') |
1895 | elif len(args) == 1: |
1896 | try: |
1897 | headers = conn.head_container(args[0]) |
1898 | object_count = int(headers.get('x-container-object-count', 0)) |
1899 | bytes_used = int(headers.get('x-container-bytes-used', 0)) |
1900 | - options.print_queue.put(''' |
1901 | + print_queue.put(''' |
1902 | Account: %s |
1903 | Container: %s |
1904 | Objects: %d |
1905 | @@ -1195,23 +1238,23 @@ |
1906 | headers.get('x-container-write', ''))) |
1907 | for key, value in headers.items(): |
1908 | if key.startswith('x-container-meta-'): |
1909 | - options.print_queue.put('%9s: %s' % ('Meta %s' % |
1910 | + print_queue.put('%9s: %s' % ('Meta %s' % |
1911 | key[len('x-container-meta-'):].title(), value)) |
1912 | for key, value in headers.items(): |
1913 | if not key.startswith('x-container-meta-') and key not in ( |
1914 | 'content-length', 'date', 'x-container-object-count', |
1915 | 'x-container-bytes-used', 'x-container-read', |
1916 | 'x-container-write'): |
1917 | - options.print_queue.put( |
1918 | + print_queue.put( |
1919 | '%9s: %s' % (key.title(), value)) |
1920 | except ClientException, err: |
1921 | if err.http_status != 404: |
1922 | raise |
1923 | - options.error_queue.put('Container %s not found' % repr(args[0])) |
1924 | + error_queue.put('Container %s not found' % repr(args[0])) |
1925 | elif len(args) == 2: |
1926 | try: |
1927 | headers = conn.head_object(args[0], args[1]) |
1928 | - options.print_queue.put(''' |
1929 | + print_queue.put(''' |
1930 | Account: %s |
1931 | Container: %s |
1932 | Object: %s |
1933 | @@ -1225,22 +1268,22 @@ |
1934 | headers.get('etag'))) |
1935 | for key, value in headers.items(): |
1936 | if key.startswith('x-object-meta-'): |
1937 | - options.print_queue.put('%14s: %s' % ('Meta %s' % |
1938 | + print_queue.put('%14s: %s' % ('Meta %s' % |
1939 | key[len('x-object-meta-'):].title(), value)) |
1940 | for key, value in headers.items(): |
1941 | if not key.startswith('x-object-meta-') and key not in ( |
1942 | 'content-type', 'content-length', 'last-modified', |
1943 | 'etag', 'date'): |
1944 | - options.print_queue.put( |
1945 | + print_queue.put( |
1946 | '%14s: %s' % (key.title(), value)) |
1947 | except ClientException, err: |
1948 | if err.http_status != 404: |
1949 | raise |
1950 | - options.error_queue.put('Object %s not found' % |
1951 | - repr('%s/%s' % (args[0], args[1]))) |
1952 | + error_queue.put('Object %s not found' % |
1953 | + repr('%s/%s' % (args[0], args[1]))) |
1954 | else: |
1955 | - options.error_queue.put('Usage: %s [options] %s' % |
1956 | - (basename(argv[0]), st_stat_help)) |
1957 | + error_queue.put('Usage: %s [options] %s' % |
1958 | + (basename(argv[0]), st_stat_help)) |
1959 | |
1960 | |
1961 | st_post_help = ''' |
1962 | @@ -1252,7 +1295,22 @@ |
1963 | or --meta option is allowed on all and used to define the user meta data |
1964 | items to set in the form Name:Value. This option can be repeated. Example: |
1965 | post -m Color:Blue -m Size:Large'''.strip('\n') |
1966 | -def st_post(options, args): |
1967 | + |
1968 | + |
1969 | +def st_post(options, args, print_queue, error_queue): |
1970 | + parser.add_option('-r', '--read-acl', dest='read_acl', help='Sets the ' |
1971 | + 'Read ACL for containers. Quick summary of ACL syntax: .r:*, ' |
1972 | + '.r:-.example.com, .r:www.example.com, account1, account2:user2') |
1973 | + parser.add_option('-w', '--write-acl', dest='write_acl', help='Sets the ' |
1974 | + 'Write ACL for containers. Quick summary of ACL syntax: account1, ' |
1975 | + 'account2:user2') |
1976 | + parser.add_option('-m', '--meta', action='append', dest='meta', default=[], |
1977 | + help='Sets a meta data item with the syntax name:value. This option ' |
1978 | + 'may be repeated. Example: -m Color:Blue -m Size:Large') |
1979 | + (options, args) = parse_args(parser, args) |
1980 | + args = args[1:] |
1981 | + if (options.read_acl or options.write_acl) and not args: |
1982 | + exit('-r and -w options only allowed for containers') |
1983 | conn = Connection(options.auth, options.user, options.key) |
1984 | if not args: |
1985 | headers = {} |
1986 | @@ -1265,7 +1323,7 @@ |
1987 | except ClientException, err: |
1988 | if err.http_status != 404: |
1989 | raise |
1990 | - options.error_queue.put('Account not found') |
1991 | + error_queue.put('Account not found') |
1992 | elif len(args) == 1: |
1993 | headers = {} |
1994 | for item in options.meta: |
1995 | @@ -1293,11 +1351,11 @@ |
1996 | except ClientException, err: |
1997 | if err.http_status != 404: |
1998 | raise |
1999 | - options.error_queue.put('Object %s not found' % |
2000 | - repr('%s/%s' % (args[0], args[1]))) |
2001 | + error_queue.put('Object %s not found' % |
2002 | + repr('%s/%s' % (args[0], args[1]))) |
2003 | else: |
2004 | - options.error_queue.put('Usage: %s [options] %s' % |
2005 | - (basename(argv[0]), st_post_help)) |
2006 | + error_queue.put('Usage: %s [options] %s' % |
2007 | + (basename(argv[0]), st_post_help)) |
2008 | |
2009 | |
2010 | st_upload_help = ''' |
2011 | @@ -1305,12 +1363,21 @@ |
2012 | Uploads to the given container the files and directories specified by the |
2013 | remaining args. -c or --changed is an option that will only upload files |
2014 | that have changed since the last upload.'''.strip('\n') |
2015 | -def st_upload(options, args): |
2016 | + |
2017 | + |
2018 | +def st_upload(options, args, print_queue, error_queue): |
2019 | + parser.add_option('-c', '--changed', action='store_true', dest='changed', |
2020 | + default=False, help='Will only upload files that have changed since ' |
2021 | + 'the last upload') |
2022 | + (options, args) = parse_args(parser, args) |
2023 | + args = args[1:] |
2024 | if len(args) < 2: |
2025 | - options.error_queue.put('Usage: %s [options] %s' % |
2026 | - (basename(argv[0]), st_upload_help)) |
2027 | + error_queue.put('Usage: %s [options] %s' % |
2028 | + (basename(argv[0]), st_upload_help)) |
2029 | return |
2030 | + |
2031 | file_queue = Queue(10000) |
2032 | + |
2033 | def _upload_file((path, dir_marker), conn): |
2034 | try: |
2035 | obj = path |
2036 | @@ -1352,11 +1419,12 @@ |
2037 | content_length=getsize(path), |
2038 | headers=put_headers) |
2039 | if options.verbose: |
2040 | - options.print_queue.put(obj) |
2041 | + print_queue.put(obj) |
2042 | except OSError, err: |
2043 | if err.errno != ENOENT: |
2044 | raise |
2045 | - options.error_queue.put('Local file %s not found' % repr(path)) |
2046 | + error_queue.put('Local file %s not found' % repr(path)) |
2047 | + |
2048 | def _upload_dir(path): |
2049 | names = listdir(path) |
2050 | if not names: |
2051 | @@ -1368,10 +1436,11 @@ |
2052 | _upload_dir(subpath) |
2053 | else: |
2054 | file_queue.put((subpath, False)) # dir_marker = False |
2055 | - url, token = get_auth(options.auth, options.user, options.key, snet=options.snet) |
2056 | + |
2057 | + url, token = get_auth(options.auth, options.user, options.key, |
2058 | + snet=options.snet) |
2059 | create_connection = lambda: Connection(options.auth, options.user, |
2060 | - options.key, preauthurl=url, |
2061 | - preauthtoken=token, snet=options.snet) |
2062 | + options.key, preauthurl=url, preauthtoken=token, snet=options.snet) |
2063 | file_threads = [QueueFunctionThread(file_queue, _upload_file, |
2064 | create_connection()) for _ in xrange(10)] |
2065 | for thread in file_threads: |
2066 | @@ -1400,12 +1469,24 @@ |
2067 | except ClientException, err: |
2068 | if err.http_status != 404: |
2069 | raise |
2070 | - options.error_queue.put('Account not found') |
2071 | + error_queue.put('Account not found') |
2072 | + |
2073 | + |
2074 | +def parse_args(parser, args, enforce_requires=True): |
2075 | + if not args: |
2076 | + args = ['-h'] |
2077 | + (options, args) = parser.parse_args(args) |
2078 | + if enforce_requires and \ |
2079 | + not (options.auth and options.user and options.key): |
2080 | + exit(''' |
2081 | +Requires ST_AUTH, ST_USER, and ST_KEY environment variables be set or |
2082 | +overridden with -A, -U, or -K.'''.strip('\n')) |
2083 | + return options, args |
2084 | |
2085 | |
2086 | if __name__ == '__main__': |
2087 | parser = OptionParser(version='%prog 1.0', usage=''' |
2088 | -Usage: %%prog [options] <command> [args] |
2089 | +Usage: %%prog <command> [options] [args] |
2090 | |
2091 | Commands: |
2092 | %(st_stat_help)s |
2093 | @@ -1424,55 +1505,18 @@ |
2094 | default=1, help='Print more info') |
2095 | parser.add_option('-q', '--quiet', action='store_const', dest='verbose', |
2096 | const=0, default=1, help='Suppress status output') |
2097 | - parser.add_option('-a', '--all', action='store_true', dest='yes_all', |
2098 | - default=False, help='Indicate that you really want the ' |
2099 | - 'whole account for commands that require --all in such ' |
2100 | - 'a case') |
2101 | - parser.add_option('-c', '--changed', action='store_true', dest='changed', |
2102 | - default=False, help='For the upload command: will ' |
2103 | - 'only upload files that have changed since the last ' |
2104 | - 'upload') |
2105 | - parser.add_option('-p', '--prefix', dest='prefix', |
2106 | - help='For the list command: will only list items ' |
2107 | - 'beginning with the prefix') |
2108 | - parser.add_option('-d', '--delimiter', dest='delimiter', |
2109 | - help='For the list command on containers: will roll up ' |
2110 | - 'items with the given delimiter (see Cloud Files ' |
2111 | - 'general documentation for what this means).') |
2112 | - parser.add_option('-r', '--read-acl', dest='read_acl', |
2113 | - help='Sets the Read ACL with post container commands. ' |
2114 | - 'Quick summary of ACL syntax: .r:*, .r:-.example.com, ' |
2115 | - '.r:www.example.com, account1, account2:user2') |
2116 | - parser.add_option('-w', '--write-acl', dest='write_acl', |
2117 | - help='Sets the Write ACL with post container commands. ' |
2118 | - 'Quick summary of ACL syntax: account1, account2:user2') |
2119 | - parser.add_option('-m', '--meta', action='append', dest='meta', default=[], |
2120 | - help='Sets a meta data item of the syntax name:value ' |
2121 | - 'for use with post commands. This option may be ' |
2122 | - 'repeated. Example: -m Color:Blue -m Size:Large') |
2123 | parser.add_option('-A', '--auth', dest='auth', |
2124 | + default=environ.get('ST_AUTH'), |
2125 | help='URL for obtaining an auth token') |
2126 | parser.add_option('-U', '--user', dest='user', |
2127 | + default=environ.get('ST_USER'), |
2128 | help='User name for obtaining an auth token') |
2129 | parser.add_option('-K', '--key', dest='key', |
2130 | + default=environ.get('ST_KEY'), |
2131 | help='Key for obtaining an auth token') |
2132 | - parser.add_option('-o', '--output', dest='out_file', |
2133 | - help='For a single file download stream the output other location ') |
2134 | - args = argv[1:] |
2135 | - if not args: |
2136 | - args.append('-h') |
2137 | - (options, args) = parser.parse_args(args) |
2138 | - if options.out_file == '-': |
2139 | - options.verbose = 0 |
2140 | - |
2141 | - required_help = ''' |
2142 | -Requires ST_AUTH, ST_USER, and ST_KEY environment variables be set or |
2143 | -overridden with -A, -U, or -K.'''.strip('\n') |
2144 | - for attr in ('auth', 'user', 'key'): |
2145 | - if not getattr(options, attr, None): |
2146 | - setattr(options, attr, environ.get('ST_%s' % attr.upper())) |
2147 | - if not getattr(options, attr, None): |
2148 | - exit(required_help) |
2149 | + parser.disable_interspersed_args() |
2150 | + (options, args) = parse_args(parser, argv[1:], enforce_requires=False) |
2151 | + parser.enable_interspersed_args() |
2152 | |
2153 | commands = ('delete', 'download', 'list', 'post', 'stat', 'upload') |
2154 | if not args or args[0] not in commands: |
2155 | @@ -1481,30 +1525,36 @@ |
2156 | exit('no such command: %s' % args[0]) |
2157 | exit() |
2158 | |
2159 | - options.print_queue = Queue(10000) |
2160 | + print_queue = Queue(10000) |
2161 | + |
2162 | def _print(item): |
2163 | if isinstance(item, unicode): |
2164 | item = item.encode('utf8') |
2165 | print item |
2166 | - print_thread = QueueFunctionThread(options.print_queue, _print) |
2167 | + |
2168 | + print_thread = QueueFunctionThread(print_queue, _print) |
2169 | print_thread.start() |
2170 | |
2171 | - options.error_queue = Queue(10000) |
2172 | + error_queue = Queue(10000) |
2173 | + |
2174 | def _error(item): |
2175 | if isinstance(item, unicode): |
2176 | item = item.encode('utf8') |
2177 | - print >>stderr, item |
2178 | - error_thread = QueueFunctionThread(options.error_queue, _error) |
2179 | + print >> stderr, item |
2180 | + |
2181 | + error_thread = QueueFunctionThread(error_queue, _error) |
2182 | error_thread.start() |
2183 | |
2184 | try: |
2185 | - globals()['st_%s' % args[0]](options, args[1:]) |
2186 | - while not options.print_queue.empty(): |
2187 | + parser.usage = globals()['st_%s_help' % args[0]] |
2188 | + globals()['st_%s' % args[0]](parser, argv[1:], print_queue, |
2189 | + error_queue) |
2190 | + while not print_queue.empty(): |
2191 | sleep(0.01) |
2192 | print_thread.abort = True |
2193 | while print_thread.isAlive(): |
2194 | print_thread.join(0.01) |
2195 | - while not options.error_queue.empty(): |
2196 | + while not error_queue.empty(): |
2197 | sleep(0.01) |
2198 | error_thread.abort = True |
2199 | while error_thread.isAlive(): |
2200 | |
2201 | === modified file 'swift/common/client.py' |
2202 | --- swift/common/client.py 2010-10-01 19:46:35 +0000 |
2203 | +++ swift/common/client.py 2010-11-18 21:42:22 +0000 |
2204 | @@ -29,8 +29,12 @@ |
2205 | except: |
2206 | from time import sleep |
2207 | |
2208 | -from swift.common.bufferedhttp \ |
2209 | - import BufferedHTTPConnection as HTTPConnection |
2210 | +try: |
2211 | + from swift.common.bufferedhttp \ |
2212 | + import BufferedHTTPConnection as HTTPConnection |
2213 | +except: |
2214 | + from httplib import HTTPConnection |
2215 | + |
2216 | |
2217 | def quote(value, safe='/'): |
2218 | """ |
looks good and works great!