Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (77.3 kB)

1
# Copyright 2011-2013 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_cloud(self.cloud, 'pithos_container')
157

    
158
    @DontRaiseKeyError
159
    def _custom_uuid(self):
160
        return self.config.get_cloud(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
    Kamaki hanldes directories the same way as OOS Storage and Pithos+:
455
    A directory  is   an  object  with  type  "application/directory"
456
    An object with path  dir/name can exist even if  dir does not exist
457
    or even if dir  is  a non  directory  object.  Users can modify dir '
458
    without affecting the dir/name object in any way.
459
    """
460

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

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

    
473

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

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

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

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

    
500

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

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

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

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

    
531

    
532
class _source_destination_command(_file_container_command):
533

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

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

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

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

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

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

588
        :param src_path: (str) source path
589

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

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

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

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

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

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

    
653
        if src_N:
654
            (method, kwargs) = src_iter
655
            for obj in method(**kwargs):
656
                name = obj['name']
657
                if name.endswith(self['suffix']):
658
                    yield (name, self._get_new_object(name, add_prefix))
659
        elif src_iter['name'].endswith(self['suffix']):
660
            name = src_iter['name']
661
            yield (name, self._get_new_object(dst_path or name, add_prefix))
662
        else:
663
            raiseCLIError('Source path %s conflicts with suffix %s' % (
664
                src_iter['name'], 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', ('-a', '--dst-account')),
694
        destination_container=ValueArgument(
695
            'use it if destination container name contains a : character',
696
            ('-D', '--dst-container')),
697
        public=ValueArgument('make object publicly accessible', '--public'),
698
        content_type=ValueArgument(
699
            'change object\'s content type', '--content-type'),
700
        recursive=FlagArgument(
701
            'copy directory and contents', ('-R', '--recursive')),
702
        prefix=FlagArgument(
703
            'Match objects prefixed with src path (feels like src_path*)',
704
            '--with-prefix',
705
            default=''),
706
        suffix=ValueArgument(
707
            'Suffix of source objects (feels like *suffix)', '--with-suffix',
708
            default=''),
709
        add_prefix=ValueArgument('Prefix targets', '--add-prefix', default=''),
710
        add_suffix=ValueArgument('Suffix targets', '--add-suffix', default=''),
711
        prefix_replace=ValueArgument(
712
            'Prefix of src to replace with dst path + add_prefix, if matched',
713
            '--prefix-to-replace',
714
            default=''),
715
        suffix_replace=ValueArgument(
716
            'Suffix of src to replace with add_suffix, if matched',
717
            '--suffix-to-replace',
718
            default=''),
719
        source_version=ValueArgument(
720
            'copy specific version', ('-S', '--source-version'))
721
    )
722

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

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

    
759

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

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

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

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

    
843

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

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

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

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

    
880

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

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

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

    
897

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

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

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

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

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

    
948

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

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

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

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

    
997

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

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

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

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

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

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

    
1176

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

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

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

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

    
1214

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

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

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

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

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

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

    
1408

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

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

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

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

    
1444

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

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

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

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

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

    
1507

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

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

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

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

    
1552

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

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

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

    
1569

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

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

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

    
1586

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

    
1595

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

    
1607

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

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

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

    
1625

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

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

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

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

    
1666

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

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

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

    
1685

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

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

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

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

    
1718

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

    
1725

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

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

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

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

    
1779

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

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

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

    
1801

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

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

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

    
1827

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

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

    
1836
    @errors.generic.all
1837
    @errors.pithos.connection
1838
    def _run(self):
1839

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

    
1846
        self._print(self.client.get_account_quota(), pretty_print)
1847

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

    
1852

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

    
1857

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

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

    
1866
    @errors.generic.all
1867
    @errors.pithos.container
1868
    def _run(self):
1869

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

    
1876
        self._print(
1877
            self.client.get_container_limit(self.container), pretty_print)
1878

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

    
1884

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

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

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

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

    
1933

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

    
1938

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

    
1943
    @errors.generic.all
1944
    @errors.pithos.connection
1945
    @errors.pithos.container
1946
    def _run(self):
1947
        self._print(
1948
            self.client.get_container_versioning(self.container), print_dict)
1949

    
1950
    def main(self, container):
1951
        super(self.__class__, self)._run()
1952
        self.container = container
1953
        self._run()
1954

    
1955

    
1956
@command(pithos_cmds)
1957
class file_versioning_set(_file_account_command, _optional_output_cmd):
1958
    """Set versioning mode (auto, none) for account or container"""
1959

    
1960
    def _check_versioning(self, versioning):
1961
        if versioning and versioning.lower() in ('auto', 'none'):
1962
            return versioning.lower()
1963
        raiseCLIError('Invalid versioning %s' % versioning, details=[
1964
            'Versioning can be auto or none'])
1965

    
1966
    @errors.generic.all
1967
    @errors.pithos.connection
1968
    @errors.pithos.container
1969
    def _run(self, versioning):
1970
        self.client.container = self.container
1971
        r = self.client.set_container_versioning(versioning)
1972
        self._optional_output(r)
1973

    
1974
    def main(self, versioning, container):
1975
        super(self.__class__, self)._run()
1976
        self._run(self._check_versioning(versioning))
1977

    
1978

    
1979
@command(pithos_cmds)
1980
class file_group(_pithos_init):
1981
    """Manage access groups and group members"""
1982

    
1983

    
1984
@command(pithos_cmds)
1985
class file_group_list(_file_account_command, _optional_json):
1986
    """list all groups and group members"""
1987

    
1988
    @errors.generic.all
1989
    @errors.pithos.connection
1990
    def _run(self):
1991
        self._print(self.client.get_account_group(), pretty_dict, delim='-')
1992

    
1993
    def main(self):
1994
        super(self.__class__, self)._run()
1995
        self._run()
1996

    
1997

    
1998
@command(pithos_cmds)
1999
class file_group_set(_file_account_command, _optional_output_cmd):
2000
    """Set a user group"""
2001

    
2002
    @errors.generic.all
2003
    @errors.pithos.connection
2004
    def _run(self, groupname, *users):
2005
        self._optional_output(self.client.set_account_group(groupname, users))
2006

    
2007
    def main(self, groupname, *users):
2008
        super(self.__class__, self)._run()
2009
        if users:
2010
            self._run(groupname, *users)
2011
        else:
2012
            raiseCLIError('No users to add in group %s' % groupname)
2013

    
2014

    
2015
@command(pithos_cmds)
2016
class file_group_delete(_file_account_command, _optional_output_cmd):
2017
    """Delete a user group"""
2018

    
2019
    @errors.generic.all
2020
    @errors.pithos.connection
2021
    def _run(self, groupname):
2022
        self._optional_output(self.client.del_account_group(groupname))
2023

    
2024
    def main(self, groupname):
2025
        super(self.__class__, self)._run()
2026
        self._run(groupname)
2027

    
2028

    
2029
@command(pithos_cmds)
2030
class file_sharers(_file_account_command, _optional_json):
2031
    """List the accounts that share objects with current user"""
2032

    
2033
    arguments = dict(
2034
        detail=FlagArgument('show detailed output', ('-l', '--details')),
2035
        marker=ValueArgument('show output greater then marker', '--marker')
2036
    )
2037

    
2038
    @errors.generic.all
2039
    @errors.pithos.connection
2040
    def _run(self):
2041
        accounts = self.client.get_sharing_accounts(marker=self['marker'])
2042
        if self['json_output'] or self['detail']:
2043
            self._print(accounts)
2044
        else:
2045
            self._print([acc['name'] for acc in accounts])
2046

    
2047
    def main(self):
2048
        super(self.__class__, self)._run()
2049
        self._run()
2050

    
2051

    
2052
def version_print(versions):
2053
    print_items([dict(id=vitem[0], created=strftime(
2054
        '%d-%m-%Y %H:%M:%S',
2055
        localtime(float(vitem[1])))) for vitem in versions])
2056

    
2057

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

    
2068
    @errors.generic.all
2069
    @errors.pithos.connection
2070
    @errors.pithos.container
2071
    @errors.pithos.object_path
2072
    def _run(self):
2073
        self._print(
2074
            self.client.get_object_versionlist(self.path), version_print)
2075

    
2076
    def main(self, container___path):
2077
        super(file_versions, self)._run(
2078
            container___path,
2079
            path_is_optional=False)
2080
        self._run()