Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (77.8 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, end) = newvalue.split('-')
139
        (start, end) = (int(start), int(end))
140
        self._value = '%s-%s' % (start, end)
141

    
142

    
143
# Command specs
144

    
145

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

    
149
    @staticmethod
150
    def _is_dir(remote_dict):
151
        return 'application/directory' == remote_dict.get(
152
            'content_type', remote_dict.get('content-type', ''))
153

    
154
    @DontRaiseKeyError
155
    def _custom_container(self):
156
        return self.config.get_cloud(self.cloud, 'pithos_container')
157

    
158
    @DontRaiseKeyError
159
    def _custom_uuid(self):
160
        return self.config.get_cloud(self.cloud, 'pithos_uuid')
161

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

    
176
    @errors.generic.all
177
    @addLogSettings
178
    def _run(self):
179
        self.base_url = None
180
        if getattr(self, 'cloud', None):
181
            self.base_url = self._custom_url('pithos')
182
        else:
183
            self.cloud = 'default'
184
        self.token = self._custom_token('pithos')
185
        self.container = self._custom_container()
186

    
187
        if getattr(self, 'auth_base', False):
188
            self.token = self.token or self.auth_base.token
189
            if not self.base_url:
190
                pithos_endpoints = self.auth_base.get_service_endpoints(
191
                    self._custom_type('pithos') or 'object-store',
192
                    self._custom_version('pithos') or '')
193
                self.base_url = pithos_endpoints['publicURL']
194
        elif not self.base_url:
195
            raise CLIBaseUrlError(service='pithos')
196

    
197
        self._set_account()
198
        self.client = PithosClient(
199
            base_url=self.base_url,
200
            token=self.token,
201
            account=self.account,
202
            container=self.container)
203

    
204
    def main(self):
205
        self._run()
206

    
207

    
208
class _file_account_command(_pithos_init):
209
    """Base class for account level storage commands"""
210

    
211
    def __init__(self, arguments={}, auth_base=None, cloud=None):
212
        super(_file_account_command, self).__init__(
213
            arguments, auth_base, cloud)
214
        self['account'] = ValueArgument(
215
            'Set user account (not permanent)', ('-A', '--account'))
216

    
217
    def _run(self, custom_account=None):
218
        super(_file_account_command, self)._run()
219
        if custom_account:
220
            self.client.account = custom_account
221
        elif self['account']:
222
            self.client.account = self['account']
223

    
224
    @errors.generic.all
225
    def main(self):
226
        self._run()
227

    
228

    
229
class _file_container_command(_file_account_command):
230
    """Base class for container level storage commands"""
231

    
232
    container = None
233
    path = None
234

    
235
    def __init__(self, arguments={}, auth_base=None, cloud=None):
236
        super(_file_container_command, self).__init__(
237
            arguments, auth_base, cloud)
238
        self['container'] = ValueArgument(
239
            'Set container to work with (temporary)', ('-C', '--container'))
240

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

    
261
        user_cont, sep, userpath = container_with_path.partition(':')
262

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

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

    
314
    def main(self, container_with_path=None, path_is_optional=True):
315
        self._run(container_with_path, path_is_optional)
316

    
317

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

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

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

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

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

    
448
    def main(self, container____path__=None):
449
        super(self.__class__, self)._run(container____path__)
450
        self._run()
451

    
452

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

    
463
    @errors.generic.all
464
    @errors.pithos.connection
465
    @errors.pithos.container
466
    def _run(self):
467
        self._optional_output(self.client.create_directory(self.path))
468

    
469
    def main(self, container___directory):
470
        super(self.__class__, self)._run(
471
            container___directory,
472
            path_is_optional=False)
473
        self._run()
474

    
475

    
476
@command(pithos_cmds)
477
class file_touch(_file_container_command, _optional_output_cmd):
478
    """Create an empty object (file)
479
    If object exists, this command will reset it to 0 length
480
    """
481

    
482
    arguments = dict(
483
        content_type=ValueArgument(
484
            'Set content type (default: application/octet-stream)',
485
            '--content-type',
486
            default='application/octet-stream')
487
    )
488

    
489
    @errors.generic.all
490
    @errors.pithos.connection
491
    @errors.pithos.container
492
    def _run(self):
493
        self._optional_output(
494
            self.client.create_object(self.path, self['content_type']))
495

    
496
    def main(self, container___path):
497
        super(file_touch, self)._run(
498
            container___path,
499
            path_is_optional=False)
500
        self._run()
501

    
502

    
503
@command(pithos_cmds)
504
class file_create(_file_container_command, _optional_output_cmd):
505
    """Create a container"""
506

    
507
    arguments = dict(
508
        versioning=ValueArgument(
509
            'set container versioning (auto/none)', '--versioning'),
510
        limit=IntArgument('set default container limit', '--limit'),
511
        meta=KeyValueArgument(
512
            'set container metadata (can be repeated)', '--meta')
513
    )
514

    
515
    @errors.generic.all
516
    @errors.pithos.connection
517
    @errors.pithos.container
518
    def _run(self, container):
519
        self._optional_output(self.client.create_container(
520
            container=container,
521
            sizelimit=self['limit'],
522
            versioning=self['versioning'],
523
            metadata=self['meta']))
524

    
525
    def main(self, container=None):
526
        super(self.__class__, self)._run(container)
527
        if container and self.container != container:
528
            raiseCLIError('Invalid container name %s' % container, details=[
529
                'Did you mean "%s" ?' % self.container,
530
                'Use --container for names containing :'])
531
        self._run(container)
532

    
533

    
534
class _source_destination_command(_file_container_command):
535

    
536
    arguments = dict(
537
        destination_account=ValueArgument('', ('-a', '--dst-account')),
538
        recursive=FlagArgument('', ('-R', '--recursive')),
539
        prefix=FlagArgument('', '--with-prefix', default=''),
540
        suffix=ValueArgument('', '--with-suffix', default=''),
541
        add_prefix=ValueArgument('', '--add-prefix', default=''),
542
        add_suffix=ValueArgument('', '--add-suffix', default=''),
543
        prefix_replace=ValueArgument('', '--prefix-to-replace', default=''),
544
        suffix_replace=ValueArgument('', '--suffix-to-replace', default=''),
545
    )
546

    
547
    def __init__(self, arguments={}, auth_base=None, cloud=None):
548
        self.arguments.update(arguments)
549
        super(_source_destination_command, self).__init__(
550
            self.arguments, auth_base, cloud)
551

    
552
    def _run(self, source_container___path, path_is_optional=False):
553
        super(_source_destination_command, self)._run(
554
            source_container___path,
555
            path_is_optional)
556
        self.dst_client = PithosClient(
557
            base_url=self.client.base_url,
558
            token=self.client.token,
559
            account=self['destination_account'] or self.client.account)
560

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

    
584
    def _get_all(self, prefix):
585
        return self.client.container_get(prefix=prefix).json
586

    
587
    def _get_src_objects(self, src_path, source_version=None):
588
        """Get a list of the source objects to be called
589

590
        :param src_path: (str) source path
591

592
        :returns: (method, params) a method that returns a list when called
593
        or (object) if it is a single object
594
        """
595
        if src_path and src_path[-1] == '/':
596
            src_path = src_path[:-1]
597

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

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

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

    
628
        if dst_path and dst_path.endswith('/'):
629
            dst_path = dst_path[:-1]
630

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

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

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

    
675

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

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

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

    
750
    def main(
751
            self, source_container___path,
752
            destination_container___path=None):
753
        super(file_copy, self)._run(
754
            source_container___path,
755
            path_is_optional=False)
756
        (dst_cont, dst_path) = self._dest_container_path(
757
            destination_container___path)
758
        self.dst_client.container = dst_cont or self.container
759
        self._run(dst_path=dst_path or '')
760

    
761

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

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

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

    
832
    def main(
833
            self, source_container___path,
834
            destination_container___path=None):
835
        super(self.__class__, self)._run(
836
            source_container___path,
837
            path_is_optional=False)
838
        (dst_cont, dst_path) = self._dest_container_path(
839
            destination_container___path)
840
        (dst_cont, dst_path) = self._dest_container_path(
841
            destination_container___path)
842
        self.dst_client.container = dst_cont or self.container
843
        self._run(dst_path=dst_path or '')
844

    
845

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

    
854
    arguments = dict(
855
        progress_bar=ProgressBarArgument(
856
            'do not show progress bar',
857
            ('-N', '--no-progress-bar'),
858
            default=False)
859
    )
860

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

    
877
    def main(self, local_path, container___path):
878
        super(self.__class__, self)._run(
879
            container___path, path_is_optional=False)
880
        self._run(local_path)
881

    
882

    
883
@command(pithos_cmds)
884
class file_truncate(_file_container_command, _optional_output_cmd):
885
    """Truncate remote file up to a size (default is 0)"""
886

    
887
    @errors.generic.all
888
    @errors.pithos.connection
889
    @errors.pithos.container
890
    @errors.pithos.object_path
891
    @errors.pithos.object_size
892
    def _run(self, size=0):
893
        self._optional_output(self.client.truncate_object(self.path, size))
894

    
895
    def main(self, container___path, size=0):
896
        super(self.__class__, self)._run(container___path)
897
        self._run(size=size)
898

    
899

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

    
910
    arguments = dict(
911
        progress_bar=ProgressBarArgument(
912
            'do not show progress bar',
913
            ('-N', '--no-progress-bar'),
914
            default=False)
915
    )
916

    
917
    def _open_file(self, local_path, start):
918
        f = open(path.abspath(local_path), 'rb')
919
        f.seek(0, 2)
920
        f_size = f.tell()
921
        f.seek(start, 0)
922
        return (f, f_size)
923

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

    
944
    def main(self, local_path, container___path, start, end):
945
        super(self.__class__, self)._run(
946
            container___path, path_is_optional=None)
947
        self.path = self.path or path.basename(local_path)
948
        self._run(local_path=local_path, start=start, end=end)
949

    
950

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

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

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

    
995
    def main(self, container___path):
996
        super(self.__class__, self)._run(
997
            container___path, path_is_optional=False)
998
        self.run()
999

    
1000

    
1001
@command(pithos_cmds)
1002
class file_upload(_file_container_command, _optional_output_cmd):
1003
    """Upload a file"""
1004

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

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

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

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

    
1179
    def main(self, local_path, container____path__=None):
1180
        super(self.__class__, self)._run(container____path__)
1181
        remote_path = self.path or path.basename(path.abspath(local_path))
1182
        self._run(local_path=local_path, remote_path=remote_path)
1183

    
1184

    
1185
@command(pithos_cmds)
1186
class file_cat(_file_container_command):
1187
    """Print remote file contents to console"""
1188

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

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

    
1217
    def main(self, container___path):
1218
        super(self.__class__, self)._run(
1219
            container___path, path_is_optional=False)
1220
        self._run()
1221

    
1222

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

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

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

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

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

    
1412
    def main(self, container___path, local_path=None):
1413
        super(self.__class__, self)._run(container___path)
1414
        self._run(local_path=local_path)
1415

    
1416

    
1417
@command(pithos_cmds)
1418
class file_hashmap(_file_container_command, _optional_json):
1419
    """Get the hash-map of an object"""
1420

    
1421
    arguments = dict(
1422
        if_match=ValueArgument('show output if ETags match', '--if-match'),
1423
        if_none_match=ValueArgument(
1424
            'show output if ETags match', '--if-none-match'),
1425
        if_modified_since=DateArgument(
1426
            'show output modified since then', '--if-modified-since'),
1427
        if_unmodified_since=DateArgument(
1428
            'show output unmodified since then', '--if-unmodified-since'),
1429
        object_version=ValueArgument(
1430
            'get the specific version', ('-O', '--object-version'))
1431
    )
1432

    
1433
    @errors.generic.all
1434
    @errors.pithos.connection
1435
    @errors.pithos.container
1436
    @errors.pithos.object_path
1437
    def _run(self):
1438
        self._print(self.client.get_object_hashmap(
1439
            self.path,
1440
            version=self['object_version'],
1441
            if_match=self['if_match'],
1442
            if_none_match=self['if_none_match'],
1443
            if_modified_since=self['if_modified_since'],
1444
            if_unmodified_since=self['if_unmodified_since']), print_dict)
1445

    
1446
    def main(self, container___path):
1447
        super(self.__class__, self)._run(
1448
            container___path,
1449
            path_is_optional=False)
1450
        self._run()
1451

    
1452

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

    
1472
    arguments = dict(
1473
        until=DateArgument('remove history until that date', '--until'),
1474
        yes=FlagArgument('Do not prompt for permission', '--yes'),
1475
        recursive=FlagArgument(
1476
            'empty dir or container and delete (if dir)',
1477
            ('-R', '--recursive'))
1478
    )
1479

    
1480
    def __init__(self, arguments={}, auth_base=None, cloud=None):
1481
        super(self.__class__, self).__init__(arguments,  auth_base, cloud)
1482
        self['delimiter'] = DelimiterArgument(
1483
            self,
1484
            parsed_name='--delimiter',
1485
            help='delete objects prefixed with <object><delimiter>')
1486

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

    
1511
    def main(self, container____path__=None):
1512
        super(self.__class__, self)._run(container____path__)
1513
        self._run()
1514

    
1515

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

    
1527
    arguments = dict(
1528
        yes=FlagArgument('Do not prompt for permission', '--yes'),
1529
        force=FlagArgument('purge even if not empty', ('-F', '--force'))
1530
    )
1531

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

    
1552
    def main(self, container=None):
1553
        super(self.__class__, self)._run(container)
1554
        if container and self.container != container:
1555
            raiseCLIError('Invalid container name %s' % container, details=[
1556
                'Did you mean "%s" ?' % self.container,
1557
                'Use --container for names containing :'])
1558
        self._run()
1559

    
1560

    
1561
@command(pithos_cmds)
1562
class file_publish(_file_container_command):
1563
    """Publish the object and print the public url"""
1564

    
1565
    @errors.generic.all
1566
    @errors.pithos.connection
1567
    @errors.pithos.container
1568
    @errors.pithos.object_path
1569
    def _run(self):
1570
        print self.client.publish_object(self.path)
1571

    
1572
    def main(self, container___path):
1573
        super(self.__class__, self)._run(
1574
            container___path, path_is_optional=False)
1575
        self._run()
1576

    
1577

    
1578
@command(pithos_cmds)
1579
class file_unpublish(_file_container_command, _optional_output_cmd):
1580
    """Unpublish an object"""
1581

    
1582
    @errors.generic.all
1583
    @errors.pithos.connection
1584
    @errors.pithos.container
1585
    @errors.pithos.object_path
1586
    def _run(self):
1587
            self._optional_output(self.client.unpublish_object(self.path))
1588

    
1589
    def main(self, container___path):
1590
        super(self.__class__, self)._run(
1591
            container___path, path_is_optional=False)
1592
        self._run()
1593

    
1594

    
1595
@command(pithos_cmds)
1596
class file_permissions(_pithos_init):
1597
    """Manage user and group accessibility for objects
1598
    Permissions are lists of users and user groups. There are read and write
1599
    permissions. Users and groups with write permission have also read
1600
    permission.
1601
    """
1602

    
1603

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

    
1615

    
1616
@command(pithos_cmds)
1617
class file_permissions_get(_file_container_command, _optional_json):
1618
    """Get read and write permissions of an object"""
1619

    
1620
    @errors.generic.all
1621
    @errors.pithos.connection
1622
    @errors.pithos.container
1623
    @errors.pithos.object_path
1624
    def _run(self):
1625
        self._print(
1626
            self.client.get_object_sharing(self.path), print_permissions)
1627

    
1628
    def main(self, container___path):
1629
        super(self.__class__, self)._run(
1630
            container___path, path_is_optional=False)
1631
        self._run()
1632

    
1633

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

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

    
1660
    @errors.generic.all
1661
    @errors.pithos.connection
1662
    @errors.pithos.container
1663
    @errors.pithos.object_path
1664
    def _run(self, read, write):
1665
        self._optional_output(self.client.set_object_sharing(
1666
            self.path, read_permission=read, write_permission=write))
1667

    
1668
    def main(self, container___path, *permissions):
1669
        super(self.__class__, self)._run(
1670
            container___path, path_is_optional=False)
1671
        read, write = self.format_permission_dict(permissions)
1672
        self._run(read, write)
1673

    
1674

    
1675
@command(pithos_cmds)
1676
class file_permissions_delete(_file_container_command, _optional_output_cmd):
1677
    """Delete all permissions set on object
1678
    To modify permissions, use /file permissions set
1679
    """
1680

    
1681
    @errors.generic.all
1682
    @errors.pithos.connection
1683
    @errors.pithos.container
1684
    @errors.pithos.object_path
1685
    def _run(self):
1686
        self._optional_output(self.client.del_object_sharing(self.path))
1687

    
1688
    def main(self, container___path):
1689
        super(self.__class__, self)._run(
1690
            container___path, path_is_optional=False)
1691
        self._run()
1692

    
1693

    
1694
@command(pithos_cmds)
1695
class file_info(_file_container_command, _optional_json):
1696
    """Get detailed information for user account, containers or objects
1697
    to get account info:    /file info
1698
    to get container info:  /file info <container>
1699
    to get object info:     /file info <container>:<path>
1700
    """
1701

    
1702
    arguments = dict(
1703
        object_version=ValueArgument(
1704
            'show specific version \ (applies only for objects)',
1705
            ('-O', '--object-version'))
1706
    )
1707

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

    
1722
    def main(self, container____path__=None):
1723
        super(self.__class__, self)._run(container____path__)
1724
        self._run()
1725

    
1726

    
1727
@command(pithos_cmds)
1728
class file_metadata(_pithos_init):
1729
    """Metadata are attached on objects. They are formed as key:value pairs.
1730
    They can have arbitary values.
1731
    """
1732

    
1733

    
1734
@command(pithos_cmds)
1735
class file_metadata_get(_file_container_command, _optional_json):
1736
    """Get metadata for account, containers or objects"""
1737

    
1738
    arguments = dict(
1739
        detail=FlagArgument('show detailed output', ('-l', '--details')),
1740
        until=DateArgument('show metadata until then', '--until'),
1741
        object_version=ValueArgument(
1742
            'show specific version (applies only for objects)',
1743
            ('-O', '--object-version'))
1744
    )
1745

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

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

    
1787

    
1788
@command(pithos_cmds)
1789
class file_metadata_set(_file_container_command, _optional_output_cmd):
1790
    """Set a piece of metadata for account, container or object"""
1791

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

    
1805
    def main(self, metakey, metaval, container____path__=None):
1806
        super(self.__class__, self)._run(container____path__)
1807
        self._run(metakey=metakey, metaval=metaval)
1808

    
1809

    
1810
@command(pithos_cmds)
1811
class file_metadata_delete(_file_container_command, _optional_output_cmd):
1812
    """Delete metadata with given key from account, container or object
1813
    - to get metadata of current account: /file metadata get
1814
    - to get metadata of a container:     /file metadata get <container>
1815
    - to get metadata of an object:       /file metadata get <container>:<path>
1816
    """
1817

    
1818
    @errors.generic.all
1819
    @errors.pithos.connection
1820
    @errors.pithos.container
1821
    @errors.pithos.object_path
1822
    def _run(self, metakey):
1823
        if self.container is None:
1824
            r = self.client.del_account_meta(metakey)
1825
        elif self.path is None:
1826
            r = self.client.del_container_meta(metakey)
1827
        else:
1828
            r = self.client.del_object_meta(self.path, metakey)
1829
        self._optional_output(r)
1830

    
1831
    def main(self, metakey, container____path__=None):
1832
        super(self.__class__, self)._run(container____path__)
1833
        self._run(metakey)
1834

    
1835

    
1836
@command(pithos_cmds)
1837
class file_quota(_file_account_command, _optional_json):
1838
    """Get account quota"""
1839

    
1840
    arguments = dict(
1841
        in_bytes=FlagArgument('Show result in bytes', ('-b', '--bytes'))
1842
    )
1843

    
1844
    @errors.generic.all
1845
    @errors.pithos.connection
1846
    def _run(self):
1847

    
1848
        def pretty_print(output):
1849
            if not self['in_bytes']:
1850
                for k in output:
1851
                    output[k] = format_size(output[k])
1852
            pretty_dict(output, '-')
1853

    
1854
        self._print(self.client.get_account_quota(), pretty_print)
1855

    
1856
    def main(self, custom_uuid=None):
1857
        super(self.__class__, self)._run(custom_account=custom_uuid)
1858
        self._run()
1859

    
1860

    
1861
@command(pithos_cmds)
1862
class file_containerlimit(_pithos_init):
1863
    """Container size limit commands"""
1864

    
1865

    
1866
@command(pithos_cmds)
1867
class file_containerlimit_get(_file_container_command, _optional_json):
1868
    """Get container size limit"""
1869

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

    
1874
    @errors.generic.all
1875
    @errors.pithos.container
1876
    def _run(self):
1877

    
1878
        def pretty_print(output):
1879
            if not self['in_bytes']:
1880
                for k, v in output.items():
1881
                    output[k] = 'unlimited' if '0' == v else format_size(v)
1882
            pretty_dict(output, '-')
1883

    
1884
        self._print(
1885
            self.client.get_container_limit(self.container), pretty_print)
1886

    
1887
    def main(self, container=None):
1888
        super(self.__class__, self)._run()
1889
        self.container = container
1890
        self._run()
1891

    
1892

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

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

    
1927
    @errors.generic.all
1928
    @errors.pithos.connection
1929
    @errors.pithos.container
1930
    def _run(self, limit):
1931
        if self.container:
1932
            self.client.container = self.container
1933
        self._optional_output(self.client.set_container_limit(limit))
1934

    
1935
    def main(self, limit, container=None):
1936
        super(self.__class__, self)._run()
1937
        limit = self._calculate_limit(limit)
1938
        self.container = container
1939
        self._run(limit)
1940

    
1941

    
1942
@command(pithos_cmds)
1943
class file_versioning(_pithos_init):
1944
    """Manage the versioning scheme of current pithos user account"""
1945

    
1946

    
1947
@command(pithos_cmds)
1948
class file_versioning_get(_file_account_command, _optional_json):
1949
    """Get  versioning for account or container"""
1950

    
1951
    @errors.generic.all
1952
    @errors.pithos.connection
1953
    @errors.pithos.container
1954
    def _run(self):
1955
        self._print(
1956
            self.client.get_container_versioning(self.container), print_dict)
1957

    
1958
    def main(self, container):
1959
        super(self.__class__, self)._run()
1960
        self.container = container
1961
        self._run()
1962

    
1963

    
1964
@command(pithos_cmds)
1965
class file_versioning_set(_file_account_command, _optional_output_cmd):
1966
    """Set versioning mode (auto, none) for account or container"""
1967

    
1968
    def _check_versioning(self, versioning):
1969
        if versioning and versioning.lower() in ('auto', 'none'):
1970
            return versioning.lower()
1971
        raiseCLIError('Invalid versioning %s' % versioning, details=[
1972
            'Versioning can be auto or none'])
1973

    
1974
    @errors.generic.all
1975
    @errors.pithos.connection
1976
    @errors.pithos.container
1977
    def _run(self, versioning):
1978
        self.client.container = self.container
1979
        r = self.client.set_container_versioning(versioning)
1980
        self._optional_output(r)
1981

    
1982
    def main(self, versioning, container):
1983
        super(self.__class__, self)._run()
1984
        self._run(self._check_versioning(versioning))
1985

    
1986

    
1987
@command(pithos_cmds)
1988
class file_group(_pithos_init):
1989
    """Manage access groups and group members"""
1990

    
1991

    
1992
@command(pithos_cmds)
1993
class file_group_list(_file_account_command, _optional_json):
1994
    """list all groups and group members"""
1995

    
1996
    @errors.generic.all
1997
    @errors.pithos.connection
1998
    def _run(self):
1999
        self._print(self.client.get_account_group(), pretty_dict, delim='-')
2000

    
2001
    def main(self):
2002
        super(self.__class__, self)._run()
2003
        self._run()
2004

    
2005

    
2006
@command(pithos_cmds)
2007
class file_group_set(_file_account_command, _optional_output_cmd):
2008
    """Set a user group"""
2009

    
2010
    @errors.generic.all
2011
    @errors.pithos.connection
2012
    def _run(self, groupname, *users):
2013
        self._optional_output(self.client.set_account_group(groupname, users))
2014

    
2015
    def main(self, groupname, *users):
2016
        super(self.__class__, self)._run()
2017
        if users:
2018
            self._run(groupname, *users)
2019
        else:
2020
            raiseCLIError('No users to add in group %s' % groupname)
2021

    
2022

    
2023
@command(pithos_cmds)
2024
class file_group_delete(_file_account_command, _optional_output_cmd):
2025
    """Delete a user group"""
2026

    
2027
    @errors.generic.all
2028
    @errors.pithos.connection
2029
    def _run(self, groupname):
2030
        self._optional_output(self.client.del_account_group(groupname))
2031

    
2032
    def main(self, groupname):
2033
        super(self.__class__, self)._run()
2034
        self._run(groupname)
2035

    
2036

    
2037
@command(pithos_cmds)
2038
class file_sharers(_file_account_command, _optional_json):
2039
    """List the accounts that share objects with current user"""
2040

    
2041
    arguments = dict(
2042
        detail=FlagArgument('show detailed output', ('-l', '--details')),
2043
        marker=ValueArgument('show output greater then marker', '--marker')
2044
    )
2045

    
2046
    @errors.generic.all
2047
    @errors.pithos.connection
2048
    def _run(self):
2049
        accounts = self.client.get_sharing_accounts(marker=self['marker'])
2050
        if self['json_output'] or self['detail']:
2051
            self._print(accounts)
2052
        else:
2053
            self._print([acc['name'] for acc in accounts])
2054

    
2055
    def main(self):
2056
        super(self.__class__, self)._run()
2057
        self._run()
2058

    
2059

    
2060
def version_print(versions):
2061
    print_items([dict(id=vitem[0], created=strftime(
2062
        '%d-%m-%Y %H:%M:%S',
2063
        localtime(float(vitem[1])))) for vitem in versions])
2064

    
2065

    
2066
@command(pithos_cmds)
2067
class file_versions(_file_container_command, _optional_json):
2068
    """Get the list of object versions
2069
    Deleted objects may still have versions that can be used to restore it and
2070
    get information about its previous state.
2071
    The version number can be used in a number of other commands, like info,
2072
    copy, move, meta. See these commands for more information, e.g.
2073
    /file info -h
2074
    """
2075

    
2076
    @errors.generic.all
2077
    @errors.pithos.connection
2078
    @errors.pithos.container
2079
    @errors.pithos.object_path
2080
    def _run(self):
2081
        self._print(
2082
            self.client.get_object_versionlist(self.path), version_print)
2083

    
2084
    def main(self, container___path):
2085
        super(file_versions, self)._run(
2086
            container___path,
2087
            path_is_optional=False)
2088
        self._run()