Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / pithos.py @ 8dbf5a1a

History | View | Annotate | Download (78 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('file', 'url')\
164
                or self.config.get('store', 'url')\
165
                or self.config.get('pithos', 'url')
166
        if not self.base_url:
167
            raise CLIBaseUrlError(service='pithos')
168

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

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

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

    
196

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

    
200
    def __init__(self, arguments={}, auth_base=None):
201
        super(_file_account_command, self).__init__(arguments, auth_base)
202
        self['account'] = ValueArgument(
203
            'Set user account (not permanent)',
204
            ('-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)',
228
            ('-C', '--container'))
229

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

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

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

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

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

    
306

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

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

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

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

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

    
440
    def main(self, container____path__=None):
441
        super(self.__class__, self)._run(container____path__)
442
        self._run()
443

    
444

    
445
@command(pithos_cmds)
446
class file_mkdir(_file_container_command, _optional_output_cmd):
447
    """Create a directory"""
448

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

    
456
    @errors.generic.all
457
    @errors.pithos.connection
458
    @errors.pithos.container
459
    def _run(self):
460
        self._optional_output(self.client.create_directory(self.path))
461

    
462
    def main(self, container___directory):
463
        super(self.__class__, self)._run(
464
            container___directory,
465
            path_is_optional=False)
466
        self._run()
467

    
468

    
469
@command(pithos_cmds)
470
class file_touch(_file_container_command, _optional_output_cmd):
471
    """Create an empty object (file)
472
    If object exists, this command will reset it to 0 length
473
    """
474

    
475
    arguments = dict(
476
        content_type=ValueArgument(
477
            'Set content type (default: application/octet-stream)',
478
            '--content-type',
479
            default='application/octet-stream')
480
    )
481

    
482
    @errors.generic.all
483
    @errors.pithos.connection
484
    @errors.pithos.container
485
    def _run(self):
486
        self._optional_output(
487
            self.client.create_object(self.path, self['content_type']))
488

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

    
495

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

    
500
    arguments = dict(
501
        versioning=ValueArgument(
502
            'set container versioning (auto/none)',
503
            '--versioning'),
504
        limit=IntArgument('set default container limit', '--limit'),
505
        meta=KeyValueArgument(
506
            'set container metadata (can be repeated)',
507
            '--meta')
508
    )
509

    
510
    @errors.generic.all
511
    @errors.pithos.connection
512
    @errors.pithos.container
513
    def _run(self, container):
514
        self._optional_output(self.client.create_container(
515
            container=container,
516
            sizelimit=self['limit'],
517
            versioning=self['versioning'],
518
            metadata=self['meta']))
519

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

    
528

    
529
class _source_destination_command(_file_container_command):
530

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

    
542
    def __init__(self, arguments={}, auth_base=None):
543
        self.arguments.update(arguments)
544
        super(_source_destination_command, self).__init__(
545
            self.arguments, auth_base)
546

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

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

    
579
    def _get_all(self, prefix):
580
        return self.client.container_get(prefix=prefix).json
581

    
582
    def _get_src_objects(self, src_path, source_version=None):
583
        """Get a list of the source objects to be called
584

585
        :param src_path: (str) source path
586

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

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

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

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

    
625
        if dst_path and dst_path.endswith('/'):
626
            dst_path = dst_path[:-1]
627

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

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

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

    
673

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

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

    
728
    @errors.generic.all
729
    @errors.pithos.connection
730
    @errors.pithos.container
731
    @errors.pithos.account
732
    def _run(self, dst_path):
733
        no_source_object = True
734
        src_account = self.client.account if (
735
            self['destination_account']) else None
736
        for src_obj, dst_obj in self.src_dst_pairs(
737
                dst_path, self['source_version']):
738
            no_source_object = False
739
            r = self.dst_client.copy_object(
740
                src_container=self.client.container,
741
                src_object=src_obj,
742
                dst_container=self.dst_client.container,
743
                dst_object=dst_obj,
744
                source_account=src_account,
745
                source_version=self['source_version'],
746
                public=self['public'],
747
                content_type=self['content_type'])
748
        if no_source_object:
749
            raiseCLIError('No object %s in container %s' % (
750
                self.path,
751
                self.container))
752
        self._optional_output(r)
753

    
754
    def main(
755
            self, source_container___path,
756
            destination_container___path=None):
757
        super(file_copy, self)._run(
758
            source_container___path,
759
            path_is_optional=False)
760
        (dst_cont, dst_path) = self._dest_container_path(
761
            destination_container___path)
762
        self.dst_client.container = dst_cont or self.container
763
        self._run(dst_path=dst_path or '')
764

    
765

    
766
@command(pithos_cmds)
767
class file_move(_source_destination_command, _optional_output_cmd):
768
    """Move/rename objects from container to (another) container
769
    Semantics:
770
    move cont:path dir
771
    .   rename path as dir/path
772
    move cont:path cont2:
773
    .   trasnfer all <obj> prefixed with path to container cont2
774
    move cont:path [cont2:]path2
775
    .   transfer path to path2
776
    Use options:
777
    1. <container1>:<path1> [container2:]<path2> : if container2 is not given,
778
    destination is container1:path2
779
    2. <container>:<path1> <path2> : move in the same container
780
    3. Can use --container= instead of <container1>
781
    """
782

    
783
    arguments = dict(
784
        destination_account=ValueArgument(
785
            'Account to move to',
786
            ('-a', '--dst-account')),
787
        destination_container=ValueArgument(
788
            'use it if destination container name contains a : character',
789
            ('-D', '--dst-container')),
790
        public=ValueArgument('make object publicly accessible', '--public'),
791
        content_type=ValueArgument(
792
            'change object\'s content type',
793
            '--content-type'),
794
        recursive=FlagArgument(
795
            'copy directory and contents',
796
            ('-R', '--recursive')),
797
        prefix=FlagArgument(
798
            'Match objects prefixed with src path (feels like src_path*)',
799
            '--with-prefix',
800
            default=''),
801
        suffix=ValueArgument(
802
            'Suffix of source objects (feels like *suffix)',
803
            '--with-suffix',
804
            default=''),
805
        add_prefix=ValueArgument('Prefix targets', '--add-prefix', default=''),
806
        add_suffix=ValueArgument('Suffix targets', '--add-suffix', default=''),
807
        prefix_replace=ValueArgument(
808
            'Prefix of src to replace with dst path + add_prefix, if matched',
809
            '--prefix-to-replace',
810
            default=''),
811
        suffix_replace=ValueArgument(
812
            'Suffix of src to replace with add_suffix, if matched',
813
            '--suffix-to-replace',
814
            default='')
815
    )
816

    
817
    @errors.generic.all
818
    @errors.pithos.connection
819
    @errors.pithos.container
820
    def _run(self, dst_path):
821
        no_source_object = True
822
        src_account = self.client.account if (
823
            self['destination_account']) else None
824
        for src_obj, dst_obj in self.src_dst_pairs(dst_path):
825
            no_source_object = False
826
            r = self.dst_client.move_object(
827
                src_container=self.container,
828
                src_object=src_obj,
829
                dst_container=self.dst_client.container,
830
                dst_object=dst_obj,
831
                source_account=src_account,
832
                public=self['public'],
833
                content_type=self['content_type'])
834
        if no_source_object:
835
            raiseCLIError('No object %s in container %s' % (
836
                self.path,
837
                self.container))
838
        self._optional_output(r)
839

    
840
    def main(
841
            self, source_container___path,
842
            destination_container___path=None):
843
        super(self.__class__, self)._run(
844
            source_container___path,
845
            path_is_optional=False)
846
        (dst_cont, dst_path) = self._dest_container_path(
847
            destination_container___path)
848
        (dst_cont, dst_path) = self._dest_container_path(
849
            destination_container___path)
850
        self.dst_client.container = dst_cont or self.container
851
        self._run(dst_path=dst_path or '')
852

    
853

    
854
@command(pithos_cmds)
855
class file_append(_file_container_command, _optional_output_cmd):
856
    """Append local file to (existing) remote object
857
    The remote object should exist.
858
    If the remote object is a directory, it is transformed into a file.
859
    In the later case, objects under the directory remain intact.
860
    """
861

    
862
    arguments = dict(
863
        progress_bar=ProgressBarArgument(
864
            'do not show progress bar',
865
            ('-N', '--no-progress-bar'),
866
            default=False)
867
    )
868

    
869
    @errors.generic.all
870
    @errors.pithos.connection
871
    @errors.pithos.container
872
    @errors.pithos.object_path
873
    def _run(self, local_path):
874
        (progress_bar, upload_cb) = self._safe_progress_bar('Appending')
875
        try:
876
            f = open(local_path, 'rb')
877
            self._optional_output(
878
                self.client.append_object(self.path, f, upload_cb))
879
        except Exception:
880
            self._safe_progress_bar_finish(progress_bar)
881
            raise
882
        finally:
883
            self._safe_progress_bar_finish(progress_bar)
884

    
885
    def main(self, local_path, container___path):
886
        super(self.__class__, self)._run(
887
            container___path,
888
            path_is_optional=False)
889
        self._run(local_path)
890

    
891

    
892
@command(pithos_cmds)
893
class file_truncate(_file_container_command, _optional_output_cmd):
894
    """Truncate remote file up to a size (default is 0)"""
895

    
896
    @errors.generic.all
897
    @errors.pithos.connection
898
    @errors.pithos.container
899
    @errors.pithos.object_path
900
    @errors.pithos.object_size
901
    def _run(self, size=0):
902
        self._optional_output(self.client.truncate_object(self.path, size))
903

    
904
    def main(self, container___path, size=0):
905
        super(self.__class__, self)._run(container___path)
906
        self._run(size=size)
907

    
908

    
909
@command(pithos_cmds)
910
class file_overwrite(_file_container_command, _optional_output_cmd):
911
    """Overwrite part (from start to end) of a remote file
912
    overwrite local-path container 10 20
913
    .   will overwrite bytes from 10 to 20 of a remote file with the same name
914
    .   as local-path basename
915
    overwrite local-path container:path 10 20
916
    .   will overwrite as above, but the remote file is named path
917
    """
918

    
919
    arguments = dict(
920
        progress_bar=ProgressBarArgument(
921
            'do not show progress bar',
922
            ('-N', '--no-progress-bar'),
923
            default=False)
924
    )
925

    
926
    def _open_file(self, local_path, start):
927
        f = open(path.abspath(local_path), 'rb')
928
        f.seek(0, 2)
929
        f_size = f.tell()
930
        f.seek(start, 0)
931
        return (f, f_size)
932

    
933
    @errors.generic.all
934
    @errors.pithos.connection
935
    @errors.pithos.container
936
    @errors.pithos.object_path
937
    @errors.pithos.object_size
938
    def _run(self, local_path, start, end):
939
        (start, end) = (int(start), int(end))
940
        (f, f_size) = self._open_file(local_path, start)
941
        (progress_bar, upload_cb) = self._safe_progress_bar(
942
            'Overwrite %s bytes' % (end - start))
943
        try:
944
            self._optional_output(self.client.overwrite_object(
945
                obj=self.path,
946
                start=start,
947
                end=end,
948
                source_file=f,
949
                upload_cb=upload_cb))
950
        finally:
951
            self._safe_progress_bar_finish(progress_bar)
952

    
953
    def main(self, local_path, container___path, start, end):
954
        super(self.__class__, self)._run(
955
            container___path,
956
            path_is_optional=None)
957
        self.path = self.path or path.basename(local_path)
958
        self._run(local_path=local_path, start=start, end=end)
959

    
960

    
961
@command(pithos_cmds)
962
class file_manifest(_file_container_command, _optional_output_cmd):
963
    """Create a remote file of uploaded parts by manifestation
964
    Remains functional for compatibility with OOS Storage. Users are advised
965
    to use the upload command instead.
966
    Manifestation is a compliant process for uploading large files. The files
967
    have to be chunked in smalled files and uploaded as <prefix><increment>
968
    where increment is 1, 2, ...
969
    Finally, the manifest command glues partial files together in one file
970
    named <prefix>
971
    The upload command is faster, easier and more intuitive than manifest
972
    """
973

    
974
    arguments = dict(
975
        etag=ValueArgument('check written data', '--etag'),
976
        content_encoding=ValueArgument(
977
            'set MIME content type',
978
            '--content-encoding'),
979
        content_disposition=ValueArgument(
980
            'the presentation style of the object',
981
            '--content-disposition'),
982
        content_type=ValueArgument(
983
            'specify content type',
984
            '--content-type',
985
            default='application/octet-stream'),
986
        sharing=SharingArgument(
987
            '\n'.join([
988
                'define object sharing policy',
989
                '    ( "read=user1,grp1,user2,... write=user1,grp2,..." )']),
990
            '--sharing'),
991
        public=FlagArgument('make object publicly accessible', '--public')
992
    )
993

    
994
    @errors.generic.all
995
    @errors.pithos.connection
996
    @errors.pithos.container
997
    @errors.pithos.object_path
998
    def _run(self):
999
        self._optional_output(self.client.create_object_by_manifestation(
1000
            self.path,
1001
            content_encoding=self['content_encoding'],
1002
            content_disposition=self['content_disposition'],
1003
            content_type=self['content_type'],
1004
            sharing=self['sharing'],
1005
            public=self['public']))
1006

    
1007
    def main(self, container___path):
1008
        super(self.__class__, self)._run(
1009
            container___path,
1010
            path_is_optional=False)
1011
        self.run()
1012

    
1013

    
1014
@command(pithos_cmds)
1015
class file_upload(_file_container_command, _optional_output_cmd):
1016
    """Upload a file"""
1017

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

    
1048
    def _check_container_limit(self, path):
1049
        cl_dict = self.client.get_container_limit()
1050
        container_limit = int(cl_dict['x-container-policy-quota'])
1051
        r = self.client.container_get()
1052
        used_bytes = sum(int(o['bytes']) for o in r.json)
1053
        path_size = get_path_size(path)
1054
        if container_limit and path_size > (container_limit - used_bytes):
1055
            raiseCLIError(
1056
                'Container(%s) (limit(%s) - used(%s)) < size(%s) of %s' % (
1057
                    self.client.container,
1058
                    format_size(container_limit),
1059
                    format_size(used_bytes),
1060
                    format_size(path_size),
1061
                    path),
1062
                importance=1, details=[
1063
                    'Check accound limit: /file quota',
1064
                    'Check container limit:',
1065
                    '\t/file containerlimit get %s' % self.client.container,
1066
                    'Increase container limit:',
1067
                    '\t/file containerlimit set <new limit> %s' % (
1068
                        self.client.container)])
1069

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

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

    
1190
    def main(self, local_path, container____path__=None):
1191
        super(self.__class__, self)._run(container____path__)
1192
        remote_path = self.path or path.basename(local_path)
1193
        self._run(local_path=local_path, remote_path=remote_path)
1194

    
1195

    
1196
@command(pithos_cmds)
1197
class file_cat(_file_container_command):
1198
    """Print remote file contents to console"""
1199

    
1200
    arguments = dict(
1201
        range=RangeArgument('show range of data', '--range'),
1202
        if_match=ValueArgument('show output if ETags match', '--if-match'),
1203
        if_none_match=ValueArgument(
1204
            'show output if ETags match',
1205
            '--if-none-match'),
1206
        if_modified_since=DateArgument(
1207
            'show output modified since then',
1208
            '--if-modified-since'),
1209
        if_unmodified_since=DateArgument(
1210
            'show output unmodified since then',
1211
            '--if-unmodified-since'),
1212
        object_version=ValueArgument(
1213
            'get the specific version',
1214
            ('-O', '--object-version'))
1215
    )
1216

    
1217
    @errors.generic.all
1218
    @errors.pithos.connection
1219
    @errors.pithos.container
1220
    @errors.pithos.object_path
1221
    def _run(self):
1222
        self.client.download_object(
1223
            self.path,
1224
            stdout,
1225
            range_str=self['range'],
1226
            version=self['object_version'],
1227
            if_match=self['if_match'],
1228
            if_none_match=self['if_none_match'],
1229
            if_modified_since=self['if_modified_since'],
1230
            if_unmodified_since=self['if_unmodified_since'])
1231

    
1232
    def main(self, container___path):
1233
        super(self.__class__, self)._run(
1234
            container___path,
1235
            path_is_optional=False)
1236
        self._run()
1237

    
1238

    
1239
@command(pithos_cmds)
1240
class file_download(_file_container_command):
1241
    """Download remote object as local file
1242
    If local destination is a directory:
1243
    *   download <container>:<path> <local dir> -R
1244
    will download all files on <container> prefixed as <path>,
1245
    to <local dir>/<full path>
1246
    *   download <container>:<path> <local dir> --exact-match
1247
    will download only one file, exactly matching <path>
1248
    ATTENTION: to download cont:dir1/dir2/file there must exist objects
1249
    cont:dir1 and cont:dir1/dir2 of type application/directory
1250
    To create directory objects, use /file mkdir
1251
    """
1252

    
1253
    arguments = dict(
1254
        resume=FlagArgument('Resume instead of overwrite', ('-r', '--resume')),
1255
        range=RangeArgument('show range of data', '--range'),
1256
        if_match=ValueArgument('show output if ETags match', '--if-match'),
1257
        if_none_match=ValueArgument(
1258
            'show output if ETags match',
1259
            '--if-none-match'),
1260
        if_modified_since=DateArgument(
1261
            'show output modified since then',
1262
            '--if-modified-since'),
1263
        if_unmodified_since=DateArgument(
1264
            'show output unmodified since then',
1265
            '--if-unmodified-since'),
1266
        object_version=ValueArgument(
1267
            'get the specific version',
1268
            ('-O', '--object-version')),
1269
        poolsize=IntArgument('set pool size', '--with-pool-size'),
1270
        progress_bar=ProgressBarArgument(
1271
            'do not show progress bar',
1272
            ('-N', '--no-progress-bar'),
1273
            default=False),
1274
        recursive=FlagArgument(
1275
            'Download a remote path and all its contents',
1276
            ('-R', '--recursive'))
1277
    )
1278

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

    
1334
        lprefix = path.abspath(local_path or path.curdir)
1335
        if path.isdir(lprefix):
1336
            for rpath, remote_is_dir in remotes:
1337
                lpath = path.sep.join([
1338
                    lprefix[:-1] if lprefix.endswith(path.sep) else lprefix,
1339
                    rpath.strip('/').replace('/', path.sep)])
1340
                if remote_is_dir:
1341
                    if path.exists(lpath) and path.isdir(lpath):
1342
                        continue
1343
                    makedirs(lpath)
1344
                elif path.exists(lpath):
1345
                    if not self['resume']:
1346
                        print('File %s exists, aborting...' % lpath)
1347
                        continue
1348
                    with open(lpath, 'rwb+') as f:
1349
                        yield (f, rpath)
1350
                else:
1351
                    with open(lpath, 'wb+') as f:
1352
                        yield (f, rpath)
1353
        elif path.exists(lprefix):
1354
            if len(remotes) > 1:
1355
                raiseCLIError(
1356
                    '%s remote objects cannot be merged in local file %s' % (
1357
                        len(remotes),
1358
                        local_path),
1359
                    details=[
1360
                        'To download multiple objects, local path should be',
1361
                        'a directory, or use download without a local path'])
1362
            (rpath, remote_is_dir) = remotes[0]
1363
            if remote_is_dir:
1364
                raiseCLIError(
1365
                    'Remote directory %s should not replace local file %s' % (
1366
                        rpath,
1367
                        local_path))
1368
            if self['resume']:
1369
                with open(lprefix, 'rwb+') as f:
1370
                    yield (f, rpath)
1371
            else:
1372
                raiseCLIError(
1373
                    'Local file %s already exist' % local_path,
1374
                    details=['Try --resume to overwrite it'])
1375
        else:
1376
            if len(remotes) > 1 or remotes[0][1]:
1377
                raiseCLIError(
1378
                    'Local directory %s does not exist' % local_path)
1379
            with open(lprefix, 'wb+') as f:
1380
                yield (f, remotes[0][0])
1381

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

    
1433
    def main(self, container___path, local_path=None):
1434
        super(self.__class__, self)._run(container___path)
1435
        self._run(local_path=local_path)
1436

    
1437

    
1438
@command(pithos_cmds)
1439
class file_hashmap(_file_container_command, _optional_json):
1440
    """Get the hash-map of an object"""
1441

    
1442
    arguments = dict(
1443
        if_match=ValueArgument('show output if ETags match', '--if-match'),
1444
        if_none_match=ValueArgument(
1445
            'show output if ETags match', '--if-none-match'),
1446
        if_modified_since=DateArgument(
1447
            'show output modified since then', '--if-modified-since'),
1448
        if_unmodified_since=DateArgument(
1449
            'show output unmodified since then', '--if-unmodified-since'),
1450
        object_version=ValueArgument(
1451
            'get the specific version', ('-O', '--object-version'))
1452
    )
1453

    
1454
    @errors.generic.all
1455
    @errors.pithos.connection
1456
    @errors.pithos.container
1457
    @errors.pithos.object_path
1458
    def _run(self):
1459
        self._print(self.client.get_object_hashmap(
1460
            self.path,
1461
            version=self['object_version'],
1462
            if_match=self['if_match'],
1463
            if_none_match=self['if_none_match'],
1464
            if_modified_since=self['if_modified_since'],
1465
            if_unmodified_since=self['if_unmodified_since']), print_dict)
1466

    
1467
    def main(self, container___path):
1468
        super(self.__class__, self)._run(
1469
            container___path,
1470
            path_is_optional=False)
1471
        self._run()
1472

    
1473

    
1474
@command(pithos_cmds)
1475
class file_delete(_file_container_command, _optional_output_cmd):
1476
    """Delete a container [or an object]
1477
    How to delete a non-empty container:
1478
    - empty the container:  /file delete -R <container>
1479
    - delete it:            /file delete <container>
1480
    .
1481
    Semantics of directory deletion:
1482
    .a preserve the contents: /file delete <container>:<directory>
1483
    .    objects of the form dir/filename can exist with a dir object
1484
    .b delete contents:       /file delete -R <container>:<directory>
1485
    .    all dir/* objects are affected, even if dir does not exist
1486
    .
1487
    To restore a deleted object OBJ in a container CONT:
1488
    - get object versions: /file versions CONT:OBJ
1489
    .   and choose the version to be restored
1490
    - restore the object:  /file copy --source-version=<version> CONT:OBJ OBJ
1491
    """
1492

    
1493
    arguments = dict(
1494
        until=DateArgument('remove history until that date', '--until'),
1495
        yes=FlagArgument('Do not prompt for permission', '--yes'),
1496
        recursive=FlagArgument(
1497
            'empty dir or container and delete (if dir)',
1498
            ('-R', '--recursive'))
1499
    )
1500

    
1501
    def __init__(self, arguments={}, auth_base=None):
1502
        super(self.__class__, self).__init__(arguments, auth_base)
1503
        self['delimiter'] = DelimiterArgument(
1504
            self,
1505
            parsed_name='--delimiter',
1506
            help='delete objects prefixed with <object><delimiter>')
1507

    
1508
    @errors.generic.all
1509
    @errors.pithos.connection
1510
    @errors.pithos.container
1511
    @errors.pithos.object_path
1512
    def _run(self):
1513
        if self.path:
1514
            if self['yes'] or ask_user(
1515
                    'Delete %s:%s ?' % (self.container, self.path)):
1516
                self._optional_output(self.client.del_object(
1517
                    self.path,
1518
                    until=self['until'], delimiter=self['delimiter']))
1519
            else:
1520
                print('Aborted')
1521
        else:
1522
            if self['recursive']:
1523
                ask_msg = 'Delete container contents'
1524
            else:
1525
                ask_msg = 'Delete container'
1526
            if self['yes'] or ask_user('%s %s ?' % (ask_msg, self.container)):
1527
                self._optional_output(self.client.del_container(
1528
                    until=self['until'], delimiter=self['delimiter']))
1529
            else:
1530
                print('Aborted')
1531

    
1532
    def main(self, container____path__=None):
1533
        super(self.__class__, self)._run(container____path__)
1534
        self._run()
1535

    
1536

    
1537
@command(pithos_cmds)
1538
class file_purge(_file_container_command, _optional_output_cmd):
1539
    """Delete a container and release related data blocks
1540
    Non-empty containers can not purged.
1541
    To purge a container with content:
1542
    .   /file delete -R <container>
1543
    .      objects are deleted, but data blocks remain on server
1544
    .   /file purge <container>
1545
    .      container and data blocks are released and deleted
1546
    """
1547

    
1548
    arguments = dict(
1549
        yes=FlagArgument('Do not prompt for permission', '--yes'),
1550
        force=FlagArgument('purge even if not empty', ('-F', '--force'))
1551
    )
1552

    
1553
    @errors.generic.all
1554
    @errors.pithos.connection
1555
    @errors.pithos.container
1556
    def _run(self):
1557
        if self['yes'] or ask_user('Purge container %s?' % self.container):
1558
            try:
1559
                r = self.client.purge_container()
1560
            except ClientError as ce:
1561
                if ce.status in (409,):
1562
                    if self['force']:
1563
                        self.client.del_container(delimiter='/')
1564
                        r = self.client.purge_container()
1565
                    else:
1566
                        raiseCLIError(ce, details=['Try -F to force-purge'])
1567
                else:
1568
                    raise
1569
            self._optional_output(r)
1570
        else:
1571
            print('Aborted')
1572

    
1573
    def main(self, container=None):
1574
        super(self.__class__, self)._run(container)
1575
        if container and self.container != container:
1576
            raiseCLIError('Invalid container name %s' % container, details=[
1577
                'Did you mean "%s" ?' % self.container,
1578
                'Use --container for names containing :'])
1579
        self._run()
1580

    
1581

    
1582
@command(pithos_cmds)
1583
class file_publish(_file_container_command):
1584
    """Publish the object and print the public url"""
1585

    
1586
    @errors.generic.all
1587
    @errors.pithos.connection
1588
    @errors.pithos.container
1589
    @errors.pithos.object_path
1590
    def _run(self):
1591
        url = self.client.publish_object(self.path)
1592
        print(url)
1593

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

    
1600

    
1601
@command(pithos_cmds)
1602
class file_unpublish(_file_container_command, _optional_output_cmd):
1603
    """Unpublish an object"""
1604

    
1605
    @errors.generic.all
1606
    @errors.pithos.connection
1607
    @errors.pithos.container
1608
    @errors.pithos.object_path
1609
    def _run(self):
1610
            self._optional_output(self.client.unpublish_object(self.path))
1611

    
1612
    def main(self, container___path):
1613
        super(self.__class__, self)._run(
1614
            container___path,
1615
            path_is_optional=False)
1616
        self._run()
1617

    
1618

    
1619
@command(pithos_cmds)
1620
class file_permissions(_pithos_init):
1621
    """Manage user and group accessibility for objects
1622
    Permissions are lists of users and user groups. There are read and write
1623
    permissions. Users and groups with write permission have also read
1624
    permission.
1625
    """
1626

    
1627

    
1628
def print_permissions(permissions_dict):
1629
    expected_keys = ('read', 'write')
1630
    if set(permissions_dict).issubset(expected_keys):
1631
        print_dict(permissions_dict)
1632
    else:
1633
        invalid_keys = set(permissions_dict.keys()).difference(expected_keys)
1634
        raiseCLIError(
1635
            'Illegal permission keys: %s' % ', '.join(invalid_keys),
1636
            importance=1, details=[
1637
                'Valid permission types: %s' % ' '.join(expected_keys)])
1638

    
1639

    
1640
@command(pithos_cmds)
1641
class file_permissions_get(_file_container_command, _optional_json):
1642
    """Get read and write permissions of an object"""
1643

    
1644
    @errors.generic.all
1645
    @errors.pithos.connection
1646
    @errors.pithos.container
1647
    @errors.pithos.object_path
1648
    def _run(self):
1649
        self._print(
1650
            self.client.get_object_sharing(self.path), print_permissions)
1651

    
1652
    def main(self, container___path):
1653
        super(self.__class__, self)._run(
1654
            container___path,
1655
            path_is_optional=False)
1656
        self._run()
1657

    
1658

    
1659
@command(pithos_cmds)
1660
class file_permissions_set(_file_container_command, _optional_output_cmd):
1661
    """Set permissions for an object
1662
    New permissions overwrite existing permissions.
1663
    Permission format:
1664
    -   read=<username>[,usergroup[,...]]
1665
    -   write=<username>[,usegroup[,...]]
1666
    E.g. to give read permissions for file F to users A and B and write for C:
1667
    .       /file permissions set F read=A,B write=C
1668
    """
1669

    
1670
    @errors.generic.all
1671
    def format_permission_dict(self, permissions):
1672
        read = False
1673
        write = False
1674
        for perms in permissions:
1675
            splstr = perms.split('=')
1676
            if 'read' == splstr[0]:
1677
                read = [ug.strip() for ug in splstr[1].split(',')]
1678
            elif 'write' == splstr[0]:
1679
                write = [ug.strip() for ug in splstr[1].split(',')]
1680
            else:
1681
                msg = 'Usage:\tread=<groups,users> write=<groups,users>'
1682
                raiseCLIError(None, msg)
1683
        return (read, write)
1684

    
1685
    @errors.generic.all
1686
    @errors.pithos.connection
1687
    @errors.pithos.container
1688
    @errors.pithos.object_path
1689
    def _run(self, read, write):
1690
        self._optional_output(self.client.set_object_sharing(
1691
            self.path,
1692
            read_permission=read, write_permission=write))
1693

    
1694
    def main(self, container___path, *permissions):
1695
        super(self.__class__, self)._run(
1696
            container___path,
1697
            path_is_optional=False)
1698
        (read, write) = self.format_permission_dict(permissions)
1699
        self._run(read, write)
1700

    
1701

    
1702
@command(pithos_cmds)
1703
class file_permissions_delete(_file_container_command, _optional_output_cmd):
1704
    """Delete all permissions set on object
1705
    To modify permissions, use /file permissions set
1706
    """
1707

    
1708
    @errors.generic.all
1709
    @errors.pithos.connection
1710
    @errors.pithos.container
1711
    @errors.pithos.object_path
1712
    def _run(self):
1713
        self._optional_output(self.client.del_object_sharing(self.path))
1714

    
1715
    def main(self, container___path):
1716
        super(self.__class__, self)._run(
1717
            container___path,
1718
            path_is_optional=False)
1719
        self._run()
1720

    
1721

    
1722
@command(pithos_cmds)
1723
class file_info(_file_container_command, _optional_json):
1724
    """Get detailed information for user account, containers or objects
1725
    to get account info:    /file info
1726
    to get container info:  /file info <container>
1727
    to get object info:     /file info <container>:<path>
1728
    """
1729

    
1730
    arguments = dict(
1731
        object_version=ValueArgument(
1732
            'show specific version \ (applies only for objects)',
1733
            ('-O', '--object-version'))
1734
    )
1735

    
1736
    @errors.generic.all
1737
    @errors.pithos.connection
1738
    @errors.pithos.container
1739
    @errors.pithos.object_path
1740
    def _run(self):
1741
        if self.container is None:
1742
            r = self.client.get_account_info()
1743
        elif self.path is None:
1744
            r = self.client.get_container_info(self.container)
1745
        else:
1746
            r = self.client.get_object_info(
1747
                self.path,
1748
                version=self['object_version'])
1749
        self._print(r, print_dict)
1750

    
1751
    def main(self, container____path__=None):
1752
        super(self.__class__, self)._run(container____path__)
1753
        self._run()
1754

    
1755

    
1756
@command(pithos_cmds)
1757
class file_metadata(_pithos_init):
1758
    """Metadata are attached on objects. They are formed as key:value pairs.
1759
    They can have arbitary values.
1760
    """
1761

    
1762

    
1763
@command(pithos_cmds)
1764
class file_metadata_get(_file_container_command, _optional_json):
1765
    """Get metadata for account, containers or objects"""
1766

    
1767
    arguments = dict(
1768
        detail=FlagArgument('show detailed output', ('-l', '--details')),
1769
        until=DateArgument('show metadata until then', '--until'),
1770
        object_version=ValueArgument(
1771
            'show specific version \ (applies only for objects)',
1772
            ('-O', '--object-version'))
1773
    )
1774

    
1775
    @errors.generic.all
1776
    @errors.pithos.connection
1777
    @errors.pithos.container
1778
    @errors.pithos.object_path
1779
    def _run(self):
1780
        until = self['until']
1781
        r = None
1782
        if self.container is None:
1783
            if self['detail']:
1784
                r = self.client.get_account_info(until=until)
1785
            else:
1786
                r = self.client.get_account_meta(until=until)
1787
                r = pretty_keys(r, '-')
1788
        elif self.path is None:
1789
            if self['detail']:
1790
                r = self.client.get_container_info(until=until)
1791
            else:
1792
                cmeta = self.client.get_container_meta(until=until)
1793
                ometa = self.client.get_container_object_meta(until=until)
1794
                r = {}
1795
                if cmeta:
1796
                    r['container-meta'] = pretty_keys(cmeta, '-')
1797
                if ometa:
1798
                    r['object-meta'] = pretty_keys(ometa, '-')
1799
        else:
1800
            if self['detail']:
1801
                r = self.client.get_object_info(
1802
                    self.path,
1803
                    version=self['object_version'])
1804
            else:
1805
                r = self.client.get_object_meta(
1806
                    self.path,
1807
                    version=self['object_version'])
1808
                r = pretty_keys(pretty_keys(r, '-'))
1809
        if r:
1810
            self._print(r, print_dict)
1811

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

    
1816

    
1817
@command(pithos_cmds)
1818
class file_metadata_set(_file_container_command, _optional_output_cmd):
1819
    """Set a piece of metadata for account, container or object"""
1820

    
1821
    @errors.generic.all
1822
    @errors.pithos.connection
1823
    @errors.pithos.container
1824
    @errors.pithos.object_path
1825
    def _run(self, metakey, metaval):
1826
        if not self.container:
1827
            r = self.client.set_account_meta({metakey: metaval})
1828
        elif not self.path:
1829
            r = self.client.set_container_meta({metakey: metaval})
1830
        else:
1831
            r = self.client.set_object_meta(self.path, {metakey: metaval})
1832
        self._optional_output(r)
1833

    
1834
    def main(self, metakey, metaval, container____path__=None):
1835
        super(self.__class__, self)._run(container____path__)
1836
        self._run(metakey=metakey, metaval=metaval)
1837

    
1838

    
1839
@command(pithos_cmds)
1840
class file_metadata_delete(_file_container_command, _optional_output_cmd):
1841
    """Delete metadata with given key from account, container or object
1842
    - to get metadata of current account: /file metadata get
1843
    - to get metadata of a container:     /file metadata get <container>
1844
    - to get metadata of an object:       /file metadata get <container>:<path>
1845
    """
1846

    
1847
    @errors.generic.all
1848
    @errors.pithos.connection
1849
    @errors.pithos.container
1850
    @errors.pithos.object_path
1851
    def _run(self, metakey):
1852
        if self.container is None:
1853
            r = self.client.del_account_meta(metakey)
1854
        elif self.path is None:
1855
            r = self.client.del_container_meta(metakey)
1856
        else:
1857
            r = self.client.del_object_meta(self.path, metakey)
1858
        self._optional_output(r)
1859

    
1860
    def main(self, metakey, container____path__=None):
1861
        super(self.__class__, self)._run(container____path__)
1862
        self._run(metakey)
1863

    
1864

    
1865
@command(pithos_cmds)
1866
class file_quota(_file_account_command, _optional_json):
1867
    """Get account quota"""
1868

    
1869
    arguments = dict(
1870
        in_bytes=FlagArgument('Show result in bytes', ('-b', '--bytes'))
1871
    )
1872

    
1873
    @errors.generic.all
1874
    @errors.pithos.connection
1875
    def _run(self):
1876

    
1877
        def pretty_print(output):
1878
            if not self['in_bytes']:
1879
                for k in output:
1880
                    output[k] = format_size(output[k])
1881
            pretty_dict(output, '-')
1882

    
1883
        self._print(self.client.get_account_quota(), pretty_print)
1884

    
1885
    def main(self, custom_uuid=None):
1886
        super(self.__class__, self)._run(custom_account=custom_uuid)
1887
        self._run()
1888

    
1889

    
1890
@command(pithos_cmds)
1891
class file_containerlimit(_pithos_init):
1892
    """Container size limit commands"""
1893

    
1894

    
1895
@command(pithos_cmds)
1896
class file_containerlimit_get(_file_container_command, _optional_json):
1897
    """Get container size limit"""
1898

    
1899
    arguments = dict(
1900
        in_bytes=FlagArgument('Show result in bytes', ('-b', '--bytes'))
1901
    )
1902

    
1903
    @errors.generic.all
1904
    @errors.pithos.container
1905
    def _run(self):
1906

    
1907
        def pretty_print(output):
1908
            if not self['in_bytes']:
1909
                for k, v in output.items():
1910
                    output[k] = 'unlimited' if '0' == v else format_size(v)
1911
            pretty_dict(output, '-')
1912

    
1913
        self._print(
1914
            self.client.get_container_limit(self.container), pretty_print)
1915

    
1916
    def main(self, container=None):
1917
        super(self.__class__, self)._run()
1918
        self.container = container
1919
        self._run()
1920

    
1921

    
1922
@command(pithos_cmds)
1923
class file_containerlimit_set(_file_account_command, _optional_output_cmd):
1924
    """Set new storage limit for a container
1925
    By default, the limit is set in bytes
1926
    Users may specify a different unit, e.g:
1927
    /file containerlimit set 2.3GB mycontainer
1928
    Valid units: B, KiB (1024 B), KB (1000 B), MiB, MB, GiB, GB, TiB, TB
1929
    To set container limit to "unlimited", use 0
1930
    """
1931

    
1932
    @errors.generic.all
1933
    def _calculate_limit(self, user_input):
1934
        limit = 0
1935
        try:
1936
            limit = int(user_input)
1937
        except ValueError:
1938
            index = 0
1939
            digits = [str(num) for num in range(0, 10)] + ['.']
1940
            while user_input[index] in digits:
1941
                index += 1
1942
            limit = user_input[:index]
1943
            format = user_input[index:]
1944
            try:
1945
                return to_bytes(limit, format)
1946
            except Exception as qe:
1947
                msg = 'Failed to convert %s to bytes' % user_input,
1948
                raiseCLIError(qe, msg, details=[
1949
                    'Syntax: containerlimit set <limit>[format] [container]',
1950
                    'e.g.: containerlimit set 2.3GB mycontainer',
1951
                    'Valid formats:',
1952
                    '(*1024): B, KiB, MiB, GiB, TiB',
1953
                    '(*1000): B, KB, MB, GB, TB'])
1954
        return limit
1955

    
1956
    @errors.generic.all
1957
    @errors.pithos.connection
1958
    @errors.pithos.container
1959
    def _run(self, limit):
1960
        if self.container:
1961
            self.client.container = self.container
1962
        self._optional_output(self.client.set_container_limit(limit))
1963

    
1964
    def main(self, limit, container=None):
1965
        super(self.__class__, self)._run()
1966
        limit = self._calculate_limit(limit)
1967
        self.container = container
1968
        self._run(limit)
1969

    
1970

    
1971
@command(pithos_cmds)
1972
class file_versioning(_pithos_init):
1973
    """Manage the versioning scheme of current pithos user account"""
1974

    
1975

    
1976
@command(pithos_cmds)
1977
class file_versioning_get(_file_account_command, _optional_json):
1978
    """Get  versioning for account or container"""
1979

    
1980
    @errors.generic.all
1981
    @errors.pithos.connection
1982
    @errors.pithos.container
1983
    def _run(self):
1984
        #if self.container:
1985
        #    r = self.client.get_container_versioning(self.container)
1986
        #else:
1987
        #    r = self.client.get_account_versioning()
1988
        self._print(
1989
            self.client.get_container_versioning(self.container) if (
1990
                self.container) else self.client.get_account_versioning(),
1991
            print_dict)
1992

    
1993
    def main(self, container=None):
1994
        super(self.__class__, self)._run()
1995
        self.container = container
1996
        self._run()
1997

    
1998

    
1999
@command(pithos_cmds)
2000
class file_versioning_set(_file_account_command, _optional_output_cmd):
2001
    """Set versioning mode (auto, none) for account or container"""
2002

    
2003
    def _check_versioning(self, versioning):
2004
        if versioning and versioning.lower() in ('auto', 'none'):
2005
            return versioning.lower()
2006
        raiseCLIError('Invalid versioning %s' % versioning, details=[
2007
            'Versioning can be auto or none'])
2008

    
2009
    @errors.generic.all
2010
    @errors.pithos.connection
2011
    @errors.pithos.container
2012
    def _run(self, versioning):
2013
        if self.container:
2014
            self.client.container = self.container
2015
            r = self.client.set_container_versioning(versioning)
2016
        else:
2017
            r = self.client.set_account_versioning(versioning)
2018
        self._optional_output(r)
2019

    
2020
    def main(self, versioning, container=None):
2021
        super(self.__class__, self)._run()
2022
        self._run(self._check_versioning(versioning))
2023

    
2024

    
2025
@command(pithos_cmds)
2026
class file_group(_pithos_init):
2027
    """Manage access groups and group members"""
2028

    
2029

    
2030
@command(pithos_cmds)
2031
class file_group_list(_file_account_command, _optional_json):
2032
    """list all groups and group members"""
2033

    
2034
    @errors.generic.all
2035
    @errors.pithos.connection
2036
    def _run(self):
2037
        self._print(self.client.get_account_group(), pretty_dict, delim='-')
2038

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

    
2043

    
2044
@command(pithos_cmds)
2045
class file_group_set(_file_account_command, _optional_output_cmd):
2046
    """Set a user group"""
2047

    
2048
    @errors.generic.all
2049
    @errors.pithos.connection
2050
    def _run(self, groupname, *users):
2051
        self._optional_output(self.client.set_account_group(groupname, users))
2052

    
2053
    def main(self, groupname, *users):
2054
        super(self.__class__, self)._run()
2055
        if users:
2056
            self._run(groupname, *users)
2057
        else:
2058
            raiseCLIError('No users to add in group %s' % groupname)
2059

    
2060

    
2061
@command(pithos_cmds)
2062
class file_group_delete(_file_account_command, _optional_output_cmd):
2063
    """Delete a user group"""
2064

    
2065
    @errors.generic.all
2066
    @errors.pithos.connection
2067
    def _run(self, groupname):
2068
        self._optional_output(self.client.del_account_group(groupname))
2069

    
2070
    def main(self, groupname):
2071
        super(self.__class__, self)._run()
2072
        self._run(groupname)
2073

    
2074

    
2075
@command(pithos_cmds)
2076
class file_sharers(_file_account_command, _optional_json):
2077
    """List the accounts that share objects with current user"""
2078

    
2079
    arguments = dict(
2080
        detail=FlagArgument('show detailed output', ('-l', '--details')),
2081
        marker=ValueArgument('show output greater then marker', '--marker')
2082
    )
2083

    
2084
    @errors.generic.all
2085
    @errors.pithos.connection
2086
    def _run(self):
2087
        accounts = self.client.get_sharing_accounts(marker=self['marker'])
2088
        if self['json_output'] or self['detail']:
2089
            self._print(accounts)
2090
        else:
2091
            self._print([acc['name'] for acc in accounts])
2092

    
2093
    def main(self):
2094
        super(self.__class__, self)._run()
2095
        self._run()
2096

    
2097

    
2098
def version_print(versions):
2099
    print_items([dict(id=vitem[0], created=strftime(
2100
        '%d-%m-%Y %H:%M:%S',
2101
        localtime(float(vitem[1])))) for vitem in versions])
2102

    
2103

    
2104
@command(pithos_cmds)
2105
class file_versions(_file_container_command, _optional_json):
2106
    """Get the list of object versions
2107
    Deleted objects may still have versions that can be used to restore it and
2108
    get information about its previous state.
2109
    The version number can be used in a number of other commands, like info,
2110
    copy, move, meta. See these commands for more information, e.g.
2111
    /file info -h
2112
    """
2113

    
2114
    @errors.generic.all
2115
    @errors.pithos.connection
2116
    @errors.pithos.container
2117
    @errors.pithos.object_path
2118
    def _run(self):
2119
        self._print(
2120
            self.client.get_object_versionlist(self.path), version_print)
2121

    
2122
    def main(self, container___path):
2123
        super(file_versions, self)._run(
2124
            container___path,
2125
            path_is_optional=False)
2126
        self._run()