Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / pithos_cli.py @ 1e29b9f6

History | View | Annotate | Download (46.1 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
35
from kamaki.cli.command_tree import CommandTree
36
from kamaki.cli.errors import raiseCLIError, CLISyntaxError
37
from kamaki.cli.utils import format_size, print_dict, pretty_keys, page_hold
38
from kamaki.cli.argument import FlagArgument, ValueArgument, IntArgument
39
from kamaki.cli.argument import KeyValueArgument, DateArgument
40
from kamaki.cli.argument import ProgressBarArgument
41
from kamaki.cli.commands import _command_init
42
from kamaki.clients.pithos import PithosClient, ClientError
43
from kamaki.cli.utils import bold
44
from sys import stdout
45
from time import localtime, strftime
46
from logging import getLogger
47

    
48
kloger = getLogger('kamaki')
49

    
50
pithos_cmds = CommandTree('store', 'Pithos+ storage commands')
51
_commands = [pithos_cmds]
52

    
53

    
54
about_directories = [
55
    'Kamaki hanldes directories the same way as OOS Storage and Pithos+:',
56
    'A directory is an object with type "application/directory"',
57
    'An object with path dir/name can exist even if dir does not exist or',
58
    'even if dir is a non directory object. Users can modify dir without',
59
    'affecting the dir/name object in any way.']
60

    
61

    
62
# Argument functionality
63

    
64

    
65
class DelimiterArgument(ValueArgument):
66
    """
67
    :value type: string
68
    :value returns: given string or /
69
    """
70

    
71
    def __init__(self, caller_obj, help='', parsed_name=None, default=None):
72
        super(DelimiterArgument, self).__init__(help, parsed_name, default)
73
        self.caller_obj = caller_obj
74

    
75
    @property
76
    def value(self):
77
        if self.caller_obj['recursive']:
78
            return '/'
79
        return getattr(self, '_value', self.default)
80

    
81
    @value.setter
82
    def value(self, newvalue):
83
        self._value = newvalue
84

    
85

    
86
class SharingArgument(ValueArgument):
87
    """Set sharing (read and/or write) groups
88
    .
89
    :value type: "read=term1,term2,... write=term1,term2,..."
90
    .
91
    :value returns: {'read':['term1', 'term2', ...],
92
    .   'write':['term1', 'term2', ...]}
93
    """
94

    
95
    @property
96
    def value(self):
97
        return getattr(self, '_value', self.default)
98

    
99
    @value.setter
100
    def value(self, newvalue):
101
        perms = {}
102
        try:
103
            permlist = newvalue.split(' ')
104
        except AttributeError:
105
            return
106
        for p in permlist:
107
            try:
108
                (key, val) = p.split('=')
109
            except ValueError as err:
110
                raiseCLIError(err, 'Error in --sharing',
111
                    details='Incorrect format',
112
                    importance=1)
113
            if key.lower() not in ('read', 'write'):
114
                raiseCLIError(err, 'Error in --sharing',
115
                    details='Invalid permission key %s' % key,
116
                    importance=1)
117
            val_list = val.split(',')
118
            if not key in perms:
119
                perms[key] = []
120
            for item in val_list:
121
                if item not in perms[key]:
122
                    perms[key].append(item)
123
        self._value = perms
124

    
125

    
126
class RangeArgument(ValueArgument):
127
    """
128
    :value type: string of the form <start>-<end> where <start> and <end> are
129
        integers
130
    :value returns: the input string, after type checking <start> and <end>
131
    """
132

    
133
    @property
134
    def value(self):
135
        return getattr(self, '_value', self.default)
136

    
137
    @value.setter
138
    def value(self, newvalue):
139
        if newvalue is None:
140
            self._value = self.default
141
            return
142
        (start, end) = newvalue.split('-')
143
        (start, end) = (int(start), int(end))
144
        self._value = '%s-%s' % (start, end)
145

    
146
# Command specs
147

    
148

    
149
class _pithos_init(_command_init):
150
    """Initialize a pithos+ kamaki client"""
151

    
152
    def main(self):
153
        self.token = self.config.get('store', 'token')\
154
            or self.config.get('global', 'token')
155
        self.base_url = self.config.get('store', 'url')\
156
            or self.config.get('global', 'url')
157
        self.account = self.config.get('store', 'account')\
158
            or self.config.get('global', 'account')
159
        self.container = self.config.get('store', 'container')\
160
            or self.config.get('global', 'container')
161
        self.client = PithosClient(base_url=self.base_url,
162
            token=self.token,
163
            account=self.account,
164
            container=self.container)
165

    
166

    
167
class _store_account_command(_pithos_init):
168
    """Base class for account level storage commands"""
169

    
170
    def __init__(self, arguments={}):
171
        super(_store_account_command, self).__init__(arguments)
172
        self['account'] = ValueArgument(
173
            'Set user account (not permanent)',
174
            '--account')
175

    
176
    def main(self):
177
        super(_store_account_command, self).main()
178
        if self['account']:
179
            self.client.account = self['account']
180

    
181

    
182
class _store_container_command(_store_account_command):
183
    """Base class for container level storage commands"""
184

    
185
    generic_err_details = ['To specify a container:',
186
    '  1. Set store.container variable (permanent)',
187
    '     /config set store.container <container>',
188
    '  2. --container=<container> (temporary, overrides 1)',
189
    '  3. Use the container:path format (temporary, overrides all)']
190
    container = None
191
    path = None
192

    
193
    def __init__(self, arguments={}):
194
        super(_store_container_command, self).__init__(arguments)
195
        self['container'] = ValueArgument(
196
            'Set container to work with (temporary)',
197
            '--container')
198

    
199
    def extract_container_and_path(self,
200
        container_with_path,
201
        path_is_optional=True):
202
        try:
203
            assert isinstance(container_with_path, str)
204
        except AssertionError as err:
205
            raiseCLIError(err)
206

    
207
        cont, sep, path = container_with_path.partition(':')
208

    
209
        if sep:
210
            if not cont:
211
                raiseCLIError(CLISyntaxError('Container is missing\n',
212
                    details=self.generic_err_details))
213
            alt_cont = self['container']
214
            if alt_cont and cont != alt_cont:
215
                raiseCLIError(CLISyntaxError(
216
                    'Conflict: 2 containers (%s, %s)' % (cont, alt_cont),
217
                    details=self.generic_err_details)
218
                )
219
            self.container = cont
220
            if not path:
221
                raiseCLIError(CLISyntaxError(
222
                    'Path is missing for object in container %s' % cont,
223
                    details=self.generic_err_details)
224
                )
225
            self.path = path
226
        else:
227
            alt_cont = self['container'] or self.client.container
228
            if alt_cont:
229
                self.container = alt_cont
230
                self.path = cont
231
            elif path_is_optional:
232
                self.container = cont
233
                self.path = None
234
            else:
235
                self.container = cont
236
                raiseCLIError(CLISyntaxError(
237
                    'Both container and path are required',
238
                    details=self.generic_err_details)
239
                )
240

    
241
    def main(self, container_with_path=None, path_is_optional=True):
242
        super(_store_container_command, self).main()
243
        if container_with_path is not None:
244
            self.extract_container_and_path(
245
                container_with_path,
246
                path_is_optional)
247
            self.client.container = self.container
248
        elif self['container']:
249
            self.client.container = self['container']
250
        self.container = self.client.container
251

    
252

    
253
@command(pithos_cmds)
254
class store_list(_store_container_command):
255
    """List containers, object trees or objects in a directory
256
    Use with:
257
    1 no parameters : containers in set account
258
    2. one parameter (container) or --container : contents of container
259
    3. <container>:<prefix> or --container=<container> <prefix>: objects in
260
    .   container starting with prefix
261
    """
262

    
263
    arguments = dict(
264
        detail=FlagArgument('show detailed output', '-l'),
265
        limit=IntArgument('limit the number of listed items', '-n'),
266
        marker=ValueArgument('show output greater that marker', '--marker'),
267
        prefix=ValueArgument('show output starting with prefix', '--prefix'),
268
        delimiter=ValueArgument('show output up to delimiter', '--delimiter'),
269
        path=ValueArgument(
270
            'show output starting with prefix up to /',
271
            '--path'),
272
        meta=ValueArgument(
273
            'show output with specified meta keys',
274
            '--meta',
275
            default=[]),
276
        if_modified_since=ValueArgument(
277
            'show output modified since then',
278
            '--if-modified-since'),
279
        if_unmodified_since=ValueArgument(
280
            'show output not modified since then',
281
            '--if-unmodified-since'),
282
        until=DateArgument('show metadata until then', '--until'),
283
        format=ValueArgument(
284
            'format to parse until data (default: d/m/Y H:M:S )',
285
            '--format'),
286
        shared=FlagArgument('show only shared', '--shared'),
287
        public=FlagArgument('show only public', '--public'),
288
        more=FlagArgument(
289
            'output results in pages (-n to set items per page, default 10)',
290
            '--more')
291
    )
292

    
293
    def print_objects(self, object_list):
294
        limit = int(self['limit']) if self['limit'] > 0 else len(object_list)
295
        for index, obj in enumerate(object_list):
296
            if 'content_type' not in obj:
297
                continue
298
            pretty_obj = obj.copy()
299
            index += 1
300
            empty_space = ' ' * (len(str(len(object_list))) - len(str(index)))
301
            if obj['content_type'] == 'application/directory':
302
                isDir = True
303
                size = 'D'
304
            else:
305
                isDir = False
306
                size = format_size(obj['bytes'])
307
                pretty_obj['bytes'] = '%s (%s)' % (obj['bytes'], size)
308
            oname = bold(obj['name'])
309
            if self['detail']:
310
                print('%s%s. %s' % (empty_space, index, oname))
311
                print_dict(pretty_keys(pretty_obj), exclude=('name'))
312
                print
313
            else:
314
                oname = '%s%s. %6s %s' % (empty_space, index, size, oname)
315
                oname += '/' if isDir else ''
316
                print(oname)
317
            if self['more']:
318
                page_hold(index, limit, len(object_list))
319

    
320
    def print_containers(self, container_list):
321
        limit = int(self['limit']) if self['limit'] > 0\
322
            else len(container_list)
323
        for index, container in enumerate(container_list):
324
            if 'bytes' in container:
325
                size = format_size(container['bytes'])
326
            cname = '%s. %s' % (index + 1, bold(container['name']))
327
            if self['detail']:
328
                print(cname)
329
                pretty_c = container.copy()
330
                if 'bytes' in container:
331
                    pretty_c['bytes'] = '%s (%s)' % (container['bytes'], size)
332
                print_dict(pretty_keys(pretty_c), exclude=('name'))
333
                print
334
            else:
335
                if 'count' in container and 'bytes' in container:
336
                    print('%s (%s, %s objects)'\
337
                    % (cname, size, container['count']))
338
                else:
339
                    print(cname)
340
            if self['more']:
341
                page_hold(index + 1, limit, len(container_list))
342

    
343
    def main(self, container____path__=None):
344
        super(self.__class__, self).main(container____path__)
345
        try:
346
            if self.container is None:
347
                r = self.client.account_get(
348
                    limit=False if self['more'] else self['limit'],
349
                    marker=self['marker'],
350
                    if_modified_since=self['if_modified_since'],
351
                    if_unmodified_since=self['if_unmodified_since'],
352
                    until=self['until'],
353
                    show_only_shared=self['shared'])
354
                self.print_containers(r.json)
355
            else:
356
                prefix = self.path if self.path\
357
                else self['prefix']
358
                r = self.client.container_get(
359
                    limit=False if self['more'] else self['limit'],
360
                    marker=self['marker'],
361
                    prefix=prefix,
362
                    delimiter=self['delimiter'],
363
                    path=self['path'],
364
                    if_modified_since=self['if_modified_since'],
365
                    if_unmodified_since=self['if_unmodified_since'],
366
                    until=self['until'],
367
                    meta=self['meta'],
368
                    show_only_shared=self['shared'])
369
                self.print_objects(r.json)
370
        except ClientError as err:
371
            if err.status == 404:
372
                if 'container' in ('%s' % err).lower():
373
                    raiseCLIError(
374
                        err,
375
                        'No container %s in account %s'\
376
                        % (self.container, self.account),
377
                        details=self.generic_err_details)
378
                elif 'object' in ('%s' % err).lower():
379
                    raiseCLIError(
380
                        err,
381
                        'No object %s in %s\'s container %s'\
382
                        % (self.path, self.account, self.container),
383
                        details=self.generic_err_details)
384
            raiseCLIError(err)
385
        except Exception as e:
386
            raiseCLIError(e)
387

    
388

    
389
@command(pithos_cmds)
390
class store_mkdir(_store_container_command):
391
    """Create a directory
392
    """
393

    
394
    __doc__ += '\n. '.join(about_directories)
395

    
396
    def main(self, container___directory):
397
        super(self.__class__,
398
            self).main(container___directory, path_is_optional=False)
399
        try:
400
            self.client.create_directory(self.path)
401
        except ClientError as err:
402
            if err.status == 404:
403
                if 'container' in ('%s' % err).lower():
404
                    raiseCLIError(
405
                        err,
406
                        'No container %s in account %s'\
407
                        % (self.container, self.account),
408
                        details=self.generic_err_details)
409
            raiseCLIError(err)
410
        except Exception as err:
411
            raiseCLIError(err)
412

    
413

    
414
@command(pithos_cmds)
415
class store_touch(_store_container_command):
416
    """Create an empty object (file)
417
    If object exists, this command will reset it to 0 length
418
    """
419

    
420
    arguments = dict(
421
        content_type=ValueArgument(
422
            'Set content type (default: application/octet-stream)',
423
            '--content-type',
424
            default='application/octet-stream')
425
    )
426

    
427
    def main(self, container___path):
428
        super(store_touch, self).main(container___path)
429
        try:
430
            self.client.create_object(self.path, self['content_type'])
431
        except ClientError as err:
432
            if err.status == 404:
433
                if 'container' in ('%s' % err).lower():
434
                    raiseCLIError(
435
                        err,
436
                        'No container %s in account %s'\
437
                        % (self.container, self.account),
438
                        details=self.generic_err_details)
439
            raiseCLIError(err)
440
        except Exception as err:
441
            raiseCLIError(err)
442

    
443

    
444
@command(pithos_cmds)
445
class store_create(_store_account_command):
446
    """Create a container"""
447

    
448
    arguments = dict(
449
        versioning=ValueArgument(
450
            'set container versioning (auto/none)',
451
            '--versioning'),
452
        quota=IntArgument('set default container quota', '--quota'),
453
        meta=KeyValueArgument(
454
            'set container metadata (can be repeated)',
455
            '--meta')
456
    )
457

    
458
    def main(self, container):
459
        super(self.__class__, self).main(container)
460
        try:
461
            self.client.container_put(quota=self['quota'],
462
                versioning=self['versioning'],
463
                metadata=self['meta'])
464
        except ClientError as err:
465
            if err.status == 404:
466
                if 'container' in ('%s' % err).lower():
467
                    raiseCLIError(
468
                        err,
469
                        'No container %s in account %s'\
470
                        % (self.container, self.account),
471
                        details=self.generic_err_details)
472
            raiseCLIError(err)
473
        except Exception as e:
474
            raiseCLIError(e)
475

    
476

    
477
@command(pithos_cmds)
478
class store_copy(_store_container_command):
479
    """Copy an object from container to (another) container
480
    Use options:
481
    1. <container1>:<path1> <container2>[:path2] : from container1 to 2, if
482
    path2 is not given, target path will be container2:path1
483
    2. <container>:<path1> <container>:<path2> : make a copy in the same
484
    container
485
    3. Use --container= instead of <container1>, but only for the first
486
    parameter
487
    """
488

    
489
    arguments = dict(
490
        source_version=ValueArgument(
491
            'copy specific version',
492
            '--source-version'),
493
        public=ValueArgument('make object publicly accessible', '--public'),
494
        content_type=ValueArgument(
495
            'change object\'s content type',
496
            '--content-type'),
497
        recursive=FlagArgument(
498
            'mass copy with delimiter /',
499
            ('-r', '--recursive'))
500
    )
501

    
502
    def __init__(self, arguments={}):
503
        super(self.__class__, self).__init__(arguments)
504
        self['delimiter'] = DelimiterArgument(
505
            self,
506
            parsed_name='--delimiter',
507
            help=u'copy objects prefixed as src_object + delimiter')
508

    
509
    def main(self, source_container___path, destination_container____path__):
510
        super(self.__class__,
511
            self).main(source_container___path, path_is_optional=False)
512
        try:
513
            dst = destination_container____path__.split(':')
514
            dst_cont = dst[0]
515
            dst_path = dst[1] if len(dst) > 1 else False
516
            self.client.copy_object(src_container=self.container,
517
                src_object=self.path,
518
                dst_container=dst_cont,
519
                dst_object=dst_path,
520
                source_version=self['source_version'],
521
                public=self['public'],
522
                content_type=self['content_type'],
523
                delimiter=self['delimiter'])
524
        except ClientError as err:
525
            raiseCLIError(err)
526

    
527

    
528
@command(pithos_cmds)
529
class store_move(_store_container_command):
530
    """Copy an object
531
    Use options:
532
    1. <container1>:<path1> <container2>[:path2] : from container1 to 2, if
533
    path2 is not given, target path will be container2:path1
534
    2. <container>:<path1> <container>:<path2> : rename
535
    3. Use --container= instead of <container1>, but only for the first
536
    parameter
537
    """
538

    
539
    arguments = dict(
540
        source_version=ValueArgument('specify version', '--source-version'),
541
        public=FlagArgument('make object publicly accessible', '--public'),
542
        content_type=ValueArgument('modify content type', '--content-type'),
543
        recursive= FlagArgument('up to delimiter /', ('-r', '--recursive'))
544
    )
545

    
546
    def __init__(self, arguments={}):
547
        super(self.__class__, self).__init__(arguments)
548
        self['delimiter']=DelimiterArgument(
549
            self,
550
            parsed_name='--delimiter',
551
            help=u'move objects prefixed as src_object + delimiter')
552

    
553
    def main(self, source_container___path, destination_container____path__):
554
        super(self.__class__,
555
            self).main(source_container___path, path_is_optional=False)
556
        try:
557
            dst = destination_container____path__.split(':')
558
            dst_cont = dst[0]
559
            dst_path = dst[1] if len(dst) > 1 else False
560
            self.client.move_object(src_container=self.container,
561
                src_object=self.path,
562
                dst_container=dst_cont,
563
                dst_object=dst_path,
564
                source_version=self['source_version'],
565
                public=self['public'],
566
                content_type=self['content_type'],
567
                delimiter=self['delimiter'])
568
        except ClientError as err:
569
            raiseCLIError(err)
570

    
571

    
572
@command(pithos_cmds)
573
class store_append(_store_container_command):
574
    """Append local file to (existing) remote object"""
575

    
576
    arguments = dict(
577
        progress_bar=ProgressBarArgument(
578
            'do not show progress bar',
579
            '--no-progress-bar',
580
            default=False)
581
    )
582

    
583
    def main(self, local_path, container___path):
584
        super(self.__class__,
585
            self).main(container___path, path_is_optional=False)
586
        try:
587
            f = open(local_path, 'rb')
588
            progress_bar = self.arguments['progress_bar']
589
            try:
590
                upload_cb = progress_bar.get_generator('Appending blocks')
591
            except Exception:
592
                upload_cb = None
593
            self.client.append_object(object=self.path,
594
                source_file=f,
595
                upload_cb=upload_cb)
596
        except ClientError as err:
597
            progress_bar.finish()
598
            raiseCLIError(err)
599
        finally:
600
            progress_bar.finish()
601

    
602

    
603
@command(pithos_cmds)
604
class store_truncate(_store_container_command):
605
    """Truncate remote file up to a size"""
606

    
607
    def main(self, container___path, size=0):
608
        super(self.__class__,
609
            self).main(container___path, path_is_optional=False)
610
        try:
611
            self.client.truncate_object(self.path, size)
612
        except ClientError as err:
613
            raiseCLIError(err)
614

    
615

    
616
@command(pithos_cmds)
617
class store_overwrite(_store_container_command):
618
    """Overwrite part (from start to end) of a remote file"""
619

    
620
    arguments = dict(
621
        progress_bar=ProgressBarArgument(
622
            'do not show progress bar',
623
            '--no-progress-bar',
624
            default=False)
625
    )
626

    
627
    def main(self, local_path, container___path, start, end):
628
        super(self.__class__,
629
            self).main(container___path, path_is_optional=False)
630
        try:
631
            f = open(local_path, 'rb')
632
            progress_bar = self.arguments['progress_bar']
633
            try:
634
                upload_cb = progress_bar.get_generator('Overwritting blocks')
635
            except Exception:
636
                upload_cb = None
637
            self.client.overwrite_object(object=self.path,
638
                start=start,
639
                end=end,
640
                source_file=f,
641
                upload_cb=upload_cb)
642
        except ClientError as err:
643
            progress_bar.finish()
644
            raiseCLIError(err)
645
        finally:
646
            progress_bar.finish()
647

    
648

    
649
@command(pithos_cmds)
650
class store_manifest(_store_container_command):
651
    """Create a remote file with uploaded parts by manifestation"""
652

    
653
    arguments = dict(
654
        etag=ValueArgument('check written data', '--etag'),
655
        content_encoding=ValueArgument(
656
            'set MIME content type',
657
            '--content-encoding'),
658
        content_disposition=ValueArgument(
659
            'the presentation style of the object',
660
            '--content-disposition'),
661
        content_type=ValueArgument('specify content type', '--content-type'),
662
        sharing=SharingArgument(
663
            'define object sharing policy \n' +\
664
            '    ( "read=user1,grp1,user2,... write=user1,grp2,..." )',
665
            '--sharing'),
666
        public=FlagArgument('make object publicly accessible', '--public')
667
    )
668

    
669
    def main(self, container___path):
670
        super(self.__class__,
671
            self).main(container___path, path_is_optional=False)
672
        try:
673
            self.client.create_object_by_manifestation(
674
                self.path,
675
                content_encoding=self['content_encoding'],
676
                content_disposition=self['content_disposition'],
677
                content_type=self['content_type'],
678
                sharing=self['sharing'],
679
                public=self['public'])
680
        except ClientError as err:
681
            raiseCLIError(err)
682

    
683

    
684
@command(pithos_cmds)
685
class store_upload(_store_container_command):
686
    """Upload a file"""
687

    
688
    arguments = dict(
689
        use_hashes=FlagArgument(
690
            'provide hashmap file instead of data',
691
            '--use-hashes'),
692
        etag=ValueArgument('check written data', '--etag'),
693
        unchunked=FlagArgument('avoid chunked transfer mode', '--unchunked'),
694
        content_encoding=ValueArgument(
695
            'set MIME content type',
696
            '--content-encoding'),
697
        content_disposition=ValueArgument(
698
            'specify objects presentation style',
699
            '--content-disposition'),
700
        content_type=ValueArgument('specify content type', '--content-type'),
701
        sharing=SharingArgument(
702
            help='define sharing object policy \n' +\
703
            '( "read=user1,grp1,user2,... write=user1,grp2,... )',
704
            parsed_name='--sharing'),
705
        public=FlagArgument('make object publicly accessible', '--public'),
706
        poolsize=IntArgument('set pool size', '--with-pool-size'),
707
        progress_bar=ProgressBarArgument(
708
            'do not show progress bar',
709
            '--no-progress-bar',
710
            default=False)
711
    )
712

    
713
    def main(self, local_path, container____path__):
714
        super(self.__class__, self).main(container____path__)
715
        remote_path = self.path if self.path else local_path
716
        poolsize = self['poolsize']
717
        if poolsize is not None:
718
            self.client.POOL_SIZE = int(poolsize)
719
        params = dict(content_encoding=self['content_encoding'],
720
            content_type=self['content_type'],
721
            content_disposition=self['content_disposition'],
722
            sharing=self['sharing'],
723
            public=self['public'])
724
        try:
725
            progress_bar = self.arguments['progress_bar']
726
            hash_bar = progress_bar.clone()
727
            with open(local_path, 'rb') as f:
728
                if self['unchunked']:
729
                    self.client.upload_object_unchunked(
730
                        remote_path,
731
                        f,
732
                        etag=self['etag'],
733
                        withHashFile=self['use_hashes'],
734
                        **params)
735
                else:
736
                    hash_cb = hash_bar.get_generator(
737
                        'Calculating block hashes')
738
                    upload_cb = progress_bar.get_generator('Uploading')
739
                    self.client.upload_object(
740
                        remote_path,
741
                        f,
742
                        hash_cb=hash_cb,
743
                        upload_cb=upload_cb,
744
                        **params)
745
                    progress_bar.finish()
746
                    hash_bar.finish()
747
        except ClientError as err:
748
            progress_bar.finish()
749
            hash_bar.finish()
750
            raiseCLIError(err, '"%s" not accessible' % container____path__)
751
        except IOError as err:
752
            progress_bar.finish()
753
            hash_bar.finish()
754
            raiseCLIError(err, 'Failed to read form file %s' % local_path, 2)
755
        print 'Upload completed'
756

    
757

    
758
@command(pithos_cmds)
759
class store_cat(_store_container_command):
760
    """Print a file to console"""
761

    
762
    arguments = dict(
763
        range=RangeArgument('show range of data', '--range'),
764
        if_match=ValueArgument('show output if ETags match', '--if-match'),
765
        if_none_match=ValueArgument(
766
            'show output if ETags match',
767
            '--if-none-match'),
768
        if_modified_since=DateArgument(
769
            'show output modified since then',
770
            '--if-modified-since'),
771
        if_unmodified_since=DateArgument(
772
            'show output unmodified since then',
773
            '--if-unmodified-since'),
774
        object_version=ValueArgument(
775
            'get the specific version',
776
            '--object-version')
777
    )
778

    
779
    def main(self, container___path):
780
        super(self.__class__,
781
            self).main(container___path, path_is_optional=False)
782
        try:
783
            self.client.download_object(self.path, stdout,
784
            range=self['range'],
785
            version=self['object_version'],
786
            if_match=self['if_match'],
787
            if_none_match=self['if_none_match'],
788
            if_modified_since=self['if_modified_since'],
789
            if_unmodified_since=self['if_unmodified_since'])
790
        except ClientError as err:
791
            raiseCLIError(err)
792

    
793

    
794
@command(pithos_cmds)
795
class store_download(_store_container_command):
796
    """Download a file"""
797

    
798
    arguments = dict(
799
        resume=FlagArgument('Resume instead of overwrite', '--resume'),
800
        range=RangeArgument('show range of data', '--range'),
801
        if_match=ValueArgument('show output if ETags match', '--if-match'),
802
        if_none_match=ValueArgument(
803
            'show output if ETags match',
804
            '--if-none-match'),
805
        if_modified_since=DateArgument(
806
            'show output modified since then',
807
            '--if-modified-since'),
808
        if_unmodified_since=DateArgument(
809
            'show output unmodified since then',
810
            '--if-unmodified-since'),
811
        object_version=ValueArgument(
812
            'get the specific version',
813
            '--object-version'),
814
        poolsize=IntArgument('set pool size', '--with-pool-size'),
815
        progress_bar=ProgressBarArgument(
816
            'do not show progress bar',
817
            '--no-progress-bar',
818
            default=False)
819
    )
820

    
821
    def main(self, container___path, local_path):
822
        super(self.__class__,
823
            self).main(container___path, path_is_optional=False)
824

    
825
        # setup output stream
826
        if local_path is None:
827
            out = stdout
828
        else:
829
            try:
830
                if self['resume']:
831
                    out = open(local_path, 'rwb+')
832
                else:
833
                    out = open(local_path, 'wb+')
834
            except IOError as err:
835
                raiseCLIError(err, 'Cannot write to file %s' % local_path, 1)
836
        poolsize = self['poolsize']
837
        if poolsize is not None:
838
            self.client.POOL_SIZE = int(poolsize)
839

    
840
        try:
841
            progress_bar = self.arguments['progress_bar']
842
            download_cb = progress_bar.get_generator('Downloading')
843
            self.client.download_object(self.path, out,
844
                download_cb=download_cb,
845
                range=self['range'],
846
                version=self['object_version'],
847
                if_match=self['if_match'],
848
                resume=self['resume'],
849
                if_none_match=self['if_none_match'],
850
                if_modified_since=self['if_modified_since'],
851
                if_unmodified_since=self['if_unmodified_since'])
852
            progress_bar.finish()
853
        except ClientError as err:
854
            progress_bar.finish()
855
            raiseCLIError(err)
856
        except KeyboardInterrupt:
857
            from threading import enumerate as activethreads
858
            stdout.write('\nFinishing active threads ')
859
            for thread in activethreads():
860
                stdout.flush()
861
                try:
862
                    thread.join()
863
                    stdout.write('.')
864
                except RuntimeError:
865
                    continue
866
            progress_bar.finish()
867
            print('\ndownload canceled by user')
868
            if local_path is not None:
869
                print('to resume, re-run with --resume')
870
        except Exception as e:
871
            progress_bar.finish()
872
            raiseCLIError(e)
873
        print
874

    
875

    
876
@command(pithos_cmds)
877
class store_hashmap(_store_container_command):
878
    """Get the hashmap of an object"""
879

    
880
    arguments = dict(
881
        if_match=ValueArgument('show output if ETags match', '--if-match'),
882
        if_none_match=ValueArgument(
883
            'show output if ETags match',
884
            '--if-none-match'),
885
        if_modified_since=DateArgument(
886
            'show output modified since then',
887
            '--if-modified-since'),
888
        if_unmodified_since=DateArgument(
889
            'show output unmodified since then',
890
            '--if-unmodified-since'),
891
        object_version=ValueArgument(
892
            'get the specific version',
893
            '--object-version')
894
    )
895

    
896
    def main(self, container___path):
897
        super(self.__class__,
898
            self).main(container___path, path_is_optional=False)
899
        try:
900
            data = self.client.get_object_hashmap(
901
                self.path,
902
                version=self['object_version'],
903
                if_match=self['if_match'],
904
                if_none_match=self['if_none_match'],
905
                if_modified_since=self['if_modified_since'],
906
                if_unmodified_since=self['if_unmodified_since'])
907
        except ClientError as err:
908
            raiseCLIError(err)
909
        print_dict(data)
910

    
911

    
912
@command(pithos_cmds)
913
class store_delete(_store_container_command):
914
    """Delete a container [or an object]"""
915

    
916
    arguments = dict(
917
        until=DateArgument('remove history until that date', '--until'),
918
        recursive=FlagArgument(
919
            'empty dir or container and delete (if dir)',
920
            ('-r', '--recursive'))
921
    )
922

    
923
    def __init__(self, arguments={}):
924
        super(self.__class__, self).__init__(arguments)
925
        self['delimiter'] = DelimiterArgument(
926
            self,
927
            parsed_name='--delimiter',
928
            help='delete objects prefixed with <object><delimiter>')
929

    
930
    def main(self, container____path__):
931
        super(self.__class__, self).main(container____path__)
932
        try:
933
            if self.path is None:
934
                self.client.del_container(
935
                    until=self['until'],
936
                    delimiter=self['delimiter'])
937
            else:
938
                # self.client.delete_object(self.path)
939
                self.client.del_object(
940
                    self.path,
941
                    until=self['until'],
942
                    delimiter=self['delimiter'])
943
        except ClientError as err:
944
            raiseCLIError(err)
945

    
946

    
947
@command(pithos_cmds)
948
class store_purge(_store_container_command):
949
    """Purge a container
950
    To completely erase a container:
951
    .   /store delete -r <container>
952
    .   /store purge <container
953
    """
954

    
955
    def main(self, container):
956
        super(self.__class__, self).main(container)
957
        try:
958
            self.client.purge_container()
959
        except ClientError as err:
960
            raiseCLIError(err)
961

    
962

    
963
@command(pithos_cmds)
964
class store_publish(_store_container_command):
965
    """Publish the object and print the public url"""
966

    
967
    def main(self, container___path):
968
        super(self.__class__,
969
            self).main(container___path, path_is_optional=False)
970
        try:
971
            url = self.client.publish_object(self.path)
972
        except ClientError as err:
973
            raiseCLIError(err)
974
        print(url)
975

    
976

    
977
@command(pithos_cmds)
978
class store_unpublish(_store_container_command):
979
    """Unpublish an object"""
980

    
981
    def main(self, container___path):
982
        super(self.__class__,
983
            self).main(container___path, path_is_optional=False)
984
        try:
985
            self.client.unpublish_object(self.path)
986
        except ClientError as err:
987
            raiseCLIError(err)
988

    
989

    
990
@command(pithos_cmds)
991
class store_permissions(_store_container_command):
992
    """Get object read / write permissions """
993

    
994
    def main(self, container___path):
995
        super(self.__class__,
996
            self).main(container___path, path_is_optional=False)
997
        try:
998
            reply = self.client.get_object_sharing(self.path)
999
            print_dict(reply)
1000
        except ClientError as err:
1001
            raiseCLIError(err)
1002

    
1003

    
1004
@command(pithos_cmds)
1005
class store_setpermissions(_store_container_command):
1006
    """Set sharing permissions """
1007

    
1008
    def format_permition_dict(self, permissions):
1009
        read = False
1010
        write = False
1011
        for perms in permissions:
1012
            splstr = perms.split('=')
1013
            if 'read' == splstr[0]:
1014
                read = [user_or_group.strip() \
1015
                for user_or_group in splstr[1].split(',')]
1016
            elif 'write' == splstr[0]:
1017
                write = [user_or_group.strip() \
1018
                for user_or_group in splstr[1].split(',')]
1019
            else:
1020
                read = False
1021
                write = False
1022
        if not read and not write:
1023
            raiseCLIError(None,
1024
            'Usage:\tread=<groups,users> write=<groups,users>')
1025
        return (read, write)
1026

    
1027
    def main(self, container___path, *permissions):
1028
        super(self.__class__,
1029
            self).main(container___path, path_is_optional=False)
1030
        (read, write) = self.format_permition_dict(permissions)
1031
        try:
1032
            self.client.set_object_sharing(self.path,
1033
                read_permition=read, write_permition=write)
1034
        except ClientError as err:
1035
            raiseCLIError(err)
1036

    
1037

    
1038
@command(pithos_cmds)
1039
class store_delpermissions(_store_container_command):
1040
    """Delete all sharing permissions"""
1041

    
1042
    def main(self, container___path):
1043
        super(self.__class__,
1044
            self).main(container___path, path_is_optional=False)
1045
        try:
1046
            self.client.del_object_sharing(self.path)
1047
        except ClientError as err:
1048
            raiseCLIError(err)
1049

    
1050

    
1051
@command(pithos_cmds)
1052
class store_info(_store_container_command):
1053
    """Get information for account [, container [or object]]"""
1054

    
1055
    arguments = dict(
1056
        object_version=ValueArgument(
1057
            'show specific version \ (applies only for objects)',
1058
            '--object-version')
1059
    )
1060

    
1061
    def main(self, container____path__=None):
1062
        super(self.__class__, self).main(container____path__)
1063
        try:
1064
            if self.container is None:
1065
                reply = self.client.get_account_info()
1066
            elif self.path is None:
1067
                reply = self.client.get_container_info(self.container)
1068
            else:
1069
                reply = self.client.get_object_info(
1070
                    self.path,
1071
                    version=self['object_version'])
1072
        except ClientError as err:
1073
            raiseCLIError(err)
1074
        print_dict(reply)
1075

    
1076

    
1077
@command(pithos_cmds)
1078
class store_meta(_store_container_command):
1079
    """Get custom meta-content for account [, container [or object]]"""
1080

    
1081
    arguments = dict(
1082
        detail=FlagArgument('show detailed output', '-l'),
1083
        until=DateArgument('show metadata until then', '--until'),
1084
        object_version=ValueArgument(
1085
            'show specific version \ (applies only for objects)',
1086
            '--object-version')
1087
    )
1088

    
1089
    def main(self, container____path__=None):
1090
        super(self.__class__, self).main(container____path__)
1091

    
1092
        detail = self['detail']
1093
        try:
1094
            until = self['until']
1095
            if self.container is None:
1096
                print(bold(self.client.account))
1097
                if detail:
1098
                    reply = self.client.get_account_info(until=until)
1099
                else:
1100
                    reply = self.client.get_account_meta(until=until)
1101
                    reply = pretty_keys(reply, '-')
1102
            elif self.path is None:
1103
                print(bold('%s: %s' % (self.client.account, self.container)))
1104
                if detail:
1105
                    reply = self.client.get_container_info(until=until)
1106
                else:
1107
                    cmeta = self.client.get_container_meta(until=until)
1108
                    ometa = self.client.get_container_object_meta(until=until)
1109
                    reply = {'container-meta': pretty_keys(cmeta, '-'),
1110
                        'object-meta': pretty_keys(ometa, '-')}
1111
            else:
1112
                print(bold('%s: %s:%s'\
1113
                    % (self.client.account, self.container, self.path)))
1114
                version = self['object_version']
1115
                if detail:
1116
                    reply = self.client.get_object_info(self.path,
1117
                        version=version)
1118
                else:
1119
                    reply = self.client.get_object_meta(self.path,
1120
                        version=version)
1121
                    reply = pretty_keys(pretty_keys(reply, '-'))
1122
        except ClientError as err:
1123
            raiseCLIError(err)
1124
        print_dict(reply)
1125

    
1126

    
1127
@command(pithos_cmds)
1128
class store_setmeta(_store_container_command):
1129
    """Set a new metadatum for account [, container [or object]]"""
1130

    
1131
    def main(self, metakey___metaval, container____path__=None):
1132
        super(self.__class__, self).main(container____path__)
1133
        try:
1134
            metakey, metavalue = metakey___metaval.split(':')
1135
        except ValueError as err:
1136
            raiseCLIError(err, 'Usage:  metakey:metavalue', importance=1)
1137
        try:
1138
            if self.container is None:
1139
                self.client.set_account_meta({metakey: metavalue})
1140
            elif self.path is None:
1141
                self.client.set_container_meta({metakey: metavalue})
1142
            else:
1143
                self.client.set_object_meta(self.path, {metakey: metavalue})
1144
        except ClientError as err:
1145
            raiseCLIError(err)
1146

    
1147

    
1148
@command(pithos_cmds)
1149
class store_delmeta(_store_container_command):
1150
    """Delete an existing metadatum of account [, container [or object]]"""
1151

    
1152
    def main(self, metakey, container____path__=None):
1153
        super(self.__class__, self).main(container____path__)
1154
        try:
1155
            if self.container is None:
1156
                self.client.del_account_meta(metakey)
1157
            elif self.path is None:
1158
                self.client.del_container_meta(metakey)
1159
            else:
1160
                self.client.del_object_meta(self.path, metakey)
1161
        except ClientError as err:
1162
            raiseCLIError(err)
1163

    
1164

    
1165
@command(pithos_cmds)
1166
class store_quota(_store_account_command):
1167
    """Get  quota for account [or container]"""
1168

    
1169
    def main(self, container=None):
1170
        super(self.__class__, self).main()
1171
        try:
1172
            if container is None:
1173
                reply = self.client.get_account_quota()
1174
            else:
1175
                reply = self.client.get_container_quota(container)
1176
        except ClientError as err:
1177
            raiseCLIError(err)
1178
        print_dict(reply)
1179

    
1180

    
1181
@command(pithos_cmds)
1182
class store_setquota(_store_account_command):
1183
    """Set new quota (in KB) for account [or container]"""
1184

    
1185
    def main(self, quota, container=None):
1186
        super(self.__class__, self).main()
1187
        try:
1188
            if container is None:
1189
                self.client.set_account_quota(quota)
1190
            else:
1191
                self.client.container = container
1192
                self.client.set_container_quota(quota)
1193
        except ClientError as err:
1194
            raiseCLIError(err)
1195

    
1196

    
1197
@command(pithos_cmds)
1198
class store_versioning(_store_account_command):
1199
    """Get  versioning for account [or container ]"""
1200

    
1201
    def main(self, container=None):
1202
        super(self.__class__, self).main()
1203
        try:
1204
            if container is None:
1205
                reply = self.client.get_account_versioning()
1206
            else:
1207
                reply = self.client.get_container_versioning(container)
1208
        except ClientError as err:
1209
            raiseCLIError(err)
1210
        print_dict(reply)
1211

    
1212

    
1213
@command(pithos_cmds)
1214
class store_setversioning(_store_account_command):
1215
    """Set new versioning (auto, none) for account [or container]"""
1216

    
1217
    def main(self, versioning, container=None):
1218
        super(self.__class__, self).main()
1219
        try:
1220
            if container is None:
1221
                self.client.set_account_versioning(versioning)
1222
            else:
1223
                self.client.container = container
1224
                self.client.set_container_versioning(versioning)
1225
        except ClientError as err:
1226
            raiseCLIError(err)
1227

    
1228

    
1229
@command(pithos_cmds)
1230
class store_group(_store_account_command):
1231
    """Get user groups details for account"""
1232

    
1233
    def main(self):
1234
        super(self.__class__, self).main()
1235
        try:
1236
            reply = self.client.get_account_group()
1237
        except ClientError as err:
1238
            raiseCLIError(err)
1239
        print_dict(reply)
1240

    
1241

    
1242
@command(pithos_cmds)
1243
class store_setgroup(_store_account_command):
1244
    """Create/update a new user group on account"""
1245

    
1246
    def main(self, groupname, *users):
1247
        super(self.__class__, self).main()
1248
        try:
1249
            self.client.set_account_group(groupname, users)
1250
        except ClientError as err:
1251
            raiseCLIError(err)
1252

    
1253

    
1254
@command(pithos_cmds)
1255
class store_delgroup(_store_account_command):
1256
    """Delete a user group on an account"""
1257

    
1258
    def main(self, groupname):
1259
        super(self.__class__, self).main()
1260
        try:
1261
            self.client.del_account_group(groupname)
1262
        except ClientError as err:
1263
            raiseCLIError(err)
1264

    
1265

    
1266
@command(pithos_cmds)
1267
class store_sharers(_store_account_command):
1268
    """List the accounts that share objects with default account"""
1269

    
1270
    arguments = dict(
1271
        detail=FlagArgument('show detailed output', '-l'),
1272
        marker=ValueArgument('show output greater then marker', '--marker')
1273
    )
1274

    
1275
    def main(self):
1276
        super(self.__class__, self).main()
1277
        try:
1278
            marker = self['marker']
1279
            accounts = self.client.get_sharing_accounts(marker=marker)
1280
        except ClientError as err:
1281
            raiseCLIError(err)
1282

    
1283
        for acc in accounts:
1284
            stdout.write(bold(acc['name']) + ' ')
1285
            if self['detail']:
1286
                print_dict(acc, exclude='name', ident=4)
1287
        if not self['detail']:
1288
            print
1289

    
1290

    
1291
@command(pithos_cmds)
1292
class store_versions(_store_container_command):
1293
    """Get the version list of an object"""
1294

    
1295
    def main(self, container___path):
1296
        super(store_versions, self).main(container___path)
1297
        try:
1298
            versions = self.client.get_object_versionlist(self.path)
1299
        except ClientError as err:
1300
            raiseCLIError(err)
1301

    
1302
        print('%s:%s versions' % (self.container, self.path))
1303
        for vitem in versions:
1304
            t = localtime(float(vitem[1]))
1305
            vid = bold(unicode(vitem[0]))
1306
            print('\t%s \t(%s)' % (vid, strftime('%d-%m-%Y %H:%M:%S', t)))
1307

    
1308