Statistics
| Branch: | Tag: | Revision:

root / kamaki / clients / pithos_cli.py @ d804de82

History | View | Annotate | Download (40.4 kB)

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.utils import format_size
37
set_api_description('store', 'Pithos+ storage commands')
38
from .pithos import PithosClient, ClientError
39
from .cli_utils import raiseCLIError
40
from kamaki.utils import print_dict, pretty_keys, print_list
41
from colors import bold
42
from sys import stdout, exit
43
import signal
44

    
45
from progress.bar import IncrementalBar
46

    
47

    
48
class ProgressBar(IncrementalBar):
49
    #suffix = '%(percent)d%% - %(eta)ds'
50
    suffix = '%(percent)d%%'
51

    
52
class _pithos_init(object):
53
    def main(self):
54
        self.token = self.config.get('store', 'token') or self.config.get('global', 'token')
55
        self.base_url = self.config.get('store', 'url') or self.config.get('global', 'url')
56
        self.account = self.config.get('store', 'account') or self.config.get('global', 'account')
57
        self.container = self.config.get('store', 'container') or self.config.get('global', 'container')
58
        self.client = PithosClient(base_url=self.base_url, token=self.token, account=self.account,
59
            container=self.container)
60

    
61
class _store_account_command(_pithos_init):
62
    """Base class for account level storage commands"""
63

    
64
    def update_parser(self, parser):
65
        parser.add_argument('--account', dest='account', metavar='NAME',
66
                          help="Specify an account to use")
67

    
68
    def progress(self, message):
69
        """Return a generator function to be used for progress tracking"""
70

    
71
        MESSAGE_LENGTH = 25
72

    
73
        def progress_gen(n):
74
            msg = message.ljust(MESSAGE_LENGTH)
75
            for i in ProgressBar(msg).iter(range(n)):
76
                yield
77
            yield
78

    
79
        return progress_gen
80

    
81
    def main(self):
82
        super(_store_account_command, self).main()
83
        if hasattr(self.args, 'account') and self.args.account is not None:
84
            self.client.account = self.args.account
85

    
86
class _store_container_command(_store_account_command):
87
    """Base class for container level storage commands"""
88

    
89
    def update_parser(self, parser):
90
        super(_store_container_command, self).update_parser(parser)
91
        parser.add_argument('--container', dest='container', metavar='NAME',
92
                          help="Specify a container to use")
93

    
94
    def extract_container_and_path(self, container_with_path, path_is_optional=True):
95
        assert isinstance(container_with_path, str)
96
        cnp = container_with_path.split(':')
97
        self.container = cnp[0]
98
        try:
99
            self.path = cnp[1]
100
        except IndexError:
101
            if path_is_optional:
102
                self.path = None
103
            else:
104
                raise CLIError(message="Object path is missing", status=11)
105

    
106
    def main(self, container_with_path=None, path_is_optional=True):
107
        super(_store_container_command, self).main()
108
        if container_with_path is not None:
109
            self.extract_container_and_path(container_with_path, path_is_optional)
110
            self.client.container = self.container
111
        elif hasattr(self.args, 'container') and self.args.container is not None:
112
            self.client.container = self.args.container
113
        else:
114
            self.container = None
115

    
116
@command()
117
class store_list(_store_container_command):
118
    """List containers, object trees or objects in a directory
119
    """
120

    
121
    def update_parser(self, parser):
122
        super(self.__class__, self).update_parser(parser)
123
        parser.add_argument('-l', action='store_true', dest='detail', default=False,
124
            help='show detailed output')
125
        parser.add_argument('-N', action='store', dest='show_size', default=1000,
126
            help='print output in chunks of size N')
127
        parser.add_argument('-n', action='store', dest='limit', default=None,
128
            help='show limited output')
129
        parser.add_argument('--marker', action='store', dest='marker', default=None,
130
            help='show output greater then marker')
131
        parser.add_argument('--prefix', action='store', dest='prefix', default=None,
132
            help='show output starting with prefix')
133
        parser.add_argument('--delimiter', action='store', dest='delimiter', default=None, 
134
            help='show output up to the delimiter')
135
        parser.add_argument('--path', action='store', dest='path', default=None, 
136
            help='show output starting with prefix up to /')
137
        parser.add_argument('--meta', action='store', dest='meta', default=None, 
138
            help='show output having the specified meta keys (e.g. --meta "meta1 meta2 ..."')
139
        parser.add_argument('--if-modified-since', action='store', dest='if_modified_since', 
140
            default=None, help='show output if modified since then')
141
        parser.add_argument('--if-unmodified-since', action='store', dest='if_unmodified_since',
142
            default=None, help='show output if not modified since then')
143
        parser.add_argument('--until', action='store', dest='until', default=None,
144
            help='show metadata until that date')
145
        dateformat = '%d/%m/%Y %H:%M:%S'
146
        parser.add_argument('--format', action='store', dest='format', default=dateformat,
147
            help='format to parse until date (default: d/m/Y H:M:S)')
148
        parser.add_argument('--shared', action='store_true', dest='shared', default=False,
149
            help='show only shared')
150
        parser.add_argument('--public', action='store_true', dest='public', default=False,
151
            help='show only public')
152

    
153
    def print_objects(self, object_list):
154
        import sys
155
        try:
156
            limit = getattr(self.args, 'show_size')
157
            limit = int(limit)
158
        except AttributeError:
159
            pass
160
        #index = 0
161
        for index,obj in enumerate(object_list):
162
            if not obj.has_key('content_type'):
163
                continue
164
            pretty_obj = obj.copy()
165
            index += 1
166
            empty_space = ' '*(len(str(len(object_list))) - len(str(index)))
167
            if obj['content_type'] == 'application/directory':
168
                isDir = True
169
                size = 'D'
170
            else:
171
                isDir = False
172
                size = format_size(obj['bytes'])
173
                pretty_obj['bytes'] = '%s (%s)'%(obj['bytes'],size)
174
            oname = bold(obj['name'])
175
            if getattr(self.args, 'detail'):
176
                print('%s%s. %s'%(empty_space, index, oname))
177
                print_dict(pretty_keys(pretty_obj), exclude=('name'))
178
                print
179
            else:
180
                oname = '%s%s. %6s %s'%(empty_space, index, size, oname)
181
                oname += '/' if isDir else ''
182
                print(oname)
183
            if limit <= index < len(object_list) and index%limit == 0:
184
                print('(press "enter" to continue)')
185
                sys.stdin.read(1)
186

    
187
    def print_containers(self, container_list):
188
        import sys
189
        try:
190
            limit = getattr(self.args, 'show_size')
191
            limit = int(limit)
192
        except AttributeError:
193
            pass
194
        for container in container_list:
195
            size = format_size(container['bytes'])
196
            index = 1+container_list.index(container)
197
            cname = '%s. %s'%(index, bold(container['name']))
198
            if getattr(self.args, 'detail'):
199
                print(cname)
200
                pretty_c = container.copy()
201
                pretty_c['bytes'] = '%s (%s)'%(container['bytes'], size)
202
                print_dict(pretty_keys(pretty_c), exclude=('name'))
203
                print
204
            else:
205
                print('%s (%s, %s objects)' % (cname, size, container['count']))
206
            if limit <= index < len(container_list) and index%limit == 0:
207
                print('(press "enter" to continue)')
208
                sys.stdin.read(1)
209

    
210
    def getuntil(self, orelse=None):
211
        if hasattr(self.args, 'until'):
212
            import time
213
            until = getattr(self.args, 'until')
214
            if until is None:
215
                return None
216
            format = getattr(self.args, 'format')
217
            #except TypeError:
218
            try:
219
                t = time.strptime(until, format)
220
            except ValueError as err:
221
                raise CLIError(message='in --until: '+unicode(err), importance=1)
222
            return int(time.mktime(t))
223
        return orelse
224
   
225
    def getmeta(self, orelse=[]):
226
        if hasattr(self.args, 'meta'):
227
            meta = getattr(self.args, 'meta')
228
            if meta is None:
229
                return []
230
            return meta.split(' ')
231
        return orelse
232

    
233
    def main(self, container____path__=None):
234
        super(self.__class__, self).main(container____path__)
235
        try:
236
            if self.container is None:
237
                r = self.client.account_get(limit=getattr(self.args, 'limit', None),
238
                    marker=getattr(self.args, 'marker', None),
239
                    if_modified_since=getattr(self.args, 'if_modified_since', None),
240
                    if_unmodified_since=getattr(self.args, 'if_unmodified_since', None),
241
                    until=self.getuntil(),
242
                    show_only_shared=getattr(self.args, 'shared', False))
243
                self.print_containers(r.json)
244
            else:
245
                r = self.client.container_get(limit=getattr(self.args, 'limit', None),
246
                    marker=getattr(self.args, 'marker', None),
247
                    prefix=getattr(self.args, 'prefix', None),
248
                    delimiter=getattr(self.args, 'delimiter', None),
249
                    path=getattr(self.args, 'path', None) if self.path is None else self.path,
250
                    if_modified_since=getattr(self.args, 'if_modified_since', None),
251
                    if_unmodified_since=getattr(self.args, 'if_unmodified_since', None),
252
                    until=self.getuntil(),
253
                    meta=self.getmeta(),
254
                    show_only_shared=getattr(self.args, 'shared', False))
255
                self.print_objects(r.json)
256
        except ClientError as err:
257
            raiseCLIError(err)
258

    
259
@command()
260
class store_mkdir(_store_container_command):
261
    """Create a directory"""
262

    
263
    def main(self, directory):
264
        super(self.__class__, self).main()
265
        try:
266
            self.client.create_directory(directory)
267
        except ClientError as err:
268
            raiseCLIError(err)
269

    
270
@command()
271
class store_create(_store_container_command):
272
    """Create a container or a directory object"""
273

    
274
    def update_parser(self, parser):
275
        super(self.__class__, self).update_parser(parser)
276
        parser.add_argument('--versioning', action='store', dest='versioning', default=None,
277
            help='set container versioning (auto/none)')
278
        parser.add_argument('--quota', action='store', dest='quota', default=None,
279
            help='set default container quota')
280
        parser.add_argument('--meta', action='store', dest='meta', default=None,
281
            help='set container metadata ("key1:val1 key2:val2 ...")')
282

    
283
    def getmeta(self, orelse=None):
284
        try:
285
            meta = getattr(self.args,'meta')
286
            metalist = meta.split(' ')
287
        except AttributeError:
288
            return orelse
289
        metadict = {}
290
        for metastr in metalist:
291
            (key,val) = metastr.split(':')
292
            metadict[key] = val
293
        return metadict
294

    
295
    def main(self, container____directory__):
296
        super(self.__class__, self).main(container____directory__)
297
        try:
298
            if self.path is None:
299
                self.client.container_put(quota=getattr(self.args, 'quota'),
300
                    versioning=getattr(self.args, 'versioning'), metadata=self.getmeta())
301
            else:
302
                self.client.create_directory(self.path)
303
        except ClientError as err:
304
            raiseCLIError(err)
305

    
306
@command()
307
class store_copy(_store_container_command):
308
    """Copy an object"""
309

    
310
    def update_parser(self, parser):
311
        super(store_copy, self).update_parser(parser)
312
        parser.add_argument('--source-version', action='store', dest='source_version', default=None,
313
            help='copy specific version')
314
        parser.add_argument('--public', action='store_true', dest='public', default=False,
315
            help='make object publicly accessible')
316
        parser.add_argument('--content-type', action='store', dest='content_type', default=None,
317
            help='change object\'s content type')
318
        parser.add_argument('--delimiter', action='store', dest='delimiter', default=None,
319
            help=u'mass copy objects with path staring with src_object + delimiter')
320
        parser.add_argument('-r', action='store_true', dest='recursive', default=False,
321
            help='mass copy with delimiter /')
322

    
323
    def getdelimiter(self):
324
        if getattr(self.args, 'recursive'):
325
            return '/'
326
        return getattr(self.args, 'delimiter')
327

    
328
    def main(self, source_container___path, destination_container____path__):
329
        super(self.__class__, self).main(source_container___path, path_is_optional=False)
330
        try:
331
            dst = destination_container____path__.split(':')
332
            dst_cont = dst[0]
333
            dst_path = dst[1] if len(dst) > 1 else False
334
            self.client.copy_object(src_container = self.container, src_object = self.path,
335
                dst_container = dst_cont, dst_object = dst_path,
336
                source_version=getattr(self.args, 'source_version'),
337
                public=getattr(self.args, 'public'),
338
                content_type=getattr(self.args,'content_type'), delimiter=self.getdelimiter())
339
        except ClientError as err:
340
            raiseCLIError(err)
341

    
342
@command()
343
class store_move(_store_container_command):
344
    """Copy an object"""
345

    
346
    def update_parser(self, parser):
347
        super(store_move, self).update_parser(parser)
348
        parser.add_argument('--source-version', action='store', dest='source_version', default=None,
349
            help='copy specific version')
350
        parser.add_argument('--public', action='store_true', dest='public', default=False,
351
            help='make object publicly accessible')
352
        parser.add_argument('--content-type', action='store', dest='content_type', default=None,
353
            help='change object\'s content type')
354
        parser.add_argument('--delimiter', action='store', dest='delimiter', default=None,
355
            help=u'mass copy objects with path staring with src_object + delimiter')
356
        parser.add_argument('-r', action='store_true', dest='recursive', default=False,
357
            help='mass copy with delimiter /')
358

    
359
    def getdelimiter(self):
360
        if getattr(self.args, 'recursive'):
361
            return '/'
362
        return getattr(self.args, 'delimiter')
363

    
364
    def main(self, source_container___path, destination_container____path__):
365
        super(self.__class__, self).main(source_container___path, path_is_optional=False)
366
        try:
367
            dst = destination_container____path__.split(':')
368
            dst_cont = dst[0]
369
            dst_path = dst[1] if len(dst) > 1 else False
370
            self.client.move_object(src_container = self.container, src_object = self.path,
371
                dst_container = dst_cont, dst_object = dst_path,
372
                source_version=getattr(self.args, 'source_version'),
373
                public=getattr(self.args, 'public'),
374
                content_type=getattr(self.args,'content_type'), delimiter=self.getdelimiter())
375
        except ClientError as err:
376
            raiseCLIError(err)
377

    
378
@command()
379
class store_append(_store_container_command):
380
    """Append local file to (existing) remote object"""
381

    
382
    def main(self, local_path, container___path):
383
        super(self.__class__, self).main(container___path, path_is_optional=False)
384
        try:
385
            f = open(local_path, 'r')
386
            upload_cb = self.progress('Appending blocks')
387
            self.client.append_object(object=self.path, source_file = f, upload_cb = upload_cb)
388
        except ClientError as err:
389
            raiseCLIError(err)
390

    
391
@command()
392
class store_truncate(_store_container_command):
393
    """Truncate remote file up to a size"""
394

    
395
    
396
    def main(self, container___path, size=0):
397
        super(self.__class__, self).main(container___path, path_is_optional=False)
398
        try:
399
            self.client.truncate_object(self.path, size)
400
        except ClientError as err:
401
            raiseCLIError(err)
402

    
403
@command()
404
class store_overwrite(_store_container_command):
405
    """Overwrite part (from start to end) of a remote file"""
406

    
407
    def main(self, local_path, container___path, start, end):
408
        super(self.__class__, self).main(container___path, path_is_optional=False)
409
        try:
410
            f = open(local_path, 'r')
411
            upload_cb = self.progress('Overwritting blocks')
412
            self.client.overwrite_object(object=self.path, start=start, end=end,
413
                source_file=f, upload_cb = upload_cb)
414
        except ClientError as err:
415
            raiseCLIError(err)
416

    
417
@command()
418
class store_manifest(_store_container_command):
419
    """Create a remote file with uploaded parts by manifestation"""
420

    
421
    def update_parser(self, parser):
422
        super(self.__class__, self).update_parser(parser)
423
        parser.add_argument('--etag', action='store', dest='etag', default=None,
424
            help='check written data')
425
        parser.add_argument('--content-encoding', action='store', dest='content_encoding',
426
            default=None, help='provide the object MIME content type')
427
        parser.add_argument('--content-disposition', action='store', dest='content_disposition',
428
            default=None, help='provide the presentation style of the object')
429
        parser.add_argument('--content-type', action='store', dest='content_type', default=None,
430
            help='create object with specific content type')
431
        parser.add_argument('--sharing', action='store', dest='sharing', default=None,
432
            help='define sharing object policy ( "read=user1,grp1,user2,... write=user1,grp2,...')
433
        parser.add_argument('--public', action='store_true', dest='public', default=False,
434
            help='make object publicly accessible')
435

    
436
    def getsharing(self, orelse={}):
437
        permstr = getattr(self.args, 'sharing')
438
        if permstr is None:
439
            return orelse
440
        perms = {}
441
        for p in permstr.split(' '):
442
            (key, val) = p.split('=')
443
            if key.lower() not in ('read', 'write'):
444
                raise CLIError(message='in --sharing: Invalid permition key', importance=1)
445
            val_list = val.split(',')
446
            if not perms.has_key(key):
447
                perms[key]=[]
448
            for item in val_list:
449
                if item not in perms[key]:
450
                    perms[key].append(item)
451
        return perms
452
        
453
    def main(self, container___path):
454
        super(self.__class__, self).main(container___path, path_is_optional=False)
455
        try:
456
            self.client.create_object_by_manifestation(self.path,
457
                content_encoding=getattr(self.args, 'content_encoding'),
458
                content_disposition=getattr(self.args, 'content_disposition'),
459
                content_type=getattr(self.args, 'content_type'), sharing=self.getsharing(),
460
                public=getattr(self.args, 'public'))
461
        except ClientError as err:
462
            raiseCLIError(err)
463

    
464
@command()
465
class store_upload(_store_container_command):
466
    """Upload a file"""
467

    
468
    def update_parser(self, parser):
469
        super(self.__class__, self).update_parser(parser)
470
        parser.add_argument('--use_hashes', action='store_true', dest='use_hashes', default=False,
471
            help='provide hashmap file instead of data')
472
        parser.add_argument('--unchunked', action='store_true', dest='unchunked', default=False,
473
            help='avoid chunked transfer mode')
474
        parser.add_argument('--etag', action='store', dest='etag', default=None,
475
            help='check written data')
476
        parser.add_argument('--content-encoding', action='store', dest='content_encoding',
477
            default=None, help='provide the object MIME content type')
478
        parser.add_argument('--content-disposition', action='store', dest='content_disposition',
479
            default=None, help='provide the presentation style of the object')
480
        parser.add_argument('--content-type', action='store', dest='content_type', default=None,
481
            help='create object with specific content type')
482
        parser.add_argument('--sharing', action='store', dest='sharing', default=None,
483
            help='define sharing object policy ( "read=user1,grp1,user2,... write=user1,grp2,...')
484
        parser.add_argument('--public', action='store_true', dest='public', default=False,
485
            help='make object publicly accessible')
486

    
487
    def getsharing(self, orelse={}):
488
        permstr = getattr(self.args, 'sharing')
489
        if permstr is None:
490
            return orelse
491
        perms = {}
492
        for p in permstr.split(' '):
493
            (key, val) = p.split('=')
494
            if key.lower() not in ('read', 'write'):
495
                raise CLIError(message='in --sharing: Invalid permition key', importance=1)
496
            val_list = val.split(',')
497
            if not perms.has_key(key):
498
                perms[key]=[]
499
            for item in val_list:
500
                if item not in perms[key]:
501
                    perms[key].append(item)
502
        return perms
503

    
504
    def main(self, local_path, container____path__):
505
        super(self.__class__, self).main(container____path__)
506
        remote_path = local_path if self.path is None else self.path
507
        try:
508
            with open(local_path) as f:
509
                if getattr(self.args, 'unchunked'):
510
                    self.client.upload_object_unchunked(remote_path, f,
511
                    etag=getattr(self.args, 'etag'), withHashFile=getattr(self.args, 'use_hashes'),
512
                    content_encoding=getattr(self.args, 'content_encoding'),
513
                    content_disposition=getattr(self.args, 'content_disposition'),
514
                    content_type=getattr(self.args, 'content_type'), sharing=self.getsharing(),
515
                    public=getattr(self.args, 'public'))
516
                else:
517
                    hash_cb = self.progress('Calculating block hashes')
518
                    upload_cb = self.progress('Uploading blocks')
519
                    self.client.upload_object(remote_path, f, hash_cb=hash_cb, upload_cb=upload_cb,
520
                    content_encoding=getattr(self.args, 'content_encoding'),
521
                    content_disposition=getattr(self.args, 'content_disposition'),
522
                    content_type=getattr(self.args, 'content_type'), sharing=self.getsharing(),
523
                    public=getattr(self.args, 'public'))
524
        except ClientError as err:
525
            raiseCLIError(err)
526

    
527
@command()
528
class store_download(_store_container_command):
529
    """Download a file"""
530

    
531
    def update_parser(self, parser):
532
        super(self.__class__, self).update_parser(parser)
533
        parser.add_argument('--no-progress-bar', action='store_true', dest='no_progress_bar',
534
            default=False, help='Dont display progress bars')
535
        parser.add_argument('--overide', action='store_true', dest='overide', default=False,
536
            help='Force download to overide an existing file')
537
        parser.add_argument('--range', action='store', dest='range', default=None,
538
            help='show range of data')
539
        parser.add_argument('--if-match', action='store', dest='if_match', default=None,
540
            help='show output if ETags match')
541
        parser.add_argument('--if-none-match', action='store', dest='if_none_match', default=None,
542
            help='show output if ETags don\'t match')
543
        parser.add_argument('--if-modified-since', action='store', dest='if_modified_since',
544
            default=None, help='show output if modified since then')
545
        parser.add_argument('--if-unmodified-since', action='store', dest='if_unmodified_since',
546
            default=None, help='show output if not modified since then')
547
        parser.add_argument('--object-version', action='store', dest='object_version', default=None,
548
            help='get the specific version')
549

    
550
    def main(self, container___path, local_path=None):
551
        super(self.__class__, self).main(container___path, path_is_optional=False)
552

    
553
        #setup output stream
554
        if local_path is None:
555
            out = stdout
556
        else:
557
            try:
558
                if getattr(self.args, 'overide'):
559
                    out = open(local_path, 'w+')
560
                else:
561
                    out = open(local_path, 'a+')
562
            except IOError as err:
563
                raise CLIError(message='Cannot write to file %s - %s'%(local_path,unicode(err)),
564
                    importance=1)
565
        download_cb = None if getattr(self.args, 'no_progress_bar') \
566
            else self.progress('Downloading')
567

    
568
        try:
569
            self.client.download_object(self.path, out, download_cb, 
570
                range=getattr(self.args, 'range'), version=getattr(self.args,'object_version'),
571
                if_match=getattr(self.args, 'if_match'), overide=getattr(self.args, 'overide'),
572
                if_none_match=getattr(self.args, 'if_none_match'),
573
                if_modified_since=getattr(self.args, 'if_modified_since'),
574
                if_unmodified_since=getattr(self.args, 'if_unmodified_since'))
575
        except ClientError as err:
576
            raiseCLIError(err)
577
        except KeyboardInterrupt:
578
            print('\ndownload canceled by user')
579
            if local_path is not None:
580
                print('re-run command to resume')
581

    
582
@command()
583
class store_hashmap(_store_container_command):
584
    """Get the hashmap of an object"""
585

    
586
    def update_parser(self, parser):
587
        super(self.__class__, self).update_parser(parser)
588
        parser.add_argument('--if-match', action='store', dest='if_match', default=None,
589
            help='show output if ETags match')
590
        parser.add_argument('--if-none-match', action='store', dest='if_none_match', default=None,
591
            help='show output if ETags dont match')
592
        parser.add_argument('--if-modified-since', action='store', dest='if_modified_since',
593
            default=None, help='show output if modified since then')
594
        parser.add_argument('--if-unmodified-since', action='store', dest='if_unmodified_since',
595
            default=None, help='show output if not modified since then')
596
        parser.add_argument('--object-version', action='store', dest='object_version', default=None,
597
            help='get the specific version')
598

    
599
    def main(self, container___path):
600
        super(self.__class__, self).main(container___path, path_is_optional=False)
601
        try:
602
            data = self.client.get_object_hashmap(self.path,
603
                version=getattr(self.args, 'object_version'),
604
                if_match=getattr(self.args, 'if_match'),
605
                if_none_match=getattr(self.args, 'if_none_match'),
606
                if_modified_since=getattr(self.args, 'if_modified_since'),
607
                if_unmodified_since=getattr(self.args, 'if_unmodified_since'))
608
        except ClientError as err:
609
            raiseCLIError(err)
610
        print_dict(data)
611

    
612
@command()
613
class store_delete(_store_container_command):
614
    """Delete a container [or an object]"""
615

    
616
    def update_parser(self, parser):
617
        super(self.__class__, self).update_parser(parser)
618
        parser.add_argument('--until', action='store', dest='until', default=None,
619
            help='remove history until that date')
620
        parser.add_argument('--format', action='store', dest='format', default='%d/%m/%Y %H:%M:%S',
621
            help='format to parse until date (default: d/m/Y H:M:S)')
622
        parser.add_argument('--delimiter', action='store', dest='delimiter',
623
            default=None, 
624
            help='mass delete objects with path staring with <object><delimiter>')
625
        parser.add_argument('-r', action='store_true', dest='recursive', default=False,
626
            help='empty dir or container and delete (if dir)')
627
    
628
    def getuntil(self, orelse=None):
629
        if hasattr(self.args, 'until'):
630
            import time
631
            until = getattr(self.args, 'until')
632
            if until is None:
633
                return None
634
            format = getattr(self.args, 'format')
635
            try:
636
                t = time.strptime(until, format)
637
            except ValueError as err:
638
                raise CLIError(message='in --until: '+unicode(err), importance=1)
639
            return int(time.mktime(t))
640
        return orelse
641

    
642
    def getdelimiter(self, orelse=None):
643
        try:
644
            dlm = getattr(self.args, 'delimiter')
645
            if dlm is None:
646
                return '/' if getattr(self.args, 'recursive') else orelse
647
        except AttributeError:
648
            return orelse
649
        return dlm
650

    
651
    def main(self, container____path__):
652
        super(self.__class__, self).main(container____path__)
653
        try:
654
            if self.path is None:
655
                self.client.del_container(until=self.getuntil(), delimiter=self.getdelimiter())
656
            else:
657
                #self.client.delete_object(self.path)
658
                self.client.del_object(self.path, until=self.getuntil(),
659
                    delimiter=self.getdelimiter())
660
        except ClientError as err:
661
            raiseCLIError(err)
662

    
663
@command()
664
class store_purge(_store_account_command):
665
    """Purge a container"""
666
    
667
    def main(self, container):
668
        super(self.__class__, self).main()
669
        try:
670
            self.client.container = container
671
            self.client.purge_container()
672
        except ClientError as err:
673
            raiseCLIError(err)
674

    
675
@command()
676
class store_publish(_store_container_command):
677
    """Publish an object"""
678

    
679
    def main(self, container___path):
680
        super(self.__class__, self).main(container___path, path_is_optional=False)
681
        try:
682
            self.client.publish_object(self.path)
683
        except ClientError as err:
684
            raiseCLIError(err)
685

    
686
@command()
687
class store_unpublish(_store_container_command):
688
    """Unpublish an object"""
689

    
690
    def main(self, container___path):
691
        super(self.__class__, self).main(container___path, path_is_optional=False)
692
        try:
693
            self.client.unpublish_object(self.path)
694
        except ClientError as err:
695
            raiseCLIError(err)
696

    
697
@command()
698
class store_permitions(_store_container_command):
699
    """Get object read/write permitions"""
700

    
701
    def main(self, container___path):
702
        super(self.__class__, self).main(container___path, path_is_optional=False)
703
        try:
704
            reply = self.client.get_object_sharing(self.path)
705
            print_dict(reply)
706
        except ClientError as err:
707
            raiseCLIError(err)
708

    
709
@command()
710
class store_setpermitions(_store_container_command):
711
    """Set sharing permitions"""
712

    
713
    def main(self, container___path, *permitions):
714
        super(self.__class__, self).main(container___path, path_is_optional=False)
715
        read = False
716
        write = False
717
        for perms in permitions:
718
            splstr = perms.split('=')
719
            if 'read' == splstr[0]:
720
                read = [user_or_group.strip() \
721
                for user_or_group in splstr[1].split(',')]
722
            elif 'write' == splstr[0]:
723
                write = [user_or_group.strip() \
724
                for user_or_group in splstr[1].split(',')]
725
            else:
726
                read = False
727
                write = False
728
        if not read and not write:
729
            raise CLIError(message='Usage:\tread=<groups,users> write=<groups,users>',
730
                importance=0)
731
        try:
732
            self.client.set_object_sharing(self.path,
733
                read_permition=read, write_permition=write)
734
        except ClientError as err:
735
            raiseCLIError(err)
736

    
737
@command()
738
class store_delpermitions(_store_container_command):
739
    """Delete all sharing permitions"""
740

    
741
    def main(self, container___path):
742
        super(self.__class__, self).main(container___path, path_is_optional=False)
743
        try:
744
            self.client.del_object_sharing(self.path)
745
        except ClientError as err:
746
            raiseCLIError(err)
747

    
748
@command()
749
class store_info(_store_container_command):
750
    """Get information for account [, container [or object]]"""
751

    
752
    
753
    def main(self, container____path__=None):
754
        super(self.__class__, self).main(container____path__)
755
        try:
756
            if self.container is None:
757
                reply = self.client.get_account_info()
758
            elif self.path is None:
759
                reply = self.client.get_container_info(self.container)
760
            else:
761
                reply = self.client.get_object_info(self.path)
762
        except ClientError as err:
763
            raiseCLIError(err)
764
        print_dict(reply)
765

    
766
@command()
767
class store_meta(_store_container_command):
768
    """Get custom meta-content for account [, container [or object]]"""
769

    
770
    def update_parser(self, parser):
771
        super(self.__class__, self).update_parser(parser)
772
        parser.add_argument('-l', action='store_true', dest='detail', default=False,
773
            help='show detailed output')
774
        parser.add_argument('--until', action='store', dest='until', default=None,
775
            help='show metadata until that date')
776
        dateformat='%d/%m/%Y %H:%M:%S'
777
        parser.add_argument('--format', action='store', dest='format', default=dateformat,
778
            help='format to parse until date (default: "d/m/Y H:M:S")')
779
        parser.add_argument('--object_version', action='store', dest='object_version', default=None,
780
            help='show specific version \ (applies only for objects)')
781

    
782
    def getuntil(self, orelse=None):
783
        if hasattr(self.args, 'until'):
784
            import time
785
            until = getattr(self.args, 'until')
786
            if until is None:
787
                return None
788
            format = getattr(self.args, 'format')
789
            #except TypeError:
790
            try:
791
                t = time.strptime(until, format)
792
            except ValueError as err:
793
                raise CLIError(message='in --until: '+unicode(err), importance=1)
794
            return int(time.mktime(t))
795
        return orelse
796

    
797
    def main(self, container____path__ = None):
798
        super(self.__class__, self).main(container____path__)
799

    
800
        try:
801
            if self.container is None:
802
                print(bold(self.client.account))
803
                r = self.client.account_head(until=self.getuntil())
804
                reply = r.headers if getattr(self.args, 'detail') \
805
                    else pretty_keys(filter_in(r.headers, 'X-Account-Meta'), '-')
806
            elif self.path is None:
807
                print(bold(self.client.account+': '+self.container))
808
                r = self.client.container_head(until=self.getuntil())
809
                if getattr(self.args, 'detail'):
810
                    reply = r.headers
811
                else:
812
                    cmeta = 'container-meta'
813
                    ometa = 'object-meta'
814
                    reply = {cmeta:pretty_keys(filter_in(r.headers, 'X-Container-Meta'), '-'),
815
                        ometa:pretty_keys(filter_in(r.headers, 'X-Container-Object-Meta'), '-')}
816
            else:
817
                print(bold(self.client.account+': '+self.container+':'+self.path))
818
                r = self.client.object_head(self.path, version=getattr(self.args, 'object_version'))
819
                reply = r.headers if getattr(self.args, 'detail') \
820
                    else pretty_keys(filter_in(r.headers, 'X-Object-Meta'), '-')
821
        except ClientError as err:
822
            raiseCLIError(err)
823
        print_dict(reply)
824

    
825
@command()
826
class store_setmeta(_store_container_command):
827
    """Set a new metadatum for account [, container [or object]]"""
828

    
829
    def main(self, metakey, metavalue, container____path__=None):
830
        super(self.__class__, self).main(container____path__)
831
        try:
832
            if self.container is None:
833
                self.client.set_account_meta({metakey:metavalue})
834
            elif self.path is None:
835
                self.client.set_container_meta({metakey:metavalue})
836
            else:
837
                self.client.set_object_meta(self.path, {metakey:metavalue})
838
        except ClientError as err:
839
            raiseCLIError(err)
840

    
841
@command()
842
class store_delmeta(_store_container_command):
843
    """Delete an existing metadatum of account [, container [or object]]"""
844

    
845
    def main(self, metakey, container____path__=None):
846
        super(self.__class__, self).main(container____path__)
847
        try:
848
            if self.container is None:
849
                self.client.del_account_meta(metakey)
850
            elif self.path is None:
851
                self.client.del_container_meta(metakey)
852
            else:
853
                self.client.del_object_meta(metakey, self.path)
854
        except ClientError as err:
855
            raiseCLIError(err)
856

    
857
@command()
858
class store_quota(_store_account_command):
859
    """Get  quota for account [or container]"""
860

    
861
    def main(self, container = None):
862
        super(self.__class__, self).main()
863
        try:
864
            if container is None:
865
                reply = self.client.get_account_quota()
866
            else:
867
                reply = self.client.get_container_quota(container)
868
        except ClientError as err:
869
            raiseCLIError(err)
870
        print_dict(reply)
871

    
872
@command()
873
class store_setquota(_store_account_command):
874
    """Set new quota (in KB) for account [or container]"""
875

    
876
    def main(self, quota, container = None):
877
        super(self.__class__, self).main()
878
        try:
879
            if container is None:
880
                self.client.set_account_quota(quota)
881
            else:
882
                self.client.container = container
883
                self.client.set_container_quota(quota)
884
        except ClientError as err:
885
            raiseCLIError(err)
886

    
887
@command()
888
class store_versioning(_store_account_command):
889
    """Get  versioning for account [or container ]"""
890

    
891
    def main(self, container = None):
892
        super(self.__class__, self).main()
893
        try:
894
            if container is None:
895
                reply = self.client.get_account_versioning()
896
            else:
897
                reply = self.client.get_container_versioning(container)
898
        except ClientError as err:
899
            raiseCLIError(err)
900
        print_dict(reply)
901

    
902
@command()
903
class store_setversioning(_store_account_command):
904
    """Set new versioning (auto, none) for account [or container]"""
905

    
906
    def main(self, versioning, container = None):
907
        super(self.__class__, self).main()
908
        try:
909
            if container is None:
910
                self.client.set_account_versioning(versioning)
911
            else:
912
                self.client.container = container
913
                self.client.set_container_versioning(versioning)
914
        except ClientError as err:
915
            raiseCLIError(err)
916

    
917
@command()
918
class store_group(_store_account_command):
919
    """Get user groups details for account"""
920

    
921
    def main(self):
922
        super(self.__class__, self).main()
923
        try:
924
            reply = self.client.get_account_group()
925
        except ClientError as err:
926
            raiseCLIError(err)
927
        print_dict(reply)
928

    
929
@command()
930
class store_setgroup(_store_account_command):
931
    """Create/update a new user group on account"""
932

    
933
    def main(self, groupname, *users):
934
        super(self.__class__, self).main()
935
        try:
936
            self.client.set_account_group(groupname, users)
937
        except ClientError as err:
938
            raiseCLIError(err)
939

    
940
@command()
941
class store_delgroup(_store_account_command):
942
    """Delete a user group on an account"""
943

    
944
    def main(self, groupname):
945
        super(self.__class__, self).main()
946
        try:
947
            self.client.del_account_group(groupname)
948
        except ClientError as err:
949
            raiseCLIError(err)