Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (78.3 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, page_hold, bold, ask_user,
43
    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 (
50
    _optional_output_cmd, _optional_json, _name_filter)
51
from kamaki.clients.pithos import PithosClient, ClientError
52
from kamaki.clients.astakos import AstakosClient
53

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

    
57

    
58
# Argument functionality
59

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

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

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

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

    
80

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

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

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

    
122

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

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

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

    
153

    
154
# Command specs
155

    
156

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

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

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

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

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

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

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

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

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

    
218

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

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

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

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

    
239

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

    
243
    container = None
244
    path = None
245

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

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

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

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

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

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

    
328

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

    
339
    arguments = dict(
340
        detail=FlagArgument('detailed output', ('-l', '--list')),
341
        limit=IntArgument('limit number of listed items', ('-n', '--number')),
342
        marker=ValueArgument('output greater that marker', '--marker'),
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_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_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
            files = self._filter_by_name(r.json)
443
            self._print(files, self.print_containers)
444
        else:
445
            prefix = (self.path and not self['name']) or self['name_pref']
446
            r = self.client.container_get(
447
                limit=False if self['more'] else self['limit'],
448
                marker=self['marker'],
449
                prefix=prefix,
450
                delimiter=self['delimiter'],
451
                path=self['path'],
452
                if_modified_since=self['if_modified_since'],
453
                if_unmodified_since=self['if_unmodified_since'],
454
                until=self['until'],
455
                meta=self['meta'],
456
                show_only_shared=self['shared'])
457
            files = self._filter_by_name(r.json)
458
            self._print(files, self.print_objects)
459

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

    
464

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

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

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

    
487

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

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

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

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

    
514

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

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

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

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

    
545

    
546
class _source_destination_command(_file_container_command):
547

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

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

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

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

    
596
    def _get_all(self, prefix):
597
        return self.client.container_get(prefix=prefix).json
598

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

602
        :param src_path: (str) source path
603

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

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

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

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

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

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

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

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

    
687

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

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

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

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

    
773

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

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

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

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

    
857

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

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

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

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

    
894

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

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

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

    
911

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

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

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

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

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

    
962

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

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

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

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

    
1012

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

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

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

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

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

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

    
1196

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

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

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

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

    
1234

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

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

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

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

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

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

    
1428

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

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

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

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

    
1464

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

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

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

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

    
1525
    def main(self, container____path__=None):
1526
        super(self.__class__, self)._run(container____path__)
1527
        self._run()
1528

    
1529

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

    
1541
    arguments = dict(
1542
        yes=FlagArgument('Do not prompt for permission', '--yes'),
1543
        force=FlagArgument('purge even if not empty', ('-F', '--force'))
1544
    )
1545

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

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

    
1574

    
1575
@command(pithos_cmds)
1576
class file_publish(_file_container_command):
1577
    """Publish the object and print the public url"""
1578

    
1579
    @errors.generic.all
1580
    @errors.pithos.connection
1581
    @errors.pithos.container
1582
    @errors.pithos.object_path
1583
    def _run(self):
1584
        print self.client.publish_object(self.path)
1585

    
1586
    def main(self, container___path):
1587
        super(self.__class__, self)._run(
1588
            container___path, path_is_optional=False)
1589
        self._run()
1590

    
1591

    
1592
@command(pithos_cmds)
1593
class file_unpublish(_file_container_command, _optional_output_cmd):
1594
    """Unpublish an object"""
1595

    
1596
    @errors.generic.all
1597
    @errors.pithos.connection
1598
    @errors.pithos.container
1599
    @errors.pithos.object_path
1600
    def _run(self):
1601
            self._optional_output(self.client.unpublish_object(self.path))
1602

    
1603
    def main(self, container___path):
1604
        super(self.__class__, self)._run(
1605
            container___path, path_is_optional=False)
1606
        self._run()
1607

    
1608

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

    
1617

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

    
1629

    
1630
@command(pithos_cmds)
1631
class file_permissions_get(_file_container_command, _optional_json):
1632
    """Get read and write permissions of an object"""
1633

    
1634
    @errors.generic.all
1635
    @errors.pithos.connection
1636
    @errors.pithos.container
1637
    @errors.pithos.object_path
1638
    def _run(self):
1639
        self._print(
1640
            self.client.get_object_sharing(self.path), print_permissions)
1641

    
1642
    def main(self, container___path):
1643
        super(self.__class__, self)._run(
1644
            container___path, path_is_optional=False)
1645
        self._run()
1646

    
1647

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

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

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

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

    
1688

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

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

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

    
1707

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

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

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

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

    
1740

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

    
1747

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

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

    
1760
    @errors.generic.all
1761
    @errors.pithos.connection
1762
    @errors.pithos.container
1763
    @errors.pithos.object_path
1764
    def _run(self):
1765
        until = self['until']
1766
        r = None
1767
        if self.container is None:
1768
            r = self.client.get_account_info(until=until)
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'] = cmeta
1778
                if ometa:
1779
                    r['object-meta'] = 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
        if r:
1790
            self._print(r, print_dict)
1791

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

    
1796

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

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

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

    
1818

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

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

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

    
1844

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

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

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

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

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

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

    
1869

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

    
1874

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

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

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

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

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

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

    
1901

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

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

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

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

    
1950

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

    
1955

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

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

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

    
1972

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

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

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

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

    
1995

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

    
2000

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

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

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

    
2014

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

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

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

    
2031

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

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

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

    
2045

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

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

    
2055
    @errors.generic.all
2056
    @errors.pithos.connection
2057
    def _run(self):
2058
        accounts = self.client.get_sharing_accounts(marker=self['marker'])
2059
        if not self['json_output']:
2060
            usernames = self._uuids2usernames(
2061
                [acc['name'] for acc in accounts])
2062
            for item in accounts:
2063
                uuid = item['name']
2064
                item['id'], item['name'] = uuid, usernames[uuid]
2065
                if not self['detail']:
2066
                    item.pop('last_modified')
2067
        self._print(accounts)
2068

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

    
2073

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

    
2079

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

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

    
2098
    def main(self, container___path):
2099
        super(file_versions, self)._run(
2100
            container___path,
2101
            path_is_optional=False)
2102
        self._run()