Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / pithos.py @ 97086fcd

History | View | Annotate | Download (77.7 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 time import localtime, strftime
35
from os import path, makedirs, walk
36
from io import StringIO
37
from pydoc import pager
38

    
39
from kamaki.cli import command
40
from kamaki.cli.command_tree import CommandTree
41
from kamaki.cli.errors import raiseCLIError, CLISyntaxError, CLIBaseUrlError
42
from kamaki.cli.utils import (
43
    format_size, to_bytes, bold, get_path_size, guess_mime_type)
44
from kamaki.cli.argument import FlagArgument, ValueArgument, IntArgument
45
from kamaki.cli.argument import KeyValueArgument, DateArgument
46
from kamaki.cli.argument import ProgressBarArgument
47
from kamaki.cli.commands import _command_init, errors
48
from kamaki.cli.commands import addLogSettings, DontRaiseKeyError
49
from kamaki.cli.commands import (
50
    _optional_output_cmd, _optional_json, _name_filter)
51
from kamaki.clients.pithos import PithosClient, ClientError
52
from kamaki.clients.astakos import AstakosClient
53

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

    
57

    
58
# Argument functionality
59

    
60

    
61
class SharingArgument(ValueArgument):
62
    """Set sharing (read and/or write) groups
63
    .
64
    :value type: "read=term1,term2,... write=term1,term2,..."
65
    .
66
    :value returns: {'read':['term1', 'term2', ...],
67
    .   'write':['term1', 'term2', ...]}
68
    """
69

    
70
    @property
71
    def value(self):
72
        return getattr(self, '_value', self.default)
73

    
74
    @value.setter
75
    def value(self, newvalue):
76
        perms = {}
77
        try:
78
            permlist = newvalue.split(' ')
79
        except AttributeError:
80
            return
81
        for p in permlist:
82
            try:
83
                (key, val) = p.split('=')
84
            except ValueError as err:
85
                raiseCLIError(
86
                    err,
87
                    'Error in --sharing',
88
                    details='Incorrect format',
89
                    importance=1)
90
            if key.lower() not in ('read', 'write'):
91
                msg = 'Error in --sharing'
92
                raiseCLIError(err, msg, importance=1, details=[
93
                    'Invalid permission key %s' % key])
94
            val_list = val.split(',')
95
            if not key in perms:
96
                perms[key] = []
97
            for item in val_list:
98
                if item not in perms[key]:
99
                    perms[key].append(item)
100
        self._value = perms
101

    
102

    
103
class RangeArgument(ValueArgument):
104
    """
105
    :value type: string of the form <start>-<end> where <start> and <end> are
106
        integers
107
    :value returns: the input string, after type checking <start> and <end>
108
    """
109

    
110
    @property
111
    def value(self):
112
        return getattr(self, '_value', self.default)
113

    
114
    @value.setter
115
    def value(self, newvalues):
116
        if not newvalues:
117
            self._value = self.default
118
            return
119
        self._value = ''
120
        for newvalue in newvalues.split(','):
121
            self._value = ('%s,' % self._value) if self._value else ''
122
            start, sep, end = newvalue.partition('-')
123
            if sep:
124
                if start:
125
                    start, end = (int(start), int(end))
126
                    assert start <= end, 'Invalid range value %s' % newvalue
127
                    self._value += '%s-%s' % (int(start), int(end))
128
                else:
129
                    self._value += '-%s' % int(end)
130
            else:
131
                self._value += '%s' % int(start)
132

    
133

    
134
# Command specs
135

    
136

    
137
class _pithos_init(_command_init):
138
    """Initialize a pithos+ kamaki client"""
139

    
140
    @staticmethod
141
    def _is_dir(remote_dict):
142
        return 'application/directory' == remote_dict.get(
143
            'content_type', remote_dict.get('content-type', ''))
144

    
145
    @DontRaiseKeyError
146
    def _custom_container(self):
147
        return self.config.get_cloud(self.cloud, 'pithos_container')
148

    
149
    @DontRaiseKeyError
150
    def _custom_uuid(self):
151
        return self.config.get_cloud(self.cloud, 'pithos_uuid')
152

    
153
    def _set_account(self):
154
        self.account = self._custom_uuid()
155
        if self.account:
156
            return
157
        if getattr(self, 'auth_base', False):
158
            self.account = self.auth_base.user_term('id', self.token)
159
        else:
160
            astakos_url = self._custom_url('astakos')
161
            astakos_token = self._custom_token('astakos') or self.token
162
            if not astakos_url:
163
                raise CLIBaseUrlError(service='astakos')
164
            astakos = AstakosClient(astakos_url, astakos_token)
165
            self.account = astakos.user_term('id')
166

    
167
    @errors.generic.all
168
    @addLogSettings
169
    def _run(self):
170
        self.base_url = None
171
        if getattr(self, 'cloud', None):
172
            self.base_url = self._custom_url('pithos')
173
        else:
174
            self.cloud = 'default'
175
        self.token = self._custom_token('pithos')
176
        self.container = self._custom_container()
177

    
178
        if getattr(self, 'auth_base', False):
179
            self.token = self.token or self.auth_base.token
180
            if not self.base_url:
181
                pithos_endpoints = self.auth_base.get_service_endpoints(
182
                    self._custom_type('pithos') or 'object-store',
183
                    self._custom_version('pithos') or '')
184
                self.base_url = pithos_endpoints['publicURL']
185
        elif not self.base_url:
186
            raise CLIBaseUrlError(service='pithos')
187

    
188
        self._set_account()
189
        self.client = PithosClient(
190
            base_url=self.base_url,
191
            token=self.token,
192
            account=self.account,
193
            container=self.container)
194

    
195
    def main(self):
196
        self._run()
197

    
198

    
199
class _file_account_command(_pithos_init):
200
    """Base class for account level storage commands"""
201

    
202
    def __init__(self, arguments={}, auth_base=None, cloud=None):
203
        super(_file_account_command, self).__init__(
204
            arguments, auth_base, cloud)
205
        self['account'] = ValueArgument(
206
            'Set user account (not permanent)', ('-A', '--account'))
207

    
208
    def _run(self, custom_account=None):
209
        super(_file_account_command, self)._run()
210
        if custom_account:
211
            self.client.account = custom_account
212
        elif self['account']:
213
            self.client.account = self['account']
214

    
215
    @errors.generic.all
216
    def main(self):
217
        self._run()
218

    
219

    
220
class _file_container_command(_file_account_command):
221
    """Base class for container level storage commands"""
222

    
223
    container = None
224
    path = None
225

    
226
    def __init__(self, arguments={}, auth_base=None, cloud=None):
227
        super(_file_container_command, self).__init__(
228
            arguments, auth_base, cloud)
229
        self['container'] = ValueArgument(
230
            'Set container to work with (temporary)', ('-C', '--container'))
231

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

    
252
        user_cont, sep, userpath = container_with_path.partition(':')
253

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

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

    
305
    def main(self, container_with_path=None, path_is_optional=True):
306
        self._run(container_with_path, path_is_optional)
307

    
308

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

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

    
344
    def print_objects(self, object_list):
345
        for index, obj in enumerate(object_list):
346
            if self['exact_match'] and self.path and not (
347
                    obj['name'] == self.path or 'content_type' in obj):
348
                continue
349
            pretty_obj = obj.copy()
350
            index += 1
351
            empty_space = ' ' * (len(str(len(object_list))) - len(str(index)))
352
            if 'subdir' in obj:
353
                continue
354
            if obj['content_type'] == 'application/directory':
355
                isDir = True
356
                size = 'D'
357
            else:
358
                isDir = False
359
                size = format_size(obj['bytes'])
360
                pretty_obj['bytes'] = '%s (%s)' % (obj['bytes'], size)
361
            oname = obj['name'] if self['more'] else bold(obj['name'])
362
            prfx = (
363
                '%s%s. ' % (empty_space, index)) if self['enum'] else ''
364
            if self['detail']:
365
                self.writeln('%s%s' % (prfx, oname))
366
                self.print_dict(pretty_obj, exclude=('name'))
367
                self.writeln()
368
            else:
369
                oname = '%s%9s %s' % (prfx, size, oname)
370
                oname += '/' if isDir else u''
371
                self.writeln(oname)
372

    
373
    def print_containers(self, container_list):
374
        for index, container in enumerate(container_list):
375
            if 'bytes' in container:
376
                size = format_size(container['bytes'])
377
            prfx = ('%s. ' % (index + 1)) if self['enum'] else ''
378
            _cname = container['name'] if (
379
                self['more']) else bold(container['name'])
380
            cname = u'%s%s' % (prfx, _cname)
381
            if self['detail']:
382
                self.writeln(cname)
383
                pretty_c = container.copy()
384
                if 'bytes' in container:
385
                    pretty_c['bytes'] = '%s (%s)' % (container['bytes'], size)
386
                self.print_dict(pretty_c, exclude=('name'))
387
                self.writeln()
388
            else:
389
                if 'count' in container and 'bytes' in container:
390
                    self.writeln('%s (%s, %s objects)' % (
391
                        cname, size, container['count']))
392
                else:
393
                    self.writeln(cname)
394

    
395
    @errors.generic.all
396
    @errors.pithos.connection
397
    @errors.pithos.object_path
398
    @errors.pithos.container
399
    def _run(self):
400
        files, prnt = None, None
401
        if self.container is None:
402
            r = self.client.account_get(
403
                limit=False if self['more'] else self['limit'],
404
                marker=self['marker'],
405
                if_modified_since=self['if_modified_since'],
406
                if_unmodified_since=self['if_unmodified_since'],
407
                until=self['until'],
408
                show_only_shared=self['shared'])
409
            files, prnt = self._filter_by_name(r.json), self.print_containers
410
        else:
411
            prefix = (self.path and not self['name']) or self['name_pref']
412
            r = self.client.container_get(
413
                limit=False if self['more'] else self['limit'],
414
                marker=self['marker'],
415
                prefix=prefix,
416
                delimiter=self['delimiter'],
417
                path=self['path'],
418
                if_modified_since=self['if_modified_since'],
419
                if_unmodified_since=self['if_unmodified_since'],
420
                until=self['until'],
421
                meta=self['meta'],
422
                show_only_shared=self['shared'])
423
            files, prnt = self._filter_by_name(r.json), self.print_objects
424
        if self['more']:
425
            outbu, self._out = self._out, StringIO()
426
        try:
427
            if self['json_output']:
428
                self._print(files)
429
            else:
430
                prnt(files)
431
        finally:
432
            if self['more']:
433
                pager(self._out.getvalue())
434
                self._out = outbu
435

    
436
    def main(self, container____path__=None):
437
        super(self.__class__, self)._run(container____path__)
438
        self._run()
439

    
440

    
441
@command(pithos_cmds)
442
class file_mkdir(_file_container_command, _optional_output_cmd):
443
    """Create a directory
444
    Kamaki hanldes directories the same way as OOS Storage and Pithos+:
445
    A directory  is   an  object  with  type  "application/directory"
446
    An object with path  dir/name can exist even if  dir does not exist
447
    or even if dir  is  a non  directory  object.  Users can modify dir '
448
    without affecting the dir/name object in any way.
449
    """
450

    
451
    @errors.generic.all
452
    @errors.pithos.connection
453
    @errors.pithos.container
454
    def _run(self):
455
        self._optional_output(self.client.create_directory(self.path))
456

    
457
    def main(self, container___directory):
458
        super(self.__class__, self)._run(
459
            container___directory, path_is_optional=False)
460
        self._run()
461

    
462

    
463
@command(pithos_cmds)
464
class file_touch(_file_container_command, _optional_output_cmd):
465
    """Create an empty object (file)
466
    If object exists, this command will reset it to 0 length
467
    """
468

    
469
    arguments = dict(
470
        content_type=ValueArgument(
471
            'Set content type (default: application/octet-stream)',
472
            '--content-type',
473
            default='application/octet-stream')
474
    )
475

    
476
    @errors.generic.all
477
    @errors.pithos.connection
478
    @errors.pithos.container
479
    def _run(self):
480
        self._optional_output(
481
            self.client.create_object(self.path, self['content_type']))
482

    
483
    def main(self, container___path):
484
        super(file_touch, self)._run(container___path, path_is_optional=False)
485
        self._run()
486

    
487

    
488
@command(pithos_cmds)
489
class file_create(_file_container_command, _optional_output_cmd):
490
    """Create a container"""
491

    
492
    arguments = dict(
493
        versioning=ValueArgument(
494
            'set container versioning (auto/none)', '--versioning'),
495
        limit=IntArgument('set default container limit', '--limit'),
496
        meta=KeyValueArgument(
497
            'set container metadata (can be repeated)', '--meta')
498
    )
499

    
500
    @errors.generic.all
501
    @errors.pithos.connection
502
    @errors.pithos.container
503
    def _run(self, container):
504
        self._optional_output(self.client.create_container(
505
            container=container,
506
            sizelimit=self['limit'],
507
            versioning=self['versioning'],
508
            metadata=self['meta']))
509

    
510
    def main(self, container=None):
511
        super(self.__class__, self)._run(container)
512
        if container and self.container != container:
513
            raiseCLIError('Invalid container name %s' % container, details=[
514
                'Did you mean "%s" ?' % self.container,
515
                'Use --container for names containing :'])
516
        self._run(container)
517

    
518

    
519
class _source_destination_command(_file_container_command):
520

    
521
    arguments = dict(
522
        destination_account=ValueArgument('', ('-a', '--dst-account')),
523
        recursive=FlagArgument('', ('-R', '--recursive')),
524
        prefix=FlagArgument('', '--with-prefix', default=''),
525
        suffix=ValueArgument('', '--with-suffix', default=''),
526
        add_prefix=ValueArgument('', '--add-prefix', default=''),
527
        add_suffix=ValueArgument('', '--add-suffix', default=''),
528
        prefix_replace=ValueArgument('', '--prefix-to-replace', default=''),
529
        suffix_replace=ValueArgument('', '--suffix-to-replace', default=''),
530
    )
531

    
532
    def __init__(self, arguments={}, auth_base=None, cloud=None):
533
        self.arguments.update(arguments)
534
        super(_source_destination_command, self).__init__(
535
            self.arguments, auth_base, cloud)
536

    
537
    def _run(self, source_container___path, path_is_optional=False):
538
        super(_source_destination_command, self)._run(
539
            source_container___path, path_is_optional)
540
        self.dst_client = PithosClient(
541
            base_url=self.client.base_url,
542
            token=self.client.token,
543
            account=self['destination_account'] or self.client.account)
544

    
545
    @errors.generic.all
546
    @errors.pithos.account
547
    def _dest_container_path(self, dest_container_path):
548
        if self['destination_container']:
549
            self.dst_client.container = self['destination_container']
550
            return (self['destination_container'], dest_container_path)
551
        if dest_container_path:
552
            dst = dest_container_path.split(':')
553
            if len(dst) > 1:
554
                try:
555
                    self.dst_client.container = dst[0]
556
                    self.dst_client.get_container_info(dst[0])
557
                except ClientError as err:
558
                    if err.status in (404, 204):
559
                        raiseCLIError(
560
                            'Destination container %s not found' % dst[0])
561
                    raise
562
                else:
563
                    self.dst_client.container = dst[0]
564
                return (dst[0], dst[1])
565
            return(None, dst[0])
566
        raiseCLIError('No destination container:path provided')
567

    
568
    def _get_all(self, prefix):
569
        return self.client.container_get(prefix=prefix).json
570

    
571
    def _get_src_objects(self, src_path, source_version=None):
572
        """Get a list of the source objects to be called
573

574
        :param src_path: (str) source path
575

576
        :returns: (method, params) a method that returns a list when called
577
        or (object) if it is a single object
578
        """
579
        if src_path and src_path[-1] == '/':
580
            src_path = src_path[:-1]
581

    
582
        if self['prefix']:
583
            return (self._get_all, dict(prefix=src_path))
584
        try:
585
            srcobj = self.client.get_object_info(
586
                src_path, version=source_version)
587
        except ClientError as srcerr:
588
            if srcerr.status == 404:
589
                raiseCLIError(
590
                    'Source object %s not in source container %s' % (
591
                        src_path, self.client.container),
592
                    details=['Hint: --with-prefix to match multiple objects'])
593
            elif srcerr.status not in (204,):
594
                raise
595
            return (self.client.list_objects, {})
596

    
597
        if self._is_dir(srcobj):
598
            if not self['recursive']:
599
                raiseCLIError(
600
                    'Object %s of cont. %s is a dir' % (
601
                        src_path, self.client.container),
602
                    details=['Use --recursive to access directories'])
603
            return (self._get_all, dict(prefix=src_path))
604
        srcobj['name'] = src_path
605
        return srcobj
606

    
607
    def src_dst_pairs(self, dst_path, source_version=None):
608
        src_iter = self._get_src_objects(self.path, source_version)
609
        src_N = isinstance(src_iter, tuple)
610
        add_prefix = self['add_prefix'].strip('/')
611

    
612
        if dst_path and dst_path.endswith('/'):
613
            dst_path = dst_path[:-1]
614

    
615
        try:
616
            dstobj = self.dst_client.get_object_info(dst_path)
617
        except ClientError as trgerr:
618
            if trgerr.status in (404,):
619
                if src_N:
620
                    raiseCLIError(
621
                        'Cannot merge multiple paths to path %s' % dst_path,
622
                        details=[
623
                            'Try to use / or a directory as destination',
624
                            'or create the destination dir (/file mkdir)',
625
                            'or use a single object as source'])
626
            elif trgerr.status not in (204,):
627
                raise
628
        else:
629
            if self._is_dir(dstobj):
630
                add_prefix = '%s/%s' % (dst_path.strip('/'), add_prefix)
631
            elif src_N:
632
                raiseCLIError(
633
                    'Cannot merge multiple paths to path' % dst_path,
634
                    details=[
635
                        'Try to use / or a directory as destination',
636
                        'or create the destination dir (/file mkdir)',
637
                        'or use a single object as source'])
638

    
639
        if src_N:
640
            (method, kwargs) = src_iter
641
            for obj in method(**kwargs):
642
                name = obj['name']
643
                if name.endswith(self['suffix']):
644
                    yield (name, self._get_new_object(name, add_prefix))
645
        elif src_iter['name'].endswith(self['suffix']):
646
            name = src_iter['name']
647
            yield (name, self._get_new_object(dst_path or name, add_prefix))
648
        else:
649
            raiseCLIError('Source path %s conflicts with suffix %s' % (
650
                src_iter['name'], self['suffix']))
651

    
652
    def _get_new_object(self, obj, add_prefix):
653
        if self['prefix_replace'] and obj.startswith(self['prefix_replace']):
654
            obj = obj[len(self['prefix_replace']):]
655
        if self['suffix_replace'] and obj.endswith(self['suffix_replace']):
656
            obj = obj[:-len(self['suffix_replace'])]
657
        return add_prefix + obj + self['add_suffix']
658

    
659

    
660
@command(pithos_cmds)
661
class file_copy(_source_destination_command, _optional_output_cmd):
662
    """Copy objects from container to (another) container
663
    Semantics:
664
    copy cont:path dir
665
    .   transfer path as dir/path
666
    copy cont:path cont2:
667
    .   trasnfer all <obj> prefixed with path to container cont2
668
    copy cont:path [cont2:]path2
669
    .   transfer path to path2
670
    Use options:
671
    1. <container1>:<path1> [container2:]<path2> : if container2 is not given,
672
    destination is container1:path2
673
    2. <container>:<path1> <path2> : make a copy in the same container
674
    3. Can use --container= instead of <container1>
675
    """
676

    
677
    arguments = dict(
678
        destination_account=ValueArgument(
679
            'Account to copy to', ('-a', '--dst-account')),
680
        destination_container=ValueArgument(
681
            'use it if destination container name contains a : character',
682
            ('-D', '--dst-container')),
683
        public=ValueArgument('make object publicly accessible', '--public'),
684
        content_type=ValueArgument(
685
            'change object\'s content type', '--content-type'),
686
        recursive=FlagArgument(
687
            'copy directory and contents', ('-R', '--recursive')),
688
        prefix=FlagArgument(
689
            'Match objects prefixed with src path (feels like src_path*)',
690
            '--with-prefix',
691
            default=''),
692
        suffix=ValueArgument(
693
            'Suffix of source objects (feels like *suffix)', '--with-suffix',
694
            default=''),
695
        add_prefix=ValueArgument('Prefix targets', '--add-prefix', default=''),
696
        add_suffix=ValueArgument('Suffix targets', '--add-suffix', default=''),
697
        prefix_replace=ValueArgument(
698
            'Prefix of src to replace with dst path + add_prefix, if matched',
699
            '--prefix-to-replace',
700
            default=''),
701
        suffix_replace=ValueArgument(
702
            'Suffix of src to replace with add_suffix, if matched',
703
            '--suffix-to-replace',
704
            default=''),
705
        source_version=ValueArgument(
706
            'copy specific version', ('-S', '--source-version'))
707
    )
708

    
709
    @errors.generic.all
710
    @errors.pithos.connection
711
    @errors.pithos.container
712
    @errors.pithos.account
713
    def _run(self, dst_path):
714
        no_source_object = True
715
        src_account = self.client.account if (
716
            self['destination_account']) else None
717
        for src_obj, dst_obj in self.src_dst_pairs(
718
                dst_path, self['source_version']):
719
            no_source_object = False
720
            r = self.dst_client.copy_object(
721
                src_container=self.client.container,
722
                src_object=src_obj,
723
                dst_container=self.dst_client.container,
724
                dst_object=dst_obj,
725
                source_account=src_account,
726
                source_version=self['source_version'],
727
                public=self['public'],
728
                content_type=self['content_type'])
729
        if no_source_object:
730
            raiseCLIError('No object %s in container %s' % (
731
                self.path, self.container))
732
        self._optional_output(r)
733

    
734
    def main(
735
            self, source_container___path,
736
            destination_container___path=None):
737
        super(file_copy, self)._run(
738
            source_container___path, path_is_optional=False)
739
        (dst_cont, dst_path) = self._dest_container_path(
740
            destination_container___path)
741
        self.dst_client.container = dst_cont or self.container
742
        self._run(dst_path=dst_path or '')
743

    
744

    
745
@command(pithos_cmds)
746
class file_move(_source_destination_command, _optional_output_cmd):
747
    """Move/rename objects from container to (another) container
748
    Semantics:
749
    move cont:path dir
750
    .   rename path as dir/path
751
    move cont:path cont2:
752
    .   trasnfer all <obj> prefixed with path to container cont2
753
    move cont:path [cont2:]path2
754
    .   transfer path to path2
755
    Use options:
756
    1. <container1>:<path1> [container2:]<path2> : if container2 is not given,
757
    destination is container1:path2
758
    2. <container>:<path1> <path2> : move in the same container
759
    3. Can use --container= instead of <container1>
760
    """
761

    
762
    arguments = dict(
763
        destination_account=ValueArgument(
764
            'Account to move to', ('-a', '--dst-account')),
765
        destination_container=ValueArgument(
766
            'use it if destination container name contains a : character',
767
            ('-D', '--dst-container')),
768
        public=ValueArgument('make object publicly accessible', '--public'),
769
        content_type=ValueArgument(
770
            'change object\'s content type', '--content-type'),
771
        recursive=FlagArgument(
772
            'copy directory and contents', ('-R', '--recursive')),
773
        prefix=FlagArgument(
774
            'Match objects prefixed with src path (feels like src_path*)',
775
            '--with-prefix',
776
            default=''),
777
        suffix=ValueArgument(
778
            'Suffix of source objects (feels like *suffix)', '--with-suffix',
779
            default=''),
780
        add_prefix=ValueArgument('Prefix targets', '--add-prefix', default=''),
781
        add_suffix=ValueArgument('Suffix targets', '--add-suffix', default=''),
782
        prefix_replace=ValueArgument(
783
            'Prefix of src to replace with dst path + add_prefix, if matched',
784
            '--prefix-to-replace',
785
            default=''),
786
        suffix_replace=ValueArgument(
787
            'Suffix of src to replace with add_suffix, if matched',
788
            '--suffix-to-replace',
789
            default='')
790
    )
791

    
792
    @errors.generic.all
793
    @errors.pithos.connection
794
    @errors.pithos.container
795
    def _run(self, dst_path):
796
        no_source_object = True
797
        src_account = self.client.account if (
798
            self['destination_account']) else None
799
        for src_obj, dst_obj in self.src_dst_pairs(dst_path):
800
            no_source_object = False
801
            r = self.dst_client.move_object(
802
                src_container=self.container,
803
                src_object=src_obj,
804
                dst_container=self.dst_client.container,
805
                dst_object=dst_obj,
806
                source_account=src_account,
807
                public=self['public'],
808
                content_type=self['content_type'])
809
        if no_source_object:
810
            raiseCLIError('No object %s in container %s' % (
811
                self.path, self.container))
812
        self._optional_output(r)
813

    
814
    def main(
815
            self, source_container___path,
816
            destination_container___path=None):
817
        super(self.__class__, self)._run(
818
            source_container___path,
819
            path_is_optional=False)
820
        (dst_cont, dst_path) = self._dest_container_path(
821
            destination_container___path)
822
        (dst_cont, dst_path) = self._dest_container_path(
823
            destination_container___path)
824
        self.dst_client.container = dst_cont or self.container
825
        self._run(dst_path=dst_path or '')
826

    
827

    
828
@command(pithos_cmds)
829
class file_append(_file_container_command, _optional_output_cmd):
830
    """Append local file to (existing) remote object
831
    The remote object should exist.
832
    If the remote object is a directory, it is transformed into a file.
833
    In the later case, objects under the directory remain intact.
834
    """
835

    
836
    arguments = dict(
837
        progress_bar=ProgressBarArgument(
838
            'do not show progress bar', ('-N', '--no-progress-bar'),
839
            default=False)
840
    )
841

    
842
    @errors.generic.all
843
    @errors.pithos.connection
844
    @errors.pithos.container
845
    @errors.pithos.object_path
846
    def _run(self, local_path):
847
        (progress_bar, upload_cb) = self._safe_progress_bar('Appending')
848
        try:
849
            with open(local_path, 'rb') as f:
850
                self._optional_output(
851
                    self.client.append_object(self.path, f, upload_cb))
852
        finally:
853
            self._safe_progress_bar_finish(progress_bar)
854

    
855
    def main(self, local_path, container___path):
856
        super(self.__class__, self)._run(
857
            container___path, path_is_optional=False)
858
        self._run(local_path)
859

    
860

    
861
@command(pithos_cmds)
862
class file_truncate(_file_container_command, _optional_output_cmd):
863
    """Truncate remote file up to a size (default is 0)"""
864

    
865
    @errors.generic.all
866
    @errors.pithos.connection
867
    @errors.pithos.container
868
    @errors.pithos.object_path
869
    @errors.pithos.object_size
870
    def _run(self, size=0):
871
        self._optional_output(self.client.truncate_object(self.path, size))
872

    
873
    def main(self, container___path, size=0):
874
        super(self.__class__, self)._run(container___path)
875
        self._run(size=size)
876

    
877

    
878
@command(pithos_cmds)
879
class file_overwrite(_file_container_command, _optional_output_cmd):
880
    """Overwrite part (from start to end) of a remote file
881
    overwrite local-path container 10 20
882
    .   will overwrite bytes from 10 to 20 of a remote file with the same name
883
    .   as local-path basename
884
    overwrite local-path container:path 10 20
885
    .   will overwrite as above, but the remote file is named path
886
    """
887

    
888
    arguments = dict(
889
        progress_bar=ProgressBarArgument(
890
            'do not show progress bar', ('-N', '--no-progress-bar'),
891
            default=False)
892
    )
893

    
894
    @errors.generic.all
895
    @errors.pithos.connection
896
    @errors.pithos.container
897
    @errors.pithos.object_path
898
    @errors.pithos.object_size
899
    def _run(self, local_path, start, end):
900
        start, end = int(start), int(end)
901
        (progress_bar, upload_cb) = self._safe_progress_bar(
902
            'Overwrite %s bytes' % (end - start))
903
        try:
904
            with open(path.abspath(local_path), 'rb') as f:
905
                self._optional_output(self.client.overwrite_object(
906
                    obj=self.path,
907
                    start=start,
908
                    end=end,
909
                    source_file=f,
910
                    upload_cb=upload_cb))
911
        finally:
912
            self._safe_progress_bar_finish(progress_bar)
913

    
914
    def main(self, local_path, container___path, start, end):
915
        super(self.__class__, self)._run(
916
            container___path, path_is_optional=None)
917
        self.path = self.path or path.basename(local_path)
918
        self._run(local_path=local_path, start=start, end=end)
919

    
920

    
921
@command(pithos_cmds)
922
class file_manifest(_file_container_command, _optional_output_cmd):
923
    """Create a remote file of uploaded parts by manifestation
924
    Remains functional for compatibility with OOS Storage. Users are advised
925
    to use the upload command instead.
926
    Manifestation is a compliant process for uploading large files. The files
927
    have to be chunked in smalled files and uploaded as <prefix><increment>
928
    where increment is 1, 2, ...
929
    Finally, the manifest command glues partial files together in one file
930
    named <prefix>
931
    The upload command is faster, easier and more intuitive than manifest
932
    """
933

    
934
    arguments = dict(
935
        etag=ValueArgument('check written data', '--etag'),
936
        content_encoding=ValueArgument(
937
            'set MIME content type', '--content-encoding'),
938
        content_disposition=ValueArgument(
939
            'the presentation style of the object', '--content-disposition'),
940
        content_type=ValueArgument(
941
            'specify content type', '--content-type',
942
            default='application/octet-stream'),
943
        sharing=SharingArgument(
944
            '\n'.join([
945
                'define object sharing policy',
946
                '    ( "read=user1,grp1,user2,... write=user1,grp2,..." )']),
947
            '--sharing'),
948
        public=FlagArgument('make object publicly accessible', '--public')
949
    )
950

    
951
    @errors.generic.all
952
    @errors.pithos.connection
953
    @errors.pithos.container
954
    @errors.pithos.object_path
955
    def _run(self):
956
        ctype, cenc = guess_mime_type(self.path)
957
        self._optional_output(self.client.create_object_by_manifestation(
958
            self.path,
959
            content_encoding=self['content_encoding'] or cenc,
960
            content_disposition=self['content_disposition'],
961
            content_type=self['content_type'] or ctype,
962
            sharing=self['sharing'],
963
            public=self['public']))
964

    
965
    def main(self, container___path):
966
        super(self.__class__, self)._run(
967
            container___path, path_is_optional=False)
968
        self.run()
969

    
970

    
971
@command(pithos_cmds)
972
class file_upload(_file_container_command, _optional_output_cmd):
973
    """Upload a file"""
974

    
975
    arguments = dict(
976
        use_hashes=FlagArgument(
977
            'provide hashmap file instead of data', '--use-hashes'),
978
        etag=ValueArgument('check written data', '--etag'),
979
        unchunked=FlagArgument('avoid chunked transfer mode', '--unchunked'),
980
        content_encoding=ValueArgument(
981
            'set MIME content type', '--content-encoding'),
982
        content_disposition=ValueArgument(
983
            'specify objects presentation style', '--content-disposition'),
984
        content_type=ValueArgument('specify content type', '--content-type'),
985
        sharing=SharingArgument(
986
            help='\n'.join([
987
                'define sharing object policy',
988
                '( "read=user1,grp1,user2,... write=user1,grp2,... )']),
989
            parsed_name='--sharing'),
990
        public=FlagArgument('make object publicly accessible', '--public'),
991
        poolsize=IntArgument('set pool size', '--with-pool-size'),
992
        progress_bar=ProgressBarArgument(
993
            'do not show progress bar',
994
            ('-N', '--no-progress-bar'),
995
            default=False),
996
        overwrite=FlagArgument('Force (over)write', ('-f', '--force')),
997
        recursive=FlagArgument(
998
            'Recursively upload directory *contents* + subdirectories',
999
            ('-R', '--recursive'))
1000
    )
1001

    
1002
    def _check_container_limit(self, path):
1003
        cl_dict = self.client.get_container_limit()
1004
        container_limit = int(cl_dict['x-container-policy-quota'])
1005
        r = self.client.container_get()
1006
        used_bytes = sum(int(o['bytes']) for o in r.json)
1007
        path_size = get_path_size(path)
1008
        if container_limit and path_size > (container_limit - used_bytes):
1009
            raiseCLIError(
1010
                'Container(%s) (limit(%s) - used(%s)) < size(%s) of %s' % (
1011
                    self.client.container,
1012
                    format_size(container_limit),
1013
                    format_size(used_bytes),
1014
                    format_size(path_size),
1015
                    path),
1016
                importance=1, details=[
1017
                    'Check accound limit: /file quota',
1018
                    'Check container limit:',
1019
                    '\t/file containerlimit get %s' % self.client.container,
1020
                    'Increase container limit:',
1021
                    '\t/file containerlimit set <new limit> %s' % (
1022
                        self.client.container)])
1023

    
1024
    def _path_pairs(self, local_path, remote_path):
1025
        """Get pairs of local and remote paths"""
1026
        lpath = path.abspath(local_path)
1027
        short_path = lpath.split(path.sep)[-1]
1028
        rpath = remote_path or short_path
1029
        if path.isdir(lpath):
1030
            if not self['recursive']:
1031
                raiseCLIError('%s is a directory' % lpath, details=[
1032
                    'Use -R to upload directory contents'])
1033
            robj = self.client.container_get(path=rpath)
1034
            if robj.json and not self['overwrite']:
1035
                raiseCLIError(
1036
                    'Objects prefixed with %s already exist' % rpath,
1037
                    importance=1,
1038
                    details=['Existing objects:'] + ['\t%s:\t%s' % (
1039
                        o['content_type'][12:],
1040
                        o['name']) for o in robj.json] + [
1041
                        'Use -f to add, overwrite or resume'])
1042
            if not self['overwrite']:
1043
                try:
1044
                    topobj = self.client.get_object_info(rpath)
1045
                    if not self._is_dir(topobj):
1046
                        raiseCLIError(
1047
                            'Object %s exists but it is not a dir' % rpath,
1048
                            importance=1, details=['Use -f to overwrite'])
1049
                except ClientError as ce:
1050
                    if ce.status not in (404, ):
1051
                        raise
1052
            self._check_container_limit(lpath)
1053
            prev = ''
1054
            for top, subdirs, files in walk(lpath):
1055
                if top != prev:
1056
                    prev = top
1057
                    try:
1058
                        rel_path = rpath + top.split(lpath)[1]
1059
                    except IndexError:
1060
                        rel_path = rpath
1061
                    self.error('mkdir %s:%s' % (
1062
                        self.client.container, rel_path))
1063
                    self.client.create_directory(rel_path)
1064
                for f in files:
1065
                    fpath = path.join(top, f)
1066
                    if path.isfile(fpath):
1067
                        rel_path = rel_path.replace(path.sep, '/')
1068
                        pathfix = f.replace(path.sep, '/')
1069
                        yield open(fpath, 'rb'), '%s/%s' % (rel_path, pathfix)
1070
                    else:
1071
                        self.error('%s is not a regular file' % fpath)
1072
        else:
1073
            if not path.isfile(lpath):
1074
                raiseCLIError(('%s is not a regular file' % lpath) if (
1075
                    path.exists(lpath)) else '%s does not exist' % lpath)
1076
            try:
1077
                robj = self.client.get_object_info(rpath)
1078
                if remote_path and self._is_dir(robj):
1079
                    rpath += '/%s' % (short_path.replace(path.sep, '/'))
1080
                    self.client.get_object_info(rpath)
1081
                if not self['overwrite']:
1082
                    raiseCLIError(
1083
                        'Object %s already exists' % rpath,
1084
                        importance=1,
1085
                        details=['use -f to overwrite or resume'])
1086
            except ClientError as ce:
1087
                if ce.status not in (404, ):
1088
                    raise
1089
            self._check_container_limit(lpath)
1090
            yield open(lpath, 'rb'), rpath
1091

    
1092
    @errors.generic.all
1093
    @errors.pithos.connection
1094
    @errors.pithos.container
1095
    @errors.pithos.object_path
1096
    @errors.pithos.local_path
1097
    def _run(self, local_path, remote_path):
1098
        if self['poolsize'] > 0:
1099
            self.client.MAX_THREADS = int(self['poolsize'])
1100
        params = dict(
1101
            content_encoding=self['content_encoding'],
1102
            content_type=self['content_type'],
1103
            content_disposition=self['content_disposition'],
1104
            sharing=self['sharing'],
1105
            public=self['public'])
1106
        uploaded = []
1107
        container_info_cache = dict()
1108
        for f, rpath in self._path_pairs(local_path, remote_path):
1109
            self.error('%s --> %s:%s' % (f.name, self.client.container, rpath))
1110
            if not (self['content_type'] and self['content_encoding']):
1111
                ctype, cenc = guess_mime_type(f.name)
1112
                params['content_type'] = self['content_type'] or ctype
1113
                params['content_encoding'] = self['content_encoding'] or cenc
1114
            if self['unchunked']:
1115
                r = self.client.upload_object_unchunked(
1116
                    rpath, f,
1117
                    etag=self['etag'], withHashFile=self['use_hashes'],
1118
                    **params)
1119
                if self['with_output'] or self['json_output']:
1120
                    r['name'] = '%s: %s' % (self.client.container, rpath)
1121
                    uploaded.append(r)
1122
            else:
1123
                try:
1124
                    (progress_bar, upload_cb) = self._safe_progress_bar(
1125
                        'Uploading %s' % f.name.split(path.sep)[-1])
1126
                    if progress_bar:
1127
                        hash_bar = progress_bar.clone()
1128
                        hash_cb = hash_bar.get_generator(
1129
                            'Calculating block hashes')
1130
                    else:
1131
                        hash_cb = None
1132
                    r = self.client.upload_object(
1133
                        rpath, f,
1134
                        hash_cb=hash_cb,
1135
                        upload_cb=upload_cb,
1136
                        container_info_cache=container_info_cache,
1137
                        **params)
1138
                    if self['with_output'] or self['json_output']:
1139
                        r['name'] = '%s: %s' % (self.client.container, rpath)
1140
                        uploaded.append(r)
1141
                except Exception:
1142
                    self._safe_progress_bar_finish(progress_bar)
1143
                    raise
1144
                finally:
1145
                    self._safe_progress_bar_finish(progress_bar)
1146
        self._optional_output(uploaded)
1147
        self.error('Upload completed')
1148

    
1149
    def main(self, local_path, container____path__=None):
1150
        super(self.__class__, self)._run(container____path__)
1151
        remote_path = self.path or path.basename(path.abspath(local_path))
1152
        self._run(local_path=local_path, remote_path=remote_path)
1153

    
1154

    
1155
@command(pithos_cmds)
1156
class file_cat(_file_container_command):
1157
    """Print remote file contents to console"""
1158

    
1159
    arguments = dict(
1160
        range=RangeArgument('show range of data', '--range'),
1161
        if_match=ValueArgument('show output if ETags match', '--if-match'),
1162
        if_none_match=ValueArgument(
1163
            'show output if ETags match', '--if-none-match'),
1164
        if_modified_since=DateArgument(
1165
            'show output modified since then', '--if-modified-since'),
1166
        if_unmodified_since=DateArgument(
1167
            'show output unmodified since then', '--if-unmodified-since'),
1168
        object_version=ValueArgument(
1169
            'get the specific version', ('-O', '--object-version'))
1170
    )
1171

    
1172
    @errors.generic.all
1173
    @errors.pithos.connection
1174
    @errors.pithos.container
1175
    @errors.pithos.object_path
1176
    def _run(self):
1177
        self.client.download_object(
1178
            self.path, self._out,
1179
            range_str=self['range'],
1180
            version=self['object_version'],
1181
            if_match=self['if_match'],
1182
            if_none_match=self['if_none_match'],
1183
            if_modified_since=self['if_modified_since'],
1184
            if_unmodified_since=self['if_unmodified_since'])
1185

    
1186
    def main(self, container___path):
1187
        super(self.__class__, self)._run(
1188
            container___path, path_is_optional=False)
1189
        self._run()
1190

    
1191

    
1192
@command(pithos_cmds)
1193
class file_download(_file_container_command):
1194
    """Download remote object as local file
1195
    If local destination is a directory:
1196
    *   download <container>:<path> <local dir> -R
1197
    will download all files on <container> prefixed as <path>,
1198
    to <local dir>/<full path> (or <local dir>\<full path> in windows)
1199
    *   download <container>:<path> <local dir>
1200
    will download only one file<path>
1201
    ATTENTION: to download cont:dir1/dir2/file there must exist objects
1202
    cont:dir1 and cont:dir1/dir2 of type application/directory
1203
    To create directory objects, use /file mkdir
1204
    """
1205

    
1206
    arguments = dict(
1207
        resume=FlagArgument('Resume instead of overwrite', ('-r', '--resume')),
1208
        range=RangeArgument('show range of data', '--range'),
1209
        if_match=ValueArgument('show output if ETags match', '--if-match'),
1210
        if_none_match=ValueArgument(
1211
            'show output if ETags match', '--if-none-match'),
1212
        if_modified_since=DateArgument(
1213
            'show output modified since then', '--if-modified-since'),
1214
        if_unmodified_since=DateArgument(
1215
            'show output unmodified since then', '--if-unmodified-since'),
1216
        object_version=ValueArgument(
1217
            'get the specific version', ('-O', '--object-version')),
1218
        poolsize=IntArgument('set pool size', '--with-pool-size'),
1219
        progress_bar=ProgressBarArgument(
1220
            'do not show progress bar', ('-N', '--no-progress-bar'),
1221
            default=False),
1222
        recursive=FlagArgument(
1223
            'Download a remote path and its contents', ('-R', '--recursive'))
1224
    )
1225

    
1226
    def _outputs(self, local_path):
1227
        """:returns: (local_file, remote_path)"""
1228
        remotes = []
1229
        if self['recursive']:
1230
            r = self.client.container_get(
1231
                prefix=self.path or '/',
1232
                if_modified_since=self['if_modified_since'],
1233
                if_unmodified_since=self['if_unmodified_since'])
1234
            dirlist = dict()
1235
            for remote in r.json:
1236
                rname = remote['name'].strip('/')
1237
                tmppath = ''
1238
                for newdir in rname.strip('/').split('/')[:-1]:
1239
                    tmppath = '/'.join([tmppath, newdir])
1240
                    dirlist.update({tmppath.strip('/'): True})
1241
                remotes.append((rname, file_download._is_dir(remote)))
1242
            dir_remotes = [r[0] for r in remotes if r[1]]
1243
            if not set(dirlist).issubset(dir_remotes):
1244
                badguys = [bg.strip('/') for bg in set(
1245
                    dirlist).difference(dir_remotes)]
1246
                raiseCLIError(
1247
                    'Some remote paths contain non existing directories',
1248
                    details=['Missing remote directories:'] + badguys)
1249
        elif self.path:
1250
            r = self.client.get_object_info(
1251
                self.path,
1252
                version=self['object_version'])
1253
            if file_download._is_dir(r):
1254
                raiseCLIError(
1255
                    'Illegal download: Remote object %s is a directory' % (
1256
                        self.path),
1257
                    details=['To download a directory, try --recursive or -R'])
1258
            if '/' in self.path.strip('/') and not local_path:
1259
                raiseCLIError(
1260
                    'Illegal download: remote object %s contains "/"' % (
1261
                        self.path),
1262
                    details=[
1263
                        'To download an object containing "/" characters',
1264
                        'either create the remote directories or',
1265
                        'specify a non-directory local path for this object'])
1266
            remotes = [(self.path, False)]
1267
        if not remotes:
1268
            if self.path:
1269
                raiseCLIError(
1270
                    'No matching path %s on container %s' % (
1271
                        self.path, self.container),
1272
                    details=[
1273
                        'To list the contents of %s, try:' % self.container,
1274
                        '   /file list %s' % self.container])
1275
            raiseCLIError(
1276
                'Illegal download of container %s' % self.container,
1277
                details=[
1278
                    'To download a whole container, try:',
1279
                    '   /file download --recursive <container>'])
1280

    
1281
        lprefix = path.abspath(local_path or path.curdir)
1282
        if path.isdir(lprefix):
1283
            for rpath, remote_is_dir in remotes:
1284
                lpath = path.sep.join([
1285
                    lprefix[:-1] if lprefix.endswith(path.sep) else lprefix,
1286
                    rpath.strip('/').replace('/', path.sep)])
1287
                if remote_is_dir:
1288
                    if path.exists(lpath) and path.isdir(lpath):
1289
                        continue
1290
                    makedirs(lpath)
1291
                elif path.exists(lpath):
1292
                    if not self['resume']:
1293
                        self.error('File %s exists, aborting...' % lpath)
1294
                        continue
1295
                    with open(lpath, 'rwb+') as f:
1296
                        yield (f, rpath)
1297
                else:
1298
                    with open(lpath, 'wb+') as f:
1299
                        yield (f, rpath)
1300
        elif path.exists(lprefix):
1301
            if len(remotes) > 1:
1302
                raiseCLIError(
1303
                    '%s remote objects cannot be merged in local file %s' % (
1304
                        len(remotes),
1305
                        local_path),
1306
                    details=[
1307
                        'To download multiple objects, local path should be',
1308
                        'a directory, or use download without a local path'])
1309
            (rpath, remote_is_dir) = remotes[0]
1310
            if remote_is_dir:
1311
                raiseCLIError(
1312
                    'Remote directory %s should not replace local file %s' % (
1313
                        rpath,
1314
                        local_path))
1315
            if self['resume']:
1316
                with open(lprefix, 'rwb+') as f:
1317
                    yield (f, rpath)
1318
            else:
1319
                raiseCLIError(
1320
                    'Local file %s already exist' % local_path,
1321
                    details=['Try --resume to overwrite it'])
1322
        else:
1323
            if len(remotes) > 1 or remotes[0][1]:
1324
                raiseCLIError(
1325
                    'Local directory %s does not exist' % local_path)
1326
            with open(lprefix, 'wb+') as f:
1327
                yield (f, remotes[0][0])
1328

    
1329
    @errors.generic.all
1330
    @errors.pithos.connection
1331
    @errors.pithos.container
1332
    @errors.pithos.object_path
1333
    @errors.pithos.local_path
1334
    def _run(self, local_path):
1335
        poolsize = self['poolsize']
1336
        if poolsize:
1337
            self.client.MAX_THREADS = int(poolsize)
1338
        progress_bar = None
1339
        try:
1340
            for f, rpath in self._outputs(local_path):
1341
                (
1342
                    progress_bar,
1343
                    download_cb) = self._safe_progress_bar(
1344
                        'Download %s' % rpath)
1345
                self.client.download_object(
1346
                    rpath, f,
1347
                    download_cb=download_cb,
1348
                    range_str=self['range'],
1349
                    version=self['object_version'],
1350
                    if_match=self['if_match'],
1351
                    resume=self['resume'],
1352
                    if_none_match=self['if_none_match'],
1353
                    if_modified_since=self['if_modified_since'],
1354
                    if_unmodified_since=self['if_unmodified_since'])
1355
        except KeyboardInterrupt:
1356
            from threading import activeCount, enumerate as activethreads
1357
            timeout = 0.5
1358
            while activeCount() > 1:
1359
                self._out.write('\nCancel %s threads: ' % (activeCount() - 1))
1360
                self._out.flush()
1361
                for thread in activethreads():
1362
                    try:
1363
                        thread.join(timeout)
1364
                        self._out.write('.' if thread.isAlive() else '*')
1365
                    except RuntimeError:
1366
                        continue
1367
                    finally:
1368
                        self._out.flush()
1369
                        timeout += 0.1
1370
            self.error('\nDownload canceled by user')
1371
            if local_path is not None:
1372
                self.error('to resume, re-run with --resume')
1373
        except Exception:
1374
            self._safe_progress_bar_finish(progress_bar)
1375
            raise
1376
        finally:
1377
            self._safe_progress_bar_finish(progress_bar)
1378

    
1379
    def main(self, container___path, local_path=None):
1380
        super(self.__class__, self)._run(container___path)
1381
        self._run(local_path=local_path)
1382

    
1383

    
1384
@command(pithos_cmds)
1385
class file_hashmap(_file_container_command, _optional_json):
1386
    """Get the hash-map of an object"""
1387

    
1388
    arguments = dict(
1389
        if_match=ValueArgument('show output if ETags match', '--if-match'),
1390
        if_none_match=ValueArgument(
1391
            'show output if ETags match', '--if-none-match'),
1392
        if_modified_since=DateArgument(
1393
            'show output modified since then', '--if-modified-since'),
1394
        if_unmodified_since=DateArgument(
1395
            'show output unmodified since then', '--if-unmodified-since'),
1396
        object_version=ValueArgument(
1397
            'get the specific version', ('-O', '--object-version'))
1398
    )
1399

    
1400
    @errors.generic.all
1401
    @errors.pithos.connection
1402
    @errors.pithos.container
1403
    @errors.pithos.object_path
1404
    def _run(self):
1405
        self._print(self.client.get_object_hashmap(
1406
            self.path,
1407
            version=self['object_version'],
1408
            if_match=self['if_match'],
1409
            if_none_match=self['if_none_match'],
1410
            if_modified_since=self['if_modified_since'],
1411
            if_unmodified_since=self['if_unmodified_since']), self.print_dict)
1412

    
1413
    def main(self, container___path):
1414
        super(self.__class__, self)._run(
1415
            container___path, path_is_optional=False)
1416
        self._run()
1417

    
1418

    
1419
@command(pithos_cmds)
1420
class file_delete(_file_container_command, _optional_output_cmd):
1421
    """Delete a container [or an object]
1422
    How to delete a non-empty container:
1423
    - empty the container:  /file delete -R <container>
1424
    - delete it:            /file delete <container>
1425
    .
1426
    Semantics of directory deletion:
1427
    .a preserve the contents: /file delete <container>:<directory>
1428
    .    objects of the form dir/filename can exist with a dir object
1429
    .b delete contents:       /file delete -R <container>:<directory>
1430
    .    all dir/* objects are affected, even if dir does not exist
1431
    .
1432
    To restore a deleted object OBJ in a container CONT:
1433
    - get object versions: /file versions CONT:OBJ
1434
    .   and choose the version to be restored
1435
    - restore the object:  /file copy --source-version=<version> CONT:OBJ OBJ
1436
    """
1437

    
1438
    arguments = dict(
1439
        until=DateArgument('remove history until that date', '--until'),
1440
        yes=FlagArgument('Do not prompt for permission', '--yes'),
1441
        recursive=FlagArgument(
1442
            'empty dir or container and delete (if dir)',
1443
            ('-R', '--recursive')),
1444
        delimiter=ValueArgument(
1445
            'delete objects prefixed with <object><delimiter>', '--delimiter')
1446
    )
1447

    
1448
    @errors.generic.all
1449
    @errors.pithos.connection
1450
    @errors.pithos.container
1451
    @errors.pithos.object_path
1452
    def _run(self):
1453
        if self.path:
1454
            if self['yes'] or self.ask_user(
1455
                    'Delete %s:%s ?' % (self.container, self.path)):
1456
                self._optional_output(self.client.del_object(
1457
                    self.path,
1458
                    until=self['until'],
1459
                    delimiter='/' if self['recursive'] else self['delimiter']))
1460
            else:
1461
                self.error('Aborted')
1462
        elif self.container:
1463
            if self['recursive']:
1464
                ask_msg = 'Delete container contents'
1465
            else:
1466
                ask_msg = 'Delete container'
1467
            if self['yes'] or self.ask_user(
1468
                    '%s %s ?' % (ask_msg, self.container)):
1469
                self._optional_output(self.client.del_container(
1470
                    until=self['until'],
1471
                    delimiter='/' if self['recursive'] else self['delimiter']))
1472
            else:
1473
                self.error('Aborted')
1474
        else:
1475
            raiseCLIError('Nothing to delete, please provide container[:path]')
1476

    
1477
    def main(self, container____path__=None):
1478
        super(self.__class__, self)._run(container____path__)
1479
        self._run()
1480

    
1481

    
1482
@command(pithos_cmds)
1483
class file_purge(_file_container_command, _optional_output_cmd):
1484
    """Delete a container and release related data blocks
1485
    Non-empty containers can not purged.
1486
    To purge a container with content:
1487
    .   /file delete -R <container>
1488
    .      objects are deleted, but data blocks remain on server
1489
    .   /file purge <container>
1490
    .      container and data blocks are released and deleted
1491
    """
1492

    
1493
    arguments = dict(
1494
        yes=FlagArgument('Do not prompt for permission', '--yes'),
1495
        force=FlagArgument('purge even if not empty', ('-F', '--force'))
1496
    )
1497

    
1498
    @errors.generic.all
1499
    @errors.pithos.connection
1500
    @errors.pithos.container
1501
    def _run(self):
1502
        if self['yes'] or self.ask_user(
1503
                'Purge container %s?' % self.container):
1504
            try:
1505
                r = self.client.purge_container()
1506
            except ClientError as ce:
1507
                if ce.status in (409,):
1508
                    if self['force']:
1509
                        self.client.del_container(delimiter='/')
1510
                        r = self.client.purge_container()
1511
                    else:
1512
                        raiseCLIError(ce, details=['Try -F to force-purge'])
1513
                else:
1514
                    raise
1515
            self._optional_output(r)
1516
        else:
1517
            self.error('Aborted')
1518

    
1519
    def main(self, container=None):
1520
        super(self.__class__, self)._run(container)
1521
        if container and self.container != container:
1522
            raiseCLIError('Invalid container name %s' % container, details=[
1523
                'Did you mean "%s" ?' % self.container,
1524
                'Use --container for names containing :'])
1525
        self._run()
1526

    
1527

    
1528
@command(pithos_cmds)
1529
class file_publish(_file_container_command):
1530
    """Publish the object and print the public url"""
1531

    
1532
    @errors.generic.all
1533
    @errors.pithos.connection
1534
    @errors.pithos.container
1535
    @errors.pithos.object_path
1536
    def _run(self):
1537
        self.writeln(self.client.publish_object(self.path))
1538

    
1539
    def main(self, container___path):
1540
        super(self.__class__, self)._run(
1541
            container___path, path_is_optional=False)
1542
        self._run()
1543

    
1544

    
1545
@command(pithos_cmds)
1546
class file_unpublish(_file_container_command, _optional_output_cmd):
1547
    """Unpublish an object"""
1548

    
1549
    @errors.generic.all
1550
    @errors.pithos.connection
1551
    @errors.pithos.container
1552
    @errors.pithos.object_path
1553
    def _run(self):
1554
            self._optional_output(self.client.unpublish_object(self.path))
1555

    
1556
    def main(self, container___path):
1557
        super(self.__class__, self)._run(
1558
            container___path, path_is_optional=False)
1559
        self._run()
1560

    
1561

    
1562
@command(pithos_cmds)
1563
class file_permissions(_pithos_init):
1564
    """Manage user and group accessibility for objects
1565
    Permissions are lists of users and user groups. There are read and write
1566
    permissions. Users and groups with write permission have also read
1567
    permission.
1568
    """
1569

    
1570

    
1571
@command(pithos_cmds)
1572
class file_permissions_get(_file_container_command, _optional_json):
1573
    """Get read and write permissions of an object"""
1574

    
1575
    def print_permissions(self, permissions_dict, out):
1576
        expected_keys = ('read', 'write')
1577
        if set(permissions_dict).issubset(expected_keys):
1578
            self.print_dict(permissions_dict, out)
1579
        else:
1580
            invalid_keys = set(permissions_dict.keys()).difference(
1581
                expected_keys)
1582
            raiseCLIError(
1583
                'Illegal permission keys: %s' % ', '.join(invalid_keys),
1584
                importance=1, details=[
1585
                    'Valid permission types: %s' % ' '.join(expected_keys)])
1586

    
1587
    @errors.generic.all
1588
    @errors.pithos.connection
1589
    @errors.pithos.container
1590
    @errors.pithos.object_path
1591
    def _run(self):
1592
        self._print(
1593
            self.client.get_object_sharing(self.path), self.print_permissions)
1594

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

    
1600

    
1601
@command(pithos_cmds)
1602
class file_permissions_set(_file_container_command, _optional_output_cmd):
1603
    """Set permissions for an object
1604
    New permissions overwrite existing permissions.
1605
    Permission format:
1606
    -   read=<username>[,usergroup[,...]]
1607
    -   write=<username>[,usegroup[,...]]
1608
    E.g. to give read permissions for file F to users A and B and write for C:
1609
    .       /file permissions set F read=A,B write=C
1610
    To share with everybody, use '*' instead of a user id or group.
1611
    E.g. to make file F available to all pithos users:
1612
    .   /file permissions set F read=*
1613
    E.g. to make file F available for editing to all pithos users:
1614
    .   /file permissions set F write=*
1615
    """
1616

    
1617
    @errors.generic.all
1618
    def format_permission_dict(self, permissions):
1619
        read, write = False, False
1620
        for perms in permissions:
1621
            splstr = perms.split('=')
1622
            if 'read' == splstr[0]:
1623
                read = [ug.strip() for ug in splstr[1].split(',')]
1624
            elif 'write' == splstr[0]:
1625
                write = [ug.strip() for ug in splstr[1].split(',')]
1626
            else:
1627
                msg = 'Usage:\tread=<groups,users> write=<groups,users>'
1628
                raiseCLIError(None, msg)
1629
        return (read, write)
1630

    
1631
    @errors.generic.all
1632
    @errors.pithos.connection
1633
    @errors.pithos.container
1634
    @errors.pithos.object_path
1635
    def _run(self, read, write):
1636
        self._optional_output(self.client.set_object_sharing(
1637
            self.path, read_permission=read, write_permission=write))
1638

    
1639
    def main(self, container___path, *permissions):
1640
        super(self.__class__, self)._run(
1641
            container___path, path_is_optional=False)
1642
        read, write = self.format_permission_dict(permissions)
1643
        self._run(read, write)
1644

    
1645

    
1646
@command(pithos_cmds)
1647
class file_permissions_delete(_file_container_command, _optional_output_cmd):
1648
    """Delete all permissions set on object
1649
    To modify permissions, use /file permissions set
1650
    """
1651

    
1652
    @errors.generic.all
1653
    @errors.pithos.connection
1654
    @errors.pithos.container
1655
    @errors.pithos.object_path
1656
    def _run(self):
1657
        self._optional_output(self.client.del_object_sharing(self.path))
1658

    
1659
    def main(self, container___path):
1660
        super(self.__class__, self)._run(
1661
            container___path, path_is_optional=False)
1662
        self._run()
1663

    
1664

    
1665
@command(pithos_cmds)
1666
class file_info(_file_container_command, _optional_json):
1667
    """Get detailed information for user account, containers or objects
1668
    to get account info:    /file info
1669
    to get container info:  /file info <container>
1670
    to get object info:     /file info <container>:<path>
1671
    """
1672

    
1673
    arguments = dict(
1674
        object_version=ValueArgument(
1675
            'show specific version \ (applies only for objects)',
1676
            ('-O', '--object-version'))
1677
    )
1678

    
1679
    @errors.generic.all
1680
    @errors.pithos.connection
1681
    @errors.pithos.container
1682
    @errors.pithos.object_path
1683
    def _run(self):
1684
        if self.container is None:
1685
            r = self.client.get_account_info()
1686
        elif self.path is None:
1687
            r = self.client.get_container_info(self.container)
1688
        else:
1689
            r = self.client.get_object_info(
1690
                self.path, version=self['object_version'])
1691
        self._print(r, self.print_dict)
1692

    
1693
    def main(self, container____path__=None):
1694
        super(self.__class__, self)._run(container____path__)
1695
        self._run()
1696

    
1697

    
1698
@command(pithos_cmds)
1699
class file_metadata(_pithos_init):
1700
    """Metadata are attached on objects. They are formed as key:value pairs.
1701
    They can have arbitary values.
1702
    """
1703

    
1704

    
1705
@command(pithos_cmds)
1706
class file_metadata_get(_file_container_command, _optional_json):
1707
    """Get metadata for account, containers or objects"""
1708

    
1709
    arguments = dict(
1710
        detail=FlagArgument('show detailed output', ('-l', '--details')),
1711
        until=DateArgument('show metadata until then', '--until'),
1712
        object_version=ValueArgument(
1713
            'show specific version (applies only for objects)',
1714
            ('-O', '--object-version'))
1715
    )
1716

    
1717
    @errors.generic.all
1718
    @errors.pithos.connection
1719
    @errors.pithos.container
1720
    @errors.pithos.object_path
1721
    def _run(self):
1722
        until = self['until']
1723
        r = None
1724
        if self.container is None:
1725
            r = self.client.get_account_info(until=until)
1726
        elif self.path is None:
1727
            if self['detail']:
1728
                r = self.client.get_container_info(until=until)
1729
            else:
1730
                cmeta = self.client.get_container_meta(until=until)
1731
                ometa = self.client.get_container_object_meta(until=until)
1732
                r = {}
1733
                if cmeta:
1734
                    r['container-meta'] = cmeta
1735
                if ometa:
1736
                    r['object-meta'] = ometa
1737
        else:
1738
            if self['detail']:
1739
                r = self.client.get_object_info(
1740
                    self.path,
1741
                    version=self['object_version'])
1742
            else:
1743
                r = self.client.get_object_meta(
1744
                    self.path,
1745
                    version=self['object_version'])
1746
        if r:
1747
            self._print(r, self.print_dict)
1748

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

    
1753

    
1754
@command(pithos_cmds)
1755
class file_metadata_set(_file_container_command, _optional_output_cmd):
1756
    """Set a piece of metadata for account, container or object"""
1757

    
1758
    @errors.generic.all
1759
    @errors.pithos.connection
1760
    @errors.pithos.container
1761
    @errors.pithos.object_path
1762
    def _run(self, metakey, metaval):
1763
        if not self.container:
1764
            r = self.client.set_account_meta({metakey: metaval})
1765
        elif not self.path:
1766
            r = self.client.set_container_meta({metakey: metaval})
1767
        else:
1768
            r = self.client.set_object_meta(self.path, {metakey: metaval})
1769
        self._optional_output(r)
1770

    
1771
    def main(self, metakey, metaval, container____path__=None):
1772
        super(self.__class__, self)._run(container____path__)
1773
        self._run(metakey=metakey, metaval=metaval)
1774

    
1775

    
1776
@command(pithos_cmds)
1777
class file_metadata_delete(_file_container_command, _optional_output_cmd):
1778
    """Delete metadata with given key from account, container or object
1779
    - to get metadata of current account: /file metadata get
1780
    - to get metadata of a container:     /file metadata get <container>
1781
    - to get metadata of an object:       /file metadata get <container>:<path>
1782
    """
1783

    
1784
    @errors.generic.all
1785
    @errors.pithos.connection
1786
    @errors.pithos.container
1787
    @errors.pithos.object_path
1788
    def _run(self, metakey):
1789
        if self.container is None:
1790
            r = self.client.del_account_meta(metakey)
1791
        elif self.path is None:
1792
            r = self.client.del_container_meta(metakey)
1793
        else:
1794
            r = self.client.del_object_meta(self.path, metakey)
1795
        self._optional_output(r)
1796

    
1797
    def main(self, metakey, container____path__=None):
1798
        super(self.__class__, self)._run(container____path__)
1799
        self._run(metakey)
1800

    
1801

    
1802
@command(pithos_cmds)
1803
class file_quota(_file_account_command, _optional_json):
1804
    """Get account quota"""
1805

    
1806
    arguments = dict(
1807
        in_bytes=FlagArgument('Show result in bytes', ('-b', '--bytes'))
1808
    )
1809

    
1810
    @errors.generic.all
1811
    @errors.pithos.connection
1812
    def _run(self):
1813

    
1814
        def pretty_print(output, **kwargs):
1815
            if not self['in_bytes']:
1816
                for k in output:
1817
                    output[k] = format_size(output[k])
1818
            self.print_dict(output, '-', **kwargs)
1819

    
1820
        self._print(self.client.get_account_quota(), pretty_print)
1821

    
1822
    def main(self, custom_uuid=None):
1823
        super(self.__class__, self)._run(custom_account=custom_uuid)
1824
        self._run()
1825

    
1826

    
1827
@command(pithos_cmds)
1828
class file_containerlimit(_pithos_init):
1829
    """Container size limit commands"""
1830

    
1831

    
1832
@command(pithos_cmds)
1833
class file_containerlimit_get(_file_container_command, _optional_json):
1834
    """Get container size limit"""
1835

    
1836
    arguments = dict(
1837
        in_bytes=FlagArgument('Show result in bytes', ('-b', '--bytes'))
1838
    )
1839

    
1840
    @errors.generic.all
1841
    @errors.pithos.container
1842
    def _run(self):
1843

    
1844
        def pretty_print(output):
1845
            if not self['in_bytes']:
1846
                for k, v in output.items():
1847
                    output[k] = 'unlimited' if '0' == v else format_size(v)
1848
            self.print_dict(output, '-')
1849

    
1850
        self._print(
1851
            self.client.get_container_limit(self.container), pretty_print)
1852

    
1853
    def main(self, container=None):
1854
        super(self.__class__, self)._run()
1855
        self.container = container
1856
        self._run()
1857

    
1858

    
1859
@command(pithos_cmds)
1860
class file_containerlimit_set(_file_account_command, _optional_output_cmd):
1861
    """Set new storage limit for a container
1862
    By default, the limit is set in bytes
1863
    Users may specify a different unit, e.g:
1864
    /file containerlimit set 2.3GB mycontainer
1865
    Valid units: B, KiB (1024 B), KB (1000 B), MiB, MB, GiB, GB, TiB, TB
1866
    To set container limit to "unlimited", use 0
1867
    """
1868

    
1869
    @errors.generic.all
1870
    def _calculate_limit(self, user_input):
1871
        limit = 0
1872
        try:
1873
            limit = int(user_input)
1874
        except ValueError:
1875
            index = 0
1876
            digits = [str(num) for num in range(0, 10)] + ['.']
1877
            while user_input[index] in digits:
1878
                index += 1
1879
            limit = user_input[:index]
1880
            format = user_input[index:]
1881
            try:
1882
                return to_bytes(limit, format)
1883
            except Exception as qe:
1884
                msg = 'Failed to convert %s to bytes' % user_input,
1885
                raiseCLIError(qe, msg, details=[
1886
                    'Syntax: containerlimit set <limit>[format] [container]',
1887
                    'e.g.: containerlimit set 2.3GB mycontainer',
1888
                    'Valid formats:',
1889
                    '(*1024): B, KiB, MiB, GiB, TiB',
1890
                    '(*1000): B, KB, MB, GB, TB'])
1891
        return limit
1892

    
1893
    @errors.generic.all
1894
    @errors.pithos.connection
1895
    @errors.pithos.container
1896
    def _run(self, limit):
1897
        if self.container:
1898
            self.client.container = self.container
1899
        self._optional_output(self.client.set_container_limit(limit))
1900

    
1901
    def main(self, limit, container=None):
1902
        super(self.__class__, self)._run()
1903
        limit = self._calculate_limit(limit)
1904
        self.container = container
1905
        self._run(limit)
1906

    
1907

    
1908
@command(pithos_cmds)
1909
class file_versioning(_pithos_init):
1910
    """Manage the versioning scheme of current pithos user account"""
1911

    
1912

    
1913
@command(pithos_cmds)
1914
class file_versioning_get(_file_account_command, _optional_json):
1915
    """Get  versioning for account or container"""
1916

    
1917
    @errors.generic.all
1918
    @errors.pithos.connection
1919
    @errors.pithos.container
1920
    def _run(self):
1921
        self._print(
1922
            self.client.get_container_versioning(self.container),
1923
            self.print_dict)
1924

    
1925
    def main(self, container):
1926
        super(self.__class__, self)._run()
1927
        self.container = container
1928
        self._run()
1929

    
1930

    
1931
@command(pithos_cmds)
1932
class file_versioning_set(_file_account_command, _optional_output_cmd):
1933
    """Set versioning mode (auto, none) for account or container"""
1934

    
1935
    def _check_versioning(self, versioning):
1936
        if versioning and versioning.lower() in ('auto', 'none'):
1937
            return versioning.lower()
1938
        raiseCLIError('Invalid versioning %s' % versioning, details=[
1939
            'Versioning can be auto or none'])
1940

    
1941
    @errors.generic.all
1942
    @errors.pithos.connection
1943
    @errors.pithos.container
1944
    def _run(self, versioning):
1945
        self.client.container = self.container
1946
        r = self.client.set_container_versioning(versioning)
1947
        self._optional_output(r)
1948

    
1949
    def main(self, versioning, container):
1950
        super(self.__class__, self)._run()
1951
        self._run(self._check_versioning(versioning))
1952

    
1953

    
1954
@command(pithos_cmds)
1955
class file_group(_pithos_init):
1956
    """Manage access groups and group members"""
1957

    
1958

    
1959
@command(pithos_cmds)
1960
class file_group_list(_file_account_command, _optional_json):
1961
    """list all groups and group members"""
1962

    
1963
    @errors.generic.all
1964
    @errors.pithos.connection
1965
    def _run(self):
1966
        self._print(
1967
            self.client.get_account_group(), self.print_dict, delim='-')
1968

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

    
1973

    
1974
@command(pithos_cmds)
1975
class file_group_set(_file_account_command, _optional_output_cmd):
1976
    """Set a user group"""
1977

    
1978
    @errors.generic.all
1979
    @errors.pithos.connection
1980
    def _run(self, groupname, *users):
1981
        self._optional_output(self.client.set_account_group(groupname, users))
1982

    
1983
    def main(self, groupname, *users):
1984
        super(self.__class__, self)._run()
1985
        if users:
1986
            self._run(groupname, *users)
1987
        else:
1988
            raiseCLIError('No users to add in group %s' % groupname)
1989

    
1990

    
1991
@command(pithos_cmds)
1992
class file_group_delete(_file_account_command, _optional_output_cmd):
1993
    """Delete a user group"""
1994

    
1995
    @errors.generic.all
1996
    @errors.pithos.connection
1997
    def _run(self, groupname):
1998
        self._optional_output(self.client.del_account_group(groupname))
1999

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

    
2004

    
2005
@command(pithos_cmds)
2006
class file_sharers(_file_account_command, _optional_json):
2007
    """List the accounts that share objects with current user"""
2008

    
2009
    arguments = dict(
2010
        detail=FlagArgument('show detailed output', ('-l', '--details')),
2011
        marker=ValueArgument('show output greater then marker', '--marker')
2012
    )
2013

    
2014
    @errors.generic.all
2015
    @errors.pithos.connection
2016
    def _run(self):
2017
        accounts = self.client.get_sharing_accounts(marker=self['marker'])
2018
        if not self['json_output']:
2019
            usernames = self._uuids2usernames(
2020
                [acc['name'] for acc in accounts])
2021
            for item in accounts:
2022
                uuid = item['name']
2023
                item['id'], item['name'] = uuid, usernames[uuid]
2024
                if not self['detail']:
2025
                    item.pop('last_modified')
2026
        self._print(accounts)
2027

    
2028
    def main(self):
2029
        super(self.__class__, self)._run()
2030
        self._run()
2031

    
2032

    
2033
@command(pithos_cmds)
2034
class file_versions(_file_container_command, _optional_json):
2035
    """Get the list of object versions
2036
    Deleted objects may still have versions that can be used to restore it and
2037
    get information about its previous state.
2038
    The version number can be used in a number of other commands, like info,
2039
    copy, move, meta. See these commands for more information, e.g.
2040
    /file info -h
2041
    """
2042

    
2043
    def version_print(self, versions, out):
2044
        self.print_items(
2045
            [dict(id=vitem[0], created=strftime(
2046
                '%d-%m-%Y %H:%M:%S',
2047
                localtime(float(vitem[1])))) for vitem in versions],
2048
            out=out)
2049

    
2050
    @errors.generic.all
2051
    @errors.pithos.connection
2052
    @errors.pithos.container
2053
    @errors.pithos.object_path
2054
    def _run(self):
2055
        self._print(
2056
            self.client.get_object_versionlist(self.path), self.version_print)
2057

    
2058
    def main(self, container___path):
2059
        super(file_versions, self)._run(
2060
            container___path, path_is_optional=False)
2061
        self._run()