Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / pithos_cli.py @ 285d7238

History | View | Annotate | Download (57.6 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
    print_items,
47
    pretty_keys,
48
    page_hold,
49
    bold,
50
    ask_user)
51
from kamaki.cli.argument import FlagArgument, ValueArgument, IntArgument
52
from kamaki.cli.argument import KeyValueArgument, DateArgument
53
from kamaki.cli.argument import ProgressBarArgument
54
from kamaki.cli.commands import _command_init, errors
55
from kamaki.clients.pithos import PithosClient, ClientError
56

    
57

    
58
kloger = getLogger('kamaki')
59

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

    
63

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

    
71

    
72
# Argument functionality
73

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

    
98

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

103
    :param end: (int)
104

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

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

    
122

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

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

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

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

    
143

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

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

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

    
183

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

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

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

    
204
# Command specs
205

    
206

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

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

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

    
228

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

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

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

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

    
247

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

    
251
    container = None
252
    path = None
253

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

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

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

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

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

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

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

    
329

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

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

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

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

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

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

    
457

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

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

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

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

    
476

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

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

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

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

    
500

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

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

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

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

    
530

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

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

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

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

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

    
610

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

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

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

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

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

    
684

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

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

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

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

    
721

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

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

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

    
738

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

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

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

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

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

    
791

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

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

    
824
    @errors.generic.all
825
    @errors.pithos.connection
826
    @errors.pithos.container
827
    @errors.pithos.object_path
828
    def _run(self):
829
        self.client.create_object_by_manifestation(
830
            self.path,
831
            content_encoding=self['content_encoding'],
832
            content_disposition=self['content_disposition'],
833
            content_type=self['content_type'],
834
            sharing=self['sharing'],
835
            public=self['public'])
836

    
837
    def main(self, container___path):
838
        super(self.__class__, self)._run(
839
            container___path,
840
            path_is_optional=False)
841
        self.run()
842

    
843

    
844
@command(pithos_cmds)
845
class store_upload(_store_container_command):
846
    """Upload a file"""
847

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

    
874
    def _remote_path(self, remote_path, local_path=''):
875
        if self['overwrite']:
876
            return remote_path
877
        try:
878
            r = self.client.get_object_info(remote_path)
879
        except ClientError as ce:
880
            if ce.status == 404:
881
                return remote_path
882
            raise ce
883
        ctype = r.get('content-type', '')
884
        if 'application/directory' == ctype.lower():
885
            ret = '%s/%s' % (remote_path, local_path)
886
            return self._remote_path(ret) if local_path else ret
887
        raiseCLIError(
888
            'Object %s already exists' % remote_path,
889
            importance=1,
890
            details=['use -f to overwrite or resume'])
891

    
892
    @errors.generic.all
893
    @errors.pithos.connection
894
    @errors.pithos.container
895
    @errors.pithos.object_path
896
    @errors.pithos.local_path
897
    def _run(self, local_path, remote_path):
898
        poolsize = self['poolsize']
899
        if poolsize > 0:
900
            self.client.POOL_SIZE = int(poolsize)
901
        params = dict(
902
            content_encoding=self['content_encoding'],
903
            content_type=self['content_type'],
904
            content_disposition=self['content_disposition'],
905
            sharing=self['sharing'],
906
            public=self['public'])
907
        remote_path = self._remote_path(remote_path, local_path)
908
        with open(path.abspath(local_path), 'rb') as f:
909
            if self['unchunked']:
910
                self.client.upload_object_unchunked(
911
                    remote_path,
912
                    f,
913
                    etag=self['etag'],
914
                    withHashFile=self['use_hashes'],
915
                    **params)
916
            else:
917
                try:
918
                    (progress_bar, upload_cb) = self._safe_progress_bar(
919
                        'Uploading')
920
                    if progress_bar:
921
                        hash_bar = progress_bar.clone()
922
                        hash_cb = hash_bar.get_generator(
923
                                    'Calculating block hashes')
924
                    else:
925
                        hash_cb = None
926
                    self.client.upload_object(
927
                        remote_path,
928
                        f,
929
                        hash_cb=hash_cb,
930
                        upload_cb=upload_cb,
931
                        **params)
932
                except Exception:
933
                    self._safe_progress_bar_finish(progress_bar)
934
                    raise
935
                finally:
936
                    self._safe_progress_bar_finish(progress_bar)
937
        print 'Upload completed'
938

    
939
    def main(self, local_path, container____path__=None):
940
        super(self.__class__, self)._run(container____path__)
941
        remote_path = self.path if self.path else path.basename(local_path)
942
        self._run(local_path=local_path, remote_path=remote_path)
943

    
944

    
945
@command(pithos_cmds)
946
class store_cat(_store_container_command):
947
    """Print remote file contents to console"""
948

    
949
    arguments = dict(
950
        range=RangeArgument('show range of data', '--range'),
951
        if_match=ValueArgument('show output if ETags match', '--if-match'),
952
        if_none_match=ValueArgument(
953
            'show output if ETags match',
954
            '--if-none-match'),
955
        if_modified_since=DateArgument(
956
            'show output modified since then',
957
            '--if-modified-since'),
958
        if_unmodified_since=DateArgument(
959
            'show output unmodified since then',
960
            '--if-unmodified-since'),
961
        object_version=ValueArgument(
962
            'get the specific version',
963
            '--object-version')
964
    )
965

    
966
    @errors.generic.all
967
    @errors.pithos.connection
968
    @errors.pithos.container
969
    @errors.pithos.object_path
970
    def _run(self):
971
        self.client.download_object(
972
            self.path,
973
            stdout,
974
            range=self['range'],
975
            version=self['object_version'],
976
            if_match=self['if_match'],
977
            if_none_match=self['if_none_match'],
978
            if_modified_since=self['if_modified_since'],
979
            if_unmodified_since=self['if_unmodified_since'])
980

    
981
    def main(self, container___path):
982
        super(self.__class__, self)._run(
983
            container___path,
984
            path_is_optional=False)
985
        self._run()
986

    
987

    
988
@command(pithos_cmds)
989
class store_download(_store_container_command):
990
    """Download remote object as local file"""
991

    
992
    arguments = dict(
993
        resume=FlagArgument('Resume instead of overwrite', '--resume'),
994
        range=RangeArgument('show range of data', '--range'),
995
        if_match=ValueArgument('show output if ETags match', '--if-match'),
996
        if_none_match=ValueArgument(
997
            'show output if ETags match',
998
            '--if-none-match'),
999
        if_modified_since=DateArgument(
1000
            'show output modified since then',
1001
            '--if-modified-since'),
1002
        if_unmodified_since=DateArgument(
1003
            'show output unmodified since then',
1004
            '--if-unmodified-since'),
1005
        object_version=ValueArgument(
1006
            'get the specific version',
1007
            '--object-version'),
1008
        poolsize=IntArgument('set pool size', '--with-pool-size'),
1009
        progress_bar=ProgressBarArgument(
1010
            'do not show progress bar',
1011
            '--no-progress-bar',
1012
            default=False)
1013
    )
1014

    
1015
    def _output_stream(self, local_path):
1016
        if local_path is None:
1017
            return stdout
1018
        return open(
1019
            path.abspath(local_path), 'rwb+' if self['resume'] else 'wb+')
1020

    
1021
    @errors.generic.all
1022
    @errors.pithos.connection
1023
    @errors.pithos.container
1024
    @errors.pithos.object_path
1025
    @errors.pithos.local_path
1026
    def _run(self, local_path):
1027
        out = self._output_stream(local_path)
1028
        poolsize = self['poolsize']
1029
        if poolsize:
1030
            self.client.POOL_SIZE = int(poolsize)
1031
        try:
1032
            (progress_bar, download_cb) = self._safe_progress_bar(
1033
                'Downloading')
1034
            self.client.download_object(
1035
                self.path,
1036
                out,
1037
                download_cb=download_cb,
1038
                range=self['range'],
1039
                version=self['object_version'],
1040
                if_match=self['if_match'],
1041
                resume=self['resume'],
1042
                if_none_match=self['if_none_match'],
1043
                if_modified_since=self['if_modified_since'],
1044
                if_unmodified_since=self['if_unmodified_since'])
1045
        except KeyboardInterrupt:
1046
            from threading import enumerate as activethreads
1047
            stdout.write('\nFinishing active threads ')
1048
            for thread in activethreads():
1049
                stdout.flush()
1050
                try:
1051
                    thread.join()
1052
                    stdout.write('.')
1053
                except RuntimeError:
1054
                    continue
1055
            print('\ndownload canceled by user')
1056
            if local_path is not None:
1057
                print('to resume, re-run with --resume')
1058
        except Exception:
1059
            self._safe_progress_bar_finish(progress_bar)
1060
            raise
1061
        finally:
1062
            self._safe_progress_bar_finish(progress_bar)
1063

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

    
1070

    
1071
@command(pithos_cmds)
1072
class store_hashmap(_store_container_command):
1073
    """Get the hash-map of an object"""
1074

    
1075
    arguments = dict(
1076
        if_match=ValueArgument('show output if ETags match', '--if-match'),
1077
        if_none_match=ValueArgument(
1078
            'show output if ETags match',
1079
            '--if-none-match'),
1080
        if_modified_since=DateArgument(
1081
            'show output modified since then',
1082
            '--if-modified-since'),
1083
        if_unmodified_since=DateArgument(
1084
            'show output unmodified since then',
1085
            '--if-unmodified-since'),
1086
        object_version=ValueArgument(
1087
            'get the specific version',
1088
            '--object-version')
1089
    )
1090

    
1091
    @errors.generic.all
1092
    @errors.pithos.connection
1093
    @errors.pithos.container
1094
    @errors.pithos.object_path
1095
    def _run(self):
1096
        data = self.client.get_object_hashmap(
1097
            self.path,
1098
            version=self['object_version'],
1099
            if_match=self['if_match'],
1100
            if_none_match=self['if_none_match'],
1101
            if_modified_since=self['if_modified_since'],
1102
            if_unmodified_since=self['if_unmodified_since'])
1103
        print_dict(data)
1104

    
1105
    def main(self, container___path):
1106
        super(self.__class__, self)._run(
1107
            container___path,
1108
            path_is_optional=False)
1109
        self._run()
1110

    
1111

    
1112
@command(pithos_cmds)
1113
class store_delete(_store_container_command):
1114
    """Delete a container [or an object]
1115
    How to delete a non-empty container:
1116
    - empty the container:  /store delete -r <container>
1117
    - delete it:            /store delete <container>
1118
    .
1119
    Semantics of directory deletion:
1120
    .a preserve the contents: /store delete <container>:<directory>
1121
    .    objects of the form dir/filename can exist with a dir object
1122
    .b delete contents:       /store delete -r <container>:<directory>
1123
    .    all dir/* objects are affected, even if dir does not exist
1124
    .
1125
    To restore a deleted object OBJ in a container CONT:
1126
    - get object versions: /store versions CONT:OBJ
1127
    .   and choose the version to be restored
1128
    - restore the object:  /store copy --source-version=<version> CONT:OBJ OBJ
1129
    """
1130

    
1131
    arguments = dict(
1132
        until=DateArgument('remove history until that date', '--until'),
1133
        yes=FlagArgument('Do not prompt for permission', '--yes'),
1134
        recursive=FlagArgument(
1135
            'empty dir or container and delete (if dir)',
1136
            ('-r', '--recursive'))
1137
    )
1138

    
1139
    def __init__(self, arguments={}):
1140
        super(self.__class__, self).__init__(arguments)
1141
        self['delimiter'] = DelimiterArgument(
1142
            self,
1143
            parsed_name='--delimiter',
1144
            help='delete objects prefixed with <object><delimiter>')
1145

    
1146
    @errors.generic.all
1147
    @errors.pithos.connection
1148
    @errors.pithos.container
1149
    @errors.pithos.object_path
1150
    def _run(self):
1151
        if self.path:
1152
            if self['yes'] or ask_user(
1153
                'Delete %s:%s ?' % (self.container, self.path)):
1154
                self.client.del_object(
1155
                    self.path,
1156
                    until=self['until'],
1157
                    delimiter=self['delimiter'])
1158
            else:
1159
                print('Aborted')
1160
        else:
1161
            ask_msg = 'Delete contents of container'\
1162
            if self['recursive'] else 'Delete container'
1163
            if self['yes'] or ask_user('%s %s ?' % (ask_msg, self.container)):
1164
                self.client.del_container(
1165
                    until=self['until'],
1166
                    delimiter=self['delimiter'])
1167
            else:
1168
                print('Aborted')
1169

    
1170
    def main(self, container____path__):
1171
        super(self.__class__, self)._run(container____path__)
1172
        self._run()
1173

    
1174

    
1175
@command(pithos_cmds)
1176
class store_purge(_store_container_command):
1177
    """Delete a container and release related data blocks
1178
    Non-empty containers can not purged.
1179
    To purge a container with content:
1180
    .   /store delete -r <container>
1181
    .      objects are deleted, but data blocks remain on server
1182
    .   /store purge <container>
1183
    .      container and data blocks are released and deleted
1184
    """
1185

    
1186
    arguments = dict(
1187
        yes=FlagArgument('Do not prompt for permission', '--yes'),
1188
    )
1189

    
1190
    @errors.generic.all
1191
    @errors.pithos.connection
1192
    @errors.pithos.container
1193
    def _run(self):
1194
        if self['yes'] or ask_user('Purge container %s?' % self.container):
1195
                self.client.purge_container()
1196
        else:
1197
            print('Aborted')
1198

    
1199
    def main(self, container):
1200
        super(self.__class__, self)._run(container)
1201
        self._run()
1202

    
1203

    
1204
@command(pithos_cmds)
1205
class store_publish(_store_container_command):
1206
    """Publish the object and print the public url"""
1207

    
1208
    @errors.generic.all
1209
    @errors.pithos.connection
1210
    @errors.pithos.container
1211
    @errors.pithos.object_path
1212
    def _run(self):
1213
        url = self.client.publish_object(self.path)
1214
        print(url)
1215

    
1216
    def main(self, container___path):
1217
        super(self.__class__, self)._run(
1218
            container___path,
1219
            path_is_optional=False)
1220
        self._run()
1221

    
1222

    
1223
@command(pithos_cmds)
1224
class store_unpublish(_store_container_command):
1225
    """Unpublish an object"""
1226

    
1227
    @errors.generic.all
1228
    @errors.pithos.connection
1229
    @errors.pithos.container
1230
    @errors.pithos.object_path
1231
    def _run(self):
1232
            self.client.unpublish_object(self.path)
1233

    
1234
    def main(self, container___path):
1235
        super(self.__class__, self)._run(
1236
            container___path,
1237
            path_is_optional=False)
1238
        self._run()
1239

    
1240

    
1241
@command(pithos_cmds)
1242
class store_permissions(_store_container_command):
1243
    """Get read and write permissions of an object
1244
    Permissions are lists of users and user groups. There is read and write
1245
    permissions. Users and groups with write permission have also read
1246
    permission.
1247
    """
1248

    
1249
    @errors.generic.all
1250
    @errors.pithos.connection
1251
    @errors.pithos.container
1252
    @errors.pithos.object_path
1253
    def _run(self):
1254
        r = self.client.get_object_sharing(self.path)
1255
        print_dict(r)
1256

    
1257
    def main(self, container___path):
1258
        super(self.__class__, self)._run(
1259
            container___path,
1260
            path_is_optional=False)
1261
        self._run()
1262

    
1263

    
1264
@command(pithos_cmds)
1265
class store_setpermissions(_store_container_command):
1266
    """Set permissions for an object
1267
    New permissions overwrite existing permissions.
1268
    Permission format:
1269
    -   read=<username>[,usergroup[,...]]
1270
    -   write=<username>[,usegroup[,...]]
1271
    E.g. to give read permissions for file F to users A and B and write for C:
1272
    .       /store setpermissions F read=A,B write=C
1273
    """
1274

    
1275
    @errors.generic.all
1276
    def format_permition_dict(self, permissions):
1277
        read = False
1278
        write = False
1279
        for perms in permissions:
1280
            splstr = perms.split('=')
1281
            if 'read' == splstr[0]:
1282
                read = [user_or_group.strip() \
1283
                for user_or_group in splstr[1].split(',')]
1284
            elif 'write' == splstr[0]:
1285
                write = [user_or_group.strip() \
1286
                for user_or_group in splstr[1].split(',')]
1287
            else:
1288
                read = False
1289
                write = False
1290
        if not read and not write:
1291
            raiseCLIError(None,
1292
            'Usage:\tread=<groups,users> write=<groups,users>')
1293
        return (read, write)
1294

    
1295
    @errors.generic.all
1296
    @errors.pithos.connection
1297
    @errors.pithos.container
1298
    @errors.pithos.object_path
1299
    def _run(self, read, write):
1300
        self.client.set_object_sharing(
1301
            self.path,
1302
            read_permition=read,
1303
            write_permition=write)
1304

    
1305
    def main(self, container___path, *permissions):
1306
        super(self.__class__, self)._run(
1307
            container___path,
1308
            path_is_optional=False)
1309
        (read, write) = self.format_permition_dict(permissions)
1310
        self._run(read, write)
1311

    
1312

    
1313
@command(pithos_cmds)
1314
class store_delpermissions(_store_container_command):
1315
    """Delete all permissions set on object
1316
    To modify permissions, use /store setpermssions
1317
    """
1318

    
1319
    @errors.generic.all
1320
    @errors.pithos.connection
1321
    @errors.pithos.container
1322
    @errors.pithos.object_path
1323
    def _run(self):
1324
        self.client.del_object_sharing(self.path)
1325

    
1326
    def main(self, container___path):
1327
        super(self.__class__, self)._run(
1328
            container___path,
1329
            path_is_optional=False)
1330
        self._run()
1331

    
1332

    
1333
@command(pithos_cmds)
1334
class store_info(_store_container_command):
1335
    """Get detailed information for user account, containers or objects
1336
    to get account info:    /store info
1337
    to get container info:  /store info <container>
1338
    to get object info:     /store info <container>:<path>
1339
    """
1340

    
1341
    arguments = dict(
1342
        object_version=ValueArgument(
1343
            'show specific version \ (applies only for objects)',
1344
            '--object-version')
1345
    )
1346

    
1347
    @errors.generic.all
1348
    @errors.pithos.connection
1349
    @errors.pithos.container
1350
    @errors.pithos.object_path
1351
    def _run(self):
1352
        if self.container is None:
1353
            r = self.client.get_account_info()
1354
        elif self.path is None:
1355
            r = self.client.get_container_info(self.container)
1356
        else:
1357
            r = self.client.get_object_info(
1358
                self.path,
1359
                version=self['object_version'])
1360
        print_dict(r)
1361

    
1362
    def main(self, container____path__=None):
1363
        super(self.__class__, self)._run(container____path__)
1364
        self._run()
1365

    
1366

    
1367
@command(pithos_cmds)
1368
class store_meta(_store_container_command):
1369
    """Get metadata for account, containers or objects"""
1370

    
1371
    arguments = dict(
1372
        detail=FlagArgument('show detailed output', '-l'),
1373
        until=DateArgument('show metadata until then', '--until'),
1374
        object_version=ValueArgument(
1375
            'show specific version \ (applies only for objects)',
1376
            '--object-version')
1377
    )
1378

    
1379
    @errors.generic.all
1380
    @errors.pithos.connection
1381
    @errors.pithos.container
1382
    @errors.pithos.object_path
1383
    def _run(self):
1384
        until = self['until']
1385
        if self.container is None:
1386
            if self['detail']:
1387
                r = self.client.get_account_info(until=until)
1388
            else:
1389
                r = self.client.get_account_meta(until=until)
1390
                r = pretty_keys(r, '-')
1391
            if r:
1392
                print(bold(self.client.account))
1393
        elif self.path is None:
1394
            if self['detail']:
1395
                r = self.client.get_container_info(until=until)
1396
            else:
1397
                cmeta = self.client.get_container_meta(until=until)
1398
                ometa = self.client.get_container_object_meta(until=until)
1399
                r = {}
1400
                if cmeta:
1401
                    r['container-meta'] = pretty_keys(cmeta, '-')
1402
                if ometa:
1403
                    r['object-meta'] = pretty_keys(ometa, '-')
1404
        else:
1405
            if self['detail']:
1406
                r = self.client.get_object_info(self.path,
1407
                    version=self['object_version'])
1408
            else:
1409
                r = self.client.get_object_meta(self.path,
1410
                    version=self['object_version'])
1411
            if r:
1412
                r = pretty_keys(pretty_keys(r, '-'))
1413
        if r:
1414
            print_dict(r)
1415

    
1416
    def main(self, container____path__=None):
1417
        super(self.__class__, self)._run(container____path__)
1418
        self._run()
1419

    
1420

    
1421
@command(pithos_cmds)
1422
class store_setmeta(_store_container_command):
1423
    """Set a piece of metadata for account, container or object
1424
    Metadata are formed as key:value pairs
1425
    """
1426

    
1427
    @errors.generic.all
1428
    @errors.pithos.connection
1429
    @errors.pithos.container
1430
    @errors.pithos.object_path
1431
    def _run(self, metakey, metaval):
1432
        if not self.container:
1433
            self.client.set_account_meta({metakey: metaval})
1434
        elif not self.path:
1435
            self.client.set_container_meta({metakey: metaval})
1436
        else:
1437
            self.client.set_object_meta(self.path, {metakey: metaval})
1438

    
1439
    def main(self, metakey, metaval, container____path__=None):
1440
        super(self.__class__, self)._run(container____path__)
1441
        self._run(metakey=metakey, metaval=metaval)
1442

    
1443

    
1444
@command(pithos_cmds)
1445
class store_delmeta(_store_container_command):
1446
    """Delete metadata with given key from account, container or object
1447
    Metadata are formed as key:value objects
1448
    - to get metadata of current account:     /store meta
1449
    - to get metadata of a container:         /store meta <container>
1450
    - to get metadata of an object:           /store meta <container>:<path>
1451
    """
1452

    
1453
    @errors.generic.all
1454
    @errors.pithos.connection
1455
    @errors.pithos.container
1456
    @errors.pithos.object_path
1457
    def _run(self, metakey):
1458
        if self.container is None:
1459
            self.client.del_account_meta(metakey)
1460
        elif self.path is None:
1461
            self.client.del_container_meta(metakey)
1462
        else:
1463
            self.client.del_object_meta(self.path, metakey)
1464

    
1465
    def main(self, metakey, container____path__=None):
1466
        super(self.__class__, self)._run(container____path__)
1467
        self._run(metakey)
1468

    
1469

    
1470
@command(pithos_cmds)
1471
class store_quota(_store_account_command):
1472
    """Get quota for account or container"""
1473

    
1474
    arguments = dict(
1475
        in_bytes=FlagArgument('Show result in bytes', ('-b', '--bytes'))
1476
        )
1477

    
1478
    @errors.generic.all
1479
    @errors.pithos.connection
1480
    @errors.pithos.container
1481
    def _run(self):
1482
        if self.container is None:
1483
            reply = self.client.get_account_quota()
1484
        else:
1485
            reply = self.client.get_container_quota(self.container)
1486
        if not self['in_bytes']:
1487
            for k in reply:
1488
                reply[k] = format_size(reply[k])
1489
        print_dict(pretty_keys(reply, '-'))
1490

    
1491
    def main(self, container=None):
1492
        super(self.__class__, self)._run()
1493
        self.container = container
1494
        self._run()
1495

    
1496

    
1497
@command(pithos_cmds)
1498
class store_setquota(_store_account_command):
1499
    """Set new quota for account or container
1500
    By default, quota is set in bytes
1501
    Users may specify a different unit, e.g:
1502
    /store setquota 2.3GB mycontainer
1503
    Accepted units: B, KiB (1024 B), KB (1000 B), MiB, MB, GiB, GB, TiB, TB
1504
    """
1505

    
1506
    @errors.generic.all
1507
    def _calculate_quota(self, user_input):
1508
        quota = 0
1509
        try:
1510
            quota = int(user_input)
1511
        except ValueError:
1512
            index = 0
1513
            digits = [str(num) for num in range(0, 10)] + ['.']
1514
            while user_input[index] in digits:
1515
                index += 1
1516
            quota = user_input[:index]
1517
            format = user_input[index:]
1518
            try:
1519
                return to_bytes(quota, format)
1520
            except Exception as qe:
1521
                raiseCLIError(qe,
1522
                    'Failed to convert %s to bytes' % user_input,
1523
                    details=[
1524
                        'Syntax: setquota <quota>[format] [container]',
1525
                        'e.g.: setquota 2.3GB mycontainer',
1526
                        'Acceptable formats:',
1527
                        '(*1024): B, KiB, MiB, GiB, TiB',
1528
                        '(*1000): B, KB, MB, GB, TB'])
1529
        return quota
1530

    
1531
    @errors.generic.all
1532
    @errors.pithos.connection
1533
    @errors.pithos.container
1534
    def _run(self, quota):
1535
        if self.container:
1536
            self.client.container = self.container
1537
            self.client.set_container_quota(quota)
1538
        else:
1539
            self.client.set_account_quota(quota)
1540

    
1541
    def main(self, quota, container=None):
1542
        super(self.__class__, self)._run()
1543
        quota = self._calculate_quota(quota)
1544
        self.container = container
1545
        self._run(quota)
1546

    
1547

    
1548
@command(pithos_cmds)
1549
class store_versioning(_store_account_command):
1550
    """Get  versioning for account or container"""
1551

    
1552
    @errors.generic.all
1553
    @errors.pithos.connection
1554
    @errors.pithos.container
1555
    def _run(self):
1556
        if self.container is None:
1557
            r = self.client.get_account_versioning()
1558
        else:
1559
            r = self.client.get_container_versioning(self.container)
1560
        print_dict(r)
1561

    
1562
    def main(self, container=None):
1563
        super(self.__class__, self)._run()
1564
        self.container = container
1565
        self._run()
1566

    
1567

    
1568
@command(pithos_cmds)
1569
class store_setversioning(_store_account_command):
1570
    """Set versioning mode (auto, none) for account or container"""
1571

    
1572
    def _check_versioning(self, versioning):
1573
        if versioning and versioning.lower() in ('auto', 'none'):
1574
            return versioning.lower()
1575
        raiseCLIError('Invalid versioning %s' % versioning, details=[
1576
            'Versioning can be auto or none'])
1577

    
1578
    @errors.generic.all
1579
    @errors.pithos.connection
1580
    @errors.pithos.container
1581
    def _run(self, versioning):
1582
        if self.container is None:
1583
            self.client.set_account_versioning(versioning)
1584
        else:
1585
            self.client.container = self.container
1586
            self.client.set_container_versioning(versioning)
1587

    
1588
    def main(self, versioning, container=None):
1589
        super(self.__class__, self)._run()
1590
        self._run(self._check_versioning(versioning))
1591

    
1592

    
1593
@command(pithos_cmds)
1594
class store_group(_store_account_command):
1595
    """Get groups and group members"""
1596

    
1597
    @errors.generic.all
1598
    @errors.pithos.connection
1599
    def _run(self):
1600
        r = self.client.get_account_group()
1601
        print_dict(pretty_keys(r, '-'))
1602

    
1603
    def main(self):
1604
        super(self.__class__, self)._run()
1605
        self._run()
1606

    
1607

    
1608
@command(pithos_cmds)
1609
class store_setgroup(_store_account_command):
1610
    """Set a user group"""
1611

    
1612
    @errors.generic.all
1613
    @errors.pithos.connection
1614
    def _run(self, groupname, *users):
1615
        self.client.set_account_group(groupname, users)
1616

    
1617
    def main(self, groupname, *users):
1618
        super(self.__class__, self)._run()
1619
        if users:
1620
            self._run(groupname, *users)
1621
        else:
1622
            raiseCLIError('No users to add in group %s' % groupname)
1623

    
1624

    
1625
@command(pithos_cmds)
1626
class store_delgroup(_store_account_command):
1627
    """Delete a user group"""
1628

    
1629
    @errors.generic.all
1630
    @errors.pithos.connection
1631
    def _run(self, groupname):
1632
        self.client.del_account_group(groupname)
1633

    
1634
    def main(self, groupname):
1635
        super(self.__class__, self)._run()
1636
        self._run(groupname)
1637

    
1638

    
1639
@command(pithos_cmds)
1640
class store_sharers(_store_account_command):
1641
    """List the accounts that share objects with current user"""
1642

    
1643
    arguments = dict(
1644
        detail=FlagArgument('show detailed output', '-l'),
1645
        marker=ValueArgument('show output greater then marker', '--marker')
1646
    )
1647

    
1648
    @errors.generic.all
1649
    @errors.pithos.connection
1650
    def _run(self):
1651
        accounts = self.client.get_sharing_accounts(marker=self['marker'])
1652
        print_items(accounts if self['detail']\
1653
        else [acc['name'] for acc in accounts])
1654

    
1655
    def main(self):
1656
        super(self.__class__, self)._run()
1657
        self._run()
1658

    
1659

    
1660
@command(pithos_cmds)
1661
class store_versions(_store_container_command):
1662
    """Get the list of object versions
1663
    Deleted objects may still have versions that can be used to restore it and
1664
    get information about its previous state.
1665
    The version number can be used in a number of other commands, like info,
1666
    copy, move, meta. See these commands for more information, e.g.
1667
    /store info -h
1668
    """
1669

    
1670
    @errors.generic.all
1671
    @errors.pithos.connection
1672
    @errors.pithos.container
1673
    @errors.pithos.object_path
1674
    def _run(self):
1675
        versions = self.client.get_object_versionlist(self.path)
1676
        print_items([dict(
1677
            id=vitem[0],
1678
            created=strftime('%d-%m-%Y %H:%M:%S', localtime(float(vitem[1])))
1679
            ) for vitem in versions])
1680

    
1681
    def main(self, container___path):
1682
        super(store_versions, self)._run(
1683
            container___path,
1684
            path_is_optional=False)
1685
        self._run()