Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / pithos_cli.py @ 16b0afe6

History | View | Annotate | Download (68.8 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
# Argument functionality
66

    
67
class DelimiterArgument(ValueArgument):
68
    """
69
    :value type: string
70
    :value returns: given string or /
71
    """
72

    
73
    def __init__(self, caller_obj, help='', parsed_name=None, default=None):
74
        super(DelimiterArgument, self).__init__(help, parsed_name, default)
75
        self.caller_obj = caller_obj
76

    
77
    @property
78
    def value(self):
79
        if self.caller_obj['recursive']:
80
            return '/'
81
        return getattr(self, '_value', self.default)
82

    
83
    @value.setter
84
    def value(self, newvalue):
85
        self._value = newvalue
86

    
87

    
88
class SharingArgument(ValueArgument):
89
    """Set sharing (read and/or write) groups
90
    .
91
    :value type: "read=term1,term2,... write=term1,term2,..."
92
    .
93
    :value returns: {'read':['term1', 'term2', ...],
94
    .   'write':['term1', 'term2', ...]}
95
    """
96

    
97
    @property
98
    def value(self):
99
        return getattr(self, '_value', self.default)
100

    
101
    @value.setter
102
    def value(self, newvalue):
103
        perms = {}
104
        try:
105
            permlist = newvalue.split(' ')
106
        except AttributeError:
107
            return
108
        for p in permlist:
109
            try:
110
                (key, val) = p.split('=')
111
            except ValueError as err:
112
                raiseCLIError(
113
                    err,
114
                    'Error in --sharing',
115
                    details='Incorrect format',
116
                    importance=1)
117
            if key.lower() not in ('read', 'write'):
118
                msg = 'Error in --sharing'
119
                raiseCLIError(err, msg, importance=1, details=[
120
                    'Invalid permission key %s' % key])
121
            val_list = val.split(',')
122
            if not key in perms:
123
                perms[key] = []
124
            for item in val_list:
125
                if item not in perms[key]:
126
                    perms[key].append(item)
127
        self._value = perms
128

    
129

    
130
class RangeArgument(ValueArgument):
131
    """
132
    :value type: string of the form <start>-<end> where <start> and <end> are
133
        integers
134
    :value returns: the input string, after type checking <start> and <end>
135
    """
136

    
137
    @property
138
    def value(self):
139
        return getattr(self, '_value', self.default)
140

    
141
    @value.setter
142
    def value(self, newvalue):
143
        if newvalue is None:
144
            self._value = self.default
145
            return
146
        (start, end) = newvalue.split('-')
147
        (start, end) = (int(start), int(end))
148
        self._value = '%s-%s' % (start, end)
149

    
150
# Command specs
151

    
152

    
153
class _pithos_init(_command_init):
154
    """Initialize a pithos+ kamaki client"""
155

    
156
    @staticmethod
157
    def _is_dir(remote_dict):
158
        return 'application/directory' == remote_dict.get(
159
            'content_type',
160
            remote_dict.get('content-type', ''))
161

    
162
    @errors.generic.all
163
    def _run(self):
164
        self.token = self.config.get('store', 'token')\
165
            or self.config.get('global', 'token')
166
        self.base_url = self.config.get('store', 'url')\
167
            or self.config.get('global', 'url')
168
        self._set_account()
169
        self.container = self.config.get('store', 'container')\
170
            or self.config.get('global', 'container')
171
        self.client = PithosClient(
172
            base_url=self.base_url,
173
            token=self.token,
174
            account=self.account,
175
            container=self.container)
176

    
177
    def main(self):
178
        self._run()
179

    
180
    def _set_account(self):
181
        astakos = AstakosClient(self.config.get('astakos', 'url'), self.token)
182
        self.account = self['account'] or astakos.term('uuid')
183

    
184
        """Backwards compatibility"""
185
        self.account = self.account\
186
            or self.config.get('store', 'account')\
187
            or self.config.get('global', 'account')
188

    
189

    
190
class _store_account_command(_pithos_init):
191
    """Base class for account level storage commands"""
192

    
193
    def __init__(self, arguments={}):
194
        super(_store_account_command, self).__init__(arguments)
195
        self['account'] = ValueArgument(
196
            'Set user account (not permanent)',
197
            '--account')
198

    
199
    def _run(self):
200
        super(_store_account_command, self)._run()
201
        if self['account']:
202
            self.client.account = self['account']
203

    
204
    @errors.generic.all
205
    def main(self):
206
        self._run()
207

    
208

    
209
class _store_container_command(_store_account_command):
210
    """Base class for container level storage commands"""
211

    
212
    container = None
213
    path = None
214

    
215
    def __init__(self, arguments={}):
216
        super(_store_container_command, self).__init__(arguments)
217
        self['container'] = ValueArgument(
218
            'Set container to work with (temporary)',
219
            '--container')
220

    
221
    def extract_container_and_path(
222
            self,
223
            container_with_path,
224
            path_is_optional=True):
225
        """Contains all heuristics for deciding what should be used as
226
        container or path. Options are:
227
        * user string of the form container:path
228
        * self.container, self.path variables set by super constructor, or
229
        explicitly by the caller application
230
        Error handling is explicit as these error cases happen only here
231
        """
232
        try:
233
            assert isinstance(container_with_path, str)
234
        except AssertionError as err:
235
            if self['container'] and path_is_optional:
236
                self.container = self['container']
237
                self.client.container = self['container']
238
                return
239
            raiseCLIError(err)
240

    
241
        user_cont, sep, userpath = container_with_path.partition(':')
242

    
243
        if sep:
244
            if not user_cont:
245
                raiseCLIError(CLISyntaxError(
246
                    'Container is missing\n',
247
                    details=errors.pithos.container_howto))
248
            alt_cont = self['container']
249
            if alt_cont and user_cont != alt_cont:
250
                raiseCLIError(CLISyntaxError(
251
                    'Conflict: 2 containers (%s, %s)' % (user_cont, alt_cont),
252
                    details=errors.pithos.container_howto)
253
                )
254
            self.container = user_cont
255
            if not userpath:
256
                raiseCLIError(CLISyntaxError(
257
                    'Path is missing for object in container %s' % user_cont,
258
                    details=errors.pithos.container_howto)
259
                )
260
            self.path = userpath
261
        else:
262
            alt_cont = self['container'] or self.client.container
263
            if alt_cont:
264
                self.container = alt_cont
265
                self.path = user_cont
266
            elif path_is_optional:
267
                self.container = user_cont
268
                self.path = None
269
            else:
270
                self.container = user_cont
271
                raiseCLIError(CLISyntaxError(
272
                    'Both container and path are required',
273
                    details=errors.pithos.container_howto)
274
                )
275

    
276
    @errors.generic.all
277
    def _run(self, container_with_path=None, path_is_optional=True):
278
        super(_store_container_command, self)._run()
279
        if self['container']:
280
            self.client.container = self['container']
281
            if container_with_path:
282
                self.path = container_with_path
283
            elif not path_is_optional:
284
                raise CLISyntaxError(
285
                    'Both container and path are required',
286
                    details=errors.pithos.container_howto)
287
        elif container_with_path:
288
            self.extract_container_and_path(
289
                container_with_path,
290
                path_is_optional)
291
            self.client.container = self.container
292
        self.container = self.client.container
293

    
294
    def main(self, container_with_path=None, path_is_optional=True):
295
        self._run(container_with_path, path_is_optional)
296

    
297

    
298
@command(pithos_cmds)
299
class store_list(_store_container_command):
300
    """List containers, object trees or objects in a directory
301
    Use with:
302
    1 no parameters : containers in current account
303
    2. one parameter (container) or --container : contents of container
304
    3. <container>:<prefix> or --container=<container> <prefix>: objects in
305
    .   container starting with prefix
306
    """
307

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

    
341
    def print_objects(self, object_list):
342
        limit = int(self['limit']) if self['limit'] > 0 else len(object_list)
343
        for index, obj in enumerate(object_list):
344
            if self['exact_match'] and self.path and not (
345
                    obj['name'] == self.path or 'content_type' in obj):
346
                continue
347
            pretty_obj = obj.copy()
348
            index += 1
349
            empty_space = ' ' * (len(str(len(object_list))) - len(str(index)))
350
            if obj['content_type'] == 'application/directory':
351
                isDir = True
352
                size = 'D'
353
            else:
354
                isDir = False
355
                size = format_size(obj['bytes'])
356
                pretty_obj['bytes'] = '%s (%s)' % (obj['bytes'], size)
357
            oname = bold(obj['name'])
358
            if self['detail']:
359
                print('%s%s. %s' % (empty_space, index, oname))
360
                print_dict(pretty_keys(pretty_obj), exclude=('name'))
361
                print
362
            else:
363
                oname = '%s%s. %6s %s' % (empty_space, index, size, oname)
364
                oname += '/' if isDir else ''
365
                print(oname)
366
            if self['more']:
367
                page_hold(index, limit, len(object_list))
368

    
369
    def print_containers(self, container_list):
370
        limit = int(self['limit']) if self['limit'] > 0\
371
            else len(container_list)
372
        for index, container in enumerate(container_list):
373
            if 'bytes' in container:
374
                size = format_size(container['bytes'])
375
            cname = '%s. %s' % (index + 1, bold(container['name']))
376
            if self['detail']:
377
                print(cname)
378
                pretty_c = container.copy()
379
                if 'bytes' in container:
380
                    pretty_c['bytes'] = '%s (%s)' % (container['bytes'], size)
381
                print_dict(pretty_keys(pretty_c), exclude=('name'))
382
                print
383
            else:
384
                if 'count' in container and 'bytes' in container:
385
                    print('%s (%s, %s objects)' % (
386
                        cname,
387
                        size,
388
                        container['count']))
389
                else:
390
                    print(cname)
391
            if self['more']:
392
                page_hold(index + 1, limit, len(container_list))
393

    
394
    @errors.generic.all
395
    @errors.pithos.connection
396
    @errors.pithos.object_path
397
    @errors.pithos.container
398
    def _run(self):
399
        if self.container is None:
400
            r = self.client.account_get(
401
                limit=False if self['more'] else self['limit'],
402
                marker=self['marker'],
403
                if_modified_since=self['if_modified_since'],
404
                if_unmodified_since=self['if_unmodified_since'],
405
                until=self['until'],
406
                show_only_shared=self['shared'])
407
            self.print_containers(r.json)
408
        else:
409
            prefix = self.path or self['prefix']
410
            r = self.client.container_get(
411
                limit=False if self['more'] else self['limit'],
412
                marker=self['marker'],
413
                prefix=prefix,
414
                delimiter=self['delimiter'],
415
                path=self['path'],
416
                if_modified_since=self['if_modified_since'],
417
                if_unmodified_since=self['if_unmodified_since'],
418
                until=self['until'],
419
                meta=self['meta'],
420
                show_only_shared=self['shared'])
421
            self.print_objects(r.json)
422

    
423
    def main(self, container____path__=None):
424
        super(self.__class__, self)._run(container____path__)
425
        self._run()
426

    
427

    
428
@command(pithos_cmds)
429
class store_mkdir(_store_container_command):
430
    """Create a directory"""
431

    
432
    __doc__ += '\n. '.join([
433
        'Kamaki hanldes directories the same way as OOS Storage and Pithos+:',
434
        'A   directory  is   an  object  with  type  "application/directory"',
435
        'An object with path  dir/name can exist even if  dir does not exist',
436
        'or even if dir  is  a non  directory  object.  Users can modify dir',
437
        'without affecting the dir/name object in any way.'])
438

    
439
    @errors.generic.all
440
    @errors.pithos.connection
441
    @errors.pithos.container
442
    def _run(self):
443
        self.client.create_directory(self.path)
444

    
445
    def main(self, container___directory):
446
        super(self.__class__, self)._run(
447
            container___directory,
448
            path_is_optional=False)
449
        self._run()
450

    
451

    
452
@command(pithos_cmds)
453
class store_touch(_store_container_command):
454
    """Create an empty object (file)
455
    If object exists, this command will reset it to 0 length
456
    """
457

    
458
    arguments = dict(
459
        content_type=ValueArgument(
460
            'Set content type (default: application/octet-stream)',
461
            '--content-type',
462
            default='application/octet-stream')
463
    )
464

    
465
    @errors.generic.all
466
    @errors.pithos.connection
467
    @errors.pithos.container
468
    def _run(self):
469
        self.client.create_object(self.path, self['content_type'])
470

    
471
    def main(self, container___path):
472
        super(store_touch, self)._run(
473
            container___path,
474
            path_is_optional=False)
475
        self._run()
476

    
477

    
478
@command(pithos_cmds)
479
class store_create(_store_container_command):
480
    """Create a container"""
481

    
482
    arguments = dict(
483
        versioning=ValueArgument(
484
            'set container versioning (auto/none)',
485
            '--versioning'),
486
        quota=IntArgument('set default container quota', '--quota'),
487
        meta=KeyValueArgument(
488
            'set container metadata (can be repeated)',
489
            '--meta')
490
    )
491

    
492
    @errors.generic.all
493
    @errors.pithos.connection
494
    @errors.pithos.container
495
    def _run(self):
496
        self.client.container_put(
497
            quota=self['quota'],
498
            versioning=self['versioning'],
499
            metadata=self['meta'])
500

    
501
    def main(self, container=None):
502
        super(self.__class__, self)._run(container)
503
        if container and self.container != container:
504
            raiseCLIError('Invalid container name %s' % container, details=[
505
                'Did you mean "%s" ?' % self.container,
506
                'Use --container for names containing :'])
507
        self._run()
508

    
509

    
510
class _source_destination_command(_store_container_command):
511

    
512
    arguments = dict(
513
        destination_account=ValueArgument('', '--dst-account'),
514
        recursive=FlagArgument('', ('-r', '--recursive')),
515
        prefix=FlagArgument('', '--with-prefix', default=''),
516
        suffix=ValueArgument('', '--with-suffix', default=''),
517
        add_prefix=ValueArgument('', '--add-prefix', default=''),
518
        add_suffix=ValueArgument('', '--add-suffix', default=''),
519
        prefix_replace=ValueArgument('', '--prefix-to-replace', default=''),
520
        suffix_replace=ValueArgument('', '--suffix-to-replace', default='')
521
    )
522

    
523
    def __init__(self, arguments={}):
524
        self.arguments.update(arguments)
525
        super(_source_destination_command, self).__init__(self.arguments)
526

    
527
    def _run(self, source_container___path, path_is_optional=False):
528
        super(_source_destination_command, self)._run(
529
            source_container___path,
530
            path_is_optional)
531
        self.dst_client = PithosClient(
532
            base_url=self.client.base_url,
533
            token=self.client.token,
534
            account=self['destination_account'] or self.client.account)
535

    
536
    @errors.generic.all
537
    @errors.pithos.account
538
    def _dest_container_path(self, dest_container_path):
539
        if self['destination_container']:
540
            self.dst_client.container = self['destination_container']
541
            return (self['destination_container'], dest_container_path)
542
        if dest_container_path:
543
            dst = dest_container_path.split(':')
544
            if len(dst) > 1:
545
                try:
546
                    self.dst_client.container = dst[0]
547
                    self.dst_client.get_container_info(dst[0])
548
                except ClientError as err:
549
                    if err.status in (404, 204):
550
                        raiseCLIError(
551
                            'Destination container %s not found' % dst[0])
552
                    raise
553
                else:
554
                    self.dst_client.container = dst[0]
555
                return (dst[0], dst[1])
556
            return(None, dst[0])
557
        raiseCLIError('No destination container:path provided')
558

    
559
    def _get_all(self, prefix):
560
        return self.client.container_get(prefix=prefix).json
561

    
562
    def _get_src_objects(self, src_path):
563
        """Get a list of the source objects to be called
564

565
        :param src_path: (str) source path
566

567
        :returns: (method, params) a method that returns a list when called
568
        or (object) if it is a single object
569
        """
570
        if src_path and src_path[-1] == '/':
571
            src_path = src_path[:-1]
572

    
573
        if self['prefix']:
574
            return (self._get_all, dict(prefix=src_path))
575
        try:
576
            srcobj = self.client.get_object_info(src_path)
577
        except ClientError as srcerr:
578
            if srcerr.status == 404:
579
                raiseCLIError(
580
                    'Source object %s not in source container %s' % (
581
                        src_path,
582
                        self.client.container),
583
                    details=['Hint: --with-prefix to match multiple objects'])
584
            elif srcerr.status not in (204,):
585
                raise
586
            return (self.client.list_objects, {})
587

    
588
        if self._is_dir(srcobj):
589
            if not self['recursive']:
590
                raiseCLIError(
591
                    'Object %s of cont. %s is a dir' % (
592
                        src_path,
593
                        self.client.container),
594
                    details=['Use --recursive to access directories'])
595
            return (self._get_all, dict(prefix=src_path))
596
        srcobj['name'] = src_path
597
        return srcobj
598

    
599
    def src_dst_pairs(self, dst_path):
600
        src_iter = self._get_src_objects(self.path)
601
        src_N = isinstance(src_iter, tuple)
602
        add_prefix = self['add_prefix'].strip('/')
603

    
604
        if dst_path and dst_path.endswith('/'):
605
            dst_path = dst_path[:-1]
606

    
607
        try:
608
            dstobj = self.dst_client.get_object_info(dst_path)
609
        except ClientError as trgerr:
610
            if trgerr.status in (404,):
611
                if src_N:
612
                    raiseCLIError(
613
                        'Cannot merge multiple paths to path %s' % dst_path,
614
                        details=[
615
                            'Try to use / or a directory as destination',
616
                            'or create the destination dir (/store mkdir)',
617
                            'or use a single object as source'])
618
            elif trgerr.status not in (204,):
619
                raise
620
        else:
621
            if self._is_dir(dstobj):
622
                add_prefix = '%s/%s' % (dst_path.strip('/'), add_prefix)
623
            elif src_N:
624
                raiseCLIError(
625
                    'Cannot merge multiple paths to path' % dst_path,
626
                    details=[
627
                        'Try to use / or a directory as destination',
628
                        'or create the destination dir (/store mkdir)',
629
                        'or use a single object as source'])
630

    
631
        if src_N:
632
            (method, kwargs) = src_iter
633
            for obj in method(**kwargs):
634
                name = obj['name']
635
                if name.endswith(self['suffix']):
636
                    yield (name, self._get_new_object(name, add_prefix))
637
        elif src_iter['name'].endswith(self['suffix']):
638
            name = src_iter['name']
639
            yield (name, self._get_new_object(dst_path or name, add_prefix))
640
        else:
641
            raiseCLIError('Source path %s conflicts with suffix %s' % (
642
                src_iter['name'],
643
                self['suffix']))
644

    
645
    def _get_new_object(self, obj, add_prefix):
646
        if self['prefix_replace'] and obj.startswith(self['prefix_replace']):
647
            obj = obj[len(self['prefix_replace']):]
648
        if self['suffix_replace'] and obj.endswith(self['suffix_replace']):
649
            obj = obj[:-len(self['suffix_replace'])]
650
        return add_prefix + obj + self['add_suffix']
651

    
652

    
653
@command(pithos_cmds)
654
class store_copy(_source_destination_command):
655
    """Copy objects from container to (another) container
656
    Semantics:
657
    copy cont:path dir
658
    .   transfer path as dir/path
659
    copy cont:path cont2:
660
    .   trasnfer all <obj> prefixed with path to container cont2
661
    copy cont:path [cont2:]path2
662
    .   transfer path to path2
663
    Use options:
664
    1. <container1>:<path1> [container2:]<path2> : if container2 is not given,
665
    destination is container1:path2
666
    2. <container>:<path1> <path2> : make a copy in the same container
667
    3. Can use --container= instead of <container1>
668
    """
669

    
670
    arguments = dict(
671
        destination_account=ValueArgument(
672
            'Account to copy to',
673
            '--dst-account'),
674
        destination_container=ValueArgument(
675
            'use it if destination container name contains a : character',
676
            '--dst-container'),
677
        source_version=ValueArgument(
678
            'copy specific version',
679
            '--source-version'),
680
        public=ValueArgument('make object publicly accessible', '--public'),
681
        content_type=ValueArgument(
682
            'change object\'s content type',
683
            '--content-type'),
684
        recursive=FlagArgument(
685
            'copy directory and contents',
686
            ('-r', '--recursive')),
687
        prefix=FlagArgument(
688
            'Match objects prefixed with src path (feels like src_path*)',
689
            '--with-prefix',
690
            default=''),
691
        suffix=ValueArgument(
692
            'Suffix of source objects (feels like *suffix)',
693
            '--with-suffix',
694
            default=''),
695
        add_prefix=ValueArgument('Prefix targets', '--add-prefix', default=''),
696
        add_suffix=ValueArgument('Suffix targets', '--add-suffix', default=''),
697
        prefix_replace=ValueArgument(
698
            'Prefix of src to replace with dst path + add_prefix, if matched',
699
            '--prefix-to-replace',
700
            default=''),
701
        suffix_replace=ValueArgument(
702
            'Suffix of src to replace with add_suffix, if matched',
703
            '--suffix-to-replace',
704
            default='')
705
    )
706

    
707
    @errors.generic.all
708
    @errors.pithos.connection
709
    @errors.pithos.container
710
    @errors.pithos.account
711
    def _run(self, dst_path):
712
        no_source_object = True
713
        src_account = self.client.account if (
714
            self['destination_account']) else None
715
        for src_obj, dst_obj in self.src_dst_pairs(dst_path):
716
            no_source_object = False
717
            self.dst_client.copy_object(
718
                src_container=self.client.container,
719
                src_object=src_obj,
720
                dst_container=self.dst_client.container,
721
                dst_object=dst_obj,
722
                source_account=src_account,
723
                source_version=self['source_version'],
724
                public=self['public'],
725
                content_type=self['content_type'])
726
        if no_source_object:
727
            raiseCLIError('No object %s in container %s' % (
728
                self.path,
729
                self.container))
730

    
731
    def main(
732
            self,
733
            source_container___path,
734
            destination_container___path=None):
735
        super(store_copy, self)._run(
736
            source_container___path,
737
            path_is_optional=False)
738
        (dst_cont, dst_path) = self._dest_container_path(
739
            destination_container___path)
740
        self.dst_client.container = dst_cont or self.container
741
        self._run(dst_path=dst_path or '')
742

    
743

    
744
@command(pithos_cmds)
745
class store_move(_source_destination_command):
746
    """Move/rename objects from container to (another) container
747
    Semantics:
748
    move cont:path dir
749
    .   rename path as dir/path
750
    move cont:path cont2:
751
    .   trasnfer all <obj> prefixed with path to container cont2
752
    move cont:path [cont2:]path2
753
    .   transfer path to path2
754
    Use options:
755
    1. <container1>:<path1> [container2:]<path2> : if container2 is not given,
756
    destination is container1:path2
757
    2. <container>:<path1> <path2> : move in the same container
758
    3. Can use --container= instead of <container1>
759
    """
760

    
761
    arguments = dict(
762
        destination_account=ValueArgument(
763
            'Account to move to',
764
            '--dst-account'),
765
        destination_container=ValueArgument(
766
            'use it if destination container name contains a : character',
767
            '--dst-container'),
768
        source_version=ValueArgument(
769
            'copy specific version',
770
            '--source-version'),
771
        public=ValueArgument('make object publicly accessible', '--public'),
772
        content_type=ValueArgument(
773
            'change object\'s content type',
774
            '--content-type'),
775
        recursive=FlagArgument(
776
            'copy directory and contents',
777
            ('-r', '--recursive')),
778
        prefix=FlagArgument(
779
            'Match objects prefixed with src path (feels like src_path*)',
780
            '--with-prefix',
781
            default=''),
782
        suffix=ValueArgument(
783
            'Suffix of source objects (feels like *suffix)',
784
            '--with-suffix',
785
            default=''),
786
        add_prefix=ValueArgument('Prefix targets', '--add-prefix', default=''),
787
        add_suffix=ValueArgument('Suffix targets', '--add-suffix', default=''),
788
        prefix_replace=ValueArgument(
789
            'Prefix of src to replace with dst path + add_prefix, if matched',
790
            '--prefix-to-replace',
791
            default=''),
792
        suffix_replace=ValueArgument(
793
            'Suffix of src to replace with add_suffix, if matched',
794
            '--suffix-to-replace',
795
            default='')
796
    )
797

    
798
    @errors.generic.all
799
    @errors.pithos.connection
800
    @errors.pithos.container
801
    def _run(self, dst_path):
802
        no_source_object = True
803
        src_account = self.client.account if (
804
            self['destination_account']) else None
805
        for src_obj, dst_obj in self.src_dst_pairs(dst_path):
806
            no_source_object = False
807
            self.dst_client.move_object(
808
                src_container=self.container,
809
                src_object=src_obj,
810
                dst_container=self.dst_client.container,
811
                dst_object=dst_obj,
812
                source_account=src_account,
813
                source_version=self['source_version'],
814
                public=self['public'],
815
                content_type=self['content_type'])
816
        if no_source_object:
817
            raiseCLIError('No object %s in container %s' % (
818
                self.path,
819
                self.container))
820

    
821
    def main(
822
            self,
823
            source_container___path,
824
            destination_container___path=None):
825
        super(self.__class__, self)._run(
826
            source_container___path,
827
            path_is_optional=False)
828
        (dst_cont, dst_path) = self._dest_container_path(
829
            destination_container___path)
830
        (dst_cont, dst_path) = self._dest_container_path(
831
            destination_container___path)
832
        self.dst_client.container = dst_cont or self.container
833
        self._run(dst_path=dst_path or '')
834

    
835

    
836
@command(pithos_cmds)
837
class store_append(_store_container_command):
838
    """Append local file to (existing) remote object
839
    The remote object should exist.
840
    If the remote object is a directory, it is transformed into a file.
841
    In the later case, objects under the directory remain intact.
842
    """
843

    
844
    arguments = dict(
845
        progress_bar=ProgressBarArgument(
846
            'do not show progress bar',
847
            '--no-progress-bar',
848
            default=False)
849
    )
850

    
851
    @errors.generic.all
852
    @errors.pithos.connection
853
    @errors.pithos.container
854
    @errors.pithos.object_path
855
    def _run(self, local_path):
856
        (progress_bar, upload_cb) = self._safe_progress_bar('Appending')
857
        try:
858
            f = open(local_path, 'rb')
859
            self.client.append_object(self.path, f, upload_cb)
860
        except Exception:
861
            self._safe_progress_bar_finish(progress_bar)
862
            raise
863
        finally:
864
            self._safe_progress_bar_finish(progress_bar)
865

    
866
    def main(self, local_path, container___path):
867
        super(self.__class__, self)._run(
868
            container___path,
869
            path_is_optional=False)
870
        self._run(local_path)
871

    
872

    
873
@command(pithos_cmds)
874
class store_truncate(_store_container_command):
875
    """Truncate remote file up to a size (default is 0)"""
876

    
877
    @errors.generic.all
878
    @errors.pithos.connection
879
    @errors.pithos.container
880
    @errors.pithos.object_path
881
    @errors.pithos.object_size
882
    def _run(self, size=0):
883
        self.client.truncate_object(self.path, size)
884

    
885
    def main(self, container___path, size=0):
886
        super(self.__class__, self)._run(container___path)
887
        self._run(size=size)
888

    
889

    
890
@command(pithos_cmds)
891
class store_overwrite(_store_container_command):
892
    """Overwrite part (from start to end) of a remote file
893
    overwrite local-path container 10 20
894
    .   will overwrite bytes from 10 to 20 of a remote file with the same name
895
    .   as local-path basename
896
    overwrite local-path container:path 10 20
897
    .   will overwrite as above, but the remote file is named path
898
    """
899

    
900
    arguments = dict(
901
        progress_bar=ProgressBarArgument(
902
            'do not show progress bar',
903
            '--no-progress-bar',
904
            default=False)
905
    )
906

    
907
    def _open_file(self, local_path, start):
908
        f = open(path.abspath(local_path), 'rb')
909
        f.seek(0, 2)
910
        f_size = f.tell()
911
        f.seek(start, 0)
912
        return (f, f_size)
913

    
914
    @errors.generic.all
915
    @errors.pithos.connection
916
    @errors.pithos.container
917
    @errors.pithos.object_path
918
    @errors.pithos.object_size
919
    def _run(self, local_path, start, end):
920
        (start, end) = (int(start), int(end))
921
        (f, f_size) = self._open_file(local_path, start)
922
        (progress_bar, upload_cb) = self._safe_progress_bar(
923
            'Overwrite %s bytes' % (end - start))
924
        try:
925
            self.client.overwrite_object(
926
                obj=self.path,
927
                start=start,
928
                end=end,
929
                source_file=f,
930
                upload_cb=upload_cb)
931
        except Exception:
932
            self._safe_progress_bar_finish(progress_bar)
933
            raise
934
        finally:
935
            self._safe_progress_bar_finish(progress_bar)
936

    
937
    def main(self, local_path, container___path, start, end):
938
        super(self.__class__, self)._run(
939
            container___path,
940
            path_is_optional=None)
941
        self.path = self.path or path.basename(local_path)
942
        self._run(local_path=local_path, start=start, end=end)
943

    
944

    
945
@command(pithos_cmds)
946
class store_manifest(_store_container_command):
947
    """Create a remote file of uploaded parts by manifestation
948
    Remains functional for compatibility with OOS Storage. Users are advised
949
    to use the upload command instead.
950
    Manifestation is a compliant process for uploading large files. The files
951
    have to be chunked in smalled files and uploaded as <prefix><increment>
952
    where increment is 1, 2, ...
953
    Finally, the manifest command glues partial files together in one file
954
    named <prefix>
955
    The upload command is faster, easier and more intuitive than manifest
956
    """
957

    
958
    arguments = dict(
959
        etag=ValueArgument('check written data', '--etag'),
960
        content_encoding=ValueArgument(
961
            'set MIME content type',
962
            '--content-encoding'),
963
        content_disposition=ValueArgument(
964
            'the presentation style of the object',
965
            '--content-disposition'),
966
        content_type=ValueArgument(
967
            'specify content type',
968
            '--content-type',
969
            default='application/octet-stream'),
970
        sharing=SharingArgument(
971
            '\n'.join([
972
                'define object sharing policy',
973
                '    ( "read=user1,grp1,user2,... write=user1,grp2,..." )']),
974
            '--sharing'),
975
        public=FlagArgument('make object publicly accessible', '--public')
976
    )
977

    
978
    @errors.generic.all
979
    @errors.pithos.connection
980
    @errors.pithos.container
981
    @errors.pithos.object_path
982
    def _run(self):
983
        self.client.create_object_by_manifestation(
984
            self.path,
985
            content_encoding=self['content_encoding'],
986
            content_disposition=self['content_disposition'],
987
            content_type=self['content_type'],
988
            sharing=self['sharing'],
989
            public=self['public'])
990

    
991
    def main(self, container___path):
992
        super(self.__class__, self)._run(
993
            container___path,
994
            path_is_optional=False)
995
        self.run()
996

    
997

    
998
@command(pithos_cmds)
999
class store_upload(_store_container_command):
1000
    """Upload a file"""
1001

    
1002
    arguments = dict(
1003
        use_hashes=FlagArgument(
1004
            'provide hashmap file instead of data',
1005
            '--use-hashes'),
1006
        etag=ValueArgument('check written data', '--etag'),
1007
        unchunked=FlagArgument('avoid chunked transfer mode', '--unchunked'),
1008
        content_encoding=ValueArgument(
1009
            'set MIME content type',
1010
            '--content-encoding'),
1011
        content_disposition=ValueArgument(
1012
            'specify objects presentation style',
1013
            '--content-disposition'),
1014
        content_type=ValueArgument('specify content type', '--content-type'),
1015
        sharing=SharingArgument(
1016
            help='\n'.join([
1017
                'define sharing object policy',
1018
                '( "read=user1,grp1,user2,... write=user1,grp2,... )']),
1019
            parsed_name='--sharing'),
1020
        public=FlagArgument('make object publicly accessible', '--public'),
1021
        poolsize=IntArgument('set pool size', '--with-pool-size'),
1022
        progress_bar=ProgressBarArgument(
1023
            'do not show progress bar',
1024
            '--no-progress-bar',
1025
            default=False),
1026
        overwrite=FlagArgument('Force overwrite, if object exists', '-f')
1027
    )
1028

    
1029
    def _remote_path(self, remote_path, local_path=''):
1030
        if self['overwrite']:
1031
            return remote_path
1032
        try:
1033
            r = self.client.get_object_info(remote_path)
1034
        except ClientError as ce:
1035
            if ce.status == 404:
1036
                return remote_path
1037
            raise ce
1038
        ctype = r.get('content-type', '')
1039
        if 'application/directory' == ctype.lower():
1040
            ret = '%s/%s' % (remote_path, local_path)
1041
            return self._remote_path(ret) if local_path else ret
1042
        raiseCLIError(
1043
            'Object %s already exists' % remote_path,
1044
            importance=1,
1045
            details=['use -f to overwrite or resume'])
1046

    
1047
    @errors.generic.all
1048
    @errors.pithos.connection
1049
    @errors.pithos.container
1050
    @errors.pithos.object_path
1051
    @errors.pithos.local_path
1052
    def _run(self, local_path, remote_path):
1053
        poolsize = self['poolsize']
1054
        if poolsize > 0:
1055
            self.client.MAX_THREADS = int(poolsize)
1056
        params = dict(
1057
            content_encoding=self['content_encoding'],
1058
            content_type=self['content_type'],
1059
            content_disposition=self['content_disposition'],
1060
            sharing=self['sharing'],
1061
            public=self['public'])
1062
        remote_path = self._remote_path(remote_path, local_path)
1063
        with open(path.abspath(local_path), 'rb') as f:
1064
            if self['unchunked']:
1065
                self.client.upload_object_unchunked(
1066
                    remote_path,
1067
                    f,
1068
                    etag=self['etag'],
1069
                    withHashFile=self['use_hashes'],
1070
                    **params)
1071
            else:
1072
                try:
1073
                    (progress_bar, upload_cb) = self._safe_progress_bar(
1074
                        'Uploading')
1075
                    if progress_bar:
1076
                        hash_bar = progress_bar.clone()
1077
                        hash_cb = hash_bar.get_generator(
1078
                            'Calculating block hashes'
1079
                        )
1080
                    else:
1081
                        hash_cb = None
1082
                    self.client.upload_object(
1083
                        remote_path,
1084
                        f,
1085
                        hash_cb=hash_cb,
1086
                        upload_cb=upload_cb,
1087
                        **params)
1088
                except Exception:
1089
                    self._safe_progress_bar_finish(progress_bar)
1090
                    raise
1091
                finally:
1092
                    self._safe_progress_bar_finish(progress_bar)
1093
        print 'Upload completed'
1094

    
1095
    def main(self, local_path, container____path__=None):
1096
        super(self.__class__, self)._run(container____path__)
1097
        remote_path = self.path or path.basename(local_path)
1098
        self._run(local_path=local_path, remote_path=remote_path)
1099

    
1100

    
1101
@command(pithos_cmds)
1102
class store_cat(_store_container_command):
1103
    """Print remote file contents to console"""
1104

    
1105
    arguments = dict(
1106
        range=RangeArgument('show range of data', '--range'),
1107
        if_match=ValueArgument('show output if ETags match', '--if-match'),
1108
        if_none_match=ValueArgument(
1109
            'show output if ETags match',
1110
            '--if-none-match'),
1111
        if_modified_since=DateArgument(
1112
            'show output modified since then',
1113
            '--if-modified-since'),
1114
        if_unmodified_since=DateArgument(
1115
            'show output unmodified since then',
1116
            '--if-unmodified-since'),
1117
        object_version=ValueArgument(
1118
            'get the specific version',
1119
            '--object-version')
1120
    )
1121

    
1122
    @errors.generic.all
1123
    @errors.pithos.connection
1124
    @errors.pithos.container
1125
    @errors.pithos.object_path
1126
    def _run(self):
1127
        self.client.download_object(
1128
            self.path,
1129
            stdout,
1130
            range_str=self['range'],
1131
            version=self['object_version'],
1132
            if_match=self['if_match'],
1133
            if_none_match=self['if_none_match'],
1134
            if_modified_since=self['if_modified_since'],
1135
            if_unmodified_since=self['if_unmodified_since'])
1136

    
1137
    def main(self, container___path):
1138
        super(self.__class__, self)._run(
1139
            container___path,
1140
            path_is_optional=False)
1141
        self._run()
1142

    
1143

    
1144
@command(pithos_cmds)
1145
class store_download(_store_container_command):
1146
    """Download remote object as local file
1147
    If local destination is a directory:
1148
    *   download <container>:<path> <local dir> -r
1149
    will download all files on <container> prefixed as <path>,
1150
    to <local dir>/<full path>
1151
    *   download <container>:<path> <local dir> --exact-match
1152
    will download only one file, exactly matching <path>
1153
    ATTENTION: to download cont:dir1/dir2/file there must exist objects
1154
    cont:dir1 and cont:dir1/dir2 of type application/directory
1155
    To create directory objects, use /store mkdir
1156
    """
1157

    
1158
    arguments = dict(
1159
        resume=FlagArgument('Resume instead of overwrite', '--resume'),
1160
        range=RangeArgument('show range of data', '--range'),
1161
        if_match=ValueArgument('show output if ETags match', '--if-match'),
1162
        if_none_match=ValueArgument(
1163
            'show output if ETags match',
1164
            '--if-none-match'),
1165
        if_modified_since=DateArgument(
1166
            'show output modified since then',
1167
            '--if-modified-since'),
1168
        if_unmodified_since=DateArgument(
1169
            'show output unmodified since then',
1170
            '--if-unmodified-since'),
1171
        object_version=ValueArgument(
1172
            'get the specific version',
1173
            '--object-version'),
1174
        poolsize=IntArgument('set pool size', '--with-pool-size'),
1175
        progress_bar=ProgressBarArgument(
1176
            'do not show progress bar',
1177
            '--no-progress-bar',
1178
            default=False),
1179
        recursive=FlagArgument(
1180
            'Download a remote path and all its contents',
1181
            '--recursive')
1182
    )
1183

    
1184
    def _outputs(self, local_path):
1185
        """:returns: (local_file, remote_path)"""
1186
        remotes = []
1187
        if self['recursive']:
1188
            r = self.client.container_get(
1189
                prefix=self.path or '/',
1190
                if_modified_since=self['if_modified_since'],
1191
                if_unmodified_since=self['if_unmodified_since'])
1192
            dirlist = dict()
1193
            for remote in r.json:
1194
                rname = remote['name'].strip('/')
1195
                tmppath = ''
1196
                for newdir in rname.strip('/').split('/')[:-1]:
1197
                    tmppath = '/'.join([tmppath, newdir])
1198
                    dirlist.update({tmppath.strip('/'): True})
1199
                remotes.append((rname, store_download._is_dir(remote)))
1200
            dir_remotes = [r[0] for r in remotes if r[1]]
1201
            if not set(dirlist).issubset(dir_remotes):
1202
                badguys = [bg.strip('/') for bg in set(
1203
                    dirlist).difference(dir_remotes)]
1204
                raiseCLIError(
1205
                    'Some remote paths contain non existing directories',
1206
                    details=['Missing remote directories:'] + badguys)
1207
        elif self.path:
1208
            r = self.client.get_object_info(
1209
                self.path,
1210
                version=self['object_version'])
1211
            if store_download._is_dir(r):
1212
                raiseCLIError(
1213
                    'Illegal download: Remote object %s is a directory' % (
1214
                        self.path),
1215
                    details=['To download a directory, try --recursive'])
1216
            if '/' in self.path.strip('/') and not local_path:
1217
                raiseCLIError(
1218
                    'Illegal download: remote object %s contains "/"' % (
1219
                        self.path),
1220
                    details=[
1221
                        'To download an object containing "/" characters',
1222
                        'either create the remote directories or',
1223
                        'specify a non-directory local path for this object'])
1224
            remotes = [(self.path, False)]
1225
        if not remotes:
1226
            if self.path:
1227
                raiseCLIError(
1228
                    'No matching path %s on container %s' % (
1229
                        self.path,
1230
                        self.container),
1231
                    details=[
1232
                        'To list the contents of %s, try:' % self.container,
1233
                        '   /store list %s' % self.container])
1234
            raiseCLIError(
1235
                'Illegal download of container %s' % self.container,
1236
                details=[
1237
                    'To download a whole container, try:',
1238
                    '   /store download --recursive <container>'])
1239

    
1240
        lprefix = path.abspath(local_path or path.curdir)
1241
        if path.isdir(lprefix):
1242
            for rpath, remote_is_dir in remotes:
1243
                lpath = '/%s/%s' % (lprefix.strip('/'), rpath.strip('/'))
1244
                if remote_is_dir:
1245
                    if path.exists(lpath) and path.isdir(lpath):
1246
                        continue
1247
                    makedirs(lpath)
1248
                elif path.exists(lpath):
1249
                    if not self['resume']:
1250
                        print('File %s exists, aborting...' % lpath)
1251
                        continue
1252
                    with open(lpath, 'rwb+') as f:
1253
                        yield (f, rpath)
1254
                else:
1255
                    with open(lpath, 'wb+') as f:
1256
                        yield (f, rpath)
1257
        elif path.exists(lprefix):
1258
            if len(remotes) > 1:
1259
                raiseCLIError(
1260
                    '%s remote objects cannot be merged in local file %s' % (
1261
                        len(remotes),
1262
                        local_path),
1263
                    details=[
1264
                        'To download multiple objects, local path should be',
1265
                        'a directory, or use download without a local path'])
1266
            (rpath, remote_is_dir) = remotes[0]
1267
            if remote_is_dir:
1268
                raiseCLIError(
1269
                    'Remote directory %s should not replace local file %s' % (
1270
                        rpath,
1271
                        local_path))
1272
            if self['resume']:
1273
                with open(lprefix, 'rwb+') as f:
1274
                    yield (f, rpath)
1275
            else:
1276
                raiseCLIError(
1277
                    'Local file %s already exist' % local_path,
1278
                    details=['Try --resume to overwrite it'])
1279
        else:
1280
            if len(remotes) > 1 or remotes[0][1]:
1281
                raiseCLIError(
1282
                    'Local directory %s does not exist' % local_path)
1283
            with open(lprefix, 'wb+') as f:
1284
                yield (f, remotes[0][0])
1285

    
1286
    @errors.generic.all
1287
    @errors.pithos.connection
1288
    @errors.pithos.container
1289
    @errors.pithos.object_path
1290
    @errors.pithos.local_path
1291
    def _run(self, local_path):
1292
        #outputs = self._outputs(local_path)
1293
        poolsize = self['poolsize']
1294
        if poolsize:
1295
            self.client.MAX_THREADS = int(poolsize)
1296
        progress_bar = None
1297
        try:
1298
            for f, rpath in self._outputs(local_path):
1299
                (
1300
                    progress_bar,
1301
                    download_cb) = self._safe_progress_bar(
1302
                        'Download %s' % rpath)
1303
                self.client.download_object(
1304
                    rpath,
1305
                    f,
1306
                    download_cb=download_cb,
1307
                    range_str=self['range'],
1308
                    version=self['object_version'],
1309
                    if_match=self['if_match'],
1310
                    resume=self['resume'],
1311
                    if_none_match=self['if_none_match'],
1312
                    if_modified_since=self['if_modified_since'],
1313
                    if_unmodified_since=self['if_unmodified_since'])
1314
        except KeyboardInterrupt:
1315
            from threading import enumerate as activethreads
1316
            stdout.write('\nFinishing active threads ')
1317
            for thread in activethreads():
1318
                stdout.flush()
1319
                try:
1320
                    thread.join()
1321
                    stdout.write('.')
1322
                except RuntimeError:
1323
                    continue
1324
            print('\ndownload canceled by user')
1325
            if local_path is not None:
1326
                print('to resume, re-run with --resume')
1327
        except Exception:
1328
            self._safe_progress_bar_finish(progress_bar)
1329
            raise
1330
        finally:
1331
            self._safe_progress_bar_finish(progress_bar)
1332

    
1333
    def main(self, container___path, local_path=None):
1334
        super(self.__class__, self)._run(container___path)
1335
        self._run(local_path=local_path)
1336

    
1337

    
1338
@command(pithos_cmds)
1339
class store_hashmap(_store_container_command):
1340
    """Get the hash-map of an object"""
1341

    
1342
    arguments = dict(
1343
        if_match=ValueArgument('show output if ETags match', '--if-match'),
1344
        if_none_match=ValueArgument(
1345
            'show output if ETags match',
1346
            '--if-none-match'),
1347
        if_modified_since=DateArgument(
1348
            'show output modified since then',
1349
            '--if-modified-since'),
1350
        if_unmodified_since=DateArgument(
1351
            'show output unmodified since then',
1352
            '--if-unmodified-since'),
1353
        object_version=ValueArgument(
1354
            'get the specific version',
1355
            '--object-version')
1356
    )
1357

    
1358
    @errors.generic.all
1359
    @errors.pithos.connection
1360
    @errors.pithos.container
1361
    @errors.pithos.object_path
1362
    def _run(self):
1363
        data = self.client.get_object_hashmap(
1364
            self.path,
1365
            version=self['object_version'],
1366
            if_match=self['if_match'],
1367
            if_none_match=self['if_none_match'],
1368
            if_modified_since=self['if_modified_since'],
1369
            if_unmodified_since=self['if_unmodified_since'])
1370
        print_dict(data)
1371

    
1372
    def main(self, container___path):
1373
        super(self.__class__, self)._run(
1374
            container___path,
1375
            path_is_optional=False)
1376
        self._run()
1377

    
1378

    
1379
@command(pithos_cmds)
1380
class store_delete(_store_container_command):
1381
    """Delete a container [or an object]
1382
    How to delete a non-empty container:
1383
    - empty the container:  /store delete -r <container>
1384
    - delete it:            /store delete <container>
1385
    .
1386
    Semantics of directory deletion:
1387
    .a preserve the contents: /store delete <container>:<directory>
1388
    .    objects of the form dir/filename can exist with a dir object
1389
    .b delete contents:       /store delete -r <container>:<directory>
1390
    .    all dir/* objects are affected, even if dir does not exist
1391
    .
1392
    To restore a deleted object OBJ in a container CONT:
1393
    - get object versions: /store versions CONT:OBJ
1394
    .   and choose the version to be restored
1395
    - restore the object:  /store copy --source-version=<version> CONT:OBJ OBJ
1396
    """
1397

    
1398
    arguments = dict(
1399
        until=DateArgument('remove history until that date', '--until'),
1400
        yes=FlagArgument('Do not prompt for permission', '--yes'),
1401
        recursive=FlagArgument(
1402
            'empty dir or container and delete (if dir)',
1403
            ('-r', '--recursive'))
1404
    )
1405

    
1406
    def __init__(self, arguments={}):
1407
        super(self.__class__, self).__init__(arguments)
1408
        self['delimiter'] = DelimiterArgument(
1409
            self,
1410
            parsed_name='--delimiter',
1411
            help='delete objects prefixed with <object><delimiter>')
1412

    
1413
    @errors.generic.all
1414
    @errors.pithos.connection
1415
    @errors.pithos.container
1416
    @errors.pithos.object_path
1417
    def _run(self):
1418
        if self.path:
1419
            if self['yes'] or ask_user(
1420
                    'Delete %s:%s ?' % (self.container, self.path)):
1421
                self.client.del_object(
1422
                    self.path,
1423
                    until=self['until'],
1424
                    delimiter=self['delimiter'])
1425
            else:
1426
                print('Aborted')
1427
        else:
1428
            if self['recursive']:
1429
                ask_msg = 'Delete container contents'
1430
            else:
1431
                ask_msg = 'Delete container'
1432
            if self['yes'] or ask_user('%s %s ?' % (ask_msg, self.container)):
1433
                self.client.del_container(
1434
                    until=self['until'],
1435
                    delimiter=self['delimiter'])
1436
            else:
1437
                print('Aborted')
1438

    
1439
    def main(self, container____path__=None):
1440
        super(self.__class__, self)._run(container____path__)
1441
        self._run()
1442

    
1443

    
1444
@command(pithos_cmds)
1445
class store_purge(_store_container_command):
1446
    """Delete a container and release related data blocks
1447
    Non-empty containers can not purged.
1448
    To purge a container with content:
1449
    .   /store delete -r <container>
1450
    .      objects are deleted, but data blocks remain on server
1451
    .   /store purge <container>
1452
    .      container and data blocks are released and deleted
1453
    """
1454

    
1455
    arguments = dict(
1456
        yes=FlagArgument('Do not prompt for permission', '--yes'),
1457
    )
1458

    
1459
    @errors.generic.all
1460
    @errors.pithos.connection
1461
    @errors.pithos.container
1462
    def _run(self):
1463
        if self['yes'] or ask_user('Purge container %s?' % self.container):
1464
                self.client.purge_container()
1465
        else:
1466
            print('Aborted')
1467

    
1468
    def main(self, container=None):
1469
        super(self.__class__, self)._run(container)
1470
        if container and self.container != container:
1471
            raiseCLIError('Invalid container name %s' % container, details=[
1472
                'Did you mean "%s" ?' % self.container,
1473
                'Use --container for names containing :'])
1474
        self._run()
1475

    
1476

    
1477
@command(pithos_cmds)
1478
class store_publish(_store_container_command):
1479
    """Publish the object and print the public url"""
1480

    
1481
    @errors.generic.all
1482
    @errors.pithos.connection
1483
    @errors.pithos.container
1484
    @errors.pithos.object_path
1485
    def _run(self):
1486
        url = self.client.publish_object(self.path)
1487
        print(url)
1488

    
1489
    def main(self, container___path):
1490
        super(self.__class__, self)._run(
1491
            container___path,
1492
            path_is_optional=False)
1493
        self._run()
1494

    
1495

    
1496
@command(pithos_cmds)
1497
class store_unpublish(_store_container_command):
1498
    """Unpublish an object"""
1499

    
1500
    @errors.generic.all
1501
    @errors.pithos.connection
1502
    @errors.pithos.container
1503
    @errors.pithos.object_path
1504
    def _run(self):
1505
            self.client.unpublish_object(self.path)
1506

    
1507
    def main(self, container___path):
1508
        super(self.__class__, self)._run(
1509
            container___path,
1510
            path_is_optional=False)
1511
        self._run()
1512

    
1513

    
1514
@command(pithos_cmds)
1515
class store_permissions(_store_container_command):
1516
    """Get read and write permissions of an object
1517
    Permissions are lists of users and user groups. There is read and write
1518
    permissions. Users and groups with write permission have also read
1519
    permission.
1520
    """
1521

    
1522
    @errors.generic.all
1523
    @errors.pithos.connection
1524
    @errors.pithos.container
1525
    @errors.pithos.object_path
1526
    def _run(self):
1527
        r = self.client.get_object_sharing(self.path)
1528
        print_dict(r)
1529

    
1530
    def main(self, container___path):
1531
        super(self.__class__, self)._run(
1532
            container___path,
1533
            path_is_optional=False)
1534
        self._run()
1535

    
1536

    
1537
@command(pithos_cmds)
1538
class store_setpermissions(_store_container_command):
1539
    """Set permissions for an object
1540
    New permissions overwrite existing permissions.
1541
    Permission format:
1542
    -   read=<username>[,usergroup[,...]]
1543
    -   write=<username>[,usegroup[,...]]
1544
    E.g. to give read permissions for file F to users A and B and write for C:
1545
    .       /store setpermissions F read=A,B write=C
1546
    """
1547

    
1548
    @errors.generic.all
1549
    def format_permition_dict(self, permissions):
1550
        read = False
1551
        write = False
1552
        for perms in permissions:
1553
            splstr = perms.split('=')
1554
            if 'read' == splstr[0]:
1555
                read = [ug.strip() for ug in splstr[1].split(',')]
1556
            elif 'write' == splstr[0]:
1557
                write = [ug.strip() for ug in splstr[1].split(',')]
1558
            else:
1559
                msg = 'Usage:\tread=<groups,users> write=<groups,users>'
1560
                raiseCLIError(None, msg)
1561
        return (read, write)
1562

    
1563
    @errors.generic.all
1564
    @errors.pithos.connection
1565
    @errors.pithos.container
1566
    @errors.pithos.object_path
1567
    def _run(self, read, write):
1568
        self.client.set_object_sharing(
1569
            self.path,
1570
            read_permition=read,
1571
            write_permition=write)
1572

    
1573
    def main(self, container___path, *permissions):
1574
        super(self.__class__, self)._run(
1575
            container___path,
1576
            path_is_optional=False)
1577
        (read, write) = self.format_permition_dict(permissions)
1578
        self._run(read, write)
1579

    
1580

    
1581
@command(pithos_cmds)
1582
class store_delpermissions(_store_container_command):
1583
    """Delete all permissions set on object
1584
    To modify permissions, use /store setpermssions
1585
    """
1586

    
1587
    @errors.generic.all
1588
    @errors.pithos.connection
1589
    @errors.pithos.container
1590
    @errors.pithos.object_path
1591
    def _run(self):
1592
        self.client.del_object_sharing(self.path)
1593

    
1594
    def main(self, container___path):
1595
        super(self.__class__, self)._run(
1596
            container___path,
1597
            path_is_optional=False)
1598
        self._run()
1599

    
1600

    
1601
@command(pithos_cmds)
1602
class store_info(_store_container_command):
1603
    """Get detailed information for user account, containers or objects
1604
    to get account info:    /store info
1605
    to get container info:  /store info <container>
1606
    to get object info:     /store info <container>:<path>
1607
    """
1608

    
1609
    arguments = dict(
1610
        object_version=ValueArgument(
1611
            'show specific version \ (applies only for objects)',
1612
            '--object-version')
1613
    )
1614

    
1615
    @errors.generic.all
1616
    @errors.pithos.connection
1617
    @errors.pithos.container
1618
    @errors.pithos.object_path
1619
    def _run(self):
1620
        if self.container is None:
1621
            r = self.client.get_account_info()
1622
        elif self.path is None:
1623
            r = self.client.get_container_info(self.container)
1624
        else:
1625
            r = self.client.get_object_info(
1626
                self.path,
1627
                version=self['object_version'])
1628
        print_dict(r)
1629

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

    
1634

    
1635
@command(pithos_cmds)
1636
class store_meta(_store_container_command):
1637
    """Get metadata for account, containers or objects"""
1638

    
1639
    arguments = dict(
1640
        detail=FlagArgument('show detailed output', '-l'),
1641
        until=DateArgument('show metadata until then', '--until'),
1642
        object_version=ValueArgument(
1643
            'show specific version \ (applies only for objects)',
1644
            '--object-version')
1645
    )
1646

    
1647
    @errors.generic.all
1648
    @errors.pithos.connection
1649
    @errors.pithos.container
1650
    @errors.pithos.object_path
1651
    def _run(self):
1652
        until = self['until']
1653
        if self.container is None:
1654
            if self['detail']:
1655
                r = self.client.get_account_info(until=until)
1656
            else:
1657
                r = self.client.get_account_meta(until=until)
1658
                r = pretty_keys(r, '-')
1659
            if r:
1660
                print(bold(self.client.account))
1661
        elif self.path is None:
1662
            if self['detail']:
1663
                r = self.client.get_container_info(until=until)
1664
            else:
1665
                cmeta = self.client.get_container_meta(until=until)
1666
                ometa = self.client.get_container_object_meta(until=until)
1667
                r = {}
1668
                if cmeta:
1669
                    r['container-meta'] = pretty_keys(cmeta, '-')
1670
                if ometa:
1671
                    r['object-meta'] = pretty_keys(ometa, '-')
1672
        else:
1673
            if self['detail']:
1674
                r = self.client.get_object_info(
1675
                    self.path,
1676
                    version=self['object_version'])
1677
            else:
1678
                r = self.client.get_object_meta(
1679
                    self.path,
1680
                    version=self['object_version'])
1681
            if r:
1682
                r = pretty_keys(pretty_keys(r, '-'))
1683
        if r:
1684
            print_dict(r)
1685

    
1686
    def main(self, container____path__=None):
1687
        super(self.__class__, self)._run(container____path__)
1688
        self._run()
1689

    
1690

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

    
1697
    @errors.generic.all
1698
    @errors.pithos.connection
1699
    @errors.pithos.container
1700
    @errors.pithos.object_path
1701
    def _run(self, metakey, metaval):
1702
        if not self.container:
1703
            self.client.set_account_meta({metakey: metaval})
1704
        elif not self.path:
1705
            self.client.set_container_meta({metakey: metaval})
1706
        else:
1707
            self.client.set_object_meta(self.path, {metakey: metaval})
1708

    
1709
    def main(self, metakey, metaval, container____path__=None):
1710
        super(self.__class__, self)._run(container____path__)
1711
        self._run(metakey=metakey, metaval=metaval)
1712

    
1713

    
1714
@command(pithos_cmds)
1715
class store_delmeta(_store_container_command):
1716
    """Delete metadata with given key from account, container or object
1717
    Metadata are formed as key:value objects
1718
    - to get metadata of current account:     /store meta
1719
    - to get metadata of a container:         /store meta <container>
1720
    - to get metadata of an object:           /store meta <container>:<path>
1721
    """
1722

    
1723
    @errors.generic.all
1724
    @errors.pithos.connection
1725
    @errors.pithos.container
1726
    @errors.pithos.object_path
1727
    def _run(self, metakey):
1728
        if self.container is None:
1729
            self.client.del_account_meta(metakey)
1730
        elif self.path is None:
1731
            self.client.del_container_meta(metakey)
1732
        else:
1733
            self.client.del_object_meta(self.path, metakey)
1734

    
1735
    def main(self, metakey, container____path__=None):
1736
        super(self.__class__, self)._run(container____path__)
1737
        self._run(metakey)
1738

    
1739

    
1740
@command(pithos_cmds)
1741
class store_quota(_store_account_command):
1742
    """Get quota for account or container"""
1743

    
1744
    arguments = dict(
1745
        in_bytes=FlagArgument('Show result in bytes', ('-b', '--bytes'))
1746
    )
1747

    
1748
    @errors.generic.all
1749
    @errors.pithos.connection
1750
    @errors.pithos.container
1751
    def _run(self):
1752
        if self.container:
1753
            reply = self.client.get_container_quota(self.container)
1754
        else:
1755
            reply = self.client.get_account_quota()
1756
        if not self['in_bytes']:
1757
            for k in reply:
1758
                reply[k] = format_size(reply[k])
1759
        print_dict(pretty_keys(reply, '-'))
1760

    
1761
    def main(self, container=None):
1762
        super(self.__class__, self)._run()
1763
        self.container = container
1764
        self._run()
1765

    
1766

    
1767
@command(pithos_cmds)
1768
class store_setquota(_store_account_command):
1769
    """Set new quota for account or container
1770
    By default, quota is set in bytes
1771
    Users may specify a different unit, e.g:
1772
    /store setquota 2.3GB mycontainer
1773
    Accepted units: B, KiB (1024 B), KB (1000 B), MiB, MB, GiB, GB, TiB, TB
1774
    """
1775

    
1776
    @errors.generic.all
1777
    def _calculate_quota(self, user_input):
1778
        quota = 0
1779
        try:
1780
            quota = int(user_input)
1781
        except ValueError:
1782
            index = 0
1783
            digits = [str(num) for num in range(0, 10)] + ['.']
1784
            while user_input[index] in digits:
1785
                index += 1
1786
            quota = user_input[:index]
1787
            format = user_input[index:]
1788
            try:
1789
                return to_bytes(quota, format)
1790
            except Exception as qe:
1791
                msg = 'Failed to convert %s to bytes' % user_input,
1792
                raiseCLIError(qe, msg, details=[
1793
                    'Syntax: setquota <quota>[format] [container]',
1794
                    'e.g.: setquota 2.3GB mycontainer',
1795
                    'Acceptable formats:',
1796
                    '(*1024): B, KiB, MiB, GiB, TiB',
1797
                    '(*1000): B, KB, MB, GB, TB'])
1798
        return quota
1799

    
1800
    @errors.generic.all
1801
    @errors.pithos.connection
1802
    @errors.pithos.container
1803
    def _run(self, quota):
1804
        if self.container:
1805
            self.client.container = self.container
1806
            self.client.set_container_quota(quota)
1807
        else:
1808
            self.client.set_account_quota(quota)
1809

    
1810
    def main(self, quota, container=None):
1811
        super(self.__class__, self)._run()
1812
        quota = self._calculate_quota(quota)
1813
        self.container = container
1814
        self._run(quota)
1815

    
1816

    
1817
@command(pithos_cmds)
1818
class store_versioning(_store_account_command):
1819
    """Get  versioning for account or container"""
1820

    
1821
    @errors.generic.all
1822
    @errors.pithos.connection
1823
    @errors.pithos.container
1824
    def _run(self):
1825
        if self.container:
1826
            r = self.client.get_container_versioning(self.container)
1827
        else:
1828
            r = self.client.get_account_versioning()
1829
        print_dict(r)
1830

    
1831
    def main(self, container=None):
1832
        super(self.__class__, self)._run()
1833
        self.container = container
1834
        self._run()
1835

    
1836

    
1837
@command(pithos_cmds)
1838
class store_setversioning(_store_account_command):
1839
    """Set versioning mode (auto, none) for account or container"""
1840

    
1841
    def _check_versioning(self, versioning):
1842
        if versioning and versioning.lower() in ('auto', 'none'):
1843
            return versioning.lower()
1844
        raiseCLIError('Invalid versioning %s' % versioning, details=[
1845
            'Versioning can be auto or none'])
1846

    
1847
    @errors.generic.all
1848
    @errors.pithos.connection
1849
    @errors.pithos.container
1850
    def _run(self, versioning):
1851
        if self.container:
1852
            self.client.container = self.container
1853
            self.client.set_container_versioning(versioning)
1854
        else:
1855
            self.client.set_account_versioning(versioning)
1856

    
1857
    def main(self, versioning, container=None):
1858
        super(self.__class__, self)._run()
1859
        self._run(self._check_versioning(versioning))
1860

    
1861

    
1862
@command(pithos_cmds)
1863
class store_group(_store_account_command):
1864
    """Get groups and group members"""
1865

    
1866
    @errors.generic.all
1867
    @errors.pithos.connection
1868
    def _run(self):
1869
        r = self.client.get_account_group()
1870
        print_dict(pretty_keys(r, '-'))
1871

    
1872
    def main(self):
1873
        super(self.__class__, self)._run()
1874
        self._run()
1875

    
1876

    
1877
@command(pithos_cmds)
1878
class store_setgroup(_store_account_command):
1879
    """Set a user group"""
1880

    
1881
    @errors.generic.all
1882
    @errors.pithos.connection
1883
    def _run(self, groupname, *users):
1884
        self.client.set_account_group(groupname, users)
1885

    
1886
    def main(self, groupname, *users):
1887
        super(self.__class__, self)._run()
1888
        if users:
1889
            self._run(groupname, *users)
1890
        else:
1891
            raiseCLIError('No users to add in group %s' % groupname)
1892

    
1893

    
1894
@command(pithos_cmds)
1895
class store_delgroup(_store_account_command):
1896
    """Delete a user group"""
1897

    
1898
    @errors.generic.all
1899
    @errors.pithos.connection
1900
    def _run(self, groupname):
1901
        self.client.del_account_group(groupname)
1902

    
1903
    def main(self, groupname):
1904
        super(self.__class__, self)._run()
1905
        self._run(groupname)
1906

    
1907

    
1908
@command(pithos_cmds)
1909
class store_sharers(_store_account_command):
1910
    """List the accounts that share objects with current user"""
1911

    
1912
    arguments = dict(
1913
        detail=FlagArgument('show detailed output', '-l'),
1914
        marker=ValueArgument('show output greater then marker', '--marker')
1915
    )
1916

    
1917
    @errors.generic.all
1918
    @errors.pithos.connection
1919
    def _run(self):
1920
        accounts = self.client.get_sharing_accounts(marker=self['marker'])
1921
        if self['detail']:
1922
            print_items(accounts)
1923
        else:
1924
            print_items([acc['name'] for acc in accounts])
1925

    
1926
    def main(self):
1927
        super(self.__class__, self)._run()
1928
        self._run()
1929

    
1930

    
1931
@command(pithos_cmds)
1932
class store_versions(_store_container_command):
1933
    """Get the list of object versions
1934
    Deleted objects may still have versions that can be used to restore it and
1935
    get information about its previous state.
1936
    The version number can be used in a number of other commands, like info,
1937
    copy, move, meta. See these commands for more information, e.g.
1938
    /store info -h
1939
    """
1940

    
1941
    @errors.generic.all
1942
    @errors.pithos.connection
1943
    @errors.pithos.container
1944
    @errors.pithos.object_path
1945
    def _run(self):
1946
        versions = self.client.get_object_versionlist(self.path)
1947
        print_items([dict(id=vitem[0], created=strftime(
1948
            '%d-%m-%Y %H:%M:%S',
1949
            localtime(float(vitem[1])))) for vitem in versions])
1950

    
1951
    def main(self, container___path):
1952
        super(store_versions, self)._run(
1953
            container___path,
1954
            path_is_optional=False)
1955
        self._run()