Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / pithos.py @ 6c068db6

History | View | Annotate | Download (80.2 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
        try:
556
            self._optional_output(self.client.create_container(
557
                container=container,
558
                sizelimit=self['limit'],
559
                versioning=self['versioning'],
560
                metadata=self['meta'],
561
                success=(201, )))
562
        except ClientError as ce:
563
            if ce.status in (202, ):
564
                raiseCLIError(ce, 'Container %s alread exists' % container)
565

    
566
    def main(self, container=None):
567
        super(self.__class__, self)._run(container)
568
        if container and self.container != container:
569
            raiseCLIError('Invalid container name %s' % container, details=[
570
                'Did you mean "%s" ?' % self.container,
571
                'Use --container for names containing :'])
572
        self._run(container)
573

    
574

    
575
class _source_destination_command(_file_container_command):
576

    
577
    arguments = dict(
578
        destination_account=ValueArgument('', ('-a', '--dst-account')),
579
        recursive=FlagArgument('', ('-R', '--recursive')),
580
        prefix=FlagArgument('', '--with-prefix', default=''),
581
        suffix=ValueArgument('', '--with-suffix', default=''),
582
        add_prefix=ValueArgument('', '--add-prefix', default=''),
583
        add_suffix=ValueArgument('', '--add-suffix', default=''),
584
        prefix_replace=ValueArgument('', '--prefix-to-replace', default=''),
585
        suffix_replace=ValueArgument('', '--suffix-to-replace', default=''),
586
    )
587

    
588
    def __init__(self, arguments={}, auth_base=None, cloud=None):
589
        self.arguments.update(arguments)
590
        super(_source_destination_command, self).__init__(
591
            self.arguments, auth_base, cloud)
592

    
593
    def _run(self, source_container___path, path_is_optional=False):
594
        super(_source_destination_command, self)._run(
595
            source_container___path, path_is_optional)
596
        self.dst_client = PithosClient(
597
            base_url=self.client.base_url,
598
            token=self.client.token,
599
            account=self['destination_account'] or self.client.account)
600

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

    
624
    def _get_all(self, prefix):
625
        return self.client.container_get(prefix=prefix).json
626

    
627
    def _get_src_objects(self, src_path, source_version=None):
628
        """Get a list of the source objects to be called
629

630
        :param src_path: (str) source path
631

632
        :returns: (method, params) a method that returns a list when called
633
        or (object) if it is a single object
634
        """
635
        if src_path and src_path[-1] == '/':
636
            src_path = src_path[:-1]
637

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

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

    
663
    def src_dst_pairs(self, dst_path, source_version=None):
664
        src_iter = self._get_src_objects(self.path, source_version)
665
        src_N = isinstance(src_iter, tuple)
666
        add_prefix = self['add_prefix'].strip('/')
667

    
668
        if dst_path and dst_path.endswith('/'):
669
            dst_path = dst_path[:-1]
670

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

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

    
708
    def _get_new_object(self, obj, add_prefix):
709
        if self['prefix_replace'] and obj.startswith(self['prefix_replace']):
710
            obj = obj[len(self['prefix_replace']):]
711
        if self['suffix_replace'] and obj.endswith(self['suffix_replace']):
712
            obj = obj[:-len(self['suffix_replace'])]
713
        return add_prefix + obj + self['add_suffix']
714

    
715

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

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

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

    
790
    def main(
791
            self, source_container___path,
792
            destination_container___path=None):
793
        super(file_copy, self)._run(
794
            source_container___path, path_is_optional=False)
795
        (dst_cont, dst_path) = self._dest_container_path(
796
            destination_container___path)
797
        self.dst_client.container = dst_cont or self.container
798
        self._run(dst_path=dst_path or '')
799

    
800

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

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

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

    
870
    def main(
871
            self, source_container___path,
872
            destination_container___path=None):
873
        super(self.__class__, self)._run(
874
            source_container___path,
875
            path_is_optional=False)
876
        (dst_cont, dst_path) = self._dest_container_path(
877
            destination_container___path)
878
        (dst_cont, dst_path) = self._dest_container_path(
879
            destination_container___path)
880
        self.dst_client.container = dst_cont or self.container
881
        self._run(dst_path=dst_path or '')
882

    
883

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

    
892
    arguments = dict(
893
        progress_bar=ProgressBarArgument(
894
            'do not show progress bar', ('-N', '--no-progress-bar'),
895
            default=False)
896
    )
897

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

    
911
    def main(self, local_path, container___path):
912
        super(self.__class__, self)._run(
913
            container___path, path_is_optional=False)
914
        self._run(local_path)
915

    
916

    
917
@command(pithos_cmds)
918
class file_truncate(_file_container_command, _optional_output_cmd):
919
    """Truncate remote file up to a size (default is 0)"""
920

    
921
    @errors.generic.all
922
    @errors.pithos.connection
923
    @errors.pithos.container
924
    @errors.pithos.object_path
925
    @errors.pithos.object_size
926
    def _run(self, size=0):
927
        self._optional_output(self.client.truncate_object(self.path, size))
928

    
929
    def main(self, container___path, size=0):
930
        super(self.__class__, self)._run(container___path)
931
        self._run(size=size)
932

    
933

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

    
944
    arguments = dict(
945
        progress_bar=ProgressBarArgument(
946
            'do not show progress bar', ('-N', '--no-progress-bar'),
947
            default=False)
948
    )
949

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

    
970
    def main(self, local_path, container___path, start, end):
971
        super(self.__class__, self)._run(
972
            container___path, path_is_optional=None)
973
        self.path = self.path or path.basename(local_path)
974
        self._run(local_path=local_path, start=start, end=end)
975

    
976

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

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

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

    
1021
    def main(self, container___path):
1022
        super(self.__class__, self)._run(
1023
            container___path, path_is_optional=False)
1024
        self.run()
1025

    
1026

    
1027
@command(pithos_cmds)
1028
class file_upload(_file_container_command, _optional_output_cmd):
1029
    """Upload a file"""
1030

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

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

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

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

    
1205
    def main(self, local_path, container____path__=None):
1206
        super(self.__class__, self)._run(container____path__)
1207
        remote_path = self.path or path.basename(path.abspath(local_path))
1208
        self._run(local_path=local_path, remote_path=remote_path)
1209

    
1210

    
1211
@command(pithos_cmds)
1212
class file_cat(_file_container_command):
1213
    """Print remote file contents to console"""
1214

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

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

    
1242
    def main(self, container___path):
1243
        super(self.__class__, self)._run(
1244
            container___path, path_is_optional=False)
1245
        self._run()
1246

    
1247

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

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

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

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

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

    
1435
    def main(self, container___path, local_path=None):
1436
        super(self.__class__, self)._run(container___path)
1437
        self._run(local_path=local_path)
1438

    
1439

    
1440
@command(pithos_cmds)
1441
class file_hashmap(_file_container_command, _optional_json):
1442
    """Get the hash-map of an object"""
1443

    
1444
    arguments = dict(
1445
        if_match=ValueArgument('show output if ETags match', '--if-match'),
1446
        if_none_match=ValueArgument(
1447
            'show output if ETags match', '--if-none-match'),
1448
        if_modified_since=DateArgument(
1449
            'show output modified since then', '--if-modified-since'),
1450
        if_unmodified_since=DateArgument(
1451
            'show output unmodified since then', '--if-unmodified-since'),
1452
        object_version=ValueArgument(
1453
            'get the specific version', ('-O', '--object-version'))
1454
    )
1455

    
1456
    @errors.generic.all
1457
    @errors.pithos.connection
1458
    @errors.pithos.container
1459
    @errors.pithos.object_path
1460
    def _run(self):
1461
        self._print(self.client.get_object_hashmap(
1462
            self.path,
1463
            version=self['object_version'],
1464
            if_match=self['if_match'],
1465
            if_none_match=self['if_none_match'],
1466
            if_modified_since=self['if_modified_since'],
1467
            if_unmodified_since=self['if_unmodified_since']), self.print_dict)
1468

    
1469
    def main(self, container___path):
1470
        super(self.__class__, self)._run(
1471
            container___path, path_is_optional=False)
1472
        self._run()
1473

    
1474

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

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

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

    
1533
    def main(self, container____path__=None):
1534
        super(self.__class__, self)._run(container____path__)
1535
        self._run()
1536

    
1537

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

    
1549
    arguments = dict(
1550
        yes=FlagArgument('Do not prompt for permission', '--yes'),
1551
        force=FlagArgument('purge even if not empty', ('-F', '--force'))
1552
    )
1553

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

    
1575
    def main(self, container=None):
1576
        super(self.__class__, self)._run(container)
1577
        if container and self.container != container:
1578
            raiseCLIError('Invalid container name %s' % container, details=[
1579
                'Did you mean "%s" ?' % self.container,
1580
                'Use --container for names containing :'])
1581
        self._run()
1582

    
1583

    
1584
@command(pithos_cmds)
1585
class file_publish(_file_container_command):
1586
    """Publish the object and print the public url"""
1587

    
1588
    @errors.generic.all
1589
    @errors.pithos.connection
1590
    @errors.pithos.container
1591
    @errors.pithos.object_path
1592
    def _run(self):
1593
        self.writeln(self.client.publish_object(self.path))
1594

    
1595
    def main(self, container___path):
1596
        super(self.__class__, self)._run(
1597
            container___path, path_is_optional=False)
1598
        self._run()
1599

    
1600

    
1601
@command(pithos_cmds)
1602
class file_unpublish(_file_container_command, _optional_output_cmd):
1603
    """Unpublish an object"""
1604

    
1605
    @errors.generic.all
1606
    @errors.pithos.connection
1607
    @errors.pithos.container
1608
    @errors.pithos.object_path
1609
    def _run(self):
1610
            self._optional_output(self.client.unpublish_object(self.path))
1611

    
1612
    def main(self, container___path):
1613
        super(self.__class__, self)._run(
1614
            container___path, path_is_optional=False)
1615
        self._run()
1616

    
1617

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

    
1626

    
1627
@command(pithos_cmds)
1628
class file_permissions_get(_file_container_command, _optional_json):
1629
    """Get read and write permissions of an object"""
1630

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

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

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

    
1656

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

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

    
1687
    @errors.generic.all
1688
    @errors.pithos.connection
1689
    @errors.pithos.container
1690
    @errors.pithos.object_path
1691
    def _run(self, read, write):
1692
        self._optional_output(self.client.set_object_sharing(
1693
            self.path, read_permission=read, write_permission=write))
1694

    
1695
    def main(self, container___path, *permissions):
1696
        super(self.__class__, self)._run(
1697
            container___path, path_is_optional=False)
1698
        read, write = self.format_permission_dict(permissions)
1699
        self._run(read, write)
1700

    
1701

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

    
1708
    @errors.generic.all
1709
    @errors.pithos.connection
1710
    @errors.pithos.container
1711
    @errors.pithos.object_path
1712
    def _run(self):
1713
        self._optional_output(self.client.del_object_sharing(self.path))
1714

    
1715
    def main(self, container___path):
1716
        super(self.__class__, self)._run(
1717
            container___path, path_is_optional=False)
1718
        self._run()
1719

    
1720

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

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

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

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

    
1753

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

    
1760

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

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

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

    
1805
    def main(self, container____path__=None):
1806
        super(self.__class__, self)._run(container____path__)
1807
        self._run()
1808

    
1809

    
1810
@command(pithos_cmds)
1811
class file_metadata_set(_file_container_command, _optional_output_cmd):
1812
    """Set a piece of metadata for account, container or object"""
1813

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

    
1827
    def main(self, metakey, metaval, container____path__=None):
1828
        super(self.__class__, self)._run(container____path__)
1829
        self._run(metakey=metakey, metaval=metaval)
1830

    
1831

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

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

    
1853
    def main(self, metakey, container____path__=None):
1854
        super(self.__class__, self)._run(container____path__)
1855
        self._run(metakey)
1856

    
1857

    
1858
@command(pithos_cmds)
1859
class file_quota(_file_account_command, _optional_json):
1860
    """Get account quota"""
1861

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

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

    
1870
        def pretty_print(output, **kwargs):
1871
            if not self['in_bytes']:
1872
                for k in output:
1873
                    output[k] = format_size(output[k])
1874
            self.print_dict(output, '-', **kwargs)
1875

    
1876
        self._print(self.client.get_account_quota(), pretty_print)
1877

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

    
1882

    
1883
@command(pithos_cmds)
1884
class file_containerlimit(_pithos_init):
1885
    """Container size limit commands"""
1886

    
1887

    
1888
@command(pithos_cmds)
1889
class file_containerlimit_get(_file_container_command, _optional_json):
1890
    """Get container size limit"""
1891

    
1892
    arguments = dict(
1893
        in_bytes=FlagArgument('Show result in bytes', ('-b', '--bytes'))
1894
    )
1895

    
1896
    @errors.generic.all
1897
    @errors.pithos.container
1898
    def _run(self):
1899

    
1900
        def pretty_print(output):
1901
            if not self['in_bytes']:
1902
                for k, v in output.items():
1903
                    output[k] = 'unlimited' if '0' == v else format_size(v)
1904
            self.print_dict(output, '-')
1905

    
1906
        self._print(
1907
            self.client.get_container_limit(self.container), pretty_print)
1908

    
1909
    def main(self, container=None):
1910
        super(self.__class__, self)._run()
1911
        self.container = container
1912
        self._run()
1913

    
1914

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

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

    
1949
    @errors.generic.all
1950
    @errors.pithos.connection
1951
    @errors.pithos.container
1952
    def _run(self, limit):
1953
        if self.container:
1954
            self.client.container = self.container
1955
        self._optional_output(self.client.set_container_limit(limit))
1956

    
1957
    def main(self, limit, container=None):
1958
        super(self.__class__, self)._run()
1959
        limit = self._calculate_limit(limit)
1960
        self.container = container
1961
        self._run(limit)
1962

    
1963

    
1964
@command(pithos_cmds)
1965
class file_versioning(_pithos_init):
1966
    """Manage the versioning scheme of current pithos user account"""
1967

    
1968

    
1969
@command(pithos_cmds)
1970
class file_versioning_get(_file_account_command, _optional_json):
1971
    """Get  versioning for account or container"""
1972

    
1973
    @errors.generic.all
1974
    @errors.pithos.connection
1975
    @errors.pithos.container
1976
    def _run(self):
1977
        self._print(
1978
            self.client.get_container_versioning(self.container),
1979
            self.print_dict)
1980

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

    
1986

    
1987
@command(pithos_cmds)
1988
class file_versioning_set(_file_account_command, _optional_output_cmd):
1989
    """Set versioning mode (auto, none) for account or container"""
1990

    
1991
    def _check_versioning(self, versioning):
1992
        if versioning and versioning.lower() in ('auto', 'none'):
1993
            return versioning.lower()
1994
        raiseCLIError('Invalid versioning %s' % versioning, details=[
1995
            'Versioning can be auto or none'])
1996

    
1997
    @errors.generic.all
1998
    @errors.pithos.connection
1999
    @errors.pithos.container
2000
    def _run(self, versioning):
2001
        self.client.container = self.container
2002
        r = self.client.set_container_versioning(versioning)
2003
        self._optional_output(r)
2004

    
2005
    def main(self, versioning, container):
2006
        super(self.__class__, self)._run()
2007
        self._run(self._check_versioning(versioning))
2008

    
2009

    
2010
@command(pithos_cmds)
2011
class file_group(_pithos_init):
2012
    """Manage access groups and group members"""
2013

    
2014

    
2015
@command(pithos_cmds)
2016
class file_group_list(_file_account_command, _optional_json):
2017
    """list all groups and group members"""
2018

    
2019
    @errors.generic.all
2020
    @errors.pithos.connection
2021
    def _run(self):
2022
        self._print(
2023
            self.client.get_account_group(), self.print_dict, delim='-')
2024

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

    
2029

    
2030
@command(pithos_cmds)
2031
class file_group_set(_file_account_command, _optional_output_cmd):
2032
    """Set a user group"""
2033

    
2034
    @errors.generic.all
2035
    @errors.pithos.connection
2036
    def _run(self, groupname, *users):
2037
        self._optional_output(self.client.set_account_group(groupname, users))
2038

    
2039
    def main(self, groupname, *users):
2040
        super(self.__class__, self)._run()
2041
        if users:
2042
            self._run(groupname, *users)
2043
        else:
2044
            raiseCLIError('No users to add in group %s' % groupname)
2045

    
2046

    
2047
@command(pithos_cmds)
2048
class file_group_delete(_file_account_command, _optional_output_cmd):
2049
    """Delete a user group"""
2050

    
2051
    @errors.generic.all
2052
    @errors.pithos.connection
2053
    def _run(self, groupname):
2054
        self._optional_output(self.client.del_account_group(groupname))
2055

    
2056
    def main(self, groupname):
2057
        super(self.__class__, self)._run()
2058
        self._run(groupname)
2059

    
2060

    
2061
@command(pithos_cmds)
2062
class file_sharers(_file_account_command, _optional_json):
2063
    """List the accounts that share objects with current user"""
2064

    
2065
    arguments = dict(
2066
        detail=FlagArgument('show detailed output', ('-l', '--details')),
2067
        marker=ValueArgument('show output greater then marker', '--marker')
2068
    )
2069

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

    
2084
    def main(self):
2085
        super(self.__class__, self)._run()
2086
        self._run()
2087

    
2088

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

    
2099
    def version_print(self, versions, out):
2100
        self.print_items(
2101
            [dict(id=vitem[0], created=strftime(
2102
                '%d-%m-%Y %H:%M:%S',
2103
                localtime(float(vitem[1])))) for vitem in versions],
2104
            out=out)
2105

    
2106
    @errors.generic.all
2107
    @errors.pithos.connection
2108
    @errors.pithos.container
2109
    @errors.pithos.object_path
2110
    def _run(self):
2111
        self._print(
2112
            self.client.get_object_versionlist(self.path), self.version_print)
2113

    
2114
    def main(self, container___path):
2115
        super(file_versions, self)._run(
2116
            container___path, path_is_optional=False)
2117
        self._run()