bc5449ef3b0829370b2e31721dc9b55ca4301a34
[kamaki] / kamaki / cli / commands / pithos_cli.py
1 # Copyright 2011-2012 GRNET S.A. All rights reserved.
2 #
3 # Redistribution and use in source and binary forms, with or
4 # without modification, are permitted provided that the following
5 # conditions are met:
6 #
7 #   1. Redistributions of source code must retain the above
8 #      copyright notice, this list of conditions and the following
9 #      disclaimer.
10 #
11 #   2. Redistributions in binary form must reproduce the above
12 #      copyright notice, this list of conditions and the following
13 #      disclaimer in the documentation and/or other materials
14 #      provided with the distribution.
15 #
16 # THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17 # OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19 # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20 # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23 # USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24 # AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27 # POSSIBILITY OF SUCH DAMAGE.
28 #
29 # The views and conclusions contained in the software and
30 # documentation are those of the authors and should not be
31 # interpreted as representing official policies, either expressed
32 # or implied, of GRNET S.A.command
33
34 from kamaki.cli import command, set_api_description, CLIError
35 from kamaki.clients.utils import filter_in
36 from kamaki.cli.utils import format_size, raiseCLIError, print_dict, pretty_keys, print_list
37 set_api_description('store', 'Pithos+ storage commands')
38 from kamaki.clients.pithos import PithosClient, ClientError
39 from colors import bold
40 from sys import stdout, exit
41 import signal
42
43 from progress.bar import IncrementalBar
44
45
46 class ProgressBar(IncrementalBar):
47     #suffix = '%(percent)d%% - %(eta)ds'
48     suffix = '%(percent)d%%'
49
50 class _pithos_init(object):
51     def main(self):
52         self.token = self.config.get('store', 'token') or self.config.get('global', 'token')
53         self.base_url = self.config.get('store', 'url') or self.config.get('global', 'url')
54         self.account = self.config.get('store', 'account') or self.config.get('global', 'account')
55         self.container = self.config.get('store', 'container') or self.config.get('global', 'container')
56         self.client = PithosClient(base_url=self.base_url, token=self.token, account=self.account,
57             container=self.container)
58
59 class _store_account_command(_pithos_init):
60     """Base class for account level storage commands"""
61
62     def update_parser(self, parser):
63         parser.add_argument('--account', dest='account', metavar='NAME',
64                           help="Specify an account to use")
65
66     def progress(self, message):
67         """Return a generator function to be used for progress tracking"""
68
69         MESSAGE_LENGTH = 25
70
71         def progress_gen(n):
72             msg = message.ljust(MESSAGE_LENGTH)
73             for i in ProgressBar(msg).iter(range(n)):
74                 yield
75             yield
76
77         return progress_gen
78
79     def main(self):
80         super(_store_account_command, self).main()
81         if hasattr(self.args, 'account') and self.args.account is not None:
82             self.client.account = self.args.account
83
84 class _store_container_command(_store_account_command):
85     """Base class for container level storage commands"""
86
87     def __init__(self):
88         self.container = None
89         self.path = None
90
91     def update_parser(self, parser):
92         super(_store_container_command, self).update_parser(parser)
93         parser.add_argument('--container', dest='container', metavar='NAME', default=None,
94             help="Specify a container to use")
95
96     def extract_container_and_path(self, container_with_path, path_is_optional=True):
97         assert isinstance(container_with_path, str)
98         if ':' not in container_with_path:
99             if hasattr(self.args, 'container'):
100                 self.container = getattr(self.args, 'container')
101             else:
102                 self.container = self.client.container
103             if self.container is None:
104                 self.container = container_with_path
105             else:
106                 self.path = container_with_path
107             if not path_is_optional and self.path is None:
108                 raise CLIError(message="Object path is missing", status=11)
109             return
110         cnp = container_with_path.split(':')
111         self.container = cnp[0]
112         try:
113             self.path = cnp[1]
114         except IndexError:
115             if path_is_optional:
116                 self.path = None
117             else:
118                 raise CLIError(message="Object path is missing", status=11)
119
120     def main(self, container_with_path=None, path_is_optional=True):
121         super(_store_container_command, self).main()
122         if container_with_path is not None:
123             self.extract_container_and_path(container_with_path, path_is_optional)
124             self.client.container = self.container
125         elif hasattr(self.args, 'container'):
126             self.client.container = getattr(self.args,'container')
127         self.container = self.client.container
128
129 """
130 @command()
131 class store_test(_store_container_command):
132     ""Test stuff""
133
134     def main(self):
135         super(self.__class__, self).main('pithos')
136         r = self.client.container_get()
137         print(unicode(r.content)+' '+unicode(r.json))
138 """
139
140 @command()
141 class store_list(_store_container_command):
142     """List containers, object trees or objects in a directory
143     """
144
145     def update_parser(self, parser):
146         super(self.__class__, self).update_parser(parser)
147         parser.add_argument('-l', action='store_true', dest='detail', default=False,
148             help='show detailed output')
149         parser.add_argument('-N', action='store', dest='show_size', default=1000,
150             help='print output in chunks of size N')
151         parser.add_argument('-n', action='store', dest='limit', default=None,
152             help='show limited output')
153         parser.add_argument('--marker', action='store', dest='marker', default=None,
154             help='show output greater then marker')
155         parser.add_argument('--prefix', action='store', dest='prefix', default=None,
156             help='show output starting with prefix')
157         parser.add_argument('--delimiter', action='store', dest='delimiter', default=None, 
158             help='show output up to the delimiter')
159         parser.add_argument('--path', action='store', dest='path', default=None, 
160             help='show output starting with prefix up to /')
161         parser.add_argument('--meta', action='store', dest='meta', default=None, 
162             help='show output having the specified meta keys (e.g. --meta "meta1 meta2 ..."')
163         parser.add_argument('--if-modified-since', action='store', dest='if_modified_since', 
164             default=None, help='show output if modified since then')
165         parser.add_argument('--if-unmodified-since', action='store', dest='if_unmodified_since',
166             default=None, help='show output if not modified since then')
167         parser.add_argument('--until', action='store', dest='until', default=None,
168             help='show metadata until that date')
169         dateformat = '%d/%m/%Y %H:%M:%S'
170         parser.add_argument('--format', action='store', dest='format', default=dateformat,
171             help='format to parse until date (default: d/m/Y H:M:S)')
172         parser.add_argument('--shared', action='store_true', dest='shared', default=False,
173             help='show only shared')
174         parser.add_argument('--public', action='store_true', dest='public', default=False,
175             help='show only public')
176
177     def print_objects(self, object_list):
178         import sys
179         try:
180             limit = getattr(self.args, 'show_size')
181             limit = int(limit)
182         except AttributeError:
183             pass
184         #index = 0
185         for index,obj in enumerate(object_list):
186             if not obj.has_key('content_type'):
187                 continue
188             pretty_obj = obj.copy()
189             index += 1
190             empty_space = ' '*(len(str(len(object_list))) - len(str(index)))
191             if obj['content_type'] == 'application/directory':
192                 isDir = True
193                 size = 'D'
194             else:
195                 isDir = False
196                 size = format_size(obj['bytes'])
197                 pretty_obj['bytes'] = '%s (%s)'%(obj['bytes'],size)
198             oname = bold(obj['name'])
199             if getattr(self.args, 'detail'):
200                 print('%s%s. %s'%(empty_space, index, oname))
201                 print_dict(pretty_keys(pretty_obj), exclude=('name'))
202                 print
203             else:
204                 oname = '%s%s. %6s %s'%(empty_space, index, size, oname)
205                 oname += '/' if isDir else ''
206                 print(oname)
207             if limit <= index < len(object_list) and index%limit == 0:
208                 print('(press "enter" to continue)')
209                 sys.stdin.read(1)
210
211     def print_containers(self, container_list):
212         import sys
213         try:
214             limit = getattr(self.args, 'show_size')
215             limit = int(limit)
216         except AttributeError:
217             pass
218         for index,container in enumerate(container_list):
219             if container.has_key('bytes'):
220                 size = format_size(container['bytes']) 
221             cname = '%s. %s'%(index+1, bold(container['name']))
222             if getattr(self.args, 'detail'):
223                 print(cname)
224                 pretty_c = container.copy()
225                 if container.has_key('bytes'):
226                     pretty_c['bytes'] = '%s (%s)'%(container['bytes'], size)
227                 print_dict(pretty_keys(pretty_c), exclude=('name'))
228                 print
229             else:
230                 if container.has_key('count') and container.has_key('bytes'):
231                     print('%s (%s, %s objects)' % (cname, size, container['count']))
232                 else:
233                     print(cname)
234             if limit <= index < len(container_list) and index%limit == 0:
235                 print('(press "enter" to continue)')
236                 sys.stdin.read(1)
237
238     def getuntil(self, orelse=None):
239         if hasattr(self.args, 'until'):
240             import time
241             until = getattr(self.args, 'until')
242             if until is None:
243                 return None
244             format = getattr(self.args, 'format')
245             #except TypeError:
246             try:
247                 t = time.strptime(until, format)
248             except ValueError as err:
249                 raise CLIError(message='in --until: '+unicode(err), importance=1)
250             return int(time.mktime(t))
251         return orelse
252    
253     def getmeta(self, orelse=[]):
254         if hasattr(self.args, 'meta'):
255             meta = getattr(self.args, 'meta')
256             if meta is None:
257                 return []
258             return meta.split(' ')
259         return orelse
260
261     def getpath(self, orelse=None):
262         if self.path is not None:
263             return self.path
264         if hasattr(self.args, 'path'):
265             return getattr(self.args, 'path')
266         return orelse
267
268     def main(self, container____path__=None):
269         super(self.__class__, self).main(container____path__)
270         try:
271             if self.container is None:
272                 r = self.client.account_get(limit=getattr(self.args, 'limit', None),
273                     marker=getattr(self.args, 'marker', None),
274                     if_modified_since=getattr(self.args, 'if_modified_since', None),
275                     if_unmodified_since=getattr(self.args, 'if_unmodified_since', None),
276                     until=self.getuntil(),
277                     show_only_shared=getattr(self.args, 'shared', False))
278                 self.print_containers(r.json)
279             else:
280                 r = self.client.container_get(limit=getattr(self.args, 'limit', None),
281                     marker=getattr(self.args, 'marker', None),
282                     prefix=getattr(self.args, 'prefix', None),
283                     delimiter=getattr(self.args, 'delimiter', None), path=self.getpath(orelse=None),
284                     if_modified_since=getattr(self.args, 'if_modified_since', None),
285                     if_unmodified_since=getattr(self.args, 'if_unmodified_since', None),
286                     until=self.getuntil(),
287                     meta=self.getmeta(),
288                     show_only_shared=getattr(self.args, 'shared', False))
289                 self.print_objects(r.json)
290         except ClientError as err:
291             raiseCLIError(err)
292
293 @command()
294 class store_mkdir(_store_container_command):
295     """Create a directory"""
296
297     def main(self, container___directory):
298         super(self.__class__, self).main(container___directory, path_is_optional=False)
299         try:
300             self.client.create_directory(self.path)
301         except ClientError as err:
302             raiseCLIError(err)
303
304 @command()
305 class store_create(_store_container_command):
306     """Create a container or a directory object"""
307
308     def update_parser(self, parser):
309         super(self.__class__, self).update_parser(parser)
310         parser.add_argument('--versioning', action='store', dest='versioning', default=None,
311             help='set container versioning (auto/none)')
312         parser.add_argument('--quota', action='store', dest='quota', default=None,
313             help='set default container quota')
314         parser.add_argument('--meta', action='store', dest='meta', default=None,
315             help='set container metadata ("key1:val1 key2:val2 ...")')
316
317     def getmeta(self, orelse=None):
318         try:
319             meta = getattr(self.args,'meta')
320             metalist = meta.split(' ')
321         except AttributeError:
322             return orelse
323         metadict = {}
324         for metastr in metalist:
325             (key,val) = metastr.split(':')
326             metadict[key] = val
327         return metadict
328
329     def main(self, container____directory__):
330         super(self.__class__, self).main(container____directory__)
331         try:
332             if self.path is None:
333                 self.client.container_put(quota=getattr(self.args, 'quota'),
334                     versioning=getattr(self.args, 'versioning'), metadata=self.getmeta())
335             else:
336                 self.client.create_directory(self.path)
337         except ClientError as err:
338             raiseCLIError(err)
339
340 @command()
341 class store_copy(_store_container_command):
342     """Copy an object"""
343
344     def update_parser(self, parser):
345         super(store_copy, self).update_parser(parser)
346         parser.add_argument('--source-version', action='store', dest='source_version', default=None,
347             help='copy specific version')
348         parser.add_argument('--public', action='store_true', dest='public', default=False,
349             help='make object publicly accessible')
350         parser.add_argument('--content-type', action='store', dest='content_type', default=None,
351             help='change object\'s content type')
352         parser.add_argument('--delimiter', action='store', dest='delimiter', default=None,
353             help=u'mass copy objects with path staring with src_object + delimiter')
354         parser.add_argument('-r', action='store_true', dest='recursive', default=False,
355             help='mass copy with delimiter /')
356
357     def getdelimiter(self):
358         if getattr(self.args, 'recursive'):
359             return '/'
360         return getattr(self.args, 'delimiter')
361
362     def main(self, source_container___path, destination_container____path__):
363         super(self.__class__, self).main(source_container___path, path_is_optional=False)
364         try:
365             dst = destination_container____path__.split(':')
366             dst_cont = dst[0]
367             dst_path = dst[1] if len(dst) > 1 else False
368             self.client.copy_object(src_container = self.container, src_object = self.path,
369                 dst_container = dst_cont, dst_object = dst_path,
370                 source_version=getattr(self.args, 'source_version'),
371                 public=getattr(self.args, 'public'),
372                 content_type=getattr(self.args,'content_type'), delimiter=self.getdelimiter())
373         except ClientError as err:
374             raiseCLIError(err)
375
376 @command()
377 class store_move(_store_container_command):
378     """Copy an object"""
379
380     def update_parser(self, parser):
381         super(store_move, self).update_parser(parser)
382         parser.add_argument('--source-version', action='store', dest='source_version', default=None,
383             help='copy specific version')
384         parser.add_argument('--public', action='store_true', dest='public', default=False,
385             help='make object publicly accessible')
386         parser.add_argument('--content-type', action='store', dest='content_type', default=None,
387             help='change object\'s content type')
388         parser.add_argument('--delimiter', action='store', dest='delimiter', default=None,
389             help=u'mass copy objects with path staring with src_object + delimiter')
390         parser.add_argument('-r', action='store_true', dest='recursive', default=False,
391             help='mass copy with delimiter /')
392
393     def getdelimiter(self):
394         if getattr(self.args, 'recursive'):
395             return '/'
396         return getattr(self.args, 'delimiter')
397
398     def main(self, source_container___path, destination_container____path__):
399         super(self.__class__, self).main(source_container___path, path_is_optional=False)
400         try:
401             dst = destination_container____path__.split(':')
402             dst_cont = dst[0]
403             dst_path = dst[1] if len(dst) > 1 else False
404             self.client.move_object(src_container = self.container, src_object = self.path,
405                 dst_container = dst_cont, dst_object = dst_path,
406                 source_version=getattr(self.args, 'source_version'),
407                 public=getattr(self.args, 'public'),
408                 content_type=getattr(self.args,'content_type'), delimiter=self.getdelimiter())
409         except ClientError as err:
410             raiseCLIError(err)
411
412 @command()
413 class store_append(_store_container_command):
414     """Append local file to (existing) remote object"""
415
416     def main(self, local_path, container___path):
417         super(self.__class__, self).main(container___path, path_is_optional=False)
418         try:
419             f = open(local_path, 'r')
420             upload_cb = self.progress('Appending blocks')
421             self.client.append_object(object=self.path, source_file = f, upload_cb = upload_cb)
422         except ClientError as err:
423             raiseCLIError(err)
424
425 @command()
426 class store_truncate(_store_container_command):
427     """Truncate remote file up to a size"""
428
429     
430     def main(self, container___path, size=0):
431         super(self.__class__, self).main(container___path, path_is_optional=False)
432         try:
433             self.client.truncate_object(self.path, size)
434         except ClientError as err:
435             raiseCLIError(err)
436
437 @command()
438 class store_overwrite(_store_container_command):
439     """Overwrite part (from start to end) of a remote file"""
440
441     def main(self, local_path, container___path, start, end):
442         super(self.__class__, self).main(container___path, path_is_optional=False)
443         try:
444             f = open(local_path, 'r')
445             upload_cb = self.progress('Overwritting blocks')
446             self.client.overwrite_object(object=self.path, start=start, end=end,
447                 source_file=f, upload_cb = upload_cb)
448         except ClientError as err:
449             raiseCLIError(err)
450
451 @command()
452 class store_manifest(_store_container_command):
453     """Create a remote file with uploaded parts by manifestation"""
454
455     def update_parser(self, parser):
456         super(self.__class__, self).update_parser(parser)
457         parser.add_argument('--etag', action='store', dest='etag', default=None,
458             help='check written data')
459         parser.add_argument('--content-encoding', action='store', dest='content_encoding',
460             default=None, help='provide the object MIME content type')
461         parser.add_argument('--content-disposition', action='store', dest='content_disposition',
462             default=None, help='provide the presentation style of the object')
463         parser.add_argument('--content-type', action='store', dest='content_type', default=None,
464             help='create object with specific content type')
465         parser.add_argument('--sharing', action='store', dest='sharing', default=None,
466             help='define sharing object policy ( "read=user1,grp1,user2,... write=user1,grp2,...')
467         parser.add_argument('--public', action='store_true', dest='public', default=False,
468             help='make object publicly accessible')
469
470     def getsharing(self, orelse={}):
471         permstr = getattr(self.args, 'sharing')
472         if permstr is None:
473             return orelse
474         perms = {}
475         for p in permstr.split(' '):
476             (key, val) = p.split('=')
477             if key.lower() not in ('read', 'write'):
478                 raise CLIError(message='in --sharing: Invalid permition key', importance=1)
479             val_list = val.split(',')
480             if not perms.has_key(key):
481                 perms[key]=[]
482             for item in val_list:
483                 if item not in perms[key]:
484                     perms[key].append(item)
485         return perms
486         
487     def main(self, container___path):
488         super(self.__class__, self).main(container___path, path_is_optional=False)
489         try:
490             self.client.create_object_by_manifestation(self.path,
491                 content_encoding=getattr(self.args, 'content_encoding'),
492                 content_disposition=getattr(self.args, 'content_disposition'),
493                 content_type=getattr(self.args, 'content_type'), sharing=self.getsharing(),
494                 public=getattr(self.args, 'public'))
495         except ClientError as err:
496             raiseCLIError(err)
497
498 @command()
499 class store_upload(_store_container_command):
500     """Upload a file"""
501
502     def update_parser(self, parser):
503         super(self.__class__, self).update_parser(parser)
504         parser.add_argument('--use_hashes', action='store_true', dest='use_hashes', default=False,
505             help='provide hashmap file instead of data')
506         parser.add_argument('--unchunked', action='store_true', dest='unchunked', default=False,
507             help='avoid chunked transfer mode')
508         parser.add_argument('--etag', action='store', dest='etag', default=None,
509             help='check written data')
510         parser.add_argument('--content-encoding', action='store', dest='content_encoding',
511             default=None, help='provide the object MIME content type')
512         parser.add_argument('--content-disposition', action='store', dest='content_disposition',
513             default=None, help='provide the presentation style of the object')
514         parser.add_argument('--content-type', action='store', dest='content_type', default=None,
515             help='create object with specific content type')
516         parser.add_argument('--sharing', action='store', dest='sharing', default=None,
517             help='define sharing object policy ( "read=user1,grp1,user2,... write=user1,grp2,...')
518         parser.add_argument('--public', action='store_true', dest='public', default=False,
519             help='make object publicly accessible')
520
521     def getsharing(self, orelse={}):
522         permstr = getattr(self.args, 'sharing')
523         if permstr is None:
524             return orelse
525         perms = {}
526         for p in permstr.split(' '):
527             (key, val) = p.split('=')
528             if key.lower() not in ('read', 'write'):
529                 raise CLIError(message='in --sharing: Invalid permition key', importance=1)
530             val_list = val.split(',')
531             if not perms.has_key(key):
532                 perms[key]=[]
533             for item in val_list:
534                 if item not in perms[key]:
535                     perms[key].append(item)
536         return perms
537
538     def main(self, local_path, container____path__):
539         super(self.__class__, self).main(container____path__)
540         remote_path = local_path if self.path is None else self.path
541         try:
542             with open(local_path) as f:
543                 if getattr(self.args, 'unchunked'):
544                     self.client.upload_object_unchunked(remote_path, f,
545                     etag=getattr(self.args, 'etag'), withHashFile=getattr(self.args, 'use_hashes'),
546                     content_encoding=getattr(self.args, 'content_encoding'),
547                     content_disposition=getattr(self.args, 'content_disposition'),
548                     content_type=getattr(self.args, 'content_type'), sharing=self.getsharing(),
549                     public=getattr(self.args, 'public'))
550                 else:
551                     hash_cb = self.progress('Calculating block hashes')
552                     upload_cb = self.progress('Uploading blocks')
553                     self.client.upload_object(remote_path, f, hash_cb=hash_cb, upload_cb=upload_cb,
554                     content_encoding=getattr(self.args, 'content_encoding'),
555                     content_disposition=getattr(self.args, 'content_disposition'),
556                     content_type=getattr(self.args, 'content_type'), sharing=self.getsharing(),
557                     public=getattr(self.args, 'public'))
558         except ClientError as err:
559             raiseCLIError(err)
560         print 'Upload completed'
561
562 @command()
563 class store_download(_store_container_command):
564     """Download a file"""
565
566     def update_parser(self, parser):
567         super(self.__class__, self).update_parser(parser)
568         parser.add_argument('--no-progress-bar', action='store_true', dest='no_progress_bar',
569             default=False, help='Dont display progress bars')
570         parser.add_argument('--overide', action='store_true', dest='overide', default=False,
571             help='Force download to overide an existing file')
572         parser.add_argument('--range', action='store', dest='range', default=None,
573             help='show range of data')
574         parser.add_argument('--if-match', action='store', dest='if_match', default=None,
575             help='show output if ETags match')
576         parser.add_argument('--if-none-match', action='store', dest='if_none_match', default=None,
577             help='show output if ETags don\'t match')
578         parser.add_argument('--if-modified-since', action='store', dest='if_modified_since',
579             default=None, help='show output if modified since then')
580         parser.add_argument('--if-unmodified-since', action='store', dest='if_unmodified_since',
581             default=None, help='show output if not modified since then')
582         parser.add_argument('--object-version', action='store', dest='object_version', default=None,
583             help='get the specific version')
584
585     def main(self, container___path, local_path=None):
586         super(self.__class__, self).main(container___path, path_is_optional=False)
587
588         #setup output stream
589         parallel = False
590         if local_path is None:
591             out = stdout
592         else:
593             try:
594                 if getattr(self.args, 'overide'):
595                     out = open(local_path, 'wb+')
596                 else:
597                     out = open(local_path, 'ab+')
598             except IOError as err:
599                 raise CLIError(message='Cannot write to file %s - %s'%(local_path,unicode(err)),
600                     importance=1)
601         download_cb = None if getattr(self.args, 'no_progress_bar') \
602             else self.progress('Downloading')
603
604
605         try:
606             self.client.download_object(self.path, out, download_cb,
607                 range=getattr(self.args, 'range'), version=getattr(self.args,'object_version'),
608                 if_match=getattr(self.args, 'if_match'), overide=getattr(self.args, 'overide'),
609                 if_none_match=getattr(self.args, 'if_none_match'),
610                 if_modified_since=getattr(self.args, 'if_modified_since'),
611                 if_unmodified_since=getattr(self.args, 'if_unmodified_since'))
612         except ClientError as err:
613             raiseCLIError(err)
614         except KeyboardInterrupt:
615             print('\ndownload canceled by user')
616             if local_path is not None:
617                 print('re-run command to resume')
618         print
619
620 @command()
621 class store_hashmap(_store_container_command):
622     """Get the hashmap of an object"""
623
624     def update_parser(self, parser):
625         super(self.__class__, self).update_parser(parser)
626         parser.add_argument('--if-match', action='store', dest='if_match', default=None,
627             help='show output if ETags match')
628         parser.add_argument('--if-none-match', action='store', dest='if_none_match', default=None,
629             help='show output if ETags dont match')
630         parser.add_argument('--if-modified-since', action='store', dest='if_modified_since',
631             default=None, help='show output if modified since then')
632         parser.add_argument('--if-unmodified-since', action='store', dest='if_unmodified_since',
633             default=None, help='show output if not modified since then')
634         parser.add_argument('--object-version', action='store', dest='object_version', default=None,
635             help='get the specific version')
636
637     def main(self, container___path):
638         super(self.__class__, self).main(container___path, path_is_optional=False)
639         try:
640             data = self.client.get_object_hashmap(self.path,
641                 version=getattr(self.args, 'object_version'),
642                 if_match=getattr(self.args, 'if_match'),
643                 if_none_match=getattr(self.args, 'if_none_match'),
644                 if_modified_since=getattr(self.args, 'if_modified_since'),
645                 if_unmodified_since=getattr(self.args, 'if_unmodified_since'))
646         except ClientError as err:
647             raiseCLIError(err)
648         print_dict(data)
649
650 @command()
651 class store_delete(_store_container_command):
652     """Delete a container [or an object]"""
653
654     def update_parser(self, parser):
655         super(self.__class__, self).update_parser(parser)
656         parser.add_argument('--until', action='store', dest='until', default=None,
657             help='remove history until that date')
658         parser.add_argument('--format', action='store', dest='format', default='%d/%m/%Y %H:%M:%S',
659             help='format to parse until date (default: d/m/Y H:M:S)')
660         parser.add_argument('--delimiter', action='store', dest='delimiter',
661             default=None, 
662             help='mass delete objects with path staring with <object><delimiter>')
663         parser.add_argument('-r', action='store_true', dest='recursive', default=False,
664             help='empty dir or container and delete (if dir)')
665     
666     def getuntil(self, orelse=None):
667         if hasattr(self.args, 'until'):
668             import time
669             until = getattr(self.args, 'until')
670             if until is None:
671                 return None
672             format = getattr(self.args, 'format')
673             try:
674                 t = time.strptime(until, format)
675             except ValueError as err:
676                 raise CLIError(message='in --until: '+unicode(err), importance=1)
677             return int(time.mktime(t))
678         return orelse
679
680     def getdelimiter(self, orelse=None):
681         try:
682             dlm = getattr(self.args, 'delimiter')
683             if dlm is None:
684                 return '/' if getattr(self.args, 'recursive') else orelse
685         except AttributeError:
686             return orelse
687         return dlm
688
689     def main(self, container____path__):
690         super(self.__class__, self).main(container____path__)
691         try:
692             if self.path is None:
693                 self.client.del_container(until=self.getuntil(), delimiter=self.getdelimiter())
694             else:
695                 #self.client.delete_object(self.path)
696                 self.client.del_object(self.path, until=self.getuntil(),
697                     delimiter=self.getdelimiter())
698         except ClientError as err:
699             raiseCLIError(err)
700
701 @command()
702 class store_purge(_store_container_command):
703     """Purge a container"""
704     
705     def main(self, container):
706         super(self.__class__, self).main()
707         try:
708             self.client.purge_container()
709         except ClientError as err:
710             raiseCLIError(err)
711
712 @command()
713 class store_publish(_store_container_command):
714     """Publish an object"""
715
716     def main(self, container___path):
717         super(self.__class__, self).main(container___path, path_is_optional=False)
718         try:
719             self.client.publish_object(self.path)
720         except ClientError as err:
721             raiseCLIError(err)
722
723 @command()
724 class store_unpublish(_store_container_command):
725     """Unpublish an object"""
726
727     def main(self, container___path):
728         super(self.__class__, self).main(container___path, path_is_optional=False)
729         try:
730             self.client.unpublish_object(self.path)
731         except ClientError as err:
732             raiseCLIError(err)
733
734 @command()
735 class store_permitions(_store_container_command):
736     """Get object read/write permitions"""
737
738     def main(self, container___path):
739         super(self.__class__, self).main(container___path, path_is_optional=False)
740         try:
741             reply = self.client.get_object_sharing(self.path)
742             print_dict(reply)
743         except ClientError as err:
744             raiseCLIError(err)
745
746 @command()
747 class store_setpermitions(_store_container_command):
748     """Set sharing permitions"""
749
750     def format_permition_dict(self,permitions):
751         read = False
752         write = False
753         for perms in permitions:
754             splstr = perms.split('=')
755             if 'read' == splstr[0]:
756                 read = [user_or_group.strip() \
757                 for user_or_group in splstr[1].split(',')]
758             elif 'write' == splstr[0]:
759                 write = [user_or_group.strip() \
760                 for user_or_group in splstr[1].split(',')]
761             else:
762                 read = False
763                 write = False
764         if not read and not write:
765             raise CLIError(message='Usage:\tread=<groups,users> write=<groups,users>',
766                 importance=0)
767         return (read,write)
768
769     def main(self, container___path, *permitions):
770         super(self.__class__, self).main(container___path, path_is_optional=False)
771         (read, write) = self.format_permition_dict(permitions)
772         try:
773             self.client.set_object_sharing(self.path,
774                 read_permition=read, write_permition=write)
775         except ClientError as err:
776             raiseCLIError(err)
777
778 @command()
779 class store_delpermitions(_store_container_command):
780     """Delete all sharing permitions"""
781
782     def main(self, container___path):
783         super(self.__class__, self).main(container___path, path_is_optional=False)
784         try:
785             self.client.del_object_sharing(self.path)
786         except ClientError as err:
787             raiseCLIError(err)
788
789 @command()
790 class store_info(_store_container_command):
791     """Get information for account [, container [or object]]"""
792
793     
794     def main(self, container____path__=None):
795         super(self.__class__, self).main(container____path__)
796         try:
797             if self.container is None:
798                 reply = self.client.get_account_info()
799             elif self.path is None:
800                 reply = self.client.get_container_info(self.container)
801             else:
802                 reply = self.client.get_object_info(self.path)
803         except ClientError as err:
804             raiseCLIError(err)
805         print_dict(reply)
806
807 @command()
808 class store_meta(_store_container_command):
809     """Get custom meta-content for account [, container [or object]]"""
810
811     def update_parser(self, parser):
812         super(self.__class__, self).update_parser(parser)
813         parser.add_argument('-l', action='store_true', dest='detail', default=False,
814             help='show detailed output')
815         parser.add_argument('--until', action='store', dest='until', default=None,
816             help='show metadata until that date')
817         dateformat='%d/%m/%Y %H:%M:%S'
818         parser.add_argument('--format', action='store', dest='format', default=dateformat,
819             help='format to parse until date (default: "d/m/Y H:M:S")')
820         parser.add_argument('--object_version', action='store', dest='object_version', default=None,
821             help='show specific version \ (applies only for objects)')
822
823     def getuntil(self, orelse=None):
824         if hasattr(self.args, 'until'):
825             import time
826             until = getattr(self.args, 'until')
827             if until is None:
828                 return None
829             format = getattr(self.args, 'format')
830             #except TypeError:
831             try:
832                 t = time.strptime(until, format)
833             except ValueError as err:
834                 raise CLIError(message='in --until: '+unicode(err), importance=1)
835             return int(time.mktime(t))
836         return orelse
837
838     def main(self, container____path__ = None):
839         super(self.__class__, self).main(container____path__)
840
841         detail = getattr(self.args, 'detail')
842         try:
843             if self.container is None:
844                 print(bold(self.client.account))
845                 if detail:
846                     reply = self.client.get_account_info(until=self.getuntil())
847                 else:
848                     reply = self.client.get_account_meta(until=self.getuntil())
849                     reply = pretty_keys(reply, '-')
850             elif self.path is None:
851                 print(bold(self.client.account+': '+self.container))
852                 if detail:
853                     reply = self.client.get_container_info(until = self.getuntil())
854                 else:
855                     cmeta = self.client.get_container_meta(until=self.getuntil())
856                     ometa = self.client.get_container_object_meta(until=self.getuntil())
857                     reply = {'container-meta':pretty_keys(cmeta, '-'),
858                         'object-meta':pretty_keys(ometa, '-')}
859             else:
860                 print(bold(self.client.account+': '+self.container+':'+self.path))
861                 version=getattr(self.args, 'object_version')
862                 if detail:
863                     reply = self.client.get_object_info(self.path, version = version)
864                 else:
865                     reply = self.client.get_object_meta(self.path, version=version)
866                     reply = pretty_keys(pretty_keys(reply, '-'))
867         except ClientError as err:
868             raiseCLIError(err)
869         print_dict(reply)
870
871 @command()
872 class store_setmeta(_store_container_command):
873     """Set a new metadatum for account [, container [or object]]"""
874
875     def main(self, metakey___metaval, container____path__=None):
876         super(self.__class__, self).main(container____path__)
877         try:
878             metakey, metavalue = metakey___metaval.split(':')
879         except ValueError:
880             raise CLIError(message='Meta variables should be formated as metakey:metavalue',
881                 importance=1)
882         try:
883             if self.container is None:
884                 self.client.set_account_meta({metakey:metavalue})
885             elif self.path is None:
886                 self.client.set_container_meta({metakey:metavalue})
887             else:
888                 self.client.set_object_meta(self.path, {metakey:metavalue})
889         except ClientError as err:
890             raiseCLIError(err)
891
892 @command()
893 class store_delmeta(_store_container_command):
894     """Delete an existing metadatum of account [, container [or object]]"""
895
896     def main(self, metakey, container____path__=None):
897         super(self.__class__, self).main(container____path__)
898         try:
899             if self.container is None:
900                 self.client.del_account_meta(metakey)
901             elif self.path is None:
902                 self.client.del_container_meta(metakey)
903             else:
904                 self.client.del_object_meta(metakey, self.path)
905         except ClientError as err:
906             raiseCLIError(err)
907
908 @command()
909 class store_quota(_store_account_command):
910     """Get  quota for account [or container]"""
911
912     def main(self, container = None):
913         super(self.__class__, self).main()
914         try:
915             if container is None:
916                 reply = self.client.get_account_quota()
917             else:
918                 reply = self.client.get_container_quota(container)
919         except ClientError as err:
920             raiseCLIError(err)
921         print_dict(reply)
922
923 @command()
924 class store_setquota(_store_account_command):
925     """Set new quota (in KB) for account [or container]"""
926
927     def main(self, quota, container = None):
928         super(self.__class__, self).main()
929         try:
930             if container is None:
931                 self.client.set_account_quota(quota)
932             else:
933                 self.client.container = container
934                 self.client.set_container_quota(quota)
935         except ClientError as err:
936             raiseCLIError(err)
937
938 @command()
939 class store_versioning(_store_account_command):
940     """Get  versioning for account [or container ]"""
941
942     def main(self, container = None):
943         super(self.__class__, self).main()
944         try:
945             if container is None:
946                 reply = self.client.get_account_versioning()
947             else:
948                 reply = self.client.get_container_versioning(container)
949         except ClientError as err:
950             raiseCLIError(err)
951         print_dict(reply)
952
953 @command()
954 class store_setversioning(_store_account_command):
955     """Set new versioning (auto, none) for account [or container]"""
956
957     def main(self, versioning, container = None):
958         super(self.__class__, self).main()
959         try:
960             if container is None:
961                 self.client.set_account_versioning(versioning)
962             else:
963                 self.client.container = container
964                 self.client.set_container_versioning(versioning)
965         except ClientError as err:
966             raiseCLIError(err)
967
968 @command()
969 class store_group(_store_account_command):
970     """Get user groups details for account"""
971
972     def main(self):
973         super(self.__class__, self).main()
974         try:
975             reply = self.client.get_account_group()
976         except ClientError as err:
977             raiseCLIError(err)
978         print_dict(reply)
979
980 @command()
981 class store_setgroup(_store_account_command):
982     """Create/update a new user group on account"""
983
984     def main(self, groupname, *users):
985         super(self.__class__, self).main()
986         try:
987             self.client.set_account_group(groupname, users)
988         except ClientError as err:
989             raiseCLIError(err)
990
991 @command()
992 class store_delgroup(_store_account_command):
993     """Delete a user group on an account"""
994
995     def main(self, groupname):
996         super(self.__class__, self).main()
997         try:
998             self.client.del_account_group(groupname)
999         except ClientError as err:
1000             raiseCLIError(err)