client library: fix double content-type header
[pithos] / snf-pithos-tools / pithos / tools / sh.py
1 #!/usr/bin/env python
2
3 # Copyright 2011-2012 GRNET S.A. All rights reserved.
4
5 # Redistribution and use in source and binary forms, with or
6 # without modification, are permitted provided that the following
7 # conditions are met:
8
9 #   1. Redistributions of source code must retain the above
10 #      copyright notice, this list of conditions and the following
11 #      disclaimer.
12
13 #   2. Redistributions in binary form must reproduce the above
14 #      copyright notice, this list of conditions and the following
15 #      disclaimer in the documentation and/or other materials
16 #      provided with the distribution.
17
18 # THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
19 # OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
21 # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
22 # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
25 # USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
26 # AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
27 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
28 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29 # POSSIBILITY OF SUCH DAMAGE.
30
31 # The views and conclusions contained in the software and
32 # documentation are those of the authors and should not be
33 # interpreted as representing official policies, either expressed
34 # or implied, of GRNET S.A.
35
36 from getpass import getuser
37 from optparse import OptionParser
38 from os import environ
39 from sys import argv, exit, stdin, stdout
40 from datetime import datetime
41
42 from pithos.tools.lib.client import Pithos_Client, Fault
43 from pithos.tools.lib.util import get_user, get_auth, get_url
44 from pithos.tools.lib.transfer import upload, download
45
46 import json
47 import logging
48 import types
49 import re
50 import time as _time
51 import os
52
53 _cli_commands = {}
54
55 def cli_command(*args):
56     def decorator(cls):
57         cls.commands = args
58         for name in args:
59             _cli_commands[name] = cls
60         return cls
61     return decorator
62
63 def class_for_cli_command(name):
64     return _cli_commands[name]
65
66 class Command(object):
67     syntax = ''
68     
69     def __init__(self, name, argv):
70         parser = OptionParser('%%prog %s [options] %s' % (name, self.syntax))
71         parser.add_option('--url', dest='url', metavar='URL',
72                           default=get_url(), help='server URL (currently: %s)' % get_url())
73         parser.add_option('--user', dest='user', metavar='USER',
74                           default=get_user(),
75                           help='account USER (currently: %s)' % get_user())
76         parser.add_option('--token', dest='token', metavar='TOKEN',
77                           default=get_auth(),
78                           help='account TOKEN (currently: %s)' % get_auth())
79         parser.add_option('-v', action='store_true', dest='verbose',
80                           default=False, help='verbose output')
81         parser.add_option('-d', action='store_true', dest='debug',
82                           default=False, help='debug output')
83         self.add_options(parser)
84         options, args = parser.parse_args(argv)
85         
86         # Add options to self
87         for opt in parser.option_list:
88             key = opt.dest
89             if key:
90                 val = getattr(options, key)
91                 setattr(self, key, val)
92         
93         self.client = Pithos_Client(self.url, self.token, self.user, self.verbose,
94                              self.debug)
95         
96         self.parser = parser
97         self.args = args
98     
99     def _build_args(self, attrs):
100         args = {}
101         for a in [a for a in attrs if getattr(self, a)]:
102             args[a] = getattr(self, a)
103         return args
104
105     def add_options(self, parser):
106         pass
107     
108     def execute(self, *args):
109         pass
110
111 @cli_command('list', 'ls')
112 class List(Command):
113     syntax = '[<container>[/<object>]]'
114     description = 'list containers or objects'
115     
116     def add_options(self, parser):
117         parser.add_option('-l', action='store_true', dest='detail',
118                           default=False, help='show detailed output')
119         parser.add_option('-n', action='store', type='int', dest='limit',
120                           default=10000, help='show limited output')
121         parser.add_option('--marker', action='store', type='str',
122                           dest='marker', default=None,
123                           help='show output greater then marker')
124         parser.add_option('--prefix', action='store', type='str',
125                           dest='prefix', default=None,
126                           help='show output starting with prefix')
127         parser.add_option('--delimiter', action='store', type='str',
128                           dest='delimiter', default=None,
129                           help='show output up to the delimiter')
130         parser.add_option('--path', action='store', type='str',
131                           dest='path', default=None,
132                           help='show output starting with prefix up to /')
133         parser.add_option('--meta', action='store', type='str',
134                           dest='meta', default=None,
135                           help='show output having the specified meta keys')
136         parser.add_option('--if-modified-since', action='store', type='str',
137                           dest='if_modified_since', default=None,
138                           help='show output if modified since then')
139         parser.add_option('--if-unmodified-since', action='store', type='str',
140                           dest='if_unmodified_since', default=None,
141                           help='show output if not modified since then')
142         parser.add_option('--until', action='store', dest='until',
143                           default=None, help='show metadata until that date')
144         parser.add_option('--format', action='store', dest='format',
145                           default='%d/%m/%Y', help='format to parse until date')
146     
147     def execute(self, container=None):
148         if container:
149             self.list_objects(container)
150         else:
151             self.list_containers()
152     
153     def list_containers(self):
154         attrs = ['limit', 'marker', 'if_modified_since',
155                  'if_unmodified_since']
156         args = self._build_args(attrs)
157         args['format'] = 'json' if self.detail else 'text'
158         
159         if getattr(self, 'until'):
160             t = _time.strptime(self.until, self.format)
161             args['until'] = int(_time.mktime(t))
162         
163         l = self.client.list_containers(**args)
164         print_list(l)
165     
166     def list_objects(self, container):
167         #prepate params
168         params = {}
169         attrs = ['limit', 'marker', 'prefix', 'delimiter', 'path',
170                  'meta', 'if_modified_since', 'if_unmodified_since']
171         args = self._build_args(attrs)
172         args['format'] = 'json' if self.detail else 'text'
173         
174         if self.until:
175             t = _time.strptime(self.until, self.format)
176             args['until'] = int(_time.mktime(t))
177         
178         container, sep, object = container.partition('/')
179         if object:
180             return
181         
182         detail = 'json'
183         #if request with meta quering disable trash filtering
184         show_trashed = True if self.meta else False
185         l = self.client.list_objects(container, **args)
186         print_list(l, detail=self.detail)
187
188 @cli_command('meta')
189 class Meta(Command):
190     syntax = '[<container>[/<object>]]'
191     description = 'get account/container/object metadata'
192     
193     def add_options(self, parser):
194         parser.add_option('-r', action='store_true', dest='restricted',
195                           default=False, help='show only user defined metadata')
196         parser.add_option('--until', action='store', dest='until',
197                           default=None, help='show metadata until that date')
198         parser.add_option('--format', action='store', dest='format',
199                           default='%d/%m/%Y', help='format to parse until date')
200         parser.add_option('--version', action='store', dest='version',
201                           default=None, help='show specific version \
202                                   (applies only for objects)')
203     
204     def execute(self, path=''):
205         container, sep, object = path.partition('/')
206         args = {'restricted': self.restricted}
207         if getattr(self, 'until'):
208             t = _time.strptime(self.until, self.format)
209             args['until'] = int(_time.mktime(t))
210         
211         if object:
212             meta = self.client.retrieve_object_metadata(container, object,
213                                                         self.restricted,
214                                                         self.version)
215         elif container:
216             meta = self.client.retrieve_container_metadata(container, **args)
217         else:
218             meta = self.client.retrieve_account_metadata(**args)
219         if meta == None:
220             print 'Entity does not exist'
221         else:
222             print_dict(meta, header=None)
223
224 @cli_command('create')
225 class CreateContainer(Command):
226     syntax = '<container> [key=val] [...]'
227     description = 'create a container'
228     
229     def add_options(self, parser):
230         parser.add_option('--versioning', action='store', dest='versioning',
231                           default=None, help='set container versioning (auto/none)')
232         parser.add_option('--quota', action='store', dest='quota',
233                           default=None, help='set default container quota')
234     
235     def execute(self, container, *args):
236         meta = {}
237         for arg in args:
238             key, sep, val = arg.partition('=')
239             meta[key] = val
240         policy = {}
241         if getattr(self, 'versioning'):
242             policy['versioning'] = self.versioning
243         if getattr(self, 'quota'):
244             policy['quota'] = self.quota
245         ret = self.client.create_container(container, meta=meta, policies=policy)
246         if not ret:
247             print 'Container already exists'
248
249 @cli_command('delete', 'rm')
250 class Delete(Command):
251     syntax = '<container>[/<object>]'
252     description = 'delete a container or an object'
253     
254     def add_options(self, parser):
255         parser.add_option('--until', action='store', dest='until',
256                           default=None, help='remove history until that date')
257         parser.add_option('--format', action='store', dest='format',
258                           default='%d/%m/%Y', help='format to parse until date')
259     
260     def execute(self, path):
261         container, sep, object = path.partition('/')
262         until = None
263         if getattr(self, 'until'):
264             t = _time.strptime(self.until, self.format)
265             until = int(_time.mktime(t))
266         
267         if object:
268             self.client.delete_object(container, object, until)
269         else:
270             self.client.delete_container(container, until)
271
272 @cli_command('get')
273 class GetObject(Command):
274     syntax = '<container>/<object>'
275     description = 'get the data of an object'
276     
277     def add_options(self, parser):
278         parser.add_option('-l', action='store_true', dest='detail',
279                           default=False, help='show detailed output')
280         parser.add_option('--range', action='store', dest='range',
281                           default=None, help='show range of data')
282         parser.add_option('--if-range', action='store', dest='if_range',
283                           default=None, help='show range of data')
284         parser.add_option('--if-match', action='store', dest='if_match',
285                           default=None, help='show output if ETags match')
286         parser.add_option('--if-none-match', action='store',
287                           dest='if_none_match', default=None,
288                           help='show output if ETags don\'t match')
289         parser.add_option('--if-modified-since', action='store', type='str',
290                           dest='if_modified_since', default=None,
291                           help='show output if modified since then')
292         parser.add_option('--if-unmodified-since', action='store', type='str',
293                           dest='if_unmodified_since', default=None,
294                           help='show output if not modified since then')
295         parser.add_option('-o', action='store', type='str',
296                           dest='file', default=None,
297                           help='save output in file')
298         parser.add_option('--version', action='store', type='str',
299                           dest='version', default=None,
300                           help='get the specific \
301                                version')
302         parser.add_option('--versionlist', action='store_true',
303                           dest='versionlist', default=False,
304                           help='get the full object version list')
305         parser.add_option('--hashmap', action='store_true',
306                           dest='hashmap', default=False,
307                           help='get the object hashmap instead')
308     
309     def execute(self, path):
310         attrs = ['if_match', 'if_none_match', 'if_modified_since',
311                  'if_unmodified_since', 'hashmap']
312         args = self._build_args(attrs)
313         args['format'] = 'json' if self.detail else 'text'
314         if self.range:
315             args['range'] = 'bytes=%s' % self.range
316         if getattr(self, 'if_range'):
317             args['if-range'] = 'If-Range:%s' % getattr(self, 'if_range')
318         
319         container, sep, object = path.partition('/')
320         data = None
321         if self.versionlist:
322             if 'detail' in args.keys():
323                 args.pop('detail')
324             args.pop('format')
325             self.detail = True
326             data = self.client.retrieve_object_versionlist(container, object, **args)
327         elif self.version:
328             data = self.client.retrieve_object_version(container, object,
329                                                        self.version, **args)
330         elif self.hashmap:
331             if 'detail' in args.keys():
332                 args.pop('detail')
333             args.pop('format')
334             self.detail = True
335             data = self.client.retrieve_object_hashmap(container, object, **args)
336         else:
337             data = self.client.retrieve_object(container, object, **args)    
338         
339         f = open(self.file, 'w') if self.file else stdout
340         if self.detail or type(data) == types.DictionaryType:
341             if self.versionlist:
342                 print_versions(data, f=f)
343             else:
344                 print_dict(data, f=f)
345         else:
346             f.write(data)
347         f.close()
348
349 @cli_command('mkdir')
350 class PutMarker(Command):
351     syntax = '<container>/<directory marker>'
352     description = 'create a directory marker'
353     
354     def execute(self, path):
355         container, sep, object = path.partition('/')
356         self.client.create_directory_marker(container, object)
357
358 @cli_command('put')
359 class PutObject(Command):
360     syntax = '<container>/<object> [key=val] [...]'
361     description = 'create/override object'
362     
363     def add_options(self, parser):
364         parser.add_option('--use_hashes', action='store_true', dest='use_hashes',
365                           default=False, help='provide hashmap instead of data')
366         parser.add_option('--chunked', action='store_true', dest='chunked',
367                           default=False, help='set chunked transfer mode')
368         parser.add_option('--etag', action='store', dest='etag',
369                           default=None, help='check written data')
370         parser.add_option('--content-encoding', action='store',
371                           dest='content_encoding', default=None,
372                           help='provide the object MIME content type')
373         parser.add_option('--content-disposition', action='store', type='str',
374                           dest='content_disposition', default=None,
375                           help='provide the presentation style of the object')
376         #parser.add_option('-S', action='store',
377         #                  dest='segment_size', default=False,
378         #                  help='use for large file support')
379         parser.add_option('--manifest', action='store',
380                           dest='x_object_manifest', default=None,
381                           help='provide object parts prefix in <container>/<object> form')
382         parser.add_option('--content-type', action='store',
383                           dest='content_type', default=None,
384                           help='create object with specific content type')
385         parser.add_option('--sharing', action='store',
386                           dest='x_object_sharing', default=None,
387                           help='define sharing object policy')
388         parser.add_option('-f', action='store',
389                           dest='srcpath', default=None,
390                           help='file descriptor to read from (pass - for standard input)')
391         parser.add_option('--public', action='store_true',
392                           dest='x_object_public', default=False,
393                           help='make object publicly accessible')
394     
395     def execute(self, path, *args):
396         if path.find('=') != -1:
397             raise Fault('Missing path argument')
398         
399         #prepare user defined meta
400         meta = {}
401         for arg in args:
402             key, sep, val = arg.partition('=')
403             meta[key] = val
404         
405         attrs = ['etag', 'content_encoding', 'content_disposition',
406                  'content_type', 'x_object_sharing', 'x_object_public']
407         args = self._build_args(attrs)
408         
409         container, sep, object = path.partition('/')
410         
411         f = None
412         if self.srcpath:
413             f = open(self.srcpath) if self.srcpath != '-' else stdin
414         
415         if self.use_hashes and not f:
416             raise Fault('Illegal option combination')
417         
418         if self.chunked:
419             self.client.create_object_using_chunks(container, object, f,
420                                                     meta=meta, **args)
421         elif self.use_hashes:
422             data = f.read()
423             hashmap = json.loads(data)
424             self.client.create_object_by_hashmap(container, object, hashmap,
425                                              meta=meta, **args)
426         elif self.x_object_manifest:
427             self.client.create_manifestation(container, object, self.x_object_manifest)
428         elif not f:
429             self.client.create_zero_length_object(container, object, meta=meta, **args)
430         else:
431             self.client.create_object(container, object, f, meta=meta, **args)
432         if f:
433             f.close()
434
435 @cli_command('copy', 'cp')
436 class CopyObject(Command):
437     syntax = '<src container>/<src object> [<dst container>/]<dst object> [key=val] [...]'
438     description = 'copy an object to a different location'
439     
440     def add_options(self, parser):
441         parser.add_option('--version', action='store',
442                           dest='version', default=False,
443                           help='copy specific version')
444         parser.add_option('--public', action='store_true',
445                           dest='public', default=False,
446                           help='make object publicly accessible')
447         parser.add_option('--content-type', action='store',
448                           dest='content_type', default=None,
449                           help='change object\'s content type')
450     
451     def execute(self, src, dst, *args):
452         src_container, sep, src_object = src.partition('/')
453         dst_container, sep, dst_object = dst.partition('/')
454         
455         #prepare user defined meta
456         meta = {}
457         for arg in args:
458             key, sep, val = arg.partition('=')
459             meta[key] = val
460         
461         if not sep:
462             dst_container = src_container
463             dst_object = dst
464         
465         args = {'content_type':self.content_type} if self.content_type else {}
466         self.client.copy_object(src_container, src_object, dst_container,
467                                 dst_object, meta, self.public, self.version,
468                                 **args)
469
470 @cli_command('set')
471 class SetMeta(Command):
472     syntax = '[<container>[/<object>]] key=val [key=val] [...]'
473     description = 'set account/container/object metadata'
474     
475     def execute(self, path, *args):
476         #in case of account fix the args
477         if path.find('=') != -1:
478             args = list(args)
479             args.append(path)
480             args = tuple(args)
481             path = ''
482         meta = {}
483         for arg in args:
484             key, sep, val = arg.partition('=')
485             meta[key.strip()] = val.strip()
486         container, sep, object = path.partition('/')
487         if object:
488             self.client.update_object_metadata(container, object, **meta)
489         elif container:
490             self.client.update_container_metadata(container, **meta)
491         else:
492             self.client.update_account_metadata(**meta)
493
494 @cli_command('update')
495 class UpdateObject(Command):
496     syntax = '<container>/<object> path [key=val] [...]'
497     description = 'update object metadata/data (default mode: append)'
498     
499     def add_options(self, parser):
500         parser.add_option('-a', action='store_true', dest='append',
501                           default=True, help='append data')
502         parser.add_option('--offset', action='store',
503                           dest='offset',
504                           default=None, help='starting offest to be updated')
505         parser.add_option('--range', action='store', dest='content_range',
506                           default=None, help='range of data to be updated')
507         parser.add_option('--chunked', action='store_true', dest='chunked',
508                           default=False, help='set chunked transfer mode')
509         parser.add_option('--content-encoding', action='store',
510                           dest='content_encoding', default=None,
511                           help='provide the object MIME content type')
512         parser.add_option('--content-disposition', action='store', type='str',
513                           dest='content_disposition', default=None,
514                           help='provide the presentation style of the object')
515         parser.add_option('--manifest', action='store', type='str',
516                           dest='x_object_manifest', default=None,
517                           help='use for large file support')        
518         parser.add_option('--sharing', action='store',
519                           dest='x_object_sharing', default=None,
520                           help='define sharing object policy')
521         parser.add_option('--nosharing', action='store_true',
522                           dest='no_sharing', default=None,
523                           help='clear object sharing policy')
524         parser.add_option('-f', action='store',
525                           dest='srcpath', default=None,
526                           help='file descriptor to read from: pass - for standard input')
527         parser.add_option('--public', action='store_true',
528                           dest='x_object_public', default=False,
529                           help='make object publicly accessible')
530         parser.add_option('--replace', action='store_true',
531                           dest='replace', default=False,
532                           help='override metadata')
533     
534     def execute(self, path, *args):
535         if path.find('=') != -1:
536             raise Fault('Missing path argument')
537         
538         #prepare user defined meta
539         meta = {}
540         for arg in args:
541             key, sep, val = arg.partition('=')
542             meta[key] = val
543         
544         
545         attrs = ['content_encoding', 'content_disposition', 'x_object_sharing',
546                  'x_object_public', 'x_object_manifest', 'replace', 'offset',
547                  'content_range']
548         args = self._build_args(attrs)
549         
550         if self.no_sharing:
551             args['x_object_sharing'] = ''
552         
553         container, sep, object = path.partition('/')
554         
555         f = None
556         if self.srcpath:
557             f = open(self.srcpath) if self.srcpath != '-' else stdin
558         
559         if self.chunked:
560             self.client.update_object_using_chunks(container, object, f,
561                                                     meta=meta, **args)
562         else:
563             self.client.update_object(container, object, f, meta=meta, **args)
564         if f:
565             f.close()
566
567 @cli_command('move', 'mv')
568 class MoveObject(Command):
569     syntax = '<src container>/<src object> [<dst container>/]<dst object>'
570     description = 'move an object to a different location'
571     
572     def add_options(self, parser):
573         parser.add_option('--public', action='store_true',
574                           dest='public', default=False,
575                           help='make object publicly accessible')
576         parser.add_option('--content-type', action='store',
577                           dest='content_type', default=None,
578                           help='change object\'s content type')
579     
580     def execute(self, src, dst, *args):
581         src_container, sep, src_object = src.partition('/')
582         dst_container, sep, dst_object = dst.partition('/')
583         if not sep:
584             dst_container = src_container
585             dst_object = dst
586         
587         #prepare user defined meta
588         meta = {}
589         for arg in args:
590             key, sep, val = arg.partition('=')
591             meta[key] = val
592         
593         args = {'content_type':self.content_type} if self.content_type else {}
594         self.client.move_object(src_container, src_object, dst_container,
595                                 dst_object, meta, self.public, **args)
596
597 @cli_command('unset')
598 class UnsetObject(Command):
599     syntax = '<container>/[<object>] key [key] [...]'
600     description = 'delete metadata info'
601     
602     def execute(self, path, *args):
603         #in case of account fix the args
604         if len(args) == 0:
605             args = list(args)
606             args.append(path)
607             args = tuple(args)
608             path = ''
609         meta = []
610         for key in args:
611             meta.append(key)
612         container, sep, object = path.partition('/')
613         if object:
614             self.client.delete_object_metadata(container, object, meta)
615         elif container:
616             self.client.delete_container_metadata(container, meta)
617         else:
618             self.client.delete_account_metadata(meta)
619
620 @cli_command('group')
621 class CreateGroup(Command):
622     syntax = 'key=val [key=val] [...]'
623     description = 'create account groups'
624     
625     def execute(self, *args):
626         groups = {}
627         for arg in args:
628             key, sep, val = arg.partition('=')
629             groups[key] = val
630         self.client.set_account_groups(**groups)
631
632 @cli_command('ungroup')
633 class DeleteGroup(Command):
634     syntax = 'key [key] [...]'
635     description = 'delete account groups'
636     
637     def execute(self, *args):
638         groups = []
639         for arg in args:
640             groups.append(arg)
641         self.client.unset_account_groups(groups)
642
643 @cli_command('policy')
644 class SetPolicy(Command):
645     syntax = 'container key=val [key=val] [...]'
646     description = 'set container policies'
647     
648     def execute(self, path, *args):
649         if path.find('=') != -1:
650             raise Fault('Missing container argument')
651         
652         container, sep, object = path.partition('/')
653         
654         if object:
655             raise Fault('Only containers have policies')
656         
657         policies = {}
658         for arg in args:
659             key, sep, val = arg.partition('=')
660             policies[key] = val
661         
662         self.client.set_container_policies(container, **policies)
663
664 @cli_command('publish')
665 class PublishObject(Command):
666     syntax = '<container>/<object>'
667     description = 'publish an object'
668     
669     def execute(self, src):
670         src_container, sep, src_object = src.partition('/')
671         
672         self.client.publish_object(src_container, src_object)
673
674 @cli_command('unpublish')
675 class UnpublishObject(Command):
676     syntax = '<container>/<object>'
677     description = 'unpublish an object'
678     
679     def execute(self, src):
680         src_container, sep, src_object = src.partition('/')
681         
682         self.client.unpublish_object(src_container, src_object)
683
684 @cli_command('sharing')
685 class SharingObject(Command):
686     syntax = 'list users sharing objects with the user'
687     description = 'list user accounts sharing objects with the user'
688     
689     def add_options(self, parser):
690         parser.add_option('-l', action='store_true', dest='detail',
691                           default=False, help='show detailed output')
692         parser.add_option('-n', action='store', type='int', dest='limit',
693                           default=10000, help='show limited output')
694         parser.add_option('--marker', action='store', type='str',
695                           dest='marker', default=None,
696                           help='show output greater then marker')
697         
698     
699     def execute(self):
700         attrs = ['limit', 'marker']
701         args = self._build_args(attrs)
702         args['format'] = 'json' if self.detail else 'text'
703         
704         print_list(self.client.list_shared_by_others(**args))
705
706 @cli_command('send')
707 class Send(Command):
708     syntax = '<file> <container>[/<prefix>]'
709     description = 'upload file to container (using prefix)'
710     
711     def execute(self, file, path):
712         container, sep, prefix = path.partition('/')
713         upload(self.client, file, container, prefix)
714
715 @cli_command('receive')
716 class Receive(Command):
717     syntax = '<container>/<object> <file>'
718     description = 'download object to file'
719     
720     def execute(self, path, file):
721         container, sep, object = path.partition('/')
722         download(self.client, container, object, file)
723
724 def print_usage():
725     cmd = Command('', [])
726     parser = cmd.parser
727     parser.usage = '%prog <command> [options]'
728     parser.print_help()
729     
730     commands = []
731     for cls in set(_cli_commands.values()):
732         name = ', '.join(cls.commands)
733         description = getattr(cls, 'description', '')
734         commands.append('  %s %s' % (name.ljust(12), description))
735     print '\nCommands:\n' + '\n'.join(sorted(commands))
736
737 def print_dict(d, header='name', f=stdout, detail=True):
738     header = header if header in d else 'subdir'
739     if header and header in d:
740         f.write('%s\n' %d.pop(header).encode('utf8'))
741     if detail:
742         patterns = ['^x_(account|container|object)_meta_(\w+)$']
743         patterns.append(patterns[0].replace('_', '-'))
744         for key, val in sorted(d.items()):
745             f.write('%s: %s\n' % (key.rjust(30), val))
746
747 def print_list(l, verbose=False, f=stdout, detail=True):
748     for elem in l:
749         #if it's empty string continue
750         if not elem:
751             continue
752         if type(elem) == types.DictionaryType:
753             print_dict(elem, f=f, detail=detail)
754         elif type(elem) == types.StringType:
755             if not verbose:
756                 elem = elem.split('Traceback')[0]
757             f.write('%s\n' % elem)
758         else:
759             f.write('%s\n' % elem)
760
761 def print_versions(data, f=stdout):
762     if 'versions' not in data:
763         f.write('%s\n' %data)
764         return
765     f.write('versions:\n')
766     for id, t in data['versions']:
767         f.write('%s @ %s\n' % (str(id).rjust(30), datetime.fromtimestamp(float(t))))
768
769
770 def main():
771     try:
772         name = argv[1]
773         cls = class_for_cli_command(name)
774     except (IndexError, KeyError):
775         print_usage()
776         exit(1)
777     
778     cmd = cls(name, argv[2:])
779     
780     try:
781         cmd.execute(*cmd.args)
782     except TypeError, e:
783         cmd.parser.print_help()
784         exit(1)
785     except Fault, f:
786         status = '%s ' % f.status if f.status else ''
787         print '%s%s' % (status, f.data)
788
789
790 if __name__ == '__main__':
791     main()