Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / pithos.py @ 5655d560

History | View | Annotate | Download (78.1 kB)

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

    
34
from sys import stdout
35
from time import localtime, strftime
36
from os import path, makedirs, walk
37

    
38
from kamaki.cli import command
39
from kamaki.cli.command_tree import CommandTree
40
from kamaki.cli.errors import raiseCLIError, CLISyntaxError
41
from kamaki.cli.utils import (
42
    format_size, to_bytes, print_dict, print_items, pretty_keys,
43
    page_hold, bold, ask_user, get_path_size, print_json)
44
from kamaki.cli.argument import FlagArgument, ValueArgument, IntArgument
45
from kamaki.cli.argument import KeyValueArgument, DateArgument
46
from kamaki.cli.argument import ProgressBarArgument
47
from kamaki.cli.commands import _command_init, errors
48
from kamaki.clients.pithos import PithosClient, ClientError
49
from kamaki.clients.astakos import AstakosClient
50

    
51
pithos_cmds = CommandTree('file', 'Pithos+/Storage API commands')
52
_commands = [pithos_cmds]
53

    
54

    
55
# Argument functionality
56

    
57
class DelimiterArgument(ValueArgument):
58
    """
59
    :value type: string
60
    :value returns: given string or /
61
    """
62

    
63
    def __init__(self, caller_obj, help='', parsed_name=None, default=None):
64
        super(DelimiterArgument, self).__init__(help, parsed_name, default)
65
        self.caller_obj = caller_obj
66

    
67
    @property
68
    def value(self):
69
        if self.caller_obj['recursive']:
70
            return '/'
71
        return getattr(self, '_value', self.default)
72

    
73
    @value.setter
74
    def value(self, newvalue):
75
        self._value = newvalue
76

    
77

    
78
class SharingArgument(ValueArgument):
79
    """Set sharing (read and/or write) groups
80
    .
81
    :value type: "read=term1,term2,... write=term1,term2,..."
82
    .
83
    :value returns: {'read':['term1', 'term2', ...],
84
    .   'write':['term1', 'term2', ...]}
85
    """
86

    
87
    @property
88
    def value(self):
89
        return getattr(self, '_value', self.default)
90

    
91
    @value.setter
92
    def value(self, newvalue):
93
        perms = {}
94
        try:
95
            permlist = newvalue.split(' ')
96
        except AttributeError:
97
            return
98
        for p in permlist:
99
            try:
100
                (key, val) = p.split('=')
101
            except ValueError as err:
102
                raiseCLIError(
103
                    err,
104
                    'Error in --sharing',
105
                    details='Incorrect format',
106
                    importance=1)
107
            if key.lower() not in ('read', 'write'):
108
                msg = 'Error in --sharing'
109
                raiseCLIError(err, msg, importance=1, details=[
110
                    'Invalid permission key %s' % key])
111
            val_list = val.split(',')
112
            if not key in perms:
113
                perms[key] = []
114
            for item in val_list:
115
                if item not in perms[key]:
116
                    perms[key].append(item)
117
        self._value = perms
118

    
119

    
120
class RangeArgument(ValueArgument):
121
    """
122
    :value type: string of the form <start>-<end> where <start> and <end> are
123
        integers
124
    :value returns: the input string, after type checking <start> and <end>
125
    """
126

    
127
    @property
128
    def value(self):
129
        return getattr(self, '_value', self.default)
130

    
131
    @value.setter
132
    def value(self, newvalue):
133
        if newvalue is None:
134
            self._value = self.default
135
            return
136
        (start, end) = newvalue.split('-')
137
        (start, end) = (int(start), int(end))
138
        self._value = '%s-%s' % (start, end)
139

    
140
# Command specs
141

    
142

    
143
class _pithos_init(_command_init):
144
    """Initialize a pithos+ kamaki client"""
145

    
146
    @staticmethod
147
    def _is_dir(remote_dict):
148
        return 'application/directory' == remote_dict.get(
149
            'content_type',
150
            remote_dict.get('content-type', ''))
151

    
152
    @errors.generic.all
153
    def _run(self):
154
        self.token = self.config.get('file', 'token')\
155
            or self.config.get('global', 'token')
156
        self.base_url = self.config.get('file', 'url')\
157
            or self.config.get('global', 'url')
158
        self._set_account()
159
        self.container = self.config.get('file', 'container')\
160
            or self.config.get('global', 'container')
161
        self.client = PithosClient(
162
            base_url=self.base_url,
163
            token=self.token,
164
            account=self.account,
165
            container=self.container)
166
        self._set_log_params()
167
        self._update_max_threads()
168

    
169
    def main(self):
170
        self._run()
171

    
172
    def _set_account(self):
173
        user = AstakosClient(self.config.get('user', 'url'), self.token)
174
        self.account = self['account'] or user.term('uuid')
175

    
176
        """Backwards compatibility"""
177
        self.account = self.account\
178
            or self.config.get('file', 'account')\
179
            or self.config.get('global', 'account')
180

    
181

    
182
class _file_account_command(_pithos_init):
183
    """Base class for account level storage commands"""
184

    
185
    def __init__(self, arguments={}):
186
        super(_file_account_command, self).__init__(arguments)
187
        self['account'] = ValueArgument(
188
            'Set user account (not permanent)',
189
            ('-A', '--account'))
190

    
191
    def _run(self, custom_account=None):
192
        super(_file_account_command, self)._run()
193
        if custom_account:
194
            self.client.account = custom_account
195
        elif self['account']:
196
            self.client.account = self['account']
197

    
198
    @errors.generic.all
199
    def main(self):
200
        self._run()
201

    
202

    
203
class _file_container_command(_file_account_command):
204
    """Base class for container level storage commands"""
205

    
206
    container = None
207
    path = None
208

    
209
    def __init__(self, arguments={}):
210
        super(_file_container_command, self).__init__(arguments)
211
        self['container'] = ValueArgument(
212
            'Set container to work with (temporary)',
213
            ('-C', '--container'))
214

    
215
    def extract_container_and_path(
216
            self,
217
            container_with_path,
218
            path_is_optional=True):
219
        """Contains all heuristics for deciding what should be used as
220
        container or path. Options are:
221
        * user string of the form container:path
222
        * self.container, self.path variables set by super constructor, or
223
        explicitly by the caller application
224
        Error handling is explicit as these error cases happen only here
225
        """
226
        try:
227
            assert isinstance(container_with_path, str)
228
        except AssertionError as err:
229
            if self['container'] and path_is_optional:
230
                self.container = self['container']
231
                self.client.container = self['container']
232
                return
233
            raiseCLIError(err)
234

    
235
        user_cont, sep, userpath = container_with_path.partition(':')
236

    
237
        if sep:
238
            if not user_cont:
239
                raiseCLIError(CLISyntaxError(
240
                    'Container is missing\n',
241
                    details=errors.pithos.container_howto))
242
            alt_cont = self['container']
243
            if alt_cont and user_cont != alt_cont:
244
                raiseCLIError(CLISyntaxError(
245
                    'Conflict: 2 containers (%s, %s)' % (user_cont, alt_cont),
246
                    details=errors.pithos.container_howto)
247
                )
248
            self.container = user_cont
249
            if not userpath:
250
                raiseCLIError(CLISyntaxError(
251
                    'Path is missing for object in container %s' % user_cont,
252
                    details=errors.pithos.container_howto)
253
                )
254
            self.path = userpath
255
        else:
256
            alt_cont = self['container'] or self.client.container
257
            if alt_cont:
258
                self.container = alt_cont
259
                self.path = user_cont
260
            elif path_is_optional:
261
                self.container = user_cont
262
                self.path = None
263
            else:
264
                self.container = user_cont
265
                raiseCLIError(CLISyntaxError(
266
                    'Both container and path are required',
267
                    details=errors.pithos.container_howto)
268
                )
269

    
270
    @errors.generic.all
271
    def _run(self, container_with_path=None, path_is_optional=True):
272
        super(_file_container_command, self)._run()
273
        if self['container']:
274
            self.client.container = self['container']
275
            if container_with_path:
276
                self.path = container_with_path
277
            elif not path_is_optional:
278
                raise CLISyntaxError(
279
                    'Both container and path are required',
280
                    details=errors.pithos.container_howto)
281
        elif container_with_path:
282
            self.extract_container_and_path(
283
                container_with_path,
284
                path_is_optional)
285
            self.client.container = self.container
286
        self.container = self.client.container
287

    
288
    def main(self, container_with_path=None, path_is_optional=True):
289
        self._run(container_with_path, path_is_optional)
290

    
291

    
292
@command(pithos_cmds)
293
class file_list(_file_container_command):
294
    """List containers, object trees or objects in a directory
295
    Use with:
296
    1 no parameters : containers in current account
297
    2. one parameter (container) or --container : contents of container
298
    3. <container>:<prefix> or --container=<container> <prefix>: objects in
299
    .   container starting with prefix
300
    """
301

    
302
    arguments = dict(
303
        detail=FlagArgument('detailed output', ('-l', '--list')),
304
        limit=IntArgument('limit number of listed items', ('-n', '--number')),
305
        marker=ValueArgument('output greater that marker', '--marker'),
306
        prefix=ValueArgument('output starting with prefix', '--prefix'),
307
        delimiter=ValueArgument('show output up to delimiter', '--delimiter'),
308
        path=ValueArgument(
309
            'show output starting with prefix up to /',
310
            '--path'),
311
        meta=ValueArgument(
312
            'show output with specified meta keys',
313
            '--meta',
314
            default=[]),
315
        if_modified_since=ValueArgument(
316
            'show output modified since then',
317
            '--if-modified-since'),
318
        if_unmodified_since=ValueArgument(
319
            'show output not modified since then',
320
            '--if-unmodified-since'),
321
        until=DateArgument('show metadata until then', '--until'),
322
        format=ValueArgument(
323
            'format to parse until data (default: d/m/Y H:M:S )',
324
            '--format'),
325
        shared=FlagArgument('show only shared', '--shared'),
326
        more=FlagArgument(
327
            'output results in pages (-n to set items per page, default 10)',
328
            '--more'),
329
        exact_match=FlagArgument(
330
            'Show only objects that match exactly with path',
331
            '--exact-match'),
332
        enum=FlagArgument('Enumerate results', '--enumerate'),
333
        json_output=FlagArgument('show output in json', ('-j', '--json'))
334
    )
335

    
336
    def print_objects(self, object_list):
337
        if self['json_output']:
338
            print_json(object_list)
339
            return
340
        limit = int(self['limit']) if self['limit'] > 0 else len(object_list)
341
        for index, obj in enumerate(object_list):
342
            if self['exact_match'] and self.path and not (
343
                    obj['name'] == self.path or 'content_type' in obj):
344
                continue
345
            pretty_obj = obj.copy()
346
            index += 1
347
            empty_space = ' ' * (len(str(len(object_list))) - len(str(index)))
348
            if obj['content_type'] == 'application/directory':
349
                isDir = True
350
                size = 'D'
351
            else:
352
                isDir = False
353
                size = format_size(obj['bytes'])
354
                pretty_obj['bytes'] = '%s (%s)' % (obj['bytes'], size)
355
            oname = bold(obj['name'])
356
            prfx = ('%s%s. ' % (empty_space, index)) if self['enum'] else ''
357
            if self['detail']:
358
                print('%s%s' % (prfx, oname))
359
                print_dict(pretty_keys(pretty_obj), exclude=('name'))
360
                print
361
            else:
362
                oname = '%s%9s %s' % (prfx, size, oname)
363
                oname += '/' if isDir else ''
364
                print(oname)
365
            if self['more']:
366
                page_hold(index, limit, len(object_list))
367

    
368
    def print_containers(self, container_list):
369
        if self['json_output']:
370
            print_json(container_list)
371
            return
372
        limit = int(self['limit']) if self['limit'] > 0\
373
            else len(container_list)
374
        for index, container in enumerate(container_list):
375
            if 'bytes' in container:
376
                size = format_size(container['bytes'])
377
            prfx = ('%s. ' % (index + 1)) if self['enum'] else ''
378
            cname = '%s%s' % (prfx, bold(container['name']))
379
            if self['detail']:
380
                print(cname)
381
                pretty_c = container.copy()
382
                if 'bytes' in container:
383
                    pretty_c['bytes'] = '%s (%s)' % (container['bytes'], size)
384
                print_dict(pretty_keys(pretty_c), exclude=('name'))
385
                print
386
            else:
387
                if 'count' in container and 'bytes' in container:
388
                    print('%s (%s, %s objects)' % (
389
                        cname,
390
                        size,
391
                        container['count']))
392
                else:
393
                    print(cname)
394
            if self['more']:
395
                page_hold(index + 1, limit, len(container_list))
396

    
397
    @errors.generic.all
398
    @errors.pithos.connection
399
    @errors.pithos.object_path
400
    @errors.pithos.container
401
    def _run(self):
402
        if self.container is None:
403
            r = self.client.account_get(
404
                limit=False if self['more'] else self['limit'],
405
                marker=self['marker'],
406
                if_modified_since=self['if_modified_since'],
407
                if_unmodified_since=self['if_unmodified_since'],
408
                until=self['until'],
409
                show_only_shared=self['shared'])
410
            self.print_containers(r.json)
411
        else:
412
            prefix = self.path or self['prefix']
413
            r = self.client.container_get(
414
                limit=False if self['more'] else self['limit'],
415
                marker=self['marker'],
416
                prefix=prefix,
417
                delimiter=self['delimiter'],
418
                path=self['path'],
419
                if_modified_since=self['if_modified_since'],
420
                if_unmodified_since=self['if_unmodified_since'],
421
                until=self['until'],
422
                meta=self['meta'],
423
                show_only_shared=self['shared'])
424
            self.print_objects(r.json)
425

    
426
    def main(self, container____path__=None):
427
        super(self.__class__, self)._run(container____path__)
428
        self._run()
429

    
430

    
431
@command(pithos_cmds)
432
class file_mkdir(_file_container_command):
433
    """Create a directory"""
434

    
435
    arguments = dict(
436
        with_output=FlagArgument('show response headers', ('--with-output')),
437
        json_output=FlagArgument('show headers in json', ('-j', '--json'))
438
    )
439

    
440
    __doc__ += '\n. '.join([
441
        'Kamaki hanldes directories the same way as OOS Storage and Pithos+:',
442
        'A   directory  is   an  object  with  type  "application/directory"',
443
        'An object with path  dir/name can exist even if  dir does not exist',
444
        'or even if dir  is  a non  directory  object.  Users can modify dir',
445
        'without affecting the dir/name object in any way.'])
446

    
447
    @errors.generic.all
448
    @errors.pithos.connection
449
    @errors.pithos.container
450
    def _run(self):
451
        r = self.client.create_directory(self.path)
452
        if self['json_output']:
453
            print_json(r)
454
        elif self['with_output']:
455
            print_dict(r)
456

    
457
    def main(self, container___directory):
458
        super(self.__class__, self)._run(
459
            container___directory,
460
            path_is_optional=False)
461
        self._run()
462

    
463

    
464
@command(pithos_cmds)
465
class file_touch(_file_container_command):
466
    """Create an empty object (file)
467
    If object exists, this command will reset it to 0 length
468
    """
469

    
470
    arguments = dict(
471
        content_type=ValueArgument(
472
            'Set content type (default: application/octet-stream)',
473
            '--content-type',
474
            default='application/octet-stream'),
475
        with_output=FlagArgument('show response headers', ('--with-output')),
476
        json_output=FlagArgument('show headers in json', ('-j', '--json'))
477
    )
478

    
479
    @errors.generic.all
480
    @errors.pithos.connection
481
    @errors.pithos.container
482
    def _run(self):
483
        r = self.client.create_object(self.path, self['content_type'])
484
        if self['json_output']:
485
            print_json(r)
486
        elif self['with_output']:
487
            print_dict(r)
488

    
489
    def main(self, container___path):
490
        super(file_touch, self)._run(
491
            container___path,
492
            path_is_optional=False)
493
        self._run()
494

    
495

    
496
@command(pithos_cmds)
497
class file_create(_file_container_command):
498
    """Create a container"""
499

    
500
    arguments = dict(
501
        versioning=ValueArgument(
502
            'set container versioning (auto/none)',
503
            '--versioning'),
504
        limit=IntArgument('set default container limit', '--limit'),
505
        meta=KeyValueArgument(
506
            'set container metadata (can be repeated)',
507
            '--meta'),
508
        with_output=FlagArgument('show response headers', ('--with-output')),
509
        json_output=FlagArgument('show headers in json', ('-j', '--json'))
510
    )
511

    
512
    @errors.generic.all
513
    @errors.pithos.connection
514
    @errors.pithos.container
515
    def _run(self, container):
516
        r = self.client.create_container(
517
            container=container, sizelimit=self['limit'],
518
            versioning=self['versioning'], metadata=self['meta'])
519
        if self['json_output']:
520
            print_json(r)
521
        elif self['with_output']:
522
            print_dict(r)
523

    
524
    def main(self, container=None):
525
        super(self.__class__, self)._run(container)
526
        if container and self.container != container:
527
            raiseCLIError('Invalid container name %s' % container, details=[
528
                'Did you mean "%s" ?' % self.container,
529
                'Use --container for names containing :'])
530
        self._run(container)
531

    
532

    
533
class _source_destination_command(_file_container_command):
534

    
535
    arguments = dict(
536
        destination_account=ValueArgument('', ('a', '--dst-account')),
537
        recursive=FlagArgument('', ('-R', '--recursive')),
538
        prefix=FlagArgument('', '--with-prefix', default=''),
539
        suffix=ValueArgument('', '--with-suffix', default=''),
540
        add_prefix=ValueArgument('', '--add-prefix', default=''),
541
        add_suffix=ValueArgument('', '--add-suffix', default=''),
542
        prefix_replace=ValueArgument('', '--prefix-to-replace', default=''),
543
        suffix_replace=ValueArgument('', '--suffix-to-replace', default=''),
544
    )
545

    
546
    def __init__(self, arguments={}):
547
        self.arguments.update(arguments)
548
        super(_source_destination_command, self).__init__(self.arguments)
549

    
550
    def _run(self, source_container___path, path_is_optional=False):
551
        super(_source_destination_command, self)._run(
552
            source_container___path,
553
            path_is_optional)
554
        self.dst_client = PithosClient(
555
            base_url=self.client.base_url,
556
            token=self.client.token,
557
            account=self['destination_account'] or self.client.account)
558

    
559
    @errors.generic.all
560
    @errors.pithos.account
561
    def _dest_container_path(self, dest_container_path):
562
        if self['destination_container']:
563
            self.dst_client.container = self['destination_container']
564
            return (self['destination_container'], dest_container_path)
565
        if dest_container_path:
566
            dst = dest_container_path.split(':')
567
            if len(dst) > 1:
568
                try:
569
                    self.dst_client.container = dst[0]
570
                    self.dst_client.get_container_info(dst[0])
571
                except ClientError as err:
572
                    if err.status in (404, 204):
573
                        raiseCLIError(
574
                            'Destination container %s not found' % dst[0])
575
                    raise
576
                else:
577
                    self.dst_client.container = dst[0]
578
                return (dst[0], dst[1])
579
            return(None, dst[0])
580
        raiseCLIError('No destination container:path provided')
581

    
582
    def _get_all(self, prefix):
583
        return self.client.container_get(prefix=prefix).json
584

    
585
    def _get_src_objects(self, src_path, source_version=None):
586
        """Get a list of the source objects to be called
587

588
        :param src_path: (str) source path
589

590
        :returns: (method, params) a method that returns a list when called
591
        or (object) if it is a single object
592
        """
593
        if src_path and src_path[-1] == '/':
594
            src_path = src_path[:-1]
595

    
596
        if self['prefix']:
597
            return (self._get_all, dict(prefix=src_path))
598
        try:
599
            srcobj = self.client.get_object_info(
600
                src_path, version=source_version)
601
        except ClientError as srcerr:
602
            if srcerr.status == 404:
603
                raiseCLIError(
604
                    'Source object %s not in source container %s' % (
605
                        src_path,
606
                        self.client.container),
607
                    details=['Hint: --with-prefix to match multiple objects'])
608
            elif srcerr.status not in (204,):
609
                raise
610
            return (self.client.list_objects, {})
611

    
612
        if self._is_dir(srcobj):
613
            if not self['recursive']:
614
                raiseCLIError(
615
                    'Object %s of cont. %s is a dir' % (
616
                        src_path,
617
                        self.client.container),
618
                    details=['Use --recursive to access directories'])
619
            return (self._get_all, dict(prefix=src_path))
620
        srcobj['name'] = src_path
621
        return srcobj
622

    
623
    def src_dst_pairs(self, dst_path, source_version=None):
624
        src_iter = self._get_src_objects(self.path, source_version)
625
        src_N = isinstance(src_iter, tuple)
626
        add_prefix = self['add_prefix'].strip('/')
627

    
628
        if dst_path and dst_path.endswith('/'):
629
            dst_path = dst_path[:-1]
630

    
631
        try:
632
            dstobj = self.dst_client.get_object_info(dst_path)
633
        except ClientError as trgerr:
634
            if trgerr.status in (404,):
635
                if src_N:
636
                    raiseCLIError(
637
                        'Cannot merge multiple paths to path %s' % dst_path,
638
                        details=[
639
                            'Try to use / or a directory as destination',
640
                            'or create the destination dir (/file mkdir)',
641
                            'or use a single object as source'])
642
            elif trgerr.status not in (204,):
643
                raise
644
        else:
645
            if self._is_dir(dstobj):
646
                add_prefix = '%s/%s' % (dst_path.strip('/'), add_prefix)
647
            elif src_N:
648
                raiseCLIError(
649
                    'Cannot merge multiple paths to path' % 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

    
655
        if src_N:
656
            (method, kwargs) = src_iter
657
            for obj in method(**kwargs):
658
                name = obj['name']
659
                if name.endswith(self['suffix']):
660
                    yield (name, self._get_new_object(name, add_prefix))
661
        elif src_iter['name'].endswith(self['suffix']):
662
            name = src_iter['name']
663
            yield (name, self._get_new_object(dst_path or name, add_prefix))
664
        else:
665
            raiseCLIError('Source path %s conflicts with suffix %s' % (
666
                src_iter['name'],
667
                self['suffix']))
668

    
669
    def _get_new_object(self, obj, add_prefix):
670
        if self['prefix_replace'] and obj.startswith(self['prefix_replace']):
671
            obj = obj[len(self['prefix_replace']):]
672
        if self['suffix_replace'] and obj.endswith(self['suffix_replace']):
673
            obj = obj[:-len(self['suffix_replace'])]
674
        return add_prefix + obj + self['add_suffix']
675

    
676

    
677
@command(pithos_cmds)
678
class file_copy(_source_destination_command):
679
    """Copy objects from container to (another) container
680
    Semantics:
681
    copy cont:path dir
682
    .   transfer path as dir/path
683
    copy cont:path cont2:
684
    .   trasnfer all <obj> prefixed with path to container cont2
685
    copy cont:path [cont2:]path2
686
    .   transfer path to path2
687
    Use options:
688
    1. <container1>:<path1> [container2:]<path2> : if container2 is not given,
689
    destination is container1:path2
690
    2. <container>:<path1> <path2> : make a copy in the same container
691
    3. Can use --container= instead of <container1>
692
    """
693

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

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

    
762

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

    
774

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

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

    
828
    @errors.generic.all
829
    @errors.pithos.connection
830
    @errors.pithos.container
831
    def _run(self, dst_path):
832
        no_source_object = True
833
        src_account = self.client.account if (
834
            self['destination_account']) else None
835
        for src_obj, dst_obj in self.src_dst_pairs(dst_path):
836
            no_source_object = False
837
            r = self.dst_client.move_object(
838
                src_container=self.container,
839
                src_object=src_obj,
840
                dst_container=self.dst_client.container,
841
                dst_object=dst_obj,
842
                source_account=src_account,
843
                public=self['public'],
844
                content_type=self['content_type'])
845
        if no_source_object:
846
            raiseCLIError('No object %s in container %s' % (
847
                self.path,
848
                self.container))
849
        if self['json_output']:
850
            print_json(r)
851
        elif self['with_output']:
852
            print_dict(r)
853

    
854
    def main(
855
            self, source_container___path,
856
            destination_container___path=None):
857
        super(self.__class__, self)._run(
858
            source_container___path,
859
            path_is_optional=False)
860
        (dst_cont, dst_path) = self._dest_container_path(
861
            destination_container___path)
862
        (dst_cont, dst_path) = self._dest_container_path(
863
            destination_container___path)
864
        self.dst_client.container = dst_cont or self.container
865
        self._run(dst_path=dst_path or '')
866

    
867

    
868
@command(pithos_cmds)
869
class file_append(_file_container_command):
870
    """Append local file to (existing) remote object
871
    The remote object should exist.
872
    If the remote object is a directory, it is transformed into a file.
873
    In the later case, objects under the directory remain intact.
874
    """
875

    
876
    arguments = dict(
877
        progress_bar=ProgressBarArgument(
878
            'do not show progress bar',
879
            ('-N', '--no-progress-bar'),
880
            default=False),
881
        with_output=FlagArgument('show response headers', ('--with-output')),
882
        json_output=FlagArgument('show headers in json', ('-j', '--json'))
883
    )
884

    
885
    @errors.generic.all
886
    @errors.pithos.connection
887
    @errors.pithos.container
888
    @errors.pithos.object_path
889
    def _run(self, local_path):
890
        (progress_bar, upload_cb) = self._safe_progress_bar('Appending')
891
        try:
892
            f = open(local_path, 'rb')
893
            r = self.client.append_object(self.path, f, upload_cb)
894
            if self['json_output']:
895
                print_json(r)
896
            elif self['with_output']:
897
                print_items(r)
898
        except Exception:
899
            self._safe_progress_bar_finish(progress_bar)
900
            raise
901
        finally:
902
            self._safe_progress_bar_finish(progress_bar)
903

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

    
910

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

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

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

    
927

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

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

    
945
    def _open_file(self, local_path, start):
946
        f = open(path.abspath(local_path), 'rb')
947
        f.seek(0, 2)
948
        f_size = f.tell()
949
        f.seek(start, 0)
950
        return (f, f_size)
951

    
952
    @errors.generic.all
953
    @errors.pithos.connection
954
    @errors.pithos.container
955
    @errors.pithos.object_path
956
    @errors.pithos.object_size
957
    def _run(self, local_path, start, end):
958
        (start, end) = (int(start), int(end))
959
        (f, f_size) = self._open_file(local_path, start)
960
        (progress_bar, upload_cb) = self._safe_progress_bar(
961
            'Overwrite %s bytes' % (end - start))
962
        try:
963
            self.client.overwrite_object(
964
                obj=self.path,
965
                start=start,
966
                end=end,
967
                source_file=f,
968
                upload_cb=upload_cb)
969
        except Exception:
970
            self._safe_progress_bar_finish(progress_bar)
971
            raise
972
        finally:
973
            self._safe_progress_bar_finish(progress_bar)
974

    
975
    def main(self, local_path, container___path, start, end):
976
        super(self.__class__, self)._run(
977
            container___path,
978
            path_is_optional=None)
979
        self.path = self.path or path.basename(local_path)
980
        self._run(local_path=local_path, start=start, end=end)
981

    
982

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

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

    
1016
    @errors.generic.all
1017
    @errors.pithos.connection
1018
    @errors.pithos.container
1019
    @errors.pithos.object_path
1020
    def _run(self):
1021
        self.client.create_object_by_manifestation(
1022
            self.path,
1023
            content_encoding=self['content_encoding'],
1024
            content_disposition=self['content_disposition'],
1025
            content_type=self['content_type'],
1026
            sharing=self['sharing'],
1027
            public=self['public'])
1028

    
1029
    def main(self, container___path):
1030
        super(self.__class__, self)._run(
1031
            container___path,
1032
            path_is_optional=False)
1033
        self.run()
1034

    
1035

    
1036
@command(pithos_cmds)
1037
class file_upload(_file_container_command):
1038
    """Upload a file"""
1039

    
1040
    arguments = dict(
1041
        use_hashes=FlagArgument(
1042
            'provide hashmap file instead of data',
1043
            '--use-hashes'),
1044
        etag=ValueArgument('check written data', '--etag'),
1045
        unchunked=FlagArgument('avoid chunked transfer mode', '--unchunked'),
1046
        content_encoding=ValueArgument(
1047
            'set MIME content type',
1048
            '--content-encoding'),
1049
        content_disposition=ValueArgument(
1050
            'specify objects presentation style',
1051
            '--content-disposition'),
1052
        content_type=ValueArgument('specify content type', '--content-type'),
1053
        sharing=SharingArgument(
1054
            help='\n'.join([
1055
                'define sharing object policy',
1056
                '( "read=user1,grp1,user2,... write=user1,grp2,... )']),
1057
            parsed_name='--sharing'),
1058
        public=FlagArgument('make object publicly accessible', '--public'),
1059
        poolsize=IntArgument('set pool size', '--with-pool-size'),
1060
        progress_bar=ProgressBarArgument(
1061
            'do not show progress bar',
1062
            ('-N', '--no-progress-bar'),
1063
            default=False),
1064
        overwrite=FlagArgument('Force (over)write', ('-f', '--force')),
1065
        recursive=FlagArgument(
1066
            'Recursively upload directory *contents* + subdirectories',
1067
            ('-R', '--recursive')),
1068
        with_output=FlagArgument(
1069
            'Show uploaded objects response headers',
1070
            ('--with-output')),
1071
        json_output=FlagArgument('show headers in json', ('-j', '--json'))
1072
    )
1073

    
1074
    def _check_container_limit(self, path):
1075
        cl_dict = self.client.get_container_limit()
1076
        container_limit = int(cl_dict['x-container-policy-quota'])
1077
        r = self.client.container_get()
1078
        used_bytes = sum(int(o['bytes']) for o in r.json)
1079
        path_size = get_path_size(path)
1080
        if container_limit and path_size > (container_limit - used_bytes):
1081
            raiseCLIError(
1082
                'Container(%s) (limit(%s) - used(%s)) < size(%s) of %s' % (
1083
                    self.client.container,
1084
                    format_size(container_limit),
1085
                    format_size(used_bytes),
1086
                    format_size(path_size),
1087
                    path),
1088
                importance=1, details=[
1089
                    'Check accound limit: /file quota',
1090
                    'Check container limit:',
1091
                    '\t/file containerlimit get %s' % self.client.container,
1092
                    'Increase container limit:',
1093
                    '\t/file containerlimit set <new limit> %s' % (
1094
                        self.client.container)])
1095

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

    
1160
    @errors.generic.all
1161
    @errors.pithos.connection
1162
    @errors.pithos.container
1163
    @errors.pithos.object_path
1164
    @errors.pithos.local_path
1165
    def _run(self, local_path, remote_path):
1166
        poolsize = self['poolsize']
1167
        if poolsize > 0:
1168
            self.client.MAX_THREADS = int(poolsize)
1169
        params = dict(
1170
            content_encoding=self['content_encoding'],
1171
            content_type=self['content_type'],
1172
            content_disposition=self['content_disposition'],
1173
            sharing=self['sharing'],
1174
            public=self['public'])
1175
        uploaded = []
1176
        container_info_cache = dict()
1177
        for f, rpath in self._path_pairs(local_path, remote_path):
1178
            print('%s --> %s:%s' % (f.name, self.client.container, rpath))
1179
            if self['unchunked']:
1180
                r = self.client.upload_object_unchunked(
1181
                    rpath, f,
1182
                    etag=self['etag'], withHashFile=self['use_hashes'],
1183
                    **params)
1184
                if self['with_output'] or self['json_output']:
1185
                    r['name'] = '%s: %s' % (self.client.container, rpath)
1186
                    uploaded.append(r)
1187
            else:
1188
                try:
1189
                    (progress_bar, upload_cb) = self._safe_progress_bar(
1190
                        'Uploading %s' % f.name.split(path.sep)[-1])
1191
                    if progress_bar:
1192
                        hash_bar = progress_bar.clone()
1193
                        hash_cb = hash_bar.get_generator(
1194
                            'Calculating block hashes')
1195
                    else:
1196
                        hash_cb = None
1197
                    r = self.client.upload_object(
1198
                        rpath, f,
1199
                        hash_cb=hash_cb,
1200
                        upload_cb=upload_cb,
1201
                        container_info_cache=container_info_cache,
1202
                        **params)
1203
                    if self['with_output'] or self['json_output']:
1204
                        r['name'] = '%s: %s' % (self.client.container, rpath)
1205
                        uploaded.append(r)
1206
                except Exception:
1207
                    self._safe_progress_bar_finish(progress_bar)
1208
                    raise
1209
                finally:
1210
                    self._safe_progress_bar_finish(progress_bar)
1211
        if self['json_output']:
1212
            print_json(uploaded)
1213
        elif self['with_output']:
1214
            print_items(uploaded)
1215
        else:
1216
            print('Upload completed')
1217

    
1218
    def main(self, local_path, container____path__=None):
1219
        super(self.__class__, self)._run(container____path__)
1220
        remote_path = self.path or path.basename(local_path)
1221
        self._run(local_path=local_path, remote_path=remote_path)
1222

    
1223

    
1224
@command(pithos_cmds)
1225
class file_cat(_file_container_command):
1226
    """Print remote file contents to console"""
1227

    
1228
    arguments = dict(
1229
        range=RangeArgument('show range of data', '--range'),
1230
        if_match=ValueArgument('show output if ETags match', '--if-match'),
1231
        if_none_match=ValueArgument(
1232
            'show output if ETags match',
1233
            '--if-none-match'),
1234
        if_modified_since=DateArgument(
1235
            'show output modified since then',
1236
            '--if-modified-since'),
1237
        if_unmodified_since=DateArgument(
1238
            'show output unmodified since then',
1239
            '--if-unmodified-since'),
1240
        object_version=ValueArgument(
1241
            'get the specific version',
1242
            ('-O', '--object-version'))
1243
    )
1244

    
1245
    @errors.generic.all
1246
    @errors.pithos.connection
1247
    @errors.pithos.container
1248
    @errors.pithos.object_path
1249
    def _run(self):
1250
        self.client.download_object(
1251
            self.path,
1252
            stdout,
1253
            range_str=self['range'],
1254
            version=self['object_version'],
1255
            if_match=self['if_match'],
1256
            if_none_match=self['if_none_match'],
1257
            if_modified_since=self['if_modified_since'],
1258
            if_unmodified_since=self['if_unmodified_since'])
1259

    
1260
    def main(self, container___path):
1261
        super(self.__class__, self)._run(
1262
            container___path,
1263
            path_is_optional=False)
1264
        self._run()
1265

    
1266

    
1267
@command(pithos_cmds)
1268
class file_download(_file_container_command):
1269
    """Download remote object as local file
1270
    If local destination is a directory:
1271
    *   download <container>:<path> <local dir> -R
1272
    will download all files on <container> prefixed as <path>,
1273
    to <local dir>/<full path>
1274
    *   download <container>:<path> <local dir> --exact-match
1275
    will download only one file, exactly matching <path>
1276
    ATTENTION: to download cont:dir1/dir2/file there must exist objects
1277
    cont:dir1 and cont:dir1/dir2 of type application/directory
1278
    To create directory objects, use /file mkdir
1279
    """
1280

    
1281
    arguments = dict(
1282
        resume=FlagArgument('Resume instead of overwrite', ('-r', '--resume')),
1283
        range=RangeArgument('show range of data', '--range'),
1284
        if_match=ValueArgument('show output if ETags match', '--if-match'),
1285
        if_none_match=ValueArgument(
1286
            'show output if ETags match',
1287
            '--if-none-match'),
1288
        if_modified_since=DateArgument(
1289
            'show output modified since then',
1290
            '--if-modified-since'),
1291
        if_unmodified_since=DateArgument(
1292
            'show output unmodified since then',
1293
            '--if-unmodified-since'),
1294
        object_version=ValueArgument(
1295
            'get the specific version',
1296
            ('-O', '--object-version')),
1297
        poolsize=IntArgument('set pool size', '--with-pool-size'),
1298
        progress_bar=ProgressBarArgument(
1299
            'do not show progress bar',
1300
            ('-N', '--no-progress-bar'),
1301
            default=False),
1302
        recursive=FlagArgument(
1303
            'Download a remote path and all its contents',
1304
            ('-R', '--recursive'))
1305
    )
1306

    
1307
    def _outputs(self, local_path):
1308
        """:returns: (local_file, remote_path)"""
1309
        remotes = []
1310
        if self['recursive']:
1311
            r = self.client.container_get(
1312
                prefix=self.path or '/',
1313
                if_modified_since=self['if_modified_since'],
1314
                if_unmodified_since=self['if_unmodified_since'])
1315
            dirlist = dict()
1316
            for remote in r.json:
1317
                rname = remote['name'].strip('/')
1318
                tmppath = ''
1319
                for newdir in rname.strip('/').split('/')[:-1]:
1320
                    tmppath = '/'.join([tmppath, newdir])
1321
                    dirlist.update({tmppath.strip('/'): True})
1322
                remotes.append((rname, file_download._is_dir(remote)))
1323
            dir_remotes = [r[0] for r in remotes if r[1]]
1324
            if not set(dirlist).issubset(dir_remotes):
1325
                badguys = [bg.strip('/') for bg in set(
1326
                    dirlist).difference(dir_remotes)]
1327
                raiseCLIError(
1328
                    'Some remote paths contain non existing directories',
1329
                    details=['Missing remote directories:'] + badguys)
1330
        elif self.path:
1331
            r = self.client.get_object_info(
1332
                self.path,
1333
                version=self['object_version'])
1334
            if file_download._is_dir(r):
1335
                raiseCLIError(
1336
                    'Illegal download: Remote object %s is a directory' % (
1337
                        self.path),
1338
                    details=['To download a directory, try --recursive'])
1339
            if '/' in self.path.strip('/') and not local_path:
1340
                raiseCLIError(
1341
                    'Illegal download: remote object %s contains "/"' % (
1342
                        self.path),
1343
                    details=[
1344
                        'To download an object containing "/" characters',
1345
                        'either create the remote directories or',
1346
                        'specify a non-directory local path for this object'])
1347
            remotes = [(self.path, False)]
1348
        if not remotes:
1349
            if self.path:
1350
                raiseCLIError(
1351
                    'No matching path %s on container %s' % (
1352
                        self.path,
1353
                        self.container),
1354
                    details=[
1355
                        'To list the contents of %s, try:' % self.container,
1356
                        '   /file list %s' % self.container])
1357
            raiseCLIError(
1358
                'Illegal download of container %s' % self.container,
1359
                details=[
1360
                    'To download a whole container, try:',
1361
                    '   /file download --recursive <container>'])
1362

    
1363
        lprefix = path.abspath(local_path or path.curdir)
1364
        if path.isdir(lprefix):
1365
            for rpath, remote_is_dir in remotes:
1366
                lpath = '/%s/%s' % (lprefix.strip('/'), rpath.strip('/'))
1367
                if remote_is_dir:
1368
                    if path.exists(lpath) and path.isdir(lpath):
1369
                        continue
1370
                    makedirs(lpath)
1371
                elif path.exists(lpath):
1372
                    if not self['resume']:
1373
                        print('File %s exists, aborting...' % lpath)
1374
                        continue
1375
                    with open(lpath, 'rwb+') as f:
1376
                        yield (f, rpath)
1377
                else:
1378
                    with open(lpath, 'wb+') as f:
1379
                        yield (f, rpath)
1380
        elif path.exists(lprefix):
1381
            if len(remotes) > 1:
1382
                raiseCLIError(
1383
                    '%s remote objects cannot be merged in local file %s' % (
1384
                        len(remotes),
1385
                        local_path),
1386
                    details=[
1387
                        'To download multiple objects, local path should be',
1388
                        'a directory, or use download without a local path'])
1389
            (rpath, remote_is_dir) = remotes[0]
1390
            if remote_is_dir:
1391
                raiseCLIError(
1392
                    'Remote directory %s should not replace local file %s' % (
1393
                        rpath,
1394
                        local_path))
1395
            if self['resume']:
1396
                with open(lprefix, 'rwb+') as f:
1397
                    yield (f, rpath)
1398
            else:
1399
                raiseCLIError(
1400
                    'Local file %s already exist' % local_path,
1401
                    details=['Try --resume to overwrite it'])
1402
        else:
1403
            if len(remotes) > 1 or remotes[0][1]:
1404
                raiseCLIError(
1405
                    'Local directory %s does not exist' % local_path)
1406
            with open(lprefix, 'wb+') as f:
1407
                yield (f, remotes[0][0])
1408

    
1409
    @errors.generic.all
1410
    @errors.pithos.connection
1411
    @errors.pithos.container
1412
    @errors.pithos.object_path
1413
    @errors.pithos.local_path
1414
    def _run(self, local_path):
1415
        #outputs = self._outputs(local_path)
1416
        poolsize = self['poolsize']
1417
        if poolsize:
1418
            self.client.MAX_THREADS = int(poolsize)
1419
        progress_bar = None
1420
        try:
1421
            for f, rpath in self._outputs(local_path):
1422
                (
1423
                    progress_bar,
1424
                    download_cb) = self._safe_progress_bar(
1425
                        'Download %s' % rpath)
1426
                self.client.download_object(
1427
                    rpath, f,
1428
                    download_cb=download_cb,
1429
                    range_str=self['range'],
1430
                    version=self['object_version'],
1431
                    if_match=self['if_match'],
1432
                    resume=self['resume'],
1433
                    if_none_match=self['if_none_match'],
1434
                    if_modified_since=self['if_modified_since'],
1435
                    if_unmodified_since=self['if_unmodified_since'])
1436
        except KeyboardInterrupt:
1437
            from threading import activeCount, enumerate as activethreads
1438
            timeout = 0.5
1439
            while activeCount() > 1:
1440
                stdout.write('\nCancel %s threads: ' % (activeCount() - 1))
1441
                stdout.flush()
1442
                for thread in activethreads():
1443
                    try:
1444
                        thread.join(timeout)
1445
                        stdout.write('.' if thread.isAlive() else '*')
1446
                    except RuntimeError:
1447
                        continue
1448
                    finally:
1449
                        stdout.flush()
1450
                        timeout += 0.1
1451
            print('\nDownload canceled by user')
1452
            if local_path is not None:
1453
                print('to resume, re-run with --resume')
1454
        except Exception:
1455
            self._safe_progress_bar_finish(progress_bar)
1456
            raise
1457
        finally:
1458
            self._safe_progress_bar_finish(progress_bar)
1459

    
1460
    def main(self, container___path, local_path=None):
1461
        super(self.__class__, self)._run(container___path)
1462
        self._run(local_path=local_path)
1463

    
1464

    
1465
@command(pithos_cmds)
1466
class file_hashmap(_file_container_command):
1467
    """Get the hash-map of an object"""
1468

    
1469
    arguments = dict(
1470
        if_match=ValueArgument('show output if ETags match', '--if-match'),
1471
        if_none_match=ValueArgument(
1472
            'show output if ETags match',
1473
            '--if-none-match'),
1474
        if_modified_since=DateArgument(
1475
            'show output modified since then',
1476
            '--if-modified-since'),
1477
        if_unmodified_since=DateArgument(
1478
            'show output unmodified since then',
1479
            '--if-unmodified-since'),
1480
        object_version=ValueArgument(
1481
            'get the specific version',
1482
            ('-O', '--object-version')),
1483
        json_output=FlagArgument('show headers in json', ('-j', '--json'))
1484
    )
1485

    
1486
    @errors.generic.all
1487
    @errors.pithos.connection
1488
    @errors.pithos.container
1489
    @errors.pithos.object_path
1490
    def _run(self):
1491
        data = self.client.get_object_hashmap(
1492
            self.path,
1493
            version=self['object_version'],
1494
            if_match=self['if_match'],
1495
            if_none_match=self['if_none_match'],
1496
            if_modified_since=self['if_modified_since'],
1497
            if_unmodified_since=self['if_unmodified_since'])
1498
        printer = print_json if self['json_output'] else print_dict
1499
        printer(data)
1500

    
1501
    def main(self, container___path):
1502
        super(self.__class__, self)._run(
1503
            container___path,
1504
            path_is_optional=False)
1505
        self._run()
1506

    
1507

    
1508
@command(pithos_cmds)
1509
class file_delete(_file_container_command):
1510
    """Delete a container [or an object]
1511
    How to delete a non-empty container:
1512
    - empty the container:  /file delete -R <container>
1513
    - delete it:            /file delete <container>
1514
    .
1515
    Semantics of directory deletion:
1516
    .a preserve the contents: /file delete <container>:<directory>
1517
    .    objects of the form dir/filename can exist with a dir object
1518
    .b delete contents:       /file delete -R <container>:<directory>
1519
    .    all dir/* objects are affected, even if dir does not exist
1520
    .
1521
    To restore a deleted object OBJ in a container CONT:
1522
    - get object versions: /file versions CONT:OBJ
1523
    .   and choose the version to be restored
1524
    - restore the object:  /file copy --source-version=<version> CONT:OBJ OBJ
1525
    """
1526

    
1527
    arguments = dict(
1528
        until=DateArgument('remove history until that date', '--until'),
1529
        yes=FlagArgument('Do not prompt for permission', '--yes'),
1530
        recursive=FlagArgument(
1531
            'empty dir or container and delete (if dir)',
1532
            ('-R', '--recursive')),
1533
        with_output=FlagArgument('show response headers', ('--with-output')),
1534
        json_output=FlagArgument('show headers in json', ('-j', '--json'))
1535
    )
1536

    
1537
    def __init__(self, arguments={}):
1538
        super(self.__class__, self).__init__(arguments)
1539
        self['delimiter'] = DelimiterArgument(
1540
            self,
1541
            parsed_name='--delimiter',
1542
            help='delete objects prefixed with <object><delimiter>')
1543

    
1544
    @errors.generic.all
1545
    @errors.pithos.connection
1546
    @errors.pithos.container
1547
    @errors.pithos.object_path
1548
    def _run(self):
1549
        r = {}
1550
        if self.path:
1551
            if self['yes'] or ask_user(
1552
                    'Delete %s:%s ?' % (self.container, self.path)):
1553
                r = self.client.del_object(
1554
                    self.path,
1555
                    until=self['until'],
1556
                    delimiter=self['delimiter'])
1557
            else:
1558
                print('Aborted')
1559
        else:
1560
            if self['recursive']:
1561
                ask_msg = 'Delete container contents'
1562
            else:
1563
                ask_msg = 'Delete container'
1564
            if self['yes'] or ask_user('%s %s ?' % (ask_msg, self.container)):
1565
                r = self.client.del_container(
1566
                    until=self['until'],
1567
                    delimiter=self['delimiter'])
1568
            else:
1569
                print('Aborted')
1570
                return
1571
        if self['json_output']:
1572
            print_json(r)
1573
        elif self['with_output']:
1574
            print_dict(r)
1575

    
1576
    def main(self, container____path__=None):
1577
        super(self.__class__, self)._run(container____path__)
1578
        self._run()
1579

    
1580

    
1581
@command(pithos_cmds)
1582
class file_purge(_file_container_command):
1583
    """Delete a container and release related data blocks
1584
    Non-empty containers can not purged.
1585
    To purge a container with content:
1586
    .   /file delete -R <container>
1587
    .      objects are deleted, but data blocks remain on server
1588
    .   /file purge <container>
1589
    .      container and data blocks are released and deleted
1590
    """
1591

    
1592
    arguments = dict(
1593
        yes=FlagArgument('Do not prompt for permission', '--yes'),
1594
        force=FlagArgument('purge even if not empty', ('-F', '--force')),
1595
        with_output=FlagArgument('show response headers', ('--with-output')),
1596
        json_output=FlagArgument('show headers in json', ('-j', '--json'))
1597
    )
1598

    
1599
    @errors.generic.all
1600
    @errors.pithos.connection
1601
    @errors.pithos.container
1602
    def _run(self):
1603
        if self['yes'] or ask_user('Purge container %s?' % self.container):
1604
            try:
1605
                r = self.client.purge_container()
1606
            except ClientError as ce:
1607
                if ce.status in (409,):
1608
                    if self['force']:
1609
                        self.client.del_container(delimiter='/')
1610
                        r = self.client.purge_container()
1611
                    else:
1612
                        raiseCLIError(ce, details=['Try -F to force-purge'])
1613
                else:
1614
                    raise
1615
            if self['json_output']:
1616
                print_json(r)
1617
            elif self['with_output']:
1618
                print_dict(r)
1619
        else:
1620
            print('Aborted')
1621

    
1622
    def main(self, container=None):
1623
        super(self.__class__, self)._run(container)
1624
        if container and self.container != container:
1625
            raiseCLIError('Invalid container name %s' % container, details=[
1626
                'Did you mean "%s" ?' % self.container,
1627
                'Use --container for names containing :'])
1628
        self._run()
1629

    
1630

    
1631
@command(pithos_cmds)
1632
class file_publish(_file_container_command):
1633
    """Publish the object and print the public url"""
1634

    
1635
    @errors.generic.all
1636
    @errors.pithos.connection
1637
    @errors.pithos.container
1638
    @errors.pithos.object_path
1639
    def _run(self):
1640
        url = self.client.publish_object(self.path)
1641
        print(url)
1642

    
1643
    def main(self, container___path):
1644
        super(self.__class__, self)._run(
1645
            container___path,
1646
            path_is_optional=False)
1647
        self._run()
1648

    
1649

    
1650
@command(pithos_cmds)
1651
class file_unpublish(_file_container_command):
1652
    """Unpublish an object"""
1653

    
1654
    arguments = dict(
1655
        with_output=FlagArgument('show response headers', ('--with-output')),
1656
        json_output=FlagArgument('show headers in json', ('-j', '--json'))
1657
    )
1658

    
1659
    @errors.generic.all
1660
    @errors.pithos.connection
1661
    @errors.pithos.container
1662
    @errors.pithos.object_path
1663
    def _run(self):
1664
            r = self.client.unpublish_object(self.path)
1665
            if self['json_output']:
1666
                print_json(r)
1667
            elif self['with_output']:
1668
                print_dict(r)
1669

    
1670
    def main(self, container___path):
1671
        super(self.__class__, self)._run(
1672
            container___path,
1673
            path_is_optional=False)
1674
        self._run()
1675

    
1676

    
1677
@command(pithos_cmds)
1678
class file_permissions(_pithos_init):
1679
    """Manage user and group accessibility for objects
1680
    Permissions are lists of users and user groups. There are read and write
1681
    permissions. Users and groups with write permission have also read
1682
    permission.
1683
    """
1684

    
1685

    
1686
@command(pithos_cmds)
1687
class file_permissions_get(_file_container_command):
1688
    """Get read and write permissions of an object"""
1689

    
1690
    @errors.generic.all
1691
    @errors.pithos.connection
1692
    @errors.pithos.container
1693
    @errors.pithos.object_path
1694
    def _run(self):
1695
        r = self.client.get_object_sharing(self.path)
1696
        print_dict(r)
1697

    
1698
    def main(self, container___path):
1699
        super(self.__class__, self)._run(
1700
            container___path,
1701
            path_is_optional=False)
1702
        self._run()
1703

    
1704

    
1705
@command(pithos_cmds)
1706
class file_permissions_set(_file_container_command):
1707
    """Set permissions for an object
1708
    New permissions overwrite existing permissions.
1709
    Permission format:
1710
    -   read=<username>[,usergroup[,...]]
1711
    -   write=<username>[,usegroup[,...]]
1712
    E.g. to give read permissions for file F to users A and B and write for C:
1713
    .       /file permissions set F read=A,B write=C
1714
    """
1715

    
1716
    @errors.generic.all
1717
    def format_permition_dict(self, permissions):
1718
        read = False
1719
        write = False
1720
        for perms in permissions:
1721
            splstr = perms.split('=')
1722
            if 'read' == splstr[0]:
1723
                read = [ug.strip() for ug in splstr[1].split(',')]
1724
            elif 'write' == splstr[0]:
1725
                write = [ug.strip() for ug in splstr[1].split(',')]
1726
            else:
1727
                msg = 'Usage:\tread=<groups,users> write=<groups,users>'
1728
                raiseCLIError(None, msg)
1729
        return (read, write)
1730

    
1731
    @errors.generic.all
1732
    @errors.pithos.connection
1733
    @errors.pithos.container
1734
    @errors.pithos.object_path
1735
    def _run(self, read, write):
1736
        self.client.set_object_sharing(
1737
            self.path,
1738
            read_permition=read,
1739
            write_permition=write)
1740

    
1741
    def main(self, container___path, *permissions):
1742
        super(self.__class__, self)._run(
1743
            container___path,
1744
            path_is_optional=False)
1745
        (read, write) = self.format_permition_dict(permissions)
1746
        self._run(read, write)
1747

    
1748

    
1749
@command(pithos_cmds)
1750
class file_permissions_delete(_file_container_command):
1751
    """Delete all permissions set on object
1752
    To modify permissions, use /file permissions set
1753
    """
1754

    
1755
    @errors.generic.all
1756
    @errors.pithos.connection
1757
    @errors.pithos.container
1758
    @errors.pithos.object_path
1759
    def _run(self):
1760
        self.client.del_object_sharing(self.path)
1761

    
1762
    def main(self, container___path):
1763
        super(self.__class__, self)._run(
1764
            container___path,
1765
            path_is_optional=False)
1766
        self._run()
1767

    
1768

    
1769
@command(pithos_cmds)
1770
class file_info(_file_container_command):
1771
    """Get detailed information for user account, containers or objects
1772
    to get account info:    /file info
1773
    to get container info:  /file info <container>
1774
    to get object info:     /file info <container>:<path>
1775
    """
1776

    
1777
    arguments = dict(
1778
        object_version=ValueArgument(
1779
            'show specific version \ (applies only for objects)',
1780
            ('-O', '--object-version')),
1781
        json_output=FlagArgument('show headers in json', ('-j', '--json'))
1782
    )
1783

    
1784
    @errors.generic.all
1785
    @errors.pithos.connection
1786
    @errors.pithos.container
1787
    @errors.pithos.object_path
1788
    def _run(self):
1789
        if self.container is None:
1790
            r = self.client.get_account_info()
1791
        elif self.path is None:
1792
            r = self.client.get_container_info(self.container)
1793
        else:
1794
            r = self.client.get_object_info(
1795
                self.path,
1796
                version=self['object_version'])
1797
        printer = print_json if self['json_output'] else print_dict
1798
        printer(r)
1799

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

    
1804

    
1805
@command(pithos_cmds)
1806
class file_metadata(_pithos_init):
1807
    """Metadata are attached on objects. They are formed as key:value pairs.
1808
    They can have arbitary values.
1809
    """
1810

    
1811

    
1812
@command(pithos_cmds)
1813
class file_metadata_get(_file_container_command):
1814
    """Get metadata for account, containers or objects"""
1815

    
1816
    arguments = dict(
1817
        detail=FlagArgument('show detailed output', ('-l', '--details')),
1818
        until=DateArgument('show metadata until then', '--until'),
1819
        object_version=ValueArgument(
1820
            'show specific version \ (applies only for objects)',
1821
            ('-O', '--object-version')),
1822
        json_output=FlagArgument('show headers in json', ('-j', '--json'))
1823
    )
1824

    
1825
    @errors.generic.all
1826
    @errors.pithos.connection
1827
    @errors.pithos.container
1828
    @errors.pithos.object_path
1829
    def _run(self):
1830
        until = self['until']
1831
        if self.container is None:
1832
            if self['detail']:
1833
                r = self.client.get_account_info(until=until)
1834
            else:
1835
                r = self.client.get_account_meta(until=until)
1836
                r = pretty_keys(r, '-')
1837
            print(bold(self.client.account))
1838
        elif self.path is None:
1839
            if self['detail']:
1840
                r = self.client.get_container_info(until=until)
1841
            else:
1842
                cmeta = self.client.get_container_meta(until=until)
1843
                ometa = self.client.get_container_object_meta(until=until)
1844
                r = {}
1845
                if cmeta:
1846
                    r['container-meta'] = pretty_keys(cmeta, '-')
1847
                if ometa:
1848
                    r['object-meta'] = pretty_keys(ometa, '-')
1849
        else:
1850
            if self['detail']:
1851
                r = self.client.get_object_info(
1852
                    self.path,
1853
                    version=self['object_version'])
1854
            else:
1855
                r = self.client.get_object_meta(
1856
                    self.path,
1857
                    version=self['object_version'])
1858
                r = pretty_keys(pretty_keys(r, '-'))
1859
        if r:
1860
            printer = print_json if self['json_output'] else print_dict
1861
            printer(r)
1862

    
1863
    def main(self, container____path__=None):
1864
        super(self.__class__, self)._run(container____path__)
1865
        self._run()
1866

    
1867

    
1868
@command(pithos_cmds)
1869
class file_metadata_set(_file_container_command):
1870
    """Set a piece of metadata for account, container or object"""
1871

    
1872
    @errors.generic.all
1873
    @errors.pithos.connection
1874
    @errors.pithos.container
1875
    @errors.pithos.object_path
1876
    def _run(self, metakey, metaval):
1877
        if not self.container:
1878
            self.client.set_account_meta({metakey: metaval})
1879
        elif not self.path:
1880
            self.client.set_container_meta({metakey: metaval})
1881
        else:
1882
            self.client.set_object_meta(self.path, {metakey: metaval})
1883

    
1884
    def main(self, metakey, metaval, container____path__=None):
1885
        super(self.__class__, self)._run(container____path__)
1886
        self._run(metakey=metakey, metaval=metaval)
1887

    
1888

    
1889
@command(pithos_cmds)
1890
class file_metadata_delete(_file_container_command):
1891
    """Delete metadata with given key from account, container or object
1892
    - to get metadata of current account: /file metadata get
1893
    - to get metadata of a container:     /file metadata get <container>
1894
    - to get metadata of an object:       /file metadata get <container>:<path>
1895
    """
1896

    
1897
    @errors.generic.all
1898
    @errors.pithos.connection
1899
    @errors.pithos.container
1900
    @errors.pithos.object_path
1901
    def _run(self, metakey):
1902
        if self.container is None:
1903
            self.client.del_account_meta(metakey)
1904
        elif self.path is None:
1905
            self.client.del_container_meta(metakey)
1906
        else:
1907
            self.client.del_object_meta(self.path, metakey)
1908

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

    
1913

    
1914
@command(pithos_cmds)
1915
class file_quota(_file_account_command):
1916
    """Get account quota"""
1917

    
1918
    arguments = dict(
1919
        in_bytes=FlagArgument('Show result in bytes', ('-b', '--bytes'))
1920
    )
1921

    
1922
    @errors.generic.all
1923
    @errors.pithos.connection
1924
    def _run(self):
1925
        reply = self.client.get_account_quota()
1926
        if not self['in_bytes']:
1927
            for k in reply:
1928
                reply[k] = format_size(reply[k])
1929
        print_dict(pretty_keys(reply, '-'))
1930

    
1931
    def main(self, custom_uuid=None):
1932
        super(self.__class__, self)._run(custom_account=custom_uuid)
1933
        self._run()
1934

    
1935

    
1936
@command(pithos_cmds)
1937
class file_containerlimit(_pithos_init):
1938
    """Container size limit commands"""
1939

    
1940

    
1941
@command(pithos_cmds)
1942
class file_containerlimit_get(_file_container_command):
1943
    """Get container size limit"""
1944

    
1945
    arguments = dict(
1946
        in_bytes=FlagArgument('Show result in bytes', ('-b', '--bytes'))
1947
    )
1948

    
1949
    @errors.generic.all
1950
    @errors.pithos.container
1951
    def _run(self):
1952
        reply = self.client.get_container_limit(self.container)
1953
        if not self['in_bytes']:
1954
            for k, v in reply.items():
1955
                reply[k] = 'unlimited' if '0' == v else format_size(v)
1956
        print_dict(pretty_keys(reply, '-'))
1957

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

    
1963

    
1964
@command(pithos_cmds)
1965
class file_containerlimit_set(_file_account_command):
1966
    """Set new storage limit for a container
1967
    By default, the limit is set in bytes
1968
    Users may specify a different unit, e.g:
1969
    /file containerlimit set 2.3GB mycontainer
1970
    Valid units: B, KiB (1024 B), KB (1000 B), MiB, MB, GiB, GB, TiB, TB
1971
    To set container limit to "unlimited", use 0
1972
    """
1973

    
1974
    @errors.generic.all
1975
    def _calculate_limit(self, user_input):
1976
        limit = 0
1977
        try:
1978
            limit = int(user_input)
1979
        except ValueError:
1980
            index = 0
1981
            digits = [str(num) for num in range(0, 10)] + ['.']
1982
            while user_input[index] in digits:
1983
                index += 1
1984
            limit = user_input[:index]
1985
            format = user_input[index:]
1986
            try:
1987
                return to_bytes(limit, format)
1988
            except Exception as qe:
1989
                msg = 'Failed to convert %s to bytes' % user_input,
1990
                raiseCLIError(qe, msg, details=[
1991
                    'Syntax: containerlimit set <limit>[format] [container]',
1992
                    'e.g.: containerlimit set 2.3GB mycontainer',
1993
                    'Valid formats:',
1994
                    '(*1024): B, KiB, MiB, GiB, TiB',
1995
                    '(*1000): B, KB, MB, GB, TB'])
1996
        return limit
1997

    
1998
    @errors.generic.all
1999
    @errors.pithos.connection
2000
    @errors.pithos.container
2001
    def _run(self, limit):
2002
        if self.container:
2003
            self.client.container = self.container
2004
        self.client.set_container_limit(limit)
2005

    
2006
    def main(self, limit, container=None):
2007
        super(self.__class__, self)._run()
2008
        limit = self._calculate_limit(limit)
2009
        self.container = container
2010
        self._run(limit)
2011

    
2012

    
2013
@command(pithos_cmds)
2014
class file_versioning(_file_account_command):
2015
    """Get  versioning for account or container"""
2016

    
2017
    @errors.generic.all
2018
    @errors.pithos.connection
2019
    @errors.pithos.container
2020
    def _run(self):
2021
        if self.container:
2022
            r = self.client.get_container_versioning(self.container)
2023
        else:
2024
            r = self.client.get_account_versioning()
2025
        print_dict(r)
2026

    
2027
    def main(self, container=None):
2028
        super(self.__class__, self)._run()
2029
        self.container = container
2030
        self._run()
2031

    
2032

    
2033
@command(pithos_cmds)
2034
class file_setversioning(_file_account_command):
2035
    """Set versioning mode (auto, none) for account or container"""
2036

    
2037
    def _check_versioning(self, versioning):
2038
        if versioning and versioning.lower() in ('auto', 'none'):
2039
            return versioning.lower()
2040
        raiseCLIError('Invalid versioning %s' % versioning, details=[
2041
            'Versioning can be auto or none'])
2042

    
2043
    @errors.generic.all
2044
    @errors.pithos.connection
2045
    @errors.pithos.container
2046
    def _run(self, versioning):
2047
        if self.container:
2048
            self.client.container = self.container
2049
            self.client.set_container_versioning(versioning)
2050
        else:
2051
            self.client.set_account_versioning(versioning)
2052

    
2053
    def main(self, versioning, container=None):
2054
        super(self.__class__, self)._run()
2055
        self._run(self._check_versioning(versioning))
2056

    
2057

    
2058
@command(pithos_cmds)
2059
class file_group(_file_account_command):
2060
    """Get groups and group members"""
2061

    
2062
    @errors.generic.all
2063
    @errors.pithos.connection
2064
    def _run(self):
2065
        r = self.client.get_account_group()
2066
        print_dict(pretty_keys(r, '-'))
2067

    
2068
    def main(self):
2069
        super(self.__class__, self)._run()
2070
        self._run()
2071

    
2072

    
2073
@command(pithos_cmds)
2074
class file_setgroup(_file_account_command):
2075
    """Set a user group"""
2076

    
2077
    @errors.generic.all
2078
    @errors.pithos.connection
2079
    def _run(self, groupname, *users):
2080
        self.client.set_account_group(groupname, users)
2081

    
2082
    def main(self, groupname, *users):
2083
        super(self.__class__, self)._run()
2084
        if users:
2085
            self._run(groupname, *users)
2086
        else:
2087
            raiseCLIError('No users to add in group %s' % groupname)
2088

    
2089

    
2090
@command(pithos_cmds)
2091
class file_delgroup(_file_account_command):
2092
    """Delete a user group"""
2093

    
2094
    @errors.generic.all
2095
    @errors.pithos.connection
2096
    def _run(self, groupname):
2097
        self.client.del_account_group(groupname)
2098

    
2099
    def main(self, groupname):
2100
        super(self.__class__, self)._run()
2101
        self._run(groupname)
2102

    
2103

    
2104
@command(pithos_cmds)
2105
class file_sharers(_file_account_command):
2106
    """List the accounts that share objects with current user"""
2107

    
2108
    arguments = dict(
2109
        detail=FlagArgument('show detailed output', ('-l', '--details')),
2110
        marker=ValueArgument('show output greater then marker', '--marker')
2111
    )
2112

    
2113
    @errors.generic.all
2114
    @errors.pithos.connection
2115
    def _run(self):
2116
        accounts = self.client.get_sharing_accounts(marker=self['marker'])
2117
        if self['detail']:
2118
            print_items(accounts)
2119
        else:
2120
            print_items([acc['name'] for acc in accounts])
2121

    
2122
    def main(self):
2123
        super(self.__class__, self)._run()
2124
        self._run()
2125

    
2126

    
2127
@command(pithos_cmds)
2128
class file_versions(_file_container_command):
2129
    """Get the list of object versions
2130
    Deleted objects may still have versions that can be used to restore it and
2131
    get information about its previous state.
2132
    The version number can be used in a number of other commands, like info,
2133
    copy, move, meta. See these commands for more information, e.g.
2134
    /file info -h
2135
    """
2136

    
2137
    @errors.generic.all
2138
    @errors.pithos.connection
2139
    @errors.pithos.container
2140
    @errors.pithos.object_path
2141
    def _run(self):
2142
        versions = self.client.get_object_versionlist(self.path)
2143
        print_items([dict(id=vitem[0], created=strftime(
2144
            '%d-%m-%Y %H:%M:%S',
2145
            localtime(float(vitem[1])))) for vitem in versions])
2146

    
2147
    def main(self, container___path):
2148
        super(file_versions, self)._run(
2149
            container___path,
2150
            path_is_optional=False)
2151
        self._run()