Merge branch 'next'
[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         parser.add_option('--shared', action='store_true', dest='shared',
147                           default=False, help='show only shared')
148         parser.add_option('--public', action='store_true', dest='public',
149                           default=False, help='show only public')
150         
151     
152     def execute(self, container=None):
153         if container:
154             self.list_objects(container)
155         else:
156             self.list_containers()
157     
158     def list_containers(self):
159         attrs = ['limit', 'marker', 'if_modified_since',
160                  'if_unmodified_since', 'shared', 'public']
161         args = self._build_args(attrs)
162         args['format'] = 'json' if self.detail else 'text'
163         
164         if getattr(self, 'until'):
165             t = _time.strptime(self.until, self.format)
166             args['until'] = int(_time.mktime(t))
167         
168         l = self.client.list_containers(**args)
169         print_list(l)
170     
171     def list_objects(self, container):
172         #prepate params
173         params = {}
174         attrs = ['limit', 'marker', 'prefix', 'delimiter', 'path',
175                  'meta', 'if_modified_since', 'if_unmodified_since',
176                  'shared', 'public']
177         args = self._build_args(attrs)
178         args['format'] = 'json' if self.detail else 'text'
179         
180         if self.until:
181             t = _time.strptime(self.until, self.format)
182             args['until'] = int(_time.mktime(t))
183         
184         container, sep, object = container.partition('/')
185         if object:
186             return
187         
188         detail = 'json'
189         #if request with meta quering disable trash filtering
190         show_trashed = True if self.meta else False
191         l = self.client.list_objects(container, **args)
192         print_list(l, detail=self.detail)
193
194 @cli_command('meta')
195 class Meta(Command):
196     syntax = '[<container>[/<object>]]'
197     description = 'get account/container/object metadata'
198     
199     def add_options(self, parser):
200         parser.add_option('-r', action='store_true', dest='restricted',
201                           default=False, help='show only user defined metadata')
202         parser.add_option('--until', action='store', dest='until',
203                           default=None, help='show metadata until that date')
204         parser.add_option('--format', action='store', dest='format',
205                           default='%d/%m/%Y', help='format to parse until date')
206         parser.add_option('--version', action='store', dest='version',
207                           default=None, help='show specific version \
208                                   (applies only for objects)')
209     
210     def execute(self, path=''):
211         container, sep, object = path.partition('/')
212         args = {'restricted': self.restricted}
213         if getattr(self, 'until'):
214             t = _time.strptime(self.until, self.format)
215             args['until'] = int(_time.mktime(t))
216         
217         if object:
218             meta = self.client.retrieve_object_metadata(container, object,
219                                                         self.restricted,
220                                                         self.version)
221         elif container:
222             meta = self.client.retrieve_container_metadata(container, **args)
223         else:
224             meta = self.client.retrieve_account_metadata(**args)
225         if meta == None:
226             print 'Entity does not exist'
227         else:
228             print_dict(meta, header=None)
229
230 @cli_command('create')
231 class CreateContainer(Command):
232     syntax = '<container> [key=val] [...]'
233     description = 'create a container'
234     
235     def add_options(self, parser):
236         parser.add_option('--versioning', action='store', dest='versioning',
237                           default=None, help='set container versioning (auto/none)')
238         parser.add_option('--quota', action='store', dest='quota',
239                           default=None, help='set default container quota')
240     
241     def execute(self, container, *args):
242         meta = {}
243         for arg in args:
244             key, sep, val = arg.partition('=')
245             meta[key] = val
246         policy = {}
247         if getattr(self, 'versioning'):
248             policy['versioning'] = self.versioning
249         if getattr(self, 'quota'):
250             policy['quota'] = self.quota
251         ret = self.client.create_container(container, meta=meta, policies=policy)
252         if not ret:
253             print 'Container already exists'
254
255 @cli_command('delete', 'rm')
256 class Delete(Command):
257     syntax = '<container>[/<object>]'
258     description = 'delete a container or an object'
259     
260     def add_options(self, parser):
261         parser.add_option('--until', action='store', dest='until',
262                           default=None, help='remove history until that date')
263         parser.add_option('--format', action='store', dest='format',
264                           default='%d/%m/%Y', help='format to parse until date')
265         parser.add_option('--delimiter', action='store', type='str',
266                           dest='delimiter', default=None,
267                           help='mass delete objects with path staring with <src object> + delimiter')
268         parser.add_option('-r', action='store_true',
269                           dest='recursive', default=False,
270                           help='mass delimiter objects with delimiter /')
271     
272     def execute(self, path):
273         container, sep, object = path.partition('/')
274         until = None
275         if getattr(self, 'until'):
276             t = _time.strptime(self.until, self.format)
277             until = int(_time.mktime(t))
278         
279         if object:
280             kwargs = {}
281             if self.delimiter:
282                 kwargs['delimiter'] = self.delimiter
283             elif self.recursive:
284                 kwargs['delimiter'] = '/'
285             self.client.delete_object(container, object, until, **kwargs)
286         else:
287             self.client.delete_container(container, until)
288
289 @cli_command('get')
290 class GetObject(Command):
291     syntax = '<container>/<object>'
292     description = 'get the data of an object'
293     
294     def add_options(self, parser):
295         parser.add_option('-l', action='store_true', dest='detail',
296                           default=False, help='show detailed output')
297         parser.add_option('--range', action='store', dest='range',
298                           default=None, help='show range of data')
299         parser.add_option('--if-range', action='store', dest='if_range',
300                           default=None, help='show range of data')
301         parser.add_option('--if-match', action='store', dest='if_match',
302                           default=None, help='show output if ETags match')
303         parser.add_option('--if-none-match', action='store',
304                           dest='if_none_match', default=None,
305                           help='show output if ETags don\'t match')
306         parser.add_option('--if-modified-since', action='store', type='str',
307                           dest='if_modified_since', default=None,
308                           help='show output if modified since then')
309         parser.add_option('--if-unmodified-since', action='store', type='str',
310                           dest='if_unmodified_since', default=None,
311                           help='show output if not modified since then')
312         parser.add_option('-o', action='store', type='str',
313                           dest='file', default=None,
314                           help='save output in file')
315         parser.add_option('--version', action='store', type='str',
316                           dest='version', default=None,
317                           help='get the specific \
318                                version')
319         parser.add_option('--versionlist', action='store_true',
320                           dest='versionlist', default=False,
321                           help='get the full object version list')
322         parser.add_option('--hashmap', action='store_true',
323                           dest='hashmap', default=False,
324                           help='get the object hashmap instead')
325     
326     def execute(self, path):
327         attrs = ['if_match', 'if_none_match', 'if_modified_since',
328                  'if_unmodified_since', 'hashmap']
329         args = self._build_args(attrs)
330         args['format'] = 'json' if self.detail else 'text'
331         if self.range:
332             args['range'] = 'bytes=%s' % self.range
333         if getattr(self, 'if_range'):
334             args['if-range'] = 'If-Range:%s' % getattr(self, 'if_range')
335         
336         container, sep, object = path.partition('/')
337         data = None
338         if self.versionlist:
339             if 'detail' in args.keys():
340                 args.pop('detail')
341             args.pop('format')
342             self.detail = True
343             data = self.client.retrieve_object_versionlist(container, object, **args)
344         elif self.version:
345             data = self.client.retrieve_object_version(container, object,
346                                                        self.version, **args)
347         elif self.hashmap:
348             if 'detail' in args.keys():
349                 args.pop('detail')
350             args.pop('format')
351             self.detail = True
352             data = self.client.retrieve_object_hashmap(container, object, **args)
353         else:
354             data = self.client.retrieve_object(container, object, **args)    
355         
356         f = open(self.file, 'w') if self.file else stdout
357         if self.detail or type(data) == types.DictionaryType:
358             if self.versionlist:
359                 print_versions(data, f=f)
360             else:
361                 print_dict(data, f=f)
362         else:
363             f.write(data)
364         f.close()
365
366 @cli_command('mkdir')
367 class PutMarker(Command):
368     syntax = '<container>/<directory marker>'
369     description = 'create a directory marker'
370     
371     def execute(self, path):
372         container, sep, object = path.partition('/')
373         self.client.create_directory_marker(container, object)
374
375 @cli_command('put')
376 class PutObject(Command):
377     syntax = '<container>/<object> [key=val] [...]'
378     description = 'create/override object'
379     
380     def add_options(self, parser):
381         parser.add_option('--use_hashes', action='store_true', dest='use_hashes',
382                           default=False, help='provide hashmap instead of data')
383         parser.add_option('--chunked', action='store_true', dest='chunked',
384                           default=False, help='set chunked transfer mode')
385         parser.add_option('--etag', action='store', dest='etag',
386                           default=None, help='check written data')
387         parser.add_option('--content-encoding', action='store',
388                           dest='content_encoding', default=None,
389                           help='provide the object MIME content type')
390         parser.add_option('--content-disposition', action='store', type='str',
391                           dest='content_disposition', default=None,
392                           help='provide the presentation style of the object')
393         #parser.add_option('-S', action='store',
394         #                  dest='segment_size', default=False,
395         #                  help='use for large file support')
396         parser.add_option('--manifest', action='store',
397                           dest='x_object_manifest', default=None,
398                           help='provide object parts prefix in <container>/<object> form')
399         parser.add_option('--content-type', action='store',
400                           dest='content_type', default=None,
401                           help='create object with specific content type')
402         parser.add_option('--sharing', action='store',
403                           dest='x_object_sharing', default=None,
404                           help='define sharing object policy')
405         parser.add_option('-f', action='store',
406                           dest='srcpath', default=None,
407                           help='file descriptor to read from (pass - for standard input)')
408         parser.add_option('--public', action='store_true',
409                           dest='x_object_public', default=False,
410                           help='make object publicly accessible')
411     
412     def execute(self, path, *args):
413         if path.find('=') != -1:
414             raise Fault('Missing path argument')
415         
416         #prepare user defined meta
417         meta = {}
418         for arg in args:
419             key, sep, val = arg.partition('=')
420             meta[key] = val
421         
422         attrs = ['etag', 'content_encoding', 'content_disposition',
423                  'content_type', 'x_object_sharing', 'x_object_public']
424         args = self._build_args(attrs)
425         
426         container, sep, object = path.partition('/')
427         
428         f = None
429         if self.srcpath:
430             f = open(self.srcpath) if self.srcpath != '-' else stdin
431         
432         if self.use_hashes and not f:
433             raise Fault('Illegal option combination')
434         
435         if self.chunked:
436             self.client.create_object_using_chunks(container, object, f,
437                                                     meta=meta, **args)
438         elif self.use_hashes:
439             data = f.read()
440             hashmap = json.loads(data)
441             self.client.create_object_by_hashmap(container, object, hashmap,
442                                              meta=meta, **args)
443         elif self.x_object_manifest:
444             self.client.create_manifestation(container, object, self.x_object_manifest)
445         elif not f:
446             self.client.create_zero_length_object(container, object, meta=meta, **args)
447         else:
448             self.client.create_object(container, object, f, meta=meta, **args)
449         if f:
450             f.close()
451
452 @cli_command('copy', 'cp')
453 class CopyObject(Command):
454     syntax = '<src container>/<src object> [<dst container>/]<dst object> [key=val] [...]'
455     description = 'copy an object to a different location'
456     
457     def add_options(self, parser):
458         parser.add_option('--version', action='store',
459                           dest='version', default=False,
460                           help='copy specific version')
461         parser.add_option('--public', action='store_true',
462                           dest='public', default=False,
463                           help='make object publicly accessible')
464         parser.add_option('--content-type', action='store',
465                           dest='content_type', default=None,
466                           help='change object\'s content type')
467         parser.add_option('--delimiter', action='store', type='str',
468                           dest='delimiter', default=None,
469                           help='mass copy objects with path staring with <src object> + delimiter')
470         parser.add_option('-r', action='store_true',
471                           dest='recursive', default=False,
472                           help='mass copy with delimiter /')
473     
474     def execute(self, src, dst, *args):
475         src_container, sep, src_object = src.partition('/')
476         dst_container, sep, dst_object = dst.partition('/')
477         
478         #prepare user defined meta
479         meta = {}
480         for arg in args:
481             key, sep, val = arg.partition('=')
482             meta[key] = val
483         
484         if not sep:
485             dst_container = src_container
486             dst_object = dst
487         
488         args = {'content_type':self.content_type} if self.content_type else {}
489         if self.delimiter:
490                 args['delimiter'] = self.delimiter
491         elif self.recursive:
492                 args['delimiter'] = '/'
493         self.client.copy_object(src_container, src_object, dst_container,
494                                 dst_object, meta, self.public, self.version,
495                                 **args)
496
497 @cli_command('set')
498 class SetMeta(Command):
499     syntax = '[<container>[/<object>]] key=val [key=val] [...]'
500     description = 'set account/container/object metadata'
501     
502     def execute(self, path, *args):
503         #in case of account fix the args
504         if path.find('=') != -1:
505             args = list(args)
506             args.append(path)
507             args = tuple(args)
508             path = ''
509         meta = {}
510         for arg in args:
511             key, sep, val = arg.partition('=')
512             meta[key.strip()] = val.strip()
513         container, sep, object = path.partition('/')
514         if object:
515             self.client.update_object_metadata(container, object, **meta)
516         elif container:
517             self.client.update_container_metadata(container, **meta)
518         else:
519             self.client.update_account_metadata(**meta)
520
521 @cli_command('update')
522 class UpdateObject(Command):
523     syntax = '<container>/<object> path [key=val] [...]'
524     description = 'update object metadata/data (default mode: append)'
525     
526     def add_options(self, parser):
527         parser.add_option('-a', action='store_true', dest='append',
528                           default=True, help='append data')
529         parser.add_option('--offset', action='store',
530                           dest='offset',
531                           default=None, help='starting offest to be updated')
532         parser.add_option('--range', action='store', dest='content_range',
533                           default=None, help='range of data to be updated')
534         parser.add_option('--chunked', action='store_true', dest='chunked',
535                           default=False, help='set chunked transfer mode')
536         parser.add_option('--content-encoding', action='store',
537                           dest='content_encoding', default=None,
538                           help='provide the object MIME content type')
539         parser.add_option('--content-disposition', action='store', type='str',
540                           dest='content_disposition', default=None,
541                           help='provide the presentation style of the object')
542         parser.add_option('--manifest', action='store', type='str',
543                           dest='x_object_manifest', default=None,
544                           help='use for large file support')        
545         parser.add_option('--sharing', action='store',
546                           dest='x_object_sharing', default=None,
547                           help='define sharing object policy')
548         parser.add_option('--nosharing', action='store_true',
549                           dest='no_sharing', default=None,
550                           help='clear object sharing policy')
551         parser.add_option('-f', action='store',
552                           dest='srcpath', default=None,
553                           help='file descriptor to read from: pass - for standard input')
554         parser.add_option('--public', action='store_true',
555                           dest='x_object_public', default=False,
556                           help='make object publicly accessible')
557         parser.add_option('--replace', action='store_true',
558                           dest='replace', default=False,
559                           help='override metadata')
560     
561     def execute(self, path, *args):
562         if path.find('=') != -1:
563             raise Fault('Missing path argument')
564         
565         #prepare user defined meta
566         meta = {}
567         for arg in args:
568             key, sep, val = arg.partition('=')
569             meta[key] = val
570         
571         
572         attrs = ['content_encoding', 'content_disposition', 'x_object_sharing',
573                  'x_object_public', 'x_object_manifest', 'replace', 'offset',
574                  'content_range']
575         args = self._build_args(attrs)
576         
577         if self.no_sharing:
578             args['x_object_sharing'] = ''
579         
580         container, sep, object = path.partition('/')
581         
582         f = None
583         if self.srcpath:
584             f = open(self.srcpath) if self.srcpath != '-' else stdin
585         
586         if self.chunked:
587             self.client.update_object_using_chunks(container, object, f,
588                                                     meta=meta, **args)
589         else:
590             self.client.update_object(container, object, f, meta=meta, **args)
591         if f:
592             f.close()
593
594 @cli_command('move', 'mv')
595 class MoveObject(Command):
596     syntax = '<src container>/<src object> [<dst container>/]<dst object>'
597     description = 'move an object to a different location'
598     
599     def add_options(self, parser):
600         parser.add_option('--public', action='store_true',
601                           dest='public', default=False,
602                           help='make object publicly accessible')
603         parser.add_option('--content-type', action='store',
604                           dest='content_type', default=None,
605                           help='change object\'s content type')
606         parser.add_option('--delimiter', action='store', type='str',
607                           dest='delimiter', default=None,
608                           help='mass move objects with path staring with <src object> + delimiter')
609         parser.add_option('-r', action='store_true',
610                           dest='recursive', default=False,
611                           help='mass move objects with delimiter /')
612     
613     def execute(self, src, dst, *args):
614         src_container, sep, src_object = src.partition('/')
615         dst_container, sep, dst_object = dst.partition('/')
616         if not sep:
617             dst_container = src_container
618             dst_object = dst
619         
620         #prepare user defined meta
621         meta = {}
622         for arg in args:
623             key, sep, val = arg.partition('=')
624             meta[key] = val
625         
626         args = {'content_type':self.content_type} if self.content_type else {}
627         if self.delimiter:
628                 args['delimiter'] = self.delimiter
629         elif self.recursive:
630                 args['delimiter'] = '/'
631         self.client.move_object(src_container, src_object, dst_container,
632                                 dst_object, meta, self.public, **args)
633
634 @cli_command('unset')
635 class UnsetObject(Command):
636     syntax = '<container>/[<object>] key [key] [...]'
637     description = 'delete metadata info'
638     
639     def execute(self, path, *args):
640         #in case of account fix the args
641         if len(args) == 0:
642             args = list(args)
643             args.append(path)
644             args = tuple(args)
645             path = ''
646         meta = []
647         for key in args:
648             meta.append(key)
649         container, sep, object = path.partition('/')
650         if object:
651             self.client.delete_object_metadata(container, object, meta)
652         elif container:
653             self.client.delete_container_metadata(container, meta)
654         else:
655             self.client.delete_account_metadata(meta)
656
657 @cli_command('group')
658 class CreateGroup(Command):
659     syntax = 'key=val [key=val] [...]'
660     description = 'create account groups'
661     
662     def execute(self, *args):
663         groups = {}
664         for arg in args:
665             key, sep, val = arg.partition('=')
666             groups[key] = val
667         self.client.set_account_groups(**groups)
668
669 @cli_command('ungroup')
670 class DeleteGroup(Command):
671     syntax = 'key [key] [...]'
672     description = 'delete account groups'
673     
674     def execute(self, *args):
675         groups = []
676         for arg in args:
677             groups.append(arg)
678         self.client.unset_account_groups(groups)
679
680 @cli_command('policy')
681 class SetPolicy(Command):
682     syntax = 'container key=val [key=val] [...]'
683     description = 'set container policies'
684     
685     def execute(self, path, *args):
686         if path.find('=') != -1:
687             raise Fault('Missing container argument')
688         
689         container, sep, object = path.partition('/')
690         
691         if object:
692             raise Fault('Only containers have policies')
693         
694         policies = {}
695         for arg in args:
696             key, sep, val = arg.partition('=')
697             policies[key] = val
698         
699         self.client.set_container_policies(container, **policies)
700
701 @cli_command('publish')
702 class PublishObject(Command):
703     syntax = '<container>/<object>'
704     description = 'publish an object'
705     
706     def execute(self, src):
707         src_container, sep, src_object = src.partition('/')
708         
709         self.client.publish_object(src_container, src_object)
710
711 @cli_command('unpublish')
712 class UnpublishObject(Command):
713     syntax = '<container>/<object>'
714     description = 'unpublish an object'
715     
716     def execute(self, src):
717         src_container, sep, src_object = src.partition('/')
718         
719         self.client.unpublish_object(src_container, src_object)
720
721 @cli_command('sharing')
722 class SharingObject(Command):
723     syntax = 'list users sharing objects with the user'
724     description = 'list user accounts sharing objects with the user'
725     
726     def add_options(self, parser):
727         parser.add_option('-l', action='store_true', dest='detail',
728                           default=False, help='show detailed output')
729         parser.add_option('-n', action='store', type='int', dest='limit',
730                           default=10000, help='show limited output')
731         parser.add_option('--marker', action='store', type='str',
732                           dest='marker', default=None,
733                           help='show output greater then marker')
734         
735     
736     def execute(self):
737         attrs = ['limit', 'marker']
738         args = self._build_args(attrs)
739         args['format'] = 'json' if self.detail else 'text'
740         
741         print_list(self.client.list_shared_by_others(**args))
742
743 @cli_command('send')
744 class Send(Command):
745     syntax = '<file> <container>[/<prefix>]'
746     description = 'upload file to container (using prefix)'
747     
748     def execute(self, file, path):
749         container, sep, prefix = path.partition('/')
750         upload(self.client, file, container, prefix)
751
752 @cli_command('receive')
753 class Receive(Command):
754     syntax = '<container>/<object> <file>'
755     description = 'download object to file'
756     
757     def execute(self, path, file):
758         container, sep, object = path.partition('/')
759         download(self.client, container, object, file)
760
761 def print_usage():
762     cmd = Command('', [])
763     parser = cmd.parser
764     parser.usage = '%prog <command> [options]'
765     parser.print_help()
766     
767     commands = []
768     for cls in set(_cli_commands.values()):
769         name = ', '.join(cls.commands)
770         description = getattr(cls, 'description', '')
771         commands.append('  %s %s' % (name.ljust(12), description))
772     print '\nCommands:\n' + '\n'.join(sorted(commands))
773
774 def print_dict(d, header='name', f=stdout, detail=True):
775     header = header if header in d else 'subdir'
776     if header and header in d:
777         f.write('%s\n' %d.pop(header).encode('utf8'))
778     if detail:
779         patterns = ['^x_(account|container|object)_meta_(\w+)$']
780         patterns.append(patterns[0].replace('_', '-'))
781         for key, val in sorted(d.items()):
782             f.write('%s: %s\n' % (key.rjust(30), val))
783
784 def print_list(l, verbose=False, f=stdout, detail=True):
785     for elem in l:
786         #if it's empty string continue
787         if not elem:
788             continue
789         if type(elem) == types.DictionaryType:
790             print_dict(elem, f=f, detail=detail)
791         elif type(elem) == types.StringType:
792             if not verbose:
793                 elem = elem.split('Traceback')[0]
794             f.write('%s\n' % elem)
795         else:
796             f.write('%s\n' % elem)
797
798 def print_versions(data, f=stdout):
799     if 'versions' not in data:
800         f.write('%s\n' %data)
801         return
802     f.write('versions:\n')
803     for id, t in data['versions']:
804         f.write('%s @ %s\n' % (str(id).rjust(30), datetime.fromtimestamp(float(t))))
805
806
807 def main():
808     try:
809         name = argv[1]
810         cls = class_for_cli_command(name)
811     except (IndexError, KeyError):
812         print_usage()
813         exit(1)
814     
815     cmd = cls(name, argv[2:])
816     
817     try:
818         cmd.execute(*cmd.args)
819     except TypeError, e:
820         cmd.parser.print_help()
821         exit(1)
822     except Fault, f:
823         status = '%s ' % f.status if f.status else ''
824         print '%s%s' % (status, f.data)
825
826
827 if __name__ == '__main__':
828     main()