Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / pithos_cli.py @ 761e0cbf

History | View | Annotate | Download (67.6 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
class _source_destination_command(_store_container_command):
530

    
531
    arguments = dict(
532
        recursive=FlagArgument('', ('-r', '--recursive')),
533
        prefix=FlagArgument('', '--with-prefix', default=''),
534
        suffix=ValueArgument('', '--with-suffix', default=''),
535
        add_prefix=ValueArgument('', '--add-prefix', default=''),
536
        add_suffix=ValueArgument('', '--add-suffix', default=''),
537
        prefix_replace=ValueArgument('', '--prefix-to-replace', default=''),
538
        suffix_replace=ValueArgument('', '--suffix-to-replace', default='')
539
    )
540

    
541
    def __init__(self, arguments={}):
542
        self.arguments.update(arguments)
543
        super(_source_destination_command, self).__init__(self.arguments)
544

    
545
    def _get_all(self, prefix):
546
        return self.client.container_get(prefix=prefix).json
547

    
548
    def _get_src_objects(self, src_cnt, src_path):
549
        """Get a list of the source objects to be called
550

551
        :param src_cnt: (str) source container
552

553
        :param src_path: (str) source path
554

555
        :returns: (method, params) a method that returns a list when called
556
        or (object) if it is a single object
557
        """
558
        if src_path and src_path[-1] == '/':
559
            src_path = src_path[:-1]
560
        self.client.container = src_cnt
561

    
562
        if self['prefix']:
563
            return (self._get_all, dict(prefix=src_path))
564
        try:
565
            srcobj = self.client.get_object_info(src_path)
566
        except ClientError as srcerr:
567
            if srcerr.status == 404:
568
                raiseCLIError(
569
                    'Source object %s not in cont. %s' % (src_path, src_cnt),
570
                    details=['Hint: --with-prefix to match multiple objects'])
571
            elif srcerr.status not in (204,):
572
                raise
573
            return (self.client.list_objects, {})
574
        if self._is_dir(srcobj):
575
            if not self['recursive']:
576
                raiseCLIError(
577
                    'Object %s of cont. %s is a dir' % (src_path, src_cnt),
578
                    details=['Use --recursive to access directories'])
579
            return (self._get_all, dict(prefix=src_path))
580
        srcobj['name'] = src_path
581
        return srcobj
582

    
583
    def src_dst_pairs(self, dst_cont, dst_path):
584
        src_iter = self._get_src_objects(self.container, self.path)
585
        src_N = isinstance(src_iter, tuple)
586
        add_prefix = self['add_prefix'].strip('/')
587

    
588
        if dst_path and dst_path.endswith('/'):
589
            dst_path = dst_path[:-1]
590

    
591
        self.client.container = dst_cont
592
        try:
593
            dstobj = self.client.get_object_info(dst_path)
594
        except ClientError as trgerr:
595
            if trgerr.status in (404,):
596
                if src_N:
597
                    raiseCLIError(
598
                        'Cannot merge multiple paths to path %s' % dst_path,
599
                        details=[
600
                            'Try to use / or a directory as destination',
601
                            'or create the destination dir (/store mkdir)',
602
                            'or use a single object as source'])
603
            elif trgerr.status not in (204,):
604
                raise
605
        else:
606
            if self._is_dir(dstobj):
607
                add_prefix = '%s/%s' % (dst_path.strip('/'), add_prefix)
608
            elif src_N:
609
                raiseCLIError(
610
                    'Cannot merge multiple paths to path' % dst_path,
611
                    details=[
612
                        'Try to use / or a directory as destination',
613
                        'or create the destination dir (/store mkdir)',
614
                        'or use a single object as source'])
615

    
616
        self.client.container = self.container
617
        if src_N:
618
            (method, kwargs) = src_iter
619
            for obj in method(**kwargs):
620
                name = obj['name']
621
                if name.endswith(self['suffix']):
622
                    yield (name, self._get_new_object(name, add_prefix))
623
        elif src_iter['name'].endswith(self['suffix']):
624
            name = src_iter['name']
625
            yield (name, self._get_new_object(dst_path or name, add_prefix))
626
        else:
627
            raiseCLIError('Source path %s conflicts with suffix %s' % (
628
                src_iter['name'],
629
                self['suffix']))
630

    
631
    def _get_new_object(self, obj, add_prefix):
632
        if self['prefix_replace'] and obj.startswith(self['prefix_replace']):
633
            obj = obj[len(self['prefix_replace']):]
634
        if self['suffix_replace'] and obj.endswith(self['suffix_replace']):
635
            obj = obj[:-len(self['suffix_replace'])]
636
        return add_prefix + obj + self['add_suffix']
637

    
638

    
639
@command(pithos_cmds)
640
class store_copy(_source_destination_command):
641
    """Copy objects from container to (another) container
642
    Semantics:
643
    copy cont:path dir
644
    .   transfer path as dir/path
645
    copy cont:path cont2:
646
    .   trasnfer all <obj> prefixed with path to container cont2
647
    copy cont:path [cont2:]path2
648
    .   transfer path to path2
649
    Use options:
650
    1. <container1>:<path1> [container2:]<path2> : if container2 is not given,
651
    destination is container1:path2
652
    2. <container>:<path1> <path2> : make a copy in the same container
653
    3. Can use --container= instead of <container1>
654
    """
655

    
656
    arguments = dict(
657
        destination_container=ValueArgument(
658
            'use it if destination container name contains a : character',
659
            '--dst-container'),
660
        source_version=ValueArgument(
661
            'copy specific version',
662
            '--source-version'),
663
        public=ValueArgument('make object publicly accessible', '--public'),
664
        content_type=ValueArgument(
665
            'change object\'s content type',
666
            '--content-type'),
667
        recursive=FlagArgument(
668
            'copy directory and contents',
669
            ('-r', '--recursive')),
670
        prefix=FlagArgument(
671
            'Match objects prefixed with src path (feels like src_path*)',
672
            '--with-prefix',
673
            default=''),
674
        suffix=ValueArgument(
675
            'Suffix of source objects (feels like *suffix)',
676
            '--with-suffix',
677
            default=''),
678
        add_prefix=ValueArgument('Prefix targets', '--add-prefix', default=''),
679
        add_suffix=ValueArgument('Suffix targets', '--add-suffix', default=''),
680
        prefix_replace=ValueArgument(
681
            'Prefix of src to replace with dst path + add_prefix, if matched',
682
            '--prefix-to-replace',
683
            default=''),
684
        suffix_replace=ValueArgument(
685
            'Suffix of src to replace with add_suffix, if matched',
686
            '--suffix-to-replace',
687
            default='')
688
    )
689

    
690
    @errors.generic.all
691
    @errors.pithos.connection
692
    @errors.pithos.container
693
    def _run(self, dst_cont, dst_path):
694
        no_source_object = True
695
        for src_object, dst_object in self.src_dst_pairs(dst_cont, dst_path):
696
            no_source_object = False
697
            self.client.copy_object(
698
                src_container=self.container,
699
                src_object=src_object,
700
                dst_container=dst_cont,
701
                dst_object=dst_object,
702
                source_version=self['source_version'],
703
                public=self['public'],
704
                content_type=self['content_type'])
705
        if no_source_object:
706
            raiseCLIError('No object %s in container %s' % (
707
                self.path,
708
                self.container))
709

    
710
    def main(
711
            self,
712
            source_container___path,
713
            destination_container___path=None):
714
        super(self.__class__, self)._run(
715
            source_container___path,
716
            path_is_optional=False)
717
        (dst_cont, dst_path) = self._dest_container_path(
718
            destination_container___path)
719
        self._run(dst_cont=dst_cont or self.container, dst_path=dst_path or '')
720

    
721

    
722
@command(pithos_cmds)
723
class store_move(_source_destination_command):
724
    """Move/rename objects from container to (another) container
725
    Semantics:
726
    move cont:path dir
727
    .   rename path as dir/path
728
    move cont:path cont2:
729
    .   trasnfer all <obj> prefixed with path to container cont2
730
    move cont:path [cont2:]path2
731
    .   transfer path to path2
732
    Use options:
733
    1. <container1>:<path1> [container2:]<path2> : if container2 is not given,
734
    destination is container1:path2
735
    2. <container>:<path1> <path2> : move in the same container
736
    3. Can use --container= instead of <container1>
737
    """
738

    
739
    arguments = dict(
740
        destination_container=ValueArgument(
741
            'use it if destination container name contains a : character',
742
            '--dst-container'),
743
        source_version=ValueArgument(
744
            'copy specific version',
745
            '--source-version'),
746
        public=ValueArgument('make object publicly accessible', '--public'),
747
        content_type=ValueArgument(
748
            'change object\'s content type',
749
            '--content-type'),
750
        recursive=FlagArgument(
751
            'copy directory and contents',
752
            ('-r', '--recursive')),
753
        prefix=FlagArgument(
754
            'Match objects prefixed with src path (feels like src_path*)',
755
            '--with-prefix',
756
            default=''),
757
        suffix=ValueArgument(
758
            'Suffix of source objects (feels like *suffix)',
759
            '--with-suffix',
760
            default=''),
761
        add_prefix=ValueArgument('Prefix targets', '--add-prefix', default=''),
762
        add_suffix=ValueArgument('Suffix targets', '--add-suffix', default=''),
763
        prefix_replace=ValueArgument(
764
            'Prefix of src to replace with dst path + add_prefix, if matched',
765
            '--prefix-to-replace',
766
            default=''),
767
        suffix_replace=ValueArgument(
768
            'Suffix of src to replace with add_suffix, if matched',
769
            '--suffix-to-replace',
770
            default='')
771
    )
772

    
773
    @errors.generic.all
774
    @errors.pithos.connection
775
    @errors.pithos.container
776
    def _run(self, dst_cont, dst_path):
777
        no_source_object = True
778
        for src_object, dst_object in self.src_dst_pairs(dst_cont, dst_path):
779
            no_source_object = False
780
            self.client.move_object(
781
                src_container=self.container,
782
                src_object=src_object,
783
                dst_container=dst_cont,
784
                dst_object=dst_object,
785
                source_version=self['source_version'],
786
                public=self['public'],
787
                content_type=self['content_type'])
788
        if no_source_object:
789
            raiseCLIError('No object %s in container %s' % (
790
                self.path,
791
                self.container))
792

    
793
    def main(
794
            self,
795
            source_container___path,
796
            destination_container___path=None):
797
        super(self.__class__, self)._run(
798
            source_container___path,
799
            path_is_optional=False)
800
        (dst_cnt, dst_path) = self._dest_container_path(
801
            destination_container___path)
802
        self._run(dst_cont=dst_cnt or self.container, dst_path=dst_path or '')
803

    
804

    
805
@command(pithos_cmds)
806
class store_append(_store_container_command):
807
    """Append local file to (existing) remote object
808
    The remote object should exist.
809
    If the remote object is a directory, it is transformed into a file.
810
    In the later case, objects under the directory remain intact.
811
    """
812

    
813
    arguments = dict(
814
        progress_bar=ProgressBarArgument(
815
            'do not show progress bar',
816
            '--no-progress-bar',
817
            default=False)
818
    )
819

    
820
    @errors.generic.all
821
    @errors.pithos.connection
822
    @errors.pithos.container
823
    @errors.pithos.object_path
824
    def _run(self, local_path):
825
        (progress_bar, upload_cb) = self._safe_progress_bar('Appending')
826
        try:
827
            f = open(local_path, 'rb')
828
            self.client.append_object(self.path, f, upload_cb)
829
        except Exception:
830
            self._safe_progress_bar_finish(progress_bar)
831
            raise
832
        finally:
833
            self._safe_progress_bar_finish(progress_bar)
834

    
835
    def main(self, local_path, container___path):
836
        super(self.__class__, self)._run(
837
            container___path,
838
            path_is_optional=False)
839
        self._run(local_path)
840

    
841

    
842
@command(pithos_cmds)
843
class store_truncate(_store_container_command):
844
    """Truncate remote file up to a size (default is 0)"""
845

    
846
    @errors.generic.all
847
    @errors.pithos.connection
848
    @errors.pithos.container
849
    @errors.pithos.object_path
850
    @errors.pithos.object_size
851
    def _run(self, size=0):
852
        self.client.truncate_object(self.path, size)
853

    
854
    def main(self, container___path, size=0):
855
        super(self.__class__, self)._run(container___path)
856
        self._run(size=size)
857

    
858

    
859
@command(pithos_cmds)
860
class store_overwrite(_store_container_command):
861
    """Overwrite part (from start to end) of a remote file
862
    overwrite local-path container 10 20
863
    .   will overwrite bytes from 10 to 20 of a remote file with the same name
864
    .   as local-path basename
865
    overwrite local-path container:path 10 20
866
    .   will overwrite as above, but the remote file is named path
867
    """
868

    
869
    arguments = dict(
870
        progress_bar=ProgressBarArgument(
871
            'do not show progress bar',
872
            '--no-progress-bar',
873
            default=False)
874
    )
875

    
876
    def _open_file(self, local_path, start):
877
        f = open(path.abspath(local_path), 'rb')
878
        f.seek(0, 2)
879
        f_size = f.tell()
880
        f.seek(start, 0)
881
        return (f, f_size)
882

    
883
    @errors.generic.all
884
    @errors.pithos.connection
885
    @errors.pithos.container
886
    @errors.pithos.object_path
887
    @errors.pithos.object_size
888
    def _run(self, local_path, start, end):
889
        (start, end) = (int(start), int(end))
890
        (f, f_size) = self._open_file(local_path, start)
891
        (progress_bar, upload_cb) = self._safe_progress_bar(
892
            'Overwrite %s bytes' % (end - start))
893
        try:
894
            self.client.overwrite_object(
895
                obj=self.path,
896
                start=start,
897
                end=end,
898
                source_file=f,
899
                upload_cb=upload_cb)
900
        except Exception:
901
            self._safe_progress_bar_finish(progress_bar)
902
            raise
903
        finally:
904
            self._safe_progress_bar_finish(progress_bar)
905

    
906
    def main(self, local_path, container___path, start, end):
907
        super(self.__class__, self)._run(
908
            container___path,
909
            path_is_optional=None)
910
        self.path = self.path or path.basename(local_path)
911
        self._run(local_path=local_path, start=start, end=end)
912

    
913

    
914
@command(pithos_cmds)
915
class store_manifest(_store_container_command):
916
    """Create a remote file of uploaded parts by manifestation
917
    Remains functional for compatibility with OOS Storage. Users are advised
918
    to use the upload command instead.
919
    Manifestation is a compliant process for uploading large files. The files
920
    have to be chunked in smalled files and uploaded as <prefix><increment>
921
    where increment is 1, 2, ...
922
    Finally, the manifest command glues partial files together in one file
923
    named <prefix>
924
    The upload command is faster, easier and more intuitive than manifest
925
    """
926

    
927
    arguments = dict(
928
        etag=ValueArgument('check written data', '--etag'),
929
        content_encoding=ValueArgument(
930
            'set MIME content type',
931
            '--content-encoding'),
932
        content_disposition=ValueArgument(
933
            'the presentation style of the object',
934
            '--content-disposition'),
935
        content_type=ValueArgument(
936
            'specify content type',
937
            '--content-type',
938
            default='application/octet-stream'),
939
        sharing=SharingArgument(
940
            '\n'.join([
941
                'define object sharing policy',
942
                '    ( "read=user1,grp1,user2,... write=user1,grp2,..." )']),
943
            '--sharing'),
944
        public=FlagArgument('make object publicly accessible', '--public')
945
    )
946

    
947
    @errors.generic.all
948
    @errors.pithos.connection
949
    @errors.pithos.container
950
    @errors.pithos.object_path
951
    def _run(self):
952
        self.client.create_object_by_manifestation(
953
            self.path,
954
            content_encoding=self['content_encoding'],
955
            content_disposition=self['content_disposition'],
956
            content_type=self['content_type'],
957
            sharing=self['sharing'],
958
            public=self['public'])
959

    
960
    def main(self, container___path):
961
        super(self.__class__, self)._run(
962
            container___path,
963
            path_is_optional=False)
964
        self.run()
965

    
966

    
967
@command(pithos_cmds)
968
class store_upload(_store_container_command):
969
    """Upload a file"""
970

    
971
    arguments = dict(
972
        use_hashes=FlagArgument(
973
            'provide hashmap file instead of data',
974
            '--use-hashes'),
975
        etag=ValueArgument('check written data', '--etag'),
976
        unchunked=FlagArgument('avoid chunked transfer mode', '--unchunked'),
977
        content_encoding=ValueArgument(
978
            'set MIME content type',
979
            '--content-encoding'),
980
        content_disposition=ValueArgument(
981
            'specify objects presentation style',
982
            '--content-disposition'),
983
        content_type=ValueArgument('specify content type', '--content-type'),
984
        sharing=SharingArgument(
985
            help='\n'.join([
986
                'define sharing object policy',
987
                '( "read=user1,grp1,user2,... write=user1,grp2,... )']),
988
            parsed_name='--sharing'),
989
        public=FlagArgument('make object publicly accessible', '--public'),
990
        poolsize=IntArgument('set pool size', '--with-pool-size'),
991
        progress_bar=ProgressBarArgument(
992
            'do not show progress bar',
993
            '--no-progress-bar',
994
            default=False),
995
        overwrite=FlagArgument('Force overwrite, if object exists', '-f')
996
    )
997

    
998
    def _remote_path(self, remote_path, local_path=''):
999
        if self['overwrite']:
1000
            return remote_path
1001
        try:
1002
            r = self.client.get_object_info(remote_path)
1003
        except ClientError as ce:
1004
            if ce.status == 404:
1005
                return remote_path
1006
            raise ce
1007
        ctype = r.get('content-type', '')
1008
        if 'application/directory' == ctype.lower():
1009
            ret = '%s/%s' % (remote_path, local_path)
1010
            return self._remote_path(ret) if local_path else ret
1011
        raiseCLIError(
1012
            'Object %s already exists' % remote_path,
1013
            importance=1,
1014
            details=['use -f to overwrite or resume'])
1015

    
1016
    @errors.generic.all
1017
    @errors.pithos.connection
1018
    @errors.pithos.container
1019
    @errors.pithos.object_path
1020
    @errors.pithos.local_path
1021
    def _run(self, local_path, remote_path):
1022
        poolsize = self['poolsize']
1023
        if poolsize > 0:
1024
            self.client.POOL_SIZE = int(poolsize)
1025
        params = dict(
1026
            content_encoding=self['content_encoding'],
1027
            content_type=self['content_type'],
1028
            content_disposition=self['content_disposition'],
1029
            sharing=self['sharing'],
1030
            public=self['public'])
1031
        remote_path = self._remote_path(remote_path, local_path)
1032
        with open(path.abspath(local_path), 'rb') as f:
1033
            if self['unchunked']:
1034
                self.client.upload_object_unchunked(
1035
                    remote_path,
1036
                    f,
1037
                    etag=self['etag'],
1038
                    withHashFile=self['use_hashes'],
1039
                    **params)
1040
            else:
1041
                try:
1042
                    (progress_bar, upload_cb) = self._safe_progress_bar(
1043
                        'Uploading')
1044
                    if progress_bar:
1045
                        hash_bar = progress_bar.clone()
1046
                        hash_cb = hash_bar.get_generator(
1047
                            'Calculating block hashes'
1048
                        )
1049
                    else:
1050
                        hash_cb = None
1051
                    self.client.upload_object(
1052
                        remote_path,
1053
                        f,
1054
                        hash_cb=hash_cb,
1055
                        upload_cb=upload_cb,
1056
                        **params)
1057
                except Exception:
1058
                    self._safe_progress_bar_finish(progress_bar)
1059
                    raise
1060
                finally:
1061
                    self._safe_progress_bar_finish(progress_bar)
1062
        print 'Upload completed'
1063

    
1064
    def main(self, local_path, container____path__=None):
1065
        super(self.__class__, self)._run(container____path__)
1066
        remote_path = self.path or path.basename(local_path)
1067
        self._run(local_path=local_path, remote_path=remote_path)
1068

    
1069

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

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

    
1091
    @errors.generic.all
1092
    @errors.pithos.connection
1093
    @errors.pithos.container
1094
    @errors.pithos.object_path
1095
    def _run(self):
1096
        self.client.download_object(
1097
            self.path,
1098
            stdout,
1099
            range_str=self['range'],
1100
            version=self['object_version'],
1101
            if_match=self['if_match'],
1102
            if_none_match=self['if_none_match'],
1103
            if_modified_since=self['if_modified_since'],
1104
            if_unmodified_since=self['if_unmodified_since'])
1105

    
1106
    def main(self, container___path):
1107
        super(self.__class__, self)._run(
1108
            container___path,
1109
            path_is_optional=False)
1110
        self._run()
1111

    
1112

    
1113
@command(pithos_cmds)
1114
class store_download(_store_container_command):
1115
    """Download remote object as local file
1116
    If local destination is a directory:
1117
    *   download <container>:<path> <local dir> -r
1118
    will download all files on <container> prefixed as <path>,
1119
    to <local dir>/<full path>
1120
    *   download <container>:<path> <local dir> --exact-match
1121
    will download only one file, exactly matching <path>
1122
    ATTENTION: to download cont:dir1/dir2/file there must exist objects
1123
    cont:dir1 and cont:dir1/dir2 of type application/directory
1124
    To create directory objects, use /store mkdir
1125
    """
1126

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

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

    
1209
        lprefix = path.abspath(local_path or path.curdir)
1210
        if path.isdir(lprefix):
1211
            for rpath, remote_is_dir in remotes:
1212
                lpath = '/%s/%s' % (lprefix.strip('/'), rpath.strip('/'))
1213
                if remote_is_dir:
1214
                    if path.exists(lpath) and path.isdir(lpath):
1215
                        continue
1216
                    makedirs(lpath)
1217
                elif path.exists(lpath):
1218
                    if not self['resume']:
1219
                        print('File %s exists, aborting...' % lpath)
1220
                        continue
1221
                    with open(lpath, 'rwb+') as f:
1222
                        yield (f, rpath)
1223
                else:
1224
                    with open(lpath, 'wb+') as f:
1225
                        yield (f, rpath)
1226
        elif path.exists(lprefix):
1227
            if len(remotes) > 1:
1228
                raiseCLIError(
1229
                    '%s remote objects cannot be merged in local file %s' % (
1230
                        len(remotes),
1231
                        local_path),
1232
                    details=[
1233
                        'To download multiple objects, local path should be',
1234
                        'a directory, or use download without a local path'])
1235
            (rpath, remote_is_dir) = remotes[0]
1236
            if remote_is_dir:
1237
                raiseCLIError(
1238
                    'Remote directory %s should not replace local file %s' % (
1239
                        rpath,
1240
                        local_path))
1241
            if self['resume']:
1242
                with open(lprefix, 'rwb+') as f:
1243
                    yield (f, rpath)
1244
            else:
1245
                raiseCLIError(
1246
                    'Local file %s already exist' % local_path,
1247
                    details=['Try --resume to overwrite it'])
1248
        else:
1249
            if len(remotes) > 1 or remotes[0][1]:
1250
                raiseCLIError(
1251
                    'Local directory %s does not exist' % local_path)
1252
            with open(lprefix, 'wb+') as f:
1253
                yield (f, remotes[0][0])
1254

    
1255
    @errors.generic.all
1256
    @errors.pithos.connection
1257
    @errors.pithos.container
1258
    @errors.pithos.object_path
1259
    @errors.pithos.local_path
1260
    def _run(self, local_path):
1261
        #outputs = self._outputs(local_path)
1262
        poolsize = self['poolsize']
1263
        if poolsize:
1264
            self.client.POOL_SIZE = int(poolsize)
1265
        progress_bar = None
1266
        try:
1267
            for f, rpath in self._outputs(local_path):
1268
                (
1269
                    progress_bar,
1270
                    download_cb) = self._safe_progress_bar(
1271
                        'Download %s' % rpath)
1272
                self.client.download_object(
1273
                    rpath,
1274
                    f,
1275
                    download_cb=download_cb,
1276
                    range_str=self['range'],
1277
                    version=self['object_version'],
1278
                    if_match=self['if_match'],
1279
                    resume=self['resume'],
1280
                    if_none_match=self['if_none_match'],
1281
                    if_modified_since=self['if_modified_since'],
1282
                    if_unmodified_since=self['if_unmodified_since'])
1283
        except KeyboardInterrupt:
1284
            from threading import enumerate as activethreads
1285
            stdout.write('\nFinishing active threads ')
1286
            for thread in activethreads():
1287
                stdout.flush()
1288
                try:
1289
                    thread.join()
1290
                    stdout.write('.')
1291
                except RuntimeError:
1292
                    continue
1293
            print('\ndownload canceled by user')
1294
            if local_path is not None:
1295
                print('to resume, re-run with --resume')
1296
        except Exception:
1297
            self._safe_progress_bar_finish(progress_bar)
1298
            raise
1299
        finally:
1300
            self._safe_progress_bar_finish(progress_bar)
1301

    
1302
    def main(self, container___path, local_path=None):
1303
        super(self.__class__, self)._run(container___path)
1304
        self._run(local_path=local_path)
1305

    
1306

    
1307
@command(pithos_cmds)
1308
class store_hashmap(_store_container_command):
1309
    """Get the hash-map of an object"""
1310

    
1311
    arguments = dict(
1312
        if_match=ValueArgument('show output if ETags match', '--if-match'),
1313
        if_none_match=ValueArgument(
1314
            'show output if ETags match',
1315
            '--if-none-match'),
1316
        if_modified_since=DateArgument(
1317
            'show output modified since then',
1318
            '--if-modified-since'),
1319
        if_unmodified_since=DateArgument(
1320
            'show output unmodified since then',
1321
            '--if-unmodified-since'),
1322
        object_version=ValueArgument(
1323
            'get the specific version',
1324
            '--object-version')
1325
    )
1326

    
1327
    @errors.generic.all
1328
    @errors.pithos.connection
1329
    @errors.pithos.container
1330
    @errors.pithos.object_path
1331
    def _run(self):
1332
        data = self.client.get_object_hashmap(
1333
            self.path,
1334
            version=self['object_version'],
1335
            if_match=self['if_match'],
1336
            if_none_match=self['if_none_match'],
1337
            if_modified_since=self['if_modified_since'],
1338
            if_unmodified_since=self['if_unmodified_since'])
1339
        print_dict(data)
1340

    
1341
    def main(self, container___path):
1342
        super(self.__class__, self)._run(
1343
            container___path,
1344
            path_is_optional=False)
1345
        self._run()
1346

    
1347

    
1348
@command(pithos_cmds)
1349
class store_delete(_store_container_command):
1350
    """Delete a container [or an object]
1351
    How to delete a non-empty container:
1352
    - empty the container:  /store delete -r <container>
1353
    - delete it:            /store delete <container>
1354
    .
1355
    Semantics of directory deletion:
1356
    .a preserve the contents: /store delete <container>:<directory>
1357
    .    objects of the form dir/filename can exist with a dir object
1358
    .b delete contents:       /store delete -r <container>:<directory>
1359
    .    all dir/* objects are affected, even if dir does not exist
1360
    .
1361
    To restore a deleted object OBJ in a container CONT:
1362
    - get object versions: /store versions CONT:OBJ
1363
    .   and choose the version to be restored
1364
    - restore the object:  /store copy --source-version=<version> CONT:OBJ OBJ
1365
    """
1366

    
1367
    arguments = dict(
1368
        until=DateArgument('remove history until that date', '--until'),
1369
        yes=FlagArgument('Do not prompt for permission', '--yes'),
1370
        recursive=FlagArgument(
1371
            'empty dir or container and delete (if dir)',
1372
            ('-r', '--recursive'))
1373
    )
1374

    
1375
    def __init__(self, arguments={}):
1376
        super(self.__class__, self).__init__(arguments)
1377
        self['delimiter'] = DelimiterArgument(
1378
            self,
1379
            parsed_name='--delimiter',
1380
            help='delete objects prefixed with <object><delimiter>')
1381

    
1382
    @errors.generic.all
1383
    @errors.pithos.connection
1384
    @errors.pithos.container
1385
    @errors.pithos.object_path
1386
    def _run(self):
1387
        if self.path:
1388
            if self['yes'] or ask_user(
1389
                    'Delete %s:%s ?' % (self.container, self.path)):
1390
                self.client.del_object(
1391
                    self.path,
1392
                    until=self['until'],
1393
                    delimiter=self['delimiter'])
1394
            else:
1395
                print('Aborted')
1396
        else:
1397
            if self['recursive']:
1398
                ask_msg = 'Delete container contents'
1399
            else:
1400
                ask_msg = 'Delete container'
1401
            if self['yes'] or ask_user('%s %s ?' % (ask_msg, self.container)):
1402
                self.client.del_container(
1403
                    until=self['until'],
1404
                    delimiter=self['delimiter'])
1405
            else:
1406
                print('Aborted')
1407

    
1408
    def main(self, container____path__=None):
1409
        super(self.__class__, self)._run(container____path__)
1410
        self._run()
1411

    
1412

    
1413
@command(pithos_cmds)
1414
class store_purge(_store_container_command):
1415
    """Delete a container and release related data blocks
1416
    Non-empty containers can not purged.
1417
    To purge a container with content:
1418
    .   /store delete -r <container>
1419
    .      objects are deleted, but data blocks remain on server
1420
    .   /store purge <container>
1421
    .      container and data blocks are released and deleted
1422
    """
1423

    
1424
    arguments = dict(
1425
        yes=FlagArgument('Do not prompt for permission', '--yes'),
1426
    )
1427

    
1428
    @errors.generic.all
1429
    @errors.pithos.connection
1430
    @errors.pithos.container
1431
    def _run(self):
1432
        if self['yes'] or ask_user('Purge container %s?' % self.container):
1433
                self.client.purge_container()
1434
        else:
1435
            print('Aborted')
1436

    
1437
    def main(self, container=None):
1438
        super(self.__class__, self)._run(container)
1439
        if container and self.container != container:
1440
            raiseCLIError('Invalid container name %s' % container, details=[
1441
                'Did you mean "%s" ?' % self.container,
1442
                'Use --container for names containing :'])
1443
        self._run()
1444

    
1445

    
1446
@command(pithos_cmds)
1447
class store_publish(_store_container_command):
1448
    """Publish the object and print the public url"""
1449

    
1450
    @errors.generic.all
1451
    @errors.pithos.connection
1452
    @errors.pithos.container
1453
    @errors.pithos.object_path
1454
    def _run(self):
1455
        url = self.client.publish_object(self.path)
1456
        print(url)
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_unpublish(_store_container_command):
1467
    """Unpublish an object"""
1468

    
1469
    @errors.generic.all
1470
    @errors.pithos.connection
1471
    @errors.pithos.container
1472
    @errors.pithos.object_path
1473
    def _run(self):
1474
            self.client.unpublish_object(self.path)
1475

    
1476
    def main(self, container___path):
1477
        super(self.__class__, self)._run(
1478
            container___path,
1479
            path_is_optional=False)
1480
        self._run()
1481

    
1482

    
1483
@command(pithos_cmds)
1484
class store_permissions(_store_container_command):
1485
    """Get read and write permissions of an object
1486
    Permissions are lists of users and user groups. There is read and write
1487
    permissions. Users and groups with write permission have also read
1488
    permission.
1489
    """
1490

    
1491
    @errors.generic.all
1492
    @errors.pithos.connection
1493
    @errors.pithos.container
1494
    @errors.pithos.object_path
1495
    def _run(self):
1496
        r = self.client.get_object_sharing(self.path)
1497
        print_dict(r)
1498

    
1499
    def main(self, container___path):
1500
        super(self.__class__, self)._run(
1501
            container___path,
1502
            path_is_optional=False)
1503
        self._run()
1504

    
1505

    
1506
@command(pithos_cmds)
1507
class store_setpermissions(_store_container_command):
1508
    """Set permissions for an object
1509
    New permissions overwrite existing permissions.
1510
    Permission format:
1511
    -   read=<username>[,usergroup[,...]]
1512
    -   write=<username>[,usegroup[,...]]
1513
    E.g. to give read permissions for file F to users A and B and write for C:
1514
    .       /store setpermissions F read=A,B write=C
1515
    """
1516

    
1517
    @errors.generic.all
1518
    def format_permition_dict(self, permissions):
1519
        read = False
1520
        write = False
1521
        for perms in permissions:
1522
            splstr = perms.split('=')
1523
            if 'read' == splstr[0]:
1524
                read = [ug.strip() for ug in splstr[1].split(',')]
1525
            elif 'write' == splstr[0]:
1526
                write = [ug.strip() for ug in splstr[1].split(',')]
1527
            else:
1528
                msg = 'Usage:\tread=<groups,users> write=<groups,users>'
1529
                raiseCLIError(None, msg)
1530
        return (read, write)
1531

    
1532
    @errors.generic.all
1533
    @errors.pithos.connection
1534
    @errors.pithos.container
1535
    @errors.pithos.object_path
1536
    def _run(self, read, write):
1537
        self.client.set_object_sharing(
1538
            self.path,
1539
            read_permition=read,
1540
            write_permition=write)
1541

    
1542
    def main(self, container___path, *permissions):
1543
        super(self.__class__, self)._run(
1544
            container___path,
1545
            path_is_optional=False)
1546
        (read, write) = self.format_permition_dict(permissions)
1547
        self._run(read, write)
1548

    
1549

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

    
1556
    @errors.generic.all
1557
    @errors.pithos.connection
1558
    @errors.pithos.container
1559
    @errors.pithos.object_path
1560
    def _run(self):
1561
        self.client.del_object_sharing(self.path)
1562

    
1563
    def main(self, container___path):
1564
        super(self.__class__, self)._run(
1565
            container___path,
1566
            path_is_optional=False)
1567
        self._run()
1568

    
1569

    
1570
@command(pithos_cmds)
1571
class store_info(_store_container_command):
1572
    """Get detailed information for user account, containers or objects
1573
    to get account info:    /store info
1574
    to get container info:  /store info <container>
1575
    to get object info:     /store info <container>:<path>
1576
    """
1577

    
1578
    arguments = dict(
1579
        object_version=ValueArgument(
1580
            'show specific version \ (applies only for objects)',
1581
            '--object-version')
1582
    )
1583

    
1584
    @errors.generic.all
1585
    @errors.pithos.connection
1586
    @errors.pithos.container
1587
    @errors.pithos.object_path
1588
    def _run(self):
1589
        if self.container is None:
1590
            r = self.client.get_account_info()
1591
        elif self.path is None:
1592
            r = self.client.get_container_info(self.container)
1593
        else:
1594
            r = self.client.get_object_info(
1595
                self.path,
1596
                version=self['object_version'])
1597
        print_dict(r)
1598

    
1599
    def main(self, container____path__=None):
1600
        super(self.__class__, self)._run(container____path__)
1601
        self._run()
1602

    
1603

    
1604
@command(pithos_cmds)
1605
class store_meta(_store_container_command):
1606
    """Get metadata for account, containers or objects"""
1607

    
1608
    arguments = dict(
1609
        detail=FlagArgument('show detailed output', '-l'),
1610
        until=DateArgument('show metadata until then', '--until'),
1611
        object_version=ValueArgument(
1612
            'show specific version \ (applies only for objects)',
1613
            '--object-version')
1614
    )
1615

    
1616
    @errors.generic.all
1617
    @errors.pithos.connection
1618
    @errors.pithos.container
1619
    @errors.pithos.object_path
1620
    def _run(self):
1621
        until = self['until']
1622
        if self.container is None:
1623
            if self['detail']:
1624
                r = self.client.get_account_info(until=until)
1625
            else:
1626
                r = self.client.get_account_meta(until=until)
1627
                r = pretty_keys(r, '-')
1628
            if r:
1629
                print(bold(self.client.account))
1630
        elif self.path is None:
1631
            if self['detail']:
1632
                r = self.client.get_container_info(until=until)
1633
            else:
1634
                cmeta = self.client.get_container_meta(until=until)
1635
                ometa = self.client.get_container_object_meta(until=until)
1636
                r = {}
1637
                if cmeta:
1638
                    r['container-meta'] = pretty_keys(cmeta, '-')
1639
                if ometa:
1640
                    r['object-meta'] = pretty_keys(ometa, '-')
1641
        else:
1642
            if self['detail']:
1643
                r = self.client.get_object_info(
1644
                    self.path,
1645
                    version=self['object_version'])
1646
            else:
1647
                r = self.client.get_object_meta(
1648
                    self.path,
1649
                    version=self['object_version'])
1650
            if r:
1651
                r = pretty_keys(pretty_keys(r, '-'))
1652
        if r:
1653
            print_dict(r)
1654

    
1655
    def main(self, container____path__=None):
1656
        super(self.__class__, self)._run(container____path__)
1657
        self._run()
1658

    
1659

    
1660
@command(pithos_cmds)
1661
class store_setmeta(_store_container_command):
1662
    """Set a piece of metadata for account, container or object
1663
    Metadata are formed as key:value pairs
1664
    """
1665

    
1666
    @errors.generic.all
1667
    @errors.pithos.connection
1668
    @errors.pithos.container
1669
    @errors.pithos.object_path
1670
    def _run(self, metakey, metaval):
1671
        if not self.container:
1672
            self.client.set_account_meta({metakey: metaval})
1673
        elif not self.path:
1674
            self.client.set_container_meta({metakey: metaval})
1675
        else:
1676
            self.client.set_object_meta(self.path, {metakey: metaval})
1677

    
1678
    def main(self, metakey, metaval, container____path__=None):
1679
        super(self.__class__, self)._run(container____path__)
1680
        self._run(metakey=metakey, metaval=metaval)
1681

    
1682

    
1683
@command(pithos_cmds)
1684
class store_delmeta(_store_container_command):
1685
    """Delete metadata with given key from account, container or object
1686
    Metadata are formed as key:value objects
1687
    - to get metadata of current account:     /store meta
1688
    - to get metadata of a container:         /store meta <container>
1689
    - to get metadata of an object:           /store meta <container>:<path>
1690
    """
1691

    
1692
    @errors.generic.all
1693
    @errors.pithos.connection
1694
    @errors.pithos.container
1695
    @errors.pithos.object_path
1696
    def _run(self, metakey):
1697
        if self.container is None:
1698
            self.client.del_account_meta(metakey)
1699
        elif self.path is None:
1700
            self.client.del_container_meta(metakey)
1701
        else:
1702
            self.client.del_object_meta(self.path, metakey)
1703

    
1704
    def main(self, metakey, container____path__=None):
1705
        super(self.__class__, self)._run(container____path__)
1706
        self._run(metakey)
1707

    
1708

    
1709
@command(pithos_cmds)
1710
class store_quota(_store_account_command):
1711
    """Get quota for account or container"""
1712

    
1713
    arguments = dict(
1714
        in_bytes=FlagArgument('Show result in bytes', ('-b', '--bytes'))
1715
    )
1716

    
1717
    @errors.generic.all
1718
    @errors.pithos.connection
1719
    @errors.pithos.container
1720
    def _run(self):
1721
        if self.container:
1722
            reply = self.client.get_container_quota(self.container)
1723
        else:
1724
            reply = self.client.get_account_quota()
1725
        if not self['in_bytes']:
1726
            for k in reply:
1727
                reply[k] = format_size(reply[k])
1728
        print_dict(pretty_keys(reply, '-'))
1729

    
1730
    def main(self, container=None):
1731
        super(self.__class__, self)._run()
1732
        self.container = container
1733
        self._run()
1734

    
1735

    
1736
@command(pithos_cmds)
1737
class store_setquota(_store_account_command):
1738
    """Set new quota for account or container
1739
    By default, quota is set in bytes
1740
    Users may specify a different unit, e.g:
1741
    /store setquota 2.3GB mycontainer
1742
    Accepted units: B, KiB (1024 B), KB (1000 B), MiB, MB, GiB, GB, TiB, TB
1743
    """
1744

    
1745
    @errors.generic.all
1746
    def _calculate_quota(self, user_input):
1747
        quota = 0
1748
        try:
1749
            quota = int(user_input)
1750
        except ValueError:
1751
            index = 0
1752
            digits = [str(num) for num in range(0, 10)] + ['.']
1753
            while user_input[index] in digits:
1754
                index += 1
1755
            quota = user_input[:index]
1756
            format = user_input[index:]
1757
            try:
1758
                return to_bytes(quota, format)
1759
            except Exception as qe:
1760
                msg = 'Failed to convert %s to bytes' % user_input,
1761
                raiseCLIError(qe, msg, details=[
1762
                    'Syntax: setquota <quota>[format] [container]',
1763
                    'e.g.: setquota 2.3GB mycontainer',
1764
                    'Acceptable formats:',
1765
                    '(*1024): B, KiB, MiB, GiB, TiB',
1766
                    '(*1000): B, KB, MB, GB, TB'])
1767
        return quota
1768

    
1769
    @errors.generic.all
1770
    @errors.pithos.connection
1771
    @errors.pithos.container
1772
    def _run(self, quota):
1773
        if self.container:
1774
            self.client.container = self.container
1775
            self.client.set_container_quota(quota)
1776
        else:
1777
            self.client.set_account_quota(quota)
1778

    
1779
    def main(self, quota, container=None):
1780
        super(self.__class__, self)._run()
1781
        quota = self._calculate_quota(quota)
1782
        self.container = container
1783
        self._run(quota)
1784

    
1785

    
1786
@command(pithos_cmds)
1787
class store_versioning(_store_account_command):
1788
    """Get  versioning for account or container"""
1789

    
1790
    @errors.generic.all
1791
    @errors.pithos.connection
1792
    @errors.pithos.container
1793
    def _run(self):
1794
        if self.container:
1795
            r = self.client.get_container_versioning(self.container)
1796
        else:
1797
            r = self.client.get_account_versioning()
1798
        print_dict(r)
1799

    
1800
    def main(self, container=None):
1801
        super(self.__class__, self)._run()
1802
        self.container = container
1803
        self._run()
1804

    
1805

    
1806
@command(pithos_cmds)
1807
class store_setversioning(_store_account_command):
1808
    """Set versioning mode (auto, none) for account or container"""
1809

    
1810
    def _check_versioning(self, versioning):
1811
        if versioning and versioning.lower() in ('auto', 'none'):
1812
            return versioning.lower()
1813
        raiseCLIError('Invalid versioning %s' % versioning, details=[
1814
            'Versioning can be auto or none'])
1815

    
1816
    @errors.generic.all
1817
    @errors.pithos.connection
1818
    @errors.pithos.container
1819
    def _run(self, versioning):
1820
        if self.container:
1821
            self.client.container = self.container
1822
            self.client.set_container_versioning(versioning)
1823
        else:
1824
            self.client.set_account_versioning(versioning)
1825

    
1826
    def main(self, versioning, container=None):
1827
        super(self.__class__, self)._run()
1828
        self._run(self._check_versioning(versioning))
1829

    
1830

    
1831
@command(pithos_cmds)
1832
class store_group(_store_account_command):
1833
    """Get groups and group members"""
1834

    
1835
    @errors.generic.all
1836
    @errors.pithos.connection
1837
    def _run(self):
1838
        r = self.client.get_account_group()
1839
        print_dict(pretty_keys(r, '-'))
1840

    
1841
    def main(self):
1842
        super(self.__class__, self)._run()
1843
        self._run()
1844

    
1845

    
1846
@command(pithos_cmds)
1847
class store_setgroup(_store_account_command):
1848
    """Set a user group"""
1849

    
1850
    @errors.generic.all
1851
    @errors.pithos.connection
1852
    def _run(self, groupname, *users):
1853
        self.client.set_account_group(groupname, users)
1854

    
1855
    def main(self, groupname, *users):
1856
        super(self.__class__, self)._run()
1857
        if users:
1858
            self._run(groupname, *users)
1859
        else:
1860
            raiseCLIError('No users to add in group %s' % groupname)
1861

    
1862

    
1863
@command(pithos_cmds)
1864
class store_delgroup(_store_account_command):
1865
    """Delete a user group"""
1866

    
1867
    @errors.generic.all
1868
    @errors.pithos.connection
1869
    def _run(self, groupname):
1870
        self.client.del_account_group(groupname)
1871

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

    
1876

    
1877
@command(pithos_cmds)
1878
class store_sharers(_store_account_command):
1879
    """List the accounts that share objects with current user"""
1880

    
1881
    arguments = dict(
1882
        detail=FlagArgument('show detailed output', '-l'),
1883
        marker=ValueArgument('show output greater then marker', '--marker')
1884
    )
1885

    
1886
    @errors.generic.all
1887
    @errors.pithos.connection
1888
    def _run(self):
1889
        accounts = self.client.get_sharing_accounts(marker=self['marker'])
1890
        if self['detail']:
1891
            print_items(accounts)
1892
        else:
1893
            print_items([acc['name'] for acc in accounts])
1894

    
1895
    def main(self):
1896
        super(self.__class__, self)._run()
1897
        self._run()
1898

    
1899

    
1900
@command(pithos_cmds)
1901
class store_versions(_store_container_command):
1902
    """Get the list of object versions
1903
    Deleted objects may still have versions that can be used to restore it and
1904
    get information about its previous state.
1905
    The version number can be used in a number of other commands, like info,
1906
    copy, move, meta. See these commands for more information, e.g.
1907
    /store info -h
1908
    """
1909

    
1910
    @errors.generic.all
1911
    @errors.pithos.connection
1912
    @errors.pithos.container
1913
    @errors.pithos.object_path
1914
    def _run(self):
1915
        versions = self.client.get_object_versionlist(self.path)
1916
        print_items([dict(id=vitem[0], created=strftime(
1917
            '%d-%m-%Y %H:%M:%S',
1918
            localtime(float(vitem[1])))) for vitem in versions])
1919

    
1920
    def main(self, container___path):
1921
        super(store_versions, self)._run(
1922
            container___path,
1923
            path_is_optional=False)
1924
        self._run()