Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / pithos_cli.py @ 7ae842c2

History | View | Annotate | Download (60.3 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
            ])
86
    elif e.status == 413:
87
        raiseCLIError(e, details=[
88
            'Get quotas:',
89
            '- total quota:      /store quota',
90
            '- container quota:  /store quota <container>',
91
            'Users shall set a higher container quota, if available:',
92
            '-                  /store setquota <quota>[unit] <container>'
93
            ])
94

    
95

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

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

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

    
112
    @value.setter
113
    def value(self, newvalue):
114
        self._value = newvalue
115

    
116

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

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

    
130
    @value.setter
131
    def value(self, newvalue):
132
        perms = {}
133
        try:
134
            permlist = newvalue.split(' ')
135
        except AttributeError:
136
            return
137
        for p in permlist:
138
            try:
139
                (key, val) = p.split('=')
140
            except ValueError as err:
141
                raiseCLIError(err, 'Error in --sharing',
142
                    details='Incorrect format',
143
                    importance=1)
144
            if key.lower() not in ('read', 'write'):
145
                raiseCLIError(err, 'Error in --sharing',
146
                    details='Invalid permission key %s' % key,
147
                    importance=1)
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(base_url=self.base_url,
193
            token=self.token,
194
            account=self.account,
195
            container=self.container)
196

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

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

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

    
209

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

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

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

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

    
228

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

    
232
    container = None
233
    path = None
234

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

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

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

    
267
        user_cont, sep, userpath = container_with_path.partition(':')
268

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

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

    
319
    def main(self, container_with_path=None, path_is_optional=True):
320
        self._run(container_with_path, path_is_optional)
321

    
322

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

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

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

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

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

    
446
    def main(self, container____path__=None):
447
        super(self.__class__, self)._run(container____path__)
448
        self._run()
449

    
450

    
451
@command(pithos_cmds)
452
class store_mkdir(_store_container_command):
453
    """Create a directory"""
454

    
455
    __doc__ += '\n. '.join(about_directories)
456

    
457
    @errors.generic.all
458
    @errors.pithos.connection
459
    @errors.pithos.container
460
    def _run(self):
461
        self.client.create_directory(self.path)
462

    
463
    def main(self, container___directory):
464
        super(self.__class__, self)._run(
465
            container___directory,
466
            path_is_optional=False)
467
        self._run()
468

    
469

    
470
@command(pithos_cmds)
471
class store_touch(_store_container_command):
472
    """Create an empty object (file)
473
    If object exists, this command will reset it to 0 length
474
    """
475

    
476
    arguments = dict(
477
        content_type=ValueArgument(
478
            'Set content type (default: application/octet-stream)',
479
            '--content-type',
480
            default='application/octet-stream')
481
    )
482

    
483
    @errors.generic.all
484
    @errors.pithos.connection
485
    @errors.pithos.container
486
    def _run(self):
487
        self.client.create_object(self.path, self['content_type'])
488

    
489
    def main(self, container___path):
490
        super(store_touch, self)._run(
491
            container___path,
492
            path_is_optional=False)
493
        self._run()
494

    
495

    
496
@command(pithos_cmds)
497
class store_create(_store_container_command):
498
    """Create a container"""
499

    
500
    arguments = dict(
501
        versioning=ValueArgument(
502
            'set container versioning (auto/none)',
503
            '--versioning'),
504
        quota=IntArgument('set default container quota', '--quota'),
505
        meta=KeyValueArgument(
506
            'set container metadata (can be repeated)',
507
            '--meta')
508
    )
509

    
510
    @errors.generic.all
511
    @errors.pithos.connection
512
    @errors.pithos.container
513
    def _run(self):
514
        self.client.container_put(quota=self['quota'],
515
            versioning=self['versioning'],
516
            metadata=self['meta'])
517

    
518
    def main(self, container=None):
519
        super(self.__class__, self)._run(container)
520
        if container and self.container != container:
521
            raiseCLIError('Invalid container name %s' % container, details=[
522
                'Did you mean "%s" ?' % self.container,
523
                'Use --container for names containing :'])
524
        self._run()
525

    
526

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

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

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

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

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

    
611

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

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

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

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

    
680
    def main(self,
681
        source_container___path,
682
        destination_container___path=None):
683
        super(self.__class__, self)._run(
684
            source_container___path,
685
            path_is_optional=False)
686
        (dst_cont, dst_path) = self._dest_container_path(
687
            destination_container___path)
688
        self._run(dst_cont=dst_cont, dst_path=dst_path or '')
689

    
690

    
691
@command(pithos_cmds)
692
class store_append(_store_container_command):
693
    """Append local file to (existing) remote object
694
    The remote object should exist.
695
    If the remote object is a directory, it is transformed into a file.
696
    In the later case, objects under the directory remain intact.
697
    """
698

    
699
    arguments = dict(
700
        progress_bar=ProgressBarArgument(
701
            'do not show progress bar',
702
            '--no-progress-bar',
703
            default=False)
704
    )
705

    
706
    @errors.generic.all
707
    @errors.pithos.connection
708
    @errors.pithos.container
709
    @errors.pithos.object_path
710
    def _run(self, local_path):
711
        (progress_bar, upload_cb) = self._safe_progress_bar('Appending')
712
        try:
713
            f = open(local_path, 'rb')
714
            self.client.append_object(self.path, f, upload_cb)
715
        except Exception:
716
            self._safe_progress_bar_finish(progress_bar)
717
            raise
718
        finally:
719
            self._safe_progress_bar_finish(progress_bar)
720

    
721
    def main(self, local_path, container___path):
722
        super(self.__class__, self)._run(
723
            container___path,
724
            path_is_optional=False)
725
        self._run(local_path)
726

    
727

    
728
@command(pithos_cmds)
729
class store_truncate(_store_container_command):
730
    """Truncate remote file up to a size (default is 0)"""
731

    
732
    @errors.generic.all
733
    @errors.pithos.connection
734
    @errors.pithos.container
735
    @errors.pithos.object_path
736
    @errors.pithos.object_size
737
    def _run(self, size=0):
738
        self.client.truncate_object(self.path, size)
739

    
740
    def main(self, container___path, size=0):
741
        super(self.__class__, self)._run(container___path)
742
        self._run(size=size)
743

    
744

    
745
@command(pithos_cmds)
746
class store_overwrite(_store_container_command):
747
    """Overwrite part (from start to end) of a remote file
748
    overwrite local-path container 10 20
749
    .   will overwrite bytes from 10 to 20 of a remote file with the same name
750
    .   as local-path basename
751
    overwrite local-path container:path 10 20
752
    .   will overwrite as above, but the remote file is named path
753
    """
754

    
755
    arguments = dict(
756
        progress_bar=ProgressBarArgument(
757
            'do not show progress bar',
758
            '--no-progress-bar',
759
            default=False)
760
    )
761

    
762
    def _open_file(self, local_path, start):
763
        f = open(path.abspath(local_path), 'rb')
764
        f.seek(0, 2)
765
        f_size = f.tell()
766
        f.seek(start, 0)
767
        return (f, f_size)
768

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

    
792
    def main(self, local_path, container___path, start, end):
793
        super(self.__class__, self)._run(
794
            container___path,
795
            path_is_optional=None)
796
        self.path = self.path or path.basename(local_path)
797
        self._run(local_path=local_path, start=start, end=end)
798

    
799

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

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

    
832
    @errors.generic.all
833
    @errors.pithos.connection
834
    @errors.pithos.container
835
    @errors.pithos.object_path
836
    def _run(self):
837
        self.client.create_object_by_manifestation(
838
            self.path,
839
            content_encoding=self['content_encoding'],
840
            content_disposition=self['content_disposition'],
841
            content_type=self['content_type'],
842
            sharing=self['sharing'],
843
            public=self['public'])
844

    
845
    def main(self, container___path):
846
        super(self.__class__, self)._run(
847
            container___path,
848
            path_is_optional=False)
849
        self.run()
850

    
851

    
852
@command(pithos_cmds)
853
class store_upload(_store_container_command):
854
    """Upload a file"""
855

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

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

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

    
947
    def main(self, local_path, container____path__=None):
948
        super(self.__class__, self)._run(container____path__)
949
        remote_path = self.path or path.basename(local_path)
950
        self._run(local_path=local_path, remote_path=remote_path)
951

    
952

    
953
@command(pithos_cmds)
954
class store_cat(_store_container_command):
955
    """Print remote file contents to console"""
956

    
957
    arguments = dict(
958
        range=RangeArgument('show range of data', '--range'),
959
        if_match=ValueArgument('show output if ETags match', '--if-match'),
960
        if_none_match=ValueArgument(
961
            'show output if ETags match',
962
            '--if-none-match'),
963
        if_modified_since=DateArgument(
964
            'show output modified since then',
965
            '--if-modified-since'),
966
        if_unmodified_since=DateArgument(
967
            'show output unmodified since then',
968
            '--if-unmodified-since'),
969
        object_version=ValueArgument(
970
            'get the specific version',
971
            '--object-version')
972
    )
973

    
974
    @errors.generic.all
975
    @errors.pithos.connection
976
    @errors.pithos.container
977
    @errors.pithos.object_path
978
    def _run(self):
979
        self.client.download_object(
980
            self.path,
981
            stdout,
982
            range=self['range'],
983
            version=self['object_version'],
984
            if_match=self['if_match'],
985
            if_none_match=self['if_none_match'],
986
            if_modified_since=self['if_modified_since'],
987
            if_unmodified_since=self['if_unmodified_since'])
988

    
989
    def main(self, container___path):
990
        super(self.__class__, self)._run(
991
            container___path,
992
            path_is_optional=False)
993
        self._run()
994

    
995

    
996
@command(pithos_cmds)
997
class store_download(_store_container_command):
998
    """Download remote object as local file
999
    If local destination is a directory:
1000
    *   download <container>:<path> <local dir> -r
1001
    will download all files on <container> prefixed as <path>,
1002
    to <local dir>/<full path>
1003
    *   download <container>:<path> <local dir> --exact-match
1004
    will download only one file, exactly matching <path>
1005
    ATTENTION: to download cont:dir1/dir2/file there must exist objects
1006
    cont:dir1 and cont:dir1/dir2 of type application/directory
1007
    To create directory objects, use /store mkdir
1008
    """
1009

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

    
1036
    def _is_dir(self, remote_dict):
1037
        return 'application/directory' == remote_dict.get('content_type', '')
1038

    
1039
    def _outputs(self, local_path):
1040
        if local_path is None:
1041
            return [(None, self.path)]
1042
        outpath = path.abspath(local_path)
1043
        if not (path.exists(outpath) or path.isdir(outpath)):
1044
            return [(outpath, self.path)]
1045
        elif self['recursive']:
1046
            remotes = self.client.container_get(
1047
                prefix=self.path,
1048
                if_modified_since=self['if_modified_since'],
1049
                if_unmodified_since=self['if_unmodified_since'])
1050
            return [(
1051
                '%s/%s' % (outpath, remote['name']),
1052
                    None if self._is_dir(remote) else remote['name'])\
1053
                for remote in remotes.json]
1054
        raiseCLIError('Illegal destination location %s' % local_path)
1055

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

    
1116
    def main(self, container___path, local_path=None):
1117
        super(self.__class__, self)._run(
1118
            container___path,
1119
            path_is_optional=False)
1120
        self._run(local_path=local_path)
1121

    
1122

    
1123
@command(pithos_cmds)
1124
class store_hashmap(_store_container_command):
1125
    """Get the hash-map of an object"""
1126

    
1127
    arguments = dict(
1128
        if_match=ValueArgument('show output if ETags match', '--if-match'),
1129
        if_none_match=ValueArgument(
1130
            'show output if ETags match',
1131
            '--if-none-match'),
1132
        if_modified_since=DateArgument(
1133
            'show output modified since then',
1134
            '--if-modified-since'),
1135
        if_unmodified_since=DateArgument(
1136
            'show output unmodified since then',
1137
            '--if-unmodified-since'),
1138
        object_version=ValueArgument(
1139
            'get the specific version',
1140
            '--object-version')
1141
    )
1142

    
1143
    @errors.generic.all
1144
    @errors.pithos.connection
1145
    @errors.pithos.container
1146
    @errors.pithos.object_path
1147
    def _run(self):
1148
        data = self.client.get_object_hashmap(
1149
            self.path,
1150
            version=self['object_version'],
1151
            if_match=self['if_match'],
1152
            if_none_match=self['if_none_match'],
1153
            if_modified_since=self['if_modified_since'],
1154
            if_unmodified_since=self['if_unmodified_since'])
1155
        print_dict(data)
1156

    
1157
    def main(self, container___path):
1158
        super(self.__class__, self)._run(
1159
            container___path,
1160
            path_is_optional=False)
1161
        self._run()
1162

    
1163

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

    
1183
    arguments = dict(
1184
        until=DateArgument('remove history until that date', '--until'),
1185
        yes=FlagArgument('Do not prompt for permission', '--yes'),
1186
        recursive=FlagArgument(
1187
            'empty dir or container and delete (if dir)',
1188
            ('-r', '--recursive'))
1189
    )
1190

    
1191
    def __init__(self, arguments={}):
1192
        super(self.__class__, self).__init__(arguments)
1193
        self['delimiter'] = DelimiterArgument(
1194
            self,
1195
            parsed_name='--delimiter',
1196
            help='delete objects prefixed with <object><delimiter>')
1197

    
1198
    @errors.generic.all
1199
    @errors.pithos.connection
1200
    @errors.pithos.container
1201
    @errors.pithos.object_path
1202
    def _run(self):
1203
        if self.path:
1204
            if self['yes'] or ask_user(
1205
                'Delete %s:%s ?' % (self.container, self.path)):
1206
                self.client.del_object(
1207
                    self.path,
1208
                    until=self['until'],
1209
                    delimiter=self['delimiter'])
1210
            else:
1211
                print('Aborted')
1212
        else:
1213
            ask_msg = 'Delete contents of container'\
1214
            if self['recursive'] else 'Delete container'
1215
            if self['yes'] or ask_user('%s %s ?' % (ask_msg, self.container)):
1216
                self.client.del_container(
1217
                    until=self['until'],
1218
                    delimiter=self['delimiter'])
1219
            else:
1220
                print('Aborted')
1221

    
1222
    def main(self, container____path__=None):
1223
        super(self.__class__, self)._run(container____path__)
1224
        self._run()
1225

    
1226

    
1227
@command(pithos_cmds)
1228
class store_purge(_store_container_command):
1229
    """Delete a container and release related data blocks
1230
    Non-empty containers can not purged.
1231
    To purge a container with content:
1232
    .   /store delete -r <container>
1233
    .      objects are deleted, but data blocks remain on server
1234
    .   /store purge <container>
1235
    .      container and data blocks are released and deleted
1236
    """
1237

    
1238
    arguments = dict(
1239
        yes=FlagArgument('Do not prompt for permission', '--yes'),
1240
    )
1241

    
1242
    @errors.generic.all
1243
    @errors.pithos.connection
1244
    @errors.pithos.container
1245
    def _run(self):
1246
        if self['yes'] or ask_user('Purge container %s?' % self.container):
1247
                self.client.purge_container()
1248
        else:
1249
            print('Aborted')
1250

    
1251
    def main(self, container=None):
1252
        super(self.__class__, self)._run(container)
1253
        if container and self.container != container:
1254
            raiseCLIError('Invalid container name %s' % container, details=[
1255
                'Did you mean "%s" ?' % self.container,
1256
                'Use --container for names containing :'])
1257
        self._run()
1258

    
1259

    
1260
@command(pithos_cmds)
1261
class store_publish(_store_container_command):
1262
    """Publish the object and print the public url"""
1263

    
1264
    @errors.generic.all
1265
    @errors.pithos.connection
1266
    @errors.pithos.container
1267
    @errors.pithos.object_path
1268
    def _run(self):
1269
        url = self.client.publish_object(self.path)
1270
        print(url)
1271

    
1272
    def main(self, container___path):
1273
        super(self.__class__, self)._run(
1274
            container___path,
1275
            path_is_optional=False)
1276
        self._run()
1277

    
1278

    
1279
@command(pithos_cmds)
1280
class store_unpublish(_store_container_command):
1281
    """Unpublish an object"""
1282

    
1283
    @errors.generic.all
1284
    @errors.pithos.connection
1285
    @errors.pithos.container
1286
    @errors.pithos.object_path
1287
    def _run(self):
1288
            self.client.unpublish_object(self.path)
1289

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

    
1296

    
1297
@command(pithos_cmds)
1298
class store_permissions(_store_container_command):
1299
    """Get read and write permissions of an object
1300
    Permissions are lists of users and user groups. There is read and write
1301
    permissions. Users and groups with write permission have also read
1302
    permission.
1303
    """
1304

    
1305
    @errors.generic.all
1306
    @errors.pithos.connection
1307
    @errors.pithos.container
1308
    @errors.pithos.object_path
1309
    def _run(self):
1310
        r = self.client.get_object_sharing(self.path)
1311
        print_dict(r)
1312

    
1313
    def main(self, container___path):
1314
        super(self.__class__, self)._run(
1315
            container___path,
1316
            path_is_optional=False)
1317
        self._run()
1318

    
1319

    
1320
@command(pithos_cmds)
1321
class store_setpermissions(_store_container_command):
1322
    """Set permissions for an object
1323
    New permissions overwrite existing permissions.
1324
    Permission format:
1325
    -   read=<username>[,usergroup[,...]]
1326
    -   write=<username>[,usegroup[,...]]
1327
    E.g. to give read permissions for file F to users A and B and write for C:
1328
    .       /store setpermissions F read=A,B write=C
1329
    """
1330

    
1331
    @errors.generic.all
1332
    def format_permition_dict(self, permissions):
1333
        read = False
1334
        write = False
1335
        for perms in permissions:
1336
            splstr = perms.split('=')
1337
            if 'read' == splstr[0]:
1338
                read = [user_or_group.strip() \
1339
                for user_or_group in splstr[1].split(',')]
1340
            elif 'write' == splstr[0]:
1341
                write = [user_or_group.strip() \
1342
                for user_or_group in splstr[1].split(',')]
1343
            else:
1344
                read = False
1345
                write = False
1346
        if not (read or write):
1347
            raiseCLIError(None,
1348
            'Usage:\tread=<groups,users> write=<groups,users>')
1349
        return (read, write)
1350

    
1351
    @errors.generic.all
1352
    @errors.pithos.connection
1353
    @errors.pithos.container
1354
    @errors.pithos.object_path
1355
    def _run(self, read, write):
1356
        self.client.set_object_sharing(
1357
            self.path,
1358
            read_permition=read,
1359
            write_permition=write)
1360

    
1361
    def main(self, container___path, *permissions):
1362
        super(self.__class__, self)._run(
1363
            container___path,
1364
            path_is_optional=False)
1365
        (read, write) = self.format_permition_dict(permissions)
1366
        self._run(read, write)
1367

    
1368

    
1369
@command(pithos_cmds)
1370
class store_delpermissions(_store_container_command):
1371
    """Delete all permissions set on object
1372
    To modify permissions, use /store setpermssions
1373
    """
1374

    
1375
    @errors.generic.all
1376
    @errors.pithos.connection
1377
    @errors.pithos.container
1378
    @errors.pithos.object_path
1379
    def _run(self):
1380
        self.client.del_object_sharing(self.path)
1381

    
1382
    def main(self, container___path):
1383
        super(self.__class__, self)._run(
1384
            container___path,
1385
            path_is_optional=False)
1386
        self._run()
1387

    
1388

    
1389
@command(pithos_cmds)
1390
class store_info(_store_container_command):
1391
    """Get detailed information for user account, containers or objects
1392
    to get account info:    /store info
1393
    to get container info:  /store info <container>
1394
    to get object info:     /store info <container>:<path>
1395
    """
1396

    
1397
    arguments = dict(
1398
        object_version=ValueArgument(
1399
            'show specific version \ (applies only for objects)',
1400
            '--object-version')
1401
    )
1402

    
1403
    @errors.generic.all
1404
    @errors.pithos.connection
1405
    @errors.pithos.container
1406
    @errors.pithos.object_path
1407
    def _run(self):
1408
        if self.container is None:
1409
            r = self.client.get_account_info()
1410
        elif self.path is None:
1411
            r = self.client.get_container_info(self.container)
1412
        else:
1413
            r = self.client.get_object_info(
1414
                self.path,
1415
                version=self['object_version'])
1416
        print_dict(r)
1417

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

    
1422

    
1423
@command(pithos_cmds)
1424
class store_meta(_store_container_command):
1425
    """Get metadata for account, containers or objects"""
1426

    
1427
    arguments = dict(
1428
        detail=FlagArgument('show detailed output', '-l'),
1429
        until=DateArgument('show metadata until then', '--until'),
1430
        object_version=ValueArgument(
1431
            'show specific version \ (applies only for objects)',
1432
            '--object-version')
1433
    )
1434

    
1435
    @errors.generic.all
1436
    @errors.pithos.connection
1437
    @errors.pithos.container
1438
    @errors.pithos.object_path
1439
    def _run(self):
1440
        until = self['until']
1441
        if self.container is None:
1442
            if self['detail']:
1443
                r = self.client.get_account_info(until=until)
1444
            else:
1445
                r = self.client.get_account_meta(until=until)
1446
                r = pretty_keys(r, '-')
1447
            if r:
1448
                print(bold(self.client.account))
1449
        elif self.path is None:
1450
            if self['detail']:
1451
                r = self.client.get_container_info(until=until)
1452
            else:
1453
                cmeta = self.client.get_container_meta(until=until)
1454
                ometa = self.client.get_container_object_meta(until=until)
1455
                r = {}
1456
                if cmeta:
1457
                    r['container-meta'] = pretty_keys(cmeta, '-')
1458
                if ometa:
1459
                    r['object-meta'] = pretty_keys(ometa, '-')
1460
        else:
1461
            if self['detail']:
1462
                r = self.client.get_object_info(self.path,
1463
                    version=self['object_version'])
1464
            else:
1465
                r = self.client.get_object_meta(self.path,
1466
                    version=self['object_version'])
1467
            if r:
1468
                r = pretty_keys(pretty_keys(r, '-'))
1469
        if r:
1470
            print_dict(r)
1471

    
1472
    def main(self, container____path__=None):
1473
        super(self.__class__, self)._run(container____path__)
1474
        self._run()
1475

    
1476

    
1477
@command(pithos_cmds)
1478
class store_setmeta(_store_container_command):
1479
    """Set a piece of metadata for account, container or object
1480
    Metadata are formed as key:value pairs
1481
    """
1482

    
1483
    @errors.generic.all
1484
    @errors.pithos.connection
1485
    @errors.pithos.container
1486
    @errors.pithos.object_path
1487
    def _run(self, metakey, metaval):
1488
        if not self.container:
1489
            self.client.set_account_meta({metakey: metaval})
1490
        elif not self.path:
1491
            self.client.set_container_meta({metakey: metaval})
1492
        else:
1493
            self.client.set_object_meta(self.path, {metakey: metaval})
1494

    
1495
    def main(self, metakey, metaval, container____path__=None):
1496
        super(self.__class__, self)._run(container____path__)
1497
        self._run(metakey=metakey, metaval=metaval)
1498

    
1499

    
1500
@command(pithos_cmds)
1501
class store_delmeta(_store_container_command):
1502
    """Delete metadata with given key from account, container or object
1503
    Metadata are formed as key:value objects
1504
    - to get metadata of current account:     /store meta
1505
    - to get metadata of a container:         /store meta <container>
1506
    - to get metadata of an object:           /store meta <container>:<path>
1507
    """
1508

    
1509
    @errors.generic.all
1510
    @errors.pithos.connection
1511
    @errors.pithos.container
1512
    @errors.pithos.object_path
1513
    def _run(self, metakey):
1514
        if self.container is None:
1515
            self.client.del_account_meta(metakey)
1516
        elif self.path is None:
1517
            self.client.del_container_meta(metakey)
1518
        else:
1519
            self.client.del_object_meta(self.path, metakey)
1520

    
1521
    def main(self, metakey, container____path__=None):
1522
        super(self.__class__, self)._run(container____path__)
1523
        self._run(metakey)
1524

    
1525

    
1526
@command(pithos_cmds)
1527
class store_quota(_store_account_command):
1528
    """Get quota for account or container"""
1529

    
1530
    arguments = dict(
1531
        in_bytes=FlagArgument('Show result in bytes', ('-b', '--bytes'))
1532
        )
1533

    
1534
    @errors.generic.all
1535
    @errors.pithos.connection
1536
    @errors.pithos.container
1537
    def _run(self):
1538
        if self.container:
1539
            reply = self.client.get_container_quota(self.container)
1540
        else:
1541
            reply = self.client.get_account_quota()
1542
        if not self['in_bytes']:
1543
            for k in reply:
1544
                reply[k] = format_size(reply[k])
1545
        print_dict(pretty_keys(reply, '-'))
1546

    
1547
    def main(self, container=None):
1548
        super(self.__class__, self)._run()
1549
        self.container = container
1550
        self._run()
1551

    
1552

    
1553
@command(pithos_cmds)
1554
class store_setquota(_store_account_command):
1555
    """Set new quota for account or container
1556
    By default, quota is set in bytes
1557
    Users may specify a different unit, e.g:
1558
    /store setquota 2.3GB mycontainer
1559
    Accepted units: B, KiB (1024 B), KB (1000 B), MiB, MB, GiB, GB, TiB, TB
1560
    """
1561

    
1562
    @errors.generic.all
1563
    def _calculate_quota(self, user_input):
1564
        quota = 0
1565
        try:
1566
            quota = int(user_input)
1567
        except ValueError:
1568
            index = 0
1569
            digits = [str(num) for num in range(0, 10)] + ['.']
1570
            while user_input[index] in digits:
1571
                index += 1
1572
            quota = user_input[:index]
1573
            format = user_input[index:]
1574
            try:
1575
                return to_bytes(quota, format)
1576
            except Exception as qe:
1577
                raiseCLIError(qe,
1578
                    'Failed to convert %s to bytes' % user_input,
1579
                    details=[
1580
                        'Syntax: setquota <quota>[format] [container]',
1581
                        'e.g.: setquota 2.3GB mycontainer',
1582
                        'Acceptable formats:',
1583
                        '(*1024): B, KiB, MiB, GiB, TiB',
1584
                        '(*1000): B, KB, MB, GB, TB'])
1585
        return quota
1586

    
1587
    @errors.generic.all
1588
    @errors.pithos.connection
1589
    @errors.pithos.container
1590
    def _run(self, quota):
1591
        if self.container:
1592
            self.client.container = self.container
1593
            self.client.set_container_quota(quota)
1594
        else:
1595
            self.client.set_account_quota(quota)
1596

    
1597
    def main(self, quota, container=None):
1598
        super(self.__class__, self)._run()
1599
        quota = self._calculate_quota(quota)
1600
        self.container = container
1601
        self._run(quota)
1602

    
1603

    
1604
@command(pithos_cmds)
1605
class store_versioning(_store_account_command):
1606
    """Get  versioning for account or container"""
1607

    
1608
    @errors.generic.all
1609
    @errors.pithos.connection
1610
    @errors.pithos.container
1611
    def _run(self):
1612
        if self.container:
1613
            r = self.client.get_container_versioning(self.container)
1614
        else:
1615
            r = self.client.get_account_versioning()
1616
        print_dict(r)
1617

    
1618
    def main(self, container=None):
1619
        super(self.__class__, self)._run()
1620
        self.container = container
1621
        self._run()
1622

    
1623

    
1624
@command(pithos_cmds)
1625
class store_setversioning(_store_account_command):
1626
    """Set versioning mode (auto, none) for account or container"""
1627

    
1628
    def _check_versioning(self, versioning):
1629
        if versioning and versioning.lower() in ('auto', 'none'):
1630
            return versioning.lower()
1631
        raiseCLIError('Invalid versioning %s' % versioning, details=[
1632
            'Versioning can be auto or none'])
1633

    
1634
    @errors.generic.all
1635
    @errors.pithos.connection
1636
    @errors.pithos.container
1637
    def _run(self, versioning):
1638
        if self.container:
1639
            self.client.container = self.container
1640
            self.client.set_container_versioning(versioning)
1641
        else:
1642
            self.client.set_account_versioning(versioning)
1643

    
1644
    def main(self, versioning, container=None):
1645
        super(self.__class__, self)._run()
1646
        self._run(self._check_versioning(versioning))
1647

    
1648

    
1649
@command(pithos_cmds)
1650
class store_group(_store_account_command):
1651
    """Get groups and group members"""
1652

    
1653
    @errors.generic.all
1654
    @errors.pithos.connection
1655
    def _run(self):
1656
        r = self.client.get_account_group()
1657
        print_dict(pretty_keys(r, '-'))
1658

    
1659
    def main(self):
1660
        super(self.__class__, self)._run()
1661
        self._run()
1662

    
1663

    
1664
@command(pithos_cmds)
1665
class store_setgroup(_store_account_command):
1666
    """Set a user group"""
1667

    
1668
    @errors.generic.all
1669
    @errors.pithos.connection
1670
    def _run(self, groupname, *users):
1671
        self.client.set_account_group(groupname, users)
1672

    
1673
    def main(self, groupname, *users):
1674
        super(self.__class__, self)._run()
1675
        if users:
1676
            self._run(groupname, *users)
1677
        else:
1678
            raiseCLIError('No users to add in group %s' % groupname)
1679

    
1680

    
1681
@command(pithos_cmds)
1682
class store_delgroup(_store_account_command):
1683
    """Delete a user group"""
1684

    
1685
    @errors.generic.all
1686
    @errors.pithos.connection
1687
    def _run(self, groupname):
1688
        self.client.del_account_group(groupname)
1689

    
1690
    def main(self, groupname):
1691
        super(self.__class__, self)._run()
1692
        self._run(groupname)
1693

    
1694

    
1695
@command(pithos_cmds)
1696
class store_sharers(_store_account_command):
1697
    """List the accounts that share objects with current user"""
1698

    
1699
    arguments = dict(
1700
        detail=FlagArgument('show detailed output', '-l'),
1701
        marker=ValueArgument('show output greater then marker', '--marker')
1702
    )
1703

    
1704
    @errors.generic.all
1705
    @errors.pithos.connection
1706
    def _run(self):
1707
        accounts = self.client.get_sharing_accounts(marker=self['marker'])
1708
        print_items(accounts if self['detail']\
1709
            else [acc['name'] for acc in accounts])
1710

    
1711
    def main(self):
1712
        super(self.__class__, self)._run()
1713
        self._run()
1714

    
1715

    
1716
@command(pithos_cmds)
1717
class store_versions(_store_container_command):
1718
    """Get the list of object versions
1719
    Deleted objects may still have versions that can be used to restore it and
1720
    get information about its previous state.
1721
    The version number can be used in a number of other commands, like info,
1722
    copy, move, meta. See these commands for more information, e.g.
1723
    /store info -h
1724
    """
1725

    
1726
    @errors.generic.all
1727
    @errors.pithos.connection
1728
    @errors.pithos.container
1729
    @errors.pithos.object_path
1730
    def _run(self):
1731
        versions = self.client.get_object_versionlist(self.path)
1732
        print_items([dict(
1733
            id=vitem[0],
1734
            created=strftime('%d-%m-%Y %H:%M:%S', localtime(float(vitem[1])))
1735
            ) for vitem in versions])
1736

    
1737
    def main(self, container___path):
1738
        super(store_versions, self)._run(
1739
            container___path,
1740
            path_is_optional=False)
1741
        self._run()