Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (80 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 time import localtime, strftime
35
from os import path, makedirs, walk
36
from io import StringIO
37
from pydoc import pager
38

    
39
from kamaki.cli import command
40
from kamaki.cli.command_tree import CommandTree
41
from kamaki.cli.errors import (
42
    raiseCLIError, CLISyntaxError, CLIBaseUrlError, CLIInvalidArgument)
43
from kamaki.cli.utils import (
44
    format_size, to_bytes, bold, get_path_size, guess_mime_type)
45
from kamaki.cli.argument import FlagArgument, ValueArgument, IntArgument
46
from kamaki.cli.argument import KeyValueArgument, DateArgument
47
from kamaki.cli.argument import ProgressBarArgument
48
from kamaki.cli.commands import _command_init, errors
49
from kamaki.cli.commands import addLogSettings, DontRaiseKeyError
50
from kamaki.cli.commands import (
51
    _optional_output_cmd, _optional_json, _name_filter)
52
from kamaki.clients.pithos import PithosClient, ClientError
53
from kamaki.clients.astakos import AstakosClient
54

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

    
58

    
59
# Argument functionality
60

    
61

    
62
class SharingArgument(ValueArgument):
63
    """Set sharing (read and/or write) groups
64
    .
65
    :value type: "read=term1,term2,... write=term1,term2,..."
66
    .
67
    :value returns: {'read':['term1', 'term2', ...],
68
    .   'write':['term1', 'term2', ...]}
69
    """
70

    
71
    @property
72
    def value(self):
73
        return getattr(self, '_value', self.default)
74

    
75
    @value.setter
76
    def value(self, newvalue):
77
        perms = {}
78
        try:
79
            permlist = newvalue.split(' ')
80
        except AttributeError:
81
            return
82
        for p in permlist:
83
            try:
84
                (key, val) = p.split('=')
85
            except ValueError as err:
86
                raiseCLIError(
87
                    err,
88
                    'Error in --sharing',
89
                    details='Incorrect format',
90
                    importance=1)
91
            if key.lower() not in ('read', 'write'):
92
                msg = 'Error in --sharing'
93
                raiseCLIError(err, msg, importance=1, details=[
94
                    'Invalid permission key %s' % key])
95
            val_list = val.split(',')
96
            if not key in perms:
97
                perms[key] = []
98
            for item in val_list:
99
                if item not in perms[key]:
100
                    perms[key].append(item)
101
        self._value = perms
102

    
103

    
104
class RangeArgument(ValueArgument):
105
    """
106
    :value type: string of the form <start>-<end> where <start> and <end> are
107
        integers
108
    :value returns: the input string, after type checking <start> and <end>
109
    """
110

    
111
    @property
112
    def value(self):
113
        return getattr(self, '_value', self.default)
114

    
115
    @value.setter
116
    def value(self, newvalues):
117
        if not newvalues:
118
            self._value = self.default
119
            return
120
        self._value = ''
121
        for newvalue in newvalues.split(','):
122
            self._value = ('%s,' % self._value) if self._value else ''
123
            start, sep, end = newvalue.partition('-')
124
            if sep:
125
                if start:
126
                    start, end = (int(start), int(end))
127
                    assert start <= end, 'Invalid range value %s' % newvalue
128
                    self._value += '%s-%s' % (int(start), int(end))
129
                else:
130
                    self._value += '-%s' % int(end)
131
            else:
132
                self._value += '%s' % int(start)
133

    
134

    
135
# Command specs
136

    
137

    
138
class _pithos_init(_command_init):
139
    """Initialize a pithos+ kamaki client"""
140

    
141
    @staticmethod
142
    def _is_dir(remote_dict):
143
        return 'application/directory' == remote_dict.get(
144
            'content_type', remote_dict.get('content-type', ''))
145

    
146
    @DontRaiseKeyError
147
    def _custom_container(self):
148
        return self.config.get_cloud(self.cloud, 'pithos_container')
149

    
150
    @DontRaiseKeyError
151
    def _custom_uuid(self):
152
        return self.config.get_cloud(self.cloud, 'pithos_uuid')
153

    
154
    def _set_account(self):
155
        self.account = self._custom_uuid()
156
        if self.account:
157
            return
158
        if getattr(self, 'auth_base', False):
159
            self.account = self.auth_base.user_term('id', self.token)
160
        else:
161
            astakos_url = self._custom_url('astakos')
162
            astakos_token = self._custom_token('astakos') or self.token
163
            if not astakos_url:
164
                raise CLIBaseUrlError(service='astakos')
165
            astakos = AstakosClient(astakos_url, astakos_token)
166
            self.account = astakos.user_term('id')
167

    
168
    @errors.generic.all
169
    @addLogSettings
170
    def _run(self):
171
        self.base_url = None
172
        if getattr(self, 'cloud', None):
173
            self.base_url = self._custom_url('pithos')
174
        else:
175
            self.cloud = 'default'
176
        self.token = self._custom_token('pithos')
177
        self.container = self._custom_container()
178

    
179
        if getattr(self, 'auth_base', False):
180
            self.token = self.token or self.auth_base.token
181
            if not self.base_url:
182
                pithos_endpoints = self.auth_base.get_service_endpoints(
183
                    self._custom_type('pithos') or 'object-store',
184
                    self._custom_version('pithos') or '')
185
                self.base_url = pithos_endpoints['publicURL']
186
        elif not self.base_url:
187
            raise CLIBaseUrlError(service='pithos')
188

    
189
        self._set_account()
190
        self.client = PithosClient(
191
            base_url=self.base_url,
192
            token=self.token,
193
            account=self.account,
194
            container=self.container)
195

    
196
    def main(self):
197
        self._run()
198

    
199

    
200
class _file_account_command(_pithos_init):
201
    """Base class for account level storage commands"""
202

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

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

    
216
    @errors.generic.all
217
    def main(self):
218
        self._run()
219

    
220

    
221
class _file_container_command(_file_account_command):
222
    """Base class for container level storage commands"""
223

    
224
    container = None
225
    path = None
226

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

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

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

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

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

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

    
307

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

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

    
346
    def print_objects(self, object_list):
347
        for index, obj in enumerate(object_list):
348
            if self['exact_match'] and self.path and not (
349
                    obj['name'] == self.path or 'content_type' in obj):
350
                continue
351
            pretty_obj = obj.copy()
352
            index += 1
353
            empty_space = ' ' * (len(str(len(object_list))) - len(str(index)))
354
            if 'subdir' in obj:
355
                continue
356
            if obj['content_type'] == 'application/directory':
357
                isDir = True
358
                size = 'D'
359
            else:
360
                isDir = False
361
                size = format_size(obj['bytes'])
362
                pretty_obj['bytes'] = '%s (%s)' % (obj['bytes'], size)
363
            oname = obj['name'] if self['more'] else bold(obj['name'])
364
            prfx = (
365
                '%s%s. ' % (empty_space, index)) if self['enum'] else ''
366
            if self['detail']:
367
                self.writeln('%s%s' % (prfx, oname))
368
                self.print_dict(pretty_obj, exclude=('name'))
369
                self.writeln()
370
            else:
371
                oname = '%s%9s %s' % (prfx, size, oname)
372
                oname += '/' if isDir else u''
373
                self.writeln(oname)
374

    
375
    def print_containers(self, container_list):
376
        for index, container in enumerate(container_list):
377
            if 'bytes' in container:
378
                size = format_size(container['bytes'])
379
            prfx = ('%s. ' % (index + 1)) if self['enum'] else ''
380
            _cname = container['name'] if (
381
                self['more']) else bold(container['name'])
382
            cname = u'%s%s' % (prfx, _cname)
383
            if self['detail']:
384
                self.writeln(cname)
385
                pretty_c = container.copy()
386
                if 'bytes' in container:
387
                    pretty_c['bytes'] = '%s (%s)' % (container['bytes'], size)
388
                self.print_dict(pretty_c, exclude=('name'))
389
                self.writeln()
390
            else:
391
                if 'count' in container and 'bytes' in container:
392
                    self.writeln('%s (%s, %s objects)' % (
393
                        cname, size, container['count']))
394
                else:
395
                    self.writeln(cname)
396
            objects = container.get('objects', [])
397
            if objects:
398
                self.print_objects(objects)
399
                self.writeln('')
400

    
401
    def _argument_context_check(self):
402
        container_level_only = ('recursive', )
403
        object_level_only = ('delimiter', 'path', 'exact_match')
404
        details, mistake = [], ''
405
        if self.container:
406
            for term in container_level_only:
407
                if self[term]:
408
                    details = [
409
                        'This is a container-level argument',
410
                        'Use it without a <container> parameter']
411
                    mistake = self.arguments[term]
412
        else:
413
            for term in object_level_only:
414
                if not self['recursive'] and self[term]:
415
                    details = [
416
                        'This is an opbject-level argument',
417
                        'Use it with a <container> parameter',
418
                        'or with the -R/--recursive argument']
419
                    mistake = self.arguments[term]
420
        if mistake and details:
421
            raise CLIInvalidArgument(
422
                'Invalid use of %s argument' % '/'.join(mistake.parsed_name),
423
                details=details + ['Try --help for more details'])
424

    
425
    def _create_object_forest(self, container_list):
426
        try:
427
            for container in container_list:
428
                self.client.container = container['name']
429
                objects = self.client.container_get(
430
                    limit=False if self['more'] else self['limit'],
431
                    marker=self['marker'],
432
                    delimiter=self['delimiter'],
433
                    path=self['path'],
434
                    if_modified_since=self['if_modified_since'],
435
                    if_unmodified_since=self['if_unmodified_since'],
436
                    until=self['until'],
437
                    meta=self['meta'],
438
                    show_only_shared=self['shared'])
439
                container['objects'] = objects.json
440
        finally:
441
            self.client.container = None
442

    
443
    @errors.generic.all
444
    @errors.pithos.connection
445
    @errors.pithos.object_path
446
    @errors.pithos.container
447
    def _run(self):
448
        files, prnt = None, None
449
        self._argument_context_check()
450
        if not self.container:
451
            r = self.client.account_get(
452
                limit=False if self['more'] else self['limit'],
453
                marker=self['marker'],
454
                if_modified_since=self['if_modified_since'],
455
                if_unmodified_since=self['if_unmodified_since'],
456
                until=self['until'],
457
                show_only_shared=self['shared'])
458
            files, prnt = self._filter_by_name(r.json), self.print_containers
459
            if self['recursive']:
460
                self._create_object_forest(files)
461
        else:
462
            prefix = (self.path and not self['name']) or self['name_pref']
463
            r = self.client.container_get(
464
                limit=False if self['more'] else self['limit'],
465
                marker=self['marker'],
466
                prefix=prefix,
467
                delimiter=self['delimiter'],
468
                path=self['path'],
469
                if_modified_since=self['if_modified_since'],
470
                if_unmodified_since=self['if_unmodified_since'],
471
                until=self['until'],
472
                meta=self['meta'],
473
                show_only_shared=self['shared'])
474
            files, prnt = self._filter_by_name(r.json), self.print_objects
475
        if self['more']:
476
            outbu, self._out = self._out, StringIO()
477
        try:
478
            if self['json_output'] or self['output_format']:
479
                self._print(files)
480
            else:
481
                prnt(files)
482
        finally:
483
            if self['more']:
484
                pager(self._out.getvalue())
485
                self._out = outbu
486

    
487
    def main(self, container____path__=None):
488
        super(self.__class__, self)._run(container____path__)
489
        self._run()
490

    
491

    
492
@command(pithos_cmds)
493
class file_mkdir(_file_container_command, _optional_output_cmd):
494
    """Create a directory
495
    Kamaki hanldes directories the same way as OOS Storage and Pithos+:
496
    A directory  is   an  object  with  type  "application/directory"
497
    An object with path  dir/name can exist even if  dir does not exist
498
    or even if dir  is  a non  directory  object.  Users can modify dir '
499
    without affecting the dir/name object in any way.
500
    """
501

    
502
    @errors.generic.all
503
    @errors.pithos.connection
504
    @errors.pithos.container
505
    def _run(self):
506
        self._optional_output(self.client.create_directory(self.path))
507

    
508
    def main(self, container___directory):
509
        super(self.__class__, self)._run(
510
            container___directory, path_is_optional=False)
511
        self._run()
512

    
513

    
514
@command(pithos_cmds)
515
class file_touch(_file_container_command, _optional_output_cmd):
516
    """Create an empty object (file)
517
    If object exists, this command will reset it to 0 length
518
    """
519

    
520
    arguments = dict(
521
        content_type=ValueArgument(
522
            'Set content type (default: application/octet-stream)',
523
            '--content-type',
524
            default='application/octet-stream')
525
    )
526

    
527
    @errors.generic.all
528
    @errors.pithos.connection
529
    @errors.pithos.container
530
    def _run(self):
531
        self._optional_output(
532
            self.client.create_object(self.path, self['content_type']))
533

    
534
    def main(self, container___path):
535
        super(file_touch, self)._run(container___path, path_is_optional=False)
536
        self._run()
537

    
538

    
539
@command(pithos_cmds)
540
class file_create(_file_container_command, _optional_output_cmd):
541
    """Create a container"""
542

    
543
    arguments = dict(
544
        versioning=ValueArgument(
545
            'set container versioning (auto/none)', '--versioning'),
546
        limit=IntArgument('set default container limit', '--limit'),
547
        meta=KeyValueArgument(
548
            'set container metadata (can be repeated)', '--meta')
549
    )
550

    
551
    @errors.generic.all
552
    @errors.pithos.connection
553
    @errors.pithos.container
554
    def _run(self, container):
555
        self._optional_output(self.client.create_container(
556
            container=container,
557
            sizelimit=self['limit'],
558
            versioning=self['versioning'],
559
            metadata=self['meta']))
560

    
561
    def main(self, container=None):
562
        super(self.__class__, self)._run(container)
563
        if container and self.container != container:
564
            raiseCLIError('Invalid container name %s' % container, details=[
565
                'Did you mean "%s" ?' % self.container,
566
                'Use --container for names containing :'])
567
        self._run(container)
568

    
569

    
570
class _source_destination_command(_file_container_command):
571

    
572
    arguments = dict(
573
        destination_account=ValueArgument('', ('-a', '--dst-account')),
574
        recursive=FlagArgument('', ('-R', '--recursive')),
575
        prefix=FlagArgument('', '--with-prefix', default=''),
576
        suffix=ValueArgument('', '--with-suffix', default=''),
577
        add_prefix=ValueArgument('', '--add-prefix', default=''),
578
        add_suffix=ValueArgument('', '--add-suffix', default=''),
579
        prefix_replace=ValueArgument('', '--prefix-to-replace', default=''),
580
        suffix_replace=ValueArgument('', '--suffix-to-replace', default=''),
581
    )
582

    
583
    def __init__(self, arguments={}, auth_base=None, cloud=None):
584
        self.arguments.update(arguments)
585
        super(_source_destination_command, self).__init__(
586
            self.arguments, auth_base, cloud)
587

    
588
    def _run(self, source_container___path, path_is_optional=False):
589
        super(_source_destination_command, self)._run(
590
            source_container___path, path_is_optional)
591
        self.dst_client = PithosClient(
592
            base_url=self.client.base_url,
593
            token=self.client.token,
594
            account=self['destination_account'] or self.client.account)
595

    
596
    @errors.generic.all
597
    @errors.pithos.account
598
    def _dest_container_path(self, dest_container_path):
599
        if self['destination_container']:
600
            self.dst_client.container = self['destination_container']
601
            return (self['destination_container'], dest_container_path)
602
        if dest_container_path:
603
            dst = dest_container_path.split(':')
604
            if len(dst) > 1:
605
                try:
606
                    self.dst_client.container = dst[0]
607
                    self.dst_client.get_container_info(dst[0])
608
                except ClientError as err:
609
                    if err.status in (404, 204):
610
                        raiseCLIError(
611
                            'Destination container %s not found' % dst[0])
612
                    raise
613
                else:
614
                    self.dst_client.container = dst[0]
615
                return (dst[0], dst[1])
616
            return(None, dst[0])
617
        raiseCLIError('No destination container:path provided')
618

    
619
    def _get_all(self, prefix):
620
        return self.client.container_get(prefix=prefix).json
621

    
622
    def _get_src_objects(self, src_path, source_version=None):
623
        """Get a list of the source objects to be called
624

625
        :param src_path: (str) source path
626

627
        :returns: (method, params) a method that returns a list when called
628
        or (object) if it is a single object
629
        """
630
        if src_path and src_path[-1] == '/':
631
            src_path = src_path[:-1]
632

    
633
        if self['prefix']:
634
            return (self._get_all, dict(prefix=src_path))
635
        try:
636
            srcobj = self.client.get_object_info(
637
                src_path, version=source_version)
638
        except ClientError as srcerr:
639
            if srcerr.status == 404:
640
                raiseCLIError(
641
                    'Source object %s not in source container %s' % (
642
                        src_path, self.client.container),
643
                    details=['Hint: --with-prefix to match multiple objects'])
644
            elif srcerr.status not in (204,):
645
                raise
646
            return (self.client.list_objects, {})
647

    
648
        if self._is_dir(srcobj):
649
            if not self['recursive']:
650
                raiseCLIError(
651
                    'Object %s of cont. %s is a dir' % (
652
                        src_path, self.client.container),
653
                    details=['Use --recursive to access directories'])
654
            return (self._get_all, dict(prefix=src_path))
655
        srcobj['name'] = src_path
656
        return srcobj
657

    
658
    def src_dst_pairs(self, dst_path, source_version=None):
659
        src_iter = self._get_src_objects(self.path, source_version)
660
        src_N = isinstance(src_iter, tuple)
661
        add_prefix = self['add_prefix'].strip('/')
662

    
663
        if dst_path and dst_path.endswith('/'):
664
            dst_path = dst_path[:-1]
665

    
666
        try:
667
            dstobj = self.dst_client.get_object_info(dst_path)
668
        except ClientError as trgerr:
669
            if trgerr.status in (404,):
670
                if src_N:
671
                    raiseCLIError(
672
                        'Cannot merge multiple paths to path %s' % dst_path,
673
                        details=[
674
                            'Try to use / or a directory as destination',
675
                            'or create the destination dir (/file mkdir)',
676
                            'or use a single object as source'])
677
            elif trgerr.status not in (204,):
678
                raise
679
        else:
680
            if self._is_dir(dstobj):
681
                add_prefix = '%s/%s' % (dst_path.strip('/'), add_prefix)
682
            elif src_N:
683
                raiseCLIError(
684
                    'Cannot merge multiple paths to path' % dst_path,
685
                    details=[
686
                        'Try to use / or a directory as destination',
687
                        'or create the destination dir (/file mkdir)',
688
                        'or use a single object as source'])
689

    
690
        if src_N:
691
            (method, kwargs) = src_iter
692
            for obj in method(**kwargs):
693
                name = obj['name']
694
                if name.endswith(self['suffix']):
695
                    yield (name, self._get_new_object(name, add_prefix))
696
        elif src_iter['name'].endswith(self['suffix']):
697
            name = src_iter['name']
698
            yield (name, self._get_new_object(dst_path or name, add_prefix))
699
        else:
700
            raiseCLIError('Source path %s conflicts with suffix %s' % (
701
                src_iter['name'], self['suffix']))
702

    
703
    def _get_new_object(self, obj, add_prefix):
704
        if self['prefix_replace'] and obj.startswith(self['prefix_replace']):
705
            obj = obj[len(self['prefix_replace']):]
706
        if self['suffix_replace'] and obj.endswith(self['suffix_replace']):
707
            obj = obj[:-len(self['suffix_replace'])]
708
        return add_prefix + obj + self['add_suffix']
709

    
710

    
711
@command(pithos_cmds)
712
class file_copy(_source_destination_command, _optional_output_cmd):
713
    """Copy objects from container to (another) container
714
    Semantics:
715
    copy cont:path dir
716
    .   transfer path as dir/path
717
    copy cont:path cont2:
718
    .   trasnfer all <obj> prefixed with path to container cont2
719
    copy cont:path [cont2:]path2
720
    .   transfer path to path2
721
    Use options:
722
    1. <container1>:<path1> [container2:]<path2> : if container2 is not given,
723
    destination is container1:path2
724
    2. <container>:<path1> <path2> : make a copy in the same container
725
    3. Can use --container= instead of <container1>
726
    """
727

    
728
    arguments = dict(
729
        destination_account=ValueArgument(
730
            'Account to copy to', ('-a', '--dst-account')),
731
        destination_container=ValueArgument(
732
            'use it if destination container name contains a : character',
733
            ('-D', '--dst-container')),
734
        public=ValueArgument('make object publicly accessible', '--public'),
735
        content_type=ValueArgument(
736
            'change object\'s content type', '--content-type'),
737
        recursive=FlagArgument(
738
            'copy directory and contents', ('-R', '--recursive')),
739
        prefix=FlagArgument(
740
            'Match objects prefixed with src path (feels like src_path*)',
741
            '--with-prefix',
742
            default=''),
743
        suffix=ValueArgument(
744
            'Suffix of source objects (feels like *suffix)', '--with-suffix',
745
            default=''),
746
        add_prefix=ValueArgument('Prefix targets', '--add-prefix', default=''),
747
        add_suffix=ValueArgument('Suffix targets', '--add-suffix', default=''),
748
        prefix_replace=ValueArgument(
749
            'Prefix of src to replace with dst path + add_prefix, if matched',
750
            '--prefix-to-replace',
751
            default=''),
752
        suffix_replace=ValueArgument(
753
            'Suffix of src to replace with add_suffix, if matched',
754
            '--suffix-to-replace',
755
            default=''),
756
        source_version=ValueArgument(
757
            'copy specific version', ('-S', '--source-version'))
758
    )
759

    
760
    @errors.generic.all
761
    @errors.pithos.connection
762
    @errors.pithos.container
763
    @errors.pithos.account
764
    def _run(self, dst_path):
765
        no_source_object = True
766
        src_account = self.client.account if (
767
            self['destination_account']) else None
768
        for src_obj, dst_obj in self.src_dst_pairs(
769
                dst_path, self['source_version']):
770
            no_source_object = False
771
            r = self.dst_client.copy_object(
772
                src_container=self.client.container,
773
                src_object=src_obj,
774
                dst_container=self.dst_client.container,
775
                dst_object=dst_obj,
776
                source_account=src_account,
777
                source_version=self['source_version'],
778
                public=self['public'],
779
                content_type=self['content_type'])
780
        if no_source_object:
781
            raiseCLIError('No object %s in container %s' % (
782
                self.path, self.container))
783
        self._optional_output(r)
784

    
785
    def main(
786
            self, source_container___path,
787
            destination_container___path=None):
788
        super(file_copy, self)._run(
789
            source_container___path, path_is_optional=False)
790
        (dst_cont, dst_path) = self._dest_container_path(
791
            destination_container___path)
792
        self.dst_client.container = dst_cont or self.container
793
        self._run(dst_path=dst_path or '')
794

    
795

    
796
@command(pithos_cmds)
797
class file_move(_source_destination_command, _optional_output_cmd):
798
    """Move/rename objects from container to (another) container
799
    Semantics:
800
    move cont:path dir
801
    .   rename path as dir/path
802
    move cont:path cont2:
803
    .   trasnfer all <obj> prefixed with path to container cont2
804
    move cont:path [cont2:]path2
805
    .   transfer path to path2
806
    Use options:
807
    1. <container1>:<path1> [container2:]<path2> : if container2 is not given,
808
    destination is container1:path2
809
    2. <container>:<path1> <path2> : move in the same container
810
    3. Can use --container= instead of <container1>
811
    """
812

    
813
    arguments = dict(
814
        destination_account=ValueArgument(
815
            'Account to move to', ('-a', '--dst-account')),
816
        destination_container=ValueArgument(
817
            'use it if destination container name contains a : character',
818
            ('-D', '--dst-container')),
819
        public=ValueArgument('make object publicly accessible', '--public'),
820
        content_type=ValueArgument(
821
            'change object\'s content type', '--content-type'),
822
        recursive=FlagArgument(
823
            'copy directory and contents', ('-R', '--recursive')),
824
        prefix=FlagArgument(
825
            'Match objects prefixed with src path (feels like src_path*)',
826
            '--with-prefix',
827
            default=''),
828
        suffix=ValueArgument(
829
            'Suffix of source objects (feels like *suffix)', '--with-suffix',
830
            default=''),
831
        add_prefix=ValueArgument('Prefix targets', '--add-prefix', default=''),
832
        add_suffix=ValueArgument('Suffix targets', '--add-suffix', default=''),
833
        prefix_replace=ValueArgument(
834
            'Prefix of src to replace with dst path + add_prefix, if matched',
835
            '--prefix-to-replace',
836
            default=''),
837
        suffix_replace=ValueArgument(
838
            'Suffix of src to replace with add_suffix, if matched',
839
            '--suffix-to-replace',
840
            default='')
841
    )
842

    
843
    @errors.generic.all
844
    @errors.pithos.connection
845
    @errors.pithos.container
846
    def _run(self, dst_path):
847
        no_source_object = True
848
        src_account = self.client.account if (
849
            self['destination_account']) else None
850
        for src_obj, dst_obj in self.src_dst_pairs(dst_path):
851
            no_source_object = False
852
            r = self.dst_client.move_object(
853
                src_container=self.container,
854
                src_object=src_obj,
855
                dst_container=self.dst_client.container,
856
                dst_object=dst_obj,
857
                source_account=src_account,
858
                public=self['public'],
859
                content_type=self['content_type'])
860
        if no_source_object:
861
            raiseCLIError('No object %s in container %s' % (
862
                self.path, self.container))
863
        self._optional_output(r)
864

    
865
    def main(
866
            self, source_container___path,
867
            destination_container___path=None):
868
        super(self.__class__, self)._run(
869
            source_container___path,
870
            path_is_optional=False)
871
        (dst_cont, dst_path) = self._dest_container_path(
872
            destination_container___path)
873
        (dst_cont, dst_path) = self._dest_container_path(
874
            destination_container___path)
875
        self.dst_client.container = dst_cont or self.container
876
        self._run(dst_path=dst_path or '')
877

    
878

    
879
@command(pithos_cmds)
880
class file_append(_file_container_command, _optional_output_cmd):
881
    """Append local file to (existing) remote object
882
    The remote object should exist.
883
    If the remote object is a directory, it is transformed into a file.
884
    In the later case, objects under the directory remain intact.
885
    """
886

    
887
    arguments = dict(
888
        progress_bar=ProgressBarArgument(
889
            'do not show progress bar', ('-N', '--no-progress-bar'),
890
            default=False)
891
    )
892

    
893
    @errors.generic.all
894
    @errors.pithos.connection
895
    @errors.pithos.container
896
    @errors.pithos.object_path
897
    def _run(self, local_path):
898
        (progress_bar, upload_cb) = self._safe_progress_bar('Appending')
899
        try:
900
            with open(local_path, 'rb') as f:
901
                self._optional_output(
902
                    self.client.append_object(self.path, f, upload_cb))
903
        finally:
904
            self._safe_progress_bar_finish(progress_bar)
905

    
906
    def main(self, local_path, container___path):
907
        super(self.__class__, self)._run(
908
            container___path, path_is_optional=False)
909
        self._run(local_path)
910

    
911

    
912
@command(pithos_cmds)
913
class file_truncate(_file_container_command, _optional_output_cmd):
914
    """Truncate remote file up to a size (default is 0)"""
915

    
916
    @errors.generic.all
917
    @errors.pithos.connection
918
    @errors.pithos.container
919
    @errors.pithos.object_path
920
    @errors.pithos.object_size
921
    def _run(self, size=0):
922
        self._optional_output(self.client.truncate_object(self.path, size))
923

    
924
    def main(self, container___path, size=0):
925
        super(self.__class__, self)._run(container___path)
926
        self._run(size=size)
927

    
928

    
929
@command(pithos_cmds)
930
class file_overwrite(_file_container_command, _optional_output_cmd):
931
    """Overwrite part (from start to end) of a remote file
932
    overwrite local-path container 10 20
933
    .   will overwrite bytes from 10 to 20 of a remote file with the same name
934
    .   as local-path basename
935
    overwrite local-path container:path 10 20
936
    .   will overwrite as above, but the remote file is named path
937
    """
938

    
939
    arguments = dict(
940
        progress_bar=ProgressBarArgument(
941
            'do not show progress bar', ('-N', '--no-progress-bar'),
942
            default=False)
943
    )
944

    
945
    @errors.generic.all
946
    @errors.pithos.connection
947
    @errors.pithos.container
948
    @errors.pithos.object_path
949
    @errors.pithos.object_size
950
    def _run(self, local_path, start, end):
951
        start, end = int(start), int(end)
952
        (progress_bar, upload_cb) = self._safe_progress_bar(
953
            'Overwrite %s bytes' % (end - start))
954
        try:
955
            with open(path.abspath(local_path), 'rb') as f:
956
                self._optional_output(self.client.overwrite_object(
957
                    obj=self.path,
958
                    start=start,
959
                    end=end,
960
                    source_file=f,
961
                    upload_cb=upload_cb))
962
        finally:
963
            self._safe_progress_bar_finish(progress_bar)
964

    
965
    def main(self, local_path, container___path, start, end):
966
        super(self.__class__, self)._run(
967
            container___path, path_is_optional=None)
968
        self.path = self.path or path.basename(local_path)
969
        self._run(local_path=local_path, start=start, end=end)
970

    
971

    
972
@command(pithos_cmds)
973
class file_manifest(_file_container_command, _optional_output_cmd):
974
    """Create a remote file of uploaded parts by manifestation
975
    Remains functional for compatibility with OOS Storage. Users are advised
976
    to use the upload command instead.
977
    Manifestation is a compliant process for uploading large files. The files
978
    have to be chunked in smalled files and uploaded as <prefix><increment>
979
    where increment is 1, 2, ...
980
    Finally, the manifest command glues partial files together in one file
981
    named <prefix>
982
    The upload command is faster, easier and more intuitive than manifest
983
    """
984

    
985
    arguments = dict(
986
        etag=ValueArgument('check written data', '--etag'),
987
        content_encoding=ValueArgument(
988
            'set MIME content type', '--content-encoding'),
989
        content_disposition=ValueArgument(
990
            'the presentation style of the object', '--content-disposition'),
991
        content_type=ValueArgument(
992
            'specify content type', '--content-type',
993
            default='application/octet-stream'),
994
        sharing=SharingArgument(
995
            '\n'.join([
996
                'define object sharing policy',
997
                '    ( "read=user1,grp1,user2,... write=user1,grp2,..." )']),
998
            '--sharing'),
999
        public=FlagArgument('make object publicly accessible', '--public')
1000
    )
1001

    
1002
    @errors.generic.all
1003
    @errors.pithos.connection
1004
    @errors.pithos.container
1005
    @errors.pithos.object_path
1006
    def _run(self):
1007
        ctype, cenc = guess_mime_type(self.path)
1008
        self._optional_output(self.client.create_object_by_manifestation(
1009
            self.path,
1010
            content_encoding=self['content_encoding'] or cenc,
1011
            content_disposition=self['content_disposition'],
1012
            content_type=self['content_type'] or ctype,
1013
            sharing=self['sharing'],
1014
            public=self['public']))
1015

    
1016
    def main(self, container___path):
1017
        super(self.__class__, self)._run(
1018
            container___path, path_is_optional=False)
1019
        self.run()
1020

    
1021

    
1022
@command(pithos_cmds)
1023
class file_upload(_file_container_command, _optional_output_cmd):
1024
    """Upload a file"""
1025

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

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

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

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

    
1200
    def main(self, local_path, container____path__=None):
1201
        super(self.__class__, self)._run(container____path__)
1202
        remote_path = self.path or path.basename(path.abspath(local_path))
1203
        self._run(local_path=local_path, remote_path=remote_path)
1204

    
1205

    
1206
@command(pithos_cmds)
1207
class file_cat(_file_container_command):
1208
    """Print remote file contents to console"""
1209

    
1210
    arguments = dict(
1211
        range=RangeArgument('show range of data', '--range'),
1212
        if_match=ValueArgument('show output if ETags match', '--if-match'),
1213
        if_none_match=ValueArgument(
1214
            'show output if ETags match', '--if-none-match'),
1215
        if_modified_since=DateArgument(
1216
            'show output modified since then', '--if-modified-since'),
1217
        if_unmodified_since=DateArgument(
1218
            'show output unmodified since then', '--if-unmodified-since'),
1219
        object_version=ValueArgument(
1220
            'get the specific version', ('-O', '--object-version'))
1221
    )
1222

    
1223
    @errors.generic.all
1224
    @errors.pithos.connection
1225
    @errors.pithos.container
1226
    @errors.pithos.object_path
1227
    def _run(self):
1228
        self.client.download_object(
1229
            self.path, self._out,
1230
            range_str=self['range'],
1231
            version=self['object_version'],
1232
            if_match=self['if_match'],
1233
            if_none_match=self['if_none_match'],
1234
            if_modified_since=self['if_modified_since'],
1235
            if_unmodified_since=self['if_unmodified_since'])
1236

    
1237
    def main(self, container___path):
1238
        super(self.__class__, self)._run(
1239
            container___path, path_is_optional=False)
1240
        self._run()
1241

    
1242

    
1243
@command(pithos_cmds)
1244
class file_download(_file_container_command):
1245
    """Download remote object as local file
1246
    If local destination is a directory:
1247
    *   download <container>:<path> <local dir> -R
1248
    will download all files on <container> prefixed as <path>,
1249
    to <local dir>/<full path> (or <local dir>\<full path> in windows)
1250
    *   download <container>:<path> <local dir>
1251
    will download only one file<path>
1252
    ATTENTION: to download cont:dir1/dir2/file there must exist objects
1253
    cont:dir1 and cont:dir1/dir2 of type application/directory
1254
    To create directory objects, use /file mkdir
1255
    """
1256

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

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

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

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

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

    
1434

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

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

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

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

    
1469

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

    
1489
    arguments = dict(
1490
        until=DateArgument('remove history until that date', '--until'),
1491
        yes=FlagArgument('Do not prompt for permission', '--yes'),
1492
        recursive=FlagArgument(
1493
            'empty dir or container and delete (if dir)',
1494
            ('-R', '--recursive')),
1495
        delimiter=ValueArgument(
1496
            'delete objects prefixed with <object><delimiter>', '--delimiter')
1497
    )
1498

    
1499
    @errors.generic.all
1500
    @errors.pithos.connection
1501
    @errors.pithos.container
1502
    @errors.pithos.object_path
1503
    def _run(self):
1504
        if self.path:
1505
            if self['yes'] or self.ask_user(
1506
                    'Delete %s:%s ?' % (self.container, self.path)):
1507
                self._optional_output(self.client.del_object(
1508
                    self.path,
1509
                    until=self['until'],
1510
                    delimiter='/' if self['recursive'] else self['delimiter']))
1511
            else:
1512
                self.error('Aborted')
1513
        elif self.container:
1514
            if self['recursive']:
1515
                ask_msg = 'Delete container contents'
1516
            else:
1517
                ask_msg = 'Delete container'
1518
            if self['yes'] or self.ask_user(
1519
                    '%s %s ?' % (ask_msg, self.container)):
1520
                self._optional_output(self.client.del_container(
1521
                    until=self['until'],
1522
                    delimiter='/' if self['recursive'] else self['delimiter']))
1523
            else:
1524
                self.error('Aborted')
1525
        else:
1526
            raiseCLIError('Nothing to delete, please provide container[:path]')
1527

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

    
1532

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

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

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

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

    
1578

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

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

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

    
1595

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

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

    
1607
    def main(self, container___path):
1608
        super(self.__class__, self)._run(
1609
            container___path, path_is_optional=False)
1610
        self._run()
1611

    
1612

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

    
1621

    
1622
@command(pithos_cmds)
1623
class file_permissions_get(_file_container_command, _optional_json):
1624
    """Get read and write permissions of an object"""
1625

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

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

    
1646
    def main(self, container___path):
1647
        super(self.__class__, self)._run(
1648
            container___path, path_is_optional=False)
1649
        self._run()
1650

    
1651

    
1652
@command(pithos_cmds)
1653
class file_permissions_set(_file_container_command, _optional_output_cmd):
1654
    """Set permissions for an object
1655
    New permissions overwrite existing permissions.
1656
    Permission format:
1657
    -   read=<username>[,usergroup[,...]]
1658
    -   write=<username>[,usegroup[,...]]
1659
    E.g. to give read permissions for file F to users A and B and write for C:
1660
    .       /file permissions set F read=A,B write=C
1661
    To share with everybody, use '*' instead of a user id or group.
1662
    E.g. to make file F available to all pithos users:
1663
    .   /file permissions set F read=*
1664
    E.g. to make file F available for editing to all pithos users:
1665
    .   /file permissions set F write=*
1666
    """
1667

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

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

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

    
1696

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

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

    
1710
    def main(self, container___path):
1711
        super(self.__class__, self)._run(
1712
            container___path, path_is_optional=False)
1713
        self._run()
1714

    
1715

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

    
1724
    arguments = dict(
1725
        object_version=ValueArgument(
1726
            'show specific version \ (applies only for objects)',
1727
            ('-O', '--object-version'))
1728
    )
1729

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

    
1744
    def main(self, container____path__=None):
1745
        super(self.__class__, self)._run(container____path__)
1746
        self._run()
1747

    
1748

    
1749
@command(pithos_cmds)
1750
class file_metadata(_pithos_init):
1751
    """Metadata are attached on objects. They are formed as key:value pairs.
1752
    They can have arbitary values.
1753
    """
1754

    
1755

    
1756
@command(pithos_cmds)
1757
class file_metadata_get(_file_container_command, _optional_json):
1758
    """Get metadata for account, containers or objects"""
1759

    
1760
    arguments = dict(
1761
        detail=FlagArgument('show detailed output', ('-l', '--details')),
1762
        until=DateArgument('show metadata until then', '--until'),
1763
        object_version=ValueArgument(
1764
            'show specific version (applies only for objects)',
1765
            ('-O', '--object-version'))
1766
    )
1767

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

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

    
1804

    
1805
@command(pithos_cmds)
1806
class file_metadata_set(_file_container_command, _optional_output_cmd):
1807
    """Set a piece of metadata for account, container or object"""
1808

    
1809
    @errors.generic.all
1810
    @errors.pithos.connection
1811
    @errors.pithos.container
1812
    @errors.pithos.object_path
1813
    def _run(self, metakey, metaval):
1814
        if not self.container:
1815
            r = self.client.set_account_meta({metakey: metaval})
1816
        elif not self.path:
1817
            r = self.client.set_container_meta({metakey: metaval})
1818
        else:
1819
            r = self.client.set_object_meta(self.path, {metakey: metaval})
1820
        self._optional_output(r)
1821

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

    
1826

    
1827
@command(pithos_cmds)
1828
class file_metadata_delete(_file_container_command, _optional_output_cmd):
1829
    """Delete metadata with given key from account, container or object
1830
    - to get metadata of current account: /file metadata get
1831
    - to get metadata of a container:     /file metadata get <container>
1832
    - to get metadata of an object:       /file metadata get <container>:<path>
1833
    """
1834

    
1835
    @errors.generic.all
1836
    @errors.pithos.connection
1837
    @errors.pithos.container
1838
    @errors.pithos.object_path
1839
    def _run(self, metakey):
1840
        if self.container is None:
1841
            r = self.client.del_account_meta(metakey)
1842
        elif self.path is None:
1843
            r = self.client.del_container_meta(metakey)
1844
        else:
1845
            r = self.client.del_object_meta(self.path, metakey)
1846
        self._optional_output(r)
1847

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

    
1852

    
1853
@command(pithos_cmds)
1854
class file_quota(_file_account_command, _optional_json):
1855
    """Get account quota"""
1856

    
1857
    arguments = dict(
1858
        in_bytes=FlagArgument('Show result in bytes', ('-b', '--bytes'))
1859
    )
1860

    
1861
    @errors.generic.all
1862
    @errors.pithos.connection
1863
    def _run(self):
1864

    
1865
        def pretty_print(output, **kwargs):
1866
            if not self['in_bytes']:
1867
                for k in output:
1868
                    output[k] = format_size(output[k])
1869
            self.print_dict(output, '-', **kwargs)
1870

    
1871
        self._print(self.client.get_account_quota(), pretty_print)
1872

    
1873
    def main(self, custom_uuid=None):
1874
        super(self.__class__, self)._run(custom_account=custom_uuid)
1875
        self._run()
1876

    
1877

    
1878
@command(pithos_cmds)
1879
class file_containerlimit(_pithos_init):
1880
    """Container size limit commands"""
1881

    
1882

    
1883
@command(pithos_cmds)
1884
class file_containerlimit_get(_file_container_command, _optional_json):
1885
    """Get container size limit"""
1886

    
1887
    arguments = dict(
1888
        in_bytes=FlagArgument('Show result in bytes', ('-b', '--bytes'))
1889
    )
1890

    
1891
    @errors.generic.all
1892
    @errors.pithos.container
1893
    def _run(self):
1894

    
1895
        def pretty_print(output):
1896
            if not self['in_bytes']:
1897
                for k, v in output.items():
1898
                    output[k] = 'unlimited' if '0' == v else format_size(v)
1899
            self.print_dict(output, '-')
1900

    
1901
        self._print(
1902
            self.client.get_container_limit(self.container), pretty_print)
1903

    
1904
    def main(self, container=None):
1905
        super(self.__class__, self)._run()
1906
        self.container = container
1907
        self._run()
1908

    
1909

    
1910
@command(pithos_cmds)
1911
class file_containerlimit_set(_file_account_command, _optional_output_cmd):
1912
    """Set new storage limit for a container
1913
    By default, the limit is set in bytes
1914
    Users may specify a different unit, e.g:
1915
    /file containerlimit set 2.3GB mycontainer
1916
    Valid units: B, KiB (1024 B), KB (1000 B), MiB, MB, GiB, GB, TiB, TB
1917
    To set container limit to "unlimited", use 0
1918
    """
1919

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

    
1944
    @errors.generic.all
1945
    @errors.pithos.connection
1946
    @errors.pithos.container
1947
    def _run(self, limit):
1948
        if self.container:
1949
            self.client.container = self.container
1950
        self._optional_output(self.client.set_container_limit(limit))
1951

    
1952
    def main(self, limit, container=None):
1953
        super(self.__class__, self)._run()
1954
        limit = self._calculate_limit(limit)
1955
        self.container = container
1956
        self._run(limit)
1957

    
1958

    
1959
@command(pithos_cmds)
1960
class file_versioning(_pithos_init):
1961
    """Manage the versioning scheme of current pithos user account"""
1962

    
1963

    
1964
@command(pithos_cmds)
1965
class file_versioning_get(_file_account_command, _optional_json):
1966
    """Get  versioning for account or container"""
1967

    
1968
    @errors.generic.all
1969
    @errors.pithos.connection
1970
    @errors.pithos.container
1971
    def _run(self):
1972
        self._print(
1973
            self.client.get_container_versioning(self.container),
1974
            self.print_dict)
1975

    
1976
    def main(self, container):
1977
        super(self.__class__, self)._run()
1978
        self.container = container
1979
        self._run()
1980

    
1981

    
1982
@command(pithos_cmds)
1983
class file_versioning_set(_file_account_command, _optional_output_cmd):
1984
    """Set versioning mode (auto, none) for account or container"""
1985

    
1986
    def _check_versioning(self, versioning):
1987
        if versioning and versioning.lower() in ('auto', 'none'):
1988
            return versioning.lower()
1989
        raiseCLIError('Invalid versioning %s' % versioning, details=[
1990
            'Versioning can be auto or none'])
1991

    
1992
    @errors.generic.all
1993
    @errors.pithos.connection
1994
    @errors.pithos.container
1995
    def _run(self, versioning):
1996
        self.client.container = self.container
1997
        r = self.client.set_container_versioning(versioning)
1998
        self._optional_output(r)
1999

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

    
2004

    
2005
@command(pithos_cmds)
2006
class file_group(_pithos_init):
2007
    """Manage access groups and group members"""
2008

    
2009

    
2010
@command(pithos_cmds)
2011
class file_group_list(_file_account_command, _optional_json):
2012
    """list all groups and group members"""
2013

    
2014
    @errors.generic.all
2015
    @errors.pithos.connection
2016
    def _run(self):
2017
        self._print(
2018
            self.client.get_account_group(), self.print_dict, delim='-')
2019

    
2020
    def main(self):
2021
        super(self.__class__, self)._run()
2022
        self._run()
2023

    
2024

    
2025
@command(pithos_cmds)
2026
class file_group_set(_file_account_command, _optional_output_cmd):
2027
    """Set a user group"""
2028

    
2029
    @errors.generic.all
2030
    @errors.pithos.connection
2031
    def _run(self, groupname, *users):
2032
        self._optional_output(self.client.set_account_group(groupname, users))
2033

    
2034
    def main(self, groupname, *users):
2035
        super(self.__class__, self)._run()
2036
        if users:
2037
            self._run(groupname, *users)
2038
        else:
2039
            raiseCLIError('No users to add in group %s' % groupname)
2040

    
2041

    
2042
@command(pithos_cmds)
2043
class file_group_delete(_file_account_command, _optional_output_cmd):
2044
    """Delete a user group"""
2045

    
2046
    @errors.generic.all
2047
    @errors.pithos.connection
2048
    def _run(self, groupname):
2049
        self._optional_output(self.client.del_account_group(groupname))
2050

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

    
2055

    
2056
@command(pithos_cmds)
2057
class file_sharers(_file_account_command, _optional_json):
2058
    """List the accounts that share objects with current user"""
2059

    
2060
    arguments = dict(
2061
        detail=FlagArgument('show detailed output', ('-l', '--details')),
2062
        marker=ValueArgument('show output greater then marker', '--marker')
2063
    )
2064

    
2065
    @errors.generic.all
2066
    @errors.pithos.connection
2067
    def _run(self):
2068
        accounts = self.client.get_sharing_accounts(marker=self['marker'])
2069
        if not (self['json_output'] or self['output_format']):
2070
            usernames = self._uuids2usernames(
2071
                [acc['name'] for acc in accounts])
2072
            for item in accounts:
2073
                uuid = item['name']
2074
                item['id'], item['name'] = uuid, usernames[uuid]
2075
                if not self['detail']:
2076
                    item.pop('last_modified')
2077
        self._print(accounts)
2078

    
2079
    def main(self):
2080
        super(self.__class__, self)._run()
2081
        self._run()
2082

    
2083

    
2084
@command(pithos_cmds)
2085
class file_versions(_file_container_command, _optional_json):
2086
    """Get the list of object versions
2087
    Deleted objects may still have versions that can be used to restore it and
2088
    get information about its previous state.
2089
    The version number can be used in a number of other commands, like info,
2090
    copy, move, meta. See these commands for more information, e.g.,
2091
    /file info -h
2092
    """
2093

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

    
2101
    @errors.generic.all
2102
    @errors.pithos.connection
2103
    @errors.pithos.container
2104
    @errors.pithos.object_path
2105
    def _run(self):
2106
        self._print(
2107
            self.client.get_object_versionlist(self.path), self.version_print)
2108

    
2109
    def main(self, container___path):
2110
        super(file_versions, self)._run(
2111
            container___path, path_is_optional=False)
2112
        self._run()