Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / pithos.py @ 534e7bbb

History | View | Annotate | Download (80 kB)

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

    
34
from time import localtime, strftime
35
from os import path, makedirs, walk
36
from io import StringIO
37
from pydoc import pager
38

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

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

    
58

    
59
# Argument functionality
60

    
61

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

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

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

    
103

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

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

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

    
134

    
135
# Command specs
136

    
137

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

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

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

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

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

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

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

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

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

    
199

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

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

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

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

    
220

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

    
224
    container = None
225
    path = None
226

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

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

    
253
        user_cont, sep, userpath = container_with_path.partition(':')
254

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

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

    
306
    def main(self, container_with_path=None, path_is_optional=True):
307
        self._run(container_with_path, path_is_optional)
308

    
309

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

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

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

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

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

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

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

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

    
493

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

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

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

    
515

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

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

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

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

    
540

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

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

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

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

    
571

    
572
class _source_destination_command(_file_container_command):
573

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

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

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

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

    
621
    def _get_all(self, prefix):
622
        return self.client.container_get(prefix=prefix).json
623

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

627
        :param src_path: (str) source path
628

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

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

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

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

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

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

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

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

    
712

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

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

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

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

    
797

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

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

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

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

    
880

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

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

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

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

    
913

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

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

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

    
930

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

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

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

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

    
973

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

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

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

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

    
1023

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

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

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

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

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

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

    
1207

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

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

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

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

    
1244

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

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

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

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

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

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

    
1436

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

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

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

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

    
1471

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

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

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

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

    
1534

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

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

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

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

    
1580

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

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

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

    
1597

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

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

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

    
1614

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

    
1623

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

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

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

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

    
1653

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

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

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

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

    
1698

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

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

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

    
1717

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

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

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

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

    
1750

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

    
1757

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

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

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

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

    
1806

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

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

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

    
1828

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

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

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

    
1854

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

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

    
1863
    @errors.generic.all
1864
    @errors.pithos.connection
1865
    def _run(self):
1866

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

    
1873
        self._print(self.client.get_account_quota(), pretty_print)
1874

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

    
1879

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

    
1884

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

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

    
1893
    @errors.generic.all
1894
    @errors.pithos.container
1895
    def _run(self):
1896

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

    
1903
        self._print(
1904
            self.client.get_container_limit(self.container), pretty_print)
1905

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

    
1911

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

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

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

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

    
1960

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

    
1965

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

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

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

    
1983

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

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

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

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

    
2006

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

    
2011

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

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

    
2022
    def main(self):
2023
        super(self.__class__, self)._run()
2024
        self._run()
2025

    
2026

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

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

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

    
2043

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

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

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

    
2057

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

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

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

    
2081
    def main(self):
2082
        super(self.__class__, self)._run()
2083
        self._run()
2084

    
2085

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

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

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

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