Statistics
| Branch: | Tag: | Revision:

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

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
        pithos_endpoints = self.auth_base.get_service_endpoints(
158
            self.config.get('pithos', 'type'),
159
            self.config.get('pithos', 'version'))
160
        self.base_url = pithos_endpoints['publicURL']
161
        self._set_account()
162
        self.container = self.config.get('file', 'container')\
163
            or self.config.get('global', 'container')
164
        self.client = PithosClient(
165
            base_url=self.base_url,
166
            token=self.token,
167
            account=self.account,
168
            container=self.container)
169
        self._set_log_params()
170
        self._update_max_threads()
171

    
172
    def main(self):
173
        self._run()
174

    
175
    def _set_account(self):
176
        self.account = self.auth_base.user_term('uuid', self.token)
177

    
178

    
179
class _file_account_command(_pithos_init):
180
    """Base class for account level storage commands"""
181

    
182
    def __init__(self, arguments={}, auth_base=None):
183
        super(_file_account_command, self).__init__(arguments, auth_base)
184
        self['account'] = ValueArgument(
185
            'Set user account (not permanent)',
186
            ('-A', '--account'))
187

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

    
195
    @errors.generic.all
196
    def main(self):
197
        self._run()
198

    
199

    
200
class _file_container_command(_file_account_command):
201
    """Base class for container level storage commands"""
202

    
203
    container = None
204
    path = None
205

    
206
    def __init__(self, arguments={}, auth_base=None):
207
        super(_file_container_command, self).__init__(arguments, auth_base)
208
        self['container'] = ValueArgument(
209
            'Set container to work with (temporary)',
210
            ('-C', '--container'))
211

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

    
232
        user_cont, sep, userpath = container_with_path.partition(':')
233

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

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

    
285
    def main(self, container_with_path=None, path_is_optional=True):
286
        self._run(container_with_path, path_is_optional)
287

    
288

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

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

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

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

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

    
422
    def main(self, container____path__=None):
423
        super(self.__class__, self)._run(container____path__)
424
        self._run()
425

    
426

    
427
@command(pithos_cmds)
428
class file_mkdir(_file_container_command, _optional_output_cmd):
429
    """Create a directory"""
430

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

    
438
    @errors.generic.all
439
    @errors.pithos.connection
440
    @errors.pithos.container
441
    def _run(self):
442
        self._optional_output(self.client.create_directory(self.path))
443

    
444
    def main(self, container___directory):
445
        super(self.__class__, self)._run(
446
            container___directory,
447
            path_is_optional=False)
448
        self._run()
449

    
450

    
451
@command(pithos_cmds)
452
class file_touch(_file_container_command, _optional_output_cmd):
453
    """Create an empty object (file)
454
    If object exists, this command will reset it to 0 length
455
    """
456

    
457
    arguments = dict(
458
        content_type=ValueArgument(
459
            'Set content type (default: application/octet-stream)',
460
            '--content-type',
461
            default='application/octet-stream')
462
    )
463

    
464
    @errors.generic.all
465
    @errors.pithos.connection
466
    @errors.pithos.container
467
    def _run(self):
468
        self._optional_output(
469
            self.client.create_object(self.path, self['content_type']))
470

    
471
    def main(self, container___path):
472
        super(file_touch, self)._run(
473
            container___path,
474
            path_is_optional=False)
475
        self._run()
476

    
477

    
478
@command(pithos_cmds)
479
class file_create(_file_container_command, _optional_output_cmd):
480
    """Create a container"""
481

    
482
    arguments = dict(
483
        versioning=ValueArgument(
484
            'set container versioning (auto/none)',
485
            '--versioning'),
486
        limit=IntArgument('set default container limit', '--limit'),
487
        meta=KeyValueArgument(
488
            'set container metadata (can be repeated)',
489
            '--meta')
490
    )
491

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

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

    
510

    
511
class _source_destination_command(_file_container_command):
512

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

    
524
    def __init__(self, arguments={}, auth_base=None):
525
        self.arguments.update(arguments)
526
        super(_source_destination_command, self).__init__(
527
            self.arguments, auth_base)
528

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

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

    
561
    def _get_all(self, prefix):
562
        return self.client.container_get(prefix=prefix).json
563

    
564
    def _get_src_objects(self, src_path, source_version=None):
565
        """Get a list of the source objects to be called
566

567
        :param src_path: (str) source path
568

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

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

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

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

    
607
        if dst_path and dst_path.endswith('/'):
608
            dst_path = dst_path[:-1]
609

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

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

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

    
655

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

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

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

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

    
747

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

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

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

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

    
835

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

    
844
    arguments = dict(
845
        progress_bar=ProgressBarArgument(
846
            'do not show progress bar',
847
            ('-N', '--no-progress-bar'),
848
            default=False)
849
    )
850

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

    
867
    def main(self, local_path, container___path):
868
        super(self.__class__, self)._run(
869
            container___path,
870
            path_is_optional=False)
871
        self._run(local_path)
872

    
873

    
874
@command(pithos_cmds)
875
class file_truncate(_file_container_command, _optional_output_cmd):
876
    """Truncate remote file up to a size (default is 0)"""
877

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

    
886
    def main(self, container___path, size=0):
887
        super(self.__class__, self)._run(container___path)
888
        self._run(size=size)
889

    
890

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

    
901
    arguments = dict(
902
        progress_bar=ProgressBarArgument(
903
            'do not show progress bar',
904
            ('-N', '--no-progress-bar'),
905
            default=False)
906
    )
907

    
908
    def _open_file(self, local_path, start):
909
        f = open(path.abspath(local_path), 'rb')
910
        f.seek(0, 2)
911
        f_size = f.tell()
912
        f.seek(start, 0)
913
        return (f, f_size)
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, local_path, start, end):
921
        (start, end) = (int(start), int(end))
922
        (f, f_size) = self._open_file(local_path, start)
923
        (progress_bar, upload_cb) = self._safe_progress_bar(
924
            'Overwrite %s bytes' % (end - start))
925
        try:
926
            self._optional_output(self.client.overwrite_object(
927
                obj=self.path,
928
                start=start,
929
                end=end,
930
                source_file=f,
931
                upload_cb=upload_cb))
932
        finally:
933
            self._safe_progress_bar_finish(progress_bar)
934

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

    
942

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

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

    
976
    @errors.generic.all
977
    @errors.pithos.connection
978
    @errors.pithos.container
979
    @errors.pithos.object_path
980
    def _run(self):
981
        self._optional_output(self.client.create_object_by_manifestation(
982
            self.path,
983
            content_encoding=self['content_encoding'],
984
            content_disposition=self['content_disposition'],
985
            content_type=self['content_type'],
986
            sharing=self['sharing'],
987
            public=self['public']))
988

    
989
    def main(self, container___path):
990
        super(self.__class__, self)._run(
991
            container___path,
992
            path_is_optional=False)
993
        self.run()
994

    
995

    
996
@command(pithos_cmds)
997
class file_upload(_file_container_command, _optional_output_cmd):
998
    """Upload a file"""
999

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

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

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

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

    
1170
    def main(self, local_path, container____path__=None):
1171
        super(self.__class__, self)._run(container____path__)
1172
        remote_path = self.path or path.basename(local_path)
1173
        self._run(local_path=local_path, remote_path=remote_path)
1174

    
1175

    
1176
@command(pithos_cmds)
1177
class file_cat(_file_container_command):
1178
    """Print remote file contents to console"""
1179

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

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

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

    
1218

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

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

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

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

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

    
1412
    def main(self, container___path, local_path=None):
1413
        super(self.__class__, self)._run(container___path)
1414
        self._run(local_path=local_path)
1415

    
1416

    
1417
@command(pithos_cmds)
1418
class file_hashmap(_file_container_command, _optional_json):
1419
    """Get the hash-map of an object"""
1420

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

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

    
1446
    def main(self, container___path):
1447
        super(self.__class__, self)._run(
1448
            container___path,
1449
            path_is_optional=False)
1450
        self._run()
1451

    
1452

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

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

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

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

    
1511
    def main(self, container____path__=None):
1512
        super(self.__class__, self)._run(container____path__)
1513
        self._run()
1514

    
1515

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

    
1527
    arguments = dict(
1528
        yes=FlagArgument('Do not prompt for permission', '--yes'),
1529
        force=FlagArgument('purge even if not empty', ('-F', '--force'))
1530
    )
1531

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

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

    
1560

    
1561
@command(pithos_cmds)
1562
class file_publish(_file_container_command):
1563
    """Publish the object and print the public url"""
1564

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

    
1573
    def main(self, container___path):
1574
        super(self.__class__, self)._run(
1575
            container___path,
1576
            path_is_optional=False)
1577
        self._run()
1578

    
1579

    
1580
@command(pithos_cmds)
1581
class file_unpublish(_file_container_command, _optional_output_cmd):
1582
    """Unpublish an object"""
1583

    
1584
    @errors.generic.all
1585
    @errors.pithos.connection
1586
    @errors.pithos.container
1587
    @errors.pithos.object_path
1588
    def _run(self):
1589
            self._optional_output(self.client.unpublish_object(self.path))
1590

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

    
1597

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

    
1606

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

    
1618

    
1619
@command(pithos_cmds)
1620
class file_permissions_get(_file_container_command, _optional_json):
1621
    """Get read and write permissions of an object"""
1622

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

    
1631
    def main(self, container___path):
1632
        super(self.__class__, self)._run(
1633
            container___path,
1634
            path_is_optional=False)
1635
        self._run()
1636

    
1637

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

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

    
1664
    @errors.generic.all
1665
    @errors.pithos.connection
1666
    @errors.pithos.container
1667
    @errors.pithos.object_path
1668
    def _run(self, read, write):
1669
        self._optional_output(self.client.set_object_sharing(
1670
            self.path,
1671
            read_permission=read, write_permission=write))
1672

    
1673
    def main(self, container___path, *permissions):
1674
        super(self.__class__, self)._run(
1675
            container___path,
1676
            path_is_optional=False)
1677
        (read, write) = self.format_permission_dict(permissions)
1678
        self._run(read, write)
1679

    
1680

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

    
1687
    @errors.generic.all
1688
    @errors.pithos.connection
1689
    @errors.pithos.container
1690
    @errors.pithos.object_path
1691
    def _run(self):
1692
        self._optional_output(self.client.del_object_sharing(self.path))
1693

    
1694
    def main(self, container___path):
1695
        super(self.__class__, self)._run(
1696
            container___path,
1697
            path_is_optional=False)
1698
        self._run()
1699

    
1700

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

    
1709
    arguments = dict(
1710
        object_version=ValueArgument(
1711
            'show specific version \ (applies only for objects)',
1712
            ('-O', '--object-version'))
1713
    )
1714

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

    
1730
    def main(self, container____path__=None):
1731
        super(self.__class__, self)._run(container____path__)
1732
        self._run()
1733

    
1734

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

    
1741

    
1742
@command(pithos_cmds)
1743
class file_metadata_get(_file_container_command, _optional_json):
1744
    """Get metadata for account, containers or objects"""
1745

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

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

    
1791
    def main(self, container____path__=None):
1792
        super(self.__class__, self)._run(container____path__)
1793
        self._run()
1794

    
1795

    
1796
@command(pithos_cmds)
1797
class file_metadata_set(_file_container_command, _optional_output_cmd):
1798
    """Set a piece of metadata for account, container or object"""
1799

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

    
1813
    def main(self, metakey, metaval, container____path__=None):
1814
        super(self.__class__, self)._run(container____path__)
1815
        self._run(metakey=metakey, metaval=metaval)
1816

    
1817

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

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

    
1839
    def main(self, metakey, container____path__=None):
1840
        super(self.__class__, self)._run(container____path__)
1841
        self._run(metakey)
1842

    
1843

    
1844
@command(pithos_cmds)
1845
class file_quota(_file_account_command, _optional_json):
1846
    """Get account quota"""
1847

    
1848
    arguments = dict(
1849
        in_bytes=FlagArgument('Show result in bytes', ('-b', '--bytes'))
1850
    )
1851

    
1852
    @errors.generic.all
1853
    @errors.pithos.connection
1854
    def _run(self):
1855

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

    
1862
        self._print(self.client.get_account_quota(), pretty_print)
1863

    
1864
    def main(self, custom_uuid=None):
1865
        super(self.__class__, self)._run(custom_account=custom_uuid)
1866
        self._run()
1867

    
1868

    
1869
@command(pithos_cmds)
1870
class file_containerlimit(_pithos_init):
1871
    """Container size limit commands"""
1872

    
1873

    
1874
@command(pithos_cmds)
1875
class file_containerlimit_get(_file_container_command, _optional_json):
1876
    """Get container size limit"""
1877

    
1878
    arguments = dict(
1879
        in_bytes=FlagArgument('Show result in bytes', ('-b', '--bytes'))
1880
    )
1881

    
1882
    @errors.generic.all
1883
    @errors.pithos.container
1884
    def _run(self):
1885

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

    
1892
        self._print(
1893
            self.client.get_container_limit(self.container), pretty_print)
1894

    
1895
    def main(self, container=None):
1896
        super(self.__class__, self)._run()
1897
        self.container = container
1898
        self._run()
1899

    
1900

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

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

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

    
1943
    def main(self, limit, container=None):
1944
        super(self.__class__, self)._run()
1945
        limit = self._calculate_limit(limit)
1946
        self.container = container
1947
        self._run(limit)
1948

    
1949

    
1950
@command(pithos_cmds)
1951
class file_versioning(_pithos_init):
1952
    """Manage the versioning scheme of current pithos user account"""
1953

    
1954

    
1955
@command(pithos_cmds)
1956
class file_versioning_get(_file_account_command, _optional_json):
1957
    """Get  versioning for account or container"""
1958

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

    
1972
    def main(self, container=None):
1973
        super(self.__class__, self)._run()
1974
        self.container = container
1975
        self._run()
1976

    
1977

    
1978
@command(pithos_cmds)
1979
class file_versioning_set(_file_account_command, _optional_output_cmd):
1980
    """Set versioning mode (auto, none) for account or container"""
1981

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

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

    
1999
    def main(self, versioning, container=None):
2000
        super(self.__class__, self)._run()
2001
        self._run(self._check_versioning(versioning))
2002

    
2003

    
2004
@command(pithos_cmds)
2005
class file_group(_pithos_init):
2006
    """Manage access groups and group members"""
2007

    
2008

    
2009
@command(pithos_cmds)
2010
class file_group_list(_file_account_command, _optional_json):
2011
    """list all groups and group members"""
2012

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

    
2018
    def main(self):
2019
        super(self.__class__, self)._run()
2020
        self._run()
2021

    
2022

    
2023
@command(pithos_cmds)
2024
class file_group_set(_file_account_command, _optional_output_cmd):
2025
    """Set a user group"""
2026

    
2027
    @errors.generic.all
2028
    @errors.pithos.connection
2029
    def _run(self, groupname, *users):
2030
        self._optional_output(self.client.set_account_group(groupname, users))
2031

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

    
2039

    
2040
@command(pithos_cmds)
2041
class file_group_delete(_file_account_command, _optional_output_cmd):
2042
    """Delete a user group"""
2043

    
2044
    @errors.generic.all
2045
    @errors.pithos.connection
2046
    def _run(self, groupname):
2047
        self._optional_output(self.client.del_account_group(groupname))
2048

    
2049
    def main(self, groupname):
2050
        super(self.__class__, self)._run()
2051
        self._run(groupname)
2052

    
2053

    
2054
@command(pithos_cmds)
2055
class file_sharers(_file_account_command, _optional_json):
2056
    """List the accounts that share objects with current user"""
2057

    
2058
    arguments = dict(
2059
        detail=FlagArgument('show detailed output', ('-l', '--details')),
2060
        marker=ValueArgument('show output greater then marker', '--marker')
2061
    )
2062

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

    
2072
    def main(self):
2073
        super(self.__class__, self)._run()
2074
        self._run()
2075

    
2076

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

    
2082

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

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

    
2101
    def main(self, container___path):
2102
        super(file_versions, self)._run(
2103
            container___path,
2104
            path_is_optional=False)
2105
        self._run()