Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / pithos.py @ 92101413

History | View | Annotate | Download (77.7 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 os import path, makedirs, walk
37

    
38
from kamaki.cli import command
39
from kamaki.cli.command_tree import CommandTree
40
from kamaki.cli.errors import raiseCLIError, CLISyntaxError, CLIBaseUrlError
41
from kamaki.cli.utils import (
42
    format_size, to_bytes, print_dict, print_items, pretty_keys, pretty_dict,
43
    page_hold, bold, ask_user, get_path_size, print_json)
44
from kamaki.cli.argument import FlagArgument, ValueArgument, IntArgument
45
from kamaki.cli.argument import KeyValueArgument, DateArgument
46
from kamaki.cli.argument import ProgressBarArgument
47
from kamaki.cli.commands import _command_init, errors
48
from kamaki.cli.commands import _optional_output_cmd, _optional_json
49
from kamaki.clients.pithos import PithosClient, ClientError
50
from kamaki.clients.astakos import AstakosClient
51

    
52
pithos_cmds = CommandTree('file', 'Pithos+/Storage API commands')
53
_commands = [pithos_cmds]
54

    
55

    
56
# Argument functionality
57

    
58
class DelimiterArgument(ValueArgument):
59
    """
60
    :value type: string
61
    :value returns: given string or /
62
    """
63

    
64
    def __init__(self, caller_obj, help='', parsed_name=None, default=None):
65
        super(DelimiterArgument, self).__init__(help, parsed_name, default)
66
        self.caller_obj = caller_obj
67

    
68
    @property
69
    def value(self):
70
        if self.caller_obj['recursive']:
71
            return '/'
72
        return getattr(self, '_value', self.default)
73

    
74
    @value.setter
75
    def value(self, newvalue):
76
        self._value = newvalue
77

    
78

    
79
class SharingArgument(ValueArgument):
80
    """Set sharing (read and/or write) groups
81
    .
82
    :value type: "read=term1,term2,... write=term1,term2,..."
83
    .
84
    :value returns: {'read':['term1', 'term2', ...],
85
    .   'write':['term1', 'term2', ...]}
86
    """
87

    
88
    @property
89
    def value(self):
90
        return getattr(self, '_value', self.default)
91

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

    
120

    
121
class RangeArgument(ValueArgument):
122
    """
123
    :value type: string of the form <start>-<end> where <start> and <end> are
124
        integers
125
    :value returns: the input string, after type checking <start> and <end>
126
    """
127

    
128
    @property
129
    def value(self):
130
        return getattr(self, '_value', self.default)
131

    
132
    @value.setter
133
    def value(self, newvalue):
134
        if newvalue is None:
135
            self._value = self.default
136
            return
137
        (start, end) = newvalue.split('-')
138
        (start, end) = (int(start), int(end))
139
        self._value = '%s-%s' % (start, end)
140

    
141
# Command specs
142

    
143

    
144
class _pithos_init(_command_init):
145
    """Initialize a pithos+ kamaki client"""
146

    
147
    @staticmethod
148
    def _is_dir(remote_dict):
149
        return 'application/directory' == remote_dict.get(
150
            'content_type', remote_dict.get('content-type', ''))
151

    
152
    @errors.generic.all
153
    def _run(self):
154
        self.token = self.config.get('file', 'token')\
155
            or self.config.get('global', 'token')
156

    
157
        if getattr(self, 'auth_base', False):
158
            pithos_endpoints = self.auth_base.get_service_endpoints(
159
                self.config.get('pithos', 'type'),
160
                self.config.get('pithos', 'version'))
161
            self.base_url = pithos_endpoints['publicURL']
162
        else:
163
            self.base_url = self.config.get('file', 'url')\
164
                or self.config.get('store', 'url')\
165
                or self.config.get('pithos', 'url')
166
        if not self.base_url:
167
            raise CLIBaseUrlError(service='pithos')
168

    
169
        self._set_account()
170
        self.container = self.config.get('file', 'container')\
171
            or self.config.get('store', 'container')\
172
            or self.config.get('pithos', 'container')\
173
            or self.config.get('global', 'container')
174
        self.client = PithosClient(
175
            base_url=self.base_url,
176
            token=self.token,
177
            account=self.account,
178
            container=self.container)
179
        self._set_log_params()
180
        self._update_max_threads()
181

    
182
    def main(self):
183
        self._run()
184

    
185
    def _set_account(self):
186
        if getattr(self, 'auth_base', False):
187
            self.account = self.auth_base.user_term('id', self.token)
188
        else:
189
            astakos_url = self.config.get('user', 'url')\
190
                or self.config.get('astakos', 'url')
191
            if not astakos_url:
192
                raise CLIBaseUrlError(service='astakos')
193
            astakos = AstakosClient(astakos_url, self.token)
194
            self.account = astakos.user_term('id')
195

    
196

    
197
class _file_account_command(_pithos_init):
198
    """Base class for account level storage commands"""
199

    
200
    def __init__(self, arguments={}, auth_base=None):
201
        super(_file_account_command, self).__init__(arguments, auth_base)
202
        self['account'] = ValueArgument(
203
            'Set user account (not permanent)',
204
            ('-A', '--account'))
205

    
206
    def _run(self, custom_account=None):
207
        super(_file_account_command, self)._run()
208
        if custom_account:
209
            self.client.account = custom_account
210
        elif self['account']:
211
            self.client.account = self['account']
212

    
213
    @errors.generic.all
214
    def main(self):
215
        self._run()
216

    
217

    
218
class _file_container_command(_file_account_command):
219
    """Base class for container level storage commands"""
220

    
221
    container = None
222
    path = None
223

    
224
    def __init__(self, arguments={}, auth_base=None):
225
        super(_file_container_command, self).__init__(arguments, auth_base)
226
        self['container'] = ValueArgument(
227
            'Set container to work with (temporary)',
228
            ('-C', '--container'))
229

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

    
250
        user_cont, sep, userpath = container_with_path.partition(':')
251

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

    
285
    @errors.generic.all
286
    def _run(self, container_with_path=None, path_is_optional=True):
287
        super(_file_container_command, self)._run()
288
        if self['container']:
289
            self.client.container = self['container']
290
            if container_with_path:
291
                self.path = container_with_path
292
            elif not path_is_optional:
293
                raise CLISyntaxError(
294
                    'Both container and path are required',
295
                    details=errors.pithos.container_howto)
296
        elif container_with_path:
297
            self.extract_container_and_path(
298
                container_with_path,
299
                path_is_optional)
300
            self.client.container = self.container
301
        self.container = self.client.container
302

    
303
    def main(self, container_with_path=None, path_is_optional=True):
304
        self._run(container_with_path, path_is_optional)
305

    
306

    
307
@command(pithos_cmds)
308
class file_list(_file_container_command, _optional_json):
309
    """List containers, object trees or objects in a directory
310
    Use with:
311
    1 no parameters : containers in current account
312
    2. one parameter (container) or --container : contents of container
313
    3. <container>:<prefix> or --container=<container> <prefix>: objects in
314
    .   container starting with prefix
315
    """
316

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

    
350
    def print_objects(self, object_list):
351
        if self['json_output']:
352
            print_json(object_list)
353
            return
354
        limit = int(self['limit']) if self['limit'] > 0 else len(object_list)
355
        for index, obj in enumerate(object_list):
356
            if self['exact_match'] and self.path and not (
357
                    obj['name'] == self.path or 'content_type' in obj):
358
                continue
359
            pretty_obj = obj.copy()
360
            index += 1
361
            empty_space = ' ' * (len(str(len(object_list))) - len(str(index)))
362
            if obj['content_type'] == 'application/directory':
363
                isDir = True
364
                size = 'D'
365
            else:
366
                isDir = False
367
                size = format_size(obj['bytes'])
368
                pretty_obj['bytes'] = '%s (%s)' % (obj['bytes'], size)
369
            oname = bold(obj['name'])
370
            prfx = ('%s%s. ' % (empty_space, index)) if self['enum'] else ''
371
            if self['detail']:
372
                print('%s%s' % (prfx, oname))
373
                print_dict(pretty_keys(pretty_obj), exclude=('name'))
374
                print
375
            else:
376
                oname = '%s%9s %s' % (prfx, size, oname)
377
                oname += '/' if isDir else ''
378
                print(oname)
379
            if self['more']:
380
                page_hold(index, limit, len(object_list))
381

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

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

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

    
444

    
445
@command(pithos_cmds)
446
class file_mkdir(_file_container_command, _optional_output_cmd):
447
    """Create a directory"""
448

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

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

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

    
468

    
469
@command(pithos_cmds)
470
class file_touch(_file_container_command, _optional_output_cmd):
471
    """Create an empty object (file)
472
    If object exists, this command will reset it to 0 length
473
    """
474

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

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

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

    
495

    
496
@command(pithos_cmds)
497
class file_create(_file_container_command, _optional_output_cmd):
498
    """Create a container"""
499

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

    
510
    @errors.generic.all
511
    @errors.pithos.connection
512
    @errors.pithos.container
513
    def _run(self, container):
514
        self._optional_output(self.client.create_container(
515
            container=container,
516
            sizelimit=self['limit'],
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(container)
527

    
528

    
529
class _source_destination_command(_file_container_command):
530

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

    
542
    def __init__(self, arguments={}, auth_base=None):
543
        self.arguments.update(arguments)
544
        super(_source_destination_command, self).__init__(
545
            self.arguments, auth_base)
546

    
547
    def _run(self, source_container___path, path_is_optional=False):
548
        super(_source_destination_command, self)._run(
549
            source_container___path,
550
            path_is_optional)
551
        self.dst_client = PithosClient(
552
            base_url=self.client.base_url,
553
            token=self.client.token,
554
            account=self['destination_account'] or self.client.account)
555

    
556
    @errors.generic.all
557
    @errors.pithos.account
558
    def _dest_container_path(self, dest_container_path):
559
        if self['destination_container']:
560
            self.dst_client.container = self['destination_container']
561
            return (self['destination_container'], dest_container_path)
562
        if dest_container_path:
563
            dst = dest_container_path.split(':')
564
            if len(dst) > 1:
565
                try:
566
                    self.dst_client.container = dst[0]
567
                    self.dst_client.get_container_info(dst[0])
568
                except ClientError as err:
569
                    if err.status in (404, 204):
570
                        raiseCLIError(
571
                            'Destination container %s not found' % dst[0])
572
                    raise
573
                else:
574
                    self.dst_client.container = dst[0]
575
                return (dst[0], dst[1])
576
            return(None, dst[0])
577
        raiseCLIError('No destination container:path provided')
578

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

    
582
    def _get_src_objects(self, src_path, source_version=None):
583
        """Get a list of the source objects to be called
584

585
        :param src_path: (str) source path
586

587
        :returns: (method, params) a method that returns a list when called
588
        or (object) if it is a single object
589
        """
590
        if src_path and src_path[-1] == '/':
591
            src_path = src_path[:-1]
592

    
593
        if self['prefix']:
594
            return (self._get_all, dict(prefix=src_path))
595
        try:
596
            srcobj = self.client.get_object_info(
597
                src_path, version=source_version)
598
        except ClientError as srcerr:
599
            if srcerr.status == 404:
600
                raiseCLIError(
601
                    'Source object %s not in source container %s' % (
602
                        src_path,
603
                        self.client.container),
604
                    details=['Hint: --with-prefix to match multiple objects'])
605
            elif srcerr.status not in (204,):
606
                raise
607
            return (self.client.list_objects, {})
608

    
609
        if self._is_dir(srcobj):
610
            if not self['recursive']:
611
                raiseCLIError(
612
                    'Object %s of cont. %s is a dir' % (
613
                        src_path,
614
                        self.client.container),
615
                    details=['Use --recursive to access directories'])
616
            return (self._get_all, dict(prefix=src_path))
617
        srcobj['name'] = src_path
618
        return srcobj
619

    
620
    def src_dst_pairs(self, dst_path, source_version=None):
621
        src_iter = self._get_src_objects(self.path, source_version)
622
        src_N = isinstance(src_iter, tuple)
623
        add_prefix = self['add_prefix'].strip('/')
624

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

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

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

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

    
673

    
674
@command(pithos_cmds)
675
class file_copy(_source_destination_command, _optional_output_cmd):
676
    """Copy objects from container to (another) container
677
    Semantics:
678
    copy cont:path dir
679
    .   transfer path as dir/path
680
    copy cont:path cont2:
681
    .   trasnfer all <obj> prefixed with path to container cont2
682
    copy cont:path [cont2:]path2
683
    .   transfer path to path2
684
    Use options:
685
    1. <container1>:<path1> [container2:]<path2> : if container2 is not given,
686
    destination is container1:path2
687
    2. <container>:<path1> <path2> : make a copy in the same container
688
    3. Can use --container= instead of <container1>
689
    """
690

    
691
    arguments = dict(
692
        destination_account=ValueArgument(
693
            'Account to copy to',
694
            ('-a', '--dst-account')),
695
        destination_container=ValueArgument(
696
            'use it if destination container name contains a : character',
697
            ('-D', '--dst-container')),
698
        public=ValueArgument('make object publicly accessible', '--public'),
699
        content_type=ValueArgument(
700
            'change object\'s content type',
701
            '--content-type'),
702
        recursive=FlagArgument(
703
            'copy directory and contents',
704
            ('-R', '--recursive')),
705
        prefix=FlagArgument(
706
            'Match objects prefixed with src path (feels like src_path*)',
707
            '--with-prefix',
708
            default=''),
709
        suffix=ValueArgument(
710
            'Suffix of source objects (feels like *suffix)',
711
            '--with-suffix',
712
            default=''),
713
        add_prefix=ValueArgument('Prefix targets', '--add-prefix', default=''),
714
        add_suffix=ValueArgument('Suffix targets', '--add-suffix', default=''),
715
        prefix_replace=ValueArgument(
716
            'Prefix of src to replace with dst path + add_prefix, if matched',
717
            '--prefix-to-replace',
718
            default=''),
719
        suffix_replace=ValueArgument(
720
            'Suffix of src to replace with add_suffix, if matched',
721
            '--suffix-to-replace',
722
            default=''),
723
        source_version=ValueArgument(
724
            'copy specific version',
725
            ('-S', '--source-version'))
726
    )
727

    
728
    @errors.generic.all
729
    @errors.pithos.connection
730
    @errors.pithos.container
731
    @errors.pithos.account
732
    def _run(self, dst_path):
733
        no_source_object = True
734
        src_account = self.client.account if (
735
            self['destination_account']) else None
736
        for src_obj, dst_obj in self.src_dst_pairs(
737
                dst_path, self['source_version']):
738
            no_source_object = False
739
            r = self.dst_client.copy_object(
740
                src_container=self.client.container,
741
                src_object=src_obj,
742
                dst_container=self.dst_client.container,
743
                dst_object=dst_obj,
744
                source_account=src_account,
745
                source_version=self['source_version'],
746
                public=self['public'],
747
                content_type=self['content_type'])
748
        if no_source_object:
749
            raiseCLIError('No object %s in container %s' % (
750
                self.path,
751
                self.container))
752
        self._optional_output(r)
753

    
754
    def main(
755
            self, source_container___path,
756
            destination_container___path=None):
757
        super(file_copy, self)._run(
758
            source_container___path,
759
            path_is_optional=False)
760
        (dst_cont, dst_path) = self._dest_container_path(
761
            destination_container___path)
762
        self.dst_client.container = dst_cont or self.container
763
        self._run(dst_path=dst_path or '')
764

    
765

    
766
@command(pithos_cmds)
767
class file_move(_source_destination_command, _optional_output_cmd):
768
    """Move/rename objects from container to (another) container
769
    Semantics:
770
    move cont:path dir
771
    .   rename path as dir/path
772
    move cont:path cont2:
773
    .   trasnfer all <obj> prefixed with path to container cont2
774
    move cont:path [cont2:]path2
775
    .   transfer path to path2
776
    Use options:
777
    1. <container1>:<path1> [container2:]<path2> : if container2 is not given,
778
    destination is container1:path2
779
    2. <container>:<path1> <path2> : move in the same container
780
    3. Can use --container= instead of <container1>
781
    """
782

    
783
    arguments = dict(
784
        destination_account=ValueArgument(
785
            'Account to move to',
786
            ('-a', '--dst-account')),
787
        destination_container=ValueArgument(
788
            'use it if destination container name contains a : character',
789
            ('-D', '--dst-container')),
790
        public=ValueArgument('make object publicly accessible', '--public'),
791
        content_type=ValueArgument(
792
            'change object\'s content type',
793
            '--content-type'),
794
        recursive=FlagArgument(
795
            'copy directory and contents',
796
            ('-R', '--recursive')),
797
        prefix=FlagArgument(
798
            'Match objects prefixed with src path (feels like src_path*)',
799
            '--with-prefix',
800
            default=''),
801
        suffix=ValueArgument(
802
            'Suffix of source objects (feels like *suffix)',
803
            '--with-suffix',
804
            default=''),
805
        add_prefix=ValueArgument('Prefix targets', '--add-prefix', default=''),
806
        add_suffix=ValueArgument('Suffix targets', '--add-suffix', default=''),
807
        prefix_replace=ValueArgument(
808
            'Prefix of src to replace with dst path + add_prefix, if matched',
809
            '--prefix-to-replace',
810
            default=''),
811
        suffix_replace=ValueArgument(
812
            'Suffix of src to replace with add_suffix, if matched',
813
            '--suffix-to-replace',
814
            default='')
815
    )
816

    
817
    @errors.generic.all
818
    @errors.pithos.connection
819
    @errors.pithos.container
820
    def _run(self, dst_path):
821
        no_source_object = True
822
        src_account = self.client.account if (
823
            self['destination_account']) else None
824
        for src_obj, dst_obj in self.src_dst_pairs(dst_path):
825
            no_source_object = False
826
            r = self.dst_client.move_object(
827
                src_container=self.container,
828
                src_object=src_obj,
829
                dst_container=self.dst_client.container,
830
                dst_object=dst_obj,
831
                source_account=src_account,
832
                public=self['public'],
833
                content_type=self['content_type'])
834
        if no_source_object:
835
            raiseCLIError('No object %s in container %s' % (
836
                self.path,
837
                self.container))
838
        self._optional_output(r)
839

    
840
    def main(
841
            self, source_container___path,
842
            destination_container___path=None):
843
        super(self.__class__, self)._run(
844
            source_container___path,
845
            path_is_optional=False)
846
        (dst_cont, dst_path) = self._dest_container_path(
847
            destination_container___path)
848
        (dst_cont, dst_path) = self._dest_container_path(
849
            destination_container___path)
850
        self.dst_client.container = dst_cont or self.container
851
        self._run(dst_path=dst_path or '')
852

    
853

    
854
@command(pithos_cmds)
855
class file_append(_file_container_command, _optional_output_cmd):
856
    """Append local file to (existing) remote object
857
    The remote object should exist.
858
    If the remote object is a directory, it is transformed into a file.
859
    In the later case, objects under the directory remain intact.
860
    """
861

    
862
    arguments = dict(
863
        progress_bar=ProgressBarArgument(
864
            'do not show progress bar',
865
            ('-N', '--no-progress-bar'),
866
            default=False)
867
    )
868

    
869
    @errors.generic.all
870
    @errors.pithos.connection
871
    @errors.pithos.container
872
    @errors.pithos.object_path
873
    def _run(self, local_path):
874
        (progress_bar, upload_cb) = self._safe_progress_bar('Appending')
875
        try:
876
            f = open(local_path, 'rb')
877
            self._optional_output(
878
                self.client.append_object(self.path, f, upload_cb))
879
        except Exception:
880
            self._safe_progress_bar_finish(progress_bar)
881
            raise
882
        finally:
883
            self._safe_progress_bar_finish(progress_bar)
884

    
885
    def main(self, local_path, container___path):
886
        super(self.__class__, self)._run(
887
            container___path,
888
            path_is_optional=False)
889
        self._run(local_path)
890

    
891

    
892
@command(pithos_cmds)
893
class file_truncate(_file_container_command, _optional_output_cmd):
894
    """Truncate remote file up to a size (default is 0)"""
895

    
896
    @errors.generic.all
897
    @errors.pithos.connection
898
    @errors.pithos.container
899
    @errors.pithos.object_path
900
    @errors.pithos.object_size
901
    def _run(self, size=0):
902
        self._optional_output(self.client.truncate_object(self.path, size))
903

    
904
    def main(self, container___path, size=0):
905
        super(self.__class__, self)._run(container___path)
906
        self._run(size=size)
907

    
908

    
909
@command(pithos_cmds)
910
class file_overwrite(_file_container_command, _optional_output_cmd):
911
    """Overwrite part (from start to end) of a remote file
912
    overwrite local-path container 10 20
913
    .   will overwrite bytes from 10 to 20 of a remote file with the same name
914
    .   as local-path basename
915
    overwrite local-path container:path 10 20
916
    .   will overwrite as above, but the remote file is named path
917
    """
918

    
919
    arguments = dict(
920
        progress_bar=ProgressBarArgument(
921
            'do not show progress bar',
922
            ('-N', '--no-progress-bar'),
923
            default=False)
924
    )
925

    
926
    def _open_file(self, local_path, start):
927
        f = open(path.abspath(local_path), 'rb')
928
        f.seek(0, 2)
929
        f_size = f.tell()
930
        f.seek(start, 0)
931
        return (f, f_size)
932

    
933
    @errors.generic.all
934
    @errors.pithos.connection
935
    @errors.pithos.container
936
    @errors.pithos.object_path
937
    @errors.pithos.object_size
938
    def _run(self, local_path, start, end):
939
        (start, end) = (int(start), int(end))
940
        (f, f_size) = self._open_file(local_path, start)
941
        (progress_bar, upload_cb) = self._safe_progress_bar(
942
            'Overwrite %s bytes' % (end - start))
943
        try:
944
            self._optional_output(self.client.overwrite_object(
945
                obj=self.path,
946
                start=start,
947
                end=end,
948
                source_file=f,
949
                upload_cb=upload_cb))
950
        finally:
951
            self._safe_progress_bar_finish(progress_bar)
952

    
953
    def main(self, local_path, container___path, start, end):
954
        super(self.__class__, self)._run(
955
            container___path,
956
            path_is_optional=None)
957
        self.path = self.path or path.basename(local_path)
958
        self._run(local_path=local_path, start=start, end=end)
959

    
960

    
961
@command(pithos_cmds)
962
class file_manifest(_file_container_command, _optional_output_cmd):
963
    """Create a remote file of uploaded parts by manifestation
964
    Remains functional for compatibility with OOS Storage. Users are advised
965
    to use the upload command instead.
966
    Manifestation is a compliant process for uploading large files. The files
967
    have to be chunked in smalled files and uploaded as <prefix><increment>
968
    where increment is 1, 2, ...
969
    Finally, the manifest command glues partial files together in one file
970
    named <prefix>
971
    The upload command is faster, easier and more intuitive than manifest
972
    """
973

    
974
    arguments = dict(
975
        etag=ValueArgument('check written data', '--etag'),
976
        content_encoding=ValueArgument(
977
            'set MIME content type',
978
            '--content-encoding'),
979
        content_disposition=ValueArgument(
980
            'the presentation style of the object',
981
            '--content-disposition'),
982
        content_type=ValueArgument(
983
            'specify content type',
984
            '--content-type',
985
            default='application/octet-stream'),
986
        sharing=SharingArgument(
987
            '\n'.join([
988
                'define object sharing policy',
989
                '    ( "read=user1,grp1,user2,... write=user1,grp2,..." )']),
990
            '--sharing'),
991
        public=FlagArgument('make object publicly accessible', '--public')
992
    )
993

    
994
    @errors.generic.all
995
    @errors.pithos.connection
996
    @errors.pithos.container
997
    @errors.pithos.object_path
998
    def _run(self):
999
        self._optional_output(self.client.create_object_by_manifestation(
1000
            self.path,
1001
            content_encoding=self['content_encoding'],
1002
            content_disposition=self['content_disposition'],
1003
            content_type=self['content_type'],
1004
            sharing=self['sharing'],
1005
            public=self['public']))
1006

    
1007
    def main(self, container___path):
1008
        super(self.__class__, self)._run(
1009
            container___path,
1010
            path_is_optional=False)
1011
        self.run()
1012

    
1013

    
1014
@command(pithos_cmds)
1015
class file_upload(_file_container_command, _optional_output_cmd):
1016
    """Upload a file"""
1017

    
1018
    arguments = dict(
1019
        use_hashes=FlagArgument(
1020
            'provide hashmap file instead of data',
1021
            '--use-hashes'),
1022
        etag=ValueArgument('check written data', '--etag'),
1023
        unchunked=FlagArgument('avoid chunked transfer mode', '--unchunked'),
1024
        content_encoding=ValueArgument(
1025
            'set MIME content type',
1026
            '--content-encoding'),
1027
        content_disposition=ValueArgument(
1028
            'specify objects presentation style',
1029
            '--content-disposition'),
1030
        content_type=ValueArgument('specify content type', '--content-type'),
1031
        sharing=SharingArgument(
1032
            help='\n'.join([
1033
                'define sharing object policy',
1034
                '( "read=user1,grp1,user2,... write=user1,grp2,... )']),
1035
            parsed_name='--sharing'),
1036
        public=FlagArgument('make object publicly accessible', '--public'),
1037
        poolsize=IntArgument('set pool size', '--with-pool-size'),
1038
        progress_bar=ProgressBarArgument(
1039
            'do not show progress bar',
1040
            ('-N', '--no-progress-bar'),
1041
            default=False),
1042
        overwrite=FlagArgument('Force (over)write', ('-f', '--force')),
1043
        recursive=FlagArgument(
1044
            'Recursively upload directory *contents* + subdirectories',
1045
            ('-R', '--recursive'))
1046
    )
1047

    
1048
    def _check_container_limit(self, path):
1049
        cl_dict = self.client.get_container_limit()
1050
        container_limit = int(cl_dict['x-container-policy-quota'])
1051
        r = self.client.container_get()
1052
        used_bytes = sum(int(o['bytes']) for o in r.json)
1053
        path_size = get_path_size(path)
1054
        if container_limit and path_size > (container_limit - used_bytes):
1055
            raiseCLIError(
1056
                'Container(%s) (limit(%s) - used(%s)) < size(%s) of %s' % (
1057
                    self.client.container,
1058
                    format_size(container_limit),
1059
                    format_size(used_bytes),
1060
                    format_size(path_size),
1061
                    path),
1062
                importance=1, details=[
1063
                    'Check accound limit: /file quota',
1064
                    'Check container limit:',
1065
                    '\t/file containerlimit get %s' % self.client.container,
1066
                    'Increase container limit:',
1067
                    '\t/file containerlimit set <new limit> %s' % (
1068
                        self.client.container)])
1069

    
1070
    def _path_pairs(self, local_path, remote_path):
1071
        """Get pairs of local and remote paths"""
1072
        lpath = path.abspath(local_path)
1073
        short_path = lpath.split(path.sep)[-1]
1074
        rpath = remote_path or short_path
1075
        if path.isdir(lpath):
1076
            if not self['recursive']:
1077
                raiseCLIError('%s is a directory' % lpath, details=[
1078
                    'Use -R to upload directory contents'])
1079
            robj = self.client.container_get(path=rpath)
1080
            if robj.json and not self['overwrite']:
1081
                raiseCLIError(
1082
                    'Objects prefixed with %s already exist' % rpath,
1083
                    importance=1,
1084
                    details=['Existing objects:'] + ['\t%s:\t%s' % (
1085
                        o['content_type'][12:],
1086
                        o['name']) for o in robj.json] + [
1087
                        'Use -f to add, overwrite or resume'])
1088
            if not self['overwrite']:
1089
                try:
1090
                    topobj = self.client.get_object_info(rpath)
1091
                    if not self._is_dir(topobj):
1092
                        raiseCLIError(
1093
                            'Object %s exists but it is not a dir' % rpath,
1094
                            importance=1, details=['Use -f to overwrite'])
1095
                except ClientError as ce:
1096
                    if ce.status != 404:
1097
                        raise
1098
            self._check_container_limit(lpath)
1099
            prev = ''
1100
            for top, subdirs, files in walk(lpath):
1101
                if top != prev:
1102
                    prev = top
1103
                    try:
1104
                        rel_path = rpath + top.split(lpath)[1]
1105
                    except IndexError:
1106
                        rel_path = rpath
1107
                    print('mkdir %s:%s' % (self.client.container, rel_path))
1108
                    self.client.create_directory(rel_path)
1109
                for f in files:
1110
                    fpath = path.join(top, f)
1111
                    if path.isfile(fpath):
1112
                        yield open(fpath, 'rb'), '%s/%s' % (rel_path, f)
1113
                    else:
1114
                        print('%s is not a regular file' % fpath)
1115
        else:
1116
            if not path.isfile(lpath):
1117
                raiseCLIError('%s is not a regular file' % lpath)
1118
            try:
1119
                robj = self.client.get_object_info(rpath)
1120
                if remote_path and self._is_dir(robj):
1121
                    rpath += '/%s' % short_path
1122
                    self.client.get_object_info(rpath)
1123
                if not self['overwrite']:
1124
                    raiseCLIError(
1125
                        'Object %s already exists' % rpath,
1126
                        importance=1,
1127
                        details=['use -f to overwrite or resume'])
1128
            except ClientError as ce:
1129
                if ce.status != 404:
1130
                    raise
1131
            self._check_container_limit(lpath)
1132
            yield open(lpath, 'rb'), rpath
1133

    
1134
    @errors.generic.all
1135
    @errors.pithos.connection
1136
    @errors.pithos.container
1137
    @errors.pithos.object_path
1138
    @errors.pithos.local_path
1139
    def _run(self, local_path, remote_path):
1140
        poolsize = self['poolsize']
1141
        if poolsize > 0:
1142
            self.client.MAX_THREADS = int(poolsize)
1143
        params = dict(
1144
            content_encoding=self['content_encoding'],
1145
            content_type=self['content_type'],
1146
            content_disposition=self['content_disposition'],
1147
            sharing=self['sharing'],
1148
            public=self['public'])
1149
        uploaded = []
1150
        container_info_cache = dict()
1151
        for f, rpath in self._path_pairs(local_path, remote_path):
1152
            print('%s --> %s:%s' % (f.name, self.client.container, rpath))
1153
            if self['unchunked']:
1154
                r = self.client.upload_object_unchunked(
1155
                    rpath, f,
1156
                    etag=self['etag'], withHashFile=self['use_hashes'],
1157
                    **params)
1158
                if self['with_output'] or self['json_output']:
1159
                    r['name'] = '%s: %s' % (self.client.container, rpath)
1160
                    uploaded.append(r)
1161
            else:
1162
                try:
1163
                    (progress_bar, upload_cb) = self._safe_progress_bar(
1164
                        'Uploading %s' % f.name.split(path.sep)[-1])
1165
                    if progress_bar:
1166
                        hash_bar = progress_bar.clone()
1167
                        hash_cb = hash_bar.get_generator(
1168
                            'Calculating block hashes')
1169
                    else:
1170
                        hash_cb = None
1171
                    r = self.client.upload_object(
1172
                        rpath, f,
1173
                        hash_cb=hash_cb,
1174
                        upload_cb=upload_cb,
1175
                        container_info_cache=container_info_cache,
1176
                        **params)
1177
                    if self['with_output'] or self['json_output']:
1178
                        r['name'] = '%s: %s' % (self.client.container, rpath)
1179
                        uploaded.append(r)
1180
                except Exception:
1181
                    self._safe_progress_bar_finish(progress_bar)
1182
                    raise
1183
                finally:
1184
                    self._safe_progress_bar_finish(progress_bar)
1185
        self._optional_output(uploaded)
1186
        print('Upload completed')
1187

    
1188
    def main(self, local_path, container____path__=None):
1189
        super(self.__class__, self)._run(container____path__)
1190
        remote_path = self.path or path.basename(local_path)
1191
        self._run(local_path=local_path, remote_path=remote_path)
1192

    
1193

    
1194
@command(pithos_cmds)
1195
class file_cat(_file_container_command):
1196
    """Print remote file contents to console"""
1197

    
1198
    arguments = dict(
1199
        range=RangeArgument('show range of data', '--range'),
1200
        if_match=ValueArgument('show output if ETags match', '--if-match'),
1201
        if_none_match=ValueArgument(
1202
            'show output if ETags match',
1203
            '--if-none-match'),
1204
        if_modified_since=DateArgument(
1205
            'show output modified since then',
1206
            '--if-modified-since'),
1207
        if_unmodified_since=DateArgument(
1208
            'show output unmodified since then',
1209
            '--if-unmodified-since'),
1210
        object_version=ValueArgument(
1211
            'get the specific version',
1212
            ('-O', '--object-version'))
1213
    )
1214

    
1215
    @errors.generic.all
1216
    @errors.pithos.connection
1217
    @errors.pithos.container
1218
    @errors.pithos.object_path
1219
    def _run(self):
1220
        self.client.download_object(
1221
            self.path,
1222
            stdout,
1223
            range_str=self['range'],
1224
            version=self['object_version'],
1225
            if_match=self['if_match'],
1226
            if_none_match=self['if_none_match'],
1227
            if_modified_since=self['if_modified_since'],
1228
            if_unmodified_since=self['if_unmodified_since'])
1229

    
1230
    def main(self, container___path):
1231
        super(self.__class__, self)._run(
1232
            container___path,
1233
            path_is_optional=False)
1234
        self._run()
1235

    
1236

    
1237
@command(pithos_cmds)
1238
class file_download(_file_container_command):
1239
    """Download remote object as local file
1240
    If local destination is a directory:
1241
    *   download <container>:<path> <local dir> -R
1242
    will download all files on <container> prefixed as <path>,
1243
    to <local dir>/<full path>
1244
    *   download <container>:<path> <local dir> --exact-match
1245
    will download only one file, exactly matching <path>
1246
    ATTENTION: to download cont:dir1/dir2/file there must exist objects
1247
    cont:dir1 and cont:dir1/dir2 of type application/directory
1248
    To create directory objects, use /file mkdir
1249
    """
1250

    
1251
    arguments = dict(
1252
        resume=FlagArgument('Resume instead of overwrite', ('-r', '--resume')),
1253
        range=RangeArgument('show range of data', '--range'),
1254
        if_match=ValueArgument('show output if ETags match', '--if-match'),
1255
        if_none_match=ValueArgument(
1256
            'show output if ETags match',
1257
            '--if-none-match'),
1258
        if_modified_since=DateArgument(
1259
            'show output modified since then',
1260
            '--if-modified-since'),
1261
        if_unmodified_since=DateArgument(
1262
            'show output unmodified since then',
1263
            '--if-unmodified-since'),
1264
        object_version=ValueArgument(
1265
            'get the specific version',
1266
            ('-O', '--object-version')),
1267
        poolsize=IntArgument('set pool size', '--with-pool-size'),
1268
        progress_bar=ProgressBarArgument(
1269
            'do not show progress bar',
1270
            ('-N', '--no-progress-bar'),
1271
            default=False),
1272
        recursive=FlagArgument(
1273
            'Download a remote path and all its contents',
1274
            ('-R', '--recursive'))
1275
    )
1276

    
1277
    def _outputs(self, local_path):
1278
        """:returns: (local_file, remote_path)"""
1279
        remotes = []
1280
        if self['recursive']:
1281
            r = self.client.container_get(
1282
                prefix=self.path or '/',
1283
                if_modified_since=self['if_modified_since'],
1284
                if_unmodified_since=self['if_unmodified_since'])
1285
            dirlist = dict()
1286
            for remote in r.json:
1287
                rname = remote['name'].strip('/')
1288
                tmppath = ''
1289
                for newdir in rname.strip('/').split('/')[:-1]:
1290
                    tmppath = '/'.join([tmppath, newdir])
1291
                    dirlist.update({tmppath.strip('/'): True})
1292
                remotes.append((rname, file_download._is_dir(remote)))
1293
            dir_remotes = [r[0] for r in remotes if r[1]]
1294
            if not set(dirlist).issubset(dir_remotes):
1295
                badguys = [bg.strip('/') for bg in set(
1296
                    dirlist).difference(dir_remotes)]
1297
                raiseCLIError(
1298
                    'Some remote paths contain non existing directories',
1299
                    details=['Missing remote directories:'] + badguys)
1300
        elif self.path:
1301
            r = self.client.get_object_info(
1302
                self.path,
1303
                version=self['object_version'])
1304
            if file_download._is_dir(r):
1305
                raiseCLIError(
1306
                    'Illegal download: Remote object %s is a directory' % (
1307
                        self.path),
1308
                    details=['To download a directory, try --recursive'])
1309
            if '/' in self.path.strip('/') and not local_path:
1310
                raiseCLIError(
1311
                    'Illegal download: remote object %s contains "/"' % (
1312
                        self.path),
1313
                    details=[
1314
                        'To download an object containing "/" characters',
1315
                        'either create the remote directories or',
1316
                        'specify a non-directory local path for this object'])
1317
            remotes = [(self.path, False)]
1318
        if not remotes:
1319
            if self.path:
1320
                raiseCLIError(
1321
                    'No matching path %s on container %s' % (
1322
                        self.path,
1323
                        self.container),
1324
                    details=[
1325
                        'To list the contents of %s, try:' % self.container,
1326
                        '   /file list %s' % self.container])
1327
            raiseCLIError(
1328
                'Illegal download of container %s' % self.container,
1329
                details=[
1330
                    'To download a whole container, try:',
1331
                    '   /file download --recursive <container>'])
1332

    
1333
        lprefix = path.abspath(local_path or path.curdir)
1334
        if path.isdir(lprefix):
1335
            for rpath, remote_is_dir in remotes:
1336
                lpath = '/%s/%s' % (lprefix.strip('/'), rpath.strip('/'))
1337
                if remote_is_dir:
1338
                    if path.exists(lpath) and path.isdir(lpath):
1339
                        continue
1340
                    makedirs(lpath)
1341
                elif path.exists(lpath):
1342
                    if not self['resume']:
1343
                        print('File %s exists, aborting...' % lpath)
1344
                        continue
1345
                    with open(lpath, 'rwb+') as f:
1346
                        yield (f, rpath)
1347
                else:
1348
                    with open(lpath, 'wb+') as f:
1349
                        yield (f, rpath)
1350
        elif path.exists(lprefix):
1351
            if len(remotes) > 1:
1352
                raiseCLIError(
1353
                    '%s remote objects cannot be merged in local file %s' % (
1354
                        len(remotes),
1355
                        local_path),
1356
                    details=[
1357
                        'To download multiple objects, local path should be',
1358
                        'a directory, or use download without a local path'])
1359
            (rpath, remote_is_dir) = remotes[0]
1360
            if remote_is_dir:
1361
                raiseCLIError(
1362
                    'Remote directory %s should not replace local file %s' % (
1363
                        rpath,
1364
                        local_path))
1365
            if self['resume']:
1366
                with open(lprefix, 'rwb+') as f:
1367
                    yield (f, rpath)
1368
            else:
1369
                raiseCLIError(
1370
                    'Local file %s already exist' % local_path,
1371
                    details=['Try --resume to overwrite it'])
1372
        else:
1373
            if len(remotes) > 1 or remotes[0][1]:
1374
                raiseCLIError(
1375
                    'Local directory %s does not exist' % local_path)
1376
            with open(lprefix, 'wb+') as f:
1377
                yield (f, remotes[0][0])
1378

    
1379
    @errors.generic.all
1380
    @errors.pithos.connection
1381
    @errors.pithos.container
1382
    @errors.pithos.object_path
1383
    @errors.pithos.local_path
1384
    def _run(self, local_path):
1385
        #outputs = self._outputs(local_path)
1386
        poolsize = self['poolsize']
1387
        if poolsize:
1388
            self.client.MAX_THREADS = int(poolsize)
1389
        progress_bar = None
1390
        try:
1391
            for f, rpath in self._outputs(local_path):
1392
                (
1393
                    progress_bar,
1394
                    download_cb) = self._safe_progress_bar(
1395
                        'Download %s' % rpath)
1396
                self.client.download_object(
1397
                    rpath, f,
1398
                    download_cb=download_cb,
1399
                    range_str=self['range'],
1400
                    version=self['object_version'],
1401
                    if_match=self['if_match'],
1402
                    resume=self['resume'],
1403
                    if_none_match=self['if_none_match'],
1404
                    if_modified_since=self['if_modified_since'],
1405
                    if_unmodified_since=self['if_unmodified_since'])
1406
        except KeyboardInterrupt:
1407
            from threading import activeCount, enumerate as activethreads
1408
            timeout = 0.5
1409
            while activeCount() > 1:
1410
                stdout.write('\nCancel %s threads: ' % (activeCount() - 1))
1411
                stdout.flush()
1412
                for thread in activethreads():
1413
                    try:
1414
                        thread.join(timeout)
1415
                        stdout.write('.' if thread.isAlive() else '*')
1416
                    except RuntimeError:
1417
                        continue
1418
                    finally:
1419
                        stdout.flush()
1420
                        timeout += 0.1
1421
            print('\nDownload canceled by user')
1422
            if local_path is not None:
1423
                print('to resume, re-run with --resume')
1424
        except Exception:
1425
            self._safe_progress_bar_finish(progress_bar)
1426
            raise
1427
        finally:
1428
            self._safe_progress_bar_finish(progress_bar)
1429

    
1430
    def main(self, container___path, local_path=None):
1431
        super(self.__class__, self)._run(container___path)
1432
        self._run(local_path=local_path)
1433

    
1434

    
1435
@command(pithos_cmds)
1436
class file_hashmap(_file_container_command, _optional_json):
1437
    """Get the hash-map of an object"""
1438

    
1439
    arguments = dict(
1440
        if_match=ValueArgument('show output if ETags match', '--if-match'),
1441
        if_none_match=ValueArgument(
1442
            'show output if ETags match', '--if-none-match'),
1443
        if_modified_since=DateArgument(
1444
            'show output modified since then', '--if-modified-since'),
1445
        if_unmodified_since=DateArgument(
1446
            'show output unmodified since then', '--if-unmodified-since'),
1447
        object_version=ValueArgument(
1448
            'get the specific version', ('-O', '--object-version'))
1449
    )
1450

    
1451
    @errors.generic.all
1452
    @errors.pithos.connection
1453
    @errors.pithos.container
1454
    @errors.pithos.object_path
1455
    def _run(self):
1456
        self._print(self.client.get_object_hashmap(
1457
            self.path,
1458
            version=self['object_version'],
1459
            if_match=self['if_match'],
1460
            if_none_match=self['if_none_match'],
1461
            if_modified_since=self['if_modified_since'],
1462
            if_unmodified_since=self['if_unmodified_since']), print_dict)
1463

    
1464
    def main(self, container___path):
1465
        super(self.__class__, self)._run(
1466
            container___path,
1467
            path_is_optional=False)
1468
        self._run()
1469

    
1470

    
1471
@command(pithos_cmds)
1472
class file_delete(_file_container_command, _optional_output_cmd):
1473
    """Delete a container [or an object]
1474
    How to delete a non-empty container:
1475
    - empty the container:  /file delete -R <container>
1476
    - delete it:            /file delete <container>
1477
    .
1478
    Semantics of directory deletion:
1479
    .a preserve the contents: /file delete <container>:<directory>
1480
    .    objects of the form dir/filename can exist with a dir object
1481
    .b delete contents:       /file delete -R <container>:<directory>
1482
    .    all dir/* objects are affected, even if dir does not exist
1483
    .
1484
    To restore a deleted object OBJ in a container CONT:
1485
    - get object versions: /file versions CONT:OBJ
1486
    .   and choose the version to be restored
1487
    - restore the object:  /file copy --source-version=<version> CONT:OBJ OBJ
1488
    """
1489

    
1490
    arguments = dict(
1491
        until=DateArgument('remove history until that date', '--until'),
1492
        yes=FlagArgument('Do not prompt for permission', '--yes'),
1493
        recursive=FlagArgument(
1494
            'empty dir or container and delete (if dir)',
1495
            ('-R', '--recursive'))
1496
    )
1497

    
1498
    def __init__(self, arguments={}, auth_base=None):
1499
        super(self.__class__, self).__init__(arguments, auth_base)
1500
        self['delimiter'] = DelimiterArgument(
1501
            self,
1502
            parsed_name='--delimiter',
1503
            help='delete objects prefixed with <object><delimiter>')
1504

    
1505
    @errors.generic.all
1506
    @errors.pithos.connection
1507
    @errors.pithos.container
1508
    @errors.pithos.object_path
1509
    def _run(self):
1510
        if self.path:
1511
            if self['yes'] or ask_user(
1512
                    'Delete %s:%s ?' % (self.container, self.path)):
1513
                self._optional_output(self.client.del_object(
1514
                    self.path,
1515
                    until=self['until'], delimiter=self['delimiter']))
1516
            else:
1517
                print('Aborted')
1518
        else:
1519
            if self['recursive']:
1520
                ask_msg = 'Delete container contents'
1521
            else:
1522
                ask_msg = 'Delete container'
1523
            if self['yes'] or ask_user('%s %s ?' % (ask_msg, self.container)):
1524
                self._optional_output(self.client.del_container(
1525
                    until=self['until'], delimiter=self['delimiter']))
1526
            else:
1527
                print('Aborted')
1528

    
1529
    def main(self, container____path__=None):
1530
        super(self.__class__, self)._run(container____path__)
1531
        self._run()
1532

    
1533

    
1534
@command(pithos_cmds)
1535
class file_purge(_file_container_command, _optional_output_cmd):
1536
    """Delete a container and release related data blocks
1537
    Non-empty containers can not purged.
1538
    To purge a container with content:
1539
    .   /file delete -R <container>
1540
    .      objects are deleted, but data blocks remain on server
1541
    .   /file purge <container>
1542
    .      container and data blocks are released and deleted
1543
    """
1544

    
1545
    arguments = dict(
1546
        yes=FlagArgument('Do not prompt for permission', '--yes'),
1547
        force=FlagArgument('purge even if not empty', ('-F', '--force'))
1548
    )
1549

    
1550
    @errors.generic.all
1551
    @errors.pithos.connection
1552
    @errors.pithos.container
1553
    def _run(self):
1554
        if self['yes'] or ask_user('Purge container %s?' % self.container):
1555
            try:
1556
                r = self.client.purge_container()
1557
            except ClientError as ce:
1558
                if ce.status in (409,):
1559
                    if self['force']:
1560
                        self.client.del_container(delimiter='/')
1561
                        r = self.client.purge_container()
1562
                    else:
1563
                        raiseCLIError(ce, details=['Try -F to force-purge'])
1564
                else:
1565
                    raise
1566
            self._optional_output(r)
1567
        else:
1568
            print('Aborted')
1569

    
1570
    def main(self, container=None):
1571
        super(self.__class__, self)._run(container)
1572
        if container and self.container != container:
1573
            raiseCLIError('Invalid container name %s' % container, details=[
1574
                'Did you mean "%s" ?' % self.container,
1575
                'Use --container for names containing :'])
1576
        self._run()
1577

    
1578

    
1579
@command(pithos_cmds)
1580
class file_publish(_file_container_command):
1581
    """Publish the object and print the public url"""
1582

    
1583
    @errors.generic.all
1584
    @errors.pithos.connection
1585
    @errors.pithos.container
1586
    @errors.pithos.object_path
1587
    def _run(self):
1588
        url = self.client.publish_object(self.path)
1589
        print(url)
1590

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

    
1597

    
1598
@command(pithos_cmds)
1599
class file_unpublish(_file_container_command, _optional_output_cmd):
1600
    """Unpublish an object"""
1601

    
1602
    @errors.generic.all
1603
    @errors.pithos.connection
1604
    @errors.pithos.container
1605
    @errors.pithos.object_path
1606
    def _run(self):
1607
            self._optional_output(self.client.unpublish_object(self.path))
1608

    
1609
    def main(self, container___path):
1610
        super(self.__class__, self)._run(
1611
            container___path,
1612
            path_is_optional=False)
1613
        self._run()
1614

    
1615

    
1616
@command(pithos_cmds)
1617
class file_permissions(_pithos_init):
1618
    """Manage user and group accessibility for objects
1619
    Permissions are lists of users and user groups. There are read and write
1620
    permissions. Users and groups with write permission have also read
1621
    permission.
1622
    """
1623

    
1624

    
1625
def print_permissions(permissions_dict):
1626
    expected_keys = ('read', 'write')
1627
    if set(permissions_dict).issubset(expected_keys):
1628
        print_dict(permissions_dict)
1629
    else:
1630
        invalid_keys = set(permissions_dict.keys()).difference(expected_keys)
1631
        raiseCLIError(
1632
            'Illegal permission keys: %s' % ', '.join(invalid_keys),
1633
            importance=1, details=[
1634
                'Valid permission types: %s' % ' '.join(expected_keys)])
1635

    
1636

    
1637
@command(pithos_cmds)
1638
class file_permissions_get(_file_container_command, _optional_json):
1639
    """Get read and write permissions of an object"""
1640

    
1641
    @errors.generic.all
1642
    @errors.pithos.connection
1643
    @errors.pithos.container
1644
    @errors.pithos.object_path
1645
    def _run(self):
1646
        self._print(
1647
            self.client.get_object_sharing(self.path), print_permissions)
1648

    
1649
    def main(self, container___path):
1650
        super(self.__class__, self)._run(
1651
            container___path,
1652
            path_is_optional=False)
1653
        self._run()
1654

    
1655

    
1656
@command(pithos_cmds)
1657
class file_permissions_set(_file_container_command, _optional_output_cmd):
1658
    """Set permissions for an object
1659
    New permissions overwrite existing permissions.
1660
    Permission format:
1661
    -   read=<username>[,usergroup[,...]]
1662
    -   write=<username>[,usegroup[,...]]
1663
    E.g. to give read permissions for file F to users A and B and write for C:
1664
    .       /file permissions set F read=A,B write=C
1665
    """
1666

    
1667
    @errors.generic.all
1668
    def format_permission_dict(self, permissions):
1669
        read = False
1670
        write = False
1671
        for perms in permissions:
1672
            splstr = perms.split('=')
1673
            if 'read' == splstr[0]:
1674
                read = [ug.strip() for ug in splstr[1].split(',')]
1675
            elif 'write' == splstr[0]:
1676
                write = [ug.strip() for ug in splstr[1].split(',')]
1677
            else:
1678
                msg = 'Usage:\tread=<groups,users> write=<groups,users>'
1679
                raiseCLIError(None, msg)
1680
        return (read, write)
1681

    
1682
    @errors.generic.all
1683
    @errors.pithos.connection
1684
    @errors.pithos.container
1685
    @errors.pithos.object_path
1686
    def _run(self, read, write):
1687
        self._optional_output(self.client.set_object_sharing(
1688
            self.path,
1689
            read_permission=read, write_permission=write))
1690

    
1691
    def main(self, container___path, *permissions):
1692
        super(self.__class__, self)._run(
1693
            container___path,
1694
            path_is_optional=False)
1695
        (read, write) = self.format_permission_dict(permissions)
1696
        self._run(read, write)
1697

    
1698

    
1699
@command(pithos_cmds)
1700
class file_permissions_delete(_file_container_command, _optional_output_cmd):
1701
    """Delete all permissions set on object
1702
    To modify permissions, use /file permissions set
1703
    """
1704

    
1705
    @errors.generic.all
1706
    @errors.pithos.connection
1707
    @errors.pithos.container
1708
    @errors.pithos.object_path
1709
    def _run(self):
1710
        self._optional_output(self.client.del_object_sharing(self.path))
1711

    
1712
    def main(self, container___path):
1713
        super(self.__class__, self)._run(
1714
            container___path,
1715
            path_is_optional=False)
1716
        self._run()
1717

    
1718

    
1719
@command(pithos_cmds)
1720
class file_info(_file_container_command, _optional_json):
1721
    """Get detailed information for user account, containers or objects
1722
    to get account info:    /file info
1723
    to get container info:  /file info <container>
1724
    to get object info:     /file info <container>:<path>
1725
    """
1726

    
1727
    arguments = dict(
1728
        object_version=ValueArgument(
1729
            'show specific version \ (applies only for objects)',
1730
            ('-O', '--object-version'))
1731
    )
1732

    
1733
    @errors.generic.all
1734
    @errors.pithos.connection
1735
    @errors.pithos.container
1736
    @errors.pithos.object_path
1737
    def _run(self):
1738
        if self.container is None:
1739
            r = self.client.get_account_info()
1740
        elif self.path is None:
1741
            r = self.client.get_container_info(self.container)
1742
        else:
1743
            r = self.client.get_object_info(
1744
                self.path,
1745
                version=self['object_version'])
1746
        self._print(r, print_dict)
1747

    
1748
    def main(self, container____path__=None):
1749
        super(self.__class__, self)._run(container____path__)
1750
        self._run()
1751

    
1752

    
1753
@command(pithos_cmds)
1754
class file_metadata(_pithos_init):
1755
    """Metadata are attached on objects. They are formed as key:value pairs.
1756
    They can have arbitary values.
1757
    """
1758

    
1759

    
1760
@command(pithos_cmds)
1761
class file_metadata_get(_file_container_command, _optional_json):
1762
    """Get metadata for account, containers or objects"""
1763

    
1764
    arguments = dict(
1765
        detail=FlagArgument('show detailed output', ('-l', '--details')),
1766
        until=DateArgument('show metadata until then', '--until'),
1767
        object_version=ValueArgument(
1768
            'show specific version \ (applies only for objects)',
1769
            ('-O', '--object-version'))
1770
    )
1771

    
1772
    @errors.generic.all
1773
    @errors.pithos.connection
1774
    @errors.pithos.container
1775
    @errors.pithos.object_path
1776
    def _run(self):
1777
        until = self['until']
1778
        r = None
1779
        if self.container is None:
1780
            if self['detail']:
1781
                r = self.client.get_account_info(until=until)
1782
            else:
1783
                r = self.client.get_account_meta(until=until)
1784
                r = pretty_keys(r, '-')
1785
        elif self.path is None:
1786
            if self['detail']:
1787
                r = self.client.get_container_info(until=until)
1788
            else:
1789
                cmeta = self.client.get_container_meta(until=until)
1790
                ometa = self.client.get_container_object_meta(until=until)
1791
                r = {}
1792
                if cmeta:
1793
                    r['container-meta'] = pretty_keys(cmeta, '-')
1794
                if ometa:
1795
                    r['object-meta'] = pretty_keys(ometa, '-')
1796
        else:
1797
            if self['detail']:
1798
                r = self.client.get_object_info(
1799
                    self.path,
1800
                    version=self['object_version'])
1801
            else:
1802
                r = self.client.get_object_meta(
1803
                    self.path,
1804
                    version=self['object_version'])
1805
                r = pretty_keys(pretty_keys(r, '-'))
1806
        if r:
1807
            self._print(r, print_dict)
1808

    
1809
    def main(self, container____path__=None):
1810
        super(self.__class__, self)._run(container____path__)
1811
        self._run()
1812

    
1813

    
1814
@command(pithos_cmds)
1815
class file_metadata_set(_file_container_command, _optional_output_cmd):
1816
    """Set a piece of metadata for account, container or object"""
1817

    
1818
    @errors.generic.all
1819
    @errors.pithos.connection
1820
    @errors.pithos.container
1821
    @errors.pithos.object_path
1822
    def _run(self, metakey, metaval):
1823
        if not self.container:
1824
            r = self.client.set_account_meta({metakey: metaval})
1825
        elif not self.path:
1826
            r = self.client.set_container_meta({metakey: metaval})
1827
        else:
1828
            r = self.client.set_object_meta(self.path, {metakey: metaval})
1829
        self._optional_output(r)
1830

    
1831
    def main(self, metakey, metaval, container____path__=None):
1832
        super(self.__class__, self)._run(container____path__)
1833
        self._run(metakey=metakey, metaval=metaval)
1834

    
1835

    
1836
@command(pithos_cmds)
1837
class file_metadata_delete(_file_container_command, _optional_output_cmd):
1838
    """Delete metadata with given key from account, container or object
1839
    - to get metadata of current account: /file metadata get
1840
    - to get metadata of a container:     /file metadata get <container>
1841
    - to get metadata of an object:       /file metadata get <container>:<path>
1842
    """
1843

    
1844
    @errors.generic.all
1845
    @errors.pithos.connection
1846
    @errors.pithos.container
1847
    @errors.pithos.object_path
1848
    def _run(self, metakey):
1849
        if self.container is None:
1850
            r = self.client.del_account_meta(metakey)
1851
        elif self.path is None:
1852
            r = self.client.del_container_meta(metakey)
1853
        else:
1854
            r = self.client.del_object_meta(self.path, metakey)
1855
        self._optional_output(r)
1856

    
1857
    def main(self, metakey, container____path__=None):
1858
        super(self.__class__, self)._run(container____path__)
1859
        self._run(metakey)
1860

    
1861

    
1862
@command(pithos_cmds)
1863
class file_quota(_file_account_command, _optional_json):
1864
    """Get account quota"""
1865

    
1866
    arguments = dict(
1867
        in_bytes=FlagArgument('Show result in bytes', ('-b', '--bytes'))
1868
    )
1869

    
1870
    @errors.generic.all
1871
    @errors.pithos.connection
1872
    def _run(self):
1873

    
1874
        def pretty_print(output):
1875
            if not self['in_bytes']:
1876
                for k in output:
1877
                    output[k] = format_size(output[k])
1878
            pretty_dict(output, '-')
1879

    
1880
        self._print(self.client.get_account_quota(), pretty_print)
1881

    
1882
    def main(self, custom_uuid=None):
1883
        super(self.__class__, self)._run(custom_account=custom_uuid)
1884
        self._run()
1885

    
1886

    
1887
@command(pithos_cmds)
1888
class file_containerlimit(_pithos_init):
1889
    """Container size limit commands"""
1890

    
1891

    
1892
@command(pithos_cmds)
1893
class file_containerlimit_get(_file_container_command, _optional_json):
1894
    """Get container size limit"""
1895

    
1896
    arguments = dict(
1897
        in_bytes=FlagArgument('Show result in bytes', ('-b', '--bytes'))
1898
    )
1899

    
1900
    @errors.generic.all
1901
    @errors.pithos.container
1902
    def _run(self):
1903

    
1904
        def pretty_print(output):
1905
            if not self['in_bytes']:
1906
                for k, v in output.items():
1907
                    output[k] = 'unlimited' if '0' == v else format_size(v)
1908
            pretty_dict(output, '-')
1909

    
1910
        self._print(
1911
            self.client.get_container_limit(self.container), pretty_print)
1912

    
1913
    def main(self, container=None):
1914
        super(self.__class__, self)._run()
1915
        self.container = container
1916
        self._run()
1917

    
1918

    
1919
@command(pithos_cmds)
1920
class file_containerlimit_set(_file_account_command, _optional_output_cmd):
1921
    """Set new storage limit for a container
1922
    By default, the limit is set in bytes
1923
    Users may specify a different unit, e.g:
1924
    /file containerlimit set 2.3GB mycontainer
1925
    Valid units: B, KiB (1024 B), KB (1000 B), MiB, MB, GiB, GB, TiB, TB
1926
    To set container limit to "unlimited", use 0
1927
    """
1928

    
1929
    @errors.generic.all
1930
    def _calculate_limit(self, user_input):
1931
        limit = 0
1932
        try:
1933
            limit = int(user_input)
1934
        except ValueError:
1935
            index = 0
1936
            digits = [str(num) for num in range(0, 10)] + ['.']
1937
            while user_input[index] in digits:
1938
                index += 1
1939
            limit = user_input[:index]
1940
            format = user_input[index:]
1941
            try:
1942
                return to_bytes(limit, format)
1943
            except Exception as qe:
1944
                msg = 'Failed to convert %s to bytes' % user_input,
1945
                raiseCLIError(qe, msg, details=[
1946
                    'Syntax: containerlimit set <limit>[format] [container]',
1947
                    'e.g.: containerlimit set 2.3GB mycontainer',
1948
                    'Valid formats:',
1949
                    '(*1024): B, KiB, MiB, GiB, TiB',
1950
                    '(*1000): B, KB, MB, GB, TB'])
1951
        return limit
1952

    
1953
    @errors.generic.all
1954
    @errors.pithos.connection
1955
    @errors.pithos.container
1956
    def _run(self, limit):
1957
        if self.container:
1958
            self.client.container = self.container
1959
        self._optional_output(self.client.set_container_limit(limit))
1960

    
1961
    def main(self, limit, container=None):
1962
        super(self.__class__, self)._run()
1963
        limit = self._calculate_limit(limit)
1964
        self.container = container
1965
        self._run(limit)
1966

    
1967

    
1968
@command(pithos_cmds)
1969
class file_versioning(_pithos_init):
1970
    """Manage the versioning scheme of current pithos user account"""
1971

    
1972

    
1973
@command(pithos_cmds)
1974
class file_versioning_get(_file_account_command, _optional_json):
1975
    """Get  versioning for account or container"""
1976

    
1977
    @errors.generic.all
1978
    @errors.pithos.connection
1979
    @errors.pithos.container
1980
    def _run(self):
1981
        #if self.container:
1982
        #    r = self.client.get_container_versioning(self.container)
1983
        #else:
1984
        #    r = self.client.get_account_versioning()
1985
        self._print(
1986
            self.client.get_container_versioning(self.container) if (
1987
                self.container) else self.client.get_account_versioning(),
1988
            print_dict)
1989

    
1990
    def main(self, container=None):
1991
        super(self.__class__, self)._run()
1992
        self.container = container
1993
        self._run()
1994

    
1995

    
1996
@command(pithos_cmds)
1997
class file_versioning_set(_file_account_command, _optional_output_cmd):
1998
    """Set versioning mode (auto, none) for account or container"""
1999

    
2000
    def _check_versioning(self, versioning):
2001
        if versioning and versioning.lower() in ('auto', 'none'):
2002
            return versioning.lower()
2003
        raiseCLIError('Invalid versioning %s' % versioning, details=[
2004
            'Versioning can be auto or none'])
2005

    
2006
    @errors.generic.all
2007
    @errors.pithos.connection
2008
    @errors.pithos.container
2009
    def _run(self, versioning):
2010
        if self.container:
2011
            self.client.container = self.container
2012
            r = self.client.set_container_versioning(versioning)
2013
        else:
2014
            r = self.client.set_account_versioning(versioning)
2015
        self._optional_output(r)
2016

    
2017
    def main(self, versioning, container=None):
2018
        super(self.__class__, self)._run()
2019
        self._run(self._check_versioning(versioning))
2020

    
2021

    
2022
@command(pithos_cmds)
2023
class file_group(_pithos_init):
2024
    """Manage access groups and group members"""
2025

    
2026

    
2027
@command(pithos_cmds)
2028
class file_group_list(_file_account_command, _optional_json):
2029
    """list all groups and group members"""
2030

    
2031
    @errors.generic.all
2032
    @errors.pithos.connection
2033
    def _run(self):
2034
        self._print(self.client.get_account_group(), pretty_dict, delim='-')
2035

    
2036
    def main(self):
2037
        super(self.__class__, self)._run()
2038
        self._run()
2039

    
2040

    
2041
@command(pithos_cmds)
2042
class file_group_set(_file_account_command, _optional_output_cmd):
2043
    """Set a user group"""
2044

    
2045
    @errors.generic.all
2046
    @errors.pithos.connection
2047
    def _run(self, groupname, *users):
2048
        self._optional_output(self.client.set_account_group(groupname, users))
2049

    
2050
    def main(self, groupname, *users):
2051
        super(self.__class__, self)._run()
2052
        if users:
2053
            self._run(groupname, *users)
2054
        else:
2055
            raiseCLIError('No users to add in group %s' % groupname)
2056

    
2057

    
2058
@command(pithos_cmds)
2059
class file_group_delete(_file_account_command, _optional_output_cmd):
2060
    """Delete a user group"""
2061

    
2062
    @errors.generic.all
2063
    @errors.pithos.connection
2064
    def _run(self, groupname):
2065
        self._optional_output(self.client.del_account_group(groupname))
2066

    
2067
    def main(self, groupname):
2068
        super(self.__class__, self)._run()
2069
        self._run(groupname)
2070

    
2071

    
2072
@command(pithos_cmds)
2073
class file_sharers(_file_account_command, _optional_json):
2074
    """List the accounts that share objects with current user"""
2075

    
2076
    arguments = dict(
2077
        detail=FlagArgument('show detailed output', ('-l', '--details')),
2078
        marker=ValueArgument('show output greater then marker', '--marker')
2079
    )
2080

    
2081
    @errors.generic.all
2082
    @errors.pithos.connection
2083
    def _run(self):
2084
        accounts = self.client.get_sharing_accounts(marker=self['marker'])
2085
        if self['json_output'] or self['detail']:
2086
            self._print(accounts)
2087
        else:
2088
            self._print([acc['name'] for acc in accounts])
2089

    
2090
    def main(self):
2091
        super(self.__class__, self)._run()
2092
        self._run()
2093

    
2094

    
2095
def version_print(versions):
2096
    print_items([dict(id=vitem[0], created=strftime(
2097
        '%d-%m-%Y %H:%M:%S',
2098
        localtime(float(vitem[1])))) for vitem in versions])
2099

    
2100

    
2101
@command(pithos_cmds)
2102
class file_versions(_file_container_command, _optional_json):
2103
    """Get the list of object versions
2104
    Deleted objects may still have versions that can be used to restore it and
2105
    get information about its previous state.
2106
    The version number can be used in a number of other commands, like info,
2107
    copy, move, meta. See these commands for more information, e.g.
2108
    /file info -h
2109
    """
2110

    
2111
    @errors.generic.all
2112
    @errors.pithos.connection
2113
    @errors.pithos.container
2114
    @errors.pithos.object_path
2115
    def _run(self):
2116
        self._print(
2117
            self.client.get_object_versionlist(self.path), version_print)
2118

    
2119
    def main(self, container___path):
2120
        super(file_versions, self)._run(
2121
            container___path,
2122
            path_is_optional=False)
2123
        self._run()