Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / pithos_cli.py @ 1f5debf7

History | View | Annotate | Download (60.4 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 user account:     /config get store.account',
84
            '           or              /config get account',
85
            '  to set the user account: /config set store.account <account>',
86
            ' ',
87
            '  to get authentication token: /config get token',
88
            '  to set authentication token: /config set token <token>'
89
            ])
90
    elif e.status == 413:
91
        raiseCLIError(e, details=[
92
            'Get quotas:',
93
            '- total quota:      /store quota',
94
            '- container quota:  /store quota <container>',
95
            'Users shall set a higher container quota, if available:',
96
            '-                  /store setquota <quota>[unit] <container>'
97
            ])
98

    
99

    
100
class DelimiterArgument(ValueArgument):
101
    """
102
    :value type: string
103
    :value returns: given string or /
104
    """
105

    
106
    def __init__(self, caller_obj, help='', parsed_name=None, default=None):
107
        super(DelimiterArgument, self).__init__(help, parsed_name, default)
108
        self.caller_obj = caller_obj
109

    
110
    @property
111
    def value(self):
112
        if self.caller_obj['recursive']:
113
            return '/'
114
        return getattr(self, '_value', self.default)
115

    
116
    @value.setter
117
    def value(self, newvalue):
118
        self._value = newvalue
119

    
120

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

    
130
    @property
131
    def value(self):
132
        return getattr(self, '_value', self.default)
133

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

    
160

    
161
class RangeArgument(ValueArgument):
162
    """
163
    :value type: string of the form <start>-<end> where <start> and <end> are
164
        integers
165
    :value returns: the input string, after type checking <start> and <end>
166
    """
167

    
168
    @property
169
    def value(self):
170
        return getattr(self, '_value', self.default)
171

    
172
    @value.setter
173
    def value(self, newvalue):
174
        if newvalue is None:
175
            self._value = self.default
176
            return
177
        (start, end) = newvalue.split('-')
178
        (start, end) = (int(start), int(end))
179
        self._value = '%s-%s' % (start, end)
180

    
181
# Command specs
182

    
183

    
184
class _pithos_init(_command_init):
185
    """Initialize a pithos+ kamaki client"""
186

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

    
201
    def main(self):
202
        self._run()
203

    
204
    def _set_account(self):
205
        astakos = AstakosClient(self.config.get('astakos', 'url'), self.token)
206
        self.account = astakos.term('uuid')
207

    
208

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

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

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

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

    
227

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

    
231
    container = None
232
    path = None
233

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

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

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

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

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

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

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

    
321

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

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

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

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

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

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

    
449

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

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

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

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

    
468

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

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

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

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

    
494

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

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

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

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

    
525

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

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

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

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

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

    
610

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

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

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

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

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

    
689

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

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

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

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

    
726

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

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

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

    
743

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

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

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

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

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

    
798

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

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

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

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

    
850

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

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

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

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

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

    
951

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

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

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

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

    
994

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

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

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

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

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

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

    
1121

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

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

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

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

    
1162

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

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

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

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

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

    
1225

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

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

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

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

    
1258

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

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

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

    
1277

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

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

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

    
1295

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

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

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

    
1318

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

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

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

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

    
1367

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

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

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

    
1387

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

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

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

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

    
1421

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

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

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

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

    
1475

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

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

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

    
1498

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

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

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

    
1524

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

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

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

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

    
1551

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

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

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

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

    
1602

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

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

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

    
1622

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

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

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

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

    
1647

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

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

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

    
1662

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

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

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

    
1679

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

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

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

    
1693

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

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

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

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

    
1714

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

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

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