Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / pithos.py @ 201baa17

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, 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

    
142
# Command specs
143

    
144

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

    
148
    @staticmethod
149
    def _is_dir(remote_dict):
150
        return 'application/directory' == remote_dict.get(
151
            'content_type', 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

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

    
170
        self._set_account()
171
        self.container = self.config.get('file', 'container')\
172
            or self.config.get('store', 'container')\
173
            or self.config.get('pithos', 'container')\
174
            or self.config.get('global', 'container')
175
        self.client = PithosClient(
176
            base_url=self.base_url,
177
            token=self.token,
178
            account=self.account,
179
            container=self.container)
180
        self._set_log_params()
181
        self._update_max_threads()
182

    
183
    def main(self):
184
        self._run()
185

    
186
    def _set_account(self):
187
        if getattr(self, 'auth_base', False):
188
            self.account = self.auth_base.user_term('id', self.token)
189
        else:
190
            astakos_url = self.config.get('user', 'url')\
191
                or self.config.get('astakos', 'url')
192
            if not astakos_url:
193
                raise CLIBaseUrlError(service='astakos')
194
            astakos = AstakosClient(astakos_url, self.token)
195
            self.account = astakos.user_term('id')
196

    
197

    
198
class _file_account_command(_pithos_init):
199
    """Base class for account level storage commands"""
200

    
201
    def __init__(self, arguments={}, auth_base=None):
202
        super(_file_account_command, self).__init__(arguments, auth_base)
203
        self['account'] = ValueArgument(
204
            'Set user account (not permanent)', ('-A', '--account'))
205

    
206
    def _run(self, custom_account=None):
207
        super(_file_account_command, self)._run()
208
        if custom_account:
209
            self.client.account = custom_account
210
        elif self['account']:
211
            self.client.account = self['account']
212

    
213
    @errors.generic.all
214
    def main(self):
215
        self._run()
216

    
217

    
218
class _file_container_command(_file_account_command):
219
    """Base class for container level storage commands"""
220

    
221
    container = None
222
    path = None
223

    
224
    def __init__(self, arguments={}, auth_base=None):
225
        super(_file_container_command, self).__init__(arguments, auth_base)
226
        self['container'] = ValueArgument(
227
            'Set container to work with (temporary)', ('-C', '--container'))
228

    
229
    def extract_container_and_path(
230
            self,
231
            container_with_path,
232
            path_is_optional=True):
233
        """Contains all heuristics for deciding what should be used as
234
        container or path. Options are:
235
        * user string of the form container:path
236
        * self.container, self.path variables set by super constructor, or
237
        explicitly by the caller application
238
        Error handling is explicit as these error cases happen only here
239
        """
240
        try:
241
            assert isinstance(container_with_path, str)
242
        except AssertionError as err:
243
            if self['container'] and path_is_optional:
244
                self.container = self['container']
245
                self.client.container = self['container']
246
                return
247
            raiseCLIError(err)
248

    
249
        user_cont, sep, userpath = container_with_path.partition(':')
250

    
251
        if sep:
252
            if not user_cont:
253
                raiseCLIError(CLISyntaxError(
254
                    'Container is missing\n',
255
                    details=errors.pithos.container_howto))
256
            alt_cont = self['container']
257
            if alt_cont and user_cont != alt_cont:
258
                raiseCLIError(CLISyntaxError(
259
                    'Conflict: 2 containers (%s, %s)' % (user_cont, alt_cont),
260
                    details=errors.pithos.container_howto)
261
                )
262
            self.container = user_cont
263
            if not userpath:
264
                raiseCLIError(CLISyntaxError(
265
                    'Path is missing for object in container %s' % user_cont,
266
                    details=errors.pithos.container_howto)
267
                )
268
            self.path = userpath
269
        else:
270
            alt_cont = self['container'] or self.client.container
271
            if alt_cont:
272
                self.container = alt_cont
273
                self.path = user_cont
274
            elif path_is_optional:
275
                self.container = user_cont
276
                self.path = None
277
            else:
278
                self.container = user_cont
279
                raiseCLIError(CLISyntaxError(
280
                    'Both container and path are required',
281
                    details=errors.pithos.container_howto)
282
                )
283

    
284
    @errors.generic.all
285
    def _run(self, container_with_path=None, path_is_optional=True):
286
        super(_file_container_command, self)._run()
287
        if self['container']:
288
            self.client.container = self['container']
289
            if container_with_path:
290
                self.path = container_with_path
291
            elif not path_is_optional:
292
                raise CLISyntaxError(
293
                    'Both container and path are required',
294
                    details=errors.pithos.container_howto)
295
        elif container_with_path:
296
            self.extract_container_and_path(
297
                container_with_path,
298
                path_is_optional)
299
            self.client.container = self.container
300
        self.container = self.client.container
301

    
302
    def main(self, container_with_path=None, path_is_optional=True):
303
        self._run(container_with_path, path_is_optional)
304

    
305

    
306
@command(pithos_cmds)
307
class file_list(_file_container_command, _optional_json):
308
    """List containers, object trees or objects in a directory
309
    Use with:
310
    1 no parameters : containers in current account
311
    2. one parameter (container) or --container : contents of container
312
    3. <container>:<prefix> or --container=<container> <prefix>: objects in
313
    .   container starting with prefix
314
    """
315

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

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

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

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

    
434
    def main(self, container____path__=None):
435
        super(self.__class__, self)._run(container____path__)
436
        self._run()
437

    
438

    
439
@command(pithos_cmds)
440
class file_mkdir(_file_container_command, _optional_output_cmd):
441
    """Create a directory"""
442

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

    
450
    @errors.generic.all
451
    @errors.pithos.connection
452
    @errors.pithos.container
453
    def _run(self):
454
        self._optional_output(self.client.create_directory(self.path))
455

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

    
462

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

    
469
    arguments = dict(
470
        content_type=ValueArgument(
471
            'Set content type (default: application/octet-stream)',
472
            '--content-type',
473
            default='application/octet-stream')
474
    )
475

    
476
    @errors.generic.all
477
    @errors.pithos.connection
478
    @errors.pithos.container
479
    def _run(self):
480
        self._optional_output(
481
            self.client.create_object(self.path, self['content_type']))
482

    
483
    def main(self, container___path):
484
        super(file_touch, self)._run(
485
            container___path,
486
            path_is_optional=False)
487
        self._run()
488

    
489

    
490
@command(pithos_cmds)
491
class file_create(_file_container_command, _optional_output_cmd):
492
    """Create a container"""
493

    
494
    arguments = dict(
495
        versioning=ValueArgument(
496
            'set container versioning (auto/none)', '--versioning'),
497
        limit=IntArgument('set default container limit', '--limit'),
498
        meta=KeyValueArgument(
499
            'set container metadata (can be repeated)', '--meta')
500
    )
501

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

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

    
520

    
521
class _source_destination_command(_file_container_command):
522

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

    
534
    def __init__(self, arguments={}, auth_base=None):
535
        self.arguments.update(arguments)
536
        super(_source_destination_command, self).__init__(
537
            self.arguments, auth_base)
538

    
539
    def _run(self, source_container___path, path_is_optional=False):
540
        super(_source_destination_command, self)._run(
541
            source_container___path,
542
            path_is_optional)
543
        self.dst_client = PithosClient(
544
            base_url=self.client.base_url,
545
            token=self.client.token,
546
            account=self['destination_account'] or self.client.account)
547

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

    
571
    def _get_all(self, prefix):
572
        return self.client.container_get(prefix=prefix).json
573

    
574
    def _get_src_objects(self, src_path, source_version=None):
575
        """Get a list of the source objects to be called
576

577
        :param src_path: (str) source path
578

579
        :returns: (method, params) a method that returns a list when called
580
        or (object) if it is a single object
581
        """
582
        if src_path and src_path[-1] == '/':
583
            src_path = src_path[:-1]
584

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

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

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

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

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

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

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

    
662

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

    
680
    arguments = dict(
681
        destination_account=ValueArgument(
682
            'Account to copy to', ('-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', '--content-type'),
689
        recursive=FlagArgument(
690
            'copy directory and contents', ('-R', '--recursive')),
691
        prefix=FlagArgument(
692
            'Match objects prefixed with src path (feels like src_path*)',
693
            '--with-prefix',
694
            default=''),
695
        suffix=ValueArgument(
696
            'Suffix of source objects (feels like *suffix)', '--with-suffix',
697
            default=''),
698
        add_prefix=ValueArgument('Prefix targets', '--add-prefix', default=''),
699
        add_suffix=ValueArgument('Suffix targets', '--add-suffix', default=''),
700
        prefix_replace=ValueArgument(
701
            'Prefix of src to replace with dst path + add_prefix, if matched',
702
            '--prefix-to-replace',
703
            default=''),
704
        suffix_replace=ValueArgument(
705
            'Suffix of src to replace with add_suffix, if matched',
706
            '--suffix-to-replace',
707
            default=''),
708
        source_version=ValueArgument(
709
            'copy specific version', ('-S', '--source-version'))
710
    )
711

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

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

    
748

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

    
766
    arguments = dict(
767
        destination_account=ValueArgument(
768
            'Account to move to', ('-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', '--content-type'),
775
        recursive=FlagArgument(
776
            'copy directory and contents', ('-R', '--recursive')),
777
        prefix=FlagArgument(
778
            'Match objects prefixed with src path (feels like src_path*)',
779
            '--with-prefix',
780
            default=''),
781
        suffix=ValueArgument(
782
            'Suffix of source objects (feels like *suffix)', '--with-suffix',
783
            default=''),
784
        add_prefix=ValueArgument('Prefix targets', '--add-prefix', default=''),
785
        add_suffix=ValueArgument('Suffix targets', '--add-suffix', default=''),
786
        prefix_replace=ValueArgument(
787
            'Prefix of src to replace with dst path + add_prefix, if matched',
788
            '--prefix-to-replace',
789
            default=''),
790
        suffix_replace=ValueArgument(
791
            'Suffix of src to replace with add_suffix, if matched',
792
            '--suffix-to-replace',
793
            default='')
794
    )
795

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

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

    
832

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

    
841
    arguments = dict(
842
        progress_bar=ProgressBarArgument(
843
            'do not show progress bar',
844
            ('-N', '--no-progress-bar'),
845
            default=False)
846
    )
847

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

    
864
    def main(self, local_path, container___path):
865
        super(self.__class__, self)._run(
866
            container___path, path_is_optional=False)
867
        self._run(local_path)
868

    
869

    
870
@command(pithos_cmds)
871
class file_truncate(_file_container_command, _optional_output_cmd):
872
    """Truncate remote file up to a size (default is 0)"""
873

    
874
    @errors.generic.all
875
    @errors.pithos.connection
876
    @errors.pithos.container
877
    @errors.pithos.object_path
878
    @errors.pithos.object_size
879
    def _run(self, size=0):
880
        self._optional_output(self.client.truncate_object(self.path, size))
881

    
882
    def main(self, container___path, size=0):
883
        super(self.__class__, self)._run(container___path)
884
        self._run(size=size)
885

    
886

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

    
897
    arguments = dict(
898
        progress_bar=ProgressBarArgument(
899
            'do not show progress bar',
900
            ('-N', '--no-progress-bar'),
901
            default=False)
902
    )
903

    
904
    def _open_file(self, local_path, start):
905
        f = open(path.abspath(local_path), 'rb')
906
        f.seek(0, 2)
907
        f_size = f.tell()
908
        f.seek(start, 0)
909
        return (f, f_size)
910

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

    
931
    def main(self, local_path, container___path, start, end):
932
        super(self.__class__, self)._run(
933
            container___path, path_is_optional=None)
934
        self.path = self.path or path.basename(local_path)
935
        self._run(local_path=local_path, start=start, end=end)
936

    
937

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

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

    
968
    @errors.generic.all
969
    @errors.pithos.connection
970
    @errors.pithos.container
971
    @errors.pithos.object_path
972
    def _run(self):
973
        self._optional_output(self.client.create_object_by_manifestation(
974
            self.path,
975
            content_encoding=self['content_encoding'],
976
            content_disposition=self['content_disposition'],
977
            content_type=self['content_type'],
978
            sharing=self['sharing'],
979
            public=self['public']))
980

    
981
    def main(self, container___path):
982
        super(self.__class__, self)._run(
983
            container___path, path_is_optional=False)
984
        self.run()
985

    
986

    
987
@command(pithos_cmds)
988
class file_upload(_file_container_command, _optional_output_cmd):
989
    """Upload a file"""
990

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

    
1018
    def _check_container_limit(self, path):
1019
        cl_dict = self.client.get_container_limit()
1020
        container_limit = int(cl_dict['x-container-policy-quota'])
1021
        r = self.client.container_get()
1022
        used_bytes = sum(int(o['bytes']) for o in r.json)
1023
        path_size = get_path_size(path)
1024
        if container_limit and path_size > (container_limit - used_bytes):
1025
            raiseCLIError(
1026
                'Container(%s) (limit(%s) - used(%s)) < size(%s) of %s' % (
1027
                    self.client.container,
1028
                    format_size(container_limit),
1029
                    format_size(used_bytes),
1030
                    format_size(path_size),
1031
                    path),
1032
                importance=1, details=[
1033
                    'Check accound limit: /file quota',
1034
                    'Check container limit:',
1035
                    '\t/file containerlimit get %s' % self.client.container,
1036
                    'Increase container limit:',
1037
                    '\t/file containerlimit set <new limit> %s' % (
1038
                        self.client.container)])
1039

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

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

    
1158
    def main(self, local_path, container____path__=None):
1159
        super(self.__class__, self)._run(container____path__)
1160
        remote_path = self.path or path.basename(local_path)
1161
        self._run(local_path=local_path, remote_path=remote_path)
1162

    
1163

    
1164
@command(pithos_cmds)
1165
class file_cat(_file_container_command):
1166
    """Print remote file contents to console"""
1167

    
1168
    arguments = dict(
1169
        range=RangeArgument('show range of data', '--range'),
1170
        if_match=ValueArgument('show output if ETags match', '--if-match'),
1171
        if_none_match=ValueArgument(
1172
            'show output if ETags match', '--if-none-match'),
1173
        if_modified_since=DateArgument(
1174
            'show output modified since then', '--if-modified-since'),
1175
        if_unmodified_since=DateArgument(
1176
            'show output unmodified since then', '--if-unmodified-since'),
1177
        object_version=ValueArgument(
1178
            'get the specific version', ('-O', '--object-version'))
1179
    )
1180

    
1181
    @errors.generic.all
1182
    @errors.pithos.connection
1183
    @errors.pithos.container
1184
    @errors.pithos.object_path
1185
    def _run(self):
1186
        self.client.download_object(
1187
            self.path,
1188
            stdout,
1189
            range_str=self['range'],
1190
            version=self['object_version'],
1191
            if_match=self['if_match'],
1192
            if_none_match=self['if_none_match'],
1193
            if_modified_since=self['if_modified_since'],
1194
            if_unmodified_since=self['if_unmodified_since'])
1195

    
1196
    def main(self, container___path):
1197
        super(self.__class__, self)._run(
1198
            container___path, path_is_optional=False)
1199
        self._run()
1200

    
1201

    
1202
@command(pithos_cmds)
1203
class file_download(_file_container_command):
1204
    """Download remote object as local file
1205
    If local destination is a directory:
1206
    *   download <container>:<path> <local dir> -R
1207
    will download all files on <container> prefixed as <path>,
1208
    to <local dir>/<full path>
1209
    *   download <container>:<path> <local dir> --exact-match
1210
    will download only one file, exactly matching <path>
1211
    ATTENTION: to download cont:dir1/dir2/file there must exist objects
1212
    cont:dir1 and cont:dir1/dir2 of type application/directory
1213
    To create directory objects, use /file mkdir
1214
    """
1215

    
1216
    arguments = dict(
1217
        resume=FlagArgument('Resume instead of overwrite', ('-r', '--resume')),
1218
        range=RangeArgument('show range of data', '--range'),
1219
        if_match=ValueArgument('show output if ETags match', '--if-match'),
1220
        if_none_match=ValueArgument(
1221
            'show output if ETags match', '--if-none-match'),
1222
        if_modified_since=DateArgument(
1223
            'show output modified since then', '--if-modified-since'),
1224
        if_unmodified_since=DateArgument(
1225
            'show output unmodified since then', '--if-unmodified-since'),
1226
        object_version=ValueArgument(
1227
            'get the specific version', ('-O', '--object-version')),
1228
        poolsize=IntArgument('set pool size', '--with-pool-size'),
1229
        progress_bar=ProgressBarArgument(
1230
            'do not show progress bar',
1231
            ('-N', '--no-progress-bar'),
1232
            default=False),
1233
        recursive=FlagArgument(
1234
            'Download a remote path and all its contents',
1235
            ('-R', '--recursive'))
1236
    )
1237

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

    
1294
        lprefix = path.abspath(local_path or path.curdir)
1295
        if path.isdir(lprefix):
1296
            for rpath, remote_is_dir in remotes:
1297
                lpath = '/%s/%s' % (lprefix.strip('/'), rpath.strip('/'))
1298
                if remote_is_dir:
1299
                    if path.exists(lpath) and path.isdir(lpath):
1300
                        continue
1301
                    makedirs(lpath)
1302
                elif path.exists(lpath):
1303
                    if not self['resume']:
1304
                        print('File %s exists, aborting...' % lpath)
1305
                        continue
1306
                    with open(lpath, 'rwb+') as f:
1307
                        yield (f, rpath)
1308
                else:
1309
                    with open(lpath, 'wb+') as f:
1310
                        yield (f, rpath)
1311
        elif path.exists(lprefix):
1312
            if len(remotes) > 1:
1313
                raiseCLIError(
1314
                    '%s remote objects cannot be merged in local file %s' % (
1315
                        len(remotes),
1316
                        local_path),
1317
                    details=[
1318
                        'To download multiple objects, local path should be',
1319
                        'a directory, or use download without a local path'])
1320
            (rpath, remote_is_dir) = remotes[0]
1321
            if remote_is_dir:
1322
                raiseCLIError(
1323
                    'Remote directory %s should not replace local file %s' % (
1324
                        rpath,
1325
                        local_path))
1326
            if self['resume']:
1327
                with open(lprefix, 'rwb+') as f:
1328
                    yield (f, rpath)
1329
            else:
1330
                raiseCLIError(
1331
                    'Local file %s already exist' % local_path,
1332
                    details=['Try --resume to overwrite it'])
1333
        else:
1334
            if len(remotes) > 1 or remotes[0][1]:
1335
                raiseCLIError(
1336
                    'Local directory %s does not exist' % local_path)
1337
            with open(lprefix, 'wb+') as f:
1338
                yield (f, remotes[0][0])
1339

    
1340
    @errors.generic.all
1341
    @errors.pithos.connection
1342
    @errors.pithos.container
1343
    @errors.pithos.object_path
1344
    @errors.pithos.local_path
1345
    def _run(self, local_path):
1346
        poolsize = self['poolsize']
1347
        if poolsize:
1348
            self.client.MAX_THREADS = int(poolsize)
1349
        progress_bar = None
1350
        try:
1351
            for f, rpath in self._outputs(local_path):
1352
                (
1353
                    progress_bar,
1354
                    download_cb) = self._safe_progress_bar(
1355
                        'Download %s' % rpath)
1356
                self.client.download_object(
1357
                    rpath, f,
1358
                    download_cb=download_cb,
1359
                    range_str=self['range'],
1360
                    version=self['object_version'],
1361
                    if_match=self['if_match'],
1362
                    resume=self['resume'],
1363
                    if_none_match=self['if_none_match'],
1364
                    if_modified_since=self['if_modified_since'],
1365
                    if_unmodified_since=self['if_unmodified_since'])
1366
        except KeyboardInterrupt:
1367
            from threading import activeCount, enumerate as activethreads
1368
            timeout = 0.5
1369
            while activeCount() > 1:
1370
                stdout.write('\nCancel %s threads: ' % (activeCount() - 1))
1371
                stdout.flush()
1372
                for thread in activethreads():
1373
                    try:
1374
                        thread.join(timeout)
1375
                        stdout.write('.' if thread.isAlive() else '*')
1376
                    except RuntimeError:
1377
                        continue
1378
                    finally:
1379
                        stdout.flush()
1380
                        timeout += 0.1
1381
            print('\nDownload canceled by user')
1382
            if local_path is not None:
1383
                print('to resume, re-run with --resume')
1384
        except Exception:
1385
            self._safe_progress_bar_finish(progress_bar)
1386
            raise
1387
        finally:
1388
            self._safe_progress_bar_finish(progress_bar)
1389

    
1390
    def main(self, container___path, local_path=None):
1391
        super(self.__class__, self)._run(container___path)
1392
        self._run(local_path=local_path)
1393

    
1394

    
1395
@command(pithos_cmds)
1396
class file_hashmap(_file_container_command, _optional_json):
1397
    """Get the hash-map of an object"""
1398

    
1399
    arguments = dict(
1400
        if_match=ValueArgument('show output if ETags match', '--if-match'),
1401
        if_none_match=ValueArgument(
1402
            'show output if ETags match', '--if-none-match'),
1403
        if_modified_since=DateArgument(
1404
            'show output modified since then', '--if-modified-since'),
1405
        if_unmodified_since=DateArgument(
1406
            'show output unmodified since then', '--if-unmodified-since'),
1407
        object_version=ValueArgument(
1408
            'get the specific version', ('-O', '--object-version'))
1409
    )
1410

    
1411
    @errors.generic.all
1412
    @errors.pithos.connection
1413
    @errors.pithos.container
1414
    @errors.pithos.object_path
1415
    def _run(self):
1416
        self._print(self.client.get_object_hashmap(
1417
            self.path,
1418
            version=self['object_version'],
1419
            if_match=self['if_match'],
1420
            if_none_match=self['if_none_match'],
1421
            if_modified_since=self['if_modified_since'],
1422
            if_unmodified_since=self['if_unmodified_since']), print_dict)
1423

    
1424
    def main(self, container___path):
1425
        super(self.__class__, self)._run(
1426
            container___path,
1427
            path_is_optional=False)
1428
        self._run()
1429

    
1430

    
1431
@command(pithos_cmds)
1432
class file_delete(_file_container_command, _optional_output_cmd):
1433
    """Delete a container [or an object]
1434
    How to delete a non-empty container:
1435
    - empty the container:  /file delete -R <container>
1436
    - delete it:            /file delete <container>
1437
    .
1438
    Semantics of directory deletion:
1439
    .a preserve the contents: /file delete <container>:<directory>
1440
    .    objects of the form dir/filename can exist with a dir object
1441
    .b delete contents:       /file delete -R <container>:<directory>
1442
    .    all dir/* objects are affected, even if dir does not exist
1443
    .
1444
    To restore a deleted object OBJ in a container CONT:
1445
    - get object versions: /file versions CONT:OBJ
1446
    .   and choose the version to be restored
1447
    - restore the object:  /file copy --source-version=<version> CONT:OBJ OBJ
1448
    """
1449

    
1450
    arguments = dict(
1451
        until=DateArgument('remove history until that date', '--until'),
1452
        yes=FlagArgument('Do not prompt for permission', '--yes'),
1453
        recursive=FlagArgument(
1454
            'empty dir or container and delete (if dir)',
1455
            ('-R', '--recursive'))
1456
    )
1457

    
1458
    def __init__(self, arguments={}, auth_base=None):
1459
        super(self.__class__, self).__init__(arguments, auth_base)
1460
        self['delimiter'] = DelimiterArgument(
1461
            self,
1462
            parsed_name='--delimiter',
1463
            help='delete objects prefixed with <object><delimiter>')
1464

    
1465
    @errors.generic.all
1466
    @errors.pithos.connection
1467
    @errors.pithos.container
1468
    @errors.pithos.object_path
1469
    def _run(self):
1470
        if self.path:
1471
            if self['yes'] or ask_user(
1472
                    'Delete %s:%s ?' % (self.container, self.path)):
1473
                self._optional_output(self.client.del_object(
1474
                    self.path,
1475
                    until=self['until'], delimiter=self['delimiter']))
1476
            else:
1477
                print('Aborted')
1478
        else:
1479
            if self['recursive']:
1480
                ask_msg = 'Delete container contents'
1481
            else:
1482
                ask_msg = 'Delete container'
1483
            if self['yes'] or ask_user('%s %s ?' % (ask_msg, self.container)):
1484
                self._optional_output(self.client.del_container(
1485
                    until=self['until'], delimiter=self['delimiter']))
1486
            else:
1487
                print('Aborted')
1488

    
1489
    def main(self, container____path__=None):
1490
        super(self.__class__, self)._run(container____path__)
1491
        self._run()
1492

    
1493

    
1494
@command(pithos_cmds)
1495
class file_purge(_file_container_command, _optional_output_cmd):
1496
    """Delete a container and release related data blocks
1497
    Non-empty containers can not purged.
1498
    To purge a container with content:
1499
    .   /file delete -R <container>
1500
    .      objects are deleted, but data blocks remain on server
1501
    .   /file purge <container>
1502
    .      container and data blocks are released and deleted
1503
    """
1504

    
1505
    arguments = dict(
1506
        yes=FlagArgument('Do not prompt for permission', '--yes'),
1507
        force=FlagArgument('purge even if not empty', ('-F', '--force'))
1508
    )
1509

    
1510
    @errors.generic.all
1511
    @errors.pithos.connection
1512
    @errors.pithos.container
1513
    def _run(self):
1514
        if self['yes'] or ask_user('Purge container %s?' % self.container):
1515
            try:
1516
                r = self.client.purge_container()
1517
            except ClientError as ce:
1518
                if ce.status in (409,):
1519
                    if self['force']:
1520
                        self.client.del_container(delimiter='/')
1521
                        r = self.client.purge_container()
1522
                    else:
1523
                        raiseCLIError(ce, details=['Try -F to force-purge'])
1524
                else:
1525
                    raise
1526
            self._optional_output(r)
1527
        else:
1528
            print('Aborted')
1529

    
1530
    def main(self, container=None):
1531
        super(self.__class__, self)._run(container)
1532
        if container and self.container != container:
1533
            raiseCLIError('Invalid container name %s' % container, details=[
1534
                'Did you mean "%s" ?' % self.container,
1535
                'Use --container for names containing :'])
1536
        self._run()
1537

    
1538

    
1539
@command(pithos_cmds)
1540
class file_publish(_file_container_command):
1541
    """Publish the object and print the public url"""
1542

    
1543
    @errors.generic.all
1544
    @errors.pithos.connection
1545
    @errors.pithos.container
1546
    @errors.pithos.object_path
1547
    def _run(self):
1548
        url = self.client.publish_object(self.path)
1549
        print(url)
1550

    
1551
    def main(self, container___path):
1552
        super(self.__class__, self)._run(
1553
            container___path, path_is_optional=False)
1554
        self._run()
1555

    
1556

    
1557
@command(pithos_cmds)
1558
class file_unpublish(_file_container_command, _optional_output_cmd):
1559
    """Unpublish an object"""
1560

    
1561
    @errors.generic.all
1562
    @errors.pithos.connection
1563
    @errors.pithos.container
1564
    @errors.pithos.object_path
1565
    def _run(self):
1566
            self._optional_output(self.client.unpublish_object(self.path))
1567

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

    
1573

    
1574
@command(pithos_cmds)
1575
class file_permissions(_pithos_init):
1576
    """Manage user and group accessibility for objects
1577
    Permissions are lists of users and user groups. There are read and write
1578
    permissions. Users and groups with write permission have also read
1579
    permission.
1580
    """
1581

    
1582

    
1583
def print_permissions(permissions_dict):
1584
    expected_keys = ('read', 'write')
1585
    if set(permissions_dict).issubset(expected_keys):
1586
        print_dict(permissions_dict)
1587
    else:
1588
        invalid_keys = set(permissions_dict.keys()).difference(expected_keys)
1589
        raiseCLIError(
1590
            'Illegal permission keys: %s' % ', '.join(invalid_keys),
1591
            importance=1, details=[
1592
                'Valid permission types: %s' % ' '.join(expected_keys)])
1593

    
1594

    
1595
@command(pithos_cmds)
1596
class file_permissions_get(_file_container_command, _optional_json):
1597
    """Get read and write permissions of an object"""
1598

    
1599
    @errors.generic.all
1600
    @errors.pithos.connection
1601
    @errors.pithos.container
1602
    @errors.pithos.object_path
1603
    def _run(self):
1604
        self._print(
1605
            self.client.get_object_sharing(self.path), print_permissions)
1606

    
1607
    def main(self, container___path):
1608
        super(self.__class__, self)._run(
1609
            container___path, path_is_optional=False)
1610
        self._run()
1611

    
1612

    
1613
@command(pithos_cmds)
1614
class file_permissions_set(_file_container_command, _optional_output_cmd):
1615
    """Set permissions for an object
1616
    New permissions overwrite existing permissions.
1617
    Permission format:
1618
    -   read=<username>[,usergroup[,...]]
1619
    -   write=<username>[,usegroup[,...]]
1620
    E.g. to give read permissions for file F to users A and B and write for C:
1621
    .       /file permissions set F read=A,B write=C
1622
    """
1623

    
1624
    @errors.generic.all
1625
    def format_permission_dict(self, permissions):
1626
        read = False
1627
        write = False
1628
        for perms in permissions:
1629
            splstr = perms.split('=')
1630
            if 'read' == splstr[0]:
1631
                read = [ug.strip() for ug in splstr[1].split(',')]
1632
            elif 'write' == splstr[0]:
1633
                write = [ug.strip() for ug in splstr[1].split(',')]
1634
            else:
1635
                msg = 'Usage:\tread=<groups,users> write=<groups,users>'
1636
                raiseCLIError(None, msg)
1637
        return (read, write)
1638

    
1639
    @errors.generic.all
1640
    @errors.pithos.connection
1641
    @errors.pithos.container
1642
    @errors.pithos.object_path
1643
    def _run(self, read, write):
1644
        self._optional_output(self.client.set_object_sharing(
1645
            self.path, read_permission=read, write_permission=write))
1646

    
1647
    def main(self, container___path, *permissions):
1648
        super(self.__class__, self)._run(
1649
            container___path, path_is_optional=False)
1650
        read, write = self.format_permission_dict(permissions)
1651
        self._run(read, write)
1652

    
1653

    
1654
@command(pithos_cmds)
1655
class file_permissions_delete(_file_container_command, _optional_output_cmd):
1656
    """Delete all permissions set on object
1657
    To modify permissions, use /file permissions set
1658
    """
1659

    
1660
    @errors.generic.all
1661
    @errors.pithos.connection
1662
    @errors.pithos.container
1663
    @errors.pithos.object_path
1664
    def _run(self):
1665
        self._optional_output(self.client.del_object_sharing(self.path))
1666

    
1667
    def main(self, container___path):
1668
        super(self.__class__, self)._run(
1669
            container___path, path_is_optional=False)
1670
        self._run()
1671

    
1672

    
1673
@command(pithos_cmds)
1674
class file_info(_file_container_command, _optional_json):
1675
    """Get detailed information for user account, containers or objects
1676
    to get account info:    /file info
1677
    to get container info:  /file info <container>
1678
    to get object info:     /file info <container>:<path>
1679
    """
1680

    
1681
    arguments = dict(
1682
        object_version=ValueArgument(
1683
            'show specific version \ (applies only for objects)',
1684
            ('-O', '--object-version'))
1685
    )
1686

    
1687
    @errors.generic.all
1688
    @errors.pithos.connection
1689
    @errors.pithos.container
1690
    @errors.pithos.object_path
1691
    def _run(self):
1692
        if self.container is None:
1693
            r = self.client.get_account_info()
1694
        elif self.path is None:
1695
            r = self.client.get_container_info(self.container)
1696
        else:
1697
            r = self.client.get_object_info(
1698
                self.path, version=self['object_version'])
1699
        self._print(r, print_dict)
1700

    
1701
    def main(self, container____path__=None):
1702
        super(self.__class__, self)._run(container____path__)
1703
        self._run()
1704

    
1705

    
1706
@command(pithos_cmds)
1707
class file_metadata(_pithos_init):
1708
    """Metadata are attached on objects. They are formed as key:value pairs.
1709
    They can have arbitary values.
1710
    """
1711

    
1712

    
1713
@command(pithos_cmds)
1714
class file_metadata_get(_file_container_command, _optional_json):
1715
    """Get metadata for account, containers or objects"""
1716

    
1717
    arguments = dict(
1718
        detail=FlagArgument('show detailed output', ('-l', '--details')),
1719
        until=DateArgument('show metadata until then', '--until'),
1720
        object_version=ValueArgument(
1721
            'show specific version \ (applies only for objects)',
1722
            ('-O', '--object-version'))
1723
    )
1724

    
1725
    @errors.generic.all
1726
    @errors.pithos.connection
1727
    @errors.pithos.container
1728
    @errors.pithos.object_path
1729
    def _run(self):
1730
        until = self['until']
1731
        r = None
1732
        if self.container is None:
1733
            if self['detail']:
1734
                r = self.client.get_account_info(until=until)
1735
            else:
1736
                r = self.client.get_account_meta(until=until)
1737
                r = pretty_keys(r, '-')
1738
        elif self.path is None:
1739
            if self['detail']:
1740
                r = self.client.get_container_info(until=until)
1741
            else:
1742
                cmeta = self.client.get_container_meta(until=until)
1743
                ometa = self.client.get_container_object_meta(until=until)
1744
                r = {}
1745
                if cmeta:
1746
                    r['container-meta'] = pretty_keys(cmeta, '-')
1747
                if ometa:
1748
                    r['object-meta'] = pretty_keys(ometa, '-')
1749
        else:
1750
            if self['detail']:
1751
                r = self.client.get_object_info(
1752
                    self.path,
1753
                    version=self['object_version'])
1754
            else:
1755
                r = self.client.get_object_meta(
1756
                    self.path,
1757
                    version=self['object_version'])
1758
                r = pretty_keys(pretty_keys(r, '-'))
1759
        if r:
1760
            self._print(r, print_dict)
1761

    
1762
    def main(self, container____path__=None):
1763
        super(self.__class__, self)._run(container____path__)
1764
        self._run()
1765

    
1766

    
1767
@command(pithos_cmds)
1768
class file_metadata_set(_file_container_command, _optional_output_cmd):
1769
    """Set a piece of metadata for account, container or object"""
1770

    
1771
    @errors.generic.all
1772
    @errors.pithos.connection
1773
    @errors.pithos.container
1774
    @errors.pithos.object_path
1775
    def _run(self, metakey, metaval):
1776
        if not self.container:
1777
            r = self.client.set_account_meta({metakey: metaval})
1778
        elif not self.path:
1779
            r = self.client.set_container_meta({metakey: metaval})
1780
        else:
1781
            r = self.client.set_object_meta(self.path, {metakey: metaval})
1782
        self._optional_output(r)
1783

    
1784
    def main(self, metakey, metaval, container____path__=None):
1785
        super(self.__class__, self)._run(container____path__)
1786
        self._run(metakey=metakey, metaval=metaval)
1787

    
1788

    
1789
@command(pithos_cmds)
1790
class file_metadata_delete(_file_container_command, _optional_output_cmd):
1791
    """Delete metadata with given key from account, container or object
1792
    - to get metadata of current account: /file metadata get
1793
    - to get metadata of a container:     /file metadata get <container>
1794
    - to get metadata of an object:       /file metadata get <container>:<path>
1795
    """
1796

    
1797
    @errors.generic.all
1798
    @errors.pithos.connection
1799
    @errors.pithos.container
1800
    @errors.pithos.object_path
1801
    def _run(self, metakey):
1802
        if self.container is None:
1803
            r = self.client.del_account_meta(metakey)
1804
        elif self.path is None:
1805
            r = self.client.del_container_meta(metakey)
1806
        else:
1807
            r = self.client.del_object_meta(self.path, metakey)
1808
        self._optional_output(r)
1809

    
1810
    def main(self, metakey, container____path__=None):
1811
        super(self.__class__, self)._run(container____path__)
1812
        self._run(metakey)
1813

    
1814

    
1815
@command(pithos_cmds)
1816
class file_quota(_file_account_command, _optional_json):
1817
    """Get account quota"""
1818

    
1819
    arguments = dict(
1820
        in_bytes=FlagArgument('Show result in bytes', ('-b', '--bytes'))
1821
    )
1822

    
1823
    @errors.generic.all
1824
    @errors.pithos.connection
1825
    def _run(self):
1826

    
1827
        def pretty_print(output):
1828
            if not self['in_bytes']:
1829
                for k in output:
1830
                    output[k] = format_size(output[k])
1831
            pretty_dict(output, '-')
1832

    
1833
        self._print(self.client.get_account_quota(), pretty_print)
1834

    
1835
    def main(self, custom_uuid=None):
1836
        super(self.__class__, self)._run(custom_account=custom_uuid)
1837
        self._run()
1838

    
1839

    
1840
@command(pithos_cmds)
1841
class file_containerlimit(_pithos_init):
1842
    """Container size limit commands"""
1843

    
1844

    
1845
@command(pithos_cmds)
1846
class file_containerlimit_get(_file_container_command, _optional_json):
1847
    """Get container size limit"""
1848

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

    
1853
    @errors.generic.all
1854
    @errors.pithos.container
1855
    def _run(self):
1856

    
1857
        def pretty_print(output):
1858
            if not self['in_bytes']:
1859
                for k, v in output.items():
1860
                    output[k] = 'unlimited' if '0' == v else format_size(v)
1861
            pretty_dict(output, '-')
1862

    
1863
        self._print(
1864
            self.client.get_container_limit(self.container), pretty_print)
1865

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

    
1871

    
1872
@command(pithos_cmds)
1873
class file_containerlimit_set(_file_account_command, _optional_output_cmd):
1874
    """Set new storage limit for a container
1875
    By default, the limit is set in bytes
1876
    Users may specify a different unit, e.g:
1877
    /file containerlimit set 2.3GB mycontainer
1878
    Valid units: B, KiB (1024 B), KB (1000 B), MiB, MB, GiB, GB, TiB, TB
1879
    To set container limit to "unlimited", use 0
1880
    """
1881

    
1882
    @errors.generic.all
1883
    def _calculate_limit(self, user_input):
1884
        limit = 0
1885
        try:
1886
            limit = int(user_input)
1887
        except ValueError:
1888
            index = 0
1889
            digits = [str(num) for num in range(0, 10)] + ['.']
1890
            while user_input[index] in digits:
1891
                index += 1
1892
            limit = user_input[:index]
1893
            format = user_input[index:]
1894
            try:
1895
                return to_bytes(limit, format)
1896
            except Exception as qe:
1897
                msg = 'Failed to convert %s to bytes' % user_input,
1898
                raiseCLIError(qe, msg, details=[
1899
                    'Syntax: containerlimit set <limit>[format] [container]',
1900
                    'e.g.: containerlimit set 2.3GB mycontainer',
1901
                    'Valid formats:',
1902
                    '(*1024): B, KiB, MiB, GiB, TiB',
1903
                    '(*1000): B, KB, MB, GB, TB'])
1904
        return limit
1905

    
1906
    @errors.generic.all
1907
    @errors.pithos.connection
1908
    @errors.pithos.container
1909
    def _run(self, limit):
1910
        if self.container:
1911
            self.client.container = self.container
1912
        self._optional_output(self.client.set_container_limit(limit))
1913

    
1914
    def main(self, limit, container=None):
1915
        super(self.__class__, self)._run()
1916
        limit = self._calculate_limit(limit)
1917
        self.container = container
1918
        self._run(limit)
1919

    
1920

    
1921
@command(pithos_cmds)
1922
class file_versioning(_pithos_init):
1923
    """Manage the versioning scheme of current pithos user account"""
1924

    
1925

    
1926
@command(pithos_cmds)
1927
class file_versioning_get(_file_account_command, _optional_json):
1928
    """Get  versioning for account or container"""
1929

    
1930
    @errors.generic.all
1931
    @errors.pithos.connection
1932
    @errors.pithos.container
1933
    def _run(self):
1934
        self._print(
1935
            self.client.get_container_versioning(self.container) if (
1936
                self.container) else self.client.get_account_versioning(),
1937
            print_dict)
1938

    
1939
    def main(self, container=None):
1940
        super(self.__class__, self)._run()
1941
        self.container = container
1942
        self._run()
1943

    
1944

    
1945
@command(pithos_cmds)
1946
class file_versioning_set(_file_account_command, _optional_output_cmd):
1947
    """Set versioning mode (auto, none) for account or container"""
1948

    
1949
    def _check_versioning(self, versioning):
1950
        if versioning and versioning.lower() in ('auto', 'none'):
1951
            return versioning.lower()
1952
        raiseCLIError('Invalid versioning %s' % versioning, details=[
1953
            'Versioning can be auto or none'])
1954

    
1955
    @errors.generic.all
1956
    @errors.pithos.connection
1957
    @errors.pithos.container
1958
    def _run(self, versioning):
1959
        if self.container:
1960
            self.client.container = self.container
1961
            r = self.client.set_container_versioning(versioning)
1962
        else:
1963
            r = self.client.set_account_versioning(versioning)
1964
        self._optional_output(r)
1965

    
1966
    def main(self, versioning, container=None):
1967
        super(self.__class__, self)._run()
1968
        self._run(self._check_versioning(versioning))
1969

    
1970

    
1971
@command(pithos_cmds)
1972
class file_group(_pithos_init):
1973
    """Manage access groups and group members"""
1974

    
1975

    
1976
@command(pithos_cmds)
1977
class file_group_list(_file_account_command, _optional_json):
1978
    """list all groups and group members"""
1979

    
1980
    @errors.generic.all
1981
    @errors.pithos.connection
1982
    def _run(self):
1983
        self._print(self.client.get_account_group(), pretty_dict, delim='-')
1984

    
1985
    def main(self):
1986
        super(self.__class__, self)._run()
1987
        self._run()
1988

    
1989

    
1990
@command(pithos_cmds)
1991
class file_group_set(_file_account_command, _optional_output_cmd):
1992
    """Set a user group"""
1993

    
1994
    @errors.generic.all
1995
    @errors.pithos.connection
1996
    def _run(self, groupname, *users):
1997
        self._optional_output(self.client.set_account_group(groupname, users))
1998

    
1999
    def main(self, groupname, *users):
2000
        super(self.__class__, self)._run()
2001
        if users:
2002
            self._run(groupname, *users)
2003
        else:
2004
            raiseCLIError('No users to add in group %s' % groupname)
2005

    
2006

    
2007
@command(pithos_cmds)
2008
class file_group_delete(_file_account_command, _optional_output_cmd):
2009
    """Delete a user group"""
2010

    
2011
    @errors.generic.all
2012
    @errors.pithos.connection
2013
    def _run(self, groupname):
2014
        self._optional_output(self.client.del_account_group(groupname))
2015

    
2016
    def main(self, groupname):
2017
        super(self.__class__, self)._run()
2018
        self._run(groupname)
2019

    
2020

    
2021
@command(pithos_cmds)
2022
class file_sharers(_file_account_command, _optional_json):
2023
    """List the accounts that share objects with current user"""
2024

    
2025
    arguments = dict(
2026
        detail=FlagArgument('show detailed output', ('-l', '--details')),
2027
        marker=ValueArgument('show output greater then marker', '--marker')
2028
    )
2029

    
2030
    @errors.generic.all
2031
    @errors.pithos.connection
2032
    def _run(self):
2033
        accounts = self.client.get_sharing_accounts(marker=self['marker'])
2034
        if self['json_output'] or self['detail']:
2035
            self._print(accounts)
2036
        else:
2037
            self._print([acc['name'] for acc in accounts])
2038

    
2039
    def main(self):
2040
        super(self.__class__, self)._run()
2041
        self._run()
2042

    
2043

    
2044
def version_print(versions):
2045
    print_items([dict(id=vitem[0], created=strftime(
2046
        '%d-%m-%Y %H:%M:%S',
2047
        localtime(float(vitem[1])))) for vitem in versions])
2048

    
2049

    
2050
@command(pithos_cmds)
2051
class file_versions(_file_container_command, _optional_json):
2052
    """Get the list of object versions
2053
    Deleted objects may still have versions that can be used to restore it and
2054
    get information about its previous state.
2055
    The version number can be used in a number of other commands, like info,
2056
    copy, move, meta. See these commands for more information, e.g.
2057
    /file info -h
2058
    """
2059

    
2060
    @errors.generic.all
2061
    @errors.pithos.connection
2062
    @errors.pithos.container
2063
    @errors.pithos.object_path
2064
    def _run(self):
2065
        self._print(
2066
            self.client.get_object_versionlist(self.path), version_print)
2067

    
2068
    def main(self, container___path):
2069
        super(file_versions, self)._run(
2070
            container___path,
2071
            path_is_optional=False)
2072
        self._run()