Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / pithos.py @ 8694bdad

History | View | Annotate | Download (78.9 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

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

    
422
    @errors.generic.all
423
    @errors.pithos.connection
424
    @errors.pithos.object_path
425
    @errors.pithos.container
426
    def _run(self):
427
        files, prnt = None, None
428
        self._argument_context_check()
429
        if not self.container:
430
            r = self.client.account_get(
431
                limit=False if self['more'] else self['limit'],
432
                marker=self['marker'],
433
                if_modified_since=self['if_modified_since'],
434
                if_unmodified_since=self['if_unmodified_since'],
435
                until=self['until'],
436
                show_only_shared=self['shared'])
437
            files, prnt = self._filter_by_name(r.json), self.print_containers
438
        else:
439
            prefix = (self.path and not self['name']) or self['name_pref']
440
            r = self.client.container_get(
441
                limit=False if self['more'] else self['limit'],
442
                marker=self['marker'],
443
                prefix=prefix,
444
                delimiter=self['delimiter'],
445
                path=self['path'],
446
                if_modified_since=self['if_modified_since'],
447
                if_unmodified_since=self['if_unmodified_since'],
448
                until=self['until'],
449
                meta=self['meta'],
450
                show_only_shared=self['shared'])
451
            files, prnt = self._filter_by_name(r.json), self.print_objects
452
        if self['more']:
453
            outbu, self._out = self._out, StringIO()
454
        try:
455
            if self['json_output']:
456
                self._print(files)
457
            else:
458
                prnt(files)
459
        finally:
460
            if self['more']:
461
                pager(self._out.getvalue())
462
                self._out = outbu
463

    
464
    def main(self, container____path__=None):
465
        super(self.__class__, self)._run(container____path__)
466
        self._run()
467

    
468

    
469
@command(pithos_cmds)
470
class file_mkdir(_file_container_command, _optional_output_cmd):
471
    """Create a directory
472
    Kamaki hanldes directories the same way as OOS Storage and Pithos+:
473
    A directory  is   an  object  with  type  "application/directory"
474
    An object with path  dir/name can exist even if  dir does not exist
475
    or even if dir  is  a non  directory  object.  Users can modify dir '
476
    without affecting the dir/name object in any way.
477
    """
478

    
479
    @errors.generic.all
480
    @errors.pithos.connection
481
    @errors.pithos.container
482
    def _run(self):
483
        self._optional_output(self.client.create_directory(self.path))
484

    
485
    def main(self, container___directory):
486
        super(self.__class__, self)._run(
487
            container___directory, path_is_optional=False)
488
        self._run()
489

    
490

    
491
@command(pithos_cmds)
492
class file_touch(_file_container_command, _optional_output_cmd):
493
    """Create an empty object (file)
494
    If object exists, this command will reset it to 0 length
495
    """
496

    
497
    arguments = dict(
498
        content_type=ValueArgument(
499
            'Set content type (default: application/octet-stream)',
500
            '--content-type',
501
            default='application/octet-stream')
502
    )
503

    
504
    @errors.generic.all
505
    @errors.pithos.connection
506
    @errors.pithos.container
507
    def _run(self):
508
        self._optional_output(
509
            self.client.create_object(self.path, self['content_type']))
510

    
511
    def main(self, container___path):
512
        super(file_touch, self)._run(container___path, path_is_optional=False)
513
        self._run()
514

    
515

    
516
@command(pithos_cmds)
517
class file_create(_file_container_command, _optional_output_cmd):
518
    """Create a container"""
519

    
520
    arguments = dict(
521
        versioning=ValueArgument(
522
            'set container versioning (auto/none)', '--versioning'),
523
        limit=IntArgument('set default container limit', '--limit'),
524
        meta=KeyValueArgument(
525
            'set container metadata (can be repeated)', '--meta')
526
    )
527

    
528
    @errors.generic.all
529
    @errors.pithos.connection
530
    @errors.pithos.container
531
    def _run(self, container):
532
        self._optional_output(self.client.create_container(
533
            container=container,
534
            sizelimit=self['limit'],
535
            versioning=self['versioning'],
536
            metadata=self['meta']))
537

    
538
    def main(self, container=None):
539
        super(self.__class__, self)._run(container)
540
        if container and self.container != container:
541
            raiseCLIError('Invalid container name %s' % container, details=[
542
                'Did you mean "%s" ?' % self.container,
543
                'Use --container for names containing :'])
544
        self._run(container)
545

    
546

    
547
class _source_destination_command(_file_container_command):
548

    
549
    arguments = dict(
550
        destination_account=ValueArgument('', ('-a', '--dst-account')),
551
        recursive=FlagArgument('', ('-R', '--recursive')),
552
        prefix=FlagArgument('', '--with-prefix', default=''),
553
        suffix=ValueArgument('', '--with-suffix', default=''),
554
        add_prefix=ValueArgument('', '--add-prefix', default=''),
555
        add_suffix=ValueArgument('', '--add-suffix', default=''),
556
        prefix_replace=ValueArgument('', '--prefix-to-replace', default=''),
557
        suffix_replace=ValueArgument('', '--suffix-to-replace', default=''),
558
    )
559

    
560
    def __init__(self, arguments={}, auth_base=None, cloud=None):
561
        self.arguments.update(arguments)
562
        super(_source_destination_command, self).__init__(
563
            self.arguments, auth_base, cloud)
564

    
565
    def _run(self, source_container___path, path_is_optional=False):
566
        super(_source_destination_command, self)._run(
567
            source_container___path, path_is_optional)
568
        self.dst_client = PithosClient(
569
            base_url=self.client.base_url,
570
            token=self.client.token,
571
            account=self['destination_account'] or self.client.account)
572

    
573
    @errors.generic.all
574
    @errors.pithos.account
575
    def _dest_container_path(self, dest_container_path):
576
        if self['destination_container']:
577
            self.dst_client.container = self['destination_container']
578
            return (self['destination_container'], dest_container_path)
579
        if dest_container_path:
580
            dst = dest_container_path.split(':')
581
            if len(dst) > 1:
582
                try:
583
                    self.dst_client.container = dst[0]
584
                    self.dst_client.get_container_info(dst[0])
585
                except ClientError as err:
586
                    if err.status in (404, 204):
587
                        raiseCLIError(
588
                            'Destination container %s not found' % dst[0])
589
                    raise
590
                else:
591
                    self.dst_client.container = dst[0]
592
                return (dst[0], dst[1])
593
            return(None, dst[0])
594
        raiseCLIError('No destination container:path provided')
595

    
596
    def _get_all(self, prefix):
597
        return self.client.container_get(prefix=prefix).json
598

    
599
    def _get_src_objects(self, src_path, source_version=None):
600
        """Get a list of the source objects to be called
601

602
        :param src_path: (str) source path
603

604
        :returns: (method, params) a method that returns a list when called
605
        or (object) if it is a single object
606
        """
607
        if src_path and src_path[-1] == '/':
608
            src_path = src_path[:-1]
609

    
610
        if self['prefix']:
611
            return (self._get_all, dict(prefix=src_path))
612
        try:
613
            srcobj = self.client.get_object_info(
614
                src_path, version=source_version)
615
        except ClientError as srcerr:
616
            if srcerr.status == 404:
617
                raiseCLIError(
618
                    'Source object %s not in source container %s' % (
619
                        src_path, self.client.container),
620
                    details=['Hint: --with-prefix to match multiple objects'])
621
            elif srcerr.status not in (204,):
622
                raise
623
            return (self.client.list_objects, {})
624

    
625
        if self._is_dir(srcobj):
626
            if not self['recursive']:
627
                raiseCLIError(
628
                    'Object %s of cont. %s is a dir' % (
629
                        src_path, self.client.container),
630
                    details=['Use --recursive to access directories'])
631
            return (self._get_all, dict(prefix=src_path))
632
        srcobj['name'] = src_path
633
        return srcobj
634

    
635
    def src_dst_pairs(self, dst_path, source_version=None):
636
        src_iter = self._get_src_objects(self.path, source_version)
637
        src_N = isinstance(src_iter, tuple)
638
        add_prefix = self['add_prefix'].strip('/')
639

    
640
        if dst_path and dst_path.endswith('/'):
641
            dst_path = dst_path[:-1]
642

    
643
        try:
644
            dstobj = self.dst_client.get_object_info(dst_path)
645
        except ClientError as trgerr:
646
            if trgerr.status in (404,):
647
                if src_N:
648
                    raiseCLIError(
649
                        'Cannot merge multiple paths to path %s' % dst_path,
650
                        details=[
651
                            'Try to use / or a directory as destination',
652
                            'or create the destination dir (/file mkdir)',
653
                            'or use a single object as source'])
654
            elif trgerr.status not in (204,):
655
                raise
656
        else:
657
            if self._is_dir(dstobj):
658
                add_prefix = '%s/%s' % (dst_path.strip('/'), add_prefix)
659
            elif src_N:
660
                raiseCLIError(
661
                    'Cannot merge multiple paths to path' % dst_path,
662
                    details=[
663
                        'Try to use / or a directory as destination',
664
                        'or create the destination dir (/file mkdir)',
665
                        'or use a single object as source'])
666

    
667
        if src_N:
668
            (method, kwargs) = src_iter
669
            for obj in method(**kwargs):
670
                name = obj['name']
671
                if name.endswith(self['suffix']):
672
                    yield (name, self._get_new_object(name, add_prefix))
673
        elif src_iter['name'].endswith(self['suffix']):
674
            name = src_iter['name']
675
            yield (name, self._get_new_object(dst_path or name, add_prefix))
676
        else:
677
            raiseCLIError('Source path %s conflicts with suffix %s' % (
678
                src_iter['name'], self['suffix']))
679

    
680
    def _get_new_object(self, obj, add_prefix):
681
        if self['prefix_replace'] and obj.startswith(self['prefix_replace']):
682
            obj = obj[len(self['prefix_replace']):]
683
        if self['suffix_replace'] and obj.endswith(self['suffix_replace']):
684
            obj = obj[:-len(self['suffix_replace'])]
685
        return add_prefix + obj + self['add_suffix']
686

    
687

    
688
@command(pithos_cmds)
689
class file_copy(_source_destination_command, _optional_output_cmd):
690
    """Copy objects from container to (another) container
691
    Semantics:
692
    copy cont:path dir
693
    .   transfer path as dir/path
694
    copy cont:path cont2:
695
    .   trasnfer all <obj> prefixed with path to container cont2
696
    copy cont:path [cont2:]path2
697
    .   transfer path to path2
698
    Use options:
699
    1. <container1>:<path1> [container2:]<path2> : if container2 is not given,
700
    destination is container1:path2
701
    2. <container>:<path1> <path2> : make a copy in the same container
702
    3. Can use --container= instead of <container1>
703
    """
704

    
705
    arguments = dict(
706
        destination_account=ValueArgument(
707
            'Account to copy to', ('-a', '--dst-account')),
708
        destination_container=ValueArgument(
709
            'use it if destination container name contains a : character',
710
            ('-D', '--dst-container')),
711
        public=ValueArgument('make object publicly accessible', '--public'),
712
        content_type=ValueArgument(
713
            'change object\'s content type', '--content-type'),
714
        recursive=FlagArgument(
715
            'copy directory and contents', ('-R', '--recursive')),
716
        prefix=FlagArgument(
717
            'Match objects prefixed with src path (feels like src_path*)',
718
            '--with-prefix',
719
            default=''),
720
        suffix=ValueArgument(
721
            'Suffix of source objects (feels like *suffix)', '--with-suffix',
722
            default=''),
723
        add_prefix=ValueArgument('Prefix targets', '--add-prefix', default=''),
724
        add_suffix=ValueArgument('Suffix targets', '--add-suffix', default=''),
725
        prefix_replace=ValueArgument(
726
            'Prefix of src to replace with dst path + add_prefix, if matched',
727
            '--prefix-to-replace',
728
            default=''),
729
        suffix_replace=ValueArgument(
730
            'Suffix of src to replace with add_suffix, if matched',
731
            '--suffix-to-replace',
732
            default=''),
733
        source_version=ValueArgument(
734
            'copy specific version', ('-S', '--source-version'))
735
    )
736

    
737
    @errors.generic.all
738
    @errors.pithos.connection
739
    @errors.pithos.container
740
    @errors.pithos.account
741
    def _run(self, dst_path):
742
        no_source_object = True
743
        src_account = self.client.account if (
744
            self['destination_account']) else None
745
        for src_obj, dst_obj in self.src_dst_pairs(
746
                dst_path, self['source_version']):
747
            no_source_object = False
748
            r = self.dst_client.copy_object(
749
                src_container=self.client.container,
750
                src_object=src_obj,
751
                dst_container=self.dst_client.container,
752
                dst_object=dst_obj,
753
                source_account=src_account,
754
                source_version=self['source_version'],
755
                public=self['public'],
756
                content_type=self['content_type'])
757
        if no_source_object:
758
            raiseCLIError('No object %s in container %s' % (
759
                self.path, self.container))
760
        self._optional_output(r)
761

    
762
    def main(
763
            self, source_container___path,
764
            destination_container___path=None):
765
        super(file_copy, self)._run(
766
            source_container___path, path_is_optional=False)
767
        (dst_cont, dst_path) = self._dest_container_path(
768
            destination_container___path)
769
        self.dst_client.container = dst_cont or self.container
770
        self._run(dst_path=dst_path or '')
771

    
772

    
773
@command(pithos_cmds)
774
class file_move(_source_destination_command, _optional_output_cmd):
775
    """Move/rename objects from container to (another) container
776
    Semantics:
777
    move cont:path dir
778
    .   rename path as dir/path
779
    move cont:path cont2:
780
    .   trasnfer all <obj> prefixed with path to container cont2
781
    move cont:path [cont2:]path2
782
    .   transfer path to path2
783
    Use options:
784
    1. <container1>:<path1> [container2:]<path2> : if container2 is not given,
785
    destination is container1:path2
786
    2. <container>:<path1> <path2> : move in the same container
787
    3. Can use --container= instead of <container1>
788
    """
789

    
790
    arguments = dict(
791
        destination_account=ValueArgument(
792
            'Account to move to', ('-a', '--dst-account')),
793
        destination_container=ValueArgument(
794
            'use it if destination container name contains a : character',
795
            ('-D', '--dst-container')),
796
        public=ValueArgument('make object publicly accessible', '--public'),
797
        content_type=ValueArgument(
798
            'change object\'s content type', '--content-type'),
799
        recursive=FlagArgument(
800
            'copy directory and contents', ('-R', '--recursive')),
801
        prefix=FlagArgument(
802
            'Match objects prefixed with src path (feels like src_path*)',
803
            '--with-prefix',
804
            default=''),
805
        suffix=ValueArgument(
806
            'Suffix of source objects (feels like *suffix)', '--with-suffix',
807
            default=''),
808
        add_prefix=ValueArgument('Prefix targets', '--add-prefix', default=''),
809
        add_suffix=ValueArgument('Suffix targets', '--add-suffix', default=''),
810
        prefix_replace=ValueArgument(
811
            'Prefix of src to replace with dst path + add_prefix, if matched',
812
            '--prefix-to-replace',
813
            default=''),
814
        suffix_replace=ValueArgument(
815
            'Suffix of src to replace with add_suffix, if matched',
816
            '--suffix-to-replace',
817
            default='')
818
    )
819

    
820
    @errors.generic.all
821
    @errors.pithos.connection
822
    @errors.pithos.container
823
    def _run(self, dst_path):
824
        no_source_object = True
825
        src_account = self.client.account if (
826
            self['destination_account']) else None
827
        for src_obj, dst_obj in self.src_dst_pairs(dst_path):
828
            no_source_object = False
829
            r = self.dst_client.move_object(
830
                src_container=self.container,
831
                src_object=src_obj,
832
                dst_container=self.dst_client.container,
833
                dst_object=dst_obj,
834
                source_account=src_account,
835
                public=self['public'],
836
                content_type=self['content_type'])
837
        if no_source_object:
838
            raiseCLIError('No object %s in container %s' % (
839
                self.path, self.container))
840
        self._optional_output(r)
841

    
842
    def main(
843
            self, source_container___path,
844
            destination_container___path=None):
845
        super(self.__class__, self)._run(
846
            source_container___path,
847
            path_is_optional=False)
848
        (dst_cont, dst_path) = self._dest_container_path(
849
            destination_container___path)
850
        (dst_cont, dst_path) = self._dest_container_path(
851
            destination_container___path)
852
        self.dst_client.container = dst_cont or self.container
853
        self._run(dst_path=dst_path or '')
854

    
855

    
856
@command(pithos_cmds)
857
class file_append(_file_container_command, _optional_output_cmd):
858
    """Append local file to (existing) remote object
859
    The remote object should exist.
860
    If the remote object is a directory, it is transformed into a file.
861
    In the later case, objects under the directory remain intact.
862
    """
863

    
864
    arguments = dict(
865
        progress_bar=ProgressBarArgument(
866
            'do not show progress bar', ('-N', '--no-progress-bar'),
867
            default=False)
868
    )
869

    
870
    @errors.generic.all
871
    @errors.pithos.connection
872
    @errors.pithos.container
873
    @errors.pithos.object_path
874
    def _run(self, local_path):
875
        (progress_bar, upload_cb) = self._safe_progress_bar('Appending')
876
        try:
877
            with open(local_path, 'rb') as f:
878
                self._optional_output(
879
                    self.client.append_object(self.path, f, upload_cb))
880
        finally:
881
            self._safe_progress_bar_finish(progress_bar)
882

    
883
    def main(self, local_path, container___path):
884
        super(self.__class__, self)._run(
885
            container___path, path_is_optional=False)
886
        self._run(local_path)
887

    
888

    
889
@command(pithos_cmds)
890
class file_truncate(_file_container_command, _optional_output_cmd):
891
    """Truncate remote file up to a size (default is 0)"""
892

    
893
    @errors.generic.all
894
    @errors.pithos.connection
895
    @errors.pithos.container
896
    @errors.pithos.object_path
897
    @errors.pithos.object_size
898
    def _run(self, size=0):
899
        self._optional_output(self.client.truncate_object(self.path, size))
900

    
901
    def main(self, container___path, size=0):
902
        super(self.__class__, self)._run(container___path)
903
        self._run(size=size)
904

    
905

    
906
@command(pithos_cmds)
907
class file_overwrite(_file_container_command, _optional_output_cmd):
908
    """Overwrite part (from start to end) of a remote file
909
    overwrite local-path container 10 20
910
    .   will overwrite bytes from 10 to 20 of a remote file with the same name
911
    .   as local-path basename
912
    overwrite local-path container:path 10 20
913
    .   will overwrite as above, but the remote file is named path
914
    """
915

    
916
    arguments = dict(
917
        progress_bar=ProgressBarArgument(
918
            'do not show progress bar', ('-N', '--no-progress-bar'),
919
            default=False)
920
    )
921

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

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

    
948

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

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

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

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

    
998

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

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

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

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

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

    
1177
    def main(self, local_path, container____path__=None):
1178
        super(self.__class__, self)._run(container____path__)
1179
        remote_path = self.path or path.basename(path.abspath(local_path))
1180
        self._run(local_path=local_path, remote_path=remote_path)
1181

    
1182

    
1183
@command(pithos_cmds)
1184
class file_cat(_file_container_command):
1185
    """Print remote file contents to console"""
1186

    
1187
    arguments = dict(
1188
        range=RangeArgument('show range of data', '--range'),
1189
        if_match=ValueArgument('show output if ETags match', '--if-match'),
1190
        if_none_match=ValueArgument(
1191
            'show output if ETags match', '--if-none-match'),
1192
        if_modified_since=DateArgument(
1193
            'show output modified since then', '--if-modified-since'),
1194
        if_unmodified_since=DateArgument(
1195
            'show output unmodified since then', '--if-unmodified-since'),
1196
        object_version=ValueArgument(
1197
            'get the specific version', ('-O', '--object-version'))
1198
    )
1199

    
1200
    @errors.generic.all
1201
    @errors.pithos.connection
1202
    @errors.pithos.container
1203
    @errors.pithos.object_path
1204
    def _run(self):
1205
        self.client.download_object(
1206
            self.path, self._out,
1207
            range_str=self['range'],
1208
            version=self['object_version'],
1209
            if_match=self['if_match'],
1210
            if_none_match=self['if_none_match'],
1211
            if_modified_since=self['if_modified_since'],
1212
            if_unmodified_since=self['if_unmodified_since'])
1213

    
1214
    def main(self, container___path):
1215
        super(self.__class__, self)._run(
1216
            container___path, path_is_optional=False)
1217
        self._run()
1218

    
1219

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

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

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

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

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

    
1407
    def main(self, container___path, local_path=None):
1408
        super(self.__class__, self)._run(container___path)
1409
        self._run(local_path=local_path)
1410

    
1411

    
1412
@command(pithos_cmds)
1413
class file_hashmap(_file_container_command, _optional_json):
1414
    """Get the hash-map of an object"""
1415

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

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

    
1441
    def main(self, container___path):
1442
        super(self.__class__, self)._run(
1443
            container___path, path_is_optional=False)
1444
        self._run()
1445

    
1446

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

    
1466
    arguments = dict(
1467
        until=DateArgument('remove history until that date', '--until'),
1468
        yes=FlagArgument('Do not prompt for permission', '--yes'),
1469
        recursive=FlagArgument(
1470
            'empty dir or container and delete (if dir)',
1471
            ('-R', '--recursive')),
1472
        delimiter=ValueArgument(
1473
            'delete objects prefixed with <object><delimiter>', '--delimiter')
1474
    )
1475

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

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

    
1509

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

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

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

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

    
1555

    
1556
@command(pithos_cmds)
1557
class file_publish(_file_container_command):
1558
    """Publish the object and print the public url"""
1559

    
1560
    @errors.generic.all
1561
    @errors.pithos.connection
1562
    @errors.pithos.container
1563
    @errors.pithos.object_path
1564
    def _run(self):
1565
        self.writeln(self.client.publish_object(self.path))
1566

    
1567
    def main(self, container___path):
1568
        super(self.__class__, self)._run(
1569
            container___path, path_is_optional=False)
1570
        self._run()
1571

    
1572

    
1573
@command(pithos_cmds)
1574
class file_unpublish(_file_container_command, _optional_output_cmd):
1575
    """Unpublish an object"""
1576

    
1577
    @errors.generic.all
1578
    @errors.pithos.connection
1579
    @errors.pithos.container
1580
    @errors.pithos.object_path
1581
    def _run(self):
1582
            self._optional_output(self.client.unpublish_object(self.path))
1583

    
1584
    def main(self, container___path):
1585
        super(self.__class__, self)._run(
1586
            container___path, path_is_optional=False)
1587
        self._run()
1588

    
1589

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

    
1598

    
1599
@command(pithos_cmds)
1600
class file_permissions_get(_file_container_command, _optional_json):
1601
    """Get read and write permissions of an object"""
1602

    
1603
    def print_permissions(self, permissions_dict, out):
1604
        expected_keys = ('read', 'write')
1605
        if set(permissions_dict).issubset(expected_keys):
1606
            self.print_dict(permissions_dict, out)
1607
        else:
1608
            invalid_keys = set(permissions_dict.keys()).difference(
1609
                expected_keys)
1610
            raiseCLIError(
1611
                'Illegal permission keys: %s' % ', '.join(invalid_keys),
1612
                importance=1, details=[
1613
                    'Valid permission types: %s' % ' '.join(expected_keys)])
1614

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

    
1623
    def main(self, container___path):
1624
        super(self.__class__, self)._run(
1625
            container___path, path_is_optional=False)
1626
        self._run()
1627

    
1628

    
1629
@command(pithos_cmds)
1630
class file_permissions_set(_file_container_command, _optional_output_cmd):
1631
    """Set permissions for an object
1632
    New permissions overwrite existing permissions.
1633
    Permission format:
1634
    -   read=<username>[,usergroup[,...]]
1635
    -   write=<username>[,usegroup[,...]]
1636
    E.g. to give read permissions for file F to users A and B and write for C:
1637
    .       /file permissions set F read=A,B write=C
1638
    To share with everybody, use '*' instead of a user id or group.
1639
    E.g. to make file F available to all pithos users:
1640
    .   /file permissions set F read=*
1641
    E.g. to make file F available for editing to all pithos users:
1642
    .   /file permissions set F write=*
1643
    """
1644

    
1645
    @errors.generic.all
1646
    def format_permission_dict(self, permissions):
1647
        read, write = False, False
1648
        for perms in permissions:
1649
            splstr = perms.split('=')
1650
            if 'read' == splstr[0]:
1651
                read = [ug.strip() for ug in splstr[1].split(',')]
1652
            elif 'write' == splstr[0]:
1653
                write = [ug.strip() for ug in splstr[1].split(',')]
1654
            else:
1655
                msg = 'Usage:\tread=<groups,users> write=<groups,users>'
1656
                raiseCLIError(None, msg)
1657
        return (read, write)
1658

    
1659
    @errors.generic.all
1660
    @errors.pithos.connection
1661
    @errors.pithos.container
1662
    @errors.pithos.object_path
1663
    def _run(self, read, write):
1664
        self._optional_output(self.client.set_object_sharing(
1665
            self.path, read_permission=read, write_permission=write))
1666

    
1667
    def main(self, container___path, *permissions):
1668
        super(self.__class__, self)._run(
1669
            container___path, path_is_optional=False)
1670
        read, write = self.format_permission_dict(permissions)
1671
        self._run(read, write)
1672

    
1673

    
1674
@command(pithos_cmds)
1675
class file_permissions_delete(_file_container_command, _optional_output_cmd):
1676
    """Delete all permissions set on object
1677
    To modify permissions, use /file permissions set
1678
    """
1679

    
1680
    @errors.generic.all
1681
    @errors.pithos.connection
1682
    @errors.pithos.container
1683
    @errors.pithos.object_path
1684
    def _run(self):
1685
        self._optional_output(self.client.del_object_sharing(self.path))
1686

    
1687
    def main(self, container___path):
1688
        super(self.__class__, self)._run(
1689
            container___path, path_is_optional=False)
1690
        self._run()
1691

    
1692

    
1693
@command(pithos_cmds)
1694
class file_info(_file_container_command, _optional_json):
1695
    """Get detailed information for user account, containers or objects
1696
    to get account info:    /file info
1697
    to get container info:  /file info <container>
1698
    to get object info:     /file info <container>:<path>
1699
    """
1700

    
1701
    arguments = dict(
1702
        object_version=ValueArgument(
1703
            'show specific version \ (applies only for objects)',
1704
            ('-O', '--object-version'))
1705
    )
1706

    
1707
    @errors.generic.all
1708
    @errors.pithos.connection
1709
    @errors.pithos.container
1710
    @errors.pithos.object_path
1711
    def _run(self):
1712
        if self.container is None:
1713
            r = self.client.get_account_info()
1714
        elif self.path is None:
1715
            r = self.client.get_container_info(self.container)
1716
        else:
1717
            r = self.client.get_object_info(
1718
                self.path, version=self['object_version'])
1719
        self._print(r, self.print_dict)
1720

    
1721
    def main(self, container____path__=None):
1722
        super(self.__class__, self)._run(container____path__)
1723
        self._run()
1724

    
1725

    
1726
@command(pithos_cmds)
1727
class file_metadata(_pithos_init):
1728
    """Metadata are attached on objects. They are formed as key:value pairs.
1729
    They can have arbitary values.
1730
    """
1731

    
1732

    
1733
@command(pithos_cmds)
1734
class file_metadata_get(_file_container_command, _optional_json):
1735
    """Get metadata for account, containers or objects"""
1736

    
1737
    arguments = dict(
1738
        detail=FlagArgument('show detailed output', ('-l', '--details')),
1739
        until=DateArgument('show metadata until then', '--until'),
1740
        object_version=ValueArgument(
1741
            'show specific version (applies only for objects)',
1742
            ('-O', '--object-version'))
1743
    )
1744

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

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

    
1781

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

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

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

    
1803

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

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

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

    
1829

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

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

    
1838
    @errors.generic.all
1839
    @errors.pithos.connection
1840
    def _run(self):
1841

    
1842
        def pretty_print(output, **kwargs):
1843
            if not self['in_bytes']:
1844
                for k in output:
1845
                    output[k] = format_size(output[k])
1846
            self.print_dict(output, '-', **kwargs)
1847

    
1848
        self._print(self.client.get_account_quota(), pretty_print)
1849

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

    
1854

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

    
1859

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

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

    
1868
    @errors.generic.all
1869
    @errors.pithos.container
1870
    def _run(self):
1871

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

    
1878
        self._print(
1879
            self.client.get_container_limit(self.container), pretty_print)
1880

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

    
1886

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

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

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

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

    
1935

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

    
1940

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

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

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

    
1958

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

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

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

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

    
1981

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

    
1986

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

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

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

    
2001

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

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

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

    
2018

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

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

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

    
2032

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

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

    
2042
    @errors.generic.all
2043
    @errors.pithos.connection
2044
    def _run(self):
2045
        accounts = self.client.get_sharing_accounts(marker=self['marker'])
2046
        if not self['json_output']:
2047
            usernames = self._uuids2usernames(
2048
                [acc['name'] for acc in accounts])
2049
            for item in accounts:
2050
                uuid = item['name']
2051
                item['id'], item['name'] = uuid, usernames[uuid]
2052
                if not self['detail']:
2053
                    item.pop('last_modified')
2054
        self._print(accounts)
2055

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

    
2060

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

    
2071
    def version_print(self, versions, out):
2072
        self.print_items(
2073
            [dict(id=vitem[0], created=strftime(
2074
                '%d-%m-%Y %H:%M:%S',
2075
                localtime(float(vitem[1])))) for vitem in versions],
2076
            out=out)
2077

    
2078
    @errors.generic.all
2079
    @errors.pithos.connection
2080
    @errors.pithos.container
2081
    @errors.pithos.object_path
2082
    def _run(self):
2083
        self._print(
2084
            self.client.get_object_versionlist(self.path), self.version_print)
2085

    
2086
    def main(self, container___path):
2087
        super(file_versions, self)._run(
2088
            container___path, path_is_optional=False)
2089
        self._run()