Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (76.9 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, pretty_dict,
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.cli.commands import _optional_output_cmd, _optional_json
49
from kamaki.clients.pithos import PithosClient, ClientError
50
from kamaki.clients.astakos import AstakosClient
51

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

    
55

    
56
# Argument functionality
57

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

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

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

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

    
78

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

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

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

    
120

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

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

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

    
141
# Command specs
142

    
143

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

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

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

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

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

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

    
182

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

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

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

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

    
203

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

    
207
    container = None
208
    path = None
209

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

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

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

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

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

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

    
292

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

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

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

    
442
    @errors.generic.all
443
    @errors.pithos.connection
444
    @errors.pithos.container
445
    def _run(self):
446
        self._optional_output(self.client.create_directory(self.path))
447

    
448
    def main(self, container___directory):
449
        super(self.__class__, self)._run(
450
            container___directory,
451
            path_is_optional=False)
452
        self._run()
453

    
454

    
455
@command(pithos_cmds)
456
class file_touch(_file_container_command, _optional_output_cmd):
457
    """Create an empty object (file)
458
    If object exists, this command will reset it to 0 length
459
    """
460

    
461
    arguments = dict(
462
        content_type=ValueArgument(
463
            'Set content type (default: application/octet-stream)',
464
            '--content-type',
465
            default='application/octet-stream')
466
    )
467

    
468
    @errors.generic.all
469
    @errors.pithos.connection
470
    @errors.pithos.container
471
    def _run(self):
472
        self._optional_output(
473
            self.client.create_object(self.path, self['content_type']))
474

    
475
    def main(self, container___path):
476
        super(file_touch, self)._run(
477
            container___path,
478
            path_is_optional=False)
479
        self._run()
480

    
481

    
482
@command(pithos_cmds)
483
class file_create(_file_container_command, _optional_output_cmd):
484
    """Create a container"""
485

    
486
    arguments = dict(
487
        versioning=ValueArgument(
488
            'set container versioning (auto/none)',
489
            '--versioning'),
490
        limit=IntArgument('set default container limit', '--limit'),
491
        meta=KeyValueArgument(
492
            'set container metadata (can be repeated)',
493
            '--meta')
494
    )
495

    
496
    @errors.generic.all
497
    @errors.pithos.connection
498
    @errors.pithos.container
499
    def _run(self, container):
500
        self._optional_output(self.client.create_container(
501
            container=container,
502
            sizelimit=self['limit'],
503
            versioning=self['versioning'],
504
            metadata=self['meta']))
505

    
506
    def main(self, container=None):
507
        super(self.__class__, self)._run(container)
508
        if container and self.container != container:
509
            raiseCLIError('Invalid container name %s' % container, details=[
510
                'Did you mean "%s" ?' % self.container,
511
                'Use --container for names containing :'])
512
        self._run(container)
513

    
514

    
515
class _source_destination_command(_file_container_command):
516

    
517
    arguments = dict(
518
        destination_account=ValueArgument('', ('a', '--dst-account')),
519
        recursive=FlagArgument('', ('-R', '--recursive')),
520
        prefix=FlagArgument('', '--with-prefix', default=''),
521
        suffix=ValueArgument('', '--with-suffix', default=''),
522
        add_prefix=ValueArgument('', '--add-prefix', default=''),
523
        add_suffix=ValueArgument('', '--add-suffix', default=''),
524
        prefix_replace=ValueArgument('', '--prefix-to-replace', default=''),
525
        suffix_replace=ValueArgument('', '--suffix-to-replace', default=''),
526
    )
527

    
528
    def __init__(self, arguments={}):
529
        self.arguments.update(arguments)
530
        super(_source_destination_command, self).__init__(self.arguments)
531

    
532
    def _run(self, source_container___path, path_is_optional=False):
533
        super(_source_destination_command, self)._run(
534
            source_container___path,
535
            path_is_optional)
536
        self.dst_client = PithosClient(
537
            base_url=self.client.base_url,
538
            token=self.client.token,
539
            account=self['destination_account'] or self.client.account)
540

    
541
    @errors.generic.all
542
    @errors.pithos.account
543
    def _dest_container_path(self, dest_container_path):
544
        if self['destination_container']:
545
            self.dst_client.container = self['destination_container']
546
            return (self['destination_container'], dest_container_path)
547
        if dest_container_path:
548
            dst = dest_container_path.split(':')
549
            if len(dst) > 1:
550
                try:
551
                    self.dst_client.container = dst[0]
552
                    self.dst_client.get_container_info(dst[0])
553
                except ClientError as err:
554
                    if err.status in (404, 204):
555
                        raiseCLIError(
556
                            'Destination container %s not found' % dst[0])
557
                    raise
558
                else:
559
                    self.dst_client.container = dst[0]
560
                return (dst[0], dst[1])
561
            return(None, dst[0])
562
        raiseCLIError('No destination container:path provided')
563

    
564
    def _get_all(self, prefix):
565
        return self.client.container_get(prefix=prefix).json
566

    
567
    def _get_src_objects(self, src_path, source_version=None):
568
        """Get a list of the source objects to be called
569

570
        :param src_path: (str) source path
571

572
        :returns: (method, params) a method that returns a list when called
573
        or (object) if it is a single object
574
        """
575
        if src_path and src_path[-1] == '/':
576
            src_path = src_path[:-1]
577

    
578
        if self['prefix']:
579
            return (self._get_all, dict(prefix=src_path))
580
        try:
581
            srcobj = self.client.get_object_info(
582
                src_path, version=source_version)
583
        except ClientError as srcerr:
584
            if srcerr.status == 404:
585
                raiseCLIError(
586
                    'Source object %s not in source container %s' % (
587
                        src_path,
588
                        self.client.container),
589
                    details=['Hint: --with-prefix to match multiple objects'])
590
            elif srcerr.status not in (204,):
591
                raise
592
            return (self.client.list_objects, {})
593

    
594
        if self._is_dir(srcobj):
595
            if not self['recursive']:
596
                raiseCLIError(
597
                    'Object %s of cont. %s is a dir' % (
598
                        src_path,
599
                        self.client.container),
600
                    details=['Use --recursive to access directories'])
601
            return (self._get_all, dict(prefix=src_path))
602
        srcobj['name'] = src_path
603
        return srcobj
604

    
605
    def src_dst_pairs(self, dst_path, source_version=None):
606
        src_iter = self._get_src_objects(self.path, source_version)
607
        src_N = isinstance(src_iter, tuple)
608
        add_prefix = self['add_prefix'].strip('/')
609

    
610
        if dst_path and dst_path.endswith('/'):
611
            dst_path = dst_path[:-1]
612

    
613
        try:
614
            dstobj = self.dst_client.get_object_info(dst_path)
615
        except ClientError as trgerr:
616
            if trgerr.status in (404,):
617
                if src_N:
618
                    raiseCLIError(
619
                        'Cannot merge multiple paths to path %s' % dst_path,
620
                        details=[
621
                            'Try to use / or a directory as destination',
622
                            'or create the destination dir (/file mkdir)',
623
                            'or use a single object as source'])
624
            elif trgerr.status not in (204,):
625
                raise
626
        else:
627
            if self._is_dir(dstobj):
628
                add_prefix = '%s/%s' % (dst_path.strip('/'), add_prefix)
629
            elif src_N:
630
                raiseCLIError(
631
                    'Cannot merge multiple paths to path' % dst_path,
632
                    details=[
633
                        'Try to use / or a directory as destination',
634
                        'or create the destination dir (/file mkdir)',
635
                        'or use a single object as source'])
636

    
637
        if src_N:
638
            (method, kwargs) = src_iter
639
            for obj in method(**kwargs):
640
                name = obj['name']
641
                if name.endswith(self['suffix']):
642
                    yield (name, self._get_new_object(name, add_prefix))
643
        elif src_iter['name'].endswith(self['suffix']):
644
            name = src_iter['name']
645
            yield (name, self._get_new_object(dst_path or name, add_prefix))
646
        else:
647
            raiseCLIError('Source path %s conflicts with suffix %s' % (
648
                src_iter['name'],
649
                self['suffix']))
650

    
651
    def _get_new_object(self, obj, add_prefix):
652
        if self['prefix_replace'] and obj.startswith(self['prefix_replace']):
653
            obj = obj[len(self['prefix_replace']):]
654
        if self['suffix_replace'] and obj.endswith(self['suffix_replace']):
655
            obj = obj[:-len(self['suffix_replace'])]
656
        return add_prefix + obj + self['add_suffix']
657

    
658

    
659
@command(pithos_cmds)
660
class file_copy(_source_destination_command, _optional_output_cmd):
661
    """Copy objects from container to (another) container
662
    Semantics:
663
    copy cont:path dir
664
    .   transfer path as dir/path
665
    copy cont:path cont2:
666
    .   trasnfer all <obj> prefixed with path to container cont2
667
    copy cont:path [cont2:]path2
668
    .   transfer path to path2
669
    Use options:
670
    1. <container1>:<path1> [container2:]<path2> : if container2 is not given,
671
    destination is container1:path2
672
    2. <container>:<path1> <path2> : make a copy in the same container
673
    3. Can use --container= instead of <container1>
674
    """
675

    
676
    arguments = dict(
677
        destination_account=ValueArgument(
678
            'Account to copy to',
679
            ('-a', '--dst-account')),
680
        destination_container=ValueArgument(
681
            'use it if destination container name contains a : character',
682
            ('-D', '--dst-container')),
683
        public=ValueArgument('make object publicly accessible', '--public'),
684
        content_type=ValueArgument(
685
            'change object\'s content type',
686
            '--content-type'),
687
        recursive=FlagArgument(
688
            'copy directory and contents',
689
            ('-R', '--recursive')),
690
        prefix=FlagArgument(
691
            'Match objects prefixed with src path (feels like src_path*)',
692
            '--with-prefix',
693
            default=''),
694
        suffix=ValueArgument(
695
            'Suffix of source objects (feels like *suffix)',
696
            '--with-suffix',
697
            default=''),
698
        add_prefix=ValueArgument('Prefix targets', '--add-prefix', default=''),
699
        add_suffix=ValueArgument('Suffix targets', '--add-suffix', default=''),
700
        prefix_replace=ValueArgument(
701
            'Prefix of src to replace with dst path + add_prefix, if matched',
702
            '--prefix-to-replace',
703
            default=''),
704
        suffix_replace=ValueArgument(
705
            'Suffix of src to replace with add_suffix, if matched',
706
            '--suffix-to-replace',
707
            default=''),
708
        source_version=ValueArgument(
709
            'copy specific version',
710
            ('-S', '--source-version'))
711
    )
712

    
713
    @errors.generic.all
714
    @errors.pithos.connection
715
    @errors.pithos.container
716
    @errors.pithos.account
717
    def _run(self, dst_path):
718
        no_source_object = True
719
        src_account = self.client.account if (
720
            self['destination_account']) else None
721
        for src_obj, dst_obj in self.src_dst_pairs(
722
                dst_path, self['source_version']):
723
            no_source_object = False
724
            r = self.dst_client.copy_object(
725
                src_container=self.client.container,
726
                src_object=src_obj,
727
                dst_container=self.dst_client.container,
728
                dst_object=dst_obj,
729
                source_account=src_account,
730
                source_version=self['source_version'],
731
                public=self['public'],
732
                content_type=self['content_type'])
733
        if no_source_object:
734
            raiseCLIError('No object %s in container %s' % (
735
                self.path,
736
                self.container))
737
        self._optional_output(r)
738

    
739
    def main(
740
            self, source_container___path,
741
            destination_container___path=None):
742
        super(file_copy, self)._run(
743
            source_container___path,
744
            path_is_optional=False)
745
        (dst_cont, dst_path) = self._dest_container_path(
746
            destination_container___path)
747
        self.dst_client.container = dst_cont or self.container
748
        self._run(dst_path=dst_path or '')
749

    
750

    
751
@command(pithos_cmds)
752
class file_move(_source_destination_command, _optional_output_cmd):
753
    """Move/rename objects from container to (another) container
754
    Semantics:
755
    move cont:path dir
756
    .   rename path as dir/path
757
    move cont:path cont2:
758
    .   trasnfer all <obj> prefixed with path to container cont2
759
    move cont:path [cont2:]path2
760
    .   transfer path to path2
761
    Use options:
762
    1. <container1>:<path1> [container2:]<path2> : if container2 is not given,
763
    destination is container1:path2
764
    2. <container>:<path1> <path2> : move in the same container
765
    3. Can use --container= instead of <container1>
766
    """
767

    
768
    arguments = dict(
769
        destination_account=ValueArgument(
770
            'Account to move to',
771
            ('-a', '--dst-account')),
772
        destination_container=ValueArgument(
773
            'use it if destination container name contains a : character',
774
            ('-D', '--dst-container')),
775
        public=ValueArgument('make object publicly accessible', '--public'),
776
        content_type=ValueArgument(
777
            'change object\'s content type',
778
            '--content-type'),
779
        recursive=FlagArgument(
780
            'copy directory and contents',
781
            ('-R', '--recursive')),
782
        prefix=FlagArgument(
783
            'Match objects prefixed with src path (feels like src_path*)',
784
            '--with-prefix',
785
            default=''),
786
        suffix=ValueArgument(
787
            'Suffix of source objects (feels like *suffix)',
788
            '--with-suffix',
789
            default=''),
790
        add_prefix=ValueArgument('Prefix targets', '--add-prefix', default=''),
791
        add_suffix=ValueArgument('Suffix targets', '--add-suffix', default=''),
792
        prefix_replace=ValueArgument(
793
            'Prefix of src to replace with dst path + add_prefix, if matched',
794
            '--prefix-to-replace',
795
            default=''),
796
        suffix_replace=ValueArgument(
797
            'Suffix of src to replace with add_suffix, if matched',
798
            '--suffix-to-replace',
799
            default='')
800
    )
801

    
802
    @errors.generic.all
803
    @errors.pithos.connection
804
    @errors.pithos.container
805
    def _run(self, dst_path):
806
        no_source_object = True
807
        src_account = self.client.account if (
808
            self['destination_account']) else None
809
        for src_obj, dst_obj in self.src_dst_pairs(dst_path):
810
            no_source_object = False
811
            r = self.dst_client.move_object(
812
                src_container=self.container,
813
                src_object=src_obj,
814
                dst_container=self.dst_client.container,
815
                dst_object=dst_obj,
816
                source_account=src_account,
817
                public=self['public'],
818
                content_type=self['content_type'])
819
        if no_source_object:
820
            raiseCLIError('No object %s in container %s' % (
821
                self.path,
822
                self.container))
823
        self._optional_output(r)
824

    
825
    def main(
826
            self, source_container___path,
827
            destination_container___path=None):
828
        super(self.__class__, self)._run(
829
            source_container___path,
830
            path_is_optional=False)
831
        (dst_cont, dst_path) = self._dest_container_path(
832
            destination_container___path)
833
        (dst_cont, dst_path) = self._dest_container_path(
834
            destination_container___path)
835
        self.dst_client.container = dst_cont or self.container
836
        self._run(dst_path=dst_path or '')
837

    
838

    
839
@command(pithos_cmds)
840
class file_append(_file_container_command, _optional_output_cmd):
841
    """Append local file to (existing) remote object
842
    The remote object should exist.
843
    If the remote object is a directory, it is transformed into a file.
844
    In the later case, objects under the directory remain intact.
845
    """
846

    
847
    arguments = dict(
848
        progress_bar=ProgressBarArgument(
849
            'do not show progress bar',
850
            ('-N', '--no-progress-bar'),
851
            default=False)
852
    )
853

    
854
    @errors.generic.all
855
    @errors.pithos.connection
856
    @errors.pithos.container
857
    @errors.pithos.object_path
858
    def _run(self, local_path):
859
        (progress_bar, upload_cb) = self._safe_progress_bar('Appending')
860
        try:
861
            f = open(local_path, 'rb')
862
            self._optional_output(
863
                self.client.append_object(self.path, f, upload_cb))
864
        except Exception:
865
            self._safe_progress_bar_finish(progress_bar)
866
            raise
867
        finally:
868
            self._safe_progress_bar_finish(progress_bar)
869

    
870
    def main(self, local_path, container___path):
871
        super(self.__class__, self)._run(
872
            container___path,
873
            path_is_optional=False)
874
        self._run(local_path)
875

    
876

    
877
@command(pithos_cmds)
878
class file_truncate(_file_container_command, _optional_output_cmd):
879
    """Truncate remote file up to a size (default is 0)"""
880

    
881
    @errors.generic.all
882
    @errors.pithos.connection
883
    @errors.pithos.container
884
    @errors.pithos.object_path
885
    @errors.pithos.object_size
886
    def _run(self, size=0):
887
        self._optional_output(self.client.truncate_object(self.path, size))
888

    
889
    def main(self, container___path, size=0):
890
        super(self.__class__, self)._run(container___path)
891
        self._run(size=size)
892

    
893

    
894
@command(pithos_cmds)
895
class file_overwrite(_file_container_command, _optional_output_cmd):
896
    """Overwrite part (from start to end) of a remote file
897
    overwrite local-path container 10 20
898
    .   will overwrite bytes from 10 to 20 of a remote file with the same name
899
    .   as local-path basename
900
    overwrite local-path container:path 10 20
901
    .   will overwrite as above, but the remote file is named path
902
    """
903

    
904
    arguments = dict(
905
        progress_bar=ProgressBarArgument(
906
            'do not show progress bar',
907
            ('-N', '--no-progress-bar'),
908
            default=False)
909
    )
910

    
911
    def _open_file(self, local_path, start):
912
        f = open(path.abspath(local_path), 'rb')
913
        f.seek(0, 2)
914
        f_size = f.tell()
915
        f.seek(start, 0)
916
        return (f, f_size)
917

    
918
    @errors.generic.all
919
    @errors.pithos.connection
920
    @errors.pithos.container
921
    @errors.pithos.object_path
922
    @errors.pithos.object_size
923
    def _run(self, local_path, start, end):
924
        (start, end) = (int(start), int(end))
925
        (f, f_size) = self._open_file(local_path, start)
926
        (progress_bar, upload_cb) = self._safe_progress_bar(
927
            'Overwrite %s bytes' % (end - start))
928
        try:
929
            self._optional_output(self.client.overwrite_object(
930
                obj=self.path,
931
                start=start,
932
                end=end,
933
                source_file=f,
934
                upload_cb=upload_cb))
935
        finally:
936
            self._safe_progress_bar_finish(progress_bar)
937

    
938
    def main(self, local_path, container___path, start, end):
939
        super(self.__class__, self)._run(
940
            container___path,
941
            path_is_optional=None)
942
        self.path = self.path or path.basename(local_path)
943
        self._run(local_path=local_path, start=start, end=end)
944

    
945

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

    
959
    arguments = dict(
960
        etag=ValueArgument('check written data', '--etag'),
961
        content_encoding=ValueArgument(
962
            'set MIME content type',
963
            '--content-encoding'),
964
        content_disposition=ValueArgument(
965
            'the presentation style of the object',
966
            '--content-disposition'),
967
        content_type=ValueArgument(
968
            'specify content type',
969
            '--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
        self._optional_output(self.client.create_object_by_manifestation(
985
            self.path,
986
            content_encoding=self['content_encoding'],
987
            content_disposition=self['content_disposition'],
988
            content_type=self['content_type'],
989
            sharing=self['sharing'],
990
            public=self['public']))
991

    
992
    def main(self, container___path):
993
        super(self.__class__, self)._run(
994
            container___path,
995
            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',
1006
            '--use-hashes'),
1007
        etag=ValueArgument('check written data', '--etag'),
1008
        unchunked=FlagArgument('avoid chunked transfer mode', '--unchunked'),
1009
        content_encoding=ValueArgument(
1010
            'set MIME content type',
1011
            '--content-encoding'),
1012
        content_disposition=ValueArgument(
1013
            'specify objects presentation style',
1014
            '--content-disposition'),
1015
        content_type=ValueArgument('specify content type', '--content-type'),
1016
        sharing=SharingArgument(
1017
            help='\n'.join([
1018
                'define sharing object policy',
1019
                '( "read=user1,grp1,user2,... write=user1,grp2,... )']),
1020
            parsed_name='--sharing'),
1021
        public=FlagArgument('make object publicly accessible', '--public'),
1022
        poolsize=IntArgument('set pool size', '--with-pool-size'),
1023
        progress_bar=ProgressBarArgument(
1024
            'do not show progress bar',
1025
            ('-N', '--no-progress-bar'),
1026
            default=False),
1027
        overwrite=FlagArgument('Force (over)write', ('-f', '--force')),
1028
        recursive=FlagArgument(
1029
            'Recursively upload directory *contents* + subdirectories',
1030
            ('-R', '--recursive'))
1031
    )
1032

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

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

    
1119
    @errors.generic.all
1120
    @errors.pithos.connection
1121
    @errors.pithos.container
1122
    @errors.pithos.object_path
1123
    @errors.pithos.local_path
1124
    def _run(self, local_path, remote_path):
1125
        poolsize = self['poolsize']
1126
        if poolsize > 0:
1127
            self.client.MAX_THREADS = int(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
            print('%s --> %s:%s' % (f.name, self.client.container, rpath))
1138
            if self['unchunked']:
1139
                r = self.client.upload_object_unchunked(
1140
                    rpath, f,
1141
                    etag=self['etag'], withHashFile=self['use_hashes'],
1142
                    **params)
1143
                if self['with_output'] or self['json_output']:
1144
                    r['name'] = '%s: %s' % (self.client.container, rpath)
1145
                    uploaded.append(r)
1146
            else:
1147
                try:
1148
                    (progress_bar, upload_cb) = self._safe_progress_bar(
1149
                        'Uploading %s' % f.name.split(path.sep)[-1])
1150
                    if progress_bar:
1151
                        hash_bar = progress_bar.clone()
1152
                        hash_cb = hash_bar.get_generator(
1153
                            'Calculating block hashes')
1154
                    else:
1155
                        hash_cb = None
1156
                    r = self.client.upload_object(
1157
                        rpath, f,
1158
                        hash_cb=hash_cb,
1159
                        upload_cb=upload_cb,
1160
                        container_info_cache=container_info_cache,
1161
                        **params)
1162
                    if self['with_output'] or self['json_output']:
1163
                        r['name'] = '%s: %s' % (self.client.container, rpath)
1164
                        uploaded.append(r)
1165
                except Exception:
1166
                    self._safe_progress_bar_finish(progress_bar)
1167
                    raise
1168
                finally:
1169
                    self._safe_progress_bar_finish(progress_bar)
1170
        self._optional_output(uploaded)
1171
        print('Upload completed')
1172

    
1173
    def main(self, local_path, container____path__=None):
1174
        super(self.__class__, self)._run(container____path__)
1175
        remote_path = self.path or path.basename(local_path)
1176
        self._run(local_path=local_path, remote_path=remote_path)
1177

    
1178

    
1179
@command(pithos_cmds)
1180
class file_cat(_file_container_command):
1181
    """Print remote file contents to console"""
1182

    
1183
    arguments = dict(
1184
        range=RangeArgument('show range of data', '--range'),
1185
        if_match=ValueArgument('show output if ETags match', '--if-match'),
1186
        if_none_match=ValueArgument(
1187
            'show output if ETags match',
1188
            '--if-none-match'),
1189
        if_modified_since=DateArgument(
1190
            'show output modified since then',
1191
            '--if-modified-since'),
1192
        if_unmodified_since=DateArgument(
1193
            'show output unmodified since then',
1194
            '--if-unmodified-since'),
1195
        object_version=ValueArgument(
1196
            'get the specific version',
1197
            ('-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,
1207
            stdout,
1208
            range_str=self['range'],
1209
            version=self['object_version'],
1210
            if_match=self['if_match'],
1211
            if_none_match=self['if_none_match'],
1212
            if_modified_since=self['if_modified_since'],
1213
            if_unmodified_since=self['if_unmodified_since'])
1214

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

    
1221

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

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

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

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

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

    
1415
    def main(self, container___path, local_path=None):
1416
        super(self.__class__, self)._run(container___path)
1417
        self._run(local_path=local_path)
1418

    
1419

    
1420
@command(pithos_cmds)
1421
class file_hashmap(_file_container_command, _optional_json):
1422
    """Get the hash-map of an object"""
1423

    
1424
    arguments = dict(
1425
        if_match=ValueArgument('show output if ETags match', '--if-match'),
1426
        if_none_match=ValueArgument(
1427
            'show output if ETags match', '--if-none-match'),
1428
        if_modified_since=DateArgument(
1429
            'show output modified since then', '--if-modified-since'),
1430
        if_unmodified_since=DateArgument(
1431
            'show output unmodified since then', '--if-unmodified-since'),
1432
        object_version=ValueArgument(
1433
            'get the specific version', ('-O', '--object-version'))
1434
    )
1435

    
1436
    @errors.generic.all
1437
    @errors.pithos.connection
1438
    @errors.pithos.container
1439
    @errors.pithos.object_path
1440
    def _run(self):
1441
        self._print(self.client.get_object_hashmap(
1442
            self.path,
1443
            version=self['object_version'],
1444
            if_match=self['if_match'],
1445
            if_none_match=self['if_none_match'],
1446
            if_modified_since=self['if_modified_since'],
1447
            if_unmodified_since=self['if_unmodified_since']), print_dict)
1448

    
1449
    def main(self, container___path):
1450
        super(self.__class__, self)._run(
1451
            container___path,
1452
            path_is_optional=False)
1453
        self._run()
1454

    
1455

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

    
1475
    arguments = dict(
1476
        until=DateArgument('remove history until that date', '--until'),
1477
        yes=FlagArgument('Do not prompt for permission', '--yes'),
1478
        recursive=FlagArgument(
1479
            'empty dir or container and delete (if dir)',
1480
            ('-R', '--recursive'))
1481
    )
1482

    
1483
    def __init__(self, arguments={}):
1484
        super(self.__class__, self).__init__(arguments)
1485
        self['delimiter'] = DelimiterArgument(
1486
            self,
1487
            parsed_name='--delimiter',
1488
            help='delete objects prefixed with <object><delimiter>')
1489

    
1490
    @errors.generic.all
1491
    @errors.pithos.connection
1492
    @errors.pithos.container
1493
    @errors.pithos.object_path
1494
    def _run(self):
1495
        if self.path:
1496
            if self['yes'] or ask_user(
1497
                    'Delete %s:%s ?' % (self.container, self.path)):
1498
                self._optional_output(self.client.del_object(
1499
                    self.path,
1500
                    until=self['until'], delimiter=self['delimiter']))
1501
            else:
1502
                print('Aborted')
1503
        else:
1504
            if self['recursive']:
1505
                ask_msg = 'Delete container contents'
1506
            else:
1507
                ask_msg = 'Delete container'
1508
            if self['yes'] or ask_user('%s %s ?' % (ask_msg, self.container)):
1509
                self._optional_output(self.client.del_container(
1510
                    until=self['until'], delimiter=self['delimiter']))
1511
            else:
1512
                print('Aborted')
1513

    
1514
    def main(self, container____path__=None):
1515
        super(self.__class__, self)._run(container____path__)
1516
        self._run()
1517

    
1518

    
1519
@command(pithos_cmds)
1520
class file_purge(_file_container_command, _optional_output_cmd):
1521
    """Delete a container and release related data blocks
1522
    Non-empty containers can not purged.
1523
    To purge a container with content:
1524
    .   /file delete -R <container>
1525
    .      objects are deleted, but data blocks remain on server
1526
    .   /file purge <container>
1527
    .      container and data blocks are released and deleted
1528
    """
1529

    
1530
    arguments = dict(
1531
        yes=FlagArgument('Do not prompt for permission', '--yes'),
1532
        force=FlagArgument('purge even if not empty', ('-F', '--force'))
1533
    )
1534

    
1535
    @errors.generic.all
1536
    @errors.pithos.connection
1537
    @errors.pithos.container
1538
    def _run(self):
1539
        if self['yes'] or ask_user('Purge container %s?' % self.container):
1540
            try:
1541
                r = self.client.purge_container()
1542
            except ClientError as ce:
1543
                if ce.status in (409,):
1544
                    if self['force']:
1545
                        self.client.del_container(delimiter='/')
1546
                        r = self.client.purge_container()
1547
                    else:
1548
                        raiseCLIError(ce, details=['Try -F to force-purge'])
1549
                else:
1550
                    raise
1551
            self._optional_output(r)
1552
        else:
1553
            print('Aborted')
1554

    
1555
    def main(self, container=None):
1556
        super(self.__class__, self)._run(container)
1557
        if container and self.container != container:
1558
            raiseCLIError('Invalid container name %s' % container, details=[
1559
                'Did you mean "%s" ?' % self.container,
1560
                'Use --container for names containing :'])
1561
        self._run()
1562

    
1563

    
1564
@command(pithos_cmds)
1565
class file_publish(_file_container_command):
1566
    """Publish the object and print the public url"""
1567

    
1568
    @errors.generic.all
1569
    @errors.pithos.connection
1570
    @errors.pithos.container
1571
    @errors.pithos.object_path
1572
    def _run(self):
1573
        url = self.client.publish_object(self.path)
1574
        print(url)
1575

    
1576
    def main(self, container___path):
1577
        super(self.__class__, self)._run(
1578
            container___path,
1579
            path_is_optional=False)
1580
        self._run()
1581

    
1582

    
1583
@command(pithos_cmds)
1584
class file_unpublish(_file_container_command, _optional_output_cmd):
1585
    """Unpublish an object"""
1586

    
1587
    @errors.generic.all
1588
    @errors.pithos.connection
1589
    @errors.pithos.container
1590
    @errors.pithos.object_path
1591
    def _run(self):
1592
            self._optional_output(self.client.unpublish_object(self.path))
1593

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

    
1600

    
1601
@command(pithos_cmds)
1602
class file_permissions(_pithos_init):
1603
    """Manage user and group accessibility for objects
1604
    Permissions are lists of users and user groups. There are read and write
1605
    permissions. Users and groups with write permission have also read
1606
    permission.
1607
    """
1608

    
1609

    
1610
def print_permissions(permissions_dict):
1611
    expected_keys = ('read', 'write')
1612
    if set(permissions_dict).issubset(expected_keys):
1613
        print_dict(permissions_dict)
1614
    else:
1615
        invalid_keys = set(permissions_dict.keys()).difference(expected_keys)
1616
        raiseCLIError(
1617
            'Illegal permission keys: %s' % ', '.join(invalid_keys),
1618
            importance=1, details=[
1619
                'Valid permission types: %s' % ' '.join(expected_keys)])
1620

    
1621

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

    
1626
    @errors.generic.all
1627
    @errors.pithos.connection
1628
    @errors.pithos.container
1629
    @errors.pithos.object_path
1630
    def _run(self):
1631
        self._print(
1632
            self.client.get_object_sharing(self.path), print_permissions)
1633

    
1634
    def main(self, container___path):
1635
        super(self.__class__, self)._run(
1636
            container___path,
1637
            path_is_optional=False)
1638
        self._run()
1639

    
1640

    
1641
@command(pithos_cmds)
1642
class file_permissions_set(_file_container_command, _optional_output_cmd):
1643
    """Set permissions for an object
1644
    New permissions overwrite existing permissions.
1645
    Permission format:
1646
    -   read=<username>[,usergroup[,...]]
1647
    -   write=<username>[,usegroup[,...]]
1648
    E.g. to give read permissions for file F to users A and B and write for C:
1649
    .       /file permissions set F read=A,B write=C
1650
    """
1651

    
1652
    @errors.generic.all
1653
    def format_permition_dict(self, permissions):
1654
        read = False
1655
        write = False
1656
        for perms in permissions:
1657
            splstr = perms.split('=')
1658
            if 'read' == splstr[0]:
1659
                read = [ug.strip() for ug in splstr[1].split(',')]
1660
            elif 'write' == splstr[0]:
1661
                write = [ug.strip() for ug in splstr[1].split(',')]
1662
            else:
1663
                msg = 'Usage:\tread=<groups,users> write=<groups,users>'
1664
                raiseCLIError(None, msg)
1665
        return (read, write)
1666

    
1667
    @errors.generic.all
1668
    @errors.pithos.connection
1669
    @errors.pithos.container
1670
    @errors.pithos.object_path
1671
    def _run(self, read, write):
1672
        self._optional_output(self.client.set_object_sharing(
1673
            self.path,
1674
            read_permition=read, write_permition=write))
1675

    
1676
    def main(self, container___path, *permissions):
1677
        super(self.__class__, self)._run(
1678
            container___path,
1679
            path_is_optional=False)
1680
        (read, write) = self.format_permition_dict(permissions)
1681
        self._run(read, write)
1682

    
1683

    
1684
@command(pithos_cmds)
1685
class file_permissions_delete(_file_container_command, _optional_output_cmd):
1686
    """Delete all permissions set on object
1687
    To modify permissions, use /file permissions set
1688
    """
1689

    
1690
    @errors.generic.all
1691
    @errors.pithos.connection
1692
    @errors.pithos.container
1693
    @errors.pithos.object_path
1694
    def _run(self):
1695
        self._optional_output(self.client.del_object_sharing(self.path))
1696

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

    
1703

    
1704
@command(pithos_cmds)
1705
class file_info(_file_container_command, _optional_json):
1706
    """Get detailed information for user account, containers or objects
1707
    to get account info:    /file info
1708
    to get container info:  /file info <container>
1709
    to get object info:     /file info <container>:<path>
1710
    """
1711

    
1712
    arguments = dict(
1713
        object_version=ValueArgument(
1714
            'show specific version \ (applies only for objects)',
1715
            ('-O', '--object-version'))
1716
    )
1717

    
1718
    @errors.generic.all
1719
    @errors.pithos.connection
1720
    @errors.pithos.container
1721
    @errors.pithos.object_path
1722
    def _run(self):
1723
        if self.container is None:
1724
            r = self.client.get_account_info()
1725
        elif self.path is None:
1726
            r = self.client.get_container_info(self.container)
1727
        else:
1728
            r = self.client.get_object_info(
1729
                self.path,
1730
                version=self['object_version'])
1731
        self._print(r, print_dict)
1732

    
1733
    def main(self, container____path__=None):
1734
        super(self.__class__, self)._run(container____path__)
1735
        self._run()
1736

    
1737

    
1738
@command(pithos_cmds)
1739
class file_metadata(_pithos_init):
1740
    """Metadata are attached on objects. They are formed as key:value pairs.
1741
    They can have arbitary values.
1742
    """
1743

    
1744

    
1745
@command(pithos_cmds)
1746
class file_metadata_get(_file_container_command, _optional_json):
1747
    """Get metadata for account, containers or objects"""
1748

    
1749
    arguments = dict(
1750
        detail=FlagArgument('show detailed output', ('-l', '--details')),
1751
        until=DateArgument('show metadata until then', '--until'),
1752
        object_version=ValueArgument(
1753
            'show specific version \ (applies only for objects)',
1754
            ('-O', '--object-version'))
1755
    )
1756

    
1757
    @errors.generic.all
1758
    @errors.pithos.connection
1759
    @errors.pithos.container
1760
    @errors.pithos.object_path
1761
    def _run(self):
1762
        until = self['until']
1763
        r = None
1764
        if self.container is None:
1765
            if self['detail']:
1766
                r = self.client.get_account_info(until=until)
1767
            else:
1768
                r = self.client.get_account_meta(until=until)
1769
                r = pretty_keys(r, '-')
1770
        elif self.path is None:
1771
            if self['detail']:
1772
                r = self.client.get_container_info(until=until)
1773
            else:
1774
                cmeta = self.client.get_container_meta(until=until)
1775
                ometa = self.client.get_container_object_meta(until=until)
1776
                r = {}
1777
                if cmeta:
1778
                    r['container-meta'] = pretty_keys(cmeta, '-')
1779
                if ometa:
1780
                    r['object-meta'] = pretty_keys(ometa, '-')
1781
        else:
1782
            if self['detail']:
1783
                r = self.client.get_object_info(
1784
                    self.path,
1785
                    version=self['object_version'])
1786
            else:
1787
                r = self.client.get_object_meta(
1788
                    self.path,
1789
                    version=self['object_version'])
1790
                r = pretty_keys(pretty_keys(r, '-'))
1791
        if r:
1792
            self._print(r, print_dict)
1793

    
1794
    def main(self, container____path__=None):
1795
        super(self.__class__, self)._run(container____path__)
1796
        self._run()
1797

    
1798

    
1799
@command(pithos_cmds)
1800
class file_metadata_set(_file_container_command, _optional_output_cmd):
1801
    """Set a piece of metadata for account, container or object"""
1802

    
1803
    @errors.generic.all
1804
    @errors.pithos.connection
1805
    @errors.pithos.container
1806
    @errors.pithos.object_path
1807
    def _run(self, metakey, metaval):
1808
        if not self.container:
1809
            r = self.client.set_account_meta({metakey: metaval})
1810
        elif not self.path:
1811
            r = self.client.set_container_meta({metakey: metaval})
1812
        else:
1813
            r = self.client.set_object_meta(self.path, {metakey: metaval})
1814
        self._optional_output(r)
1815

    
1816
    def main(self, metakey, metaval, container____path__=None):
1817
        super(self.__class__, self)._run(container____path__)
1818
        self._run(metakey=metakey, metaval=metaval)
1819

    
1820

    
1821
@command(pithos_cmds)
1822
class file_metadata_delete(_file_container_command, _optional_output_cmd):
1823
    """Delete metadata with given key from account, container or object
1824
    - to get metadata of current account: /file metadata get
1825
    - to get metadata of a container:     /file metadata get <container>
1826
    - to get metadata of an object:       /file metadata get <container>:<path>
1827
    """
1828

    
1829
    @errors.generic.all
1830
    @errors.pithos.connection
1831
    @errors.pithos.container
1832
    @errors.pithos.object_path
1833
    def _run(self, metakey):
1834
        if self.container is None:
1835
            r = self.client.del_account_meta(metakey)
1836
        elif self.path is None:
1837
            r = self.client.del_container_meta(metakey)
1838
        else:
1839
            r = self.client.del_object_meta(self.path, metakey)
1840
        self._optional_output(r)
1841

    
1842
    def main(self, metakey, container____path__=None):
1843
        super(self.__class__, self)._run(container____path__)
1844
        self._run(metakey)
1845

    
1846

    
1847
@command(pithos_cmds)
1848
class file_quota(_file_account_command, _optional_json):
1849
    """Get account quota"""
1850

    
1851
    arguments = dict(
1852
        in_bytes=FlagArgument('Show result in bytes', ('-b', '--bytes'))
1853
    )
1854

    
1855
    @errors.generic.all
1856
    @errors.pithos.connection
1857
    def _run(self):
1858

    
1859
        def pretty_print(output):
1860
            if not self['in_bytes']:
1861
                for k in output:
1862
                    output[k] = format_size(output[k])
1863
            pretty_dict(output, '-')
1864

    
1865
        self._print(self.client.get_account_quota(), pretty_print)
1866

    
1867
    def main(self, custom_uuid=None):
1868
        super(self.__class__, self)._run(custom_account=custom_uuid)
1869
        self._run()
1870

    
1871

    
1872
@command(pithos_cmds)
1873
class file_containerlimit(_pithos_init):
1874
    """Container size limit commands"""
1875

    
1876

    
1877
@command(pithos_cmds)
1878
class file_containerlimit_get(_file_container_command, _optional_json):
1879
    """Get container size limit"""
1880

    
1881
    arguments = dict(
1882
        in_bytes=FlagArgument('Show result in bytes', ('-b', '--bytes'))
1883
    )
1884

    
1885
    @errors.generic.all
1886
    @errors.pithos.container
1887
    def _run(self):
1888

    
1889
        def pretty_print(output):
1890
            if not self['in_bytes']:
1891
                for k, v in output.items():
1892
                    output[k] = 'unlimited' if '0' == v else format_size(v)
1893
            pretty_dict(output, '-')
1894

    
1895
        self._print(
1896
            self.client.get_container_limit(self.container), pretty_print)
1897

    
1898
    def main(self, container=None):
1899
        super(self.__class__, self)._run()
1900
        self.container = container
1901
        self._run()
1902

    
1903

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

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

    
1938
    @errors.generic.all
1939
    @errors.pithos.connection
1940
    @errors.pithos.container
1941
    def _run(self, limit):
1942
        if self.container:
1943
            self.client.container = self.container
1944
        self._optional_output(self.client.set_container_limit(limit))
1945

    
1946
    def main(self, limit, container=None):
1947
        super(self.__class__, self)._run()
1948
        limit = self._calculate_limit(limit)
1949
        self.container = container
1950
        self._run(limit)
1951

    
1952

    
1953
@command(pithos_cmds)
1954
class file_versioning(_pithos_init):
1955
    """Manage the versioning scheme of current pithos user account"""
1956

    
1957

    
1958
@command(pithos_cmds)
1959
class file_versioning_get(_file_account_command, _optional_json):
1960
    """Get  versioning for account or container"""
1961

    
1962
    @errors.generic.all
1963
    @errors.pithos.connection
1964
    @errors.pithos.container
1965
    def _run(self):
1966
        #if self.container:
1967
        #    r = self.client.get_container_versioning(self.container)
1968
        #else:
1969
        #    r = self.client.get_account_versioning()
1970
        self._print(
1971
            self.client.get_container_versioning(self.container) if (
1972
                self.container) else self.client.get_account_versioning(),
1973
            print_dict)
1974

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

    
1980

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

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

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

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

    
2006

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

    
2011

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

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

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

    
2025

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

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

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

    
2042

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

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

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

    
2056

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

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

    
2066
    @errors.generic.all
2067
    @errors.pithos.connection
2068
    def _run(self):
2069
        accounts = self.client.get_sharing_accounts(marker=self['marker'])
2070
        if self['json_output'] or self['detail']:
2071
            self._print(accounts)
2072
        else:
2073
            self._print([acc['name'] for acc in accounts])
2074

    
2075
    def main(self):
2076
        super(self.__class__, self)._run()
2077
        self._run()
2078

    
2079

    
2080
def version_print(versions):
2081
    print_items([dict(id=vitem[0], created=strftime(
2082
        '%d-%m-%Y %H:%M:%S',
2083
        localtime(float(vitem[1])))) for vitem in versions])
2084

    
2085

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

    
2096
    @errors.generic.all
2097
    @errors.pithos.connection
2098
    @errors.pithos.container
2099
    @errors.pithos.object_path
2100
    def _run(self):
2101
        self._print(
2102
            self.client.get_object_versionlist(self.path), version_print)
2103

    
2104
    def main(self, container___path):
2105
        super(file_versions, self)._run(
2106
            container___path,
2107
            path_is_optional=False)
2108
        self._run()