Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / pithos_cli.py @ 7147e1ca

History | View | Annotate | Download (75.2 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 kamaki.cli import command
35
from kamaki.cli.command_tree import CommandTree
36
from kamaki.cli.errors import raiseCLIError, CLISyntaxError
37
from kamaki.cli.utils import (
38
    format_size,
39
    print_dict,
40
    pretty_keys,
41
    page_hold,
42
    ask_user)
43
from kamaki.cli.argument import FlagArgument, ValueArgument, IntArgument
44
from kamaki.cli.argument import KeyValueArgument, DateArgument
45
from kamaki.cli.argument import ProgressBarArgument
46
from kamaki.cli.commands import _command_init
47
from kamaki.clients.pithos import PithosClient, ClientError
48
from kamaki.cli.utils import bold
49
from sys import stdout
50
from time import localtime, strftime
51
from logging import getLogger
52
from os import path
53

    
54
kloger = getLogger('kamaki')
55

    
56
pithos_cmds = CommandTree('store', 'Pithos+ storage commands')
57
_commands = [pithos_cmds]
58

    
59

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

    
67

    
68
# Argument functionality
69

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

    
94

    
95
def check_range(start, end):
96
    """
97
    :param start: (int)
98

99
    :param end: (int)
100

101
    :returns: (int(start), int(end))
102

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

    
118

    
119
class DelimiterArgument(ValueArgument):
120
    """
121
    :value type: string
122
    :value returns: given string or /
123
    """
124

    
125
    def __init__(self, caller_obj, help='', parsed_name=None, default=None):
126
        super(DelimiterArgument, self).__init__(help, parsed_name, default)
127
        self.caller_obj = caller_obj
128

    
129
    @property
130
    def value(self):
131
        if self.caller_obj['recursive']:
132
            return '/'
133
        return getattr(self, '_value', self.default)
134

    
135
    @value.setter
136
    def value(self, newvalue):
137
        self._value = newvalue
138

    
139

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

    
149
    @property
150
    def value(self):
151
        return getattr(self, '_value', self.default)
152

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

    
179

    
180
class RangeArgument(ValueArgument):
181
    """
182
    :value type: string of the form <start>-<end> where <start> and <end> are
183
        integers
184
    :value returns: the input string, after type checking <start> and <end>
185
    """
186

    
187
    @property
188
    def value(self):
189
        return getattr(self, '_value', self.default)
190

    
191
    @value.setter
192
    def value(self, newvalue):
193
        if newvalue is None:
194
            self._value = self.default
195
            return
196
        (start, end) = newvalue.split('-')
197
        (start, end) = (int(start), int(end))
198
        self._value = '%s-%s' % (start, end)
199

    
200
# Command specs
201

    
202

    
203
class _pithos_init(_command_init):
204
    """Initialize a pithos+ kamaki client"""
205

    
206
    def main(self):
207
        self.token = self.config.get('store', 'token')\
208
            or self.config.get('global', 'token')
209
        self.base_url = self.config.get('store', 'url')\
210
            or self.config.get('global', 'url')
211
        self.account = self.config.get('store', 'account')\
212
            or self.config.get('global', 'account')
213
        self.container = self.config.get('store', 'container')\
214
            or self.config.get('global', 'container')
215
        self.client = PithosClient(base_url=self.base_url,
216
            token=self.token,
217
            account=self.account,
218
            container=self.container)
219

    
220

    
221
class _store_account_command(_pithos_init):
222
    """Base class for account level storage commands"""
223

    
224
    def __init__(self, arguments={}):
225
        super(_store_account_command, self).__init__(arguments)
226
        self['account'] = ValueArgument(
227
            'Set user account (not permanent)',
228
            '--account')
229

    
230
    def main(self):
231
        super(_store_account_command, self).main()
232
        if self['account']:
233
            self.client.account = self['account']
234

    
235

    
236
class _store_container_command(_store_account_command):
237
    """Base class for container level storage commands"""
238

    
239
    generic_err_details = ['To specify a container:',
240
    '  1. Set store.container variable (permanent)',
241
    '     /config set store.container <container>',
242
    '  2. --container=<container> (temporary, overrides 1)',
243
    '  3. Use the container:path format (temporary, overrides all)']
244

    
245
    container = None
246
    path = None
247

    
248
    def __init__(self, arguments={}):
249
        super(_store_container_command, self).__init__(arguments)
250
        self['container'] = ValueArgument(
251
            'Set container to work with (temporary)',
252
            '--container')
253

    
254
    def extract_container_and_path(self,
255
        container_with_path,
256
        path_is_optional=True):
257
        try:
258
            assert isinstance(container_with_path, str)
259
        except AssertionError as err:
260
            raiseCLIError(err)
261

    
262
        cont, sep, path = container_with_path.partition(':')
263

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

    
296
    def main(self, container_with_path=None, path_is_optional=True):
297
        super(_store_container_command, self).main()
298
        if container_with_path is not None:
299
            self.extract_container_and_path(
300
                container_with_path,
301
                path_is_optional)
302
            self.client.container = self.container
303
        elif self['container']:
304
            self.client.container = self['container']
305
        self.container = self.client.container
306

    
307

    
308
@command(pithos_cmds)
309
class store_list(_store_container_command):
310
    """List containers, object trees or objects in a directory
311
    Use with:
312
    1 no parameters : containers in set account
313
    2. one parameter (container) or --container : contents of container
314
    3. <container>:<prefix> or --container=<container> <prefix>: objects in
315
    .   container starting with prefix
316
    """
317

    
318
    arguments = dict(
319
        detail=FlagArgument('show detailed output', '-l'),
320
        limit=IntArgument('limit the number of listed items', '-n'),
321
        marker=ValueArgument('show output greater that marker', '--marker'),
322
        prefix=ValueArgument('show output starting with prefix', '--prefix'),
323
        delimiter=ValueArgument('show output up to delimiter', '--delimiter'),
324
        path=ValueArgument(
325
            'show output starting with prefix up to /',
326
            '--path'),
327
        meta=ValueArgument(
328
            'show output with specified meta keys',
329
            '--meta',
330
            default=[]),
331
        if_modified_since=ValueArgument(
332
            'show output modified since then',
333
            '--if-modified-since'),
334
        if_unmodified_since=ValueArgument(
335
            'show output not modified since then',
336
            '--if-unmodified-since'),
337
        until=DateArgument('show metadata until then', '--until'),
338
        format=ValueArgument(
339
            'format to parse until data (default: d/m/Y H:M:S )',
340
            '--format'),
341
        shared=FlagArgument('show only shared', '--shared'),
342
        public=FlagArgument('show only public', '--public'),
343
        more=FlagArgument(
344
            'output results in pages (-n to set items per page, default 10)',
345
            '--more'),
346
        exact_match=FlagArgument(
347
            'Show only objects that match exactly with path',
348
            '--exact-match')
349
    )
350

    
351
    def print_objects(self, object_list):
352
        limit = int(self['limit']) if self['limit'] > 0 else len(object_list)
353
        for index, obj in enumerate(object_list):
354
            if (self['exact_match'] and self.path and\
355
                obj['name'] != self.path) or 'content_type' not in obj:
356
                continue
357
            pretty_obj = obj.copy()
358
            index += 1
359
            empty_space = ' ' * (len(str(len(object_list))) - len(str(index)))
360
            if obj['content_type'] == 'application/directory':
361
                isDir = True
362
                size = 'D'
363
            else:
364
                isDir = False
365
                size = format_size(obj['bytes'])
366
                pretty_obj['bytes'] = '%s (%s)' % (obj['bytes'], size)
367
            oname = bold(obj['name'])
368
            if self['detail']:
369
                print('%s%s. %s' % (empty_space, index, oname))
370
                print_dict(pretty_keys(pretty_obj), exclude=('name'))
371
                print
372
            else:
373
                oname = '%s%s. %6s %s' % (empty_space, index, size, oname)
374
                oname += '/' if isDir else ''
375
                print(oname)
376
            if self['more']:
377
                page_hold(index, limit, len(object_list))
378

    
379
    def print_containers(self, container_list):
380
        limit = int(self['limit']) if self['limit'] > 0\
381
            else len(container_list)
382
        for index, container in enumerate(container_list):
383
            if 'bytes' in container:
384
                size = format_size(container['bytes'])
385
            cname = '%s. %s' % (index + 1, bold(container['name']))
386
            if self['detail']:
387
                print(cname)
388
                pretty_c = container.copy()
389
                if 'bytes' in container:
390
                    pretty_c['bytes'] = '%s (%s)' % (container['bytes'], size)
391
                print_dict(pretty_keys(pretty_c), exclude=('name'))
392
                print
393
            else:
394
                if 'count' in container and 'bytes' in container:
395
                    print('%s (%s, %s objects)'\
396
                    % (cname, size, container['count']))
397
                else:
398
                    print(cname)
399
            if self['more']:
400
                page_hold(index + 1, limit, len(container_list))
401

    
402
    def main(self, container____path__=None):
403
        super(self.__class__, self).main(container____path__)
404
        try:
405
            if self.container is None:
406
                r = self.client.account_get(
407
                    limit=False if self['more'] else self['limit'],
408
                    marker=self['marker'],
409
                    if_modified_since=self['if_modified_since'],
410
                    if_unmodified_since=self['if_unmodified_since'],
411
                    until=self['until'],
412
                    show_only_shared=self['shared'])
413
                self.print_containers(r.json)
414
            else:
415
                prefix = self.path if self.path else self['prefix']
416
                r = self.client.container_get(
417
                    limit=False if self['more'] else self['limit'],
418
                    marker=self['marker'],
419
                    prefix=prefix,
420
                    delimiter=self['delimiter'],
421
                    path=self['path'],
422
                    if_modified_since=self['if_modified_since'],
423
                    if_unmodified_since=self['if_unmodified_since'],
424
                    until=self['until'],
425
                    meta=self['meta'],
426
                    show_only_shared=self['shared'])
427
                self.print_objects(r.json)
428
        except ClientError as err:
429
            if err.status == 404:
430
                if 'container' in ('%s' % err).lower():
431
                    raiseCLIError(
432
                        err,
433
                        'No container %s in account %s'\
434
                        % (self.container, self.account),
435
                        details=self.generic_err_details)
436
                elif 'object' in ('%s' % err).lower():
437
                    raiseCLIError(
438
                        err,
439
                        'No object %s in %s\'s container %s'\
440
                        % (self.path, self.account, self.container),
441
                        details=self.generic_err_details)
442
            raise_connection_errors(err)
443
            raiseCLIError(err)
444
        except Exception as e:
445
            raiseCLIError(e)
446

    
447

    
448
@command(pithos_cmds)
449
class store_mkdir(_store_container_command):
450
    """Create a directory"""
451

    
452
    __doc__ += '\n. '.join(about_directories)
453

    
454
    def main(self, container___directory):
455
        super(self.__class__,
456
            self).main(container___directory, path_is_optional=False)
457
        try:
458
            self.client.create_directory(self.path)
459
        except ClientError as err:
460
            if err.status == 404:
461
                if 'container' in ('%s' % err).lower():
462
                    raiseCLIError(
463
                        err,
464
                        'No container %s in account %s'\
465
                        % (self.container, self.account),
466
                        details=self.generic_err_details)
467
                elif 'object' in ('%s' % err).lower():
468
                    raiseCLIError(
469
                        err,
470
                        'No object %s in container %s'\
471
                        % (self.path, self.container),
472
                        details=self.generic_err_details)
473
            raise_connection_errors(err)
474
            raiseCLIError(err)
475
        except Exception as err:
476
            raiseCLIError(err)
477

    
478

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

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

    
492
    def main(self, container___path):
493
        super(store_touch, self).main(container___path)
494
        try:
495
            self.client.create_object(self.path, self['content_type'])
496
        except ClientError as err:
497
            if err.status == 404:
498
                if 'container' in ('%s' % err).lower():
499
                    raiseCLIError(
500
                        err,
501
                        'No container %s in account %s'\
502
                        % (self.container, self.account),
503
                        details=self.generic_err_details)
504
                elif 'object' in ('%s' % err).lower():
505
                    raiseCLIError(
506
                        err,
507
                        'No object %s in container %s'\
508
                        % (self.path, self.container),
509
                        details=self.generic_err_details)
510
            raise_connection_errors(err)
511
            raiseCLIError(err)
512
        except Exception as err:
513
            raiseCLIError(err)
514

    
515

    
516
@command(pithos_cmds)
517
class store_create(_store_container_command):
518
    """Create a container"""
519

    
520
    arguments = dict(
521
        versioning=ValueArgument(
522
            'set container versioning (auto/none)',
523
            '--versioning'),
524
        quota=IntArgument('set default container quota', '--quota'),
525
        meta=KeyValueArgument(
526
            'set container metadata (can be repeated)',
527
            '--meta')
528
    )
529

    
530
    def main(self, container):
531
        super(self.__class__, self).main(container)
532
        try:
533
            self.client.container_put(quota=self['quota'],
534
                versioning=self['versioning'],
535
                metadata=self['meta'])
536
        except ClientError as err:
537
            if err.status == 404:
538
                if 'container' in ('%s' % err).lower():
539
                    raiseCLIError(
540
                        err,
541
                        'No container %s in account %s'\
542
                        % (self.container, self.account),
543
                        details=self.generic_err_details)
544
                elif 'object' in ('%s' % err).lower():
545
                    raiseCLIError(
546
                        err,
547
                        'No object %s in container %s'\
548
                        % (self.path, self.container),
549
                        details=self.generic_err_details)
550
            raise_connection_errors(err)
551
            raiseCLIError(err)
552
        except Exception as e:
553
            raiseCLIError(e)
554

    
555

    
556
@command(pithos_cmds)
557
class store_copy(_store_container_command):
558
    """Copy objects from container to (another) container
559
    Semantics:
560
    copy cont:path path2
561
    .   will copy all <obj> prefixed with path, as path2<obj>
562
    .   or as path2 if path corresponds to just one whole object
563
    copy cont:path cont2:
564
    .   will copy all <obj> prefixed with path to container cont2
565
    copy cont:path [cont2:]path2 --exact-match
566
    .   will copy at most one <obj> as a new object named path2,
567
    .   provided path corresponds to a whole object path
568
    copy cont:path [cont2:]path2 --replace
569
    .   will copy all <obj> prefixed with path, replacing path with path2
570
    where <obj> is a full file or directory object path.
571
    Use options:
572
    1. <container1>:<path1> [container2:]<path2> : if container2 is not given,
573
    destination is container1:path2
574
    2. <container>:<path1> <path2> : make a copy in the same container
575
    3. Can use --container= instead of <container1>
576
    """
577

    
578
    arguments = dict(
579
        source_version=ValueArgument(
580
            'copy specific version',
581
            '--source-version'),
582
        public=ValueArgument('make object publicly accessible', '--public'),
583
        content_type=ValueArgument(
584
            'change object\'s content type',
585
            '--content-type'),
586
        recursive=FlagArgument(
587
            'mass copy with delimiter /',
588
            ('-r', '--recursive')),
589
        exact_match=FlagArgument(
590
            'Copy only the object that fully matches path',
591
            '--exact-match'),
592
        replace=FlagArgument('Replace src. path with dst. path', '--replace')
593
    )
594

    
595
    def _objlist(self, dst_path):
596
        if self['exact_match']:
597
            return [(dst_path if dst_path else self.path, self.path)]
598
        r = self.client.container_get(prefix=self.path)
599
        if len(r.json) == 1:
600
            obj = r.json[0]
601
            return [(obj['name'], dst_path if dst_path else obj['name'])]
602
        return [(obj['name'], '%s%s' % (
603
                    dst_path,
604
                    obj['name'][len(self.path) if self['replace'] else 0:])
605
                ) for obj in r.json]
606

    
607
    def main(self, source_container___path, destination_container___path):
608
        super(self.__class__,
609
            self).main(source_container___path, path_is_optional=False)
610
        try:
611
            dst = destination_container___path.split(':')
612
            (dst_cont, dst_path) = (dst[0], dst[1])\
613
            if len(dst) > 1 else (None, dst[0])
614
            for src_object, dst_object in self._objlist(dst_path):
615
                self.client.copy_object(src_container=self.container,
616
                    src_object=src_object,
617
                    dst_container=dst_cont if dst_cont else self.container,
618
                    dst_object=dst_object,
619
                    source_version=self['source_version'],
620
                    public=self['public'],
621
                    content_type=self['content_type'])
622
        except ClientError as err:
623
            if err.status == 404:
624
                if 'object' in ('%s' % err).lower():
625
                    raiseCLIError(
626
                        err,
627
                        'No object %s in container %s'\
628
                        % (self.path, self.container),
629
                        details=self.generic_err_details)
630
                elif 'container' in ('%s' % err).lower():
631
                    cont_msg = '(either %s or %s)' % (
632
                        self.container,
633
                        dst_cont
634
                    ) if dst_cont else self.container
635
                    raiseCLIError(
636
                        err,
637
                        'No container %s in account %s'\
638
                        % (cont_msg, self.account),
639
                        details=self.generic_err_details)
640
            raise_connection_errors(err)
641
            raiseCLIError(err)
642
        except Exception as e:
643
            raiseCLIError(e)
644

    
645

    
646
@command(pithos_cmds)
647
class store_move(_store_container_command):
648
    """Move/rename objects
649
    Semantics:
650
    move cont:path path2
651
    .   will move all <obj> prefixed with path, as path2<obj>
652
    .   or as path2 if path corresponds to just one whole object
653
    move cont:path cont2:
654
    .   will move all <obj> prefixed with path to container cont2
655
    move cont:path [cont2:]path2 --exact-match
656
    .   will move at most one <obj> as a new object named path2,
657
    .   provided path corresponds to a whole object path
658
    move cont:path [cont2:]path2 --replace
659
    .   will move all <obj> prefixed with path, replacing path with path2
660
    where <obj> is a full file or directory object path.
661
    Use options:
662
    1. <container1>:<path1> [container2:]<path2> : if container2 not given,
663
    destination is container1:path2
664
    2. <container>:<path1> path2 : rename
665
    3. Can use --container= instead of <container1>
666
    """
667

    
668
    arguments = dict(
669
        source_version=ValueArgument('specify version', '--source-version'),
670
        public=FlagArgument('make object publicly accessible', '--public'),
671
        content_type=ValueArgument('modify content type', '--content-type'),
672
        recursive=FlagArgument('up to delimiter /', ('-r', '--recursive')),
673
        exact_match=FlagArgument(
674
            'Copy only the object that fully matches path',
675
            '--exact-match'),
676
        replace=FlagArgument('Replace src. path with dst. path', '--replace')
677
    )
678

    
679
    def _objlist(self, dst_path):
680
        if self['exact_match']:
681
            return [(dst_path if dst_path else self.path, self.path)]
682
        r = self.client.container_get(prefix=self.path)
683
        if len(r.json) == 1:
684
            obj = r.json[0]
685
            return [(obj['name'], dst_path if dst_path else obj['name'])]
686
        return [(obj['name'], '%s%s' % (
687
                    dst_path,
688
                    obj['name'][len(self.path) if self['replace'] else 0:])
689
                ) for obj in r.json]
690

    
691
    def main(self, source_container___path, destination_container____path__):
692
        super(self.__class__,
693
            self).main(source_container___path, path_is_optional=False)
694
        try:
695
            dst = destination_container____path__.split(':')
696
            (dst_cont, dst_path) = (dst[0], dst[1])\
697
            if len(dst) > 1 else (None, dst[0])
698
            for src_object, dst_object in self._objlist(dst_path):
699
                self.client.move_object(
700
                    src_container=self.container,
701
                    src_object=src_object,
702
                    dst_container=dst_cont if dst_cont else self.container,
703
                    dst_object=dst_object,
704
                    source_version=self['source_version'],
705
                    public=self['public'],
706
                    content_type=self['content_type'])
707
        except ClientError as err:
708
            if err.status == 404:
709
                if 'object' in ('%s' % err).lower():
710
                    raiseCLIError(
711
                        err,
712
                        'No object %s in container %s'\
713
                        % (self.path, self.container),
714
                        details=self.generic_err_details)
715
                elif 'container' in ('%s' % err).lower():
716
                    cont_msg = '(either %s or %s)' % (
717
                        self.container,
718
                        dst_cont
719
                    ) if dst_cont else self.container
720
                    raiseCLIError(
721
                        err,
722
                        'No container %s in account %s'\
723
                        % (cont_msg, self.account),
724
                        details=self.generic_err_details)
725
            raise_connection_errors(err)
726
            raiseCLIError(err)
727
        except Exception as e:
728
            raiseCLIError(e)
729

    
730

    
731
@command(pithos_cmds)
732
class store_append(_store_container_command):
733
    """Append local file to (existing) remote object
734
    The remote object should exist.
735
    If the remote object is a directory, it is transformed into a file.
736
    In the later case, objects under the directory remain intact.
737
    """
738

    
739
    arguments = dict(
740
        progress_bar=ProgressBarArgument(
741
            'do not show progress bar',
742
            '--no-progress-bar',
743
            default=False)
744
    )
745

    
746
    def main(self, local_path, container___path):
747
        super(self.__class__, self).main(
748
            container___path,
749
            path_is_optional=False)
750
        progress_bar = self.arguments['progress_bar']
751
        try:
752
            upload_cb = progress_bar.get_generator('Appending blocks')
753
        except Exception:
754
            upload_cb = None
755
        try:
756
            f = open(local_path, 'rb')
757
            self.client.append_object(self.path, f, upload_cb)
758
        except ClientError as err:
759
            progress_bar.finish()
760
            if err.status == 404:
761
                if 'container' in ('%s' % err).lower():
762
                    raiseCLIError(
763
                        err,
764
                        'No container %s in account %s'\
765
                        % (self.container, self.account),
766
                        details=self.generic_err_details)
767
                elif 'object' in ('%s' % err).lower():
768
                    raiseCLIError(
769
                        err,
770
                        'No object %s in container %s'\
771
                        % (self.path, self.container),
772
                        details=self.generic_err_details)
773
            raise_connection_errors(err)
774
            raiseCLIError(err)
775
        except Exception as e:
776
            progress_bar.finish()
777
            raiseCLIError(e)
778
        finally:
779
            progress_bar.finish()
780

    
781

    
782
@command(pithos_cmds)
783
class store_truncate(_store_container_command):
784
    """Truncate remote file up to a size (default is 0)"""
785

    
786
    def main(self, container___path, size=0):
787
        super(self.__class__, self).main(container___path)
788
        try:
789
            self.client.truncate_object(self.path, size)
790
        except ClientError as err:
791
            if err.status == 404:
792
                if 'container' in ('%s' % err).lower():
793
                    raiseCLIError(
794
                        err,
795
                        'No container %s in account %s'\
796
                        % (self.container, self.account),
797
                        details=self.generic_err_details)
798
                elif 'object' in ('%s' % err).lower():
799
                    raiseCLIError(
800
                        err,
801
                        'No object %s in container %s'\
802
                        % (self.path, self.container),
803
                        details=self.generic_err_details)
804
            if err.status == 400 and\
805
                'object length is smaller than range length'\
806
                in ('%s' % err).lower():
807
                raiseCLIError(err, 'Object %s:%s <= %sb' % (
808
                    self.container,
809
                    self.path,
810
                    size))
811
            raise_connection_errors(err)
812
            raiseCLIError(err)
813
        except Exception as e:
814
            raiseCLIError(e)
815

    
816

    
817
@command(pithos_cmds)
818
class store_overwrite(_store_container_command):
819
    """Overwrite part (from start to end) of a remote file
820
    overwrite local-path container 10 20
821
    .   will overwrite bytes from 10 to 20 of a remote file with the same name
822
    .   as local-path basename
823
    overwrite local-path container:path 10 20
824
    .   will overwrite as above, but the remote file is named path
825
    """
826

    
827
    arguments = dict(
828
        progress_bar=ProgressBarArgument(
829
            'do not show progress bar',
830
            '--no-progress-bar',
831
            default=False)
832
    )
833

    
834
    def main(self, local_path, container____path__, start, end):
835
        (start, end) = check_range(start, end)
836
        super(self.__class__, self).main(container____path__)
837
        try:
838
            f = open(local_path, 'rb')
839
            f.seek(0, 2)
840
            f_size = f.tell()
841
            f.seek(start, 0)
842
        except Exception as e:
843
            raiseCLIError(e)
844
        progress_bar = self.arguments['progress_bar']
845
        try:
846
            upload_cb = progress_bar.get_generator(
847
                'Overwriting %s blocks' % end - start)
848
        except Exception:
849
            upload_cb = None
850
        try:
851
            self.path = self.path if self.path else path.basename(local_path)
852
            self.client.overwrite_object(
853
                obj=self.path,
854
                start=start,
855
                end=end,
856
                source_file=f,
857
                upload_cb=upload_cb)
858
        except ClientError as err:
859
            progress_bar.finish()
860
            if (err.status == 400 and\
861
            'content length does not match range' in ('%s' % err).lower()) or\
862
            err.status == 416:
863
                raiseCLIError(err, details=[
864
                    'Content size: %s' % f_size,
865
                    'Range: %s-%s (size: %s)' % (start, end, end - start)])
866
            elif err.status == 404:
867
                if 'container' in ('%s' % err).lower():
868
                    raiseCLIError(
869
                        err,
870
                        'No container %s in account %s'\
871
                        % (self.container, self.account),
872
                        details=self.generic_err_details)
873
                elif 'object' in ('%s' % err).lower():
874
                    raiseCLIError(
875
                        err,
876
                        'No object %s in container %s'\
877
                        % (self.path, self.container),
878
                        details=self.generic_err_details)
879
            raise_connection_errors(err)
880
            raiseCLIError(err)
881
        except Exception as e:
882
            progress_bar.finish()
883
            raiseCLIError(e)
884
        finally:
885
            progress_bar.finish()
886

    
887

    
888
@command(pithos_cmds)
889
class store_manifest(_store_container_command):
890
    """Create a remote file of uploaded parts by manifestation
891
    Remains functional for compatibility with OOS Storage. Users are advised
892
    to use the upload command instead.
893
    Manifestation is a compliant process for uploading large files. The files
894
    have to be chunked in smalled files and uploaded as <prefix><increment>
895
    where increment is 1, 2, ...
896
    Finally, the manifest command glues partial files together in one file
897
    named <prefix>
898
    The upload command is faster, easier and more intuitive than manifest
899
    """
900

    
901
    arguments = dict(
902
        etag=ValueArgument('check written data', '--etag'),
903
        content_encoding=ValueArgument(
904
            'set MIME content type',
905
            '--content-encoding'),
906
        content_disposition=ValueArgument(
907
            'the presentation style of the object',
908
            '--content-disposition'),
909
        content_type=ValueArgument(
910
            'specify content type',
911
            '--content-type',
912
            default='application/octet-stream'),
913
        sharing=SharingArgument(
914
            'define object sharing policy \n' +\
915
            '    ( "read=user1,grp1,user2,... write=user1,grp2,..." )',
916
            '--sharing'),
917
        public=FlagArgument('make object publicly accessible', '--public')
918
    )
919

    
920
    def main(self, container___path):
921
        super(self.__class__,
922
            self).main(container___path, path_is_optional=False)
923
        try:
924
            self.client.create_object_by_manifestation(
925
                self.path,
926
                content_encoding=self['content_encoding'],
927
                content_disposition=self['content_disposition'],
928
                content_type=self['content_type'],
929
                sharing=self['sharing'],
930
                public=self['public'])
931
        except ClientError as err:
932
            if err.status == 404:
933
                if 'container' in ('%s' % err).lower():
934
                    raiseCLIError(
935
                        err,
936
                        'No container %s in account %s'\
937
                        % (self.container, self.account),
938
                        details=self.generic_err_details)
939
                elif 'object' in ('%s' % err).lower():
940
                    raiseCLIError(
941
                        err,
942
                        'No object %s in container %s'\
943
                        % (self.path, self.container),
944
                        details=self.generic_err_details)
945
            raise_connection_errors(err)
946
            raiseCLIError(err)
947
        except Exception as e:
948
            raiseCLIError(e)
949

    
950

    
951
@command(pithos_cmds)
952
class store_upload(_store_container_command):
953
    """Upload a file"""
954

    
955
    arguments = dict(
956
        use_hashes=FlagArgument(
957
            'provide hashmap file instead of data',
958
            '--use-hashes'),
959
        etag=ValueArgument('check written data', '--etag'),
960
        unchunked=FlagArgument('avoid chunked transfer mode', '--unchunked'),
961
        content_encoding=ValueArgument(
962
            'set MIME content type',
963
            '--content-encoding'),
964
        content_disposition=ValueArgument(
965
            'specify objects presentation style',
966
            '--content-disposition'),
967
        content_type=ValueArgument('specify content type', '--content-type'),
968
        sharing=SharingArgument(
969
            help='define sharing object policy \n' +\
970
            '( "read=user1,grp1,user2,... write=user1,grp2,... )',
971
            parsed_name='--sharing'),
972
        public=FlagArgument('make object publicly accessible', '--public'),
973
        poolsize=IntArgument('set pool size', '--with-pool-size'),
974
        progress_bar=ProgressBarArgument(
975
            'do not show progress bar',
976
            '--no-progress-bar',
977
            default=False),
978
        overwrite=FlagArgument('Force overwrite, if object exists', '-f')
979
    )
980

    
981
    def _remote_path(self, remote_path, local_path=''):
982
        if self['overwrite']:
983
            return remote_path
984
        try:
985
            r = self.client.get_object_info(remote_path)
986
        except ClientError as ce:
987
            if ce.status == 404:
988
                return remote_path
989
            raise ce
990
        ctype = r.get('content-type', '')
991
        if 'application/directory' == ctype.lower():
992
            ret = '%s/%s' % (remote_path, local_path)
993
            return self._remote_path(ret) if local_path else ret
994
        raiseCLIError(
995
            'Object %s already exists' % remote_path,
996
            importance=1,
997
            details=['use -f to overwrite or resume'])
998

    
999
    def main(self, local_path, container____path__=None):
1000
        super(self.__class__, self).main(container____path__)
1001
        remote_path = self.path if self.path else path.basename(local_path)
1002
        poolsize = self['poolsize']
1003
        if poolsize > 0:
1004
            self.client.POOL_SIZE = int(poolsize)
1005
        params = dict(
1006
            content_encoding=self['content_encoding'],
1007
            content_type=self['content_type'],
1008
            content_disposition=self['content_disposition'],
1009
            sharing=self['sharing'],
1010
            public=self['public'])
1011
        try:
1012
            progress_bar = self.arguments['progress_bar']
1013
            hash_bar = progress_bar.clone()
1014
            remote_path = self._remote_path(remote_path, local_path)
1015
            with open(path.abspath(local_path), 'rb') as f:
1016
                if self['unchunked']:
1017
                    self.client.upload_object_unchunked(
1018
                        remote_path,
1019
                        f,
1020
                        etag=self['etag'],
1021
                        withHashFile=self['use_hashes'],
1022
                        **params)
1023
                else:
1024
                    hash_cb = hash_bar.get_generator(
1025
                        'Calculating block hashes')
1026
                    upload_cb = progress_bar.get_generator('Uploading')
1027
                    self.client.upload_object(
1028
                        remote_path,
1029
                        f,
1030
                        hash_cb=hash_cb,
1031
                        upload_cb=upload_cb,
1032
                        **params)
1033
                    progress_bar.finish()
1034
                    hash_bar.finish()
1035
        except ClientError as err:
1036
            try:
1037
                progress_bar.finish()
1038
                hash_bar.finish()
1039
            except Exception:
1040
                pass
1041
            if err.status == 404:
1042
                if 'container' in ('%s' % err).lower():
1043
                    raiseCLIError(
1044
                        err,
1045
                        'No container %s in account %s'\
1046
                        % (self.container, self.account),
1047
                        details=[self.generic_err_details])
1048
            elif err.status == 800:
1049
                raiseCLIError(err, details=[
1050
                    'Possible cause: temporary server failure',
1051
                    'Try to re-upload the file',
1052
                    'For more error details, try kamaki store upload -d'])
1053
            raise_connection_errors(err)
1054
            raiseCLIError(err, 'Failed to upload to %s' % container____path__)
1055
        except IOError as err:
1056
            try:
1057
                progress_bar.finish()
1058
                hash_bar.finish()
1059
            except Exception:
1060
                pass
1061
            raiseCLIError(err, 'Failed to read form file %s' % local_path, 2)
1062
        except Exception as e:
1063
            try:
1064
                progress_bar.finish()
1065
                hash_bar.finish()
1066
            except Exception:
1067
                pass
1068
            raiseCLIError(e)
1069
        print 'Upload completed'
1070

    
1071

    
1072
@command(pithos_cmds)
1073
class store_cat(_store_container_command):
1074
    """Print remote file contents to console"""
1075

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

    
1093
    def main(self, container___path):
1094
        super(self.__class__, self).main(
1095
            container___path,
1096
            path_is_optional=False)
1097
        try:
1098
            self.client.download_object(self.path, stdout,
1099
            range=self['range'],
1100
            version=self['object_version'],
1101
            if_match=self['if_match'],
1102
            if_none_match=self['if_none_match'],
1103
            if_modified_since=self['if_modified_since'],
1104
            if_unmodified_since=self['if_unmodified_since'])
1105
        except ClientError as err:
1106
            if err.status == 404:
1107
                if 'container' in ('%s' % err).lower():
1108
                    raiseCLIError(
1109
                        err,
1110
                        'No container %s in account %s'\
1111
                        % (self.container, self.account),
1112
                        details=self.generic_err_details)
1113
                elif 'object' in ('%s' % err).lower():
1114
                    raiseCLIError(
1115
                        err,
1116
                        'No object %s in container %s'\
1117
                        % (self.path, self.container),
1118
                        details=self.generic_err_details)
1119
            raise_connection_errors(err)
1120
            raiseCLIError(err)
1121
        except Exception as e:
1122
            raiseCLIError(e)
1123

    
1124

    
1125
@command(pithos_cmds)
1126
class store_download(_store_container_command):
1127
    """Download remote object as local file"""
1128

    
1129
    arguments = dict(
1130
        resume=FlagArgument('Resume instead of overwrite', '--resume'),
1131
        range=RangeArgument('show range of data', '--range'),
1132
        if_match=ValueArgument('show output if ETags match', '--if-match'),
1133
        if_none_match=ValueArgument(
1134
            'show output if ETags match',
1135
            '--if-none-match'),
1136
        if_modified_since=DateArgument(
1137
            'show output modified since then',
1138
            '--if-modified-since'),
1139
        if_unmodified_since=DateArgument(
1140
            'show output unmodified since then',
1141
            '--if-unmodified-since'),
1142
        object_version=ValueArgument(
1143
            'get the specific version',
1144
            '--object-version'),
1145
        poolsize=IntArgument('set pool size', '--with-pool-size'),
1146
        progress_bar=ProgressBarArgument(
1147
            'do not show progress bar',
1148
            '--no-progress-bar',
1149
            default=False)
1150
    )
1151

    
1152
    def _output_stream(self, local_path):
1153
        if local_path is None:
1154
            return stdout
1155
        try:
1156
            return open(
1157
                path.abspath(local_path),
1158
                'rwb+' if self['resume'] else 'wb+')
1159
        except IOError as err:
1160
            raiseCLIError(err, 'Cannot write to file %s' % local_path, 1)
1161

    
1162
    def main(self, container___path, local_path=None):
1163
        super(self.__class__, self).main(
1164
            container___path,
1165
            path_is_optional=False)
1166

    
1167
        out = self._output_stream(local_path)
1168
        poolsize = self['poolsize']
1169
        if poolsize is not None:
1170
            self.client.POOL_SIZE = int(poolsize)
1171

    
1172
        try:
1173
            progress_bar = self.arguments['progress_bar']
1174
            download_cb = progress_bar.get_generator('Downloading')
1175
            self.client.download_object(
1176
                self.path,
1177
                out,
1178
                download_cb=download_cb,
1179
                range=self['range'],
1180
                version=self['object_version'],
1181
                if_match=self['if_match'],
1182
                resume=self['resume'],
1183
                if_none_match=self['if_none_match'],
1184
                if_modified_since=self['if_modified_since'],
1185
                if_unmodified_since=self['if_unmodified_since'])
1186
            progress_bar.finish()
1187
        except ClientError as err:
1188
            progress_bar.finish()
1189
            if err.status == 404:
1190
                if 'container' in ('%s' % err).lower():
1191
                    raiseCLIError(
1192
                        err,
1193
                        'No container %s in account %s'\
1194
                        % (self.container, self.account),
1195
                        details=self.generic_err_details)
1196
                elif 'object' in ('%s' % err).lower():
1197
                    raiseCLIError(
1198
                        err,
1199
                        'No object %s in container %s'\
1200
                        % (self.path, self.container),
1201
                        details=self.generic_err_details)
1202
            raise_connection_errors(err)
1203
            raiseCLIError(err, '"%s" not accessible' % container___path)
1204
        except IOError as err:
1205
            try:
1206
                progress_bar.finish()
1207
            except Exception:
1208
                pass
1209
            raiseCLIError(err, 'Failed to write on file %s' % local_path, 2)
1210
        except KeyboardInterrupt:
1211
            from threading import enumerate as activethreads
1212
            stdout.write('\nFinishing active threads ')
1213
            for thread in activethreads():
1214
                stdout.flush()
1215
                try:
1216
                    thread.join()
1217
                    stdout.write('.')
1218
                except RuntimeError:
1219
                    continue
1220
            try:
1221
                progress_bar.finish()
1222
            except Exception:
1223
                pass
1224
            print('\ndownload canceled by user')
1225
            if local_path is not None:
1226
                print('to resume, re-run with --resume')
1227
        except Exception as e:
1228
            try:
1229
                progress_bar.finish()
1230
            except Exception:
1231
                pass
1232
            raiseCLIError(e)
1233
        print
1234

    
1235

    
1236
@command(pithos_cmds)
1237
class store_hashmap(_store_container_command):
1238
    """Get the hash-map of an object"""
1239

    
1240
    arguments = dict(
1241
        if_match=ValueArgument('show output if ETags match', '--if-match'),
1242
        if_none_match=ValueArgument(
1243
            'show output if ETags match',
1244
            '--if-none-match'),
1245
        if_modified_since=DateArgument(
1246
            'show output modified since then',
1247
            '--if-modified-since'),
1248
        if_unmodified_since=DateArgument(
1249
            'show output unmodified since then',
1250
            '--if-unmodified-since'),
1251
        object_version=ValueArgument(
1252
            'get the specific version',
1253
            '--object-version')
1254
    )
1255

    
1256
    def main(self, container___path):
1257
        super(self.__class__, self).main(
1258
            container___path,
1259
            path_is_optional=False)
1260
        try:
1261
            data = self.client.get_object_hashmap(
1262
                self.path,
1263
                version=self['object_version'],
1264
                if_match=self['if_match'],
1265
                if_none_match=self['if_none_match'],
1266
                if_modified_since=self['if_modified_since'],
1267
                if_unmodified_since=self['if_unmodified_since'])
1268
        except ClientError as err:
1269
            if err.status == 404:
1270
                if 'container' in ('%s' % err).lower():
1271
                    raiseCLIError(
1272
                        err,
1273
                        'No container %s in account %s'\
1274
                        % (self.container, self.account),
1275
                        details=self.generic_err_details)
1276
                elif 'object' in ('%s' % err).lower():
1277
                    raiseCLIError(
1278
                        err,
1279
                        'No object %s in container %s'\
1280
                        % (self.path, self.container),
1281
                        details=self.generic_err_details)
1282
            raise_connection_errors(err)
1283
            raiseCLIError(err)
1284
        except Exception as e:
1285
            raiseCLIError(e)
1286
        print_dict(data)
1287

    
1288

    
1289
@command(pithos_cmds)
1290
class store_delete(_store_container_command):
1291
    """Delete a container [or an object]
1292
    How to delete a non-empty container:
1293
    - empty the container:  /store delete -r <container>
1294
    - delete it:            /store delete <container>
1295
    .
1296
    Semantics of directory deletion:
1297
    .a preserve the contents: /store delete <container>:<directory>
1298
    .    objects of the form dir/filename can exist with a dir object
1299
    .b delete contents:       /store delete -r <container>:<directory>
1300
    .    all dir/* objects are affected, even if dir does not exist
1301
    .
1302
    To restore a deleted object OBJ in a container CONT:
1303
    - get object versions: /store versions CONT:OBJ
1304
    .   and choose the version to be restored
1305
    - restore the object:  /store copy --source-version=<version> CONT:OBJ OBJ
1306
    """
1307

    
1308
    arguments = dict(
1309
        until=DateArgument('remove history until that date', '--until'),
1310
        yes=FlagArgument('Do not prompt for permission', '--yes'),
1311
        recursive=FlagArgument(
1312
            'empty dir or container and delete (if dir)',
1313
            ('-r', '--recursive'))
1314
    )
1315

    
1316
    def __init__(self, arguments={}):
1317
        super(self.__class__, self).__init__(arguments)
1318
        self['delimiter'] = DelimiterArgument(
1319
            self,
1320
            parsed_name='--delimiter',
1321
            help='delete objects prefixed with <object><delimiter>')
1322

    
1323
    def main(self, container____path__):
1324
        super(self.__class__, self).main(container____path__)
1325
        try:
1326
            if (not self.path):
1327
                if self['yes'] or ask_user(
1328
                    'Delete container %s ?' % self.container):
1329
                    self.client.del_container(
1330
                        until=self['until'],
1331
                        delimiter=self['delimiter'])
1332
            else:
1333
                if self['yes'] or ask_user(
1334
                    'Delete %s:%s ?' % (self.container, self.path)):
1335
                    self.client.del_object(
1336
                        self.path,
1337
                        until=self['until'],
1338
                        delimiter=self['delimiter'])
1339
        except ClientError as err:
1340
            if err.status == 404:
1341
                if 'container' in ('%s' % err).lower():
1342
                    raiseCLIError(
1343
                        err,
1344
                        'No container %s in account %s'\
1345
                        % (self.container, self.account),
1346
                        details=self.generic_err_details)
1347
                elif 'object' in ('%s' % err).lower():
1348
                    raiseCLIError(
1349
                        err,
1350
                        'No object %s in container %s'\
1351
                        % (self.path, self.container),
1352
                        details=self.generic_err_details)
1353
            raise_connection_errors(err)
1354
            raiseCLIError(err)
1355
        except Exception as e:
1356
            raiseCLIError(e)
1357

    
1358

    
1359
@command(pithos_cmds)
1360
class store_purge(_store_container_command):
1361
    """Delete a container and release related data blocks
1362
    Non-empty containers can not purged.
1363
    To purge a container with content:
1364
    .   /store delete -r <container>
1365
    .      objects are deleted, but data blocks remain on server
1366
    .   /store purge <container>
1367
    .      container and data blocks are released and deleted
1368
    """
1369

    
1370
    arguments = dict(
1371
        yes=FlagArgument('Do not prompt for permission', '--yes'),
1372
    )
1373

    
1374
    def main(self, container):
1375
        super(self.__class__, self).main(container)
1376
        try:
1377
            if self['yes'] or ask_user(
1378
                'Purge container %s?' % self.container):
1379
                self.client.purge_container()
1380
        except ClientError as err:
1381
            if err.status == 404:
1382
                if 'container' in ('%s' % err).lower():
1383
                    raiseCLIError(
1384
                        err,
1385
                        'No container %s in account %s'\
1386
                        % (self.container, self.account),
1387
                        details=self.generic_err_details)
1388
            raise_connection_errors(err)
1389
            raiseCLIError(err)
1390
        except Exception as e:
1391
            raiseCLIError(e)
1392

    
1393

    
1394
@command(pithos_cmds)
1395
class store_publish(_store_container_command):
1396
    """Publish the object and print the public url"""
1397

    
1398
    def main(self, container___path):
1399
        super(self.__class__, self).main(
1400
            container___path,
1401
            path_is_optional=False)
1402
        try:
1403
            url = self.client.publish_object(self.path)
1404
        except ClientError as err:
1405
            if err.status == 404:
1406
                if 'container' in ('%s' % err).lower():
1407
                    raiseCLIError(
1408
                        err,
1409
                        'No container %s in account %s'\
1410
                        % (self.container, self.account),
1411
                        details=self.generic_err_details)
1412
                elif 'object' in ('%s' % err).lower():
1413
                    raiseCLIError(
1414
                        err,
1415
                        'No object %s in container %s'\
1416
                        % (self.path, self.container),
1417
                        details=self.generic_err_details)
1418
            raise_connection_errors(err)
1419
            raiseCLIError(err)
1420
        except Exception as e:
1421
            raiseCLIError(e)
1422
        print(url)
1423

    
1424

    
1425
@command(pithos_cmds)
1426
class store_unpublish(_store_container_command):
1427
    """Unpublish an object"""
1428

    
1429
    def main(self, container___path):
1430
        super(self.__class__, self).main(
1431
            container___path,
1432
            path_is_optional=False)
1433
        try:
1434
            self.client.unpublish_object(self.path)
1435
        except ClientError as err:
1436
            if err.status == 404:
1437
                if 'container' in ('%s' % err).lower():
1438
                    raiseCLIError(
1439
                        err,
1440
                        'No container %s in account %s'\
1441
                        % (self.container, self.account),
1442
                        details=self.generic_err_details)
1443
                elif 'object' in ('%s' % err).lower():
1444
                    raiseCLIError(
1445
                        err,
1446
                        'No object %s in container %s'\
1447
                        % (self.path, self.container),
1448
                        details=self.generic_err_details)
1449
            raise_connection_errors(err)
1450
            raiseCLIError(err)
1451
        except Exception as e:
1452
            raiseCLIError(e)
1453

    
1454

    
1455
@command(pithos_cmds)
1456
class store_permissions(_store_container_command):
1457
    """Get read and write permissions of an object
1458
    Permissions are lists of users and user groups. There is read and write
1459
    permissions. Users and groups with write permission have also read
1460
    permission.
1461
    """
1462

    
1463
    def main(self, container___path):
1464
        super(self.__class__, self).main(
1465
            container___path,
1466
            path_is_optional=False)
1467
        try:
1468
            reply = self.client.get_object_sharing(self.path)
1469
        except ClientError as err:
1470
            if err.status == 404:
1471
                if 'container' in ('%s' % err).lower():
1472
                    raiseCLIError(
1473
                        err,
1474
                        'No container %s in account %s'\
1475
                        % (self.container, self.account),
1476
                        details=self.generic_err_details)
1477
                elif 'object' in ('%s' % err).lower():
1478
                    raiseCLIError(
1479
                        err,
1480
                        'No object %s in container %s'\
1481
                        % (self.path, self.container),
1482
                        details=self.generic_err_details)
1483
            raise_connection_errors(err)
1484
            raiseCLIError(err)
1485
        except Exception as e:
1486
            raiseCLIError(e)
1487
        print_dict(reply)
1488

    
1489

    
1490
@command(pithos_cmds)
1491
class store_setpermissions(_store_container_command):
1492
    """Set permissions for an object
1493
    New permissions overwrite existing permissions.
1494
    Permission format:
1495
    -   read=<username>[,usergroup[,...]]
1496
    -   write=<username>[,usegroup[,...]]
1497
    E.g. to give read permissions for file F to users A and B and write for C:
1498
    .       /store setpermissions F read=A,B write=C
1499
    """
1500

    
1501
    def format_permition_dict(self, permissions):
1502
        read = False
1503
        write = False
1504
        for perms in permissions:
1505
            splstr = perms.split('=')
1506
            if 'read' == splstr[0]:
1507
                read = [user_or_group.strip() \
1508
                for user_or_group in splstr[1].split(',')]
1509
            elif 'write' == splstr[0]:
1510
                write = [user_or_group.strip() \
1511
                for user_or_group in splstr[1].split(',')]
1512
            else:
1513
                read = False
1514
                write = False
1515
        if not read and not write:
1516
            raiseCLIError(None,
1517
            'Usage:\tread=<groups,users> write=<groups,users>')
1518
        return (read, write)
1519

    
1520
    def main(self, container___path, *permissions):
1521
        super(self.__class__,
1522
            self).main(container___path, path_is_optional=False)
1523
        (read, write) = self.format_permition_dict(permissions)
1524
        try:
1525
            self.client.set_object_sharing(self.path,
1526
                read_permition=read, write_permition=write)
1527
        except ClientError as err:
1528
            if err.status == 404:
1529
                if 'container' in ('%s' % err).lower():
1530
                    raiseCLIError(
1531
                        err,
1532
                        'No container %s in account %s'\
1533
                        % (self.container, self.account),
1534
                        details=self.generic_err_details)
1535
                elif 'object' in ('%s' % err).lower():
1536
                    raiseCLIError(
1537
                        err,
1538
                        'No object %s in container %s'\
1539
                        % (self.path, self.container),
1540
                        details=self.generic_err_details)
1541
            raise_connection_errors(err)
1542
            raiseCLIError(err)
1543
        except Exception as e:
1544
            raiseCLIError(e)
1545

    
1546

    
1547
@command(pithos_cmds)
1548
class store_delpermissions(_store_container_command):
1549
    """Delete all permissions set on object
1550
    To modify permissions, use /store setpermssions
1551
    """
1552

    
1553
    def main(self, container___path):
1554
        super(self.__class__,
1555
            self).main(container___path, path_is_optional=False)
1556
        try:
1557
            self.client.del_object_sharing(self.path)
1558
        except ClientError as err:
1559
            if err.status == 404:
1560
                if 'container' in ('%s' % err).lower():
1561
                    raiseCLIError(
1562
                        err,
1563
                        'No container %s in account %s'\
1564
                        % (self.container, self.account),
1565
                        details=self.generic_err_details)
1566
                elif 'object' in ('%s' % err).lower():
1567
                    raiseCLIError(
1568
                        err,
1569
                        'No object %s in container %s'\
1570
                        % (self.path, self.container),
1571
                        details=self.generic_err_details)
1572
            raise_connection_errors(err)
1573
            raiseCLIError(err)
1574
        except Exception as e:
1575
            raiseCLIError(e)
1576

    
1577

    
1578
@command(pithos_cmds)
1579
class store_info(_store_container_command):
1580
    """Get detailed information for user account, containers or objects
1581
    to get account info:    /store info
1582
    to get container info:  /store info <container>
1583
    to get object info:     /store info <container>:<path>
1584
    """
1585

    
1586
    arguments = dict(
1587
        object_version=ValueArgument(
1588
            'show specific version \ (applies only for objects)',
1589
            '--object-version')
1590
    )
1591

    
1592
    def main(self, container____path__=None):
1593
        super(self.__class__, self).main(container____path__)
1594
        try:
1595
            if self.container is None:
1596
                reply = self.client.get_account_info()
1597
            elif self.path is None:
1598
                reply = self.client.get_container_info(self.container)
1599
            else:
1600
                reply = self.client.get_object_info(
1601
                    self.path,
1602
                    version=self['object_version'])
1603
        except ClientError as err:
1604
            if err.status == 404:
1605
                if 'container' in ('%s' % err).lower():
1606
                    raiseCLIError(
1607
                        err,
1608
                        'No container %s in account %s'\
1609
                        % (self.container, self.account),
1610
                        details=self.generic_err_details)
1611
                elif 'object' in ('%s' % err).lower():
1612
                    raiseCLIError(
1613
                        err,
1614
                        'No object %s in container %s'\
1615
                        % (self.path, self.container),
1616
                        details=self.generic_err_details)
1617
            raise_connection_errors(err)
1618
            raiseCLIError(err)
1619
        except Exception as e:
1620
            raiseCLIError(e)
1621
        print_dict(reply)
1622

    
1623

    
1624
@command(pithos_cmds)
1625
class store_meta(_store_container_command):
1626
    """Get metadata for account, containers or objects"""
1627

    
1628
    arguments = dict(
1629
        detail=FlagArgument('show detailed output', '-l'),
1630
        until=DateArgument('show metadata until then', '--until'),
1631
        object_version=ValueArgument(
1632
            'show specific version \ (applies only for objects)',
1633
            '--object-version')
1634
    )
1635

    
1636
    def main(self, container____path__=None):
1637
        super(self.__class__, self).main(container____path__)
1638

    
1639
        detail = self['detail']
1640
        try:
1641
            until = self['until']
1642
            if self.container is None:
1643
                if detail:
1644
                    reply = self.client.get_account_info(until=until)
1645
                else:
1646
                    reply = self.client.get_account_meta(until=until)
1647
                    reply = pretty_keys(reply, '-')
1648
                if reply:
1649
                    print(bold(self.client.account))
1650
            elif self.path is None:
1651
                if detail:
1652
                    reply = self.client.get_container_info(until=until)
1653
                else:
1654
                    cmeta = self.client.get_container_meta(until=until)
1655
                    ometa = self.client.get_container_object_meta(until=until)
1656
                    reply = {}
1657
                    if cmeta:
1658
                        reply['container-meta'] = pretty_keys(cmeta, '-')
1659
                    if ometa:
1660
                        reply['object-meta'] = pretty_keys(ometa, '-')
1661
            else:
1662
                if detail:
1663
                    reply = self.client.get_object_info(self.path,
1664
                        version=self['object_version'])
1665
                else:
1666
                    reply = self.client.get_object_meta(self.path,
1667
                        version=self['object_version'])
1668
                if reply:
1669
                    reply = pretty_keys(pretty_keys(reply, '-'))
1670
        except ClientError as err:
1671
            if err.status == 404:
1672
                if 'container' in ('%s' % err).lower():
1673
                    raiseCLIError(
1674
                        err,
1675
                        'No container %s in account %s'\
1676
                        % (self.container, self.account),
1677
                        details=self.generic_err_details)
1678
                elif 'object' in ('%s' % err).lower():
1679
                    raiseCLIError(
1680
                        err,
1681
                        'No object %s in container %s'\
1682
                        % (self.path, self.container),
1683
                        details=self.generic_err_details)
1684
                else:
1685
                    raiseCLIError(err, details=self.generic_err_details)
1686
            raise_connection_errors(err)
1687
            raiseCLIError(err)
1688
        except Exception as e:
1689
            raiseCLIError(e)
1690
        if reply:
1691
            print_dict(reply)
1692

    
1693

    
1694
@command(pithos_cmds)
1695
class store_setmeta(_store_container_command):
1696
    """Set a piece of metadata for account, container or object
1697
    Metadata are formed as key:value pairs
1698
    """
1699

    
1700
    def main(self, metakey___metaval, container____path__=None):
1701
        super(self.__class__, self).main(container____path__)
1702
        try:
1703
            metakey, metavalue = metakey___metaval.split(':')
1704
        except ValueError as err:
1705
            raiseCLIError(err,
1706
                'Cannot parse %s as a key:value pair' % metakey___metaval,
1707
                details=['Syntax:',
1708
                    '   store setmeta metakey:metavalue [cont[:path]]'
1709
                ],
1710
                importance=1,
1711
                )
1712
        try:
1713
            if self.container is None:
1714
                self.client.set_account_meta({metakey: metavalue})
1715
            elif self.path is None:
1716
                self.client.set_container_meta({metakey: metavalue})
1717
            else:
1718
                self.client.set_object_meta(self.path, {metakey: metavalue})
1719
        except ClientError as err:
1720
            if err.status == 404:
1721
                if 'container' in ('%s' % err).lower():
1722
                    raiseCLIError(
1723
                        err,
1724
                        'No container %s in account %s'\
1725
                        % (self.container, self.account),
1726
                        details=self.generic_err_details)
1727
                elif 'object' in ('%s' % err).lower():
1728
                    raiseCLIError(
1729
                        err,
1730
                        'No object %s in container %s'\
1731
                        % (self.path, self.container),
1732
                        details=self.generic_err_details)
1733
                else:
1734
                    raiseCLIError(err, details=self.generic_err_details)
1735
            raise_connection_errors(err)
1736
            raiseCLIError(err)
1737
        except Exception as err:
1738
            raiseCLIError(err)
1739

    
1740

    
1741
@command(pithos_cmds)
1742
class store_delmeta(_store_container_command):
1743
    """Delete metadata with given key from account, container or object
1744
    Metadata are formed as key:value objects
1745
    - to get metadata of current account:     /store meta
1746
    - to get metadata of a container:         /store meta <container>
1747
    - to get metadata of an object:           /store meta <container>:<path>
1748
    """
1749

    
1750
    def main(self, metakey, container____path__=None):
1751
        super(self.__class__, self).main(container____path__)
1752
        try:
1753
            if self.container is None:
1754
                self.client.del_account_meta(metakey)
1755
            elif self.path is None:
1756
                self.client.del_container_meta(metakey)
1757
            else:
1758
                self.client.del_object_meta(self.path, metakey)
1759
        except ClientError as err:
1760
            if err.status == 404:
1761
                if 'container' in ('%s' % err).lower():
1762
                    raiseCLIError(
1763
                        err,
1764
                        'No container %s in account %s'\
1765
                        % (self.container, self.account),
1766
                        details=self.generic_err_details)
1767
                elif 'object' in ('%s' % err).lower():
1768
                    raiseCLIError(
1769
                        err,
1770
                        'No object %s in container %s'\
1771
                        % (self.path, self.container),
1772
                        details=self.generic_err_details)
1773
                else:
1774
                    raiseCLIError(err, details=self.generic_err_details)
1775
            raise_connection_errors(err)
1776
            raiseCLIError(err)
1777
        except Exception as err:
1778
            raiseCLIError(err)
1779

    
1780

    
1781
@command(pithos_cmds)
1782
class store_quota(_store_account_command):
1783
    """Get quota (in KB) for account or container"""
1784

    
1785
    def main(self, container=None):
1786
        super(self.__class__, self).main()
1787
        try:
1788
            if container is None:
1789
                reply = self.client.get_account_quota()
1790
            else:
1791
                reply = self.client.get_container_quota(container)
1792
        except ClientError as err:
1793
            if err.status == 404:
1794
                if 'container' in ('%s' % err).lower():
1795
                    raiseCLIError(err,
1796
                        'No container %s in account %s'\
1797
                        % (container, self.account))
1798
            raise_connection_errors(err)
1799
            raiseCLIError(err)
1800
        except Exception as err:
1801
            raiseCLIError(err)
1802
        print_dict(pretty_keys(reply, '-'))
1803

    
1804

    
1805
@command(pithos_cmds)
1806
class store_setquota(_store_account_command):
1807
    """Set new quota (in KB) for account or container"""
1808

    
1809
    def main(self, quota, container=None):
1810
        super(self.__class__, self).main()
1811
        try:
1812
            if container is None:
1813
                self.client.set_account_quota(quota)
1814
            else:
1815
                self.client.container = container
1816
                self.client.set_container_quota(quota)
1817
        except ClientError as err:
1818
            if err.status == 404:
1819
                if 'container' in ('%s' % err).lower():
1820
                    raiseCLIError(err,
1821
                        'No container %s in account %s'\
1822
                        % (container, self.account))
1823
            raise_connection_errors(err)
1824
            raiseCLIError(err)
1825
        except Exception as err:
1826
            raiseCLIError(err)
1827

    
1828

    
1829
@command(pithos_cmds)
1830
class store_versioning(_store_account_command):
1831
    """Get  versioning for account or container"""
1832

    
1833
    def main(self, container=None):
1834
        super(self.__class__, self).main()
1835
        try:
1836
            if container is None:
1837
                reply = self.client.get_account_versioning()
1838
            else:
1839
                reply = self.client.get_container_versioning(container)
1840
        except ClientError as err:
1841
            if err.status == 404:
1842
                if 'container' in ('%s' % err).lower():
1843
                    raiseCLIError(
1844
                        err,
1845
                        'No container %s in account %s'\
1846
                        % (self.container, self.account),
1847
                        details=self.generic_err_details)
1848
                else:
1849
                    raiseCLIError(err, details=self.generic_err_details)
1850
            raise_connection_errors(err)
1851
            raiseCLIError(err)
1852
        except Exception as err:
1853
            raiseCLIError(err)
1854
        print_dict(reply)
1855

    
1856

    
1857
@command(pithos_cmds)
1858
class store_setversioning(_store_account_command):
1859
    """Set versioning mode (auto, none) for account or container"""
1860

    
1861
    def main(self, versioning, container=None):
1862
        super(self.__class__, self).main()
1863
        try:
1864
            if container is None:
1865
                self.client.set_account_versioning(versioning)
1866
            else:
1867
                self.client.container = container
1868
                self.client.set_container_versioning(versioning)
1869
        except ClientError as err:
1870
            if err.status == 404:
1871
                if 'container' in ('%s' % err).lower():
1872
                    raiseCLIError(
1873
                        err,
1874
                        'No container %s in account %s'\
1875
                        % (self.container, self.account),
1876
                        details=self.generic_err_details)
1877
                else:
1878
                    raiseCLIError(err, details=self.generic_err_details)
1879
            raise_connection_errors(err)
1880
            raiseCLIError(err)
1881
        except Exception as err:
1882
            raiseCLIError(err)
1883

    
1884

    
1885
@command(pithos_cmds)
1886
class store_group(_store_account_command):
1887
    """Get groups and group members"""
1888

    
1889
    def main(self):
1890
        super(self.__class__, self).main()
1891
        try:
1892
            reply = self.client.get_account_group()
1893
        except ClientError as err:
1894
            raise_connection_errors(err)
1895
            raiseCLIError(err)
1896
        except Exception as err:
1897
            raiseCLIError(err)
1898
        print_dict(pretty_keys(reply, '-'))
1899

    
1900

    
1901
@command(pithos_cmds)
1902
class store_setgroup(_store_account_command):
1903
    """Set a user group"""
1904

    
1905
    def main(self, groupname, *users):
1906
        super(self.__class__, self).main()
1907
        try:
1908
            self.client.set_account_group(groupname, users)
1909
        except ClientError as err:
1910
            raise_connection_errors(err)
1911
            raiseCLIError(err)
1912
        except Exception as err:
1913
            raiseCLIError(err)
1914

    
1915

    
1916
@command(pithos_cmds)
1917
class store_delgroup(_store_account_command):
1918
    """Delete a user group"""
1919

    
1920
    def main(self, groupname):
1921
        super(self.__class__, self).main()
1922
        try:
1923
            self.client.del_account_group(groupname)
1924
        except ClientError as err:
1925
            raise_connection_errors(err)
1926
            raiseCLIError(err)
1927
        except Exception as err:
1928
            raiseCLIError(err)
1929

    
1930

    
1931
@command(pithos_cmds)
1932
class store_sharers(_store_account_command):
1933
    """List the accounts that share objects with current user"""
1934

    
1935
    arguments = dict(
1936
        detail=FlagArgument('show detailed output', '-l'),
1937
        marker=ValueArgument('show output greater then marker', '--marker')
1938
    )
1939

    
1940
    def main(self):
1941
        super(self.__class__, self).main()
1942
        try:
1943
            marker = self['marker']
1944
            accounts = self.client.get_sharing_accounts(marker=marker)
1945
        except ClientError as err:
1946
            raise_connection_errors(err)
1947
            raiseCLIError(err)
1948
        except Exception as err:
1949
            raiseCLIError(err)
1950

    
1951
        for acc in accounts:
1952
            stdout.write(bold(acc['name']) + ' ')
1953
            if self['detail']:
1954
                print_dict(acc, exclude='name')
1955
        if not self['detail']:
1956
            print
1957

    
1958

    
1959
@command(pithos_cmds)
1960
class store_versions(_store_container_command):
1961
    """Get the list of object versions
1962
    Deleted objects may still have versions that can be used to restore it and
1963
    get information about its previous state.
1964
    The version number can be used in a number of other commands, like info,
1965
    copy, move, meta. See these commands for more information, e.g.
1966
    /store info -h
1967
    """
1968

    
1969
    def main(self, container___path):
1970
        super(store_versions, self).main(container___path)
1971
        try:
1972
            versions = self.client.get_object_versionlist(self.path)
1973

    
1974
            for vitem in versions:
1975
                t = localtime(float(vitem[1]))
1976
                vid = bold(unicode(vitem[0]))
1977
                print('\t%s \t(%s)' % (vid, strftime('%d-%m-%Y %H:%M:%S', t)))
1978
        except ClientError as err:
1979
            if err.status == 404:
1980
                if 'container' in ('%s' % err).lower():
1981
                    raiseCLIError(
1982
                        err,
1983
                        'No container %s in account %s'\
1984
                        % (self.container, self.account),
1985
                        details=self.generic_err_details)
1986
                elif 'object' in ('%s' % err).lower():
1987
                    raiseCLIError(
1988
                        err,
1989
                        'No object %s in container %s'\
1990
                        % (self.path, self.container),
1991
                        details=self.generic_err_details)
1992
                else:
1993
                    raiseCLIError(err, details=self.generic_err_details)
1994
            raise_connection_errors(err)
1995
            raiseCLIError(err)
1996
        except Exception as err:
1997
            raiseCLIError(err)