Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / pithos.py @ 8c54338a

History | View | Annotate | Download (77.5 kB)

1
# Copyright 2011-2012 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)
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 _optional_output_cmd, _optional_json
49
from kamaki.clients.pithos import PithosClient, ClientError
50
from kamaki.clients.astakos import AstakosClient
51

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

    
55

    
56
# Argument functionality
57

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

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

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

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

    
78

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

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

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

    
120

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

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

    
132
    @value.setter
133
    def value(self, newvalue):
134
        if newvalue is None:
135
            self._value = self.default
136
            return
137
        (start, end) = newvalue.split('-')
138
        (start, end) = (int(start), int(end))
139
        self._value = '%s-%s' % (start, end)
140

    
141
# Command specs
142

    
143

    
144
class _pithos_init(_command_init):
145
    """Initialize a pithos+ kamaki client"""
146

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

    
152
    @errors.generic.all
153
    def _run(self):
154
        self.token = self.config.get('file', 'token')\
155
            or self.config.get('global', 'token')
156

    
157
        if getattr(self, 'auth_base', False):
158
            pithos_endpoints = self.auth_base.get_service_endpoints(
159
                self.config.get('pithos', 'type'),
160
                self.config.get('pithos', 'version'))
161
            self.base_url = pithos_endpoints['publicURL']
162
        else:
163
            self.base_url = self.config.get('pithos', 'url')
164
        if not self.base_url:
165
            raise CLIBaseUrlError(service='pithos')
166

    
167
        self._set_account()
168
        self.container = self.config.get('file', 'container')\
169
            or self.config.get('global', 'container')
170
        self.client = PithosClient(
171
            base_url=self.base_url,
172
            token=self.token,
173
            account=self.account,
174
            container=self.container)
175
        self._set_log_params()
176
        self._update_max_threads()
177

    
178
    def main(self):
179
        self._run()
180

    
181
    def _set_account(self):
182
        if getattr(self, 'base_url', False):
183
            self.account = self.auth_base.user_term('id', self.token)
184
        else:
185
            astakos_url = self.config('astakos', 'get')
186
            if not astakos_url:
187
                raise CLIBaseUrlError(service='astakos')
188
            astakos = AstakosClient(astakos_url, self.token)
189
            self.account = astakos.user_term('id')
190

    
191

    
192
class _file_account_command(_pithos_init):
193
    """Base class for account level storage commands"""
194

    
195
    def __init__(self, arguments={}, auth_base=None):
196
        super(_file_account_command, self).__init__(arguments, auth_base)
197
        self['account'] = ValueArgument(
198
            'Set user account (not permanent)',
199
            ('-A', '--account'))
200

    
201
    def _run(self, custom_account=None):
202
        super(_file_account_command, self)._run()
203
        if custom_account:
204
            self.client.account = custom_account
205
        elif self['account']:
206
            self.client.account = self['account']
207

    
208
    @errors.generic.all
209
    def main(self):
210
        self._run()
211

    
212

    
213
class _file_container_command(_file_account_command):
214
    """Base class for container level storage commands"""
215

    
216
    container = None
217
    path = None
218

    
219
    def __init__(self, arguments={}, auth_base=None):
220
        super(_file_container_command, self).__init__(arguments, auth_base)
221
        self['container'] = ValueArgument(
222
            'Set container to work with (temporary)',
223
            ('-C', '--container'))
224

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

    
245
        user_cont, sep, userpath = container_with_path.partition(':')
246

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

    
280
    @errors.generic.all
281
    def _run(self, container_with_path=None, path_is_optional=True):
282
        super(_file_container_command, self)._run()
283
        if self['container']:
284
            self.client.container = self['container']
285
            if container_with_path:
286
                self.path = container_with_path
287
            elif not path_is_optional:
288
                raise CLISyntaxError(
289
                    'Both container and path are required',
290
                    details=errors.pithos.container_howto)
291
        elif container_with_path:
292
            self.extract_container_and_path(
293
                container_with_path,
294
                path_is_optional)
295
            self.client.container = self.container
296
        self.container = self.client.container
297

    
298
    def main(self, container_with_path=None, path_is_optional=True):
299
        self._run(container_with_path, path_is_optional)
300

    
301

    
302
@command(pithos_cmds)
303
class file_list(_file_container_command, _optional_json):
304
    """List containers, object trees or objects in a directory
305
    Use with:
306
    1 no parameters : containers in current account
307
    2. one parameter (container) or --container : contents of container
308
    3. <container>:<prefix> or --container=<container> <prefix>: objects in
309
    .   container starting with prefix
310
    """
311

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

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

    
377
    def print_containers(self, container_list):
378
        if self['json_output']:
379
            print_json(container_list)
380
            return
381
        limit = int(self['limit']) if self['limit'] > 0\
382
            else len(container_list)
383
        for index, container in enumerate(container_list):
384
            if 'bytes' in container:
385
                size = format_size(container['bytes'])
386
            prfx = ('%s. ' % (index + 1)) if self['enum'] else ''
387
            cname = '%s%s' % (prfx, bold(container['name']))
388
            if self['detail']:
389
                print(cname)
390
                pretty_c = container.copy()
391
                if 'bytes' in container:
392
                    pretty_c['bytes'] = '%s (%s)' % (container['bytes'], size)
393
                print_dict(pretty_keys(pretty_c), exclude=('name'))
394
                print
395
            else:
396
                if 'count' in container and 'bytes' in container:
397
                    print('%s (%s, %s objects)' % (
398
                        cname,
399
                        size,
400
                        container['count']))
401
                else:
402
                    print(cname)
403
            if self['more']:
404
                page_hold(index + 1, limit, len(container_list))
405

    
406
    @errors.generic.all
407
    @errors.pithos.connection
408
    @errors.pithos.object_path
409
    @errors.pithos.container
410
    def _run(self):
411
        if self.container is None:
412
            r = self.client.account_get(
413
                limit=False if self['more'] else self['limit'],
414
                marker=self['marker'],
415
                if_modified_since=self['if_modified_since'],
416
                if_unmodified_since=self['if_unmodified_since'],
417
                until=self['until'],
418
                show_only_shared=self['shared'])
419
            self._print(r.json, self.print_containers)
420
        else:
421
            prefix = self.path or self['prefix']
422
            r = self.client.container_get(
423
                limit=False if self['more'] else self['limit'],
424
                marker=self['marker'],
425
                prefix=prefix,
426
                delimiter=self['delimiter'],
427
                path=self['path'],
428
                if_modified_since=self['if_modified_since'],
429
                if_unmodified_since=self['if_unmodified_since'],
430
                until=self['until'],
431
                meta=self['meta'],
432
                show_only_shared=self['shared'])
433
            self._print(r.json, self.print_objects)
434

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

    
439

    
440
@command(pithos_cmds)
441
class file_mkdir(_file_container_command, _optional_output_cmd):
442
    """Create a directory"""
443

    
444
    __doc__ += '\n. '.join([
445
        'Kamaki hanldes directories the same way as OOS Storage and Pithos+:',
446
        'A   directory  is   an  object  with  type  "application/directory"',
447
        'An object with path  dir/name can exist even if  dir does not exist',
448
        'or even if dir  is  a non  directory  object.  Users can modify dir',
449
        'without affecting the dir/name object in any way.'])
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,
460
            path_is_optional=False)
461
        self._run()
462

    
463

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

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

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

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

    
490

    
491
@command(pithos_cmds)
492
class file_create(_file_container_command, _optional_output_cmd):
493
    """Create a container"""
494

    
495
    arguments = dict(
496
        versioning=ValueArgument(
497
            'set container versioning (auto/none)',
498
            '--versioning'),
499
        limit=IntArgument('set default container limit', '--limit'),
500
        meta=KeyValueArgument(
501
            'set container metadata (can be repeated)',
502
            '--meta')
503
    )
504

    
505
    @errors.generic.all
506
    @errors.pithos.connection
507
    @errors.pithos.container
508
    def _run(self, container):
509
        self._optional_output(self.client.create_container(
510
            container=container,
511
            sizelimit=self['limit'],
512
            versioning=self['versioning'],
513
            metadata=self['meta']))
514

    
515
    def main(self, container=None):
516
        super(self.__class__, self)._run(container)
517
        if container and self.container != container:
518
            raiseCLIError('Invalid container name %s' % container, details=[
519
                'Did you mean "%s" ?' % self.container,
520
                'Use --container for names containing :'])
521
        self._run(container)
522

    
523

    
524
class _source_destination_command(_file_container_command):
525

    
526
    arguments = dict(
527
        destination_account=ValueArgument('', ('a', '--dst-account')),
528
        recursive=FlagArgument('', ('-R', '--recursive')),
529
        prefix=FlagArgument('', '--with-prefix', default=''),
530
        suffix=ValueArgument('', '--with-suffix', default=''),
531
        add_prefix=ValueArgument('', '--add-prefix', default=''),
532
        add_suffix=ValueArgument('', '--add-suffix', default=''),
533
        prefix_replace=ValueArgument('', '--prefix-to-replace', default=''),
534
        suffix_replace=ValueArgument('', '--suffix-to-replace', default=''),
535
    )
536

    
537
    def __init__(self, arguments={}, auth_base=None):
538
        self.arguments.update(arguments)
539
        super(_source_destination_command, self).__init__(
540
            self.arguments, auth_base)
541

    
542
    def _run(self, source_container___path, path_is_optional=False):
543
        super(_source_destination_command, self)._run(
544
            source_container___path,
545
            path_is_optional)
546
        self.dst_client = PithosClient(
547
            base_url=self.client.base_url,
548
            token=self.client.token,
549
            account=self['destination_account'] or self.client.account)
550

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

    
574
    def _get_all(self, prefix):
575
        return self.client.container_get(prefix=prefix).json
576

    
577
    def _get_src_objects(self, src_path, source_version=None):
578
        """Get a list of the source objects to be called
579

580
        :param src_path: (str) source path
581

582
        :returns: (method, params) a method that returns a list when called
583
        or (object) if it is a single object
584
        """
585
        if src_path and src_path[-1] == '/':
586
            src_path = src_path[:-1]
587

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

    
604
        if self._is_dir(srcobj):
605
            if not self['recursive']:
606
                raiseCLIError(
607
                    'Object %s of cont. %s is a dir' % (
608
                        src_path,
609
                        self.client.container),
610
                    details=['Use --recursive to access directories'])
611
            return (self._get_all, dict(prefix=src_path))
612
        srcobj['name'] = src_path
613
        return srcobj
614

    
615
    def src_dst_pairs(self, dst_path, source_version=None):
616
        src_iter = self._get_src_objects(self.path, source_version)
617
        src_N = isinstance(src_iter, tuple)
618
        add_prefix = self['add_prefix'].strip('/')
619

    
620
        if dst_path and dst_path.endswith('/'):
621
            dst_path = dst_path[:-1]
622

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

    
647
        if src_N:
648
            (method, kwargs) = src_iter
649
            for obj in method(**kwargs):
650
                name = obj['name']
651
                if name.endswith(self['suffix']):
652
                    yield (name, self._get_new_object(name, add_prefix))
653
        elif src_iter['name'].endswith(self['suffix']):
654
            name = src_iter['name']
655
            yield (name, self._get_new_object(dst_path or name, add_prefix))
656
        else:
657
            raiseCLIError('Source path %s conflicts with suffix %s' % (
658
                src_iter['name'],
659
                self['suffix']))
660

    
661
    def _get_new_object(self, obj, add_prefix):
662
        if self['prefix_replace'] and obj.startswith(self['prefix_replace']):
663
            obj = obj[len(self['prefix_replace']):]
664
        if self['suffix_replace'] and obj.endswith(self['suffix_replace']):
665
            obj = obj[:-len(self['suffix_replace'])]
666
        return add_prefix + obj + self['add_suffix']
667

    
668

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

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

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

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

    
760

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

    
778
    arguments = dict(
779
        destination_account=ValueArgument(
780
            'Account to move to',
781
            ('-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',
788
            '--content-type'),
789
        recursive=FlagArgument(
790
            'copy directory and contents',
791
            ('-R', '--recursive')),
792
        prefix=FlagArgument(
793
            'Match objects prefixed with src path (feels like src_path*)',
794
            '--with-prefix',
795
            default=''),
796
        suffix=ValueArgument(
797
            'Suffix of source objects (feels like *suffix)',
798
            '--with-suffix',
799
            default=''),
800
        add_prefix=ValueArgument('Prefix targets', '--add-prefix', default=''),
801
        add_suffix=ValueArgument('Suffix targets', '--add-suffix', default=''),
802
        prefix_replace=ValueArgument(
803
            'Prefix of src to replace with dst path + add_prefix, if matched',
804
            '--prefix-to-replace',
805
            default=''),
806
        suffix_replace=ValueArgument(
807
            'Suffix of src to replace with add_suffix, if matched',
808
            '--suffix-to-replace',
809
            default='')
810
    )
811

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

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

    
848

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

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

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

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

    
886

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

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

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

    
903

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

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

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

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

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

    
955

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

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

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

    
1002
    def main(self, container___path):
1003
        super(self.__class__, self)._run(
1004
            container___path,
1005
            path_is_optional=False)
1006
        self.run()
1007

    
1008

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

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

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

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

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

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

    
1188

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

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

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

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

    
1231

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

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

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

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

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

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

    
1429

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

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

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

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

    
1465

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

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

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

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

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

    
1528

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

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

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

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

    
1573

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

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

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

    
1592

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

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

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

    
1610

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

    
1619

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

    
1631

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

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

    
1644
    def main(self, container___path):
1645
        super(self.__class__, self)._run(
1646
            container___path,
1647
            path_is_optional=False)
1648
        self._run()
1649

    
1650

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

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

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

    
1686
    def main(self, container___path, *permissions):
1687
        super(self.__class__, self)._run(
1688
            container___path,
1689
            path_is_optional=False)
1690
        (read, write) = self.format_permission_dict(permissions)
1691
        self._run(read, write)
1692

    
1693

    
1694
@command(pithos_cmds)
1695
class file_permissions_delete(_file_container_command, _optional_output_cmd):
1696
    """Delete all permissions set on object
1697
    To modify permissions, use /file permissions set
1698
    """
1699

    
1700
    @errors.generic.all
1701
    @errors.pithos.connection
1702
    @errors.pithos.container
1703
    @errors.pithos.object_path
1704
    def _run(self):
1705
        self._optional_output(self.client.del_object_sharing(self.path))
1706

    
1707
    def main(self, container___path):
1708
        super(self.__class__, self)._run(
1709
            container___path,
1710
            path_is_optional=False)
1711
        self._run()
1712

    
1713

    
1714
@command(pithos_cmds)
1715
class file_info(_file_container_command, _optional_json):
1716
    """Get detailed information for user account, containers or objects
1717
    to get account info:    /file info
1718
    to get container info:  /file info <container>
1719
    to get object info:     /file info <container>:<path>
1720
    """
1721

    
1722
    arguments = dict(
1723
        object_version=ValueArgument(
1724
            'show specific version \ (applies only for objects)',
1725
            ('-O', '--object-version'))
1726
    )
1727

    
1728
    @errors.generic.all
1729
    @errors.pithos.connection
1730
    @errors.pithos.container
1731
    @errors.pithos.object_path
1732
    def _run(self):
1733
        if self.container is None:
1734
            r = self.client.get_account_info()
1735
        elif self.path is None:
1736
            r = self.client.get_container_info(self.container)
1737
        else:
1738
            r = self.client.get_object_info(
1739
                self.path,
1740
                version=self['object_version'])
1741
        self._print(r, print_dict)
1742

    
1743
    def main(self, container____path__=None):
1744
        super(self.__class__, self)._run(container____path__)
1745
        self._run()
1746

    
1747

    
1748
@command(pithos_cmds)
1749
class file_metadata(_pithos_init):
1750
    """Metadata are attached on objects. They are formed as key:value pairs.
1751
    They can have arbitary values.
1752
    """
1753

    
1754

    
1755
@command(pithos_cmds)
1756
class file_metadata_get(_file_container_command, _optional_json):
1757
    """Get metadata for account, containers or objects"""
1758

    
1759
    arguments = dict(
1760
        detail=FlagArgument('show detailed output', ('-l', '--details')),
1761
        until=DateArgument('show metadata until then', '--until'),
1762
        object_version=ValueArgument(
1763
            'show specific version \ (applies only for objects)',
1764
            ('-O', '--object-version'))
1765
    )
1766

    
1767
    @errors.generic.all
1768
    @errors.pithos.connection
1769
    @errors.pithos.container
1770
    @errors.pithos.object_path
1771
    def _run(self):
1772
        until = self['until']
1773
        r = None
1774
        if self.container is None:
1775
            if self['detail']:
1776
                r = self.client.get_account_info(until=until)
1777
            else:
1778
                r = self.client.get_account_meta(until=until)
1779
                r = pretty_keys(r, '-')
1780
        elif self.path is None:
1781
            if self['detail']:
1782
                r = self.client.get_container_info(until=until)
1783
            else:
1784
                cmeta = self.client.get_container_meta(until=until)
1785
                ometa = self.client.get_container_object_meta(until=until)
1786
                r = {}
1787
                if cmeta:
1788
                    r['container-meta'] = pretty_keys(cmeta, '-')
1789
                if ometa:
1790
                    r['object-meta'] = pretty_keys(ometa, '-')
1791
        else:
1792
            if self['detail']:
1793
                r = self.client.get_object_info(
1794
                    self.path,
1795
                    version=self['object_version'])
1796
            else:
1797
                r = self.client.get_object_meta(
1798
                    self.path,
1799
                    version=self['object_version'])
1800
                r = pretty_keys(pretty_keys(r, '-'))
1801
        if r:
1802
            self._print(r, print_dict)
1803

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

    
1808

    
1809
@command(pithos_cmds)
1810
class file_metadata_set(_file_container_command, _optional_output_cmd):
1811
    """Set a piece of metadata for account, container or object"""
1812

    
1813
    @errors.generic.all
1814
    @errors.pithos.connection
1815
    @errors.pithos.container
1816
    @errors.pithos.object_path
1817
    def _run(self, metakey, metaval):
1818
        if not self.container:
1819
            r = self.client.set_account_meta({metakey: metaval})
1820
        elif not self.path:
1821
            r = self.client.set_container_meta({metakey: metaval})
1822
        else:
1823
            r = self.client.set_object_meta(self.path, {metakey: metaval})
1824
        self._optional_output(r)
1825

    
1826
    def main(self, metakey, metaval, container____path__=None):
1827
        super(self.__class__, self)._run(container____path__)
1828
        self._run(metakey=metakey, metaval=metaval)
1829

    
1830

    
1831
@command(pithos_cmds)
1832
class file_metadata_delete(_file_container_command, _optional_output_cmd):
1833
    """Delete metadata with given key from account, container or object
1834
    - to get metadata of current account: /file metadata get
1835
    - to get metadata of a container:     /file metadata get <container>
1836
    - to get metadata of an object:       /file metadata get <container>:<path>
1837
    """
1838

    
1839
    @errors.generic.all
1840
    @errors.pithos.connection
1841
    @errors.pithos.container
1842
    @errors.pithos.object_path
1843
    def _run(self, metakey):
1844
        if self.container is None:
1845
            r = self.client.del_account_meta(metakey)
1846
        elif self.path is None:
1847
            r = self.client.del_container_meta(metakey)
1848
        else:
1849
            r = self.client.del_object_meta(self.path, metakey)
1850
        self._optional_output(r)
1851

    
1852
    def main(self, metakey, container____path__=None):
1853
        super(self.__class__, self)._run(container____path__)
1854
        self._run(metakey)
1855

    
1856

    
1857
@command(pithos_cmds)
1858
class file_quota(_file_account_command, _optional_json):
1859
    """Get account quota"""
1860

    
1861
    arguments = dict(
1862
        in_bytes=FlagArgument('Show result in bytes', ('-b', '--bytes'))
1863
    )
1864

    
1865
    @errors.generic.all
1866
    @errors.pithos.connection
1867
    def _run(self):
1868

    
1869
        def pretty_print(output):
1870
            if not self['in_bytes']:
1871
                for k in output:
1872
                    output[k] = format_size(output[k])
1873
            pretty_dict(output, '-')
1874

    
1875
        self._print(self.client.get_account_quota(), pretty_print)
1876

    
1877
    def main(self, custom_uuid=None):
1878
        super(self.__class__, self)._run(custom_account=custom_uuid)
1879
        self._run()
1880

    
1881

    
1882
@command(pithos_cmds)
1883
class file_containerlimit(_pithos_init):
1884
    """Container size limit commands"""
1885

    
1886

    
1887
@command(pithos_cmds)
1888
class file_containerlimit_get(_file_container_command, _optional_json):
1889
    """Get container size limit"""
1890

    
1891
    arguments = dict(
1892
        in_bytes=FlagArgument('Show result in bytes', ('-b', '--bytes'))
1893
    )
1894

    
1895
    @errors.generic.all
1896
    @errors.pithos.container
1897
    def _run(self):
1898

    
1899
        def pretty_print(output):
1900
            if not self['in_bytes']:
1901
                for k, v in output.items():
1902
                    output[k] = 'unlimited' if '0' == v else format_size(v)
1903
            pretty_dict(output, '-')
1904

    
1905
        self._print(
1906
            self.client.get_container_limit(self.container), pretty_print)
1907

    
1908
    def main(self, container=None):
1909
        super(self.__class__, self)._run()
1910
        self.container = container
1911
        self._run()
1912

    
1913

    
1914
@command(pithos_cmds)
1915
class file_containerlimit_set(_file_account_command, _optional_output_cmd):
1916
    """Set new storage limit for a container
1917
    By default, the limit is set in bytes
1918
    Users may specify a different unit, e.g:
1919
    /file containerlimit set 2.3GB mycontainer
1920
    Valid units: B, KiB (1024 B), KB (1000 B), MiB, MB, GiB, GB, TiB, TB
1921
    To set container limit to "unlimited", use 0
1922
    """
1923

    
1924
    @errors.generic.all
1925
    def _calculate_limit(self, user_input):
1926
        limit = 0
1927
        try:
1928
            limit = int(user_input)
1929
        except ValueError:
1930
            index = 0
1931
            digits = [str(num) for num in range(0, 10)] + ['.']
1932
            while user_input[index] in digits:
1933
                index += 1
1934
            limit = user_input[:index]
1935
            format = user_input[index:]
1936
            try:
1937
                return to_bytes(limit, format)
1938
            except Exception as qe:
1939
                msg = 'Failed to convert %s to bytes' % user_input,
1940
                raiseCLIError(qe, msg, details=[
1941
                    'Syntax: containerlimit set <limit>[format] [container]',
1942
                    'e.g.: containerlimit set 2.3GB mycontainer',
1943
                    'Valid formats:',
1944
                    '(*1024): B, KiB, MiB, GiB, TiB',
1945
                    '(*1000): B, KB, MB, GB, TB'])
1946
        return limit
1947

    
1948
    @errors.generic.all
1949
    @errors.pithos.connection
1950
    @errors.pithos.container
1951
    def _run(self, limit):
1952
        if self.container:
1953
            self.client.container = self.container
1954
        self._optional_output(self.client.set_container_limit(limit))
1955

    
1956
    def main(self, limit, container=None):
1957
        super(self.__class__, self)._run()
1958
        limit = self._calculate_limit(limit)
1959
        self.container = container
1960
        self._run(limit)
1961

    
1962

    
1963
@command(pithos_cmds)
1964
class file_versioning(_pithos_init):
1965
    """Manage the versioning scheme of current pithos user account"""
1966

    
1967

    
1968
@command(pithos_cmds)
1969
class file_versioning_get(_file_account_command, _optional_json):
1970
    """Get  versioning for account or container"""
1971

    
1972
    @errors.generic.all
1973
    @errors.pithos.connection
1974
    @errors.pithos.container
1975
    def _run(self):
1976
        #if self.container:
1977
        #    r = self.client.get_container_versioning(self.container)
1978
        #else:
1979
        #    r = self.client.get_account_versioning()
1980
        self._print(
1981
            self.client.get_container_versioning(self.container) if (
1982
                self.container) else self.client.get_account_versioning(),
1983
            print_dict)
1984

    
1985
    def main(self, container=None):
1986
        super(self.__class__, self)._run()
1987
        self.container = container
1988
        self._run()
1989

    
1990

    
1991
@command(pithos_cmds)
1992
class file_versioning_set(_file_account_command, _optional_output_cmd):
1993
    """Set versioning mode (auto, none) for account or container"""
1994

    
1995
    def _check_versioning(self, versioning):
1996
        if versioning and versioning.lower() in ('auto', 'none'):
1997
            return versioning.lower()
1998
        raiseCLIError('Invalid versioning %s' % versioning, details=[
1999
            'Versioning can be auto or none'])
2000

    
2001
    @errors.generic.all
2002
    @errors.pithos.connection
2003
    @errors.pithos.container
2004
    def _run(self, versioning):
2005
        if self.container:
2006
            self.client.container = self.container
2007
            r = self.client.set_container_versioning(versioning)
2008
        else:
2009
            r = self.client.set_account_versioning(versioning)
2010
        self._optional_output(r)
2011

    
2012
    def main(self, versioning, container=None):
2013
        super(self.__class__, self)._run()
2014
        self._run(self._check_versioning(versioning))
2015

    
2016

    
2017
@command(pithos_cmds)
2018
class file_group(_pithos_init):
2019
    """Manage access groups and group members"""
2020

    
2021

    
2022
@command(pithos_cmds)
2023
class file_group_list(_file_account_command, _optional_json):
2024
    """list all groups and group members"""
2025

    
2026
    @errors.generic.all
2027
    @errors.pithos.connection
2028
    def _run(self):
2029
        self._print(self.client.get_account_group(), pretty_dict, delim='-')
2030

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

    
2035

    
2036
@command(pithos_cmds)
2037
class file_group_set(_file_account_command, _optional_output_cmd):
2038
    """Set a user group"""
2039

    
2040
    @errors.generic.all
2041
    @errors.pithos.connection
2042
    def _run(self, groupname, *users):
2043
        self._optional_output(self.client.set_account_group(groupname, users))
2044

    
2045
    def main(self, groupname, *users):
2046
        super(self.__class__, self)._run()
2047
        if users:
2048
            self._run(groupname, *users)
2049
        else:
2050
            raiseCLIError('No users to add in group %s' % groupname)
2051

    
2052

    
2053
@command(pithos_cmds)
2054
class file_group_delete(_file_account_command, _optional_output_cmd):
2055
    """Delete a user group"""
2056

    
2057
    @errors.generic.all
2058
    @errors.pithos.connection
2059
    def _run(self, groupname):
2060
        self._optional_output(self.client.del_account_group(groupname))
2061

    
2062
    def main(self, groupname):
2063
        super(self.__class__, self)._run()
2064
        self._run(groupname)
2065

    
2066

    
2067
@command(pithos_cmds)
2068
class file_sharers(_file_account_command, _optional_json):
2069
    """List the accounts that share objects with current user"""
2070

    
2071
    arguments = dict(
2072
        detail=FlagArgument('show detailed output', ('-l', '--details')),
2073
        marker=ValueArgument('show output greater then marker', '--marker')
2074
    )
2075

    
2076
    @errors.generic.all
2077
    @errors.pithos.connection
2078
    def _run(self):
2079
        accounts = self.client.get_sharing_accounts(marker=self['marker'])
2080
        if self['json_output'] or self['detail']:
2081
            self._print(accounts)
2082
        else:
2083
            self._print([acc['name'] for acc in accounts])
2084

    
2085
    def main(self):
2086
        super(self.__class__, self)._run()
2087
        self._run()
2088

    
2089

    
2090
def version_print(versions):
2091
    print_items([dict(id=vitem[0], created=strftime(
2092
        '%d-%m-%Y %H:%M:%S',
2093
        localtime(float(vitem[1])))) for vitem in versions])
2094

    
2095

    
2096
@command(pithos_cmds)
2097
class file_versions(_file_container_command, _optional_json):
2098
    """Get the list of object versions
2099
    Deleted objects may still have versions that can be used to restore it and
2100
    get information about its previous state.
2101
    The version number can be used in a number of other commands, like info,
2102
    copy, move, meta. See these commands for more information, e.g.
2103
    /file info -h
2104
    """
2105

    
2106
    @errors.generic.all
2107
    @errors.pithos.connection
2108
    @errors.pithos.container
2109
    @errors.pithos.object_path
2110
    def _run(self):
2111
        self._print(
2112
            self.client.get_object_versionlist(self.path), version_print)
2113

    
2114
    def main(self, container___path):
2115
        super(file_versions, self)._run(
2116
            container___path,
2117
            path_is_optional=False)
2118
        self._run()