Merge lp:~gholt/swift/stwork into lp:~hudson-openstack/swift/trunk

Proposed by gholt
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
Reviewer Review Type Date Requested Status
Chuck Thier (community) Approve
clayg Approve
Review via email: mp+41214@code.launchpad.net

Description of the change

Cleaned up st command line parsing; always use included client.py as well

To post a comment you must log in.
lp:~gholt/swift/stwork updated
131. By gholt

Fix ST_ environ bug in st

132. By gholt

Keep the first command line parse from exiting due to missing required options

133. By gholt

Cosmetic fixes for some continuation line idents

134. By gholt

Cleaner environ-based option defaults

Revision history for this message
clayg (clay-gerrard) wrote :

looks good and works great!

review: Approve
Revision history for this message
Jay Payne (letterj) wrote :

Awesome work!

When I run:
./st -A http://127.0.0.1:11000/v1.0 -U test:tester -K testing -o - download 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]

Revision history for this message
Jay Payne (letterj) wrote :

> Awesome work!
>
>
> When I run:
> ./st -A http://127.0.0.1:11000/v1.0 -U test:tester -K testing -o - download
> 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]

Revision history for this message
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.

lp:~gholt/swift/stwork updated
135. By gholt

This is to make letterj happy. Well, less unhappy.

136. By gholt

Fallback to HTTPConnection if BufferedHTTPConnection is nbot available

Revision history for this message
Chuck Thier (cthier) wrote :

Much cleaner now, thanks!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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 """