Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (77.3 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
                        yield open(fpath, 'rb'), '%s/%s' % (rel_path, f)
1095
                    else:
1096
                        print('%s is not a regular file' % fpath)
1097
        else:
1098
            if not path.isfile(lpath):
1099
                raiseCLIError('%s is not a regular file' % lpath)
1100
            try:
1101
                robj = self.client.get_object_info(rpath)
1102
                if remote_path and self._is_dir(robj):
1103
                    rpath += '/%s' % short_path
1104
                    self.client.get_object_info(rpath)
1105
                if not self['overwrite']:
1106
                    raiseCLIError(
1107
                        'Object %s already exists' % rpath,
1108
                        importance=1,
1109
                        details=['use -f to overwrite or resume'])
1110
            except ClientError as ce:
1111
                if ce.status != 404:
1112
                    raise
1113
            self._check_container_limit(lpath)
1114
            yield open(lpath, 'rb'), rpath
1115

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

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

    
1175

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

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

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

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

    
1213

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

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

    
1250
    def _outputs(self, local_path):
1251
        """:returns: (local_file, remote_path)"""
1252
        remotes = []
1253
        if self['recursive']:
1254
            r = self.client.container_get(
1255
                prefix=self.path or '/',
1256
                if_modified_since=self['if_modified_since'],
1257
                if_unmodified_since=self['if_unmodified_since'])
1258
            dirlist = dict()
1259
            for remote in r.json:
1260
                rname = remote['name'].strip('/')
1261
                tmppath = ''
1262
                for newdir in rname.strip('/').split('/')[:-1]:
1263
                    tmppath = '/'.join([tmppath, newdir])
1264
                    dirlist.update({tmppath.strip('/'): True})
1265
                remotes.append((rname, file_download._is_dir(remote)))
1266
            dir_remotes = [r[0] for r in remotes if r[1]]
1267
            if not set(dirlist).issubset(dir_remotes):
1268
                badguys = [bg.strip('/') for bg in set(
1269
                    dirlist).difference(dir_remotes)]
1270
                raiseCLIError(
1271
                    'Some remote paths contain non existing directories',
1272
                    details=['Missing remote directories:'] + badguys)
1273
        elif self.path:
1274
            r = self.client.get_object_info(
1275
                self.path,
1276
                version=self['object_version'])
1277
            if file_download._is_dir(r):
1278
                raiseCLIError(
1279
                    'Illegal download: Remote object %s is a directory' % (
1280
                        self.path),
1281
                    details=['To download a directory, try --recursive'])
1282
            if '/' in self.path.strip('/') and not local_path:
1283
                raiseCLIError(
1284
                    'Illegal download: remote object %s contains "/"' % (
1285
                        self.path),
1286
                    details=[
1287
                        'To download an object containing "/" characters',
1288
                        'either create the remote directories or',
1289
                        'specify a non-directory local path for this object'])
1290
            remotes = [(self.path, False)]
1291
        if not remotes:
1292
            if self.path:
1293
                raiseCLIError(
1294
                    'No matching path %s on container %s' % (
1295
                        self.path,
1296
                        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 = '/%s/%s' % (lprefix.strip('/'), rpath.strip('/'))
1310
                if remote_is_dir:
1311
                    if path.exists(lpath) and path.isdir(lpath):
1312
                        continue
1313
                    makedirs(lpath)
1314
                elif path.exists(lpath):
1315
                    if not self['resume']:
1316
                        print('File %s exists, aborting...' % lpath)
1317
                        continue
1318
                    with open(lpath, 'rwb+') as f:
1319
                        yield (f, rpath)
1320
                else:
1321
                    with open(lpath, 'wb+') as f:
1322
                        yield (f, rpath)
1323
        elif path.exists(lprefix):
1324
            if len(remotes) > 1:
1325
                raiseCLIError(
1326
                    '%s remote objects cannot be merged in local file %s' % (
1327
                        len(remotes),
1328
                        local_path),
1329
                    details=[
1330
                        'To download multiple objects, local path should be',
1331
                        'a directory, or use download without a local path'])
1332
            (rpath, remote_is_dir) = remotes[0]
1333
            if remote_is_dir:
1334
                raiseCLIError(
1335
                    'Remote directory %s should not replace local file %s' % (
1336
                        rpath,
1337
                        local_path))
1338
            if self['resume']:
1339
                with open(lprefix, 'rwb+') as f:
1340
                    yield (f, rpath)
1341
            else:
1342
                raiseCLIError(
1343
                    'Local file %s already exist' % local_path,
1344
                    details=['Try --resume to overwrite it'])
1345
        else:
1346
            if len(remotes) > 1 or remotes[0][1]:
1347
                raiseCLIError(
1348
                    'Local directory %s does not exist' % local_path)
1349
            with open(lprefix, 'wb+') as f:
1350
                yield (f, remotes[0][0])
1351

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

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

    
1406

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

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

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

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

    
1442

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

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

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

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

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

    
1505

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

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

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

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

    
1550

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

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

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

    
1568

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

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

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

    
1585

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

    
1594

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

    
1606

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

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

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

    
1624

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

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

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

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

    
1665

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

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

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

    
1684

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

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

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

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

    
1717

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

    
1724

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

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

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

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

    
1778

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

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

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

    
1800

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

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

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

    
1826

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

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

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

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

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

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

    
1851

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

    
1856

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

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

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

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

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

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

    
1883

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

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

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

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

    
1932

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

    
1937

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

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

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

    
1956

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

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

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

    
1978
    def main(self, versioning, container=None):
1979
        super(self.__class__, self)._run()
1980
        self._run(self._check_versioning(versioning))
1981

    
1982

    
1983
@command(pithos_cmds)
1984
class file_group(_pithos_init):
1985
    """Manage access groups and group members"""
1986

    
1987

    
1988
@command(pithos_cmds)
1989
class file_group_list(_file_account_command, _optional_json):
1990
    """list all groups and group members"""
1991

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

    
1997
    def main(self):
1998
        super(self.__class__, self)._run()
1999
        self._run()
2000

    
2001

    
2002
@command(pithos_cmds)
2003
class file_group_set(_file_account_command, _optional_output_cmd):
2004
    """Set a user group"""
2005

    
2006
    @errors.generic.all
2007
    @errors.pithos.connection
2008
    def _run(self, groupname, *users):
2009
        self._optional_output(self.client.set_account_group(groupname, users))
2010

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

    
2018

    
2019
@command(pithos_cmds)
2020
class file_group_delete(_file_account_command, _optional_output_cmd):
2021
    """Delete a user group"""
2022

    
2023
    @errors.generic.all
2024
    @errors.pithos.connection
2025
    def _run(self, groupname):
2026
        self._optional_output(self.client.del_account_group(groupname))
2027

    
2028
    def main(self, groupname):
2029
        super(self.__class__, self)._run()
2030
        self._run(groupname)
2031

    
2032

    
2033
@command(pithos_cmds)
2034
class file_sharers(_file_account_command, _optional_json):
2035
    """List the accounts that share objects with current user"""
2036

    
2037
    arguments = dict(
2038
        detail=FlagArgument('show detailed output', ('-l', '--details')),
2039
        marker=ValueArgument('show output greater then marker', '--marker')
2040
    )
2041

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

    
2051
    def main(self):
2052
        super(self.__class__, self)._run()
2053
        self._run()
2054

    
2055

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

    
2061

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

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

    
2080
    def main(self, container___path):
2081
        super(file_versions, self)._run(
2082
            container___path,
2083
            path_is_optional=False)
2084
        self._run()