Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / pithos_cli.py @ 8741c407

History | View | Annotate | Download (69 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
            ('-A', '--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
            ('-C', '--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('detailed output', ('-l', '--list')),
310
        limit=IntArgument('limit number of listed items', ('-n', '--number')),
311
        marker=ValueArgument('output greater that marker', '--marker'),
312
        prefix=ValueArgument('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('', ('a', '--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
            ('-a', '--dst-account')),
674
        destination_container=ValueArgument(
675
            'use it if destination container name contains a : character',
676
            ('-D', '--dst-container')),
677
        source_version=ValueArgument(
678
            'copy specific version',
679
            ('-S', '--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
            ('-a', '--dst-account')),
765
        destination_container=ValueArgument(
766
            'use it if destination container name contains a : character',
767
            ('-D', '--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
            ('-N', '--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
            ('-N', '--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
            ('-N', '--no-progress-bar'),
1025
            default=False),
1026
        overwrite=FlagArgument('Force (over)write', ('-f', '--force'))
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
                    else:
1080
                        hash_cb = None
1081
                    self.client.upload_object(
1082
                        remote_path,
1083
                        f,
1084
                        hash_cb=hash_cb,
1085
                        upload_cb=upload_cb,
1086
                        **params)
1087
                except Exception:
1088
                    self._safe_progress_bar_finish(progress_bar)
1089
                    raise
1090
                finally:
1091
                    self._safe_progress_bar_finish(progress_bar)
1092
        print 'Upload completed'
1093

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

    
1099

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

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

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

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

    
1142

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

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

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

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

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

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

    
1336

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

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

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

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

    
1377

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

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

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

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

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

    
1442

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

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

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

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

    
1475

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

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

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

    
1494

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

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

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

    
1512

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

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

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

    
1535

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

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

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

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

    
1579

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

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

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

    
1599

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

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

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

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

    
1633

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

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

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

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

    
1689

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

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

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

    
1712

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

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

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

    
1738

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

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

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

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

    
1765

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

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

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

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

    
1815

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

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

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

    
1835

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

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

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

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

    
1860

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

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

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

    
1875

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

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

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

    
1892

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

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

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

    
1906

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

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

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

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

    
1929

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

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

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