Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (59.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, makedirs
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
    If local destination is a directory:
992
    *   download <container>:<path> <local dir> -r
993
    will download all files on <container> prefixed as <path>,
994
    to <local dir>/<full path>
995
    *   download <container>:<path> <local dir> --exact-match
996
    will download only one file, exactly matching <path>
997
    ATTENTION: to download cont:dir1/dir2/file there must exist objects
998
    cont:dir1 and cont:dir1/dir2 of type application/directory
999
    To create directory objects, use /store mkdir
1000
    """
1001

    
1002
    arguments = dict(
1003
        resume=FlagArgument('Resume instead of overwrite', '--resume'),
1004
        range=RangeArgument('show range of data', '--range'),
1005
        if_match=ValueArgument('show output if ETags match', '--if-match'),
1006
        if_none_match=ValueArgument(
1007
            'show output if ETags match',
1008
            '--if-none-match'),
1009
        if_modified_since=DateArgument(
1010
            'show output modified since then',
1011
            '--if-modified-since'),
1012
        if_unmodified_since=DateArgument(
1013
            'show output unmodified since then',
1014
            '--if-unmodified-since'),
1015
        object_version=ValueArgument(
1016
            'get the specific version',
1017
            '--object-version'),
1018
        poolsize=IntArgument('set pool size', '--with-pool-size'),
1019
        progress_bar=ProgressBarArgument(
1020
            'do not show progress bar',
1021
            '--no-progress-bar',
1022
            default=False),
1023
        recursive=FlagArgument(
1024
            'Download a remote directory and all its contents',
1025
            '-r, --resursive')
1026
    )
1027

    
1028
    def _is_dir(self, remote_dict):
1029
        return 'application/directory' == remote_dict.get('content_type', '')
1030

    
1031
    def _outputs(self, local_path):
1032
        if local_path is None:
1033
            return [(None, self.path)]
1034
        outpath = path.abspath(local_path)
1035
        if not (path.exists(outpath) or path.isdir(outpath)):
1036
            return [(outpath, self.path)]
1037
        elif self['recursive']:
1038
            remotes = self.client.container_get(
1039
                prefix=self.path,
1040
                if_modified_since=self['if_modified_since'],
1041
                if_unmodified_since=self['if_unmodified_since'])
1042
            return [(
1043
                '%s/%s' % (outpath, remote['name']),
1044
                    None if self._is_dir(remote) else remote['name'])\
1045
                for remote in remotes.json]
1046
        raiseCLIError('Illegal destination location %s' % local_path)
1047

    
1048
    @errors.generic.all
1049
    @errors.pithos.connection
1050
    @errors.pithos.container
1051
    @errors.pithos.object_path
1052
    @errors.pithos.local_path
1053
    def _run(self, local_path):
1054
        outputs = self._outputs(local_path)
1055
        poolsize = self['poolsize']
1056
        if poolsize:
1057
            self.client.POOL_SIZE = int(poolsize)
1058
        if not outputs:
1059
            raiseCLIError('No objects prefixed as %s on container %s' % (
1060
                self.path,
1061
                self.container))
1062
        wmode = 'rwb+' if self['resume'] else 'wb+'
1063
        progress_bar = None
1064
        try:
1065
            for lpath, rpath in sorted(outputs):
1066
                if not rpath:
1067
                    if not path.isdir(lpath):
1068
                        print('Create directory %s' % lpath)
1069
                        makedirs(lpath)
1070
                    continue
1071
                print('\nFrom %s:%s to %s' % (
1072
                    self.container,
1073
                    rpath,
1074
                    lpath))
1075
                (progress_bar,
1076
                    download_cb) = self._safe_progress_bar('Downloading')
1077
                self.client.download_object(
1078
                    rpath,
1079
                    open(lpath, wmode) if lpath else stdout,
1080
                    download_cb=download_cb,
1081
                    range=self['range'],
1082
                    version=self['object_version'],
1083
                    if_match=self['if_match'],
1084
                    resume=self['resume'],
1085
                    if_none_match=self['if_none_match'],
1086
                    if_modified_since=self['if_modified_since'],
1087
                    if_unmodified_since=self['if_unmodified_since'])
1088
        except KeyboardInterrupt:
1089
            from threading import enumerate as activethreads
1090
            stdout.write('\nFinishing active threads ')
1091
            for thread in activethreads():
1092
                stdout.flush()
1093
                try:
1094
                    thread.join()
1095
                    stdout.write('.')
1096
                except RuntimeError:
1097
                    continue
1098
            print('\ndownload canceled by user')
1099
            if local_path is not None:
1100
                print('to resume, re-run with --resume')
1101
        except Exception:
1102
            self._safe_progress_bar_finish(progress_bar)
1103
            raise
1104
        finally:
1105
            self._safe_progress_bar_finish(progress_bar)
1106

    
1107
    def main(self, container___path, local_path=None):
1108
        super(self.__class__, self)._run(
1109
            container___path,
1110
            path_is_optional=False)
1111
        self._run(local_path=local_path)
1112

    
1113

    
1114
@command(pithos_cmds)
1115
class store_hashmap(_store_container_command):
1116
    """Get the hash-map of an object"""
1117

    
1118
    arguments = dict(
1119
        if_match=ValueArgument('show output if ETags match', '--if-match'),
1120
        if_none_match=ValueArgument(
1121
            'show output if ETags match',
1122
            '--if-none-match'),
1123
        if_modified_since=DateArgument(
1124
            'show output modified since then',
1125
            '--if-modified-since'),
1126
        if_unmodified_since=DateArgument(
1127
            'show output unmodified since then',
1128
            '--if-unmodified-since'),
1129
        object_version=ValueArgument(
1130
            'get the specific version',
1131
            '--object-version')
1132
    )
1133

    
1134
    @errors.generic.all
1135
    @errors.pithos.connection
1136
    @errors.pithos.container
1137
    @errors.pithos.object_path
1138
    def _run(self):
1139
        data = self.client.get_object_hashmap(
1140
            self.path,
1141
            version=self['object_version'],
1142
            if_match=self['if_match'],
1143
            if_none_match=self['if_none_match'],
1144
            if_modified_since=self['if_modified_since'],
1145
            if_unmodified_since=self['if_unmodified_since'])
1146
        print_dict(data)
1147

    
1148
    def main(self, container___path):
1149
        super(self.__class__, self)._run(
1150
            container___path,
1151
            path_is_optional=False)
1152
        self._run()
1153

    
1154

    
1155
@command(pithos_cmds)
1156
class store_delete(_store_container_command):
1157
    """Delete a container [or an object]
1158
    How to delete a non-empty container:
1159
    - empty the container:  /store delete -r <container>
1160
    - delete it:            /store delete <container>
1161
    .
1162
    Semantics of directory deletion:
1163
    .a preserve the contents: /store delete <container>:<directory>
1164
    .    objects of the form dir/filename can exist with a dir object
1165
    .b delete contents:       /store delete -r <container>:<directory>
1166
    .    all dir/* objects are affected, even if dir does not exist
1167
    .
1168
    To restore a deleted object OBJ in a container CONT:
1169
    - get object versions: /store versions CONT:OBJ
1170
    .   and choose the version to be restored
1171
    - restore the object:  /store copy --source-version=<version> CONT:OBJ OBJ
1172
    """
1173

    
1174
    arguments = dict(
1175
        until=DateArgument('remove history until that date', '--until'),
1176
        yes=FlagArgument('Do not prompt for permission', '--yes'),
1177
        recursive=FlagArgument(
1178
            'empty dir or container and delete (if dir)',
1179
            ('-r', '--recursive'))
1180
    )
1181

    
1182
    def __init__(self, arguments={}):
1183
        super(self.__class__, self).__init__(arguments)
1184
        self['delimiter'] = DelimiterArgument(
1185
            self,
1186
            parsed_name='--delimiter',
1187
            help='delete objects prefixed with <object><delimiter>')
1188

    
1189
    @errors.generic.all
1190
    @errors.pithos.connection
1191
    @errors.pithos.container
1192
    @errors.pithos.object_path
1193
    def _run(self):
1194
        if self.path:
1195
            if self['yes'] or ask_user(
1196
                'Delete %s:%s ?' % (self.container, self.path)):
1197
                self.client.del_object(
1198
                    self.path,
1199
                    until=self['until'],
1200
                    delimiter=self['delimiter'])
1201
            else:
1202
                print('Aborted')
1203
        else:
1204
            ask_msg = 'Delete contents of container'\
1205
            if self['recursive'] else 'Delete container'
1206
            if self['yes'] or ask_user('%s %s ?' % (ask_msg, self.container)):
1207
                self.client.del_container(
1208
                    until=self['until'],
1209
                    delimiter=self['delimiter'])
1210
            else:
1211
                print('Aborted')
1212

    
1213
    def main(self, container____path__):
1214
        super(self.__class__, self)._run(container____path__)
1215
        self._run()
1216

    
1217

    
1218
@command(pithos_cmds)
1219
class store_purge(_store_container_command):
1220
    """Delete a container and release related data blocks
1221
    Non-empty containers can not purged.
1222
    To purge a container with content:
1223
    .   /store delete -r <container>
1224
    .      objects are deleted, but data blocks remain on server
1225
    .   /store purge <container>
1226
    .      container and data blocks are released and deleted
1227
    """
1228

    
1229
    arguments = dict(
1230
        yes=FlagArgument('Do not prompt for permission', '--yes'),
1231
    )
1232

    
1233
    @errors.generic.all
1234
    @errors.pithos.connection
1235
    @errors.pithos.container
1236
    def _run(self):
1237
        if self['yes'] or ask_user('Purge container %s?' % self.container):
1238
                self.client.purge_container()
1239
        else:
1240
            print('Aborted')
1241

    
1242
    def main(self, container):
1243
        super(self.__class__, self)._run(container)
1244
        self._run()
1245

    
1246

    
1247
@command(pithos_cmds)
1248
class store_publish(_store_container_command):
1249
    """Publish the object and print the public url"""
1250

    
1251
    @errors.generic.all
1252
    @errors.pithos.connection
1253
    @errors.pithos.container
1254
    @errors.pithos.object_path
1255
    def _run(self):
1256
        url = self.client.publish_object(self.path)
1257
        print(url)
1258

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

    
1265

    
1266
@command(pithos_cmds)
1267
class store_unpublish(_store_container_command):
1268
    """Unpublish an object"""
1269

    
1270
    @errors.generic.all
1271
    @errors.pithos.connection
1272
    @errors.pithos.container
1273
    @errors.pithos.object_path
1274
    def _run(self):
1275
            self.client.unpublish_object(self.path)
1276

    
1277
    def main(self, container___path):
1278
        super(self.__class__, self)._run(
1279
            container___path,
1280
            path_is_optional=False)
1281
        self._run()
1282

    
1283

    
1284
@command(pithos_cmds)
1285
class store_permissions(_store_container_command):
1286
    """Get read and write permissions of an object
1287
    Permissions are lists of users and user groups. There is read and write
1288
    permissions. Users and groups with write permission have also read
1289
    permission.
1290
    """
1291

    
1292
    @errors.generic.all
1293
    @errors.pithos.connection
1294
    @errors.pithos.container
1295
    @errors.pithos.object_path
1296
    def _run(self):
1297
        r = self.client.get_object_sharing(self.path)
1298
        print_dict(r)
1299

    
1300
    def main(self, container___path):
1301
        super(self.__class__, self)._run(
1302
            container___path,
1303
            path_is_optional=False)
1304
        self._run()
1305

    
1306

    
1307
@command(pithos_cmds)
1308
class store_setpermissions(_store_container_command):
1309
    """Set permissions for an object
1310
    New permissions overwrite existing permissions.
1311
    Permission format:
1312
    -   read=<username>[,usergroup[,...]]
1313
    -   write=<username>[,usegroup[,...]]
1314
    E.g. to give read permissions for file F to users A and B and write for C:
1315
    .       /store setpermissions F read=A,B write=C
1316
    """
1317

    
1318
    @errors.generic.all
1319
    def format_permition_dict(self, permissions):
1320
        read = False
1321
        write = False
1322
        for perms in permissions:
1323
            splstr = perms.split('=')
1324
            if 'read' == splstr[0]:
1325
                read = [user_or_group.strip() \
1326
                for user_or_group in splstr[1].split(',')]
1327
            elif 'write' == splstr[0]:
1328
                write = [user_or_group.strip() \
1329
                for user_or_group in splstr[1].split(',')]
1330
            else:
1331
                read = False
1332
                write = False
1333
        if not read and not write:
1334
            raiseCLIError(None,
1335
            'Usage:\tread=<groups,users> write=<groups,users>')
1336
        return (read, write)
1337

    
1338
    @errors.generic.all
1339
    @errors.pithos.connection
1340
    @errors.pithos.container
1341
    @errors.pithos.object_path
1342
    def _run(self, read, write):
1343
        self.client.set_object_sharing(
1344
            self.path,
1345
            read_permition=read,
1346
            write_permition=write)
1347

    
1348
    def main(self, container___path, *permissions):
1349
        super(self.__class__, self)._run(
1350
            container___path,
1351
            path_is_optional=False)
1352
        (read, write) = self.format_permition_dict(permissions)
1353
        self._run(read, write)
1354

    
1355

    
1356
@command(pithos_cmds)
1357
class store_delpermissions(_store_container_command):
1358
    """Delete all permissions set on object
1359
    To modify permissions, use /store setpermssions
1360
    """
1361

    
1362
    @errors.generic.all
1363
    @errors.pithos.connection
1364
    @errors.pithos.container
1365
    @errors.pithos.object_path
1366
    def _run(self):
1367
        self.client.del_object_sharing(self.path)
1368

    
1369
    def main(self, container___path):
1370
        super(self.__class__, self)._run(
1371
            container___path,
1372
            path_is_optional=False)
1373
        self._run()
1374

    
1375

    
1376
@command(pithos_cmds)
1377
class store_info(_store_container_command):
1378
    """Get detailed information for user account, containers or objects
1379
    to get account info:    /store info
1380
    to get container info:  /store info <container>
1381
    to get object info:     /store info <container>:<path>
1382
    """
1383

    
1384
    arguments = dict(
1385
        object_version=ValueArgument(
1386
            'show specific version \ (applies only for objects)',
1387
            '--object-version')
1388
    )
1389

    
1390
    @errors.generic.all
1391
    @errors.pithos.connection
1392
    @errors.pithos.container
1393
    @errors.pithos.object_path
1394
    def _run(self):
1395
        if self.container is None:
1396
            r = self.client.get_account_info()
1397
        elif self.path is None:
1398
            r = self.client.get_container_info(self.container)
1399
        else:
1400
            r = self.client.get_object_info(
1401
                self.path,
1402
                version=self['object_version'])
1403
        print_dict(r)
1404

    
1405
    def main(self, container____path__=None):
1406
        super(self.__class__, self)._run(container____path__)
1407
        self._run()
1408

    
1409

    
1410
@command(pithos_cmds)
1411
class store_meta(_store_container_command):
1412
    """Get metadata for account, containers or objects"""
1413

    
1414
    arguments = dict(
1415
        detail=FlagArgument('show detailed output', '-l'),
1416
        until=DateArgument('show metadata until then', '--until'),
1417
        object_version=ValueArgument(
1418
            'show specific version \ (applies only for objects)',
1419
            '--object-version')
1420
    )
1421

    
1422
    @errors.generic.all
1423
    @errors.pithos.connection
1424
    @errors.pithos.container
1425
    @errors.pithos.object_path
1426
    def _run(self):
1427
        until = self['until']
1428
        if self.container is None:
1429
            if self['detail']:
1430
                r = self.client.get_account_info(until=until)
1431
            else:
1432
                r = self.client.get_account_meta(until=until)
1433
                r = pretty_keys(r, '-')
1434
            if r:
1435
                print(bold(self.client.account))
1436
        elif self.path is None:
1437
            if self['detail']:
1438
                r = self.client.get_container_info(until=until)
1439
            else:
1440
                cmeta = self.client.get_container_meta(until=until)
1441
                ometa = self.client.get_container_object_meta(until=until)
1442
                r = {}
1443
                if cmeta:
1444
                    r['container-meta'] = pretty_keys(cmeta, '-')
1445
                if ometa:
1446
                    r['object-meta'] = pretty_keys(ometa, '-')
1447
        else:
1448
            if self['detail']:
1449
                r = self.client.get_object_info(self.path,
1450
                    version=self['object_version'])
1451
            else:
1452
                r = self.client.get_object_meta(self.path,
1453
                    version=self['object_version'])
1454
            if r:
1455
                r = pretty_keys(pretty_keys(r, '-'))
1456
        if r:
1457
            print_dict(r)
1458

    
1459
    def main(self, container____path__=None):
1460
        super(self.__class__, self)._run(container____path__)
1461
        self._run()
1462

    
1463

    
1464
@command(pithos_cmds)
1465
class store_setmeta(_store_container_command):
1466
    """Set a piece of metadata for account, container or object
1467
    Metadata are formed as key:value pairs
1468
    """
1469

    
1470
    @errors.generic.all
1471
    @errors.pithos.connection
1472
    @errors.pithos.container
1473
    @errors.pithos.object_path
1474
    def _run(self, metakey, metaval):
1475
        if not self.container:
1476
            self.client.set_account_meta({metakey: metaval})
1477
        elif not self.path:
1478
            self.client.set_container_meta({metakey: metaval})
1479
        else:
1480
            self.client.set_object_meta(self.path, {metakey: metaval})
1481

    
1482
    def main(self, metakey, metaval, container____path__=None):
1483
        super(self.__class__, self)._run(container____path__)
1484
        self._run(metakey=metakey, metaval=metaval)
1485

    
1486

    
1487
@command(pithos_cmds)
1488
class store_delmeta(_store_container_command):
1489
    """Delete metadata with given key from account, container or object
1490
    Metadata are formed as key:value objects
1491
    - to get metadata of current account:     /store meta
1492
    - to get metadata of a container:         /store meta <container>
1493
    - to get metadata of an object:           /store meta <container>:<path>
1494
    """
1495

    
1496
    @errors.generic.all
1497
    @errors.pithos.connection
1498
    @errors.pithos.container
1499
    @errors.pithos.object_path
1500
    def _run(self, metakey):
1501
        if self.container is None:
1502
            self.client.del_account_meta(metakey)
1503
        elif self.path is None:
1504
            self.client.del_container_meta(metakey)
1505
        else:
1506
            self.client.del_object_meta(self.path, metakey)
1507

    
1508
    def main(self, metakey, container____path__=None):
1509
        super(self.__class__, self)._run(container____path__)
1510
        self._run(metakey)
1511

    
1512

    
1513
@command(pithos_cmds)
1514
class store_quota(_store_account_command):
1515
    """Get quota for account or container"""
1516

    
1517
    arguments = dict(
1518
        in_bytes=FlagArgument('Show result in bytes', ('-b', '--bytes'))
1519
        )
1520

    
1521
    @errors.generic.all
1522
    @errors.pithos.connection
1523
    @errors.pithos.container
1524
    def _run(self):
1525
        if self.container is None:
1526
            reply = self.client.get_account_quota()
1527
        else:
1528
            reply = self.client.get_container_quota(self.container)
1529
        if not self['in_bytes']:
1530
            for k in reply:
1531
                reply[k] = format_size(reply[k])
1532
        print_dict(pretty_keys(reply, '-'))
1533

    
1534
    def main(self, container=None):
1535
        super(self.__class__, self)._run()
1536
        self.container = container
1537
        self._run()
1538

    
1539

    
1540
@command(pithos_cmds)
1541
class store_setquota(_store_account_command):
1542
    """Set new quota for account or container
1543
    By default, quota is set in bytes
1544
    Users may specify a different unit, e.g:
1545
    /store setquota 2.3GB mycontainer
1546
    Accepted units: B, KiB (1024 B), KB (1000 B), MiB, MB, GiB, GB, TiB, TB
1547
    """
1548

    
1549
    @errors.generic.all
1550
    def _calculate_quota(self, user_input):
1551
        quota = 0
1552
        try:
1553
            quota = int(user_input)
1554
        except ValueError:
1555
            index = 0
1556
            digits = [str(num) for num in range(0, 10)] + ['.']
1557
            while user_input[index] in digits:
1558
                index += 1
1559
            quota = user_input[:index]
1560
            format = user_input[index:]
1561
            try:
1562
                return to_bytes(quota, format)
1563
            except Exception as qe:
1564
                raiseCLIError(qe,
1565
                    'Failed to convert %s to bytes' % user_input,
1566
                    details=[
1567
                        'Syntax: setquota <quota>[format] [container]',
1568
                        'e.g.: setquota 2.3GB mycontainer',
1569
                        'Acceptable formats:',
1570
                        '(*1024): B, KiB, MiB, GiB, TiB',
1571
                        '(*1000): B, KB, MB, GB, TB'])
1572
        return quota
1573

    
1574
    @errors.generic.all
1575
    @errors.pithos.connection
1576
    @errors.pithos.container
1577
    def _run(self, quota):
1578
        if self.container:
1579
            self.client.container = self.container
1580
            self.client.set_container_quota(quota)
1581
        else:
1582
            self.client.set_account_quota(quota)
1583

    
1584
    def main(self, quota, container=None):
1585
        super(self.__class__, self)._run()
1586
        quota = self._calculate_quota(quota)
1587
        self.container = container
1588
        self._run(quota)
1589

    
1590

    
1591
@command(pithos_cmds)
1592
class store_versioning(_store_account_command):
1593
    """Get  versioning for account or container"""
1594

    
1595
    @errors.generic.all
1596
    @errors.pithos.connection
1597
    @errors.pithos.container
1598
    def _run(self):
1599
        if self.container is None:
1600
            r = self.client.get_account_versioning()
1601
        else:
1602
            r = self.client.get_container_versioning(self.container)
1603
        print_dict(r)
1604

    
1605
    def main(self, container=None):
1606
        super(self.__class__, self)._run()
1607
        self.container = container
1608
        self._run()
1609

    
1610

    
1611
@command(pithos_cmds)
1612
class store_setversioning(_store_account_command):
1613
    """Set versioning mode (auto, none) for account or container"""
1614

    
1615
    def _check_versioning(self, versioning):
1616
        if versioning and versioning.lower() in ('auto', 'none'):
1617
            return versioning.lower()
1618
        raiseCLIError('Invalid versioning %s' % versioning, details=[
1619
            'Versioning can be auto or none'])
1620

    
1621
    @errors.generic.all
1622
    @errors.pithos.connection
1623
    @errors.pithos.container
1624
    def _run(self, versioning):
1625
        if self.container is None:
1626
            self.client.set_account_versioning(versioning)
1627
        else:
1628
            self.client.container = self.container
1629
            self.client.set_container_versioning(versioning)
1630

    
1631
    def main(self, versioning, container=None):
1632
        super(self.__class__, self)._run()
1633
        self._run(self._check_versioning(versioning))
1634

    
1635

    
1636
@command(pithos_cmds)
1637
class store_group(_store_account_command):
1638
    """Get groups and group members"""
1639

    
1640
    @errors.generic.all
1641
    @errors.pithos.connection
1642
    def _run(self):
1643
        r = self.client.get_account_group()
1644
        print_dict(pretty_keys(r, '-'))
1645

    
1646
    def main(self):
1647
        super(self.__class__, self)._run()
1648
        self._run()
1649

    
1650

    
1651
@command(pithos_cmds)
1652
class store_setgroup(_store_account_command):
1653
    """Set a user group"""
1654

    
1655
    @errors.generic.all
1656
    @errors.pithos.connection
1657
    def _run(self, groupname, *users):
1658
        self.client.set_account_group(groupname, users)
1659

    
1660
    def main(self, groupname, *users):
1661
        super(self.__class__, self)._run()
1662
        if users:
1663
            self._run(groupname, *users)
1664
        else:
1665
            raiseCLIError('No users to add in group %s' % groupname)
1666

    
1667

    
1668
@command(pithos_cmds)
1669
class store_delgroup(_store_account_command):
1670
    """Delete a user group"""
1671

    
1672
    @errors.generic.all
1673
    @errors.pithos.connection
1674
    def _run(self, groupname):
1675
        self.client.del_account_group(groupname)
1676

    
1677
    def main(self, groupname):
1678
        super(self.__class__, self)._run()
1679
        self._run(groupname)
1680

    
1681

    
1682
@command(pithos_cmds)
1683
class store_sharers(_store_account_command):
1684
    """List the accounts that share objects with current user"""
1685

    
1686
    arguments = dict(
1687
        detail=FlagArgument('show detailed output', '-l'),
1688
        marker=ValueArgument('show output greater then marker', '--marker')
1689
    )
1690

    
1691
    @errors.generic.all
1692
    @errors.pithos.connection
1693
    def _run(self):
1694
        accounts = self.client.get_sharing_accounts(marker=self['marker'])
1695
        print_items(accounts if self['detail']\
1696
        else [acc['name'] for acc in accounts])
1697

    
1698
    def main(self):
1699
        super(self.__class__, self)._run()
1700
        self._run()
1701

    
1702

    
1703
@command(pithos_cmds)
1704
class store_versions(_store_container_command):
1705
    """Get the list of object versions
1706
    Deleted objects may still have versions that can be used to restore it and
1707
    get information about its previous state.
1708
    The version number can be used in a number of other commands, like info,
1709
    copy, move, meta. See these commands for more information, e.g.
1710
    /store info -h
1711
    """
1712

    
1713
    @errors.generic.all
1714
    @errors.pithos.connection
1715
    @errors.pithos.container
1716
    @errors.pithos.object_path
1717
    def _run(self):
1718
        versions = self.client.get_object_versionlist(self.path)
1719
        print_items([dict(
1720
            id=vitem[0],
1721
            created=strftime('%d-%m-%Y %H:%M:%S', localtime(float(vitem[1])))
1722
            ) for vitem in versions])
1723

    
1724
    def main(self, container___path):
1725
        super(store_versions, self)._run(
1726
            container___path,
1727
            path_is_optional=False)
1728
        self._run()