Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (60.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
from kamaki.clients.astakos import AstakosClient
57

    
58

    
59
kloger = getLogger('kamaki')
60

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

    
64

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

    
72

    
73
# Argument functionality
74

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

    
93

    
94
class DelimiterArgument(ValueArgument):
95
    """
96
    :value type: string
97
    :value returns: given string or /
98
    """
99

    
100
    def __init__(self, caller_obj, help='', parsed_name=None, default=None):
101
        super(DelimiterArgument, self).__init__(help, parsed_name, default)
102
        self.caller_obj = caller_obj
103

    
104
    @property
105
    def value(self):
106
        if self.caller_obj['recursive']:
107
            return '/'
108
        return getattr(self, '_value', self.default)
109

    
110
    @value.setter
111
    def value(self, newvalue):
112
        self._value = newvalue
113

    
114

    
115
class SharingArgument(ValueArgument):
116
    """Set sharing (read and/or write) groups
117
    .
118
    :value type: "read=term1,term2,... write=term1,term2,..."
119
    .
120
    :value returns: {'read':['term1', 'term2', ...],
121
    .   'write':['term1', 'term2', ...]}
122
    """
123

    
124
    @property
125
    def value(self):
126
        return getattr(self, '_value', self.default)
127

    
128
    @value.setter
129
    def value(self, newvalue):
130
        perms = {}
131
        try:
132
            permlist = newvalue.split(' ')
133
        except AttributeError:
134
            return
135
        for p in permlist:
136
            try:
137
                (key, val) = p.split('=')
138
            except ValueError as err:
139
                raiseCLIError(
140
                    err,
141
                    'Error in --sharing',
142
                    details='Incorrect format',
143
                    importance=1)
144
            if key.lower() not in ('read', 'write'):
145
                msg = 'Error in --sharing'
146
                raiseCLIError(err, msg, importance=1, details=[
147
                    'Invalid permission key %s' % key])
148
            val_list = val.split(',')
149
            if not key in perms:
150
                perms[key] = []
151
            for item in val_list:
152
                if item not in perms[key]:
153
                    perms[key].append(item)
154
        self._value = perms
155

    
156

    
157
class RangeArgument(ValueArgument):
158
    """
159
    :value type: string of the form <start>-<end> where <start> and <end> are
160
        integers
161
    :value returns: the input string, after type checking <start> and <end>
162
    """
163

    
164
    @property
165
    def value(self):
166
        return getattr(self, '_value', self.default)
167

    
168
    @value.setter
169
    def value(self, newvalue):
170
        if newvalue is None:
171
            self._value = self.default
172
            return
173
        (start, end) = newvalue.split('-')
174
        (start, end) = (int(start), int(end))
175
        self._value = '%s-%s' % (start, end)
176

    
177
# Command specs
178

    
179

    
180
class _pithos_init(_command_init):
181
    """Initialize a pithos+ kamaki client"""
182

    
183
    @errors.generic.all
184
    def _run(self):
185
        self.token = self.config.get('store', 'token')\
186
            or self.config.get('global', 'token')
187
        self.base_url = self.config.get('store', 'url')\
188
            or self.config.get('global', 'url')
189
        self._set_account()
190
        self.container = self.config.get('store', 'container')\
191
            or self.config.get('global', 'container')
192
        self.client = PithosClient(
193
            base_url=self.base_url,
194
            token=self.token,
195
            account=self.account,
196
            container=self.container)
197

    
198
    def main(self):
199
        self._run()
200

    
201
    def _set_account(self):
202
        astakos = AstakosClient(self.config.get('astakos', 'url'), self.token)
203
        self.account = self['account'] or astakos.term('uuid')
204

    
205
        """Backwards compatibility"""
206
        self.account = self.account\
207
            or self.config.get('store', 'account')\
208
            or self.config.get('global', 'account')
209

    
210

    
211
class _store_account_command(_pithos_init):
212
    """Base class for account level storage commands"""
213

    
214
    def __init__(self, arguments={}):
215
        super(_store_account_command, self).__init__(arguments)
216
        self['account'] = ValueArgument(
217
            'Set user account (not permanent)',
218
            '--account')
219

    
220
    def _run(self):
221
        super(_store_account_command, self)._run()
222
        if self['account']:
223
            self.client.account = self['account']
224

    
225
    @errors.generic.all
226
    def main(self):
227
        self._run()
228

    
229

    
230
class _store_container_command(_store_account_command):
231
    """Base class for container level storage commands"""
232

    
233
    container = None
234
    path = None
235

    
236
    def __init__(self, arguments={}):
237
        super(_store_container_command, self).__init__(arguments)
238
        self['container'] = ValueArgument(
239
            'Set container to work with (temporary)',
240
            '--container')
241

    
242
    @errors.generic.all
243
    def _dest_container_path(self, dest_container_path):
244
        if self['destination_container']:
245
            return (self['destination_container'], dest_container_path)
246
        dst = dest_container_path.split(':')
247
        return (dst[0], dst[1]) if len(dst) > 1 else (None, dst[0])
248

    
249
    def extract_container_and_path(
250
            self,
251
            container_with_path,
252
            path_is_optional=True):
253
        """Contains all heuristics for deciding what should be used as
254
        container or path. Options are:
255
        * user string of the form container:path
256
        * self.container, self.path variables set by super constructor, or
257
        explicitly by the caller application
258
        Error handling is explicit as these error cases happen only here
259
        """
260
        try:
261
            assert isinstance(container_with_path, str)
262
        except AssertionError as err:
263
            if self['container'] and path_is_optional:
264
                self.container = self['container']
265
                self.client.container = self['container']
266
                return
267
            raiseCLIError(err)
268

    
269
        user_cont, sep, userpath = container_with_path.partition(':')
270

    
271
        if sep:
272
            if not user_cont:
273
                raiseCLIError(CLISyntaxError(
274
                    'Container is missing\n',
275
                    details=errors.pithos.container_howto))
276
            alt_cont = self['container']
277
            if alt_cont and user_cont != alt_cont:
278
                raiseCLIError(CLISyntaxError(
279
                    'Conflict: 2 containers (%s, %s)' % (user_cont, alt_cont),
280
                    details=errors.pithos.container_howto)
281
                )
282
            self.container = user_cont
283
            if not userpath:
284
                raiseCLIError(CLISyntaxError(
285
                    'Path is missing for object in container %s' % user_cont,
286
                    details=errors.pithos.container_howto)
287
                )
288
            self.path = userpath
289
        else:
290
            alt_cont = self['container'] or self.client.container
291
            if alt_cont:
292
                self.container = alt_cont
293
                self.path = user_cont
294
            elif path_is_optional:
295
                self.container = user_cont
296
                self.path = None
297
            else:
298
                self.container = user_cont
299
                raiseCLIError(CLISyntaxError(
300
                    'Both container and path are required',
301
                    details=errors.pithos.container_howto)
302
                )
303

    
304
    @errors.generic.all
305
    def _run(self, container_with_path=None, path_is_optional=True):
306
        super(_store_container_command, self)._run()
307
        if self['container']:
308
            self.client.container = self['container']
309
            if container_with_path:
310
                self.path = container_with_path
311
            elif not path_is_optional:
312
                raise CLISyntaxError(
313
                    'Both container and path are required',
314
                    details=errors.pithos.container_howto)
315
        elif container_with_path:
316
            self.extract_container_and_path(
317
                container_with_path,
318
                path_is_optional)
319
            self.client.container = self.container
320
        self.container = self.client.container
321

    
322
    def main(self, container_with_path=None, path_is_optional=True):
323
        self._run(container_with_path, path_is_optional)
324

    
325

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

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

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

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

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

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

    
455

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

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

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

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

    
474

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

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

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

    
494
    def main(self, container___path):
495
        super(store_touch, self)._run(
496
            container___path,
497
            path_is_optional=False)
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(
520
            quota=self['quota'],
521
            versioning=self['versioning'],
522
            metadata=self['meta'])
523

    
524
    def main(self, container=None):
525
        super(self.__class__, self)._run(container)
526
        if container and self.container != container:
527
            raiseCLIError('Invalid container name %s' % container, details=[
528
                'Did you mean "%s" ?' % self.container,
529
                'Use --container for names containing :'])
530
        self._run()
531

    
532

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

    
555
    arguments = dict(
556
        destination_container=ValueArgument(
557
            'use it if destination container name contains a : character',
558
            '--dst-container'),
559
        source_version=ValueArgument(
560
            'copy specific version',
561
            '--source-version'),
562
        public=ValueArgument('make object publicly accessible', '--public'),
563
        content_type=ValueArgument(
564
            'change object\'s content type',
565
            '--content-type'),
566
        recursive=FlagArgument(
567
            'mass copy with delimiter /',
568
            ('-r', '--recursive')),
569
        exact_match=FlagArgument(
570
            'Copy only the object that fully matches path',
571
            '--exact-match'),
572
        replace=FlagArgument('Replace src. path with dst. path', '--replace')
573
    )
574

    
575
    def _objlist(self, dst_path):
576
        if self['exact_match']:
577
            return [(dst_path or self.path, self.path)]
578
        r = self.client.container_get(prefix=self.path)
579
        if len(r.json) == 1:
580
            obj = r.json[0]
581
            return [(obj['name'], dst_path or obj['name'])]
582
        start = len(self.path) if self['replace'] else 0
583
        return [(obj['name'], '%s%s' % (
584
            dst_path,
585
            obj['name'][start:])) for obj in r.json]
586

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

    
607
    def main(
608
            self,
609
            source_container___path,
610
            destination_container___path=None):
611
        super(self.__class__, self)._run(
612
            source_container___path,
613
            path_is_optional=False)
614
        (dst_cont, dst_path) = self._dest_container_path(
615
            destination_container___path)
616
        self._run(dst_cont=dst_cont, dst_path=dst_path or '')
617

    
618

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

    
641
    arguments = dict(
642
        destination_container=ValueArgument(
643
            'use it if destination container name contains a : character',
644
            '--dst-container'),
645
        source_version=ValueArgument('specify version', '--source-version'),
646
        public=FlagArgument('make object publicly accessible', '--public'),
647
        content_type=ValueArgument('modify content type', '--content-type'),
648
        recursive=FlagArgument('up to delimiter /', ('-r', '--recursive')),
649
        exact_match=FlagArgument(
650
            'Copy only the object that fully matches path',
651
            '--exact-match'),
652
        replace=FlagArgument('Replace src. path with dst. path', '--replace')
653
    )
654

    
655
    def _objlist(self, dst_path):
656
        if self['exact_match']:
657
            return [(dst_path or self.path, self.path)]
658
        r = self.client.container_get(prefix=self.path)
659
        if len(r.json) == 1:
660
            obj = r.json[0]
661
            return [(obj['name'], dst_path or obj['name'])]
662
        return [(
663
            obj['name'],
664
            '%s%s' % (
665
                dst_path,
666
                obj['name'][len(self.path) if self['replace'] else 0:]
667
            )) for obj in r.json]
668

    
669
    @errors.generic.all
670
    @errors.pithos.connection
671
    @errors.pithos.container
672
    def _run(self, dst_cont, dst_path):
673
        no_source_object = True
674
        for src_object, dst_object in self._objlist(dst_path):
675
            no_source_object = False
676
            self.client.move_object(
677
                src_container=self.container,
678
                src_object=src_object,
679
                dst_container=dst_cont or self.container,
680
                dst_object=dst_object,
681
                source_version=self['source_version'],
682
                public=self['public'],
683
                content_type=self['content_type'])
684
        if no_source_object:
685
            raiseCLIError('No object %s in container %s' % (
686
                self.path,
687
                self.container))
688

    
689
    def main(
690
            self,
691
            source_container___path,
692
            destination_container___path=None):
693
        super(self.__class__, self)._run(
694
            source_container___path,
695
            path_is_optional=False)
696
        (dst_cont, dst_path) = self._dest_container_path(
697
            destination_container___path)
698
        self._run(dst_cont=dst_cont, dst_path=dst_path or '')
699

    
700

    
701
@command(pithos_cmds)
702
class store_append(_store_container_command):
703
    """Append local file to (existing) remote object
704
    The remote object should exist.
705
    If the remote object is a directory, it is transformed into a file.
706
    In the later case, objects under the directory remain intact.
707
    """
708

    
709
    arguments = dict(
710
        progress_bar=ProgressBarArgument(
711
            'do not show progress bar',
712
            '--no-progress-bar',
713
            default=False)
714
    )
715

    
716
    @errors.generic.all
717
    @errors.pithos.connection
718
    @errors.pithos.container
719
    @errors.pithos.object_path
720
    def _run(self, local_path):
721
        (progress_bar, upload_cb) = self._safe_progress_bar('Appending')
722
        try:
723
            f = open(local_path, 'rb')
724
            self.client.append_object(self.path, f, upload_cb)
725
        except Exception:
726
            self._safe_progress_bar_finish(progress_bar)
727
            raise
728
        finally:
729
            self._safe_progress_bar_finish(progress_bar)
730

    
731
    def main(self, local_path, container___path):
732
        super(self.__class__, self)._run(
733
            container___path,
734
            path_is_optional=False)
735
        self._run(local_path)
736

    
737

    
738
@command(pithos_cmds)
739
class store_truncate(_store_container_command):
740
    """Truncate remote file up to a size (default is 0)"""
741

    
742
    @errors.generic.all
743
    @errors.pithos.connection
744
    @errors.pithos.container
745
    @errors.pithos.object_path
746
    @errors.pithos.object_size
747
    def _run(self, size=0):
748
        self.client.truncate_object(self.path, size)
749

    
750
    def main(self, container___path, size=0):
751
        super(self.__class__, self)._run(container___path)
752
        self._run(size=size)
753

    
754

    
755
@command(pithos_cmds)
756
class store_overwrite(_store_container_command):
757
    """Overwrite part (from start to end) of a remote file
758
    overwrite local-path container 10 20
759
    .   will overwrite bytes from 10 to 20 of a remote file with the same name
760
    .   as local-path basename
761
    overwrite local-path container:path 10 20
762
    .   will overwrite as above, but the remote file is named path
763
    """
764

    
765
    arguments = dict(
766
        progress_bar=ProgressBarArgument(
767
            'do not show progress bar',
768
            '--no-progress-bar',
769
            default=False)
770
    )
771

    
772
    def _open_file(self, local_path, start):
773
        f = open(path.abspath(local_path), 'rb')
774
        f.seek(0, 2)
775
        f_size = f.tell()
776
        f.seek(start, 0)
777
        return (f, f_size)
778

    
779
    @errors.generic.all
780
    @errors.pithos.connection
781
    @errors.pithos.container
782
    @errors.pithos.object_path
783
    @errors.pithos.object_size
784
    def _run(self, local_path, start, end):
785
        (start, end) = (int(start), int(end))
786
        (f, f_size) = self._open_file(local_path, start)
787
        (progress_bar, upload_cb) = self._safe_progress_bar(
788
            'Overwrite %s bytes' % (end - start))
789
        try:
790
            self.client.overwrite_object(
791
                obj=self.path,
792
                start=start,
793
                end=end,
794
                source_file=f,
795
                upload_cb=upload_cb)
796
        except Exception:
797
            self._safe_progress_bar_finish(progress_bar)
798
            raise
799
        finally:
800
            self._safe_progress_bar_finish(progress_bar)
801

    
802
    def main(self, local_path, container___path, start, end):
803
        super(self.__class__, self)._run(
804
            container___path,
805
            path_is_optional=None)
806
        self.path = self.path or path.basename(local_path)
807
        self._run(local_path=local_path, start=start, end=end)
808

    
809

    
810
@command(pithos_cmds)
811
class store_manifest(_store_container_command):
812
    """Create a remote file of uploaded parts by manifestation
813
    Remains functional for compatibility with OOS Storage. Users are advised
814
    to use the upload command instead.
815
    Manifestation is a compliant process for uploading large files. The files
816
    have to be chunked in smalled files and uploaded as <prefix><increment>
817
    where increment is 1, 2, ...
818
    Finally, the manifest command glues partial files together in one file
819
    named <prefix>
820
    The upload command is faster, easier and more intuitive than manifest
821
    """
822

    
823
    arguments = dict(
824
        etag=ValueArgument('check written data', '--etag'),
825
        content_encoding=ValueArgument(
826
            'set MIME content type',
827
            '--content-encoding'),
828
        content_disposition=ValueArgument(
829
            'the presentation style of the object',
830
            '--content-disposition'),
831
        content_type=ValueArgument(
832
            'specify content type',
833
            '--content-type',
834
            default='application/octet-stream'),
835
        sharing=SharingArgument(
836
            '\n'.join([
837
                'define object sharing policy',
838
                '    ( "read=user1,grp1,user2,... write=user1,grp2,..." )']),
839
            '--sharing'),
840
        public=FlagArgument('make object publicly accessible', '--public')
841
    )
842

    
843
    @errors.generic.all
844
    @errors.pithos.connection
845
    @errors.pithos.container
846
    @errors.pithos.object_path
847
    def _run(self):
848
        self.client.create_object_by_manifestation(
849
            self.path,
850
            content_encoding=self['content_encoding'],
851
            content_disposition=self['content_disposition'],
852
            content_type=self['content_type'],
853
            sharing=self['sharing'],
854
            public=self['public'])
855

    
856
    def main(self, container___path):
857
        super(self.__class__, self)._run(
858
            container___path,
859
            path_is_optional=False)
860
        self.run()
861

    
862

    
863
@command(pithos_cmds)
864
class store_upload(_store_container_command):
865
    """Upload a file"""
866

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

    
894
    def _remote_path(self, remote_path, local_path=''):
895
        if self['overwrite']:
896
            return remote_path
897
        try:
898
            r = self.client.get_object_info(remote_path)
899
        except ClientError as ce:
900
            if ce.status == 404:
901
                return remote_path
902
            raise ce
903
        ctype = r.get('content-type', '')
904
        if 'application/directory' == ctype.lower():
905
            ret = '%s/%s' % (remote_path, local_path)
906
            return self._remote_path(ret) if local_path else ret
907
        raiseCLIError(
908
            'Object %s already exists' % remote_path,
909
            importance=1,
910
            details=['use -f to overwrite or resume'])
911

    
912
    @errors.generic.all
913
    @errors.pithos.connection
914
    @errors.pithos.container
915
    @errors.pithos.object_path
916
    @errors.pithos.local_path
917
    def _run(self, local_path, remote_path):
918
        poolsize = self['poolsize']
919
        if poolsize > 0:
920
            self.client.POOL_SIZE = int(poolsize)
921
        params = dict(
922
            content_encoding=self['content_encoding'],
923
            content_type=self['content_type'],
924
            content_disposition=self['content_disposition'],
925
            sharing=self['sharing'],
926
            public=self['public'])
927
        remote_path = self._remote_path(remote_path, local_path)
928
        with open(path.abspath(local_path), 'rb') as f:
929
            if self['unchunked']:
930
                self.client.upload_object_unchunked(
931
                    remote_path,
932
                    f,
933
                    etag=self['etag'],
934
                    withHashFile=self['use_hashes'],
935
                    **params)
936
            else:
937
                try:
938
                    (progress_bar, upload_cb) = self._safe_progress_bar(
939
                        'Uploading')
940
                    if progress_bar:
941
                        hash_bar = progress_bar.clone()
942
                        hash_cb = hash_bar.get_generator(
943
                            'Calculating block hashes'
944
                        )
945
                    else:
946
                        hash_cb = None
947
                    self.client.upload_object(
948
                        remote_path,
949
                        f,
950
                        hash_cb=hash_cb,
951
                        upload_cb=upload_cb,
952
                        **params)
953
                except Exception:
954
                    self._safe_progress_bar_finish(progress_bar)
955
                    raise
956
                finally:
957
                    self._safe_progress_bar_finish(progress_bar)
958
        print 'Upload completed'
959

    
960
    def main(self, local_path, container____path__=None):
961
        super(self.__class__, self)._run(container____path__)
962
        remote_path = self.path or path.basename(local_path)
963
        self._run(local_path=local_path, remote_path=remote_path)
964

    
965

    
966
@command(pithos_cmds)
967
class store_cat(_store_container_command):
968
    """Print remote file contents to console"""
969

    
970
    arguments = dict(
971
        range=RangeArgument('show range of data', '--range'),
972
        if_match=ValueArgument('show output if ETags match', '--if-match'),
973
        if_none_match=ValueArgument(
974
            'show output if ETags match',
975
            '--if-none-match'),
976
        if_modified_since=DateArgument(
977
            'show output modified since then',
978
            '--if-modified-since'),
979
        if_unmodified_since=DateArgument(
980
            'show output unmodified since then',
981
            '--if-unmodified-since'),
982
        object_version=ValueArgument(
983
            'get the specific version',
984
            '--object-version')
985
    )
986

    
987
    @errors.generic.all
988
    @errors.pithos.connection
989
    @errors.pithos.container
990
    @errors.pithos.object_path
991
    def _run(self):
992
        self.client.download_object(
993
            self.path,
994
            stdout,
995
            range=self['range'],
996
            version=self['object_version'],
997
            if_match=self['if_match'],
998
            if_none_match=self['if_none_match'],
999
            if_modified_since=self['if_modified_since'],
1000
            if_unmodified_since=self['if_unmodified_since'])
1001

    
1002
    def main(self, container___path):
1003
        super(self.__class__, self)._run(
1004
            container___path,
1005
            path_is_optional=False)
1006
        self._run()
1007

    
1008

    
1009
@command(pithos_cmds)
1010
class store_download(_store_container_command):
1011
    """Download remote object as local file
1012
    If local destination is a directory:
1013
    *   download <container>:<path> <local dir> -r
1014
    will download all files on <container> prefixed as <path>,
1015
    to <local dir>/<full path>
1016
    *   download <container>:<path> <local dir> --exact-match
1017
    will download only one file, exactly matching <path>
1018
    ATTENTION: to download cont:dir1/dir2/file there must exist objects
1019
    cont:dir1 and cont:dir1/dir2 of type application/directory
1020
    To create directory objects, use /store mkdir
1021
    """
1022

    
1023
    arguments = dict(
1024
        resume=FlagArgument('Resume instead of overwrite', '--resume'),
1025
        range=RangeArgument('show range of data', '--range'),
1026
        if_match=ValueArgument('show output if ETags match', '--if-match'),
1027
        if_none_match=ValueArgument(
1028
            'show output if ETags match',
1029
            '--if-none-match'),
1030
        if_modified_since=DateArgument(
1031
            'show output modified since then',
1032
            '--if-modified-since'),
1033
        if_unmodified_since=DateArgument(
1034
            'show output unmodified since then',
1035
            '--if-unmodified-since'),
1036
        object_version=ValueArgument(
1037
            'get the specific version',
1038
            '--object-version'),
1039
        poolsize=IntArgument('set pool size', '--with-pool-size'),
1040
        progress_bar=ProgressBarArgument(
1041
            'do not show progress bar',
1042
            '--no-progress-bar',
1043
            default=False),
1044
        recursive=FlagArgument(
1045
            'Download a remote directory and all its contents',
1046
            '-r, --recursive')
1047
    )
1048

    
1049
    def _is_dir(self, remote_dict):
1050
        return 'application/directory' == remote_dict.get('content_type', '')
1051

    
1052
    def _outputs(self, local_path):
1053
        if local_path is None:
1054
            return [(None, self.path)]
1055
        outpath = path.abspath(local_path)
1056
        if not (path.exists(outpath) or path.isdir(outpath)):
1057
            return [(outpath, self.path)]
1058
        elif self['recursive']:
1059
            remotes = self.client.container_get(
1060
                prefix=self.path,
1061
                if_modified_since=self['if_modified_since'],
1062
                if_unmodified_since=self['if_unmodified_since'])
1063
            return [(
1064
                '%s/%s' % (outpath, remote['name']),
1065
                None if self._is_dir(remote) else remote['name']
1066
            ) for remote in remotes.json]
1067
        raiseCLIError('Illegal destination location %s' % local_path)
1068

    
1069
    @errors.generic.all
1070
    @errors.pithos.connection
1071
    @errors.pithos.container
1072
    @errors.pithos.object_path
1073
    @errors.pithos.local_path
1074
    def _run(self, local_path):
1075
        outputs = self._outputs(local_path)
1076
        poolsize = self['poolsize']
1077
        if poolsize:
1078
            self.client.POOL_SIZE = int(poolsize)
1079
        if not outputs:
1080
            raiseCLIError('No objects prefixed as %s on container %s' % (
1081
                self.path,
1082
                self.container))
1083
        progress_bar = None
1084
        try:
1085
            for lpath, rpath in sorted(outputs):
1086
                if not rpath:
1087
                    if not path.isdir(lpath):
1088
                        print('Create directory %s' % lpath)
1089
                        makedirs(lpath)
1090
                    continue
1091
                wmode = 'rwb+' if path.exists(lpath) and self['resume']\
1092
                    else 'wb+'
1093
                print('\nFrom %s:%s to %s' % (
1094
                    self.container,
1095
                    rpath,
1096
                    lpath))
1097
                (progress_bar,
1098
                    download_cb) = self._safe_progress_bar('Downloading')
1099
                self.client.download_object(
1100
                    rpath,
1101
                    open(lpath, wmode) if lpath else stdout,
1102
                    download_cb=download_cb,
1103
                    range=self['range'],
1104
                    version=self['object_version'],
1105
                    if_match=self['if_match'],
1106
                    resume=self['resume'],
1107
                    if_none_match=self['if_none_match'],
1108
                    if_modified_since=self['if_modified_since'],
1109
                    if_unmodified_since=self['if_unmodified_since'])
1110
        except KeyboardInterrupt:
1111
            from threading import enumerate as activethreads
1112
            stdout.write('\nFinishing active threads ')
1113
            for thread in activethreads():
1114
                stdout.flush()
1115
                try:
1116
                    thread.join()
1117
                    stdout.write('.')
1118
                except RuntimeError:
1119
                    continue
1120
            print('\ndownload canceled by user')
1121
            if local_path is not None:
1122
                print('to resume, re-run with --resume')
1123
        except Exception:
1124
            self._safe_progress_bar_finish(progress_bar)
1125
            raise
1126
        finally:
1127
            self._safe_progress_bar_finish(progress_bar)
1128

    
1129
    def main(self, container___path, local_path=None):
1130
        super(self.__class__, self)._run(
1131
            container___path,
1132
            path_is_optional=False)
1133
        self._run(local_path=local_path)
1134

    
1135

    
1136
@command(pithos_cmds)
1137
class store_hashmap(_store_container_command):
1138
    """Get the hash-map of an object"""
1139

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

    
1156
    @errors.generic.all
1157
    @errors.pithos.connection
1158
    @errors.pithos.container
1159
    @errors.pithos.object_path
1160
    def _run(self):
1161
        data = self.client.get_object_hashmap(
1162
            self.path,
1163
            version=self['object_version'],
1164
            if_match=self['if_match'],
1165
            if_none_match=self['if_none_match'],
1166
            if_modified_since=self['if_modified_since'],
1167
            if_unmodified_since=self['if_unmodified_since'])
1168
        print_dict(data)
1169

    
1170
    def main(self, container___path):
1171
        super(self.__class__, self)._run(
1172
            container___path,
1173
            path_is_optional=False)
1174
        self._run()
1175

    
1176

    
1177
@command(pithos_cmds)
1178
class store_delete(_store_container_command):
1179
    """Delete a container [or an object]
1180
    How to delete a non-empty container:
1181
    - empty the container:  /store delete -r <container>
1182
    - delete it:            /store delete <container>
1183
    .
1184
    Semantics of directory deletion:
1185
    .a preserve the contents: /store delete <container>:<directory>
1186
    .    objects of the form dir/filename can exist with a dir object
1187
    .b delete contents:       /store delete -r <container>:<directory>
1188
    .    all dir/* objects are affected, even if dir does not exist
1189
    .
1190
    To restore a deleted object OBJ in a container CONT:
1191
    - get object versions: /store versions CONT:OBJ
1192
    .   and choose the version to be restored
1193
    - restore the object:  /store copy --source-version=<version> CONT:OBJ OBJ
1194
    """
1195

    
1196
    arguments = dict(
1197
        until=DateArgument('remove history until that date', '--until'),
1198
        yes=FlagArgument('Do not prompt for permission', '--yes'),
1199
        recursive=FlagArgument(
1200
            'empty dir or container and delete (if dir)',
1201
            ('-r', '--recursive'))
1202
    )
1203

    
1204
    def __init__(self, arguments={}):
1205
        super(self.__class__, self).__init__(arguments)
1206
        self['delimiter'] = DelimiterArgument(
1207
            self,
1208
            parsed_name='--delimiter',
1209
            help='delete objects prefixed with <object><delimiter>')
1210

    
1211
    @errors.generic.all
1212
    @errors.pithos.connection
1213
    @errors.pithos.container
1214
    @errors.pithos.object_path
1215
    def _run(self):
1216
        if self.path:
1217
            if self['yes'] or ask_user(
1218
                    'Delete %s:%s ?' % (self.container, self.path)):
1219
                print('is until? (%s)' % self['until'])
1220
                self.client.del_object(
1221
                    self.path,
1222
                    until=self['until'],
1223
                    delimiter=self['delimiter'])
1224
            else:
1225
                print('Aborted')
1226
        else:
1227
            if self['recursive']:
1228
                ask_msg = 'Delete container contents'
1229
            else:
1230
                ask_msg = 'Delete container'
1231
            if self['yes'] or ask_user('%s %s ?' % (ask_msg, self.container)):
1232
                self.client.del_container(
1233
                    until=self['until'],
1234
                    delimiter=self['delimiter'])
1235
            else:
1236
                print('Aborted')
1237

    
1238
    def main(self, container____path__=None):
1239
        super(self.__class__, self)._run(container____path__)
1240
        self._run()
1241

    
1242

    
1243
@command(pithos_cmds)
1244
class store_purge(_store_container_command):
1245
    """Delete a container and release related data blocks
1246
    Non-empty containers can not purged.
1247
    To purge a container with content:
1248
    .   /store delete -r <container>
1249
    .      objects are deleted, but data blocks remain on server
1250
    .   /store purge <container>
1251
    .      container and data blocks are released and deleted
1252
    """
1253

    
1254
    arguments = dict(
1255
        yes=FlagArgument('Do not prompt for permission', '--yes'),
1256
    )
1257

    
1258
    @errors.generic.all
1259
    @errors.pithos.connection
1260
    @errors.pithos.container
1261
    def _run(self):
1262
        if self['yes'] or ask_user('Purge container %s?' % self.container):
1263
                self.client.purge_container()
1264
        else:
1265
            print('Aborted')
1266

    
1267
    def main(self, container=None):
1268
        super(self.__class__, self)._run(container)
1269
        if container and self.container != container:
1270
            raiseCLIError('Invalid container name %s' % container, details=[
1271
                'Did you mean "%s" ?' % self.container,
1272
                'Use --container for names containing :'])
1273
        self._run()
1274

    
1275

    
1276
@command(pithos_cmds)
1277
class store_publish(_store_container_command):
1278
    """Publish the object and print the public url"""
1279

    
1280
    @errors.generic.all
1281
    @errors.pithos.connection
1282
    @errors.pithos.container
1283
    @errors.pithos.object_path
1284
    def _run(self):
1285
        url = self.client.publish_object(self.path)
1286
        print(url)
1287

    
1288
    def main(self, container___path):
1289
        super(self.__class__, self)._run(
1290
            container___path,
1291
            path_is_optional=False)
1292
        self._run()
1293

    
1294

    
1295
@command(pithos_cmds)
1296
class store_unpublish(_store_container_command):
1297
    """Unpublish an object"""
1298

    
1299
    @errors.generic.all
1300
    @errors.pithos.connection
1301
    @errors.pithos.container
1302
    @errors.pithos.object_path
1303
    def _run(self):
1304
            self.client.unpublish_object(self.path)
1305

    
1306
    def main(self, container___path):
1307
        super(self.__class__, self)._run(
1308
            container___path,
1309
            path_is_optional=False)
1310
        self._run()
1311

    
1312

    
1313
@command(pithos_cmds)
1314
class store_permissions(_store_container_command):
1315
    """Get read and write permissions of an object
1316
    Permissions are lists of users and user groups. There is read and write
1317
    permissions. Users and groups with write permission have also read
1318
    permission.
1319
    """
1320

    
1321
    @errors.generic.all
1322
    @errors.pithos.connection
1323
    @errors.pithos.container
1324
    @errors.pithos.object_path
1325
    def _run(self):
1326
        r = self.client.get_object_sharing(self.path)
1327
        print_dict(r)
1328

    
1329
    def main(self, container___path):
1330
        super(self.__class__, self)._run(
1331
            container___path,
1332
            path_is_optional=False)
1333
        self._run()
1334

    
1335

    
1336
@command(pithos_cmds)
1337
class store_setpermissions(_store_container_command):
1338
    """Set permissions for an object
1339
    New permissions overwrite existing permissions.
1340
    Permission format:
1341
    -   read=<username>[,usergroup[,...]]
1342
    -   write=<username>[,usegroup[,...]]
1343
    E.g. to give read permissions for file F to users A and B and write for C:
1344
    .       /store setpermissions F read=A,B write=C
1345
    """
1346

    
1347
    @errors.generic.all
1348
    def format_permition_dict(self, permissions):
1349
        read = False
1350
        write = False
1351
        for perms in permissions:
1352
            splstr = perms.split('=')
1353
            if 'read' == splstr[0]:
1354
                read = [ug.strip() for ug in splstr[1].split(',')]
1355
            elif 'write' == splstr[0]:
1356
                write = [ug.strip() for ug in splstr[1].split(',')]
1357
            else:
1358
                msg = 'Usage:\tread=<groups,users> write=<groups,users>'
1359
                raiseCLIError(None, msg)
1360
        return (read, write)
1361

    
1362
    @errors.generic.all
1363
    @errors.pithos.connection
1364
    @errors.pithos.container
1365
    @errors.pithos.object_path
1366
    def _run(self, read, write):
1367
        self.client.set_object_sharing(
1368
            self.path,
1369
            read_permition=read,
1370
            write_permition=write)
1371

    
1372
    def main(self, container___path, *permissions):
1373
        super(self.__class__, self)._run(
1374
            container___path,
1375
            path_is_optional=False)
1376
        (read, write) = self.format_permition_dict(permissions)
1377
        self._run(read, write)
1378

    
1379

    
1380
@command(pithos_cmds)
1381
class store_delpermissions(_store_container_command):
1382
    """Delete all permissions set on object
1383
    To modify permissions, use /store setpermssions
1384
    """
1385

    
1386
    @errors.generic.all
1387
    @errors.pithos.connection
1388
    @errors.pithos.container
1389
    @errors.pithos.object_path
1390
    def _run(self):
1391
        self.client.del_object_sharing(self.path)
1392

    
1393
    def main(self, container___path):
1394
        super(self.__class__, self)._run(
1395
            container___path,
1396
            path_is_optional=False)
1397
        self._run()
1398

    
1399

    
1400
@command(pithos_cmds)
1401
class store_info(_store_container_command):
1402
    """Get detailed information for user account, containers or objects
1403
    to get account info:    /store info
1404
    to get container info:  /store info <container>
1405
    to get object info:     /store info <container>:<path>
1406
    """
1407

    
1408
    arguments = dict(
1409
        object_version=ValueArgument(
1410
            'show specific version \ (applies only for objects)',
1411
            '--object-version')
1412
    )
1413

    
1414
    @errors.generic.all
1415
    @errors.pithos.connection
1416
    @errors.pithos.container
1417
    @errors.pithos.object_path
1418
    def _run(self):
1419
        if self.container is None:
1420
            r = self.client.get_account_info()
1421
        elif self.path is None:
1422
            r = self.client.get_container_info(self.container)
1423
        else:
1424
            r = self.client.get_object_info(
1425
                self.path,
1426
                version=self['object_version'])
1427
        print_dict(r)
1428

    
1429
    def main(self, container____path__=None):
1430
        super(self.__class__, self)._run(container____path__)
1431
        self._run()
1432

    
1433

    
1434
@command(pithos_cmds)
1435
class store_meta(_store_container_command):
1436
    """Get metadata for account, containers or objects"""
1437

    
1438
    arguments = dict(
1439
        detail=FlagArgument('show detailed output', '-l'),
1440
        until=DateArgument('show metadata until then', '--until'),
1441
        object_version=ValueArgument(
1442
            'show specific version \ (applies only for objects)',
1443
            '--object-version')
1444
    )
1445

    
1446
    @errors.generic.all
1447
    @errors.pithos.connection
1448
    @errors.pithos.container
1449
    @errors.pithos.object_path
1450
    def _run(self):
1451
        until = self['until']
1452
        if self.container is None:
1453
            if self['detail']:
1454
                r = self.client.get_account_info(until=until)
1455
            else:
1456
                r = self.client.get_account_meta(until=until)
1457
                r = pretty_keys(r, '-')
1458
            if r:
1459
                print(bold(self.client.account))
1460
        elif self.path is None:
1461
            if self['detail']:
1462
                r = self.client.get_container_info(until=until)
1463
            else:
1464
                cmeta = self.client.get_container_meta(until=until)
1465
                ometa = self.client.get_container_object_meta(until=until)
1466
                r = {}
1467
                if cmeta:
1468
                    r['container-meta'] = pretty_keys(cmeta, '-')
1469
                if ometa:
1470
                    r['object-meta'] = pretty_keys(ometa, '-')
1471
        else:
1472
            if self['detail']:
1473
                r = self.client.get_object_info(
1474
                    self.path,
1475
                    version=self['object_version'])
1476
            else:
1477
                r = self.client.get_object_meta(
1478
                    self.path,
1479
                    version=self['object_version'])
1480
            if r:
1481
                r = pretty_keys(pretty_keys(r, '-'))
1482
        if r:
1483
            print_dict(r)
1484

    
1485
    def main(self, container____path__=None):
1486
        super(self.__class__, self)._run(container____path__)
1487
        self._run()
1488

    
1489

    
1490
@command(pithos_cmds)
1491
class store_setmeta(_store_container_command):
1492
    """Set a piece of metadata for account, container or object
1493
    Metadata are formed as key:value pairs
1494
    """
1495

    
1496
    @errors.generic.all
1497
    @errors.pithos.connection
1498
    @errors.pithos.container
1499
    @errors.pithos.object_path
1500
    def _run(self, metakey, metaval):
1501
        if not self.container:
1502
            self.client.set_account_meta({metakey: metaval})
1503
        elif not self.path:
1504
            self.client.set_container_meta({metakey: metaval})
1505
        else:
1506
            self.client.set_object_meta(self.path, {metakey: metaval})
1507

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

    
1512

    
1513
@command(pithos_cmds)
1514
class store_delmeta(_store_container_command):
1515
    """Delete metadata with given key from account, container or object
1516
    Metadata are formed as key:value objects
1517
    - to get metadata of current account:     /store meta
1518
    - to get metadata of a container:         /store meta <container>
1519
    - to get metadata of an object:           /store meta <container>:<path>
1520
    """
1521

    
1522
    @errors.generic.all
1523
    @errors.pithos.connection
1524
    @errors.pithos.container
1525
    @errors.pithos.object_path
1526
    def _run(self, metakey):
1527
        if self.container is None:
1528
            self.client.del_account_meta(metakey)
1529
        elif self.path is None:
1530
            self.client.del_container_meta(metakey)
1531
        else:
1532
            self.client.del_object_meta(self.path, metakey)
1533

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

    
1538

    
1539
@command(pithos_cmds)
1540
class store_quota(_store_account_command):
1541
    """Get quota for account or container"""
1542

    
1543
    arguments = dict(
1544
        in_bytes=FlagArgument('Show result in bytes', ('-b', '--bytes'))
1545
    )
1546

    
1547
    @errors.generic.all
1548
    @errors.pithos.connection
1549
    @errors.pithos.container
1550
    def _run(self):
1551
        if self.container:
1552
            reply = self.client.get_container_quota(self.container)
1553
        else:
1554
            reply = self.client.get_account_quota()
1555
        if not self['in_bytes']:
1556
            for k in reply:
1557
                reply[k] = format_size(reply[k])
1558
        print_dict(pretty_keys(reply, '-'))
1559

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

    
1565

    
1566
@command(pithos_cmds)
1567
class store_setquota(_store_account_command):
1568
    """Set new quota for account or container
1569
    By default, quota is set in bytes
1570
    Users may specify a different unit, e.g:
1571
    /store setquota 2.3GB mycontainer
1572
    Accepted units: B, KiB (1024 B), KB (1000 B), MiB, MB, GiB, GB, TiB, TB
1573
    """
1574

    
1575
    @errors.generic.all
1576
    def _calculate_quota(self, user_input):
1577
        quota = 0
1578
        try:
1579
            quota = int(user_input)
1580
        except ValueError:
1581
            index = 0
1582
            digits = [str(num) for num in range(0, 10)] + ['.']
1583
            while user_input[index] in digits:
1584
                index += 1
1585
            quota = user_input[:index]
1586
            format = user_input[index:]
1587
            try:
1588
                return to_bytes(quota, format)
1589
            except Exception as qe:
1590
                msg = 'Failed to convert %s to bytes' % user_input,
1591
                raiseCLIError(qe, msg, details=[
1592
                    'Syntax: setquota <quota>[format] [container]',
1593
                    'e.g.: setquota 2.3GB mycontainer',
1594
                    'Acceptable formats:',
1595
                    '(*1024): B, KiB, MiB, GiB, TiB',
1596
                    '(*1000): B, KB, MB, GB, TB'])
1597
        return quota
1598

    
1599
    @errors.generic.all
1600
    @errors.pithos.connection
1601
    @errors.pithos.container
1602
    def _run(self, quota):
1603
        if self.container:
1604
            self.client.container = self.container
1605
            self.client.set_container_quota(quota)
1606
        else:
1607
            self.client.set_account_quota(quota)
1608

    
1609
    def main(self, quota, container=None):
1610
        super(self.__class__, self)._run()
1611
        quota = self._calculate_quota(quota)
1612
        self.container = container
1613
        self._run(quota)
1614

    
1615

    
1616
@command(pithos_cmds)
1617
class store_versioning(_store_account_command):
1618
    """Get  versioning for account or container"""
1619

    
1620
    @errors.generic.all
1621
    @errors.pithos.connection
1622
    @errors.pithos.container
1623
    def _run(self):
1624
        if self.container:
1625
            r = self.client.get_container_versioning(self.container)
1626
        else:
1627
            r = self.client.get_account_versioning()
1628
        print_dict(r)
1629

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

    
1635

    
1636
@command(pithos_cmds)
1637
class store_setversioning(_store_account_command):
1638
    """Set versioning mode (auto, none) for account or container"""
1639

    
1640
    def _check_versioning(self, versioning):
1641
        if versioning and versioning.lower() in ('auto', 'none'):
1642
            return versioning.lower()
1643
        raiseCLIError('Invalid versioning %s' % versioning, details=[
1644
            'Versioning can be auto or none'])
1645

    
1646
    @errors.generic.all
1647
    @errors.pithos.connection
1648
    @errors.pithos.container
1649
    def _run(self, versioning):
1650
        if self.container:
1651
            self.client.container = self.container
1652
            self.client.set_container_versioning(versioning)
1653
        else:
1654
            self.client.set_account_versioning(versioning)
1655

    
1656
    def main(self, versioning, container=None):
1657
        super(self.__class__, self)._run()
1658
        self._run(self._check_versioning(versioning))
1659

    
1660

    
1661
@command(pithos_cmds)
1662
class store_group(_store_account_command):
1663
    """Get groups and group members"""
1664

    
1665
    @errors.generic.all
1666
    @errors.pithos.connection
1667
    def _run(self):
1668
        r = self.client.get_account_group()
1669
        print_dict(pretty_keys(r, '-'))
1670

    
1671
    def main(self):
1672
        super(self.__class__, self)._run()
1673
        self._run()
1674

    
1675

    
1676
@command(pithos_cmds)
1677
class store_setgroup(_store_account_command):
1678
    """Set a user group"""
1679

    
1680
    @errors.generic.all
1681
    @errors.pithos.connection
1682
    def _run(self, groupname, *users):
1683
        self.client.set_account_group(groupname, users)
1684

    
1685
    def main(self, groupname, *users):
1686
        super(self.__class__, self)._run()
1687
        if users:
1688
            self._run(groupname, *users)
1689
        else:
1690
            raiseCLIError('No users to add in group %s' % groupname)
1691

    
1692

    
1693
@command(pithos_cmds)
1694
class store_delgroup(_store_account_command):
1695
    """Delete a user group"""
1696

    
1697
    @errors.generic.all
1698
    @errors.pithos.connection
1699
    def _run(self, groupname):
1700
        self.client.del_account_group(groupname)
1701

    
1702
    def main(self, groupname):
1703
        super(self.__class__, self)._run()
1704
        self._run(groupname)
1705

    
1706

    
1707
@command(pithos_cmds)
1708
class store_sharers(_store_account_command):
1709
    """List the accounts that share objects with current user"""
1710

    
1711
    arguments = dict(
1712
        detail=FlagArgument('show detailed output', '-l'),
1713
        marker=ValueArgument('show output greater then marker', '--marker')
1714
    )
1715

    
1716
    @errors.generic.all
1717
    @errors.pithos.connection
1718
    def _run(self):
1719
        accounts = self.client.get_sharing_accounts(marker=self['marker'])
1720
        if self['detail']:
1721
            print_items(accounts)
1722
        else:
1723
            print_items([acc['name'] for acc in accounts])
1724

    
1725
    def main(self):
1726
        super(self.__class__, self)._run()
1727
        self._run()
1728

    
1729

    
1730
@command(pithos_cmds)
1731
class store_versions(_store_container_command):
1732
    """Get the list of object versions
1733
    Deleted objects may still have versions that can be used to restore it and
1734
    get information about its previous state.
1735
    The version number can be used in a number of other commands, like info,
1736
    copy, move, meta. See these commands for more information, e.g.
1737
    /store info -h
1738
    """
1739

    
1740
    @errors.generic.all
1741
    @errors.pithos.connection
1742
    @errors.pithos.container
1743
    @errors.pithos.object_path
1744
    def _run(self):
1745
        versions = self.client.get_object_versionlist(self.path)
1746
        print_items([dict(id=vitem[0], created=strftime(
1747
            '%d-%m-%Y %H:%M:%S',
1748
            localtime(float(vitem[1])))) for vitem in versions])
1749

    
1750
    def main(self, container___path):
1751
        super(store_versions, self)._run(
1752
            container___path,
1753
            path_is_optional=False)
1754
        self._run()