Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / pithos.py @ 776b275c

History | View | Annotate | Download (78.6 kB)

1
# Copyright 2011-2013 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, guess_mime_type)
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 addLogSettings, DontRaiseKeyError
49
from kamaki.cli.commands import _optional_output_cmd, _optional_json
50
from kamaki.clients.pithos import PithosClient, ClientError
51
from kamaki.clients.astakos import AstakosClient
52

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

    
56

    
57
# Argument functionality
58

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

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

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

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

    
79

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

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

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

    
121

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

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

    
133
    @value.setter
134
    def value(self, newvalues):
135
        if not newvalues:
136
            self._value = self.default
137
            return
138
        self._value = ''
139
        for newvalue in newvalues.split(','):
140
            self._value = ('%s,' % self._value) if self._value else ''
141
            start, sep, end = newvalue.partition('-')
142
            if sep:
143
                if start:
144
                    start, end = (int(start), int(end))
145
                    assert start <= end, 'Invalid range value %s' % newvalue
146
                    self._value += '%s-%s' % (int(start), int(end))
147
                else:
148
                    self._value += '-%s' % int(end)
149
            else:
150
                self._value += '%s' % int(start)
151

    
152

    
153
# Command specs
154

    
155

    
156
class _pithos_init(_command_init):
157
    """Initialize a pithos+ kamaki client"""
158

    
159
    @staticmethod
160
    def _is_dir(remote_dict):
161
        return 'application/directory' == remote_dict.get(
162
            'content_type', remote_dict.get('content-type', ''))
163

    
164
    @DontRaiseKeyError
165
    def _custom_container(self):
166
        return self.config.get_cloud(self.cloud, 'pithos_container')
167

    
168
    @DontRaiseKeyError
169
    def _custom_uuid(self):
170
        return self.config.get_cloud(self.cloud, 'pithos_uuid')
171

    
172
    def _set_account(self):
173
        self.account = self._custom_uuid()
174
        if self.account:
175
            return
176
        if getattr(self, 'auth_base', False):
177
            self.account = self.auth_base.user_term('id', self.token)
178
        else:
179
            astakos_url = self._custom_url('astakos')
180
            astakos_token = self._custom_token('astakos') or self.token
181
            if not astakos_url:
182
                raise CLIBaseUrlError(service='astakos')
183
            astakos = AstakosClient(astakos_url, astakos_token)
184
            self.account = astakos.user_term('id')
185

    
186
    @errors.generic.all
187
    @addLogSettings
188
    def _run(self):
189
        self.base_url = None
190
        if getattr(self, 'cloud', None):
191
            self.base_url = self._custom_url('pithos')
192
        else:
193
            self.cloud = 'default'
194
        self.token = self._custom_token('pithos')
195
        self.container = self._custom_container()
196

    
197
        if getattr(self, 'auth_base', False):
198
            self.token = self.token or self.auth_base.token
199
            if not self.base_url:
200
                pithos_endpoints = self.auth_base.get_service_endpoints(
201
                    self._custom_type('pithos') or 'object-store',
202
                    self._custom_version('pithos') or '')
203
                self.base_url = pithos_endpoints['publicURL']
204
        elif not self.base_url:
205
            raise CLIBaseUrlError(service='pithos')
206

    
207
        self._set_account()
208
        self.client = PithosClient(
209
            base_url=self.base_url,
210
            token=self.token,
211
            account=self.account,
212
            container=self.container)
213

    
214
    def main(self):
215
        self._run()
216

    
217

    
218
class _file_account_command(_pithos_init):
219
    """Base class for account level storage commands"""
220

    
221
    def __init__(self, arguments={}, auth_base=None, cloud=None):
222
        super(_file_account_command, self).__init__(
223
            arguments, auth_base, cloud)
224
        self['account'] = ValueArgument(
225
            'Set user account (not permanent)', ('-A', '--account'))
226

    
227
    def _run(self, custom_account=None):
228
        super(_file_account_command, self)._run()
229
        if custom_account:
230
            self.client.account = custom_account
231
        elif self['account']:
232
            self.client.account = self['account']
233

    
234
    @errors.generic.all
235
    def main(self):
236
        self._run()
237

    
238

    
239
class _file_container_command(_file_account_command):
240
    """Base class for container level storage commands"""
241

    
242
    container = None
243
    path = None
244

    
245
    def __init__(self, arguments={}, auth_base=None, cloud=None):
246
        super(_file_container_command, self).__init__(
247
            arguments, auth_base, cloud)
248
        self['container'] = ValueArgument(
249
            'Set container to work with (temporary)', ('-C', '--container'))
250

    
251
    def extract_container_and_path(
252
            self,
253
            container_with_path,
254
            path_is_optional=True):
255
        """Contains all heuristics for deciding what should be used as
256
        container or path. Options are:
257
        * user string of the form container:path
258
        * self.container, self.path variables set by super constructor, or
259
        explicitly by the caller application
260
        Error handling is explicit as these error cases happen only here
261
        """
262
        try:
263
            assert isinstance(container_with_path, str)
264
        except AssertionError as err:
265
            if self['container'] and path_is_optional:
266
                self.container = self['container']
267
                self.client.container = self['container']
268
                return
269
            raiseCLIError(err)
270

    
271
        user_cont, sep, userpath = container_with_path.partition(':')
272

    
273
        if sep:
274
            if not user_cont:
275
                raiseCLIError(CLISyntaxError(
276
                    'Container is missing\n',
277
                    details=errors.pithos.container_howto))
278
            alt_cont = self['container']
279
            if alt_cont and user_cont != alt_cont:
280
                raiseCLIError(CLISyntaxError(
281
                    'Conflict: 2 containers (%s, %s)' % (user_cont, alt_cont),
282
                    details=errors.pithos.container_howto)
283
                )
284
            self.container = user_cont
285
            if not userpath:
286
                raiseCLIError(CLISyntaxError(
287
                    'Path is missing for object in container %s' % user_cont,
288
                    details=errors.pithos.container_howto)
289
                )
290
            self.path = userpath
291
        else:
292
            alt_cont = self['container'] or self.client.container
293
            if alt_cont:
294
                self.container = alt_cont
295
                self.path = user_cont
296
            elif path_is_optional:
297
                self.container = user_cont
298
                self.path = None
299
            else:
300
                self.container = user_cont
301
                raiseCLIError(CLISyntaxError(
302
                    'Both container and path are required',
303
                    details=errors.pithos.container_howto)
304
                )
305

    
306
    @errors.generic.all
307
    def _run(self, container_with_path=None, path_is_optional=True):
308
        super(_file_container_command, self)._run()
309
        if self['container']:
310
            self.client.container = self['container']
311
            if container_with_path:
312
                self.path = container_with_path
313
            elif not path_is_optional:
314
                raise CLISyntaxError(
315
                    'Both container and path are required',
316
                    details=errors.pithos.container_howto)
317
        elif container_with_path:
318
            self.extract_container_and_path(
319
                container_with_path,
320
                path_is_optional)
321
            self.client.container = self.container
322
        self.container = self.client.container
323

    
324
    def main(self, container_with_path=None, path_is_optional=True):
325
        self._run(container_with_path, path_is_optional)
326

    
327

    
328
@command(pithos_cmds)
329
class file_list(_file_container_command, _optional_json):
330
    """List containers, object trees or objects in a directory
331
    Use with:
332
    1 no parameters : containers in current account
333
    2. one parameter (container) or --container : contents of container
334
    3. <container>:<prefix> or --container=<container> <prefix>: objects in
335
    .   container starting with prefix
336
    """
337

    
338
    arguments = dict(
339
        detail=FlagArgument('detailed output', ('-l', '--list')),
340
        limit=IntArgument('limit number of listed items', ('-n', '--number')),
341
        marker=ValueArgument('output greater that marker', '--marker'),
342
        prefix=ValueArgument('output starting with prefix', '--prefix'),
343
        delimiter=ValueArgument('show output up to delimiter', '--delimiter'),
344
        path=ValueArgument(
345
            'show output starting with prefix up to /', '--path'),
346
        meta=ValueArgument(
347
            'show output with specified meta keys', '--meta',
348
            default=[]),
349
        if_modified_since=ValueArgument(
350
            'show output modified since then', '--if-modified-since'),
351
        if_unmodified_since=ValueArgument(
352
            'show output not modified since then', '--if-unmodified-since'),
353
        until=DateArgument('show metadata until then', '--until'),
354
        format=ValueArgument(
355
            'format to parse until data (default: d/m/Y H:M:S )', '--format'),
356
        shared=FlagArgument('show only shared', '--shared'),
357
        more=FlagArgument(
358
            'output results in pages (-n to set items per page, default 10)',
359
            '--more'),
360
        exact_match=FlagArgument(
361
            'Show only objects that match exactly with path',
362
            '--exact-match'),
363
        enum=FlagArgument('Enumerate results', '--enumerate')
364
    )
365

    
366
    def print_objects(self, object_list):
367
        if self['json_output']:
368
            print_json(object_list)
369
            return
370
        limit = int(self['limit']) if self['limit'] > 0 else len(object_list)
371
        for index, obj in enumerate(object_list):
372
            if self['exact_match'] and self.path and not (
373
                    obj['name'] == self.path or 'content_type' in obj):
374
                continue
375
            pretty_obj = obj.copy()
376
            index += 1
377
            empty_space = ' ' * (len(str(len(object_list))) - len(str(index)))
378
            if 'subdir' in obj:
379
                continue
380
            if obj['content_type'] == 'application/directory':
381
                isDir = True
382
                size = 'D'
383
            else:
384
                isDir = False
385
                size = format_size(obj['bytes'])
386
                pretty_obj['bytes'] = '%s (%s)' % (obj['bytes'], size)
387
            oname = bold(obj['name'])
388
            prfx = ('%s%s. ' % (empty_space, index)) if self['enum'] else ''
389
            if self['detail']:
390
                print('%s%s' % (prfx, oname))
391
                print_dict(pretty_keys(pretty_obj), exclude=('name'))
392
                print
393
            else:
394
                oname = '%s%9s %s' % (prfx, size, oname)
395
                oname += '/' if isDir else ''
396
                print(oname)
397
            if self['more']:
398
                page_hold(index, limit, len(object_list))
399

    
400
    def print_containers(self, container_list):
401
        if self['json_output']:
402
            print_json(container_list)
403
            return
404
        limit = int(self['limit']) if self['limit'] > 0\
405
            else len(container_list)
406
        for index, container in enumerate(container_list):
407
            if 'bytes' in container:
408
                size = format_size(container['bytes'])
409
            prfx = ('%s. ' % (index + 1)) if self['enum'] else ''
410
            cname = '%s%s' % (prfx, bold(container['name']))
411
            if self['detail']:
412
                print(cname)
413
                pretty_c = container.copy()
414
                if 'bytes' in container:
415
                    pretty_c['bytes'] = '%s (%s)' % (container['bytes'], size)
416
                print_dict(pretty_keys(pretty_c), exclude=('name'))
417
                print
418
            else:
419
                if 'count' in container and 'bytes' in container:
420
                    print('%s (%s, %s objects)' % (
421
                        cname,
422
                        size,
423
                        container['count']))
424
                else:
425
                    print(cname)
426
            if self['more']:
427
                page_hold(index + 1, limit, len(container_list))
428

    
429
    @errors.generic.all
430
    @errors.pithos.connection
431
    @errors.pithos.object_path
432
    @errors.pithos.container
433
    def _run(self):
434
        if self.container is None:
435
            r = self.client.account_get(
436
                limit=False if self['more'] else self['limit'],
437
                marker=self['marker'],
438
                if_modified_since=self['if_modified_since'],
439
                if_unmodified_since=self['if_unmodified_since'],
440
                until=self['until'],
441
                show_only_shared=self['shared'])
442
            self._print(r.json, self.print_containers)
443
        else:
444
            prefix = self.path or self['prefix']
445
            r = self.client.container_get(
446
                limit=False if self['more'] else self['limit'],
447
                marker=self['marker'],
448
                prefix=prefix,
449
                delimiter=self['delimiter'],
450
                path=self['path'],
451
                if_modified_since=self['if_modified_since'],
452
                if_unmodified_since=self['if_unmodified_since'],
453
                until=self['until'],
454
                meta=self['meta'],
455
                show_only_shared=self['shared'])
456
            self._print(r.json, self.print_objects)
457

    
458
    def main(self, container____path__=None):
459
        super(self.__class__, self)._run(container____path__)
460
        self._run()
461

    
462

    
463
@command(pithos_cmds)
464
class file_mkdir(_file_container_command, _optional_output_cmd):
465
    """Create a directory
466
    Kamaki hanldes directories the same way as OOS Storage and Pithos+:
467
    A directory  is   an  object  with  type  "application/directory"
468
    An object with path  dir/name can exist even if  dir does not exist
469
    or even if dir  is  a non  directory  object.  Users can modify dir '
470
    without affecting the dir/name object in any way.
471
    """
472

    
473
    @errors.generic.all
474
    @errors.pithos.connection
475
    @errors.pithos.container
476
    def _run(self):
477
        self._optional_output(self.client.create_directory(self.path))
478

    
479
    def main(self, container___directory):
480
        super(self.__class__, self)._run(
481
            container___directory,
482
            path_is_optional=False)
483
        self._run()
484

    
485

    
486
@command(pithos_cmds)
487
class file_touch(_file_container_command, _optional_output_cmd):
488
    """Create an empty object (file)
489
    If object exists, this command will reset it to 0 length
490
    """
491

    
492
    arguments = dict(
493
        content_type=ValueArgument(
494
            'Set content type (default: application/octet-stream)',
495
            '--content-type',
496
            default='application/octet-stream')
497
    )
498

    
499
    @errors.generic.all
500
    @errors.pithos.connection
501
    @errors.pithos.container
502
    def _run(self):
503
        self._optional_output(
504
            self.client.create_object(self.path, self['content_type']))
505

    
506
    def main(self, container___path):
507
        super(file_touch, self)._run(
508
            container___path,
509
            path_is_optional=False)
510
        self._run()
511

    
512

    
513
@command(pithos_cmds)
514
class file_create(_file_container_command, _optional_output_cmd):
515
    """Create a container"""
516

    
517
    arguments = dict(
518
        versioning=ValueArgument(
519
            'set container versioning (auto/none)', '--versioning'),
520
        limit=IntArgument('set default container limit', '--limit'),
521
        meta=KeyValueArgument(
522
            'set container metadata (can be repeated)', '--meta')
523
    )
524

    
525
    @errors.generic.all
526
    @errors.pithos.connection
527
    @errors.pithos.container
528
    def _run(self, container):
529
        self._optional_output(self.client.create_container(
530
            container=container,
531
            sizelimit=self['limit'],
532
            versioning=self['versioning'],
533
            metadata=self['meta']))
534

    
535
    def main(self, container=None):
536
        super(self.__class__, self)._run(container)
537
        if container and self.container != container:
538
            raiseCLIError('Invalid container name %s' % container, details=[
539
                'Did you mean "%s" ?' % self.container,
540
                'Use --container for names containing :'])
541
        self._run(container)
542

    
543

    
544
class _source_destination_command(_file_container_command):
545

    
546
    arguments = dict(
547
        destination_account=ValueArgument('', ('-a', '--dst-account')),
548
        recursive=FlagArgument('', ('-R', '--recursive')),
549
        prefix=FlagArgument('', '--with-prefix', default=''),
550
        suffix=ValueArgument('', '--with-suffix', default=''),
551
        add_prefix=ValueArgument('', '--add-prefix', default=''),
552
        add_suffix=ValueArgument('', '--add-suffix', default=''),
553
        prefix_replace=ValueArgument('', '--prefix-to-replace', default=''),
554
        suffix_replace=ValueArgument('', '--suffix-to-replace', default=''),
555
    )
556

    
557
    def __init__(self, arguments={}, auth_base=None, cloud=None):
558
        self.arguments.update(arguments)
559
        super(_source_destination_command, self).__init__(
560
            self.arguments, auth_base, cloud)
561

    
562
    def _run(self, source_container___path, path_is_optional=False):
563
        super(_source_destination_command, self)._run(
564
            source_container___path,
565
            path_is_optional)
566
        self.dst_client = PithosClient(
567
            base_url=self.client.base_url,
568
            token=self.client.token,
569
            account=self['destination_account'] or self.client.account)
570

    
571
    @errors.generic.all
572
    @errors.pithos.account
573
    def _dest_container_path(self, dest_container_path):
574
        if self['destination_container']:
575
            self.dst_client.container = self['destination_container']
576
            return (self['destination_container'], dest_container_path)
577
        if dest_container_path:
578
            dst = dest_container_path.split(':')
579
            if len(dst) > 1:
580
                try:
581
                    self.dst_client.container = dst[0]
582
                    self.dst_client.get_container_info(dst[0])
583
                except ClientError as err:
584
                    if err.status in (404, 204):
585
                        raiseCLIError(
586
                            'Destination container %s not found' % dst[0])
587
                    raise
588
                else:
589
                    self.dst_client.container = dst[0]
590
                return (dst[0], dst[1])
591
            return(None, dst[0])
592
        raiseCLIError('No destination container:path provided')
593

    
594
    def _get_all(self, prefix):
595
        return self.client.container_get(prefix=prefix).json
596

    
597
    def _get_src_objects(self, src_path, source_version=None):
598
        """Get a list of the source objects to be called
599

600
        :param src_path: (str) source path
601

602
        :returns: (method, params) a method that returns a list when called
603
        or (object) if it is a single object
604
        """
605
        if src_path and src_path[-1] == '/':
606
            src_path = src_path[:-1]
607

    
608
        if self['prefix']:
609
            return (self._get_all, dict(prefix=src_path))
610
        try:
611
            srcobj = self.client.get_object_info(
612
                src_path, version=source_version)
613
        except ClientError as srcerr:
614
            if srcerr.status == 404:
615
                raiseCLIError(
616
                    'Source object %s not in source container %s' % (
617
                        src_path, self.client.container),
618
                    details=['Hint: --with-prefix to match multiple objects'])
619
            elif srcerr.status not in (204,):
620
                raise
621
            return (self.client.list_objects, {})
622

    
623
        if self._is_dir(srcobj):
624
            if not self['recursive']:
625
                raiseCLIError(
626
                    'Object %s of cont. %s is a dir' % (
627
                        src_path, self.client.container),
628
                    details=['Use --recursive to access directories'])
629
            return (self._get_all, dict(prefix=src_path))
630
        srcobj['name'] = src_path
631
        return srcobj
632

    
633
    def src_dst_pairs(self, dst_path, source_version=None):
634
        src_iter = self._get_src_objects(self.path, source_version)
635
        src_N = isinstance(src_iter, tuple)
636
        add_prefix = self['add_prefix'].strip('/')
637

    
638
        if dst_path and dst_path.endswith('/'):
639
            dst_path = dst_path[:-1]
640

    
641
        try:
642
            dstobj = self.dst_client.get_object_info(dst_path)
643
        except ClientError as trgerr:
644
            if trgerr.status in (404,):
645
                if src_N:
646
                    raiseCLIError(
647
                        'Cannot merge multiple paths to path %s' % dst_path,
648
                        details=[
649
                            'Try to use / or a directory as destination',
650
                            'or create the destination dir (/file mkdir)',
651
                            'or use a single object as source'])
652
            elif trgerr.status not in (204,):
653
                raise
654
        else:
655
            if self._is_dir(dstobj):
656
                add_prefix = '%s/%s' % (dst_path.strip('/'), add_prefix)
657
            elif src_N:
658
                raiseCLIError(
659
                    'Cannot merge multiple paths to path' % dst_path,
660
                    details=[
661
                        'Try to use / or a directory as destination',
662
                        'or create the destination dir (/file mkdir)',
663
                        'or use a single object as source'])
664

    
665
        if src_N:
666
            (method, kwargs) = src_iter
667
            for obj in method(**kwargs):
668
                name = obj['name']
669
                if name.endswith(self['suffix']):
670
                    yield (name, self._get_new_object(name, add_prefix))
671
        elif src_iter['name'].endswith(self['suffix']):
672
            name = src_iter['name']
673
            yield (name, self._get_new_object(dst_path or name, add_prefix))
674
        else:
675
            raiseCLIError('Source path %s conflicts with suffix %s' % (
676
                src_iter['name'], self['suffix']))
677

    
678
    def _get_new_object(self, obj, add_prefix):
679
        if self['prefix_replace'] and obj.startswith(self['prefix_replace']):
680
            obj = obj[len(self['prefix_replace']):]
681
        if self['suffix_replace'] and obj.endswith(self['suffix_replace']):
682
            obj = obj[:-len(self['suffix_replace'])]
683
        return add_prefix + obj + self['add_suffix']
684

    
685

    
686
@command(pithos_cmds)
687
class file_copy(_source_destination_command, _optional_output_cmd):
688
    """Copy objects from container to (another) container
689
    Semantics:
690
    copy cont:path dir
691
    .   transfer path as dir/path
692
    copy cont:path cont2:
693
    .   trasnfer all <obj> prefixed with path to container cont2
694
    copy cont:path [cont2:]path2
695
    .   transfer path to path2
696
    Use options:
697
    1. <container1>:<path1> [container2:]<path2> : if container2 is not given,
698
    destination is container1:path2
699
    2. <container>:<path1> <path2> : make a copy in the same container
700
    3. Can use --container= instead of <container1>
701
    """
702

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

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

    
760
    def main(
761
            self, source_container___path,
762
            destination_container___path=None):
763
        super(file_copy, self)._run(
764
            source_container___path,
765
            path_is_optional=False)
766
        (dst_cont, dst_path) = self._dest_container_path(
767
            destination_container___path)
768
        self.dst_client.container = dst_cont or self.container
769
        self._run(dst_path=dst_path or '')
770

    
771

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

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

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

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

    
855

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

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

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

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

    
892

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

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

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

    
909

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

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

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

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

    
954
    def main(self, local_path, container___path, start, end):
955
        super(self.__class__, self)._run(
956
            container___path, 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', '--content-encoding'),
978
        content_disposition=ValueArgument(
979
            'the presentation style of the object', '--content-disposition'),
980
        content_type=ValueArgument(
981
            'specify content type', '--content-type',
982
            default='application/octet-stream'),
983
        sharing=SharingArgument(
984
            '\n'.join([
985
                'define object sharing policy',
986
                '    ( "read=user1,grp1,user2,... write=user1,grp2,..." )']),
987
            '--sharing'),
988
        public=FlagArgument('make object publicly accessible', '--public')
989
    )
990

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

    
1005
    def main(self, container___path):
1006
        super(self.__class__, self)._run(
1007
            container___path, path_is_optional=False)
1008
        self.run()
1009

    
1010

    
1011
@command(pithos_cmds)
1012
class file_upload(_file_container_command, _optional_output_cmd):
1013
    """Upload a file"""
1014

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

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

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

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

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

    
1194

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

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

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

    
1227
    def main(self, container___path):
1228
        super(self.__class__, self)._run(
1229
            container___path, path_is_optional=False)
1230
        self._run()
1231

    
1232

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

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

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

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

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

    
1422
    def main(self, container___path, local_path=None):
1423
        super(self.__class__, self)._run(container___path)
1424
        self._run(local_path=local_path)
1425

    
1426

    
1427
@command(pithos_cmds)
1428
class file_hashmap(_file_container_command, _optional_json):
1429
    """Get the hash-map of an object"""
1430

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

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

    
1456
    def main(self, container___path):
1457
        super(self.__class__, self)._run(
1458
            container___path,
1459
            path_is_optional=False)
1460
        self._run()
1461

    
1462

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

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

    
1490
    def __init__(self, arguments={}, auth_base=None, cloud=None):
1491
        super(self.__class__, self).__init__(arguments,  auth_base, cloud)
1492
        self['delimiter'] = DelimiterArgument(
1493
            self,
1494
            parsed_name='--delimiter',
1495
            help='delete objects prefixed with <object><delimiter>')
1496

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

    
1521
    def main(self, container____path__=None):
1522
        super(self.__class__, self)._run(container____path__)
1523
        self._run()
1524

    
1525

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

    
1537
    arguments = dict(
1538
        yes=FlagArgument('Do not prompt for permission', '--yes'),
1539
        force=FlagArgument('purge even if not empty', ('-F', '--force'))
1540
    )
1541

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

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

    
1570

    
1571
@command(pithos_cmds)
1572
class file_publish(_file_container_command):
1573
    """Publish the object and print the public url"""
1574

    
1575
    @errors.generic.all
1576
    @errors.pithos.connection
1577
    @errors.pithos.container
1578
    @errors.pithos.object_path
1579
    def _run(self):
1580
        print self.client.publish_object(self.path)
1581

    
1582
    def main(self, container___path):
1583
        super(self.__class__, self)._run(
1584
            container___path, path_is_optional=False)
1585
        self._run()
1586

    
1587

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

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

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

    
1604

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

    
1613

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

    
1625

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

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

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

    
1643

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

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

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

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

    
1684

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

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

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

    
1703

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

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

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

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

    
1736

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

    
1743

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

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

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

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

    
1797

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

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

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

    
1819

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

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

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

    
1845

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

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

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

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

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

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

    
1870

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

    
1875

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

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

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

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

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

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

    
1902

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

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

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

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

    
1951

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

    
1956

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

    
1961
    @errors.generic.all
1962
    @errors.pithos.connection
1963
    @errors.pithos.container
1964
    def _run(self):
1965
        self._print(
1966
            self.client.get_container_versioning(self.container), print_dict)
1967

    
1968
    def main(self, container):
1969
        super(self.__class__, self)._run()
1970
        self.container = container
1971
        self._run()
1972

    
1973

    
1974
@command(pithos_cmds)
1975
class file_versioning_set(_file_account_command, _optional_output_cmd):
1976
    """Set versioning mode (auto, none) for account or container"""
1977

    
1978
    def _check_versioning(self, versioning):
1979
        if versioning and versioning.lower() in ('auto', 'none'):
1980
            return versioning.lower()
1981
        raiseCLIError('Invalid versioning %s' % versioning, details=[
1982
            'Versioning can be auto or none'])
1983

    
1984
    @errors.generic.all
1985
    @errors.pithos.connection
1986
    @errors.pithos.container
1987
    def _run(self, versioning):
1988
        self.client.container = self.container
1989
        r = self.client.set_container_versioning(versioning)
1990
        self._optional_output(r)
1991

    
1992
    def main(self, versioning, container):
1993
        super(self.__class__, self)._run()
1994
        self._run(self._check_versioning(versioning))
1995

    
1996

    
1997
@command(pithos_cmds)
1998
class file_group(_pithos_init):
1999
    """Manage access groups and group members"""
2000

    
2001

    
2002
@command(pithos_cmds)
2003
class file_group_list(_file_account_command, _optional_json):
2004
    """list all groups and group members"""
2005

    
2006
    @errors.generic.all
2007
    @errors.pithos.connection
2008
    def _run(self):
2009
        self._print(self.client.get_account_group(), pretty_dict, delim='-')
2010

    
2011
    def main(self):
2012
        super(self.__class__, self)._run()
2013
        self._run()
2014

    
2015

    
2016
@command(pithos_cmds)
2017
class file_group_set(_file_account_command, _optional_output_cmd):
2018
    """Set a user group"""
2019

    
2020
    @errors.generic.all
2021
    @errors.pithos.connection
2022
    def _run(self, groupname, *users):
2023
        self._optional_output(self.client.set_account_group(groupname, users))
2024

    
2025
    def main(self, groupname, *users):
2026
        super(self.__class__, self)._run()
2027
        if users:
2028
            self._run(groupname, *users)
2029
        else:
2030
            raiseCLIError('No users to add in group %s' % groupname)
2031

    
2032

    
2033
@command(pithos_cmds)
2034
class file_group_delete(_file_account_command, _optional_output_cmd):
2035
    """Delete a user group"""
2036

    
2037
    @errors.generic.all
2038
    @errors.pithos.connection
2039
    def _run(self, groupname):
2040
        self._optional_output(self.client.del_account_group(groupname))
2041

    
2042
    def main(self, groupname):
2043
        super(self.__class__, self)._run()
2044
        self._run(groupname)
2045

    
2046

    
2047
@command(pithos_cmds)
2048
class file_sharers(_file_account_command, _optional_json):
2049
    """List the accounts that share objects with current user"""
2050

    
2051
    arguments = dict(
2052
        detail=FlagArgument('show detailed output', ('-l', '--details')),
2053
        marker=ValueArgument('show output greater then marker', '--marker')
2054
    )
2055

    
2056
    @errors.generic.all
2057
    @errors.pithos.connection
2058
    def _run(self):
2059
        accounts = self.client.get_sharing_accounts(marker=self['marker'])
2060
        uuids = [acc['name'] for acc in accounts]
2061
        try:
2062
            astakos_responce = self.auth_base.post_user_catalogs(uuids)
2063
            usernames = astakos_responce.json
2064
            r = usernames['uuid_catalog']
2065
        except Exception as e:
2066
            print 'WARNING: failed to call user_catalogs, %s' % e
2067
            r = dict(sharer_uuid=uuids)
2068
            usernames = accounts
2069
        if self['json_output'] or self['detail']:
2070
            self._print(usernames)
2071
        else:
2072
            self._print(r, print_dict)
2073

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

    
2078

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

    
2084

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

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

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