Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (77.6 kB)

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

    
34
from sys import stdout
35
from time import localtime, strftime
36
from 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 addLogSettings, DontRaiseKeyError
49
from kamaki.cli.commands import _optional_output_cmd, _optional_json
50
from kamaki.clients.pithos import PithosClient, ClientError
51
from kamaki.clients.astakos import AstakosClient
52

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

    
56

    
57
# Argument functionality
58

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

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

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

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

    
79

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

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

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

    
121

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

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

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

    
142

    
143
# Command specs
144

    
145

    
146
class _pithos_init(_command_init):
147
    """Initialize a pithos+ kamaki client"""
148

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

    
154
    @DontRaiseKeyError
155
    def _custom_container(self):
156
        return self.config.get_remote(self.cloud, 'pithos_container')
157

    
158
    @DontRaiseKeyError
159
    def _custom_uuid(self):
160
        return self.config.get_remote(self.cloud, 'pithos_uuid')
161

    
162
    def _set_account(self):
163
        self.account = self._custom_uuid()
164
        if self.account:
165
            return
166
        if getattr(self, 'auth_base', False):
167
            self.account = self.auth_base.user_term('id', self.token)
168
        else:
169
            astakos_url = self._custom_url('astakos')
170
            astakos_token = self._custom_token('astakos') or self.token
171
            if not astakos_url:
172
                raise CLIBaseUrlError(service='astakos')
173
            astakos = AstakosClient(astakos_url, astakos_token)
174
            self.account = astakos.user_term('id')
175

    
176
    @errors.generic.all
177
    @addLogSettings
178
    def _run(self):
179
        self.base_url = None
180
        if getattr(self, 'cloud', None):
181
            self.base_url = self._custom_url('pithos')
182
        else:
183
            self.cloud = 'default'
184
        self.token = self._custom_token('pithos')
185
        self.container = self._custom_container()
186

    
187
        if getattr(self, 'auth_base', False):
188
            self.token = self.token or self.auth_base.token
189
            if not self.base_url:
190
                pithos_endpoints = self.auth_base.get_service_endpoints(
191
                    self._custom_type('pithos') or 'object-store',
192
                    self._custom_version('pithos') or '')
193
                self.base_url = pithos_endpoints['publicURL']
194
        elif not self.base_url:
195
            raise CLIBaseUrlError(service='pithos')
196

    
197
        self._set_account()
198
        self.client = PithosClient(
199
            base_url=self.base_url,
200
            token=self.token,
201
            account=self.account,
202
            container=self.container)
203

    
204
    def main(self):
205
        self._run()
206

    
207

    
208
class _file_account_command(_pithos_init):
209
    """Base class for account level storage commands"""
210

    
211
    def __init__(self, arguments={}, auth_base=None, cloud=None):
212
        super(_file_account_command, self).__init__(
213
            arguments, auth_base, cloud)
214
        self['account'] = ValueArgument(
215
            'Set user account (not permanent)', ('-A', '--account'))
216

    
217
    def _run(self, custom_account=None):
218
        super(_file_account_command, self)._run()
219
        if custom_account:
220
            self.client.account = custom_account
221
        elif self['account']:
222
            self.client.account = self['account']
223

    
224
    @errors.generic.all
225
    def main(self):
226
        self._run()
227

    
228

    
229
class _file_container_command(_file_account_command):
230
    """Base class for container level storage commands"""
231

    
232
    container = None
233
    path = None
234

    
235
    def __init__(self, arguments={}, auth_base=None, cloud=None):
236
        super(_file_container_command, self).__init__(
237
            arguments, auth_base, cloud)
238
        self['container'] = ValueArgument(
239
            'Set container to work with (temporary)', ('-C', '--container'))
240

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

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

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

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

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

    
317

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

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

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

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

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

    
446
    def main(self, container____path__=None):
447
        super(self.__class__, self)._run(container____path__)
448
        self._run()
449

    
450

    
451
@command(pithos_cmds)
452
class file_mkdir(_file_container_command, _optional_output_cmd):
453
    """Create a directory"""
454

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

    
462
    @errors.generic.all
463
    @errors.pithos.connection
464
    @errors.pithos.container
465
    def _run(self):
466
        self._optional_output(self.client.create_directory(self.path))
467

    
468
    def main(self, container___directory):
469
        super(self.__class__, self)._run(
470
            container___directory,
471
            path_is_optional=False)
472
        self._run()
473

    
474

    
475
@command(pithos_cmds)
476
class file_touch(_file_container_command, _optional_output_cmd):
477
    """Create an empty object (file)
478
    If object exists, this command will reset it to 0 length
479
    """
480

    
481
    arguments = dict(
482
        content_type=ValueArgument(
483
            'Set content type (default: application/octet-stream)',
484
            '--content-type',
485
            default='application/octet-stream')
486
    )
487

    
488
    @errors.generic.all
489
    @errors.pithos.connection
490
    @errors.pithos.container
491
    def _run(self):
492
        self._optional_output(
493
            self.client.create_object(self.path, self['content_type']))
494

    
495
    def main(self, container___path):
496
        super(file_touch, self)._run(
497
            container___path,
498
            path_is_optional=False)
499
        self._run()
500

    
501

    
502
@command(pithos_cmds)
503
class file_create(_file_container_command, _optional_output_cmd):
504
    """Create a container"""
505

    
506
    arguments = dict(
507
        versioning=ValueArgument(
508
            'set container versioning (auto/none)', '--versioning'),
509
        limit=IntArgument('set default container limit', '--limit'),
510
        meta=KeyValueArgument(
511
            'set container metadata (can be repeated)', '--meta')
512
    )
513

    
514
    @errors.generic.all
515
    @errors.pithos.connection
516
    @errors.pithos.container
517
    def _run(self, container):
518
        self._optional_output(self.client.create_container(
519
            container=container,
520
            sizelimit=self['limit'],
521
            versioning=self['versioning'],
522
            metadata=self['meta']))
523

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

    
532

    
533
class _source_destination_command(_file_container_command):
534

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

    
546
    def __init__(self, arguments={}, auth_base=None, cloud=None):
547
        self.arguments.update(arguments)
548
        super(_source_destination_command, self).__init__(
549
            self.arguments, auth_base, cloud)
550

    
551
    def _run(self, source_container___path, path_is_optional=False):
552
        super(_source_destination_command, self)._run(
553
            source_container___path,
554
            path_is_optional)
555
        self.dst_client = PithosClient(
556
            base_url=self.client.base_url,
557
            token=self.client.token,
558
            account=self['destination_account'] or self.client.account)
559

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

    
583
    def _get_all(self, prefix):
584
        return self.client.container_get(prefix=prefix).json
585

    
586
    def _get_src_objects(self, src_path, source_version=None):
587
        """Get a list of the source objects to be called
588

589
        :param src_path: (str) source path
590

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

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

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

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

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

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

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

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

    
674

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

    
692
    arguments = dict(
693
        destination_account=ValueArgument(
694
            'Account to copy to', ('-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', '--content-type'),
701
        recursive=FlagArgument(
702
            'copy directory and contents', ('-R', '--recursive')),
703
        prefix=FlagArgument(
704
            'Match objects prefixed with src path (feels like src_path*)',
705
            '--with-prefix',
706
            default=''),
707
        suffix=ValueArgument(
708
            'Suffix of source objects (feels like *suffix)', '--with-suffix',
709
            default=''),
710
        add_prefix=ValueArgument('Prefix targets', '--add-prefix', default=''),
711
        add_suffix=ValueArgument('Suffix targets', '--add-suffix', default=''),
712
        prefix_replace=ValueArgument(
713
            'Prefix of src to replace with dst path + add_prefix, if matched',
714
            '--prefix-to-replace',
715
            default=''),
716
        suffix_replace=ValueArgument(
717
            'Suffix of src to replace with add_suffix, if matched',
718
            '--suffix-to-replace',
719
            default=''),
720
        source_version=ValueArgument(
721
            'copy specific version', ('-S', '--source-version'))
722
    )
723

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

    
749
    def main(
750
            self, source_container___path,
751
            destination_container___path=None):
752
        super(file_copy, self)._run(
753
            source_container___path,
754
            path_is_optional=False)
755
        (dst_cont, dst_path) = self._dest_container_path(
756
            destination_container___path)
757
        self.dst_client.container = dst_cont or self.container
758
        self._run(dst_path=dst_path or '')
759

    
760

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

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

    
808
    @errors.generic.all
809
    @errors.pithos.connection
810
    @errors.pithos.container
811
    def _run(self, dst_path):
812
        no_source_object = True
813
        src_account = self.client.account if (
814
            self['destination_account']) else None
815
        for src_obj, dst_obj in self.src_dst_pairs(dst_path):
816
            no_source_object = False
817
            r = self.dst_client.move_object(
818
                src_container=self.container,
819
                src_object=src_obj,
820
                dst_container=self.dst_client.container,
821
                dst_object=dst_obj,
822
                source_account=src_account,
823
                public=self['public'],
824
                content_type=self['content_type'])
825
        if no_source_object:
826
            raiseCLIError('No object %s in container %s' % (
827
                self.path,
828
                self.container))
829
        self._optional_output(r)
830

    
831
    def main(
832
            self, source_container___path,
833
            destination_container___path=None):
834
        super(self.__class__, self)._run(
835
            source_container___path,
836
            path_is_optional=False)
837
        (dst_cont, dst_path) = self._dest_container_path(
838
            destination_container___path)
839
        (dst_cont, dst_path) = self._dest_container_path(
840
            destination_container___path)
841
        self.dst_client.container = dst_cont or self.container
842
        self._run(dst_path=dst_path or '')
843

    
844

    
845
@command(pithos_cmds)
846
class file_append(_file_container_command, _optional_output_cmd):
847
    """Append local file to (existing) remote object
848
    The remote object should exist.
849
    If the remote object is a directory, it is transformed into a file.
850
    In the later case, objects under the directory remain intact.
851
    """
852

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

    
860
    @errors.generic.all
861
    @errors.pithos.connection
862
    @errors.pithos.container
863
    @errors.pithos.object_path
864
    def _run(self, local_path):
865
        (progress_bar, upload_cb) = self._safe_progress_bar('Appending')
866
        try:
867
            f = open(local_path, 'rb')
868
            self._optional_output(
869
                self.client.append_object(self.path, f, upload_cb))
870
        except Exception:
871
            self._safe_progress_bar_finish(progress_bar)
872
            raise
873
        finally:
874
            self._safe_progress_bar_finish(progress_bar)
875

    
876
    def main(self, local_path, container___path):
877
        super(self.__class__, self)._run(
878
            container___path, path_is_optional=False)
879
        self._run(local_path)
880

    
881

    
882
@command(pithos_cmds)
883
class file_truncate(_file_container_command, _optional_output_cmd):
884
    """Truncate remote file up to a size (default is 0)"""
885

    
886
    @errors.generic.all
887
    @errors.pithos.connection
888
    @errors.pithos.container
889
    @errors.pithos.object_path
890
    @errors.pithos.object_size
891
    def _run(self, size=0):
892
        self._optional_output(self.client.truncate_object(self.path, size))
893

    
894
    def main(self, container___path, size=0):
895
        super(self.__class__, self)._run(container___path)
896
        self._run(size=size)
897

    
898

    
899
@command(pithos_cmds)
900
class file_overwrite(_file_container_command, _optional_output_cmd):
901
    """Overwrite part (from start to end) of a remote file
902
    overwrite local-path container 10 20
903
    .   will overwrite bytes from 10 to 20 of a remote file with the same name
904
    .   as local-path basename
905
    overwrite local-path container:path 10 20
906
    .   will overwrite as above, but the remote file is named path
907
    """
908

    
909
    arguments = dict(
910
        progress_bar=ProgressBarArgument(
911
            'do not show progress bar',
912
            ('-N', '--no-progress-bar'),
913
            default=False)
914
    )
915

    
916
    def _open_file(self, local_path, start):
917
        f = open(path.abspath(local_path), 'rb')
918
        f.seek(0, 2)
919
        f_size = f.tell()
920
        f.seek(start, 0)
921
        return (f, f_size)
922

    
923
    @errors.generic.all
924
    @errors.pithos.connection
925
    @errors.pithos.container
926
    @errors.pithos.object_path
927
    @errors.pithos.object_size
928
    def _run(self, local_path, start, end):
929
        (start, end) = (int(start), int(end))
930
        (f, f_size) = self._open_file(local_path, start)
931
        (progress_bar, upload_cb) = self._safe_progress_bar(
932
            'Overwrite %s bytes' % (end - start))
933
        try:
934
            self._optional_output(self.client.overwrite_object(
935
                obj=self.path,
936
                start=start,
937
                end=end,
938
                source_file=f,
939
                upload_cb=upload_cb))
940
        finally:
941
            self._safe_progress_bar_finish(progress_bar)
942

    
943
    def main(self, local_path, container___path, start, end):
944
        super(self.__class__, self)._run(
945
            container___path, path_is_optional=None)
946
        self.path = self.path or path.basename(local_path)
947
        self._run(local_path=local_path, start=start, end=end)
948

    
949

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

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

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

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

    
998

    
999
@command(pithos_cmds)
1000
class file_upload(_file_container_command, _optional_output_cmd):
1001
    """Upload a file"""
1002

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

    
1030
    def _check_container_limit(self, path):
1031
        cl_dict = self.client.get_container_limit()
1032
        container_limit = int(cl_dict['x-container-policy-quota'])
1033
        r = self.client.container_get()
1034
        used_bytes = sum(int(o['bytes']) for o in r.json)
1035
        path_size = get_path_size(path)
1036
        if container_limit and path_size > (container_limit - used_bytes):
1037
            raiseCLIError(
1038
                'Container(%s) (limit(%s) - used(%s)) < size(%s) of %s' % (
1039
                    self.client.container,
1040
                    format_size(container_limit),
1041
                    format_size(used_bytes),
1042
                    format_size(path_size),
1043
                    path),
1044
                importance=1, details=[
1045
                    'Check accound limit: /file quota',
1046
                    'Check container limit:',
1047
                    '\t/file containerlimit get %s' % self.client.container,
1048
                    'Increase container limit:',
1049
                    '\t/file containerlimit set <new limit> %s' % (
1050
                        self.client.container)])
1051

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

    
1118
    @errors.generic.all
1119
    @errors.pithos.connection
1120
    @errors.pithos.container
1121
    @errors.pithos.object_path
1122
    @errors.pithos.local_path
1123
    def _run(self, local_path, remote_path):
1124
        poolsize = self['poolsize']
1125
        if poolsize > 0:
1126
            self.client.MAX_THREADS = int(poolsize)
1127
        params = dict(
1128
            content_encoding=self['content_encoding'],
1129
            content_type=self['content_type'],
1130
            content_disposition=self['content_disposition'],
1131
            sharing=self['sharing'],
1132
            public=self['public'])
1133
        uploaded = []
1134
        container_info_cache = dict()
1135
        for f, rpath in self._path_pairs(local_path, remote_path):
1136
            print('%s --> %s:%s' % (f.name, self.client.container, rpath))
1137
            if self['unchunked']:
1138
                r = self.client.upload_object_unchunked(
1139
                    rpath, f,
1140
                    etag=self['etag'], withHashFile=self['use_hashes'],
1141
                    **params)
1142
                if self['with_output'] or self['json_output']:
1143
                    r['name'] = '%s: %s' % (self.client.container, rpath)
1144
                    uploaded.append(r)
1145
            else:
1146
                try:
1147
                    (progress_bar, upload_cb) = self._safe_progress_bar(
1148
                        'Uploading %s' % f.name.split(path.sep)[-1])
1149
                    if progress_bar:
1150
                        hash_bar = progress_bar.clone()
1151
                        hash_cb = hash_bar.get_generator(
1152
                            'Calculating block hashes')
1153
                    else:
1154
                        hash_cb = None
1155
                    r = self.client.upload_object(
1156
                        rpath, f,
1157
                        hash_cb=hash_cb,
1158
                        upload_cb=upload_cb,
1159
                        container_info_cache=container_info_cache,
1160
                        **params)
1161
                    if self['with_output'] or self['json_output']:
1162
                        r['name'] = '%s: %s' % (self.client.container, rpath)
1163
                        uploaded.append(r)
1164
                except Exception:
1165
                    self._safe_progress_bar_finish(progress_bar)
1166
                    raise
1167
                finally:
1168
                    self._safe_progress_bar_finish(progress_bar)
1169
        self._optional_output(uploaded)
1170
        print('Upload completed')
1171

    
1172
    def main(self, local_path, container____path__=None):
1173
        super(self.__class__, self)._run(container____path__)
1174
        remote_path = self.path or path.basename(local_path)
1175
        self._run(local_path=local_path, remote_path=remote_path)
1176

    
1177

    
1178
@command(pithos_cmds)
1179
class file_cat(_file_container_command):
1180
    """Print remote file contents to console"""
1181

    
1182
    arguments = dict(
1183
        range=RangeArgument('show range of data', '--range'),
1184
        if_match=ValueArgument('show output if ETags match', '--if-match'),
1185
        if_none_match=ValueArgument(
1186
            'show output if ETags match', '--if-none-match'),
1187
        if_modified_since=DateArgument(
1188
            'show output modified since then', '--if-modified-since'),
1189
        if_unmodified_since=DateArgument(
1190
            'show output unmodified since then', '--if-unmodified-since'),
1191
        object_version=ValueArgument(
1192
            'get the specific version', ('-O', '--object-version'))
1193
    )
1194

    
1195
    @errors.generic.all
1196
    @errors.pithos.connection
1197
    @errors.pithos.container
1198
    @errors.pithos.object_path
1199
    def _run(self):
1200
        self.client.download_object(
1201
            self.path,
1202
            stdout,
1203
            range_str=self['range'],
1204
            version=self['object_version'],
1205
            if_match=self['if_match'],
1206
            if_none_match=self['if_none_match'],
1207
            if_modified_since=self['if_modified_since'],
1208
            if_unmodified_since=self['if_unmodified_since'])
1209

    
1210
    def main(self, container___path):
1211
        super(self.__class__, self)._run(
1212
            container___path, path_is_optional=False)
1213
        self._run()
1214

    
1215

    
1216
@command(pithos_cmds)
1217
class file_download(_file_container_command):
1218
    """Download remote object as local file
1219
    If local destination is a directory:
1220
    *   download <container>:<path> <local dir> -R
1221
    will download all files on <container> prefixed as <path>,
1222
    to <local dir>/<full path> (or <local dir>\<full path> in windows)
1223
    *   download <container>:<path> <local dir> --exact-match
1224
    will download only one file, exactly matching <path>
1225
    ATTENTION: to download cont:dir1/dir2/file there must exist objects
1226
    cont:dir1 and cont:dir1/dir2 of type application/directory
1227
    To create directory objects, use /file mkdir
1228
    """
1229

    
1230
    arguments = dict(
1231
        resume=FlagArgument('Resume instead of overwrite', ('-r', '--resume')),
1232
        range=RangeArgument('show range of data', '--range'),
1233
        if_match=ValueArgument('show output if ETags match', '--if-match'),
1234
        if_none_match=ValueArgument(
1235
            'show output if ETags match', '--if-none-match'),
1236
        if_modified_since=DateArgument(
1237
            'show output modified since then', '--if-modified-since'),
1238
        if_unmodified_since=DateArgument(
1239
            'show output unmodified since then', '--if-unmodified-since'),
1240
        object_version=ValueArgument(
1241
            'get the specific version', ('-O', '--object-version')),
1242
        poolsize=IntArgument('set pool size', '--with-pool-size'),
1243
        progress_bar=ProgressBarArgument(
1244
            'do not show progress bar',
1245
            ('-N', '--no-progress-bar'),
1246
            default=False),
1247
        recursive=FlagArgument(
1248
            'Download a remote path and all its contents',
1249
            ('-R', '--recursive'))
1250
    )
1251

    
1252
    def _outputs(self, local_path):
1253
        """:returns: (local_file, remote_path)"""
1254
        remotes = []
1255
        if self['recursive']:
1256
            r = self.client.container_get(
1257
                prefix=self.path or '/',
1258
                if_modified_since=self['if_modified_since'],
1259
                if_unmodified_since=self['if_unmodified_since'])
1260
            dirlist = dict()
1261
            for remote in r.json:
1262
                rname = remote['name'].strip('/')
1263
                tmppath = ''
1264
                for newdir in rname.strip('/').split('/')[:-1]:
1265
                    tmppath = '/'.join([tmppath, newdir])
1266
                    dirlist.update({tmppath.strip('/'): True})
1267
                remotes.append((rname, file_download._is_dir(remote)))
1268
            dir_remotes = [r[0] for r in remotes if r[1]]
1269
            if not set(dirlist).issubset(dir_remotes):
1270
                badguys = [bg.strip('/') for bg in set(
1271
                    dirlist).difference(dir_remotes)]
1272
                raiseCLIError(
1273
                    'Some remote paths contain non existing directories',
1274
                    details=['Missing remote directories:'] + badguys)
1275
        elif self.path:
1276
            r = self.client.get_object_info(
1277
                self.path,
1278
                version=self['object_version'])
1279
            if file_download._is_dir(r):
1280
                raiseCLIError(
1281
                    'Illegal download: Remote object %s is a directory' % (
1282
                        self.path),
1283
                    details=['To download a directory, try --recursive or -R'])
1284
            if '/' in self.path.strip('/') and not local_path:
1285
                raiseCLIError(
1286
                    'Illegal download: remote object %s contains "/"' % (
1287
                        self.path),
1288
                    details=[
1289
                        'To download an object containing "/" characters',
1290
                        'either create the remote directories or',
1291
                        'specify a non-directory local path for this object'])
1292
            remotes = [(self.path, False)]
1293
        if not remotes:
1294
            if self.path:
1295
                raiseCLIError(
1296
                    'No matching path %s on container %s' % (
1297
                        self.path, self.container),
1298
                    details=[
1299
                        'To list the contents of %s, try:' % self.container,
1300
                        '   /file list %s' % self.container])
1301
            raiseCLIError(
1302
                'Illegal download of container %s' % self.container,
1303
                details=[
1304
                    'To download a whole container, try:',
1305
                    '   /file download --recursive <container>'])
1306

    
1307
        lprefix = path.abspath(local_path or path.curdir)
1308
        if path.isdir(lprefix):
1309
            for rpath, remote_is_dir in remotes:
1310
                lpath = path.sep.join([
1311
                    lprefix[:-1] if lprefix.endswith(path.sep) else lprefix,
1312
                    rpath.strip('/').replace('/', path.sep)])
1313
                if remote_is_dir:
1314
                    if path.exists(lpath) and path.isdir(lpath):
1315
                        continue
1316
                    makedirs(lpath)
1317
                elif path.exists(lpath):
1318
                    if not self['resume']:
1319
                        print('File %s exists, aborting...' % lpath)
1320
                        continue
1321
                    with open(lpath, 'rwb+') as f:
1322
                        yield (f, rpath)
1323
                else:
1324
                    with open(lpath, 'wb+') as f:
1325
                        yield (f, rpath)
1326
        elif path.exists(lprefix):
1327
            if len(remotes) > 1:
1328
                raiseCLIError(
1329
                    '%s remote objects cannot be merged in local file %s' % (
1330
                        len(remotes),
1331
                        local_path),
1332
                    details=[
1333
                        'To download multiple objects, local path should be',
1334
                        'a directory, or use download without a local path'])
1335
            (rpath, remote_is_dir) = remotes[0]
1336
            if remote_is_dir:
1337
                raiseCLIError(
1338
                    'Remote directory %s should not replace local file %s' % (
1339
                        rpath,
1340
                        local_path))
1341
            if self['resume']:
1342
                with open(lprefix, 'rwb+') as f:
1343
                    yield (f, rpath)
1344
            else:
1345
                raiseCLIError(
1346
                    'Local file %s already exist' % local_path,
1347
                    details=['Try --resume to overwrite it'])
1348
        else:
1349
            if len(remotes) > 1 or remotes[0][1]:
1350
                raiseCLIError(
1351
                    'Local directory %s does not exist' % local_path)
1352
            with open(lprefix, 'wb+') as f:
1353
                yield (f, remotes[0][0])
1354

    
1355
    @errors.generic.all
1356
    @errors.pithos.connection
1357
    @errors.pithos.container
1358
    @errors.pithos.object_path
1359
    @errors.pithos.local_path
1360
    def _run(self, local_path):
1361
        poolsize = self['poolsize']
1362
        if poolsize:
1363
            self.client.MAX_THREADS = int(poolsize)
1364
        progress_bar = None
1365
        try:
1366
            for f, rpath in self._outputs(local_path):
1367
                (
1368
                    progress_bar,
1369
                    download_cb) = self._safe_progress_bar(
1370
                        'Download %s' % rpath)
1371
                self.client.download_object(
1372
                    rpath, f,
1373
                    download_cb=download_cb,
1374
                    range_str=self['range'],
1375
                    version=self['object_version'],
1376
                    if_match=self['if_match'],
1377
                    resume=self['resume'],
1378
                    if_none_match=self['if_none_match'],
1379
                    if_modified_since=self['if_modified_since'],
1380
                    if_unmodified_since=self['if_unmodified_since'])
1381
        except KeyboardInterrupt:
1382
            from threading import activeCount, enumerate as activethreads
1383
            timeout = 0.5
1384
            while activeCount() > 1:
1385
                stdout.write('\nCancel %s threads: ' % (activeCount() - 1))
1386
                stdout.flush()
1387
                for thread in activethreads():
1388
                    try:
1389
                        thread.join(timeout)
1390
                        stdout.write('.' if thread.isAlive() else '*')
1391
                    except RuntimeError:
1392
                        continue
1393
                    finally:
1394
                        stdout.flush()
1395
                        timeout += 0.1
1396
            print('\nDownload canceled by user')
1397
            if local_path is not None:
1398
                print('to resume, re-run with --resume')
1399
        except Exception:
1400
            self._safe_progress_bar_finish(progress_bar)
1401
            raise
1402
        finally:
1403
            self._safe_progress_bar_finish(progress_bar)
1404

    
1405
    def main(self, container___path, local_path=None):
1406
        super(self.__class__, self)._run(container___path)
1407
        self._run(local_path=local_path)
1408

    
1409

    
1410
@command(pithos_cmds)
1411
class file_hashmap(_file_container_command, _optional_json):
1412
    """Get the hash-map of an object"""
1413

    
1414
    arguments = dict(
1415
        if_match=ValueArgument('show output if ETags match', '--if-match'),
1416
        if_none_match=ValueArgument(
1417
            'show output if ETags match', '--if-none-match'),
1418
        if_modified_since=DateArgument(
1419
            'show output modified since then', '--if-modified-since'),
1420
        if_unmodified_since=DateArgument(
1421
            'show output unmodified since then', '--if-unmodified-since'),
1422
        object_version=ValueArgument(
1423
            'get the specific version', ('-O', '--object-version'))
1424
    )
1425

    
1426
    @errors.generic.all
1427
    @errors.pithos.connection
1428
    @errors.pithos.container
1429
    @errors.pithos.object_path
1430
    def _run(self):
1431
        self._print(self.client.get_object_hashmap(
1432
            self.path,
1433
            version=self['object_version'],
1434
            if_match=self['if_match'],
1435
            if_none_match=self['if_none_match'],
1436
            if_modified_since=self['if_modified_since'],
1437
            if_unmodified_since=self['if_unmodified_since']), print_dict)
1438

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

    
1445

    
1446
@command(pithos_cmds)
1447
class file_delete(_file_container_command, _optional_output_cmd):
1448
    """Delete a container [or an object]
1449
    How to delete a non-empty container:
1450
    - empty the container:  /file delete -R <container>
1451
    - delete it:            /file delete <container>
1452
    .
1453
    Semantics of directory deletion:
1454
    .a preserve the contents: /file delete <container>:<directory>
1455
    .    objects of the form dir/filename can exist with a dir object
1456
    .b delete contents:       /file delete -R <container>:<directory>
1457
    .    all dir/* objects are affected, even if dir does not exist
1458
    .
1459
    To restore a deleted object OBJ in a container CONT:
1460
    - get object versions: /file versions CONT:OBJ
1461
    .   and choose the version to be restored
1462
    - restore the object:  /file copy --source-version=<version> CONT:OBJ OBJ
1463
    """
1464

    
1465
    arguments = dict(
1466
        until=DateArgument('remove history until that date', '--until'),
1467
        yes=FlagArgument('Do not prompt for permission', '--yes'),
1468
        recursive=FlagArgument(
1469
            'empty dir or container and delete (if dir)',
1470
            ('-R', '--recursive'))
1471
    )
1472

    
1473
    def __init__(self, arguments={}, auth_base=None, cloud=None):
1474
        super(self.__class__, self).__init__(arguments,  auth_base, cloud)
1475
        self['delimiter'] = DelimiterArgument(
1476
            self,
1477
            parsed_name='--delimiter',
1478
            help='delete objects prefixed with <object><delimiter>')
1479

    
1480
    @errors.generic.all
1481
    @errors.pithos.connection
1482
    @errors.pithos.container
1483
    @errors.pithos.object_path
1484
    def _run(self):
1485
        if self.path:
1486
            if self['yes'] or ask_user(
1487
                    'Delete %s:%s ?' % (self.container, self.path)):
1488
                self._optional_output(self.client.del_object(
1489
                    self.path,
1490
                    until=self['until'], delimiter=self['delimiter']))
1491
            else:
1492
                print('Aborted')
1493
        else:
1494
            if self['recursive']:
1495
                ask_msg = 'Delete container contents'
1496
            else:
1497
                ask_msg = 'Delete container'
1498
            if self['yes'] or ask_user('%s %s ?' % (ask_msg, self.container)):
1499
                self._optional_output(self.client.del_container(
1500
                    until=self['until'], delimiter=self['delimiter']))
1501
            else:
1502
                print('Aborted')
1503

    
1504
    def main(self, container____path__=None):
1505
        super(self.__class__, self)._run(container____path__)
1506
        self._run()
1507

    
1508

    
1509
@command(pithos_cmds)
1510
class file_purge(_file_container_command, _optional_output_cmd):
1511
    """Delete a container and release related data blocks
1512
    Non-empty containers can not purged.
1513
    To purge a container with content:
1514
    .   /file delete -R <container>
1515
    .      objects are deleted, but data blocks remain on server
1516
    .   /file purge <container>
1517
    .      container and data blocks are released and deleted
1518
    """
1519

    
1520
    arguments = dict(
1521
        yes=FlagArgument('Do not prompt for permission', '--yes'),
1522
        force=FlagArgument('purge even if not empty', ('-F', '--force'))
1523
    )
1524

    
1525
    @errors.generic.all
1526
    @errors.pithos.connection
1527
    @errors.pithos.container
1528
    def _run(self):
1529
        if self['yes'] or ask_user('Purge container %s?' % self.container):
1530
            try:
1531
                r = self.client.purge_container()
1532
            except ClientError as ce:
1533
                if ce.status in (409,):
1534
                    if self['force']:
1535
                        self.client.del_container(delimiter='/')
1536
                        r = self.client.purge_container()
1537
                    else:
1538
                        raiseCLIError(ce, details=['Try -F to force-purge'])
1539
                else:
1540
                    raise
1541
            self._optional_output(r)
1542
        else:
1543
            print('Aborted')
1544

    
1545
    def main(self, container=None):
1546
        super(self.__class__, self)._run(container)
1547
        if container and self.container != container:
1548
            raiseCLIError('Invalid container name %s' % container, details=[
1549
                'Did you mean "%s" ?' % self.container,
1550
                'Use --container for names containing :'])
1551
        self._run()
1552

    
1553

    
1554
@command(pithos_cmds)
1555
class file_publish(_file_container_command):
1556
    """Publish the object and print the public url"""
1557

    
1558
    @errors.generic.all
1559
    @errors.pithos.connection
1560
    @errors.pithos.container
1561
    @errors.pithos.object_path
1562
    def _run(self):
1563
        url = self.client.publish_object(self.path)
1564
        print(url)
1565

    
1566
    def main(self, container___path):
1567
        super(self.__class__, self)._run(
1568
            container___path, path_is_optional=False)
1569
        self._run()
1570

    
1571

    
1572
@command(pithos_cmds)
1573
class file_unpublish(_file_container_command, _optional_output_cmd):
1574
    """Unpublish an object"""
1575

    
1576
    @errors.generic.all
1577
    @errors.pithos.connection
1578
    @errors.pithos.container
1579
    @errors.pithos.object_path
1580
    def _run(self):
1581
            self._optional_output(self.client.unpublish_object(self.path))
1582

    
1583
    def main(self, container___path):
1584
        super(self.__class__, self)._run(
1585
            container___path, path_is_optional=False)
1586
        self._run()
1587

    
1588

    
1589
@command(pithos_cmds)
1590
class file_permissions(_pithos_init):
1591
    """Manage user and group accessibility for objects
1592
    Permissions are lists of users and user groups. There are read and write
1593
    permissions. Users and groups with write permission have also read
1594
    permission.
1595
    """
1596

    
1597

    
1598
def print_permissions(permissions_dict):
1599
    expected_keys = ('read', 'write')
1600
    if set(permissions_dict).issubset(expected_keys):
1601
        print_dict(permissions_dict)
1602
    else:
1603
        invalid_keys = set(permissions_dict.keys()).difference(expected_keys)
1604
        raiseCLIError(
1605
            'Illegal permission keys: %s' % ', '.join(invalid_keys),
1606
            importance=1, details=[
1607
                'Valid permission types: %s' % ' '.join(expected_keys)])
1608

    
1609

    
1610
@command(pithos_cmds)
1611
class file_permissions_get(_file_container_command, _optional_json):
1612
    """Get read and write permissions of an object"""
1613

    
1614
    @errors.generic.all
1615
    @errors.pithos.connection
1616
    @errors.pithos.container
1617
    @errors.pithos.object_path
1618
    def _run(self):
1619
        self._print(
1620
            self.client.get_object_sharing(self.path), print_permissions)
1621

    
1622
    def main(self, container___path):
1623
        super(self.__class__, self)._run(
1624
            container___path, path_is_optional=False)
1625
        self._run()
1626

    
1627

    
1628
@command(pithos_cmds)
1629
class file_permissions_set(_file_container_command, _optional_output_cmd):
1630
    """Set permissions for an object
1631
    New permissions overwrite existing permissions.
1632
    Permission format:
1633
    -   read=<username>[,usergroup[,...]]
1634
    -   write=<username>[,usegroup[,...]]
1635
    E.g. to give read permissions for file F to users A and B and write for C:
1636
    .       /file permissions set F read=A,B write=C
1637
    """
1638

    
1639
    @errors.generic.all
1640
    def format_permission_dict(self, permissions):
1641
        read = False
1642
        write = False
1643
        for perms in permissions:
1644
            splstr = perms.split('=')
1645
            if 'read' == splstr[0]:
1646
                read = [ug.strip() for ug in splstr[1].split(',')]
1647
            elif 'write' == splstr[0]:
1648
                write = [ug.strip() for ug in splstr[1].split(',')]
1649
            else:
1650
                msg = 'Usage:\tread=<groups,users> write=<groups,users>'
1651
                raiseCLIError(None, msg)
1652
        return (read, write)
1653

    
1654
    @errors.generic.all
1655
    @errors.pithos.connection
1656
    @errors.pithos.container
1657
    @errors.pithos.object_path
1658
    def _run(self, read, write):
1659
        self._optional_output(self.client.set_object_sharing(
1660
            self.path, read_permission=read, write_permission=write))
1661

    
1662
    def main(self, container___path, *permissions):
1663
        super(self.__class__, self)._run(
1664
            container___path, path_is_optional=False)
1665
        read, write = self.format_permission_dict(permissions)
1666
        self._run(read, write)
1667

    
1668

    
1669
@command(pithos_cmds)
1670
class file_permissions_delete(_file_container_command, _optional_output_cmd):
1671
    """Delete all permissions set on object
1672
    To modify permissions, use /file permissions set
1673
    """
1674

    
1675
    @errors.generic.all
1676
    @errors.pithos.connection
1677
    @errors.pithos.container
1678
    @errors.pithos.object_path
1679
    def _run(self):
1680
        self._optional_output(self.client.del_object_sharing(self.path))
1681

    
1682
    def main(self, container___path):
1683
        super(self.__class__, self)._run(
1684
            container___path, path_is_optional=False)
1685
        self._run()
1686

    
1687

    
1688
@command(pithos_cmds)
1689
class file_info(_file_container_command, _optional_json):
1690
    """Get detailed information for user account, containers or objects
1691
    to get account info:    /file info
1692
    to get container info:  /file info <container>
1693
    to get object info:     /file info <container>:<path>
1694
    """
1695

    
1696
    arguments = dict(
1697
        object_version=ValueArgument(
1698
            'show specific version \ (applies only for objects)',
1699
            ('-O', '--object-version'))
1700
    )
1701

    
1702
    @errors.generic.all
1703
    @errors.pithos.connection
1704
    @errors.pithos.container
1705
    @errors.pithos.object_path
1706
    def _run(self):
1707
        if self.container is None:
1708
            r = self.client.get_account_info()
1709
        elif self.path is None:
1710
            r = self.client.get_container_info(self.container)
1711
        else:
1712
            r = self.client.get_object_info(
1713
                self.path, version=self['object_version'])
1714
        self._print(r, print_dict)
1715

    
1716
    def main(self, container____path__=None):
1717
        super(self.__class__, self)._run(container____path__)
1718
        self._run()
1719

    
1720

    
1721
@command(pithos_cmds)
1722
class file_metadata(_pithos_init):
1723
    """Metadata are attached on objects. They are formed as key:value pairs.
1724
    They can have arbitary values.
1725
    """
1726

    
1727

    
1728
@command(pithos_cmds)
1729
class file_metadata_get(_file_container_command, _optional_json):
1730
    """Get metadata for account, containers or objects"""
1731

    
1732
    arguments = dict(
1733
        detail=FlagArgument('show detailed output', ('-l', '--details')),
1734
        until=DateArgument('show metadata until then', '--until'),
1735
        object_version=ValueArgument(
1736
            'show specific version (applies only for objects)',
1737
            ('-O', '--object-version'))
1738
    )
1739

    
1740
    @errors.generic.all
1741
    @errors.pithos.connection
1742
    @errors.pithos.container
1743
    @errors.pithos.object_path
1744
    def _run(self):
1745
        until = self['until']
1746
        r = None
1747
        if self.container is None:
1748
            if self['detail']:
1749
                r = self.client.get_account_info(until=until)
1750
            else:
1751
                r = self.client.get_account_meta(until=until)
1752
                r = pretty_keys(r, '-')
1753
        elif self.path is None:
1754
            if self['detail']:
1755
                r = self.client.get_container_info(until=until)
1756
            else:
1757
                cmeta = self.client.get_container_meta(until=until)
1758
                ometa = self.client.get_container_object_meta(until=until)
1759
                r = {}
1760
                if cmeta:
1761
                    r['container-meta'] = pretty_keys(cmeta, '-')
1762
                if ometa:
1763
                    r['object-meta'] = pretty_keys(ometa, '-')
1764
        else:
1765
            if self['detail']:
1766
                r = self.client.get_object_info(
1767
                    self.path,
1768
                    version=self['object_version'])
1769
            else:
1770
                r = self.client.get_object_meta(
1771
                    self.path,
1772
                    version=self['object_version'])
1773
                r = pretty_keys(pretty_keys(r, '-'))
1774
        if r:
1775
            self._print(r, print_dict)
1776

    
1777
    def main(self, container____path__=None):
1778
        super(self.__class__, self)._run(container____path__)
1779
        self._run()
1780

    
1781

    
1782
@command(pithos_cmds)
1783
class file_metadata_set(_file_container_command, _optional_output_cmd):
1784
    """Set a piece of metadata for account, container or object"""
1785

    
1786
    @errors.generic.all
1787
    @errors.pithos.connection
1788
    @errors.pithos.container
1789
    @errors.pithos.object_path
1790
    def _run(self, metakey, metaval):
1791
        if not self.container:
1792
            r = self.client.set_account_meta({metakey: metaval})
1793
        elif not self.path:
1794
            r = self.client.set_container_meta({metakey: metaval})
1795
        else:
1796
            r = self.client.set_object_meta(self.path, {metakey: metaval})
1797
        self._optional_output(r)
1798

    
1799
    def main(self, metakey, metaval, container____path__=None):
1800
        super(self.__class__, self)._run(container____path__)
1801
        self._run(metakey=metakey, metaval=metaval)
1802

    
1803

    
1804
@command(pithos_cmds)
1805
class file_metadata_delete(_file_container_command, _optional_output_cmd):
1806
    """Delete metadata with given key from account, container or object
1807
    - to get metadata of current account: /file metadata get
1808
    - to get metadata of a container:     /file metadata get <container>
1809
    - to get metadata of an object:       /file metadata get <container>:<path>
1810
    """
1811

    
1812
    @errors.generic.all
1813
    @errors.pithos.connection
1814
    @errors.pithos.container
1815
    @errors.pithos.object_path
1816
    def _run(self, metakey):
1817
        if self.container is None:
1818
            r = self.client.del_account_meta(metakey)
1819
        elif self.path is None:
1820
            r = self.client.del_container_meta(metakey)
1821
        else:
1822
            r = self.client.del_object_meta(self.path, metakey)
1823
        self._optional_output(r)
1824

    
1825
    def main(self, metakey, container____path__=None):
1826
        super(self.__class__, self)._run(container____path__)
1827
        self._run(metakey)
1828

    
1829

    
1830
@command(pithos_cmds)
1831
class file_quota(_file_account_command, _optional_json):
1832
    """Get account quota"""
1833

    
1834
    arguments = dict(
1835
        in_bytes=FlagArgument('Show result in bytes', ('-b', '--bytes'))
1836
    )
1837

    
1838
    @errors.generic.all
1839
    @errors.pithos.connection
1840
    def _run(self):
1841

    
1842
        def pretty_print(output):
1843
            if not self['in_bytes']:
1844
                for k in output:
1845
                    output[k] = format_size(output[k])
1846
            pretty_dict(output, '-')
1847

    
1848
        self._print(self.client.get_account_quota(), pretty_print)
1849

    
1850
    def main(self, custom_uuid=None):
1851
        super(self.__class__, self)._run(custom_account=custom_uuid)
1852
        self._run()
1853

    
1854

    
1855
@command(pithos_cmds)
1856
class file_containerlimit(_pithos_init):
1857
    """Container size limit commands"""
1858

    
1859

    
1860
@command(pithos_cmds)
1861
class file_containerlimit_get(_file_container_command, _optional_json):
1862
    """Get container size limit"""
1863

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

    
1868
    @errors.generic.all
1869
    @errors.pithos.container
1870
    def _run(self):
1871

    
1872
        def pretty_print(output):
1873
            if not self['in_bytes']:
1874
                for k, v in output.items():
1875
                    output[k] = 'unlimited' if '0' == v else format_size(v)
1876
            pretty_dict(output, '-')
1877

    
1878
        self._print(
1879
            self.client.get_container_limit(self.container), pretty_print)
1880

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

    
1886

    
1887
@command(pithos_cmds)
1888
class file_containerlimit_set(_file_account_command, _optional_output_cmd):
1889
    """Set new storage limit for a container
1890
    By default, the limit is set in bytes
1891
    Users may specify a different unit, e.g:
1892
    /file containerlimit set 2.3GB mycontainer
1893
    Valid units: B, KiB (1024 B), KB (1000 B), MiB, MB, GiB, GB, TiB, TB
1894
    To set container limit to "unlimited", use 0
1895
    """
1896

    
1897
    @errors.generic.all
1898
    def _calculate_limit(self, user_input):
1899
        limit = 0
1900
        try:
1901
            limit = int(user_input)
1902
        except ValueError:
1903
            index = 0
1904
            digits = [str(num) for num in range(0, 10)] + ['.']
1905
            while user_input[index] in digits:
1906
                index += 1
1907
            limit = user_input[:index]
1908
            format = user_input[index:]
1909
            try:
1910
                return to_bytes(limit, format)
1911
            except Exception as qe:
1912
                msg = 'Failed to convert %s to bytes' % user_input,
1913
                raiseCLIError(qe, msg, details=[
1914
                    'Syntax: containerlimit set <limit>[format] [container]',
1915
                    'e.g.: containerlimit set 2.3GB mycontainer',
1916
                    'Valid formats:',
1917
                    '(*1024): B, KiB, MiB, GiB, TiB',
1918
                    '(*1000): B, KB, MB, GB, TB'])
1919
        return limit
1920

    
1921
    @errors.generic.all
1922
    @errors.pithos.connection
1923
    @errors.pithos.container
1924
    def _run(self, limit):
1925
        if self.container:
1926
            self.client.container = self.container
1927
        self._optional_output(self.client.set_container_limit(limit))
1928

    
1929
    def main(self, limit, container=None):
1930
        super(self.__class__, self)._run()
1931
        limit = self._calculate_limit(limit)
1932
        self.container = container
1933
        self._run(limit)
1934

    
1935

    
1936
@command(pithos_cmds)
1937
class file_versioning(_pithos_init):
1938
    """Manage the versioning scheme of current pithos user account"""
1939

    
1940

    
1941
@command(pithos_cmds)
1942
class file_versioning_get(_file_account_command, _optional_json):
1943
    """Get  versioning for account or container"""
1944

    
1945
    @errors.generic.all
1946
    @errors.pithos.connection
1947
    @errors.pithos.container
1948
    def _run(self):
1949
        self._print(
1950
            self.client.get_container_versioning(self.container) if (
1951
                self.container) else self.client.get_account_versioning(),
1952
            print_dict)
1953

    
1954
    def main(self, container=None):
1955
        super(self.__class__, self)._run()
1956
        self.container = container
1957
        self._run()
1958

    
1959

    
1960
@command(pithos_cmds)
1961
class file_versioning_set(_file_account_command, _optional_output_cmd):
1962
    """Set versioning mode (auto, none) for account or container"""
1963

    
1964
    def _check_versioning(self, versioning):
1965
        if versioning and versioning.lower() in ('auto', 'none'):
1966
            return versioning.lower()
1967
        raiseCLIError('Invalid versioning %s' % versioning, details=[
1968
            'Versioning can be auto or none'])
1969

    
1970
    @errors.generic.all
1971
    @errors.pithos.connection
1972
    @errors.pithos.container
1973
    def _run(self, versioning):
1974
        if self.container:
1975
            self.client.container = self.container
1976
            r = self.client.set_container_versioning(versioning)
1977
        else:
1978
            r = self.client.set_account_versioning(versioning)
1979
        self._optional_output(r)
1980

    
1981
    def main(self, versioning, container=None):
1982
        super(self.__class__, self)._run()
1983
        self._run(self._check_versioning(versioning))
1984

    
1985

    
1986
@command(pithos_cmds)
1987
class file_group(_pithos_init):
1988
    """Manage access groups and group members"""
1989

    
1990

    
1991
@command(pithos_cmds)
1992
class file_group_list(_file_account_command, _optional_json):
1993
    """list all groups and group members"""
1994

    
1995
    @errors.generic.all
1996
    @errors.pithos.connection
1997
    def _run(self):
1998
        self._print(self.client.get_account_group(), pretty_dict, delim='-')
1999

    
2000
    def main(self):
2001
        super(self.__class__, self)._run()
2002
        self._run()
2003

    
2004

    
2005
@command(pithos_cmds)
2006
class file_group_set(_file_account_command, _optional_output_cmd):
2007
    """Set a user group"""
2008

    
2009
    @errors.generic.all
2010
    @errors.pithos.connection
2011
    def _run(self, groupname, *users):
2012
        self._optional_output(self.client.set_account_group(groupname, users))
2013

    
2014
    def main(self, groupname, *users):
2015
        super(self.__class__, self)._run()
2016
        if users:
2017
            self._run(groupname, *users)
2018
        else:
2019
            raiseCLIError('No users to add in group %s' % groupname)
2020

    
2021

    
2022
@command(pithos_cmds)
2023
class file_group_delete(_file_account_command, _optional_output_cmd):
2024
    """Delete a user group"""
2025

    
2026
    @errors.generic.all
2027
    @errors.pithos.connection
2028
    def _run(self, groupname):
2029
        self._optional_output(self.client.del_account_group(groupname))
2030

    
2031
    def main(self, groupname):
2032
        super(self.__class__, self)._run()
2033
        self._run(groupname)
2034

    
2035

    
2036
@command(pithos_cmds)
2037
class file_sharers(_file_account_command, _optional_json):
2038
    """List the accounts that share objects with current user"""
2039

    
2040
    arguments = dict(
2041
        detail=FlagArgument('show detailed output', ('-l', '--details')),
2042
        marker=ValueArgument('show output greater then marker', '--marker')
2043
    )
2044

    
2045
    @errors.generic.all
2046
    @errors.pithos.connection
2047
    def _run(self):
2048
        accounts = self.client.get_sharing_accounts(marker=self['marker'])
2049
        if self['json_output'] or self['detail']:
2050
            self._print(accounts)
2051
        else:
2052
            self._print([acc['name'] for acc in accounts])
2053

    
2054
    def main(self):
2055
        super(self.__class__, self)._run()
2056
        self._run()
2057

    
2058

    
2059
def version_print(versions):
2060
    print_items([dict(id=vitem[0], created=strftime(
2061
        '%d-%m-%Y %H:%M:%S',
2062
        localtime(float(vitem[1])))) for vitem in versions])
2063

    
2064

    
2065
@command(pithos_cmds)
2066
class file_versions(_file_container_command, _optional_json):
2067
    """Get the list of object versions
2068
    Deleted objects may still have versions that can be used to restore it and
2069
    get information about its previous state.
2070
    The version number can be used in a number of other commands, like info,
2071
    copy, move, meta. See these commands for more information, e.g.
2072
    /file info -h
2073
    """
2074

    
2075
    @errors.generic.all
2076
    @errors.pithos.connection
2077
    @errors.pithos.container
2078
    @errors.pithos.object_path
2079
    def _run(self):
2080
        self._print(
2081
            self.client.get_object_versionlist(self.path), version_print)
2082

    
2083
    def main(self, container___path):
2084
        super(file_versions, self)._run(
2085
            container___path,
2086
            path_is_optional=False)
2087
        self._run()