Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / pithos_cli.py @ 5fdccdec

History | View | Annotate | Download (69 kB)

1
# Copyright 2011-2012 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
#
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
#
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
#
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.command
33

    
34
from sys import stdout
35
from time import localtime, strftime
36
from logging import getLogger
37
from os import path, makedirs
38

    
39
from kamaki.cli import command
40
from kamaki.cli.command_tree import CommandTree
41
from kamaki.cli.errors import raiseCLIError, CLISyntaxError
42
from kamaki.cli.utils import (
43
    format_size,
44
    to_bytes,
45
    print_dict,
46
    print_items,
47
    pretty_keys,
48
    page_hold,
49
    bold,
50
    ask_user)
51
from kamaki.cli.argument import FlagArgument, ValueArgument, IntArgument
52
from kamaki.cli.argument import KeyValueArgument, DateArgument
53
from kamaki.cli.argument import ProgressBarArgument
54
from kamaki.cli.commands import _command_init, errors
55
from kamaki.clients.pithos import PithosClient, ClientError
56
from kamaki.clients.astakos import AstakosClient
57

    
58

    
59
kloger = getLogger('kamaki')
60

    
61
pithos_cmds = CommandTree('store', 'Pithos+ storage commands')
62
_commands = [pithos_cmds]
63

    
64

    
65
# Argument functionality
66

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

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

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

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

    
87

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

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

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

    
129

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

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

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

    
150
# Command specs
151

    
152

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

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

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

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

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

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

    
190

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

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

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

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

    
209

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

    
213
    container = None
214
    path = None
215

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

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

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

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

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

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

    
298

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

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

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

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

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

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

    
428

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

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

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

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

    
452

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

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

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

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

    
478

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

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

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

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

    
510

    
511
class _source_destination_command(_store_container_command):
512

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

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

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

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

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

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

566
        :param src_path: (str) source path
567

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

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

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

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

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

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

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

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

    
653

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

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

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

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

    
744

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

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

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

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

    
836

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

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

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

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

    
873

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

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

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

    
890

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

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

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

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

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

    
945

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

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

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

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

    
998

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

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

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

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

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

    
1100

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

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

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

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

    
1143

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

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

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

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

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

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

    
1337

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

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

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

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

    
1378

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

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

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

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

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

    
1443

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

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

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

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

    
1476

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

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

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

    
1495

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

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

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

    
1513

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

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

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

    
1536

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

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

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

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

    
1580

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

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

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

    
1600

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

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

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

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

    
1634

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

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

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

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

    
1690

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

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

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

    
1713

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

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

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

    
1739

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

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

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

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

    
1766

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

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

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

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

    
1816

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

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

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

    
1836

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

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

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

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

    
1861

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

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

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

    
1876

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

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

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

    
1893

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

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

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

    
1907

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

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

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

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

    
1930

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

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

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