Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / pithos.py @ 7806f19d

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, 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, newvalue):
135
        if newvalue is None:
136
            self._value = self.default
137
            return
138
        start, sep, end = newvalue.partition('-')
139
        if sep:
140
            if start:
141
                self._value = '%s-%s' % (int(start), int(end))
142
            else:
143
                self._value = '-%s' % int(end)
144
        else:
145
            self._value = '%s' % int(start)
146

    
147

    
148
# Command specs
149

    
150

    
151
class _pithos_init(_command_init):
152
    """Initialize a pithos+ kamaki client"""
153

    
154
    @staticmethod
155
    def _is_dir(remote_dict):
156
        return 'application/directory' == remote_dict.get(
157
            'content_type', remote_dict.get('content-type', ''))
158

    
159
    @DontRaiseKeyError
160
    def _custom_container(self):
161
        return self.config.get_cloud(self.cloud, 'pithos_container')
162

    
163
    @DontRaiseKeyError
164
    def _custom_uuid(self):
165
        return self.config.get_cloud(self.cloud, 'pithos_uuid')
166

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

    
181
    @errors.generic.all
182
    @addLogSettings
183
    def _run(self):
184
        self.base_url = None
185
        if getattr(self, 'cloud', None):
186
            self.base_url = self._custom_url('pithos')
187
        else:
188
            self.cloud = 'default'
189
        self.token = self._custom_token('pithos')
190
        self.container = self._custom_container()
191

    
192
        if getattr(self, 'auth_base', False):
193
            self.token = self.token or self.auth_base.token
194
            if not self.base_url:
195
                pithos_endpoints = self.auth_base.get_service_endpoints(
196
                    self._custom_type('pithos') or 'object-store',
197
                    self._custom_version('pithos') or '')
198
                self.base_url = pithos_endpoints['publicURL']
199
        elif not self.base_url:
200
            raise CLIBaseUrlError(service='pithos')
201

    
202
        self._set_account()
203
        self.client = PithosClient(
204
            base_url=self.base_url,
205
            token=self.token,
206
            account=self.account,
207
            container=self.container)
208

    
209
    def main(self):
210
        self._run()
211

    
212

    
213
class _file_account_command(_pithos_init):
214
    """Base class for account level storage commands"""
215

    
216
    def __init__(self, arguments={}, auth_base=None, cloud=None):
217
        super(_file_account_command, self).__init__(
218
            arguments, auth_base, cloud)
219
        self['account'] = ValueArgument(
220
            'Set user account (not permanent)', ('-A', '--account'))
221

    
222
    def _run(self, custom_account=None):
223
        super(_file_account_command, self)._run()
224
        if custom_account:
225
            self.client.account = custom_account
226
        elif self['account']:
227
            self.client.account = self['account']
228

    
229
    @errors.generic.all
230
    def main(self):
231
        self._run()
232

    
233

    
234
class _file_container_command(_file_account_command):
235
    """Base class for container level storage commands"""
236

    
237
    container = None
238
    path = None
239

    
240
    def __init__(self, arguments={}, auth_base=None, cloud=None):
241
        super(_file_container_command, self).__init__(
242
            arguments, auth_base, cloud)
243
        self['container'] = ValueArgument(
244
            'Set container to work with (temporary)', ('-C', '--container'))
245

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

    
266
        user_cont, sep, userpath = container_with_path.partition(':')
267

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

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

    
319
    def main(self, container_with_path=None, path_is_optional=True):
320
        self._run(container_with_path, path_is_optional)
321

    
322

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

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

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

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

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

    
453
    def main(self, container____path__=None):
454
        super(self.__class__, self)._run(container____path__)
455
        self._run()
456

    
457

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

    
468
    @errors.generic.all
469
    @errors.pithos.connection
470
    @errors.pithos.container
471
    def _run(self):
472
        self._optional_output(self.client.create_directory(self.path))
473

    
474
    def main(self, container___directory):
475
        super(self.__class__, self)._run(
476
            container___directory,
477
            path_is_optional=False)
478
        self._run()
479

    
480

    
481
@command(pithos_cmds)
482
class file_touch(_file_container_command, _optional_output_cmd):
483
    """Create an empty object (file)
484
    If object exists, this command will reset it to 0 length
485
    """
486

    
487
    arguments = dict(
488
        content_type=ValueArgument(
489
            'Set content type (default: application/octet-stream)',
490
            '--content-type',
491
            default='application/octet-stream')
492
    )
493

    
494
    @errors.generic.all
495
    @errors.pithos.connection
496
    @errors.pithos.container
497
    def _run(self):
498
        self._optional_output(
499
            self.client.create_object(self.path, self['content_type']))
500

    
501
    def main(self, container___path):
502
        super(file_touch, self)._run(
503
            container___path,
504
            path_is_optional=False)
505
        self._run()
506

    
507

    
508
@command(pithos_cmds)
509
class file_create(_file_container_command, _optional_output_cmd):
510
    """Create a container"""
511

    
512
    arguments = dict(
513
        versioning=ValueArgument(
514
            'set container versioning (auto/none)', '--versioning'),
515
        limit=IntArgument('set default container limit', '--limit'),
516
        meta=KeyValueArgument(
517
            'set container metadata (can be repeated)', '--meta')
518
    )
519

    
520
    @errors.generic.all
521
    @errors.pithos.connection
522
    @errors.pithos.container
523
    def _run(self, container):
524
        self._optional_output(self.client.create_container(
525
            container=container,
526
            sizelimit=self['limit'],
527
            versioning=self['versioning'],
528
            metadata=self['meta']))
529

    
530
    def main(self, container=None):
531
        super(self.__class__, self)._run(container)
532
        if container and self.container != container:
533
            raiseCLIError('Invalid container name %s' % container, details=[
534
                'Did you mean "%s" ?' % self.container,
535
                'Use --container for names containing :'])
536
        self._run(container)
537

    
538

    
539
class _source_destination_command(_file_container_command):
540

    
541
    arguments = dict(
542
        destination_account=ValueArgument('', ('-a', '--dst-account')),
543
        recursive=FlagArgument('', ('-R', '--recursive')),
544
        prefix=FlagArgument('', '--with-prefix', default=''),
545
        suffix=ValueArgument('', '--with-suffix', default=''),
546
        add_prefix=ValueArgument('', '--add-prefix', default=''),
547
        add_suffix=ValueArgument('', '--add-suffix', default=''),
548
        prefix_replace=ValueArgument('', '--prefix-to-replace', default=''),
549
        suffix_replace=ValueArgument('', '--suffix-to-replace', default=''),
550
    )
551

    
552
    def __init__(self, arguments={}, auth_base=None, cloud=None):
553
        self.arguments.update(arguments)
554
        super(_source_destination_command, self).__init__(
555
            self.arguments, auth_base, cloud)
556

    
557
    def _run(self, source_container___path, path_is_optional=False):
558
        super(_source_destination_command, self)._run(
559
            source_container___path,
560
            path_is_optional)
561
        self.dst_client = PithosClient(
562
            base_url=self.client.base_url,
563
            token=self.client.token,
564
            account=self['destination_account'] or self.client.account)
565

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

    
589
    def _get_all(self, prefix):
590
        return self.client.container_get(prefix=prefix).json
591

    
592
    def _get_src_objects(self, src_path, source_version=None):
593
        """Get a list of the source objects to be called
594

595
        :param src_path: (str) source path
596

597
        :returns: (method, params) a method that returns a list when called
598
        or (object) if it is a single object
599
        """
600
        if src_path and src_path[-1] == '/':
601
            src_path = src_path[:-1]
602

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

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

    
628
    def src_dst_pairs(self, dst_path, source_version=None):
629
        src_iter = self._get_src_objects(self.path, source_version)
630
        src_N = isinstance(src_iter, tuple)
631
        add_prefix = self['add_prefix'].strip('/')
632

    
633
        if dst_path and dst_path.endswith('/'):
634
            dst_path = dst_path[:-1]
635

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

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

    
673
    def _get_new_object(self, obj, add_prefix):
674
        if self['prefix_replace'] and obj.startswith(self['prefix_replace']):
675
            obj = obj[len(self['prefix_replace']):]
676
        if self['suffix_replace'] and obj.endswith(self['suffix_replace']):
677
            obj = obj[:-len(self['suffix_replace'])]
678
        return add_prefix + obj + self['add_suffix']
679

    
680

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

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

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

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

    
766

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

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

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

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

    
850

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

    
859
    arguments = dict(
860
        progress_bar=ProgressBarArgument(
861
            'do not show progress bar',
862
            ('-N', '--no-progress-bar'),
863
            default=False)
864
    )
865

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

    
882
    def main(self, local_path, container___path):
883
        super(self.__class__, self)._run(
884
            container___path, path_is_optional=False)
885
        self._run(local_path)
886

    
887

    
888
@command(pithos_cmds)
889
class file_truncate(_file_container_command, _optional_output_cmd):
890
    """Truncate remote file up to a size (default is 0)"""
891

    
892
    @errors.generic.all
893
    @errors.pithos.connection
894
    @errors.pithos.container
895
    @errors.pithos.object_path
896
    @errors.pithos.object_size
897
    def _run(self, size=0):
898
        self._optional_output(self.client.truncate_object(self.path, size))
899

    
900
    def main(self, container___path, size=0):
901
        super(self.__class__, self)._run(container___path)
902
        self._run(size=size)
903

    
904

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

    
915
    arguments = dict(
916
        progress_bar=ProgressBarArgument(
917
            'do not show progress bar',
918
            ('-N', '--no-progress-bar'),
919
            default=False)
920
    )
921

    
922
    def _open_file(self, local_path, start):
923
        f = open(path.abspath(local_path), 'rb')
924
        f.seek(0, 2)
925
        f_size = f.tell()
926
        f.seek(start, 0)
927
        return (f, f_size)
928

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

    
949
    def main(self, local_path, container___path, start, end):
950
        super(self.__class__, self)._run(
951
            container___path, path_is_optional=None)
952
        self.path = self.path or path.basename(local_path)
953
        self._run(local_path=local_path, start=start, end=end)
954

    
955

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

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

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

    
1000
    def main(self, container___path):
1001
        super(self.__class__, self)._run(
1002
            container___path, path_is_optional=False)
1003
        self.run()
1004

    
1005

    
1006
@command(pithos_cmds)
1007
class file_upload(_file_container_command, _optional_output_cmd):
1008
    """Upload a file"""
1009

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

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

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

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

    
1184
    def main(self, local_path, container____path__=None):
1185
        super(self.__class__, self)._run(container____path__)
1186
        remote_path = self.path or path.basename(path.abspath(local_path))
1187
        self._run(local_path=local_path, remote_path=remote_path)
1188

    
1189

    
1190
@command(pithos_cmds)
1191
class file_cat(_file_container_command):
1192
    """Print remote file contents to console"""
1193

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

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

    
1222
    def main(self, container___path):
1223
        super(self.__class__, self)._run(
1224
            container___path, path_is_optional=False)
1225
        self._run()
1226

    
1227

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

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

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

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

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

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

    
1421

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

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

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

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

    
1457

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

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

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

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

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

    
1520

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

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

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

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

    
1565

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

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

    
1577
    def main(self, container___path):
1578
        super(self.__class__, self)._run(
1579
            container___path, path_is_optional=False)
1580
        self._run()
1581

    
1582

    
1583
@command(pithos_cmds)
1584
class file_unpublish(_file_container_command, _optional_output_cmd):
1585
    """Unpublish an object"""
1586

    
1587
    @errors.generic.all
1588
    @errors.pithos.connection
1589
    @errors.pithos.container
1590
    @errors.pithos.object_path
1591
    def _run(self):
1592
            self._optional_output(self.client.unpublish_object(self.path))
1593

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

    
1599

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

    
1608

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

    
1620

    
1621
@command(pithos_cmds)
1622
class file_permissions_get(_file_container_command, _optional_json):
1623
    """Get read and write permissions of an object"""
1624

    
1625
    @errors.generic.all
1626
    @errors.pithos.connection
1627
    @errors.pithos.container
1628
    @errors.pithos.object_path
1629
    def _run(self):
1630
        self._print(
1631
            self.client.get_object_sharing(self.path), print_permissions)
1632

    
1633
    def main(self, container___path):
1634
        super(self.__class__, self)._run(
1635
            container___path, path_is_optional=False)
1636
        self._run()
1637

    
1638

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

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

    
1665
    @errors.generic.all
1666
    @errors.pithos.connection
1667
    @errors.pithos.container
1668
    @errors.pithos.object_path
1669
    def _run(self, read, write):
1670
        self._optional_output(self.client.set_object_sharing(
1671
            self.path, read_permission=read, write_permission=write))
1672

    
1673
    def main(self, container___path, *permissions):
1674
        super(self.__class__, self)._run(
1675
            container___path, path_is_optional=False)
1676
        read, write = self.format_permission_dict(permissions)
1677
        self._run(read, write)
1678

    
1679

    
1680
@command(pithos_cmds)
1681
class file_permissions_delete(_file_container_command, _optional_output_cmd):
1682
    """Delete all permissions set on object
1683
    To modify permissions, use /file permissions set
1684
    """
1685

    
1686
    @errors.generic.all
1687
    @errors.pithos.connection
1688
    @errors.pithos.container
1689
    @errors.pithos.object_path
1690
    def _run(self):
1691
        self._optional_output(self.client.del_object_sharing(self.path))
1692

    
1693
    def main(self, container___path):
1694
        super(self.__class__, self)._run(
1695
            container___path, path_is_optional=False)
1696
        self._run()
1697

    
1698

    
1699
@command(pithos_cmds)
1700
class file_info(_file_container_command, _optional_json):
1701
    """Get detailed information for user account, containers or objects
1702
    to get account info:    /file info
1703
    to get container info:  /file info <container>
1704
    to get object info:     /file info <container>:<path>
1705
    """
1706

    
1707
    arguments = dict(
1708
        object_version=ValueArgument(
1709
            'show specific version \ (applies only for objects)',
1710
            ('-O', '--object-version'))
1711
    )
1712

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

    
1727
    def main(self, container____path__=None):
1728
        super(self.__class__, self)._run(container____path__)
1729
        self._run()
1730

    
1731

    
1732
@command(pithos_cmds)
1733
class file_metadata(_pithos_init):
1734
    """Metadata are attached on objects. They are formed as key:value pairs.
1735
    They can have arbitary values.
1736
    """
1737

    
1738

    
1739
@command(pithos_cmds)
1740
class file_metadata_get(_file_container_command, _optional_json):
1741
    """Get metadata for account, containers or objects"""
1742

    
1743
    arguments = dict(
1744
        detail=FlagArgument('show detailed output', ('-l', '--details')),
1745
        until=DateArgument('show metadata until then', '--until'),
1746
        object_version=ValueArgument(
1747
            'show specific version (applies only for objects)',
1748
            ('-O', '--object-version'))
1749
    )
1750

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

    
1788
    def main(self, container____path__=None):
1789
        super(self.__class__, self)._run(container____path__)
1790
        self._run()
1791

    
1792

    
1793
@command(pithos_cmds)
1794
class file_metadata_set(_file_container_command, _optional_output_cmd):
1795
    """Set a piece of metadata for account, container or object"""
1796

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

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

    
1814

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

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

    
1836
    def main(self, metakey, container____path__=None):
1837
        super(self.__class__, self)._run(container____path__)
1838
        self._run(metakey)
1839

    
1840

    
1841
@command(pithos_cmds)
1842
class file_quota(_file_account_command, _optional_json):
1843
    """Get account quota"""
1844

    
1845
    arguments = dict(
1846
        in_bytes=FlagArgument('Show result in bytes', ('-b', '--bytes'))
1847
    )
1848

    
1849
    @errors.generic.all
1850
    @errors.pithos.connection
1851
    def _run(self):
1852

    
1853
        def pretty_print(output):
1854
            if not self['in_bytes']:
1855
                for k in output:
1856
                    output[k] = format_size(output[k])
1857
            pretty_dict(output, '-')
1858

    
1859
        self._print(self.client.get_account_quota(), pretty_print)
1860

    
1861
    def main(self, custom_uuid=None):
1862
        super(self.__class__, self)._run(custom_account=custom_uuid)
1863
        self._run()
1864

    
1865

    
1866
@command(pithos_cmds)
1867
class file_containerlimit(_pithos_init):
1868
    """Container size limit commands"""
1869

    
1870

    
1871
@command(pithos_cmds)
1872
class file_containerlimit_get(_file_container_command, _optional_json):
1873
    """Get container size limit"""
1874

    
1875
    arguments = dict(
1876
        in_bytes=FlagArgument('Show result in bytes', ('-b', '--bytes'))
1877
    )
1878

    
1879
    @errors.generic.all
1880
    @errors.pithos.container
1881
    def _run(self):
1882

    
1883
        def pretty_print(output):
1884
            if not self['in_bytes']:
1885
                for k, v in output.items():
1886
                    output[k] = 'unlimited' if '0' == v else format_size(v)
1887
            pretty_dict(output, '-')
1888

    
1889
        self._print(
1890
            self.client.get_container_limit(self.container), pretty_print)
1891

    
1892
    def main(self, container=None):
1893
        super(self.__class__, self)._run()
1894
        self.container = container
1895
        self._run()
1896

    
1897

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

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

    
1932
    @errors.generic.all
1933
    @errors.pithos.connection
1934
    @errors.pithos.container
1935
    def _run(self, limit):
1936
        if self.container:
1937
            self.client.container = self.container
1938
        self._optional_output(self.client.set_container_limit(limit))
1939

    
1940
    def main(self, limit, container=None):
1941
        super(self.__class__, self)._run()
1942
        limit = self._calculate_limit(limit)
1943
        self.container = container
1944
        self._run(limit)
1945

    
1946

    
1947
@command(pithos_cmds)
1948
class file_versioning(_pithos_init):
1949
    """Manage the versioning scheme of current pithos user account"""
1950

    
1951

    
1952
@command(pithos_cmds)
1953
class file_versioning_get(_file_account_command, _optional_json):
1954
    """Get  versioning for account or container"""
1955

    
1956
    @errors.generic.all
1957
    @errors.pithos.connection
1958
    @errors.pithos.container
1959
    def _run(self):
1960
        self._print(
1961
            self.client.get_container_versioning(self.container), print_dict)
1962

    
1963
    def main(self, container):
1964
        super(self.__class__, self)._run()
1965
        self.container = container
1966
        self._run()
1967

    
1968

    
1969
@command(pithos_cmds)
1970
class file_versioning_set(_file_account_command, _optional_output_cmd):
1971
    """Set versioning mode (auto, none) for account or container"""
1972

    
1973
    def _check_versioning(self, versioning):
1974
        if versioning and versioning.lower() in ('auto', 'none'):
1975
            return versioning.lower()
1976
        raiseCLIError('Invalid versioning %s' % versioning, details=[
1977
            'Versioning can be auto or none'])
1978

    
1979
    @errors.generic.all
1980
    @errors.pithos.connection
1981
    @errors.pithos.container
1982
    def _run(self, versioning):
1983
        self.client.container = self.container
1984
        r = self.client.set_container_versioning(versioning)
1985
        self._optional_output(r)
1986

    
1987
    def main(self, versioning, container):
1988
        super(self.__class__, self)._run()
1989
        self._run(self._check_versioning(versioning))
1990

    
1991

    
1992
@command(pithos_cmds)
1993
class file_group(_pithos_init):
1994
    """Manage access groups and group members"""
1995

    
1996

    
1997
@command(pithos_cmds)
1998
class file_group_list(_file_account_command, _optional_json):
1999
    """list all groups and group members"""
2000

    
2001
    @errors.generic.all
2002
    @errors.pithos.connection
2003
    def _run(self):
2004
        self._print(self.client.get_account_group(), pretty_dict, delim='-')
2005

    
2006
    def main(self):
2007
        super(self.__class__, self)._run()
2008
        self._run()
2009

    
2010

    
2011
@command(pithos_cmds)
2012
class file_group_set(_file_account_command, _optional_output_cmd):
2013
    """Set a user group"""
2014

    
2015
    @errors.generic.all
2016
    @errors.pithos.connection
2017
    def _run(self, groupname, *users):
2018
        self._optional_output(self.client.set_account_group(groupname, users))
2019

    
2020
    def main(self, groupname, *users):
2021
        super(self.__class__, self)._run()
2022
        if users:
2023
            self._run(groupname, *users)
2024
        else:
2025
            raiseCLIError('No users to add in group %s' % groupname)
2026

    
2027

    
2028
@command(pithos_cmds)
2029
class file_group_delete(_file_account_command, _optional_output_cmd):
2030
    """Delete a user group"""
2031

    
2032
    @errors.generic.all
2033
    @errors.pithos.connection
2034
    def _run(self, groupname):
2035
        self._optional_output(self.client.del_account_group(groupname))
2036

    
2037
    def main(self, groupname):
2038
        super(self.__class__, self)._run()
2039
        self._run(groupname)
2040

    
2041

    
2042
@command(pithos_cmds)
2043
class file_sharers(_file_account_command, _optional_json):
2044
    """List the accounts that share objects with current user"""
2045

    
2046
    arguments = dict(
2047
        detail=FlagArgument('show detailed output', ('-l', '--details')),
2048
        marker=ValueArgument('show output greater then marker', '--marker')
2049
    )
2050

    
2051
    @errors.generic.all
2052
    @errors.pithos.connection
2053
    def _run(self):
2054
        accounts = self.client.get_sharing_accounts(marker=self['marker'])
2055
        uuids = [acc['name'] for acc in accounts]
2056
        try:
2057
            astakos_responce = self.auth_base.post_user_catalogs(uuids)
2058
            usernames = astakos_responce.json
2059
            r = usernames['uuid_catalog']
2060
        except Exception as e:
2061
            print 'WARNING: failed to call user_catalogs, %s' % e
2062
            r = dict(sharer_uuid=uuids)
2063
            usernames = accounts
2064
        if self['json_output'] or self['detail']:
2065
            self._print(usernames)
2066
        else:
2067
            self._print(r, print_dict)
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()