Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / pithos_cli.py @ 6736f171

History | View | Annotate | Download (67 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
    @errors.generic.all
222
    def _dest_container_path(self, dest_container_path):
223
        if self['destination_container']:
224
            return (self['destination_container'], dest_container_path)
225
        if dest_container_path:
226
            dst = dest_container_path.split(':')
227
            if len(dst) > 1:
228
                try:
229
                    self.client.container = dst[0]
230
                    self.client.get_container_info(dst[0])
231
                except ClientError as err:
232
                    if err.status in (404, 204):
233
                        raiseCLIError(
234
                            'Destination container %s not found' % dst[0])
235
                    raise
236
                return (dst[0], dst[1])
237
            return(None, dst[0])
238
        raiseCLIError('No destination container:path provided')
239

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

    
260
        user_cont, sep, userpath = container_with_path.partition(':')
261

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

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

    
313
    def main(self, container_with_path=None, path_is_optional=True):
314
        self._run(container_with_path, path_is_optional)
315

    
316

    
317
@command(pithos_cmds)
318
class store_list(_store_container_command):
319
    """List containers, object trees or objects in a directory
320
    Use with:
321
    1 no parameters : containers in current account
322
    2. one parameter (container) or --container : contents of container
323
    3. <container>:<prefix> or --container=<container> <prefix>: objects in
324
    .   container starting with prefix
325
    """
326

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

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

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

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

    
442
    def main(self, container____path__=None):
443
        super(self.__class__, self)._run(container____path__)
444
        self._run()
445

    
446

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

    
451
    __doc__ += '\n. '.join([
452
        'Kamaki hanldes directories the same way as OOS Storage and Pithos+:',
453
        'A   directory  is   an  object  with  type  "application/directory"',
454
        'An object with path  dir/name can exist even if  dir does not exist',
455
        'or even if dir  is  a non  directory  object.  Users can modify dir',
456
        'without affecting the dir/name object in any way.'])
457

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

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

    
470

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

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

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

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

    
496

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

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

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

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

    
528

    
529
@command(pithos_cmds)
530
class store_copy(_store_container_command):
531
    """Copy objects from container to (another) container
532
    Semantics:
533
    copy cont:path dir
534
    .   transfer path as dir/path
535
    copy cont:path cont2:
536
    .   trasnfer all <obj> prefixed with path to container cont2
537
    copy cont:path [cont2:]path2
538
    .   transfer path to path2
539
    Use options:
540
    1. <container1>:<path1> [container2:]<path2> : if container2 is not given,
541
    destination is container1:path2
542
    2. <container>:<path1> <path2> : make a copy in the same container
543
    3. Can use --container= instead of <container1>
544
    """
545

    
546
    arguments = dict(
547
        destination_container=ValueArgument(
548
            'use it if destination container name contains a : character',
549
            '--dst-container'),
550
        source_version=ValueArgument(
551
            'copy specific version',
552
            '--source-version'),
553
        public=ValueArgument('make object publicly accessible', '--public'),
554
        content_type=ValueArgument(
555
            'change object\'s content type',
556
            '--content-type'),
557
        recursive=FlagArgument(
558
            'copy directory and contents',
559
            ('-r', '--recursive')),
560
        prefix=FlagArgument(
561
            'Match objects prefixed with src path (feels like src_path*)',
562
            '--with-prefix',
563
            default=''),
564
        suffix=ValueArgument(
565
            'Suffix of source objects (feels like *suffix)',
566
            '--with-suffix',
567
            default=''),
568
        add_prefix=ValueArgument('Prefix targets', '--add-prefix', default=''),
569
        add_suffix=ValueArgument('Suffix targets', '--add-suffix', default=''),
570
        prefix_replace=ValueArgument(
571
            'Prefix of src to replace with dst path + add_prefix, if matched',
572
            '--prefix-to-replace',
573
            default=''),
574
        suffix_replace=ValueArgument(
575
            'Suffix of src to replace with add_suffix, if matched',
576
            '--suffix-to-replace',
577
            default='')
578
    )
579

    
580
    def _get_all(self, prefix):
581
        return self.client.container_get(prefix=prefix).json
582

    
583
    def _get_src_objects(self, src_cnt, src_path):
584
        """Get a list of the source objects to be called
585

586
        :param src_cnt: (str) source container
587

588
        :param src_path: (str) source path
589

590
        :returns: (method, params) a method that returns a list when called
591
        or (object) if it is a single object
592
        """
593
        if src_path and src_path[-1] == '/':
594
            src_path = src_path[:-1]
595
        self.client.container = src_cnt
596

    
597
        if self['prefix']:
598
            return (self._get_all, dict(prefix=src_path))
599
        try:
600
            srcobj = self.client.get_object_info(src_path)
601
        except ClientError as srcerr:
602
            if srcerr.status == 404:
603
                raiseCLIError(
604
                    'Source object %s not in cont. %s' % (src_path, src_cnt),
605
                    details=['Hint: --with-prefix to match multiple objects'])
606
            elif srcerr.status not in (204,):
607
                raise
608
            return (self.client.list_objects, {})
609
        if self._is_dir(srcobj):
610
            if not self['recursive']:
611
                raiseCLIError(
612
                    'Object %s of cont. %s is a dir' % (src_path, src_cnt),
613
                    details=['Use --recursive to access directories'])
614
            return (self._get_all, dict(prefix=src_path))
615
        srcobj['name'] = src_path
616
        return srcobj
617

    
618
    def _objlist(self, dst_cont, dst_path):
619
        src_iter = self._get_src_objects(self.container, self.path)
620
        src_N = isinstance(src_iter, tuple)
621
        add_prefix = self['add_prefix'].strip('/')
622

    
623
        if dst_path and dst_path.endswith('/'):
624
            dst_path = dst_path[:-1]
625

    
626
        self.client.container = dst_cont
627
        try:
628
            dstobj = self.client.get_object_info(dst_path)
629
        except ClientError as trgerr:
630
            if trgerr.status in (404,):
631
                if src_N:
632
                    raiseCLIError(
633
                        'Cannot merge multiple paths to path %s' % dst_path,
634
                        details=[
635
                            'Try to use / or a directory as destination',
636
                            'or create the destination dir (/store mkdir)',
637
                            'or use a single object as source'])
638
            elif trgerr.status not in (204,):
639
                raise
640
        else:
641
            if self._is_dir(dstobj):
642
                add_prefix = '%s/%s' % (dst_path.strip('/'), add_prefix)
643
            elif src_N:
644
                raiseCLIError(
645
                    'Cannot merge multiple paths to path' % dst_path,
646
                    details=[
647
                        'Try to use / or a directory as destination',
648
                        'or create the destination dir (/store mkdir)',
649
                        'or use a single object as source'])
650

    
651
        self.client.container = self.container
652
        if src_N:
653
            (method, kwargs) = src_iter
654
            for obj in method(**kwargs):
655
                name = obj['name']
656
                if name.endswith(self['suffix']):
657
                    yield (name, self._get_new_object(name, add_prefix))
658
        elif src_iter['name'].endswith(self['suffix']):
659
            name = src_iter['name']
660
            yield (name, self._get_new_object(dst_path or name, add_prefix))
661
        else:
662
            raiseCLIError('Source path %s conflicts with suffix %s' % (
663
                src_iter['name'],
664
                self['suffix']))
665

    
666
    def _get_new_object(self, obj, add_prefix):
667
        if self['prefix_replace'] and obj.startswith(self['prefix_replace']):
668
            obj = obj[len(self['prefix_replace']):]
669
        if self['suffix_replace'] and obj.endswith(self['suffix_replace']):
670
            obj = obj[:-len(self['suffix_replace'])]
671
        return add_prefix + obj + self['add_suffix']
672

    
673
    @errors.generic.all
674
    @errors.pithos.connection
675
    @errors.pithos.container
676
    def _run(self, dst_cont, dst_path):
677
        no_source_object = True
678
        for src_object, dst_object in self._objlist(dst_cont, dst_path):
679
            no_source_object = False
680
            self.client.copy_object(
681
                src_container=self.container,
682
                src_object=src_object,
683
                dst_container=dst_cont,
684
                dst_object=dst_object,
685
                source_version=self['source_version'],
686
                public=self['public'],
687
                content_type=self['content_type'])
688
        if no_source_object:
689
            raiseCLIError('No object %s in container %s' % (
690
                self.path,
691
                self.container))
692

    
693
    def main(
694
            self,
695
            source_container___path,
696
            destination_container___path=None):
697
        super(self.__class__, self)._run(
698
            source_container___path,
699
            path_is_optional=False)
700
        (dst_cont, dst_path) = self._dest_container_path(
701
            destination_container___path)
702
        self._run(dst_cont=dst_cont or self.container, dst_path=dst_path or '')
703

    
704

    
705
@command(pithos_cmds)
706
class store_move(_store_container_command):
707
    """Move/rename objects
708
    Semantics:
709
    move cont:path path2
710
    .   will move all <obj> prefixed with path, as path2<obj>
711
    .   or as path2 if path corresponds to just one whole object
712
    move cont:path cont2:
713
    .   will move all <obj> prefixed with path to container cont2
714
    move cont:path [cont2:]path2 --exact-match
715
    .   will move at most one <obj> as a new object named path2,
716
    .   provided path corresponds to a whole object path
717
    move cont:path [cont2:]path2 --replace
718
    .   will move all <obj> prefixed with path, replacing path with path2
719
    where <obj> is a full file or directory object path.
720
    Use options:
721
    1. <container1>:<path1> [container2:]<path2> : if container2 not given,
722
    destination is container1:path2
723
    2. <container>:<path1> path2 : rename
724
    3. Can use --container= instead of <container1>
725
    """
726

    
727
    arguments = dict(
728
        destination_container=ValueArgument(
729
            'use it if destination container name contains a : character',
730
            '--dst-container'),
731
        source_version=ValueArgument('specify version', '--source-version'),
732
        public=FlagArgument('make object publicly accessible', '--public'),
733
        content_type=ValueArgument('modify content type', '--content-type'),
734
        recursive=FlagArgument('up to delimiter /', ('-r', '--recursive')),
735
        exact_match=FlagArgument(
736
            'Copy only the object that fully matches path',
737
            '--exact-match'),
738
        replace=FlagArgument('Replace src. path with dst. path', '--replace')
739
    )
740

    
741
    def _objlist(self, dst_path):
742
        if self['exact_match']:
743
            return [(dst_path or self.path, self.path)]
744
        r = self.client.container_get(prefix=self.path)
745
        if len(r.json) == 1:
746
            obj = r.json[0]
747
            return [(obj['name'], dst_path or obj['name'])]
748
        return [(
749
            obj['name'],
750
            '%s%s' % (
751
                dst_path,
752
                obj['name'][len(self.path) if self['replace'] else 0:]
753
            )) for obj in r.json]
754

    
755
    @errors.generic.all
756
    @errors.pithos.connection
757
    @errors.pithos.container
758
    def _run(self, dst_cont, dst_path):
759
        no_source_object = True
760
        for src_object, dst_object in self._objlist(dst_path):
761
            no_source_object = False
762
            self.client.move_object(
763
                src_container=self.container,
764
                src_object=src_object,
765
                dst_container=dst_cont or self.container,
766
                dst_object=dst_object,
767
                source_version=self['source_version'],
768
                public=self['public'],
769
                content_type=self['content_type'])
770
        if no_source_object:
771
            raiseCLIError('No object %s in container %s' % (
772
                self.path,
773
                self.container))
774

    
775
    def main(
776
            self,
777
            source_container___path,
778
            destination_container___path=None):
779
        super(self.__class__, self)._run(
780
            source_container___path,
781
            path_is_optional=False)
782
        (dst_cont, dst_path) = self._dest_container_path(
783
            destination_container___path)
784
        self._run(dst_cont=dst_cont, dst_path=dst_path or '')
785

    
786

    
787
@command(pithos_cmds)
788
class store_append(_store_container_command):
789
    """Append local file to (existing) remote object
790
    The remote object should exist.
791
    If the remote object is a directory, it is transformed into a file.
792
    In the later case, objects under the directory remain intact.
793
    """
794

    
795
    arguments = dict(
796
        progress_bar=ProgressBarArgument(
797
            'do not show progress bar',
798
            '--no-progress-bar',
799
            default=False)
800
    )
801

    
802
    @errors.generic.all
803
    @errors.pithos.connection
804
    @errors.pithos.container
805
    @errors.pithos.object_path
806
    def _run(self, local_path):
807
        (progress_bar, upload_cb) = self._safe_progress_bar('Appending')
808
        try:
809
            f = open(local_path, 'rb')
810
            self.client.append_object(self.path, f, upload_cb)
811
        except Exception:
812
            self._safe_progress_bar_finish(progress_bar)
813
            raise
814
        finally:
815
            self._safe_progress_bar_finish(progress_bar)
816

    
817
    def main(self, local_path, container___path):
818
        super(self.__class__, self)._run(
819
            container___path,
820
            path_is_optional=False)
821
        self._run(local_path)
822

    
823

    
824
@command(pithos_cmds)
825
class store_truncate(_store_container_command):
826
    """Truncate remote file up to a size (default is 0)"""
827

    
828
    @errors.generic.all
829
    @errors.pithos.connection
830
    @errors.pithos.container
831
    @errors.pithos.object_path
832
    @errors.pithos.object_size
833
    def _run(self, size=0):
834
        self.client.truncate_object(self.path, size)
835

    
836
    def main(self, container___path, size=0):
837
        super(self.__class__, self)._run(container___path)
838
        self._run(size=size)
839

    
840

    
841
@command(pithos_cmds)
842
class store_overwrite(_store_container_command):
843
    """Overwrite part (from start to end) of a remote file
844
    overwrite local-path container 10 20
845
    .   will overwrite bytes from 10 to 20 of a remote file with the same name
846
    .   as local-path basename
847
    overwrite local-path container:path 10 20
848
    .   will overwrite as above, but the remote file is named path
849
    """
850

    
851
    arguments = dict(
852
        progress_bar=ProgressBarArgument(
853
            'do not show progress bar',
854
            '--no-progress-bar',
855
            default=False)
856
    )
857

    
858
    def _open_file(self, local_path, start):
859
        f = open(path.abspath(local_path), 'rb')
860
        f.seek(0, 2)
861
        f_size = f.tell()
862
        f.seek(start, 0)
863
        return (f, f_size)
864

    
865
    @errors.generic.all
866
    @errors.pithos.connection
867
    @errors.pithos.container
868
    @errors.pithos.object_path
869
    @errors.pithos.object_size
870
    def _run(self, local_path, start, end):
871
        (start, end) = (int(start), int(end))
872
        (f, f_size) = self._open_file(local_path, start)
873
        (progress_bar, upload_cb) = self._safe_progress_bar(
874
            'Overwrite %s bytes' % (end - start))
875
        try:
876
            self.client.overwrite_object(
877
                obj=self.path,
878
                start=start,
879
                end=end,
880
                source_file=f,
881
                upload_cb=upload_cb)
882
        except Exception:
883
            self._safe_progress_bar_finish(progress_bar)
884
            raise
885
        finally:
886
            self._safe_progress_bar_finish(progress_bar)
887

    
888
    def main(self, local_path, container___path, start, end):
889
        super(self.__class__, self)._run(
890
            container___path,
891
            path_is_optional=None)
892
        self.path = self.path or path.basename(local_path)
893
        self._run(local_path=local_path, start=start, end=end)
894

    
895

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

    
909
    arguments = dict(
910
        etag=ValueArgument('check written data', '--etag'),
911
        content_encoding=ValueArgument(
912
            'set MIME content type',
913
            '--content-encoding'),
914
        content_disposition=ValueArgument(
915
            'the presentation style of the object',
916
            '--content-disposition'),
917
        content_type=ValueArgument(
918
            'specify content type',
919
            '--content-type',
920
            default='application/octet-stream'),
921
        sharing=SharingArgument(
922
            '\n'.join([
923
                'define object sharing policy',
924
                '    ( "read=user1,grp1,user2,... write=user1,grp2,..." )']),
925
            '--sharing'),
926
        public=FlagArgument('make object publicly accessible', '--public')
927
    )
928

    
929
    @errors.generic.all
930
    @errors.pithos.connection
931
    @errors.pithos.container
932
    @errors.pithos.object_path
933
    def _run(self):
934
        self.client.create_object_by_manifestation(
935
            self.path,
936
            content_encoding=self['content_encoding'],
937
            content_disposition=self['content_disposition'],
938
            content_type=self['content_type'],
939
            sharing=self['sharing'],
940
            public=self['public'])
941

    
942
    def main(self, container___path):
943
        super(self.__class__, self)._run(
944
            container___path,
945
            path_is_optional=False)
946
        self.run()
947

    
948

    
949
@command(pithos_cmds)
950
class store_upload(_store_container_command):
951
    """Upload a file"""
952

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

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

    
998
    @errors.generic.all
999
    @errors.pithos.connection
1000
    @errors.pithos.container
1001
    @errors.pithos.object_path
1002
    @errors.pithos.local_path
1003
    def _run(self, local_path, remote_path):
1004
        poolsize = self['poolsize']
1005
        if poolsize > 0:
1006
            self.client.POOL_SIZE = int(poolsize)
1007
        params = dict(
1008
            content_encoding=self['content_encoding'],
1009
            content_type=self['content_type'],
1010
            content_disposition=self['content_disposition'],
1011
            sharing=self['sharing'],
1012
            public=self['public'])
1013
        remote_path = self._remote_path(remote_path, local_path)
1014
        with open(path.abspath(local_path), 'rb') as f:
1015
            if self['unchunked']:
1016
                self.client.upload_object_unchunked(
1017
                    remote_path,
1018
                    f,
1019
                    etag=self['etag'],
1020
                    withHashFile=self['use_hashes'],
1021
                    **params)
1022
            else:
1023
                try:
1024
                    (progress_bar, upload_cb) = self._safe_progress_bar(
1025
                        'Uploading')
1026
                    if progress_bar:
1027
                        hash_bar = progress_bar.clone()
1028
                        hash_cb = hash_bar.get_generator(
1029
                            'Calculating block hashes'
1030
                        )
1031
                    else:
1032
                        hash_cb = None
1033
                    self.client.upload_object(
1034
                        remote_path,
1035
                        f,
1036
                        hash_cb=hash_cb,
1037
                        upload_cb=upload_cb,
1038
                        **params)
1039
                except Exception:
1040
                    self._safe_progress_bar_finish(progress_bar)
1041
                    raise
1042
                finally:
1043
                    self._safe_progress_bar_finish(progress_bar)
1044
        print 'Upload completed'
1045

    
1046
    def main(self, local_path, container____path__=None):
1047
        super(self.__class__, self)._run(container____path__)
1048
        remote_path = self.path or path.basename(local_path)
1049
        self._run(local_path=local_path, remote_path=remote_path)
1050

    
1051

    
1052
@command(pithos_cmds)
1053
class store_cat(_store_container_command):
1054
    """Print remote file contents to console"""
1055

    
1056
    arguments = dict(
1057
        range=RangeArgument('show range of data', '--range'),
1058
        if_match=ValueArgument('show output if ETags match', '--if-match'),
1059
        if_none_match=ValueArgument(
1060
            'show output if ETags match',
1061
            '--if-none-match'),
1062
        if_modified_since=DateArgument(
1063
            'show output modified since then',
1064
            '--if-modified-since'),
1065
        if_unmodified_since=DateArgument(
1066
            'show output unmodified since then',
1067
            '--if-unmodified-since'),
1068
        object_version=ValueArgument(
1069
            'get the specific version',
1070
            '--object-version')
1071
    )
1072

    
1073
    @errors.generic.all
1074
    @errors.pithos.connection
1075
    @errors.pithos.container
1076
    @errors.pithos.object_path
1077
    def _run(self):
1078
        self.client.download_object(
1079
            self.path,
1080
            stdout,
1081
            range_str=self['range'],
1082
            version=self['object_version'],
1083
            if_match=self['if_match'],
1084
            if_none_match=self['if_none_match'],
1085
            if_modified_since=self['if_modified_since'],
1086
            if_unmodified_since=self['if_unmodified_since'])
1087

    
1088
    def main(self, container___path):
1089
        super(self.__class__, self)._run(
1090
            container___path,
1091
            path_is_optional=False)
1092
        self._run()
1093

    
1094

    
1095
@command(pithos_cmds)
1096
class store_download(_store_container_command):
1097
    """Download remote object as local file
1098
    If local destination is a directory:
1099
    *   download <container>:<path> <local dir> -r
1100
    will download all files on <container> prefixed as <path>,
1101
    to <local dir>/<full path>
1102
    *   download <container>:<path> <local dir> --exact-match
1103
    will download only one file, exactly matching <path>
1104
    ATTENTION: to download cont:dir1/dir2/file there must exist objects
1105
    cont:dir1 and cont:dir1/dir2 of type application/directory
1106
    To create directory objects, use /store mkdir
1107
    """
1108

    
1109
    arguments = dict(
1110
        resume=FlagArgument('Resume instead of overwrite', '--resume'),
1111
        range=RangeArgument('show range of data', '--range'),
1112
        if_match=ValueArgument('show output if ETags match', '--if-match'),
1113
        if_none_match=ValueArgument(
1114
            'show output if ETags match',
1115
            '--if-none-match'),
1116
        if_modified_since=DateArgument(
1117
            'show output modified since then',
1118
            '--if-modified-since'),
1119
        if_unmodified_since=DateArgument(
1120
            'show output unmodified since then',
1121
            '--if-unmodified-since'),
1122
        object_version=ValueArgument(
1123
            'get the specific version',
1124
            '--object-version'),
1125
        poolsize=IntArgument('set pool size', '--with-pool-size'),
1126
        progress_bar=ProgressBarArgument(
1127
            'do not show progress bar',
1128
            '--no-progress-bar',
1129
            default=False),
1130
        recursive=FlagArgument(
1131
            'Download a remote path and all its contents',
1132
            '--recursive')
1133
    )
1134

    
1135
    def _outputs(self, local_path):
1136
        """:returns: (local_file, remote_path)"""
1137
        remotes = []
1138
        if self['recursive']:
1139
            r = self.client.container_get(
1140
                prefix=self.path or '/',
1141
                if_modified_since=self['if_modified_since'],
1142
                if_unmodified_since=self['if_unmodified_since'])
1143
            dirlist = dict()
1144
            for remote in r.json:
1145
                rname = remote['name'].strip('/')
1146
                tmppath = ''
1147
                for newdir in rname.strip('/').split('/')[:-1]:
1148
                    tmppath = '/'.join([tmppath, newdir])
1149
                    dirlist.update({tmppath.strip('/'): True})
1150
                remotes.append((rname, store_download._is_dir(remote)))
1151
            dir_remotes = [r[0] for r in remotes if r[1]]
1152
            if not set(dirlist).issubset(dir_remotes):
1153
                badguys = [bg.strip('/') for bg in set(
1154
                    dirlist).difference(dir_remotes)]
1155
                raiseCLIError(
1156
                    'Some remote paths contain non existing directories',
1157
                    details=['Missing remote directories:'] + badguys)
1158
        elif self.path:
1159
            r = self.client.get_object_info(
1160
                self.path,
1161
                version=self['object_version'])
1162
            if store_download._is_dir(r):
1163
                raiseCLIError(
1164
                    'Illegal download: Remote object %s is a directory' % (
1165
                        self.path),
1166
                    details=['To download a directory, try --recursive'])
1167
            if '/' in self.path.strip('/') and not local_path:
1168
                raiseCLIError(
1169
                    'Illegal download: remote object %s contains "/"' % (
1170
                        self.path),
1171
                    details=[
1172
                        'To download an object containing "/" characters',
1173
                        'either create the remote directories or',
1174
                        'specify a non-directory local path for this object'])
1175
            remotes = [(self.path, False)]
1176
        if not remotes:
1177
            if self.path:
1178
                raiseCLIError(
1179
                    'No matching path %s on container %s' % (
1180
                        self.path,
1181
                        self.container),
1182
                    details=[
1183
                        'To list the contents of %s, try:' % self.container,
1184
                        '   /store list %s' % self.container])
1185
            raiseCLIError(
1186
                'Illegal download of container %s' % self.container,
1187
                details=[
1188
                    'To download a whole container, try:',
1189
                    '   /store download --recursive <container>'])
1190

    
1191
        lprefix = path.abspath(local_path or path.curdir)
1192
        if path.isdir(lprefix):
1193
            for rpath, remote_is_dir in remotes:
1194
                lpath = '/%s/%s' % (lprefix.strip('/'), rpath.strip('/'))
1195
                if remote_is_dir:
1196
                    if path.exists(lpath) and path.isdir(lpath):
1197
                        continue
1198
                    makedirs(lpath)
1199
                elif path.exists(lpath):
1200
                    if not self['resume']:
1201
                        print('File %s exists, aborting...' % lpath)
1202
                        continue
1203
                    with open(lpath, 'rwb+') as f:
1204
                        yield (f, rpath)
1205
                else:
1206
                    with open(lpath, 'wb+') as f:
1207
                        yield (f, rpath)
1208
        elif path.exists(lprefix):
1209
            if len(remotes) > 1:
1210
                raiseCLIError(
1211
                    '%s remote objects cannot be merged in local file %s' % (
1212
                        len(remotes),
1213
                        local_path),
1214
                    details=[
1215
                        'To download multiple objects, local path should be',
1216
                        'a directory, or use download without a local path'])
1217
            (rpath, remote_is_dir) = remotes[0]
1218
            if remote_is_dir:
1219
                raiseCLIError(
1220
                    'Remote directory %s should not replace local file %s' % (
1221
                        rpath,
1222
                        local_path))
1223
            if self['resume']:
1224
                with open(lprefix, 'rwb+') as f:
1225
                    yield (f, rpath)
1226
            else:
1227
                raiseCLIError(
1228
                    'Local file %s already exist' % local_path,
1229
                    details=['Try --resume to overwrite it'])
1230
        else:
1231
            if len(remotes) > 1 or remotes[0][1]:
1232
                raiseCLIError(
1233
                    'Local directory %s does not exist' % local_path)
1234
            with open(lprefix, 'wb+') as f:
1235
                yield (f, remotes[0][0])
1236

    
1237
    @errors.generic.all
1238
    @errors.pithos.connection
1239
    @errors.pithos.container
1240
    @errors.pithos.object_path
1241
    @errors.pithos.local_path
1242
    def _run(self, local_path):
1243
        #outputs = self._outputs(local_path)
1244
        poolsize = self['poolsize']
1245
        if poolsize:
1246
            self.client.POOL_SIZE = int(poolsize)
1247
        progress_bar = None
1248
        try:
1249
            for f, rpath in self._outputs(local_path):
1250
                (
1251
                    progress_bar,
1252
                    download_cb) = self._safe_progress_bar(
1253
                        'Download %s' % rpath)
1254
                self.client.download_object(
1255
                    rpath,
1256
                    f,
1257
                    download_cb=download_cb,
1258
                    range_str=self['range'],
1259
                    version=self['object_version'],
1260
                    if_match=self['if_match'],
1261
                    resume=self['resume'],
1262
                    if_none_match=self['if_none_match'],
1263
                    if_modified_since=self['if_modified_since'],
1264
                    if_unmodified_since=self['if_unmodified_since'])
1265
        except KeyboardInterrupt:
1266
            from threading import enumerate as activethreads
1267
            stdout.write('\nFinishing active threads ')
1268
            for thread in activethreads():
1269
                stdout.flush()
1270
                try:
1271
                    thread.join()
1272
                    stdout.write('.')
1273
                except RuntimeError:
1274
                    continue
1275
            print('\ndownload canceled by user')
1276
            if local_path is not None:
1277
                print('to resume, re-run with --resume')
1278
        except Exception:
1279
            self._safe_progress_bar_finish(progress_bar)
1280
            raise
1281
        finally:
1282
            self._safe_progress_bar_finish(progress_bar)
1283

    
1284
    def main(self, container___path, local_path=None):
1285
        super(self.__class__, self)._run(container___path)
1286
        self._run(local_path=local_path)
1287

    
1288

    
1289
@command(pithos_cmds)
1290
class store_hashmap(_store_container_command):
1291
    """Get the hash-map of an object"""
1292

    
1293
    arguments = dict(
1294
        if_match=ValueArgument('show output if ETags match', '--if-match'),
1295
        if_none_match=ValueArgument(
1296
            'show output if ETags match',
1297
            '--if-none-match'),
1298
        if_modified_since=DateArgument(
1299
            'show output modified since then',
1300
            '--if-modified-since'),
1301
        if_unmodified_since=DateArgument(
1302
            'show output unmodified since then',
1303
            '--if-unmodified-since'),
1304
        object_version=ValueArgument(
1305
            'get the specific version',
1306
            '--object-version')
1307
    )
1308

    
1309
    @errors.generic.all
1310
    @errors.pithos.connection
1311
    @errors.pithos.container
1312
    @errors.pithos.object_path
1313
    def _run(self):
1314
        data = self.client.get_object_hashmap(
1315
            self.path,
1316
            version=self['object_version'],
1317
            if_match=self['if_match'],
1318
            if_none_match=self['if_none_match'],
1319
            if_modified_since=self['if_modified_since'],
1320
            if_unmodified_since=self['if_unmodified_since'])
1321
        print_dict(data)
1322

    
1323
    def main(self, container___path):
1324
        super(self.__class__, self)._run(
1325
            container___path,
1326
            path_is_optional=False)
1327
        self._run()
1328

    
1329

    
1330
@command(pithos_cmds)
1331
class store_delete(_store_container_command):
1332
    """Delete a container [or an object]
1333
    How to delete a non-empty container:
1334
    - empty the container:  /store delete -r <container>
1335
    - delete it:            /store delete <container>
1336
    .
1337
    Semantics of directory deletion:
1338
    .a preserve the contents: /store delete <container>:<directory>
1339
    .    objects of the form dir/filename can exist with a dir object
1340
    .b delete contents:       /store delete -r <container>:<directory>
1341
    .    all dir/* objects are affected, even if dir does not exist
1342
    .
1343
    To restore a deleted object OBJ in a container CONT:
1344
    - get object versions: /store versions CONT:OBJ
1345
    .   and choose the version to be restored
1346
    - restore the object:  /store copy --source-version=<version> CONT:OBJ OBJ
1347
    """
1348

    
1349
    arguments = dict(
1350
        until=DateArgument('remove history until that date', '--until'),
1351
        yes=FlagArgument('Do not prompt for permission', '--yes'),
1352
        recursive=FlagArgument(
1353
            'empty dir or container and delete (if dir)',
1354
            ('-r', '--recursive'))
1355
    )
1356

    
1357
    def __init__(self, arguments={}):
1358
        super(self.__class__, self).__init__(arguments)
1359
        self['delimiter'] = DelimiterArgument(
1360
            self,
1361
            parsed_name='--delimiter',
1362
            help='delete objects prefixed with <object><delimiter>')
1363

    
1364
    @errors.generic.all
1365
    @errors.pithos.connection
1366
    @errors.pithos.container
1367
    @errors.pithos.object_path
1368
    def _run(self):
1369
        if self.path:
1370
            if self['yes'] or ask_user(
1371
                    'Delete %s:%s ?' % (self.container, self.path)):
1372
                self.client.del_object(
1373
                    self.path,
1374
                    until=self['until'],
1375
                    delimiter=self['delimiter'])
1376
            else:
1377
                print('Aborted')
1378
        else:
1379
            if self['recursive']:
1380
                ask_msg = 'Delete container contents'
1381
            else:
1382
                ask_msg = 'Delete container'
1383
            if self['yes'] or ask_user('%s %s ?' % (ask_msg, self.container)):
1384
                self.client.del_container(
1385
                    until=self['until'],
1386
                    delimiter=self['delimiter'])
1387
            else:
1388
                print('Aborted')
1389

    
1390
    def main(self, container____path__=None):
1391
        super(self.__class__, self)._run(container____path__)
1392
        self._run()
1393

    
1394

    
1395
@command(pithos_cmds)
1396
class store_purge(_store_container_command):
1397
    """Delete a container and release related data blocks
1398
    Non-empty containers can not purged.
1399
    To purge a container with content:
1400
    .   /store delete -r <container>
1401
    .      objects are deleted, but data blocks remain on server
1402
    .   /store purge <container>
1403
    .      container and data blocks are released and deleted
1404
    """
1405

    
1406
    arguments = dict(
1407
        yes=FlagArgument('Do not prompt for permission', '--yes'),
1408
    )
1409

    
1410
    @errors.generic.all
1411
    @errors.pithos.connection
1412
    @errors.pithos.container
1413
    def _run(self):
1414
        if self['yes'] or ask_user('Purge container %s?' % self.container):
1415
                self.client.purge_container()
1416
        else:
1417
            print('Aborted')
1418

    
1419
    def main(self, container=None):
1420
        super(self.__class__, self)._run(container)
1421
        if container and self.container != container:
1422
            raiseCLIError('Invalid container name %s' % container, details=[
1423
                'Did you mean "%s" ?' % self.container,
1424
                'Use --container for names containing :'])
1425
        self._run()
1426

    
1427

    
1428
@command(pithos_cmds)
1429
class store_publish(_store_container_command):
1430
    """Publish the object and print the public url"""
1431

    
1432
    @errors.generic.all
1433
    @errors.pithos.connection
1434
    @errors.pithos.container
1435
    @errors.pithos.object_path
1436
    def _run(self):
1437
        url = self.client.publish_object(self.path)
1438
        print(url)
1439

    
1440
    def main(self, container___path):
1441
        super(self.__class__, self)._run(
1442
            container___path,
1443
            path_is_optional=False)
1444
        self._run()
1445

    
1446

    
1447
@command(pithos_cmds)
1448
class store_unpublish(_store_container_command):
1449
    """Unpublish an object"""
1450

    
1451
    @errors.generic.all
1452
    @errors.pithos.connection
1453
    @errors.pithos.container
1454
    @errors.pithos.object_path
1455
    def _run(self):
1456
            self.client.unpublish_object(self.path)
1457

    
1458
    def main(self, container___path):
1459
        super(self.__class__, self)._run(
1460
            container___path,
1461
            path_is_optional=False)
1462
        self._run()
1463

    
1464

    
1465
@command(pithos_cmds)
1466
class store_permissions(_store_container_command):
1467
    """Get read and write permissions of an object
1468
    Permissions are lists of users and user groups. There is read and write
1469
    permissions. Users and groups with write permission have also read
1470
    permission.
1471
    """
1472

    
1473
    @errors.generic.all
1474
    @errors.pithos.connection
1475
    @errors.pithos.container
1476
    @errors.pithos.object_path
1477
    def _run(self):
1478
        r = self.client.get_object_sharing(self.path)
1479
        print_dict(r)
1480

    
1481
    def main(self, container___path):
1482
        super(self.__class__, self)._run(
1483
            container___path,
1484
            path_is_optional=False)
1485
        self._run()
1486

    
1487

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

    
1499
    @errors.generic.all
1500
    def format_permition_dict(self, permissions):
1501
        read = False
1502
        write = False
1503
        for perms in permissions:
1504
            splstr = perms.split('=')
1505
            if 'read' == splstr[0]:
1506
                read = [ug.strip() for ug in splstr[1].split(',')]
1507
            elif 'write' == splstr[0]:
1508
                write = [ug.strip() for ug in splstr[1].split(',')]
1509
            else:
1510
                msg = 'Usage:\tread=<groups,users> write=<groups,users>'
1511
                raiseCLIError(None, msg)
1512
        return (read, write)
1513

    
1514
    @errors.generic.all
1515
    @errors.pithos.connection
1516
    @errors.pithos.container
1517
    @errors.pithos.object_path
1518
    def _run(self, read, write):
1519
        self.client.set_object_sharing(
1520
            self.path,
1521
            read_permition=read,
1522
            write_permition=write)
1523

    
1524
    def main(self, container___path, *permissions):
1525
        super(self.__class__, self)._run(
1526
            container___path,
1527
            path_is_optional=False)
1528
        (read, write) = self.format_permition_dict(permissions)
1529
        self._run(read, write)
1530

    
1531

    
1532
@command(pithos_cmds)
1533
class store_delpermissions(_store_container_command):
1534
    """Delete all permissions set on object
1535
    To modify permissions, use /store setpermssions
1536
    """
1537

    
1538
    @errors.generic.all
1539
    @errors.pithos.connection
1540
    @errors.pithos.container
1541
    @errors.pithos.object_path
1542
    def _run(self):
1543
        self.client.del_object_sharing(self.path)
1544

    
1545
    def main(self, container___path):
1546
        super(self.__class__, self)._run(
1547
            container___path,
1548
            path_is_optional=False)
1549
        self._run()
1550

    
1551

    
1552
@command(pithos_cmds)
1553
class store_info(_store_container_command):
1554
    """Get detailed information for user account, containers or objects
1555
    to get account info:    /store info
1556
    to get container info:  /store info <container>
1557
    to get object info:     /store info <container>:<path>
1558
    """
1559

    
1560
    arguments = dict(
1561
        object_version=ValueArgument(
1562
            'show specific version \ (applies only for objects)',
1563
            '--object-version')
1564
    )
1565

    
1566
    @errors.generic.all
1567
    @errors.pithos.connection
1568
    @errors.pithos.container
1569
    @errors.pithos.object_path
1570
    def _run(self):
1571
        if self.container is None:
1572
            r = self.client.get_account_info()
1573
        elif self.path is None:
1574
            r = self.client.get_container_info(self.container)
1575
        else:
1576
            r = self.client.get_object_info(
1577
                self.path,
1578
                version=self['object_version'])
1579
        print_dict(r)
1580

    
1581
    def main(self, container____path__=None):
1582
        super(self.__class__, self)._run(container____path__)
1583
        self._run()
1584

    
1585

    
1586
@command(pithos_cmds)
1587
class store_meta(_store_container_command):
1588
    """Get metadata for account, containers or objects"""
1589

    
1590
    arguments = dict(
1591
        detail=FlagArgument('show detailed output', '-l'),
1592
        until=DateArgument('show metadata until then', '--until'),
1593
        object_version=ValueArgument(
1594
            'show specific version \ (applies only for objects)',
1595
            '--object-version')
1596
    )
1597

    
1598
    @errors.generic.all
1599
    @errors.pithos.connection
1600
    @errors.pithos.container
1601
    @errors.pithos.object_path
1602
    def _run(self):
1603
        until = self['until']
1604
        if self.container is None:
1605
            if self['detail']:
1606
                r = self.client.get_account_info(until=until)
1607
            else:
1608
                r = self.client.get_account_meta(until=until)
1609
                r = pretty_keys(r, '-')
1610
            if r:
1611
                print(bold(self.client.account))
1612
        elif self.path is None:
1613
            if self['detail']:
1614
                r = self.client.get_container_info(until=until)
1615
            else:
1616
                cmeta = self.client.get_container_meta(until=until)
1617
                ometa = self.client.get_container_object_meta(until=until)
1618
                r = {}
1619
                if cmeta:
1620
                    r['container-meta'] = pretty_keys(cmeta, '-')
1621
                if ometa:
1622
                    r['object-meta'] = pretty_keys(ometa, '-')
1623
        else:
1624
            if self['detail']:
1625
                r = self.client.get_object_info(
1626
                    self.path,
1627
                    version=self['object_version'])
1628
            else:
1629
                r = self.client.get_object_meta(
1630
                    self.path,
1631
                    version=self['object_version'])
1632
            if r:
1633
                r = pretty_keys(pretty_keys(r, '-'))
1634
        if r:
1635
            print_dict(r)
1636

    
1637
    def main(self, container____path__=None):
1638
        super(self.__class__, self)._run(container____path__)
1639
        self._run()
1640

    
1641

    
1642
@command(pithos_cmds)
1643
class store_setmeta(_store_container_command):
1644
    """Set a piece of metadata for account, container or object
1645
    Metadata are formed as key:value pairs
1646
    """
1647

    
1648
    @errors.generic.all
1649
    @errors.pithos.connection
1650
    @errors.pithos.container
1651
    @errors.pithos.object_path
1652
    def _run(self, metakey, metaval):
1653
        if not self.container:
1654
            self.client.set_account_meta({metakey: metaval})
1655
        elif not self.path:
1656
            self.client.set_container_meta({metakey: metaval})
1657
        else:
1658
            self.client.set_object_meta(self.path, {metakey: metaval})
1659

    
1660
    def main(self, metakey, metaval, container____path__=None):
1661
        super(self.__class__, self)._run(container____path__)
1662
        self._run(metakey=metakey, metaval=metaval)
1663

    
1664

    
1665
@command(pithos_cmds)
1666
class store_delmeta(_store_container_command):
1667
    """Delete metadata with given key from account, container or object
1668
    Metadata are formed as key:value objects
1669
    - to get metadata of current account:     /store meta
1670
    - to get metadata of a container:         /store meta <container>
1671
    - to get metadata of an object:           /store meta <container>:<path>
1672
    """
1673

    
1674
    @errors.generic.all
1675
    @errors.pithos.connection
1676
    @errors.pithos.container
1677
    @errors.pithos.object_path
1678
    def _run(self, metakey):
1679
        if self.container is None:
1680
            self.client.del_account_meta(metakey)
1681
        elif self.path is None:
1682
            self.client.del_container_meta(metakey)
1683
        else:
1684
            self.client.del_object_meta(self.path, metakey)
1685

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

    
1690

    
1691
@command(pithos_cmds)
1692
class store_quota(_store_account_command):
1693
    """Get quota for account or container"""
1694

    
1695
    arguments = dict(
1696
        in_bytes=FlagArgument('Show result in bytes', ('-b', '--bytes'))
1697
    )
1698

    
1699
    @errors.generic.all
1700
    @errors.pithos.connection
1701
    @errors.pithos.container
1702
    def _run(self):
1703
        if self.container:
1704
            reply = self.client.get_container_quota(self.container)
1705
        else:
1706
            reply = self.client.get_account_quota()
1707
        if not self['in_bytes']:
1708
            for k in reply:
1709
                reply[k] = format_size(reply[k])
1710
        print_dict(pretty_keys(reply, '-'))
1711

    
1712
    def main(self, container=None):
1713
        super(self.__class__, self)._run()
1714
        self.container = container
1715
        self._run()
1716

    
1717

    
1718
@command(pithos_cmds)
1719
class store_setquota(_store_account_command):
1720
    """Set new quota for account or container
1721
    By default, quota is set in bytes
1722
    Users may specify a different unit, e.g:
1723
    /store setquota 2.3GB mycontainer
1724
    Accepted units: B, KiB (1024 B), KB (1000 B), MiB, MB, GiB, GB, TiB, TB
1725
    """
1726

    
1727
    @errors.generic.all
1728
    def _calculate_quota(self, user_input):
1729
        quota = 0
1730
        try:
1731
            quota = int(user_input)
1732
        except ValueError:
1733
            index = 0
1734
            digits = [str(num) for num in range(0, 10)] + ['.']
1735
            while user_input[index] in digits:
1736
                index += 1
1737
            quota = user_input[:index]
1738
            format = user_input[index:]
1739
            try:
1740
                return to_bytes(quota, format)
1741
            except Exception as qe:
1742
                msg = 'Failed to convert %s to bytes' % user_input,
1743
                raiseCLIError(qe, msg, details=[
1744
                    'Syntax: setquota <quota>[format] [container]',
1745
                    'e.g.: setquota 2.3GB mycontainer',
1746
                    'Acceptable formats:',
1747
                    '(*1024): B, KiB, MiB, GiB, TiB',
1748
                    '(*1000): B, KB, MB, GB, TB'])
1749
        return quota
1750

    
1751
    @errors.generic.all
1752
    @errors.pithos.connection
1753
    @errors.pithos.container
1754
    def _run(self, quota):
1755
        if self.container:
1756
            self.client.container = self.container
1757
            self.client.set_container_quota(quota)
1758
        else:
1759
            self.client.set_account_quota(quota)
1760

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

    
1767

    
1768
@command(pithos_cmds)
1769
class store_versioning(_store_account_command):
1770
    """Get  versioning for account or container"""
1771

    
1772
    @errors.generic.all
1773
    @errors.pithos.connection
1774
    @errors.pithos.container
1775
    def _run(self):
1776
        if self.container:
1777
            r = self.client.get_container_versioning(self.container)
1778
        else:
1779
            r = self.client.get_account_versioning()
1780
        print_dict(r)
1781

    
1782
    def main(self, container=None):
1783
        super(self.__class__, self)._run()
1784
        self.container = container
1785
        self._run()
1786

    
1787

    
1788
@command(pithos_cmds)
1789
class store_setversioning(_store_account_command):
1790
    """Set versioning mode (auto, none) for account or container"""
1791

    
1792
    def _check_versioning(self, versioning):
1793
        if versioning and versioning.lower() in ('auto', 'none'):
1794
            return versioning.lower()
1795
        raiseCLIError('Invalid versioning %s' % versioning, details=[
1796
            'Versioning can be auto or none'])
1797

    
1798
    @errors.generic.all
1799
    @errors.pithos.connection
1800
    @errors.pithos.container
1801
    def _run(self, versioning):
1802
        if self.container:
1803
            self.client.container = self.container
1804
            self.client.set_container_versioning(versioning)
1805
        else:
1806
            self.client.set_account_versioning(versioning)
1807

    
1808
    def main(self, versioning, container=None):
1809
        super(self.__class__, self)._run()
1810
        self._run(self._check_versioning(versioning))
1811

    
1812

    
1813
@command(pithos_cmds)
1814
class store_group(_store_account_command):
1815
    """Get groups and group members"""
1816

    
1817
    @errors.generic.all
1818
    @errors.pithos.connection
1819
    def _run(self):
1820
        r = self.client.get_account_group()
1821
        print_dict(pretty_keys(r, '-'))
1822

    
1823
    def main(self):
1824
        super(self.__class__, self)._run()
1825
        self._run()
1826

    
1827

    
1828
@command(pithos_cmds)
1829
class store_setgroup(_store_account_command):
1830
    """Set a user group"""
1831

    
1832
    @errors.generic.all
1833
    @errors.pithos.connection
1834
    def _run(self, groupname, *users):
1835
        self.client.set_account_group(groupname, users)
1836

    
1837
    def main(self, groupname, *users):
1838
        super(self.__class__, self)._run()
1839
        if users:
1840
            self._run(groupname, *users)
1841
        else:
1842
            raiseCLIError('No users to add in group %s' % groupname)
1843

    
1844

    
1845
@command(pithos_cmds)
1846
class store_delgroup(_store_account_command):
1847
    """Delete a user group"""
1848

    
1849
    @errors.generic.all
1850
    @errors.pithos.connection
1851
    def _run(self, groupname):
1852
        self.client.del_account_group(groupname)
1853

    
1854
    def main(self, groupname):
1855
        super(self.__class__, self)._run()
1856
        self._run(groupname)
1857

    
1858

    
1859
@command(pithos_cmds)
1860
class store_sharers(_store_account_command):
1861
    """List the accounts that share objects with current user"""
1862

    
1863
    arguments = dict(
1864
        detail=FlagArgument('show detailed output', '-l'),
1865
        marker=ValueArgument('show output greater then marker', '--marker')
1866
    )
1867

    
1868
    @errors.generic.all
1869
    @errors.pithos.connection
1870
    def _run(self):
1871
        accounts = self.client.get_sharing_accounts(marker=self['marker'])
1872
        if self['detail']:
1873
            print_items(accounts)
1874
        else:
1875
            print_items([acc['name'] for acc in accounts])
1876

    
1877
    def main(self):
1878
        super(self.__class__, self)._run()
1879
        self._run()
1880

    
1881

    
1882
@command(pithos_cmds)
1883
class store_versions(_store_container_command):
1884
    """Get the list of object versions
1885
    Deleted objects may still have versions that can be used to restore it and
1886
    get information about its previous state.
1887
    The version number can be used in a number of other commands, like info,
1888
    copy, move, meta. See these commands for more information, e.g.
1889
    /store info -h
1890
    """
1891

    
1892
    @errors.generic.all
1893
    @errors.pithos.connection
1894
    @errors.pithos.container
1895
    @errors.pithos.object_path
1896
    def _run(self):
1897
        versions = self.client.get_object_versionlist(self.path)
1898
        print_items([dict(id=vitem[0], created=strftime(
1899
            '%d-%m-%Y %H:%M:%S',
1900
            localtime(float(vitem[1])))) for vitem in versions])
1901

    
1902
    def main(self, container___path):
1903
        super(store_versions, self)._run(
1904
            container___path,
1905
            path_is_optional=False)
1906
        self._run()