Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / pithos_cli.py @ ca092af4

History | View | Annotate | Download (71.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 sys import stdout
35
from time import localtime, strftime
36
from logging import getLogger
37
from os import path
38

    
39
from kamaki.cli import command
40
from kamaki.cli.command_tree import CommandTree
41
from kamaki.cli.errors import raiseCLIError, CLISyntaxError
42
from kamaki.cli.utils import (
43
    format_size,
44
    to_bytes,
45
    print_dict,
46
    pretty_keys,
47
    page_hold,
48
    ask_user)
49
from kamaki.cli.argument import FlagArgument, ValueArgument, IntArgument
50
from kamaki.cli.argument import KeyValueArgument, DateArgument
51
from kamaki.cli.argument import ProgressBarArgument
52
from kamaki.cli.commands import _command_init, errors
53
from kamaki.clients.pithos import PithosClient, ClientError
54
from kamaki.cli.utils import bold
55

    
56

    
57
kloger = getLogger('kamaki')
58

    
59
pithos_cmds = CommandTree('store', 'Pithos+ storage commands')
60
_commands = [pithos_cmds]
61

    
62

    
63
about_directories = [
64
    'Kamaki hanldes directories the same way as OOS Storage and Pithos+:',
65
    'A directory is an object with type "application/directory"',
66
    'An object with path dir/name can exist even if dir does not exist or',
67
    'even if dir is a non directory object. Users can modify dir without',
68
    'affecting the dir/name object in any way.']
69

    
70

    
71
# Argument functionality
72

    
73
def raise_connection_errors(e):
74
    if e.status in range(200) + [403, 401]:
75
        raiseCLIError(e, details=[
76
            'Please check the service url and the authentication information',
77
            ' ',
78
            '  to get the service url: /config get store.url',
79
            '  to set the service url: /config set store.url <url>',
80
            ' ',
81
            '  to get user the account: /config get store.account',
82
            '           or              /config get account',
83
            '  to set the user account: /config set store.account <account>',
84
            ' ',
85
            '  to get authentication token: /config get token',
86
            '  to set authentication token: /config set token <token>'
87
            ])
88
    elif e.status == 413:
89
        raiseCLIError(e, details=[
90
            'Get quotas:',
91
            '- total quota:      /store quota',
92
            '- container quota:  /store quota <container>',
93
            'Users shall set a higher container quota, if available:',
94
            '-                  /store setquota <quota>[unit] <container>'
95
            ])
96

    
97

    
98
def check_range(start, end):
99
    """
100
    :param start: (int)
101

102
    :param end: (int)
103

104
    :returns: (int(start), int(end))
105

106
    :raises CLIError - Invalid start/end value in range
107
    :raises CLIError - Invalid range
108
    """
109
    try:
110
        start = int(start)
111
    except ValueError as e:
112
        raiseCLIError(e, 'Invalid start value %s in range' % start)
113
    try:
114
        end = int(end)
115
    except ValueError as e:
116
        raiseCLIError(e, 'Invalid end value %s in range' % end)
117
    if start > end:
118
        raiseCLIError('Invalid range %s-%s' % (start, end))
119
    return (start, end)
120

    
121

    
122
class DelimiterArgument(ValueArgument):
123
    """
124
    :value type: string
125
    :value returns: given string or /
126
    """
127

    
128
    def __init__(self, caller_obj, help='', parsed_name=None, default=None):
129
        super(DelimiterArgument, self).__init__(help, parsed_name, default)
130
        self.caller_obj = caller_obj
131

    
132
    @property
133
    def value(self):
134
        if self.caller_obj['recursive']:
135
            return '/'
136
        return getattr(self, '_value', self.default)
137

    
138
    @value.setter
139
    def value(self, newvalue):
140
        self._value = newvalue
141

    
142

    
143
class SharingArgument(ValueArgument):
144
    """Set sharing (read and/or write) groups
145
    .
146
    :value type: "read=term1,term2,... write=term1,term2,..."
147
    .
148
    :value returns: {'read':['term1', 'term2', ...],
149
    .   'write':['term1', 'term2', ...]}
150
    """
151

    
152
    @property
153
    def value(self):
154
        return getattr(self, '_value', self.default)
155

    
156
    @value.setter
157
    def value(self, newvalue):
158
        perms = {}
159
        try:
160
            permlist = newvalue.split(' ')
161
        except AttributeError:
162
            return
163
        for p in permlist:
164
            try:
165
                (key, val) = p.split('=')
166
            except ValueError as err:
167
                raiseCLIError(err, 'Error in --sharing',
168
                    details='Incorrect format',
169
                    importance=1)
170
            if key.lower() not in ('read', 'write'):
171
                raiseCLIError(err, 'Error in --sharing',
172
                    details='Invalid permission key %s' % key,
173
                    importance=1)
174
            val_list = val.split(',')
175
            if not key in perms:
176
                perms[key] = []
177
            for item in val_list:
178
                if item not in perms[key]:
179
                    perms[key].append(item)
180
        self._value = perms
181

    
182

    
183
class RangeArgument(ValueArgument):
184
    """
185
    :value type: string of the form <start>-<end> where <start> and <end> are
186
        integers
187
    :value returns: the input string, after type checking <start> and <end>
188
    """
189

    
190
    @property
191
    def value(self):
192
        return getattr(self, '_value', self.default)
193

    
194
    @value.setter
195
    def value(self, newvalue):
196
        if newvalue is None:
197
            self._value = self.default
198
            return
199
        (start, end) = newvalue.split('-')
200
        (start, end) = (int(start), int(end))
201
        self._value = '%s-%s' % (start, end)
202

    
203
# Command specs
204

    
205

    
206
class _pithos_init(_command_init):
207
    """Initialize a pithos+ kamaki client"""
208

    
209
    @errors.generic.all
210
    def _run(self):
211
        self.token = self.config.get('store', 'token')\
212
            or self.config.get('global', 'token')
213
        self.base_url = self.config.get('store', 'url')\
214
            or self.config.get('global', 'url')
215
        self.account = self.config.get('store', 'account')\
216
            or self.config.get('global', 'account')
217
        self.container = self.config.get('store', 'container')\
218
            or self.config.get('global', 'container')
219
        self.client = PithosClient(base_url=self.base_url,
220
            token=self.token,
221
            account=self.account,
222
            container=self.container)
223

    
224
    def main(self):
225
        self._run()
226

    
227

    
228
class _store_account_command(_pithos_init):
229
    """Base class for account level storage commands"""
230

    
231
    def __init__(self, arguments={}):
232
        super(_store_account_command, self).__init__(arguments)
233
        self['account'] = ValueArgument(
234
            'Set user account (not permanent)',
235
            '--account')
236

    
237
    def _run(self):
238
        super(_store_account_command, self)._run()
239
        if self['account']:
240
            self.client.account = self['account']
241

    
242
    @errors.generic.all
243
    def main(self):
244
        self._run()
245

    
246

    
247
class _store_container_command(_store_account_command):
248
    """Base class for container level storage commands"""
249

    
250
    container = None
251
    path = None
252

    
253
    def __init__(self, arguments={}):
254
        super(_store_container_command, self).__init__(arguments)
255
        self['container'] = ValueArgument(
256
            'Set container to work with (temporary)',
257
            '--container')
258

    
259
    @errors.generic.all
260
    def _dest_container_path(self, dest_container_path):
261
        dst = dest_container_path.split(':')
262
        return (dst[0], dst[1]) if len(dst) > 1 else (None, dst[0])
263

    
264
    def extract_container_and_path(self,
265
        container_with_path,
266
        path_is_optional=True):
267
        """Contains all heuristics for deciding what should be used as
268
        container or path. Options are:
269
        * user string of the form container:path
270
        * self.container, self.path variables set by super constructor, or
271
        explicitly by the caller application
272
        Error handling is explicit as these error cases happen only here
273
        """
274
        try:
275
            assert isinstance(container_with_path, str)
276
        except AssertionError as err:
277
            raiseCLIError(err)
278

    
279
        user_cont, sep, userpath = container_with_path.partition(':')
280

    
281
        if sep:
282
            if not user_cont:
283
                raiseCLIError(CLISyntaxError('Container is missing\n',
284
                    details=errors.pithos.container_howto))
285
            alt_cont = self['container']
286
            if alt_cont and user_cont != alt_cont:
287
                raiseCLIError(CLISyntaxError(
288
                    'Conflict: 2 containers (%s, %s)' % (user_cont, alt_cont),
289
                    details=errors.pithos.container_howto)
290
                )
291
            self.container = user_cont
292
            if not userpath:
293
                raiseCLIError(CLISyntaxError(
294
                    'Path is missing for object in container %s' % user_cont,
295
                    details=errors.pithos.container_howto)
296
                )
297
            self.path = userpath
298
        else:
299
            alt_cont = self['container'] or self.client.container
300
            if alt_cont:
301
                self.container = alt_cont
302
                self.path = user_cont
303
            elif path_is_optional:
304
                self.container = user_cont
305
                self.path = None
306
            else:
307
                self.container = user_cont
308
                raiseCLIError(CLISyntaxError(
309
                    'Both container and path are required',
310
                    details=errors.pithos.container_howto)
311
                )
312

    
313
    @errors.generic.all
314
    def _run(self, container_with_path=None, path_is_optional=True):
315
        super(_store_container_command, self)._run()
316
        if container_with_path is not None:
317
            self.extract_container_and_path(
318
                container_with_path,
319
                path_is_optional)
320
            self.client.container = self.container
321
        elif self['container']:
322
            self.client.container = self['container']
323
        self.container = self.client.container
324

    
325
    def main(self, container_with_path=None, path_is_optional=True):
326
        self._run(container_with_path, path_is_optional)
327

    
328

    
329
@command(pithos_cmds)
330
class store_list(_store_container_command):
331
    """List containers, object trees or objects in a directory
332
    Use with:
333
    1 no parameters : containers in set account
334
    2. one parameter (container) or --container : contents of container
335
    3. <container>:<prefix> or --container=<container> <prefix>: objects in
336
    .   container starting with prefix
337
    """
338

    
339
    arguments = dict(
340
        detail=FlagArgument('show detailed output', '-l'),
341
        limit=IntArgument('limit the number of listed items', '-n'),
342
        marker=ValueArgument('show output greater that marker', '--marker'),
343
        prefix=ValueArgument('show output starting with prefix', '--prefix'),
344
        delimiter=ValueArgument('show output up to delimiter', '--delimiter'),
345
        path=ValueArgument(
346
            'show output starting with prefix up to /',
347
            '--path'),
348
        meta=ValueArgument(
349
            'show output with specified meta keys',
350
            '--meta',
351
            default=[]),
352
        if_modified_since=ValueArgument(
353
            'show output modified since then',
354
            '--if-modified-since'),
355
        if_unmodified_since=ValueArgument(
356
            'show output not modified since then',
357
            '--if-unmodified-since'),
358
        until=DateArgument('show metadata until then', '--until'),
359
        format=ValueArgument(
360
            'format to parse until data (default: d/m/Y H:M:S )',
361
            '--format'),
362
        shared=FlagArgument('show only shared', '--shared'),
363
        public=FlagArgument('show only public', '--public'),
364
        more=FlagArgument(
365
            'output results in pages (-n to set items per page, default 10)',
366
            '--more'),
367
        exact_match=FlagArgument(
368
            'Show only objects that match exactly with path',
369
            '--exact-match')
370
    )
371

    
372
    def print_objects(self, object_list):
373
        limit = int(self['limit']) if self['limit'] > 0 else len(object_list)
374
        for index, obj in enumerate(object_list):
375
            if (self['exact_match'] and self.path and\
376
                obj['name'] != self.path) or 'content_type' not in obj:
377
                continue
378
            pretty_obj = obj.copy()
379
            index += 1
380
            empty_space = ' ' * (len(str(len(object_list))) - len(str(index)))
381
            if obj['content_type'] == 'application/directory':
382
                isDir = True
383
                size = 'D'
384
            else:
385
                isDir = False
386
                size = format_size(obj['bytes'])
387
                pretty_obj['bytes'] = '%s (%s)' % (obj['bytes'], size)
388
            oname = bold(obj['name'])
389
            if self['detail']:
390
                print('%s%s. %s' % (empty_space, index, oname))
391
                print_dict(pretty_keys(pretty_obj), exclude=('name'))
392
                print
393
            else:
394
                oname = '%s%s. %6s %s' % (empty_space, index, size, oname)
395
                oname += '/' if isDir else ''
396
                print(oname)
397
            if self['more']:
398
                page_hold(index, limit, len(object_list))
399

    
400
    def print_containers(self, container_list):
401
        limit = int(self['limit']) if self['limit'] > 0\
402
            else len(container_list)
403
        for index, container in enumerate(container_list):
404
            if 'bytes' in container:
405
                size = format_size(container['bytes'])
406
            cname = '%s. %s' % (index + 1, bold(container['name']))
407
            if self['detail']:
408
                print(cname)
409
                pretty_c = container.copy()
410
                if 'bytes' in container:
411
                    pretty_c['bytes'] = '%s (%s)' % (container['bytes'], size)
412
                print_dict(pretty_keys(pretty_c), exclude=('name'))
413
                print
414
            else:
415
                if 'count' in container and 'bytes' in container:
416
                    print('%s (%s, %s objects)'\
417
                    % (cname, size, container['count']))
418
                else:
419
                    print(cname)
420
            if self['more']:
421
                page_hold(index + 1, limit, len(container_list))
422

    
423
    @errors.generic.all
424
    @errors.pithos.connection
425
    @errors.pithos.object_path
426
    @errors.pithos.container
427
    def _run(self):
428
        if self.container is None:
429
            r = self.client.account_get(
430
                limit=False if self['more'] else self['limit'],
431
                marker=self['marker'],
432
                if_modified_since=self['if_modified_since'],
433
                if_unmodified_since=self['if_unmodified_since'],
434
                until=self['until'],
435
                show_only_shared=self['shared'])
436
            self.print_containers(r.json)
437
        else:
438
            prefix = self.path if self.path else self['prefix']
439
            r = self.client.container_get(
440
                limit=False if self['more'] else self['limit'],
441
                marker=self['marker'],
442
                prefix=prefix,
443
                delimiter=self['delimiter'],
444
                path=self['path'],
445
                if_modified_since=self['if_modified_since'],
446
                if_unmodified_since=self['if_unmodified_since'],
447
                until=self['until'],
448
                meta=self['meta'],
449
                show_only_shared=self['shared'])
450
            self.print_objects(r.json)
451

    
452
    def main(self, container____path__=None):
453
        super(self.__class__, self)._run(container____path__)
454
        self._run()
455

    
456

    
457
@command(pithos_cmds)
458
class store_mkdir(_store_container_command):
459
    """Create a directory"""
460

    
461
    __doc__ += '\n. '.join(about_directories)
462

    
463
    @errors.generic.all
464
    @errors.pithos.connection
465
    @errors.pithos.container
466
    def _run(self):
467
        self.client.create_directory(self.path)
468

    
469
    def main(self, container___directory):
470
        super(self.__class__, self)._run(
471
            container___directory,
472
            path_is_optional=False)
473
        self._run()
474

    
475

    
476
@command(pithos_cmds)
477
class store_touch(_store_container_command):
478
    """Create an empty object (file)
479
    If object exists, this command will reset it to 0 length
480
    """
481

    
482
    arguments = dict(
483
        content_type=ValueArgument(
484
            'Set content type (default: application/octet-stream)',
485
            '--content-type',
486
            default='application/octet-stream')
487
    )
488

    
489
    @errors.generic.all
490
    @errors.pithos.connection
491
    @errors.pithos.container
492
    def _run(self):
493
        self.client.create_object(self.path, self['content_type'])
494

    
495
    def main(self, container___path):
496
        super(store_touch, self)._run(container___path)
497
        self._run()
498

    
499

    
500
@command(pithos_cmds)
501
class store_create(_store_container_command):
502
    """Create a container"""
503

    
504
    arguments = dict(
505
        versioning=ValueArgument(
506
            'set container versioning (auto/none)',
507
            '--versioning'),
508
        quota=IntArgument('set default container quota', '--quota'),
509
        meta=KeyValueArgument(
510
            'set container metadata (can be repeated)',
511
            '--meta')
512
    )
513

    
514
    @errors.generic.all
515
    @errors.pithos.connection
516
    @errors.pithos.container
517
    def _run(self):
518
        self.client.container_put(quota=self['quota'],
519
            versioning=self['versioning'],
520
            metadata=self['meta'])
521

    
522
    def main(self, container):
523
        super(self.__class__, self)._run(container)
524
        if self.container != container:
525
            raiseCLIError('Invalid container name %s' % container, details=[
526
                'Did you mean "%s" ?' % self.container])
527
        self._run()
528

    
529

    
530
@command(pithos_cmds)
531
class store_copy(_store_container_command):
532
    """Copy objects from container to (another) container
533
    Semantics:
534
    copy cont:path path2
535
    .   will copy all <obj> prefixed with path, as path2<obj>
536
    .   or as path2 if path corresponds to just one whole object
537
    copy cont:path cont2:
538
    .   will copy all <obj> prefixed with path to container cont2
539
    copy cont:path [cont2:]path2 --exact-match
540
    .   will copy at most one <obj> as a new object named path2,
541
    .   provided path corresponds to a whole object path
542
    copy cont:path [cont2:]path2 --replace
543
    .   will copy all <obj> prefixed with path, replacing path with path2
544
    where <obj> is a full file or directory object path.
545
    Use options:
546
    1. <container1>:<path1> [container2:]<path2> : if container2 is not given,
547
    destination is container1:path2
548
    2. <container>:<path1> <path2> : make a copy in the same container
549
    3. Can use --container= instead of <container1>
550
    """
551

    
552
    arguments = dict(
553
        source_version=ValueArgument(
554
            'copy specific version',
555
            '--source-version'),
556
        public=ValueArgument('make object publicly accessible', '--public'),
557
        content_type=ValueArgument(
558
            'change object\'s content type',
559
            '--content-type'),
560
        recursive=FlagArgument(
561
            'mass copy with delimiter /',
562
            ('-r', '--recursive')),
563
        exact_match=FlagArgument(
564
            'Copy only the object that fully matches path',
565
            '--exact-match'),
566
        replace=FlagArgument('Replace src. path with dst. path', '--replace')
567
    )
568

    
569
    def _objlist(self, dst_path):
570
        if self['exact_match']:
571
            return [(dst_path if dst_path else self.path, self.path)]
572
        r = self.client.container_get(prefix=self.path)
573
        if len(r.json) == 1:
574
            obj = r.json[0]
575
            return [(obj['name'], dst_path if dst_path else obj['name'])]
576
        return [(obj['name'], '%s%s' % (
577
                    dst_path,
578
                    obj['name'][len(self.path) if self['replace'] else 0:])
579
                ) for obj in r.json]
580

    
581
    @errors.generic.all
582
    @errors.pithos.connection
583
    @errors.pithos.container
584
    def _run(self, dst_cont, dst_path):
585
        no_source_object = True
586
        for src_object, dst_object in self._objlist(dst_path):
587
            no_source_object = False
588
            self.client.copy_object(
589
                src_container=self.container,
590
                src_object=src_object,
591
                dst_container=dst_cont if dst_cont else self.container,
592
                dst_object=dst_object,
593
                source_version=self['source_version'],
594
                public=self['public'],
595
                content_type=self['content_type'])
596
        if no_source_object:
597
            raiseCLIError('No object %s in container %s' % (
598
                self.path,
599
                self.container))
600

    
601
    def main(self, source_container___path, destination_container___path):
602
        super(self.__class__, self)._run(
603
            source_container___path,
604
            path_is_optional=False)
605
        (dst_cont, dst_path) = self._dest_container_path(
606
            destination_container___path)
607
        self._run(dst_cont=dst_cont, dst_path=dst_path)
608

    
609

    
610
@command(pithos_cmds)
611
class store_move(_store_container_command):
612
    """Move/rename objects
613
    Semantics:
614
    move cont:path path2
615
    .   will move all <obj> prefixed with path, as path2<obj>
616
    .   or as path2 if path corresponds to just one whole object
617
    move cont:path cont2:
618
    .   will move all <obj> prefixed with path to container cont2
619
    move cont:path [cont2:]path2 --exact-match
620
    .   will move at most one <obj> as a new object named path2,
621
    .   provided path corresponds to a whole object path
622
    move cont:path [cont2:]path2 --replace
623
    .   will move all <obj> prefixed with path, replacing path with path2
624
    where <obj> is a full file or directory object path.
625
    Use options:
626
    1. <container1>:<path1> [container2:]<path2> : if container2 not given,
627
    destination is container1:path2
628
    2. <container>:<path1> path2 : rename
629
    3. Can use --container= instead of <container1>
630
    """
631

    
632
    arguments = dict(
633
        source_version=ValueArgument('specify version', '--source-version'),
634
        public=FlagArgument('make object publicly accessible', '--public'),
635
        content_type=ValueArgument('modify content type', '--content-type'),
636
        recursive=FlagArgument('up to delimiter /', ('-r', '--recursive')),
637
        exact_match=FlagArgument(
638
            'Copy only the object that fully matches path',
639
            '--exact-match'),
640
        replace=FlagArgument('Replace src. path with dst. path', '--replace')
641
    )
642

    
643
    def _objlist(self, dst_path):
644
        if self['exact_match']:
645
            return [(dst_path if dst_path else self.path, self.path)]
646
        r = self.client.container_get(prefix=self.path)
647
        if len(r.json) == 1:
648
            obj = r.json[0]
649
            return [(obj['name'], dst_path if dst_path else obj['name'])]
650
        return [(obj['name'], '%s%s' % (
651
                    dst_path,
652
                    obj['name'][len(self.path) if self['replace'] else 0:])
653
                ) for obj in r.json]
654

    
655
    @errors.generic.all
656
    @errors.pithos.connection
657
    @errors.pithos.container
658
    def _run(self, dst_cont, dst_path):
659
        no_source_object = True
660
        for src_object, dst_object in self._objlist(dst_path):
661
            no_source_object = False
662
            self.client.move_object(
663
                src_container=self.container,
664
                src_object=src_object,
665
                dst_container=dst_cont if dst_cont else self.container,
666
                dst_object=dst_object,
667
                source_version=self['source_version'],
668
                public=self['public'],
669
                content_type=self['content_type'])
670
        if no_source_object:
671
            raiseCLIError('No object %s in container %s' % (
672
                self.path,
673
                self.container))
674

    
675
    def main(self, source_container___path, destination_container___path):
676
        super(self.__class__, self)._run(
677
            source_container___path,
678
            path_is_optional=False)
679
        (dst_cont, dst_path) = self._dest_container_path(
680
            destination_container___path)
681
        self._run(dst_cont=dst_cont, dst_path=dst_path)
682

    
683

    
684
@command(pithos_cmds)
685
class store_append(_store_container_command):
686
    """Append local file to (existing) remote object
687
    The remote object should exist.
688
    If the remote object is a directory, it is transformed into a file.
689
    In the later case, objects under the directory remain intact.
690
    """
691

    
692
    arguments = dict(
693
        progress_bar=ProgressBarArgument(
694
            'do not show progress bar',
695
            '--no-progress-bar',
696
            default=False)
697
    )
698

    
699
    @errors.generic.all
700
    @errors.pithos.connection
701
    @errors.pithos.container
702
    @errors.pithos.object_path
703
    def _run(self, local_path):
704
        (progress_bar, upload_cb) = self._safe_progress_bar('Appending')
705
        try:
706
            f = open(local_path, 'rb')
707
            self.client.append_object(self.path, f, upload_cb)
708
        except Exception:
709
            self._safe_progress_bar_finish(progress_bar)
710
            raise
711
        finally:
712
            self._safe_progress_bar_finish(progress_bar)
713

    
714
    def main(self, local_path, container___path):
715
        super(self.__class__, self)._run(
716
            container___path,
717
            path_is_optional=False)
718
        self._run(local_path)
719

    
720

    
721
@command(pithos_cmds)
722
class store_truncate(_store_container_command):
723
    """Truncate remote file up to a size (default is 0)"""
724

    
725
    @errors.generic.all
726
    @errors.pithos.connection
727
    @errors.pithos.container
728
    @errors.pithos.object_path
729
    @errors.pithos.object_size
730
    def _run(self, size=0):
731
        self.client.truncate_object(self.path, size)
732

    
733
    def main(self, container___path, size=0):
734
        super(self.__class__, self)._run(container___path)
735
        self._run(size=size)
736

    
737

    
738
@command(pithos_cmds)
739
class store_overwrite(_store_container_command):
740
    """Overwrite part (from start to end) of a remote file
741
    overwrite local-path container 10 20
742
    .   will overwrite bytes from 10 to 20 of a remote file with the same name
743
    .   as local-path basename
744
    overwrite local-path container:path 10 20
745
    .   will overwrite as above, but the remote file is named path
746
    """
747

    
748
    arguments = dict(
749
        progress_bar=ProgressBarArgument(
750
            'do not show progress bar',
751
            '--no-progress-bar',
752
            default=False)
753
    )
754

    
755
    def _open_file(self, local_path, start):
756
        f = open(path.abspath(local_path), 'rb')
757
        f.seek(0, 2)
758
        f_size = f.tell()
759
        f.seek(start, 0)
760
        return (f, f_size)
761

    
762
    @errors.generic.all
763
    @errors.pithos.connection
764
    @errors.pithos.container
765
    @errors.pithos.object_path
766
    @errors.pithos.object_size
767
    def _run(self, local_path, start, end):
768
        (start, end) = (int(start), int(end))
769
        (f, f_size) = self._open_file(local_path, start)
770
        (progress_bar, upload_cb) = self._safe_progress_bar(
771
            'Overwrite %s bytes' % (end - start))
772
        try:
773
            self.client.overwrite_object(
774
                obj=self.path,
775
                start=start,
776
                end=end,
777
                source_file=f,
778
                upload_cb=upload_cb)
779
        except Exception:
780
            self._safe_progress_bar_finish(progress_bar)
781
            raise
782
        finally:
783
            self._safe_progress_bar_finish(progress_bar)
784

    
785
    def main(self, local_path, container____path__, start, end):
786
        super(self.__class__, self)._run(container____path__)
787
        self.path = self.path if self.path else path.basename(local_path)
788
        self._run(local_path=local_path, start=start, end=end)
789

    
790

    
791
@command(pithos_cmds)
792
class store_manifest(_store_container_command):
793
    """Create a remote file of uploaded parts by manifestation
794
    Remains functional for compatibility with OOS Storage. Users are advised
795
    to use the upload command instead.
796
    Manifestation is a compliant process for uploading large files. The files
797
    have to be chunked in smalled files and uploaded as <prefix><increment>
798
    where increment is 1, 2, ...
799
    Finally, the manifest command glues partial files together in one file
800
    named <prefix>
801
    The upload command is faster, easier and more intuitive than manifest
802
    """
803

    
804
    arguments = dict(
805
        etag=ValueArgument('check written data', '--etag'),
806
        content_encoding=ValueArgument(
807
            'set MIME content type',
808
            '--content-encoding'),
809
        content_disposition=ValueArgument(
810
            'the presentation style of the object',
811
            '--content-disposition'),
812
        content_type=ValueArgument(
813
            'specify content type',
814
            '--content-type',
815
            default='application/octet-stream'),
816
        sharing=SharingArgument(
817
            'define object sharing policy \n' +\
818
            '    ( "read=user1,grp1,user2,... write=user1,grp2,..." )',
819
            '--sharing'),
820
        public=FlagArgument('make object publicly accessible', '--public')
821
    )
822

    
823
    def main(self, container___path):
824
        super(self.__class__,
825
            self).main(container___path, path_is_optional=False)
826
        try:
827
            self.client.create_object_by_manifestation(
828
                self.path,
829
                content_encoding=self['content_encoding'],
830
                content_disposition=self['content_disposition'],
831
                content_type=self['content_type'],
832
                sharing=self['sharing'],
833
                public=self['public'])
834
        except ClientError as err:
835
            if err.status == 404:
836
                if 'container' in ('%s' % err).lower():
837
                    raiseCLIError(
838
                        err,
839
                        'No container %s in account %s'\
840
                        % (self.container, self.account),
841
                        details=errors.pithos.container_howto)
842
                elif 'object' in ('%s' % err).lower():
843
                    raiseCLIError(
844
                        err,
845
                        'No object %s in container %s'\
846
                        % (self.path, self.container),
847
                        details=errors.pithos.container_howto)
848
            raise_connection_errors(err)
849
            raiseCLIError(err)
850
        except Exception as e:
851
            raiseCLIError(e)
852

    
853

    
854
@command(pithos_cmds)
855
class store_upload(_store_container_command):
856
    """Upload a file"""
857

    
858
    arguments = dict(
859
        use_hashes=FlagArgument(
860
            'provide hashmap file instead of data',
861
            '--use-hashes'),
862
        etag=ValueArgument('check written data', '--etag'),
863
        unchunked=FlagArgument('avoid chunked transfer mode', '--unchunked'),
864
        content_encoding=ValueArgument(
865
            'set MIME content type',
866
            '--content-encoding'),
867
        content_disposition=ValueArgument(
868
            'specify objects presentation style',
869
            '--content-disposition'),
870
        content_type=ValueArgument('specify content type', '--content-type'),
871
        sharing=SharingArgument(
872
            help='define sharing object policy \n' +\
873
            '( "read=user1,grp1,user2,... write=user1,grp2,... )',
874
            parsed_name='--sharing'),
875
        public=FlagArgument('make object publicly accessible', '--public'),
876
        poolsize=IntArgument('set pool size', '--with-pool-size'),
877
        progress_bar=ProgressBarArgument(
878
            'do not show progress bar',
879
            '--no-progress-bar',
880
            default=False),
881
        overwrite=FlagArgument('Force overwrite, if object exists', '-f')
882
    )
883

    
884
    def _remote_path(self, remote_path, local_path=''):
885
        if self['overwrite']:
886
            return remote_path
887
        try:
888
            r = self.client.get_object_info(remote_path)
889
        except ClientError as ce:
890
            if ce.status == 404:
891
                return remote_path
892
            raise ce
893
        ctype = r.get('content-type', '')
894
        if 'application/directory' == ctype.lower():
895
            ret = '%s/%s' % (remote_path, local_path)
896
            return self._remote_path(ret) if local_path else ret
897
        raiseCLIError(
898
            'Object %s already exists' % remote_path,
899
            importance=1,
900
            details=['use -f to overwrite or resume'])
901

    
902
    def main(self, local_path, container____path__=None):
903
        super(self.__class__, self).main(container____path__)
904
        remote_path = self.path if self.path else path.basename(local_path)
905
        poolsize = self['poolsize']
906
        if poolsize > 0:
907
            self.client.POOL_SIZE = int(poolsize)
908
        params = dict(
909
            content_encoding=self['content_encoding'],
910
            content_type=self['content_type'],
911
            content_disposition=self['content_disposition'],
912
            sharing=self['sharing'],
913
            public=self['public'])
914
        try:
915
            progress_bar = self.arguments['progress_bar']
916
            hash_bar = progress_bar.clone()
917
            remote_path = self._remote_path(remote_path, local_path)
918
            with open(path.abspath(local_path), 'rb') as f:
919
                if self['unchunked']:
920
                    self.client.upload_object_unchunked(
921
                        remote_path,
922
                        f,
923
                        etag=self['etag'],
924
                        withHashFile=self['use_hashes'],
925
                        **params)
926
                else:
927
                    hash_cb = hash_bar.get_generator(
928
                        'Calculating block hashes')
929
                    upload_cb = progress_bar.get_generator('Uploading')
930
                    self.client.upload_object(
931
                        remote_path,
932
                        f,
933
                        hash_cb=hash_cb,
934
                        upload_cb=upload_cb,
935
                        **params)
936
                    progress_bar.finish()
937
                    hash_bar.finish()
938
        except ClientError as err:
939
            try:
940
                progress_bar.finish()
941
                hash_bar.finish()
942
            except Exception:
943
                pass
944
            if err.status == 404:
945
                if 'container' in ('%s' % err).lower():
946
                    raiseCLIError(
947
                        err,
948
                        'No container %s in account %s'\
949
                        % (self.container, self.account),
950
                        details=[errors.pithos.container_howto])
951
            elif err.status == 800:
952
                raiseCLIError(err, details=[
953
                    'Possible cause: temporary server failure',
954
                    'Try to re-upload the file',
955
                    'For more error details, try kamaki store upload -d'])
956
            raise_connection_errors(err)
957
            raiseCLIError(err, 'Failed to upload to %s' % container____path__)
958
        except IOError as err:
959
            try:
960
                progress_bar.finish()
961
                hash_bar.finish()
962
            except Exception:
963
                pass
964
            raiseCLIError(err, 'Failed to read form file %s' % local_path, 2)
965
        except Exception as e:
966
            try:
967
                progress_bar.finish()
968
                hash_bar.finish()
969
            except Exception:
970
                pass
971
            raiseCLIError(e)
972
        print 'Upload completed'
973

    
974

    
975
@command(pithos_cmds)
976
class store_cat(_store_container_command):
977
    """Print remote file contents to console"""
978

    
979
    arguments = dict(
980
        range=RangeArgument('show range of data', '--range'),
981
        if_match=ValueArgument('show output if ETags match', '--if-match'),
982
        if_none_match=ValueArgument(
983
            'show output if ETags match',
984
            '--if-none-match'),
985
        if_modified_since=DateArgument(
986
            'show output modified since then',
987
            '--if-modified-since'),
988
        if_unmodified_since=DateArgument(
989
            'show output unmodified since then',
990
            '--if-unmodified-since'),
991
        object_version=ValueArgument(
992
            'get the specific version',
993
            '--object-version')
994
    )
995

    
996
    def main(self, container___path):
997
        super(self.__class__, self).main(
998
            container___path,
999
            path_is_optional=False)
1000
        try:
1001
            self.client.download_object(self.path, stdout,
1002
            range=self['range'],
1003
            version=self['object_version'],
1004
            if_match=self['if_match'],
1005
            if_none_match=self['if_none_match'],
1006
            if_modified_since=self['if_modified_since'],
1007
            if_unmodified_since=self['if_unmodified_since'])
1008
        except ClientError as err:
1009
            if err.status == 404:
1010
                if 'container' in ('%s' % err).lower():
1011
                    raiseCLIError(
1012
                        err,
1013
                        'No container %s in account %s'\
1014
                        % (self.container, self.account),
1015
                        details=errors.pithos.container_howto)
1016
                elif 'object' in ('%s' % err).lower():
1017
                    raiseCLIError(
1018
                        err,
1019
                        'No object %s in container %s'\
1020
                        % (self.path, self.container),
1021
                        details=errors.pithos.container_howto)
1022
            raise_connection_errors(err)
1023
            raiseCLIError(err)
1024
        except Exception as e:
1025
            raiseCLIError(e)
1026

    
1027

    
1028
@command(pithos_cmds)
1029
class store_download(_store_container_command):
1030
    """Download remote object as local file"""
1031

    
1032
    arguments = dict(
1033
        resume=FlagArgument('Resume instead of overwrite', '--resume'),
1034
        range=RangeArgument('show range of data', '--range'),
1035
        if_match=ValueArgument('show output if ETags match', '--if-match'),
1036
        if_none_match=ValueArgument(
1037
            'show output if ETags match',
1038
            '--if-none-match'),
1039
        if_modified_since=DateArgument(
1040
            'show output modified since then',
1041
            '--if-modified-since'),
1042
        if_unmodified_since=DateArgument(
1043
            'show output unmodified since then',
1044
            '--if-unmodified-since'),
1045
        object_version=ValueArgument(
1046
            'get the specific version',
1047
            '--object-version'),
1048
        poolsize=IntArgument('set pool size', '--with-pool-size'),
1049
        progress_bar=ProgressBarArgument(
1050
            'do not show progress bar',
1051
            '--no-progress-bar',
1052
            default=False)
1053
    )
1054

    
1055
    def _output_stream(self, local_path):
1056
        if local_path is None:
1057
            return stdout
1058
        try:
1059
            return open(
1060
                path.abspath(local_path),
1061
                'rwb+' if self['resume'] else 'wb+')
1062
        except IOError as err:
1063
            raiseCLIError(err, 'Cannot write to file %s' % local_path, 1)
1064

    
1065
    def main(self, container___path, local_path=None):
1066
        super(self.__class__, self).main(
1067
            container___path,
1068
            path_is_optional=False)
1069

    
1070
        out = self._output_stream(local_path)
1071
        poolsize = self['poolsize']
1072
        if poolsize is not None:
1073
            self.client.POOL_SIZE = int(poolsize)
1074

    
1075
        try:
1076
            progress_bar = self.arguments['progress_bar']
1077
            download_cb = progress_bar.get_generator('Downloading')
1078
            self.client.download_object(
1079
                self.path,
1080
                out,
1081
                download_cb=download_cb,
1082
                range=self['range'],
1083
                version=self['object_version'],
1084
                if_match=self['if_match'],
1085
                resume=self['resume'],
1086
                if_none_match=self['if_none_match'],
1087
                if_modified_since=self['if_modified_since'],
1088
                if_unmodified_since=self['if_unmodified_since'])
1089
            progress_bar.finish()
1090
        except ClientError as err:
1091
            progress_bar.finish()
1092
            if err.status == 404:
1093
                if 'container' in ('%s' % err).lower():
1094
                    raiseCLIError(
1095
                        err,
1096
                        'No container %s in account %s'\
1097
                        % (self.container, self.account),
1098
                        details=errors.pithos.container_howto)
1099
                elif 'object' in ('%s' % err).lower():
1100
                    raiseCLIError(
1101
                        err,
1102
                        'No object %s in container %s'\
1103
                        % (self.path, self.container),
1104
                        details=errors.pithos.container_howto)
1105
            raise_connection_errors(err)
1106
            raiseCLIError(err, '"%s" not accessible' % container___path)
1107
        except IOError as err:
1108
            try:
1109
                progress_bar.finish()
1110
            except Exception:
1111
                pass
1112
            raiseCLIError(err, 'Failed to write on file %s' % local_path, 2)
1113
        except KeyboardInterrupt:
1114
            from threading import enumerate as activethreads
1115
            stdout.write('\nFinishing active threads ')
1116
            for thread in activethreads():
1117
                stdout.flush()
1118
                try:
1119
                    thread.join()
1120
                    stdout.write('.')
1121
                except RuntimeError:
1122
                    continue
1123
            try:
1124
                progress_bar.finish()
1125
            except Exception:
1126
                pass
1127
            print('\ndownload canceled by user')
1128
            if local_path is not None:
1129
                print('to resume, re-run with --resume')
1130
        except Exception as e:
1131
            try:
1132
                progress_bar.finish()
1133
            except Exception:
1134
                pass
1135
            raiseCLIError(e)
1136
        print
1137

    
1138

    
1139
@command(pithos_cmds)
1140
class store_hashmap(_store_container_command):
1141
    """Get the hash-map of an object"""
1142

    
1143
    arguments = dict(
1144
        if_match=ValueArgument('show output if ETags match', '--if-match'),
1145
        if_none_match=ValueArgument(
1146
            'show output if ETags match',
1147
            '--if-none-match'),
1148
        if_modified_since=DateArgument(
1149
            'show output modified since then',
1150
            '--if-modified-since'),
1151
        if_unmodified_since=DateArgument(
1152
            'show output unmodified since then',
1153
            '--if-unmodified-since'),
1154
        object_version=ValueArgument(
1155
            'get the specific version',
1156
            '--object-version')
1157
    )
1158

    
1159
    def main(self, container___path):
1160
        super(self.__class__, self).main(
1161
            container___path,
1162
            path_is_optional=False)
1163
        try:
1164
            data = self.client.get_object_hashmap(
1165
                self.path,
1166
                version=self['object_version'],
1167
                if_match=self['if_match'],
1168
                if_none_match=self['if_none_match'],
1169
                if_modified_since=self['if_modified_since'],
1170
                if_unmodified_since=self['if_unmodified_since'])
1171
        except ClientError as err:
1172
            if err.status == 404:
1173
                if 'container' in ('%s' % err).lower():
1174
                    raiseCLIError(
1175
                        err,
1176
                        'No container %s in account %s'\
1177
                        % (self.container, self.account),
1178
                        details=errors.pithos.container_howto)
1179
                elif 'object' in ('%s' % err).lower():
1180
                    raiseCLIError(
1181
                        err,
1182
                        'No object %s in container %s'\
1183
                        % (self.path, self.container),
1184
                        details=errors.pithos.container_howto)
1185
            raise_connection_errors(err)
1186
            raiseCLIError(err)
1187
        except Exception as e:
1188
            raiseCLIError(e)
1189
        print_dict(data)
1190

    
1191

    
1192
@command(pithos_cmds)
1193
class store_delete(_store_container_command):
1194
    """Delete a container [or an object]
1195
    How to delete a non-empty container:
1196
    - empty the container:  /store delete -r <container>
1197
    - delete it:            /store delete <container>
1198
    .
1199
    Semantics of directory deletion:
1200
    .a preserve the contents: /store delete <container>:<directory>
1201
    .    objects of the form dir/filename can exist with a dir object
1202
    .b delete contents:       /store delete -r <container>:<directory>
1203
    .    all dir/* objects are affected, even if dir does not exist
1204
    .
1205
    To restore a deleted object OBJ in a container CONT:
1206
    - get object versions: /store versions CONT:OBJ
1207
    .   and choose the version to be restored
1208
    - restore the object:  /store copy --source-version=<version> CONT:OBJ OBJ
1209
    """
1210

    
1211
    arguments = dict(
1212
        until=DateArgument('remove history until that date', '--until'),
1213
        yes=FlagArgument('Do not prompt for permission', '--yes'),
1214
        recursive=FlagArgument(
1215
            'empty dir or container and delete (if dir)',
1216
            ('-r', '--recursive'))
1217
    )
1218

    
1219
    def __init__(self, arguments={}):
1220
        super(self.__class__, self).__init__(arguments)
1221
        self['delimiter'] = DelimiterArgument(
1222
            self,
1223
            parsed_name='--delimiter',
1224
            help='delete objects prefixed with <object><delimiter>')
1225

    
1226
    def main(self, container____path__):
1227
        super(self.__class__, self).main(container____path__)
1228
        try:
1229
            if (not self.path):
1230
                if self['yes'] or ask_user(
1231
                    'Delete container %s ?' % self.container):
1232
                    self.client.del_container(
1233
                        until=self['until'],
1234
                        delimiter=self['delimiter'])
1235
            else:
1236
                if self['yes'] or ask_user(
1237
                    'Delete %s:%s ?' % (self.container, self.path)):
1238
                    self.client.del_object(
1239
                        self.path,
1240
                        until=self['until'],
1241
                        delimiter=self['delimiter'])
1242
        except ClientError as err:
1243
            if err.status == 404:
1244
                if 'container' in ('%s' % err).lower():
1245
                    raiseCLIError(
1246
                        err,
1247
                        'No container %s in account %s'\
1248
                        % (self.container, self.account),
1249
                        details=errors.pithos.container_howto)
1250
                elif 'object' in ('%s' % err).lower():
1251
                    raiseCLIError(
1252
                        err,
1253
                        'No object %s in container %s'\
1254
                        % (self.path, self.container),
1255
                        details=errors.pithos.container_howto)
1256
            raise_connection_errors(err)
1257
            raiseCLIError(err)
1258
        except Exception as e:
1259
            raiseCLIError(e)
1260

    
1261

    
1262
@command(pithos_cmds)
1263
class store_purge(_store_container_command):
1264
    """Delete a container and release related data blocks
1265
    Non-empty containers can not purged.
1266
    To purge a container with content:
1267
    .   /store delete -r <container>
1268
    .      objects are deleted, but data blocks remain on server
1269
    .   /store purge <container>
1270
    .      container and data blocks are released and deleted
1271
    """
1272

    
1273
    arguments = dict(
1274
        yes=FlagArgument('Do not prompt for permission', '--yes'),
1275
    )
1276

    
1277
    def main(self, container):
1278
        super(self.__class__, self).main(container)
1279
        try:
1280
            if self['yes'] or ask_user(
1281
                'Purge container %s?' % self.container):
1282
                self.client.purge_container()
1283
        except ClientError as err:
1284
            if err.status == 404:
1285
                if 'container' in ('%s' % err).lower():
1286
                    raiseCLIError(
1287
                        err,
1288
                        'No container %s in account %s'\
1289
                        % (self.container, self.account),
1290
                        details=errors.pithos.container_howto)
1291
            raise_connection_errors(err)
1292
            raiseCLIError(err)
1293
        except Exception as e:
1294
            raiseCLIError(e)
1295

    
1296

    
1297
@command(pithos_cmds)
1298
class store_publish(_store_container_command):
1299
    """Publish the object and print the public url"""
1300

    
1301
    def main(self, container___path):
1302
        super(self.__class__, self).main(
1303
            container___path,
1304
            path_is_optional=False)
1305
        try:
1306
            url = self.client.publish_object(self.path)
1307
        except ClientError as err:
1308
            if err.status == 404:
1309
                if 'container' in ('%s' % err).lower():
1310
                    raiseCLIError(
1311
                        err,
1312
                        'No container %s in account %s'\
1313
                        % (self.container, self.account),
1314
                        details=errors.pithos.container_howto)
1315
                elif 'object' in ('%s' % err).lower():
1316
                    raiseCLIError(
1317
                        err,
1318
                        'No object %s in container %s'\
1319
                        % (self.path, self.container),
1320
                        details=errors.pithos.container_howto)
1321
            raise_connection_errors(err)
1322
            raiseCLIError(err)
1323
        except Exception as e:
1324
            raiseCLIError(e)
1325
        print(url)
1326

    
1327

    
1328
@command(pithos_cmds)
1329
class store_unpublish(_store_container_command):
1330
    """Unpublish an object"""
1331

    
1332
    def main(self, container___path):
1333
        super(self.__class__, self).main(
1334
            container___path,
1335
            path_is_optional=False)
1336
        try:
1337
            self.client.unpublish_object(self.path)
1338
        except ClientError as err:
1339
            if err.status == 404:
1340
                if 'container' in ('%s' % err).lower():
1341
                    raiseCLIError(
1342
                        err,
1343
                        'No container %s in account %s'\
1344
                        % (self.container, self.account),
1345
                        details=errors.pithos.container_howto)
1346
                elif 'object' in ('%s' % err).lower():
1347
                    raiseCLIError(
1348
                        err,
1349
                        'No object %s in container %s'\
1350
                        % (self.path, self.container),
1351
                        details=errors.pithos.container_howto)
1352
            raise_connection_errors(err)
1353
            raiseCLIError(err)
1354
        except Exception as e:
1355
            raiseCLIError(e)
1356

    
1357

    
1358
@command(pithos_cmds)
1359
class store_permissions(_store_container_command):
1360
    """Get read and write permissions of an object
1361
    Permissions are lists of users and user groups. There is read and write
1362
    permissions. Users and groups with write permission have also read
1363
    permission.
1364
    """
1365

    
1366
    def main(self, container___path):
1367
        super(self.__class__, self).main(
1368
            container___path,
1369
            path_is_optional=False)
1370
        try:
1371
            reply = self.client.get_object_sharing(self.path)
1372
        except ClientError as err:
1373
            if err.status == 404:
1374
                if 'container' in ('%s' % err).lower():
1375
                    raiseCLIError(
1376
                        err,
1377
                        'No container %s in account %s'\
1378
                        % (self.container, self.account),
1379
                        details=errors.pithos.container_howto)
1380
                elif 'object' in ('%s' % err).lower():
1381
                    raiseCLIError(
1382
                        err,
1383
                        'No object %s in container %s'\
1384
                        % (self.path, self.container),
1385
                        details=errors.pithos.container_howto)
1386
            raise_connection_errors(err)
1387
            raiseCLIError(err)
1388
        except Exception as e:
1389
            raiseCLIError(e)
1390
        print_dict(reply)
1391

    
1392

    
1393
@command(pithos_cmds)
1394
class store_setpermissions(_store_container_command):
1395
    """Set permissions for an object
1396
    New permissions overwrite existing permissions.
1397
    Permission format:
1398
    -   read=<username>[,usergroup[,...]]
1399
    -   write=<username>[,usegroup[,...]]
1400
    E.g. to give read permissions for file F to users A and B and write for C:
1401
    .       /store setpermissions F read=A,B write=C
1402
    """
1403

    
1404
    def format_permition_dict(self, permissions):
1405
        read = False
1406
        write = False
1407
        for perms in permissions:
1408
            splstr = perms.split('=')
1409
            if 'read' == splstr[0]:
1410
                read = [user_or_group.strip() \
1411
                for user_or_group in splstr[1].split(',')]
1412
            elif 'write' == splstr[0]:
1413
                write = [user_or_group.strip() \
1414
                for user_or_group in splstr[1].split(',')]
1415
            else:
1416
                read = False
1417
                write = False
1418
        if not read and not write:
1419
            raiseCLIError(None,
1420
            'Usage:\tread=<groups,users> write=<groups,users>')
1421
        return (read, write)
1422

    
1423
    def main(self, container___path, *permissions):
1424
        super(self.__class__,
1425
            self).main(container___path, path_is_optional=False)
1426
        (read, write) = self.format_permition_dict(permissions)
1427
        try:
1428
            self.client.set_object_sharing(self.path,
1429
                read_permition=read, write_permition=write)
1430
        except ClientError as err:
1431
            if err.status == 404:
1432
                if 'container' in ('%s' % err).lower():
1433
                    raiseCLIError(
1434
                        err,
1435
                        'No container %s in account %s'\
1436
                        % (self.container, self.account),
1437
                        details=errors.pithos.container_howto)
1438
                elif 'object' in ('%s' % err).lower():
1439
                    raiseCLIError(
1440
                        err,
1441
                        'No object %s in container %s'\
1442
                        % (self.path, self.container),
1443
                        details=errors.pithos.container_howto)
1444
            raise_connection_errors(err)
1445
            raiseCLIError(err)
1446
        except Exception as e:
1447
            raiseCLIError(e)
1448

    
1449

    
1450
@command(pithos_cmds)
1451
class store_delpermissions(_store_container_command):
1452
    """Delete all permissions set on object
1453
    To modify permissions, use /store setpermssions
1454
    """
1455

    
1456
    def main(self, container___path):
1457
        super(self.__class__,
1458
            self).main(container___path, path_is_optional=False)
1459
        try:
1460
            self.client.del_object_sharing(self.path)
1461
        except ClientError as err:
1462
            if err.status == 404:
1463
                if 'container' in ('%s' % err).lower():
1464
                    raiseCLIError(
1465
                        err,
1466
                        'No container %s in account %s'\
1467
                        % (self.container, self.account),
1468
                        details=errors.pithos.container_howto)
1469
                elif 'object' in ('%s' % err).lower():
1470
                    raiseCLIError(
1471
                        err,
1472
                        'No object %s in container %s'\
1473
                        % (self.path, self.container),
1474
                        details=errors.pithos.container_howto)
1475
            raise_connection_errors(err)
1476
            raiseCLIError(err)
1477
        except Exception as e:
1478
            raiseCLIError(e)
1479

    
1480

    
1481
@command(pithos_cmds)
1482
class store_info(_store_container_command):
1483
    """Get detailed information for user account, containers or objects
1484
    to get account info:    /store info
1485
    to get container info:  /store info <container>
1486
    to get object info:     /store info <container>:<path>
1487
    """
1488

    
1489
    arguments = dict(
1490
        object_version=ValueArgument(
1491
            'show specific version \ (applies only for objects)',
1492
            '--object-version')
1493
    )
1494

    
1495
    def main(self, container____path__=None):
1496
        super(self.__class__, self).main(container____path__)
1497
        try:
1498
            if self.container is None:
1499
                reply = self.client.get_account_info()
1500
            elif self.path is None:
1501
                reply = self.client.get_container_info(self.container)
1502
            else:
1503
                reply = self.client.get_object_info(
1504
                    self.path,
1505
                    version=self['object_version'])
1506
        except ClientError as err:
1507
            if err.status == 404:
1508
                if 'container' in ('%s' % err).lower():
1509
                    raiseCLIError(
1510
                        err,
1511
                        'No container %s in account %s'\
1512
                        % (self.container, self.account),
1513
                        details=errors.pithos.container_howto)
1514
                elif 'object' in ('%s' % err).lower():
1515
                    raiseCLIError(
1516
                        err,
1517
                        'No object %s in container %s'\
1518
                        % (self.path, self.container),
1519
                        details=errors.pithos.container_howto)
1520
            raise_connection_errors(err)
1521
            raiseCLIError(err)
1522
        except Exception as e:
1523
            raiseCLIError(e)
1524
        print_dict(reply)
1525

    
1526

    
1527
@command(pithos_cmds)
1528
class store_meta(_store_container_command):
1529
    """Get metadata for account, containers or objects"""
1530

    
1531
    arguments = dict(
1532
        detail=FlagArgument('show detailed output', '-l'),
1533
        until=DateArgument('show metadata until then', '--until'),
1534
        object_version=ValueArgument(
1535
            'show specific version \ (applies only for objects)',
1536
            '--object-version')
1537
    )
1538

    
1539
    def main(self, container____path__=None):
1540
        super(self.__class__, self).main(container____path__)
1541

    
1542
        detail = self['detail']
1543
        try:
1544
            until = self['until']
1545
            if self.container is None:
1546
                if detail:
1547
                    reply = self.client.get_account_info(until=until)
1548
                else:
1549
                    reply = self.client.get_account_meta(until=until)
1550
                    reply = pretty_keys(reply, '-')
1551
                if reply:
1552
                    print(bold(self.client.account))
1553
            elif self.path is None:
1554
                if detail:
1555
                    reply = self.client.get_container_info(until=until)
1556
                else:
1557
                    cmeta = self.client.get_container_meta(until=until)
1558
                    ometa = self.client.get_container_object_meta(until=until)
1559
                    reply = {}
1560
                    if cmeta:
1561
                        reply['container-meta'] = pretty_keys(cmeta, '-')
1562
                    if ometa:
1563
                        reply['object-meta'] = pretty_keys(ometa, '-')
1564
            else:
1565
                if detail:
1566
                    reply = self.client.get_object_info(self.path,
1567
                        version=self['object_version'])
1568
                else:
1569
                    reply = self.client.get_object_meta(self.path,
1570
                        version=self['object_version'])
1571
                if reply:
1572
                    reply = pretty_keys(pretty_keys(reply, '-'))
1573
        except ClientError as err:
1574
            if err.status == 404:
1575
                if 'container' in ('%s' % err).lower():
1576
                    raiseCLIError(
1577
                        err,
1578
                        'No container %s in account %s'\
1579
                        % (self.container, self.account),
1580
                        details=errors.pithos.container_howto)
1581
                elif 'object' in ('%s' % err).lower():
1582
                    raiseCLIError(
1583
                        err,
1584
                        'No object %s in container %s'\
1585
                        % (self.path, self.container),
1586
                        details=errors.pithos.container_howto)
1587
                else:
1588
                    raiseCLIError(err, details=errors.pithos.container_howto)
1589
            raise_connection_errors(err)
1590
            raiseCLIError(err)
1591
        except Exception as e:
1592
            raiseCLIError(e)
1593
        if reply:
1594
            print_dict(reply)
1595

    
1596

    
1597
@command(pithos_cmds)
1598
class store_setmeta(_store_container_command):
1599
    """Set a piece of metadata for account, container or object
1600
    Metadata are formed as key:value pairs
1601
    """
1602

    
1603
    def main(self, metakey___metaval, container____path__=None):
1604
        super(self.__class__, self).main(container____path__)
1605
        try:
1606
            metakey, metavalue = metakey___metaval.split(':')
1607
        except ValueError as err:
1608
            raiseCLIError(err,
1609
                'Cannot parse %s as a key:value pair' % metakey___metaval,
1610
                details=['Syntax:',
1611
                    '   store setmeta metakey:metavalue [cont[:path]]'
1612
                ],
1613
                importance=1,
1614
                )
1615
        try:
1616
            if self.container is None:
1617
                self.client.set_account_meta({metakey: metavalue})
1618
            elif self.path is None:
1619
                self.client.set_container_meta({metakey: metavalue})
1620
            else:
1621
                self.client.set_object_meta(self.path, {metakey: metavalue})
1622
        except ClientError as err:
1623
            if err.status == 404:
1624
                if 'container' in ('%s' % err).lower():
1625
                    raiseCLIError(
1626
                        err,
1627
                        'No container %s in account %s'\
1628
                        % (self.container, self.account),
1629
                        details=errors.pithos.container_howto)
1630
                elif 'object' in ('%s' % err).lower():
1631
                    raiseCLIError(
1632
                        err,
1633
                        'No object %s in container %s'\
1634
                        % (self.path, self.container),
1635
                        details=errors.pithos.container_howto)
1636
                else:
1637
                    raiseCLIError(err, details=errors.pithos.container_howto)
1638
            raise_connection_errors(err)
1639
            raiseCLIError(err)
1640
        except Exception as err:
1641
            raiseCLIError(err)
1642

    
1643

    
1644
@command(pithos_cmds)
1645
class store_delmeta(_store_container_command):
1646
    """Delete metadata with given key from account, container or object
1647
    Metadata are formed as key:value objects
1648
    - to get metadata of current account:     /store meta
1649
    - to get metadata of a container:         /store meta <container>
1650
    - to get metadata of an object:           /store meta <container>:<path>
1651
    """
1652

    
1653
    def main(self, metakey, container____path__=None):
1654
        super(self.__class__, self).main(container____path__)
1655
        try:
1656
            if self.container is None:
1657
                self.client.del_account_meta(metakey)
1658
            elif self.path is None:
1659
                self.client.del_container_meta(metakey)
1660
            else:
1661
                self.client.del_object_meta(self.path, metakey)
1662
        except ClientError as err:
1663
            if err.status == 404:
1664
                if 'container' in ('%s' % err).lower():
1665
                    raiseCLIError(
1666
                        err,
1667
                        'No container %s in account %s'\
1668
                        % (self.container, self.account),
1669
                        details=errors.pithos.container_howto)
1670
                elif 'object' in ('%s' % err).lower():
1671
                    raiseCLIError(
1672
                        err,
1673
                        'No object %s in container %s'\
1674
                        % (self.path, self.container),
1675
                        details=errors.pithos.container_howto)
1676
                else:
1677
                    raiseCLIError(err, details=errors.pithos.container_howto)
1678
            raise_connection_errors(err)
1679
            raiseCLIError(err)
1680
        except Exception as err:
1681
            raiseCLIError(err)
1682

    
1683

    
1684
@command(pithos_cmds)
1685
class store_quota(_store_account_command):
1686
    """Get quota for account or container"""
1687

    
1688
    arguments = dict(
1689
        in_bytes=FlagArgument('Show result in bytes', ('-b', '--bytes'))
1690
        )
1691

    
1692
    def main(self, container=None):
1693
        super(self.__class__, self).main()
1694
        try:
1695
            if container is None:
1696
                reply = self.client.get_account_quota()
1697
            else:
1698
                reply = self.client.get_container_quota(container)
1699
        except ClientError as err:
1700
            if err.status == 404:
1701
                if 'container' in ('%s' % err).lower():
1702
                    raiseCLIError(err,
1703
                        'No container %s in account %s'\
1704
                        % (container, self.account))
1705
            raise_connection_errors(err)
1706
            raiseCLIError(err)
1707
        except Exception as err:
1708
            raiseCLIError(err)
1709
        if not self['in_bytes']:
1710
            for k in reply:
1711
                reply[k] = format_size(reply[k])
1712
        print_dict(pretty_keys(reply, '-'))
1713

    
1714

    
1715
@command(pithos_cmds)
1716
class store_setquota(_store_account_command):
1717
    """Set new quota for account or container
1718
    By default, quota is set in bytes
1719
    Users may specify a different unit, e.g:
1720
    /store setquota 2.3GB mycontainer
1721
    Accepted units: B, KiB (1024 B), KB (1000 B), MiB, MB, GiB, GB, TiB, TB
1722
    """
1723

    
1724
    def _calculate_quota(self, user_input):
1725
        quota = 0
1726
        try:
1727
            quota = int(user_input)
1728
        except ValueError:
1729
            index = 0
1730
            digits = [str(num) for num in range(0, 10)] + ['.']
1731
            while user_input[index] in digits:
1732
                index += 1
1733
            quota = user_input[:index]
1734
            format = user_input[index:]
1735
            try:
1736
                return to_bytes(quota, format)
1737
            except Exception as qe:
1738
                raiseCLIError(qe,
1739
                    'Failed to convert %s to bytes' % user_input,
1740
                    details=['Syntax: setquota <quota>[format] [container]',
1741
                        'e.g.: setquota 2.3GB mycontainer',
1742
                        'Acceptable formats:',
1743
                        '(*1024): B, KiB, MiB, GiB, TiB',
1744
                        '(*1000): B, KB, MB, GB, TB'])
1745
        return quota
1746

    
1747
    def main(self, quota, container=None):
1748
        super(self.__class__, self).main()
1749
        quota = self._calculate_quota(quota)
1750
        try:
1751
            if container is None:
1752
                self.client.set_account_quota(quota)
1753
            else:
1754
                self.client.container = container
1755
                self.client.set_container_quota(quota)
1756
        except ClientError as err:
1757
            if err.status == 404:
1758
                if 'container' in ('%s' % err).lower():
1759
                    raiseCLIError(err,
1760
                        'No container %s in account %s'\
1761
                        % (container, self.account))
1762
            raise_connection_errors(err)
1763
            raiseCLIError(err)
1764
        except Exception as err:
1765
            raiseCLIError(err)
1766

    
1767

    
1768
@command(pithos_cmds)
1769
class store_versioning(_store_account_command):
1770
    """Get  versioning for account or container"""
1771

    
1772
    def main(self, container=None):
1773
        super(self.__class__, self).main()
1774
        try:
1775
            if container is None:
1776
                reply = self.client.get_account_versioning()
1777
            else:
1778
                reply = self.client.get_container_versioning(container)
1779
        except ClientError as err:
1780
            if err.status == 404:
1781
                if 'container' in ('%s' % err).lower():
1782
                    raiseCLIError(
1783
                        err,
1784
                        'No container %s in account %s'\
1785
                        % (self.container, self.account),
1786
                        details=errors.pithos.container_howto)
1787
                else:
1788
                    raiseCLIError(err, details=errors.pithos.container_howto)
1789
            raise_connection_errors(err)
1790
            raiseCLIError(err)
1791
        except Exception as err:
1792
            raiseCLIError(err)
1793
        print_dict(reply)
1794

    
1795

    
1796
@command(pithos_cmds)
1797
class store_setversioning(_store_account_command):
1798
    """Set versioning mode (auto, none) for account or container"""
1799

    
1800
    def main(self, versioning, container=None):
1801
        super(self.__class__, self).main()
1802
        try:
1803
            if container is None:
1804
                self.client.set_account_versioning(versioning)
1805
            else:
1806
                self.client.container = container
1807
                self.client.set_container_versioning(versioning)
1808
        except ClientError as err:
1809
            if err.status == 404:
1810
                if 'container' in ('%s' % err).lower():
1811
                    raiseCLIError(
1812
                        err,
1813
                        'No container %s in account %s'\
1814
                        % (self.container, self.account),
1815
                        details=errors.pithos.container_howto)
1816
                else:
1817
                    raiseCLIError(err, details=errors.pithos.container_howto)
1818
            raise_connection_errors(err)
1819
            raiseCLIError(err)
1820
        except Exception as err:
1821
            raiseCLIError(err)
1822

    
1823

    
1824
@command(pithos_cmds)
1825
class store_group(_store_account_command):
1826
    """Get groups and group members"""
1827

    
1828
    def main(self):
1829
        super(self.__class__, self).main()
1830
        try:
1831
            reply = self.client.get_account_group()
1832
        except ClientError as err:
1833
            raise_connection_errors(err)
1834
            raiseCLIError(err)
1835
        except Exception as err:
1836
            raiseCLIError(err)
1837
        print_dict(pretty_keys(reply, '-'))
1838

    
1839

    
1840
@command(pithos_cmds)
1841
class store_setgroup(_store_account_command):
1842
    """Set a user group"""
1843

    
1844
    def main(self, groupname, *users):
1845
        super(self.__class__, self).main()
1846
        try:
1847
            self.client.set_account_group(groupname, users)
1848
        except ClientError as err:
1849
            raise_connection_errors(err)
1850
            raiseCLIError(err)
1851
        except Exception as err:
1852
            raiseCLIError(err)
1853

    
1854

    
1855
@command(pithos_cmds)
1856
class store_delgroup(_store_account_command):
1857
    """Delete a user group"""
1858

    
1859
    def main(self, groupname):
1860
        super(self.__class__, self).main()
1861
        try:
1862
            self.client.del_account_group(groupname)
1863
        except ClientError as err:
1864
            raise_connection_errors(err)
1865
            raiseCLIError(err)
1866
        except Exception as err:
1867
            raiseCLIError(err)
1868

    
1869

    
1870
@command(pithos_cmds)
1871
class store_sharers(_store_account_command):
1872
    """List the accounts that share objects with current user"""
1873

    
1874
    arguments = dict(
1875
        detail=FlagArgument('show detailed output', '-l'),
1876
        marker=ValueArgument('show output greater then marker', '--marker')
1877
    )
1878

    
1879
    def main(self):
1880
        super(self.__class__, self).main()
1881
        try:
1882
            marker = self['marker']
1883
            accounts = self.client.get_sharing_accounts(marker=marker)
1884
        except ClientError as err:
1885
            raise_connection_errors(err)
1886
            raiseCLIError(err)
1887
        except Exception as err:
1888
            raiseCLIError(err)
1889

    
1890
        for acc in accounts:
1891
            stdout.write(bold(acc['name']) + ' ')
1892
            if self['detail']:
1893
                print_dict(acc, exclude='name')
1894
        if not self['detail']:
1895
            print
1896

    
1897

    
1898
@command(pithos_cmds)
1899
class store_versions(_store_container_command):
1900
    """Get the list of object versions
1901
    Deleted objects may still have versions that can be used to restore it and
1902
    get information about its previous state.
1903
    The version number can be used in a number of other commands, like info,
1904
    copy, move, meta. See these commands for more information, e.g.
1905
    /store info -h
1906
    """
1907

    
1908
    def main(self, container___path):
1909
        super(store_versions, self).main(container___path)
1910
        try:
1911
            versions = self.client.get_object_versionlist(self.path)
1912

    
1913
            for vitem in versions:
1914
                t = localtime(float(vitem[1]))
1915
                vid = bold(unicode(vitem[0]))
1916
                print('\t%s \t(%s)' % (vid, strftime('%d-%m-%Y %H:%M:%S', t)))
1917
        except ClientError as err:
1918
            if err.status == 404:
1919
                if 'container' in ('%s' % err).lower():
1920
                    raiseCLIError(
1921
                        err,
1922
                        'No container %s in account %s'\
1923
                        % (self.container, self.account),
1924
                        details=errors.pithos.container_howto)
1925
                elif 'object' in ('%s' % err).lower():
1926
                    raiseCLIError(
1927
                        err,
1928
                        'No object %s in container %s'\
1929
                        % (self.path, self.container),
1930
                        details=errors.pithos.container_howto)
1931
                else:
1932
                    raiseCLIError(err, details=errors.pithos.container_howto)
1933
            raise_connection_errors(err)
1934
            raiseCLIError(err)
1935
        except Exception as err:
1936
            raiseCLIError(err)