Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / pithos.py @ 54b6be76

History | View | Annotate | Download (77.1 kB)

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

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

    
38
from kamaki.cli import command
39
from kamaki.cli.command_tree import CommandTree
40
from kamaki.cli.errors import raiseCLIError, CLISyntaxError, CLIBaseUrlError
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', remote_dict.get('content-type', ''))
151

    
152
    @errors.generic.all
153
    def _run(self):
154
        self.token = self.config.get('file', 'token')\
155
            or self.config.get('global', 'token')
156

    
157
        if getattr(self, 'auth_base', False):
158
            pithos_endpoints = self.auth_base.get_service_endpoints(
159
                self.config.get('pithos', 'type'),
160
                self.config.get('pithos', 'version'))
161
            self.base_url = pithos_endpoints['publicURL']
162
        else:
163
            self.base_url = self.config.get('pithos', 'url')
164
        if not self.base_url:
165
            raise CLIBaseUrlError(service='pithos')
166

    
167
        self._set_account()
168
        self.container = self.config.get('file', 'container')\
169
            or self.config.get('global', 'container')
170
        self.client = PithosClient(
171
            base_url=self.base_url,
172
            token=self.token,
173
            account=self.account,
174
            container=self.container)
175
        self._set_log_params()
176
        self._update_max_threads()
177

    
178
    def main(self):
179
        self._run()
180

    
181
    def _set_account(self):
182
        self.account = self.auth_base.user_term('uuid', self.token)
183

    
184

    
185
class _file_account_command(_pithos_init):
186
    """Base class for account level storage commands"""
187

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

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

    
201
    @errors.generic.all
202
    def main(self):
203
        self._run()
204

    
205

    
206
class _file_container_command(_file_account_command):
207
    """Base class for container level storage commands"""
208

    
209
    container = None
210
    path = None
211

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

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

    
238
        user_cont, sep, userpath = container_with_path.partition(':')
239

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

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

    
291
    def main(self, container_with_path=None, path_is_optional=True):
292
        self._run(container_with_path, path_is_optional)
293

    
294

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

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

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

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

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

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

    
432

    
433
@command(pithos_cmds)
434
class file_mkdir(_file_container_command, _optional_output_cmd):
435
    """Create a directory"""
436

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

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

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

    
456

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

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

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

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

    
483

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

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

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

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

    
516

    
517
class _source_destination_command(_file_container_command):
518

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

    
530
    def __init__(self, arguments={}, auth_base=None):
531
        self.arguments.update(arguments)
532
        super(_source_destination_command, self).__init__(
533
            self.arguments, auth_base)
534

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

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

    
567
    def _get_all(self, prefix):
568
        return self.client.container_get(prefix=prefix).json
569

    
570
    def _get_src_objects(self, src_path, source_version=None):
571
        """Get a list of the source objects to be called
572

573
        :param src_path: (str) source path
574

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

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

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

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

    
613
        if dst_path and dst_path.endswith('/'):
614
            dst_path = dst_path[:-1]
615

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

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

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

    
661

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

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

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

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

    
753

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

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

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

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

    
841

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

    
850
    arguments = dict(
851
        progress_bar=ProgressBarArgument(
852
            'do not show progress bar',
853
            ('-N', '--no-progress-bar'),
854
            default=False)
855
    )
856

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

    
873
    def main(self, local_path, container___path):
874
        super(self.__class__, self)._run(
875
            container___path,
876
            path_is_optional=False)
877
        self._run(local_path)
878

    
879

    
880
@command(pithos_cmds)
881
class file_truncate(_file_container_command, _optional_output_cmd):
882
    """Truncate remote file up to a size (default is 0)"""
883

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

    
892
    def main(self, container___path, size=0):
893
        super(self.__class__, self)._run(container___path)
894
        self._run(size=size)
895

    
896

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

    
907
    arguments = dict(
908
        progress_bar=ProgressBarArgument(
909
            'do not show progress bar',
910
            ('-N', '--no-progress-bar'),
911
            default=False)
912
    )
913

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

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

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

    
948

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

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

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

    
995
    def main(self, container___path):
996
        super(self.__class__, self)._run(
997
            container___path,
998
            path_is_optional=False)
999
        self.run()
1000

    
1001

    
1002
@command(pithos_cmds)
1003
class file_upload(_file_container_command, _optional_output_cmd):
1004
    """Upload a file"""
1005

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

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

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

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

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

    
1181

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

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

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

    
1218
    def main(self, container___path):
1219
        super(self.__class__, self)._run(
1220
            container___path,
1221
            path_is_optional=False)
1222
        self._run()
1223

    
1224

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

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

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

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

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

    
1418
    def main(self, container___path, local_path=None):
1419
        super(self.__class__, self)._run(container___path)
1420
        self._run(local_path=local_path)
1421

    
1422

    
1423
@command(pithos_cmds)
1424
class file_hashmap(_file_container_command, _optional_json):
1425
    """Get the hash-map of an object"""
1426

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

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

    
1452
    def main(self, container___path):
1453
        super(self.__class__, self)._run(
1454
            container___path,
1455
            path_is_optional=False)
1456
        self._run()
1457

    
1458

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

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

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

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

    
1517
    def main(self, container____path__=None):
1518
        super(self.__class__, self)._run(container____path__)
1519
        self._run()
1520

    
1521

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

    
1533
    arguments = dict(
1534
        yes=FlagArgument('Do not prompt for permission', '--yes'),
1535
        force=FlagArgument('purge even if not empty', ('-F', '--force'))
1536
    )
1537

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

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

    
1566

    
1567
@command(pithos_cmds)
1568
class file_publish(_file_container_command):
1569
    """Publish the object and print the public url"""
1570

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

    
1579
    def main(self, container___path):
1580
        super(self.__class__, self)._run(
1581
            container___path,
1582
            path_is_optional=False)
1583
        self._run()
1584

    
1585

    
1586
@command(pithos_cmds)
1587
class file_unpublish(_file_container_command, _optional_output_cmd):
1588
    """Unpublish an object"""
1589

    
1590
    @errors.generic.all
1591
    @errors.pithos.connection
1592
    @errors.pithos.container
1593
    @errors.pithos.object_path
1594
    def _run(self):
1595
            self._optional_output(self.client.unpublish_object(self.path))
1596

    
1597
    def main(self, container___path):
1598
        super(self.__class__, self)._run(
1599
            container___path,
1600
            path_is_optional=False)
1601
        self._run()
1602

    
1603

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

    
1612

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

    
1624

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

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

    
1637
    def main(self, container___path):
1638
        super(self.__class__, self)._run(
1639
            container___path,
1640
            path_is_optional=False)
1641
        self._run()
1642

    
1643

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

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

    
1670
    @errors.generic.all
1671
    @errors.pithos.connection
1672
    @errors.pithos.container
1673
    @errors.pithos.object_path
1674
    def _run(self, read, write):
1675
        self._optional_output(self.client.set_object_sharing(
1676
            self.path,
1677
            read_permission=read, write_permission=write))
1678

    
1679
    def main(self, container___path, *permissions):
1680
        super(self.__class__, self)._run(
1681
            container___path,
1682
            path_is_optional=False)
1683
        (read, write) = self.format_permission_dict(permissions)
1684
        self._run(read, write)
1685

    
1686

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

    
1693
    @errors.generic.all
1694
    @errors.pithos.connection
1695
    @errors.pithos.container
1696
    @errors.pithos.object_path
1697
    def _run(self):
1698
        self._optional_output(self.client.del_object_sharing(self.path))
1699

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

    
1706

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

    
1715
    arguments = dict(
1716
        object_version=ValueArgument(
1717
            'show specific version \ (applies only for objects)',
1718
            ('-O', '--object-version'))
1719
    )
1720

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

    
1736
    def main(self, container____path__=None):
1737
        super(self.__class__, self)._run(container____path__)
1738
        self._run()
1739

    
1740

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

    
1747

    
1748
@command(pithos_cmds)
1749
class file_metadata_get(_file_container_command, _optional_json):
1750
    """Get metadata for account, containers or objects"""
1751

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

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

    
1797
    def main(self, container____path__=None):
1798
        super(self.__class__, self)._run(container____path__)
1799
        self._run()
1800

    
1801

    
1802
@command(pithos_cmds)
1803
class file_metadata_set(_file_container_command, _optional_output_cmd):
1804
    """Set a piece of metadata for account, container or object"""
1805

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

    
1819
    def main(self, metakey, metaval, container____path__=None):
1820
        super(self.__class__, self)._run(container____path__)
1821
        self._run(metakey=metakey, metaval=metaval)
1822

    
1823

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

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

    
1845
    def main(self, metakey, container____path__=None):
1846
        super(self.__class__, self)._run(container____path__)
1847
        self._run(metakey)
1848

    
1849

    
1850
@command(pithos_cmds)
1851
class file_quota(_file_account_command, _optional_json):
1852
    """Get account quota"""
1853

    
1854
    arguments = dict(
1855
        in_bytes=FlagArgument('Show result in bytes', ('-b', '--bytes'))
1856
    )
1857

    
1858
    @errors.generic.all
1859
    @errors.pithos.connection
1860
    def _run(self):
1861

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

    
1868
        self._print(self.client.get_account_quota(), pretty_print)
1869

    
1870
    def main(self, custom_uuid=None):
1871
        super(self.__class__, self)._run(custom_account=custom_uuid)
1872
        self._run()
1873

    
1874

    
1875
@command(pithos_cmds)
1876
class file_containerlimit(_pithos_init):
1877
    """Container size limit commands"""
1878

    
1879

    
1880
@command(pithos_cmds)
1881
class file_containerlimit_get(_file_container_command, _optional_json):
1882
    """Get container size limit"""
1883

    
1884
    arguments = dict(
1885
        in_bytes=FlagArgument('Show result in bytes', ('-b', '--bytes'))
1886
    )
1887

    
1888
    @errors.generic.all
1889
    @errors.pithos.container
1890
    def _run(self):
1891

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

    
1898
        self._print(
1899
            self.client.get_container_limit(self.container), pretty_print)
1900

    
1901
    def main(self, container=None):
1902
        super(self.__class__, self)._run()
1903
        self.container = container
1904
        self._run()
1905

    
1906

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

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

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

    
1949
    def main(self, limit, container=None):
1950
        super(self.__class__, self)._run()
1951
        limit = self._calculate_limit(limit)
1952
        self.container = container
1953
        self._run(limit)
1954

    
1955

    
1956
@command(pithos_cmds)
1957
class file_versioning(_pithos_init):
1958
    """Manage the versioning scheme of current pithos user account"""
1959

    
1960

    
1961
@command(pithos_cmds)
1962
class file_versioning_get(_file_account_command, _optional_json):
1963
    """Get  versioning for account or container"""
1964

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

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

    
1983

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

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

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

    
2005
    def main(self, versioning, container=None):
2006
        super(self.__class__, self)._run()
2007
        self._run(self._check_versioning(versioning))
2008

    
2009

    
2010
@command(pithos_cmds)
2011
class file_group(_pithos_init):
2012
    """Manage access groups and group members"""
2013

    
2014

    
2015
@command(pithos_cmds)
2016
class file_group_list(_file_account_command, _optional_json):
2017
    """list all groups and group members"""
2018

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

    
2024
    def main(self):
2025
        super(self.__class__, self)._run()
2026
        self._run()
2027

    
2028

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

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

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

    
2045

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

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

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

    
2059

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

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

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

    
2078
    def main(self):
2079
        super(self.__class__, self)._run()
2080
        self._run()
2081

    
2082

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

    
2088

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

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

    
2107
    def main(self, container___path):
2108
        super(file_versions, self)._run(
2109
            container___path,
2110
            path_is_optional=False)
2111
        self._run()