Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / pithos.py @ 00336c85

History | View | Annotate | Download (76.4 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
41
from kamaki.cli.utils import (
42
    format_size, to_bytes, print_dict, print_items, pretty_keys,
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, _optional_output_cmd
48
from kamaki.clients.pithos import PithosClient, ClientError
49
from kamaki.clients.astakos import AstakosClient
50

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

    
54

    
55
# Argument functionality
56

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

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

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

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

    
77

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

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

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

    
119

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

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

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

    
140
# Command specs
141

    
142

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

    
146
    @staticmethod
147
    def _is_dir(remote_dict):
148
        return 'application/directory' == remote_dict.get(
149
            'content_type',
150
            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
        self.base_url = self.config.get('file', 'url')\
157
            or self.config.get('global', 'url')
158
        self._set_account()
159
        self.container = self.config.get('file', 'container')\
160
            or self.config.get('global', 'container')
161
        self.client = PithosClient(
162
            base_url=self.base_url,
163
            token=self.token,
164
            account=self.account,
165
            container=self.container)
166
        self._set_log_params()
167
        self._update_max_threads()
168

    
169
    def main(self):
170
        self._run()
171

    
172
    def _set_account(self):
173
        user = AstakosClient(self.config.get('user', 'url'), self.token)
174
        self.account = self['account'] or user.term('uuid')
175

    
176
        """Backwards compatibility"""
177
        self.account = self.account\
178
            or self.config.get('file', 'account')\
179
            or self.config.get('global', 'account')
180

    
181

    
182
class _file_account_command(_pithos_init):
183
    """Base class for account level storage commands"""
184

    
185
    def __init__(self, arguments={}):
186
        super(_file_account_command, self).__init__(arguments)
187
        self['account'] = ValueArgument(
188
            'Set user account (not permanent)',
189
            ('-A', '--account'))
190

    
191
    def _run(self, custom_account=None):
192
        super(_file_account_command, self)._run()
193
        if custom_account:
194
            self.client.account = custom_account
195
        elif self['account']:
196
            self.client.account = self['account']
197

    
198
    @errors.generic.all
199
    def main(self):
200
        self._run()
201

    
202

    
203
class _file_container_command(_file_account_command):
204
    """Base class for container level storage commands"""
205

    
206
    container = None
207
    path = None
208

    
209
    def __init__(self, arguments={}):
210
        super(_file_container_command, self).__init__(arguments)
211
        self['container'] = ValueArgument(
212
            'Set container to work with (temporary)',
213
            ('-C', '--container'))
214

    
215
    def extract_container_and_path(
216
            self,
217
            container_with_path,
218
            path_is_optional=True):
219
        """Contains all heuristics for deciding what should be used as
220
        container or path. Options are:
221
        * user string of the form container:path
222
        * self.container, self.path variables set by super constructor, or
223
        explicitly by the caller application
224
        Error handling is explicit as these error cases happen only here
225
        """
226
        try:
227
            assert isinstance(container_with_path, str)
228
        except AssertionError as err:
229
            if self['container'] and path_is_optional:
230
                self.container = self['container']
231
                self.client.container = self['container']
232
                return
233
            raiseCLIError(err)
234

    
235
        user_cont, sep, userpath = container_with_path.partition(':')
236

    
237
        if sep:
238
            if not user_cont:
239
                raiseCLIError(CLISyntaxError(
240
                    'Container is missing\n',
241
                    details=errors.pithos.container_howto))
242
            alt_cont = self['container']
243
            if alt_cont and user_cont != alt_cont:
244
                raiseCLIError(CLISyntaxError(
245
                    'Conflict: 2 containers (%s, %s)' % (user_cont, alt_cont),
246
                    details=errors.pithos.container_howto)
247
                )
248
            self.container = user_cont
249
            if not userpath:
250
                raiseCLIError(CLISyntaxError(
251
                    'Path is missing for object in container %s' % user_cont,
252
                    details=errors.pithos.container_howto)
253
                )
254
            self.path = userpath
255
        else:
256
            alt_cont = self['container'] or self.client.container
257
            if alt_cont:
258
                self.container = alt_cont
259
                self.path = user_cont
260
            elif path_is_optional:
261
                self.container = user_cont
262
                self.path = None
263
            else:
264
                self.container = user_cont
265
                raiseCLIError(CLISyntaxError(
266
                    'Both container and path are required',
267
                    details=errors.pithos.container_howto)
268
                )
269

    
270
    @errors.generic.all
271
    def _run(self, container_with_path=None, path_is_optional=True):
272
        super(_file_container_command, self)._run()
273
        if self['container']:
274
            self.client.container = self['container']
275
            if container_with_path:
276
                self.path = container_with_path
277
            elif not path_is_optional:
278
                raise CLISyntaxError(
279
                    'Both container and path are required',
280
                    details=errors.pithos.container_howto)
281
        elif container_with_path:
282
            self.extract_container_and_path(
283
                container_with_path,
284
                path_is_optional)
285
            self.client.container = self.container
286
        self.container = self.client.container
287

    
288
    def main(self, container_with_path=None, path_is_optional=True):
289
        self._run(container_with_path, path_is_optional)
290

    
291

    
292
@command(pithos_cmds)
293
class file_list(_file_container_command):
294
    """List containers, object trees or objects in a directory
295
    Use with:
296
    1 no parameters : containers in current account
297
    2. one parameter (container) or --container : contents of container
298
    3. <container>:<prefix> or --container=<container> <prefix>: objects in
299
    .   container starting with prefix
300
    """
301

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

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

    
368
    def print_containers(self, container_list):
369
        if self['json_output']:
370
            print_json(container_list)
371
            return
372
        limit = int(self['limit']) if self['limit'] > 0\
373
            else len(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 = '%s%s' % (prfx, bold(container['name']))
379
            if self['detail']:
380
                print(cname)
381
                pretty_c = container.copy()
382
                if 'bytes' in container:
383
                    pretty_c['bytes'] = '%s (%s)' % (container['bytes'], size)
384
                print_dict(pretty_keys(pretty_c), exclude=('name'))
385
                print
386
            else:
387
                if 'count' in container and 'bytes' in container:
388
                    print('%s (%s, %s objects)' % (
389
                        cname,
390
                        size,
391
                        container['count']))
392
                else:
393
                    print(cname)
394
            if self['more']:
395
                page_hold(index + 1, limit, len(container_list))
396

    
397
    @errors.generic.all
398
    @errors.pithos.connection
399
    @errors.pithos.object_path
400
    @errors.pithos.container
401
    def _run(self):
402
        if self.container is None:
403
            r = self.client.account_get(
404
                limit=False if self['more'] else self['limit'],
405
                marker=self['marker'],
406
                if_modified_since=self['if_modified_since'],
407
                if_unmodified_since=self['if_unmodified_since'],
408
                until=self['until'],
409
                show_only_shared=self['shared'])
410
            self.print_containers(r.json)
411
        else:
412
            prefix = self.path or self['prefix']
413
            r = self.client.container_get(
414
                limit=False if self['more'] else self['limit'],
415
                marker=self['marker'],
416
                prefix=prefix,
417
                delimiter=self['delimiter'],
418
                path=self['path'],
419
                if_modified_since=self['if_modified_since'],
420
                if_unmodified_since=self['if_unmodified_since'],
421
                until=self['until'],
422
                meta=self['meta'],
423
                show_only_shared=self['shared'])
424
            self.print_objects(r.json)
425

    
426
    def main(self, container____path__=None):
427
        super(self.__class__, self)._run(container____path__)
428
        self._run()
429

    
430

    
431
@command(pithos_cmds)
432
class file_mkdir(_file_container_command, _optional_output_cmd):
433
    """Create a directory"""
434

    
435
    __doc__ += '\n. '.join([
436
        'Kamaki hanldes directories the same way as OOS Storage and Pithos+:',
437
        'A   directory  is   an  object  with  type  "application/directory"',
438
        'An object with path  dir/name can exist even if  dir does not exist',
439
        'or even if dir  is  a non  directory  object.  Users can modify dir',
440
        'without affecting the dir/name object in any way.'])
441

    
442
    @errors.generic.all
443
    @errors.pithos.connection
444
    @errors.pithos.container
445
    def _run(self):
446
        self._optional_output(self.client.create_directory(self.path))
447

    
448
    def main(self, container___directory):
449
        super(self.__class__, self)._run(
450
            container___directory,
451
            path_is_optional=False)
452
        self._run()
453

    
454

    
455
@command(pithos_cmds)
456
class file_touch(_file_container_command, _optional_output_cmd):
457
    """Create an empty object (file)
458
    If object exists, this command will reset it to 0 length
459
    """
460

    
461
    arguments = dict(
462
        content_type=ValueArgument(
463
            'Set content type (default: application/octet-stream)',
464
            '--content-type',
465
            default='application/octet-stream')
466
    )
467

    
468
    @errors.generic.all
469
    @errors.pithos.connection
470
    @errors.pithos.container
471
    def _run(self):
472
        self._optional_output(
473
            self.client.create_object(self.path, self['content_type']))
474

    
475
    def main(self, container___path):
476
        super(file_touch, self)._run(
477
            container___path,
478
            path_is_optional=False)
479
        self._run()
480

    
481

    
482
@command(pithos_cmds)
483
class file_create(_file_container_command, _optional_output_cmd):
484
    """Create a container"""
485

    
486
    arguments = dict(
487
        versioning=ValueArgument(
488
            'set container versioning (auto/none)',
489
            '--versioning'),
490
        limit=IntArgument('set default container limit', '--limit'),
491
        meta=KeyValueArgument(
492
            'set container metadata (can be repeated)',
493
            '--meta')
494
    )
495

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

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

    
514

    
515
class _source_destination_command(_file_container_command):
516

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

    
528
    def __init__(self, arguments={}):
529
        self.arguments.update(arguments)
530
        super(_source_destination_command, self).__init__(self.arguments)
531

    
532
    def _run(self, source_container___path, path_is_optional=False):
533
        super(_source_destination_command, self)._run(
534
            source_container___path,
535
            path_is_optional)
536
        self.dst_client = PithosClient(
537
            base_url=self.client.base_url,
538
            token=self.client.token,
539
            account=self['destination_account'] or self.client.account)
540

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

    
564
    def _get_all(self, prefix):
565
        return self.client.container_get(prefix=prefix).json
566

    
567
    def _get_src_objects(self, src_path, source_version=None):
568
        """Get a list of the source objects to be called
569

570
        :param src_path: (str) source path
571

572
        :returns: (method, params) a method that returns a list when called
573
        or (object) if it is a single object
574
        """
575
        if src_path and src_path[-1] == '/':
576
            src_path = src_path[:-1]
577

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

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

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

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

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

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

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

    
658

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

    
676
    arguments = dict(
677
        destination_account=ValueArgument(
678
            'Account to copy to',
679
            ('-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',
686
            '--content-type'),
687
        recursive=FlagArgument(
688
            'copy directory and contents',
689
            ('-R', '--recursive')),
690
        prefix=FlagArgument(
691
            'Match objects prefixed with src path (feels like src_path*)',
692
            '--with-prefix',
693
            default=''),
694
        suffix=ValueArgument(
695
            'Suffix of source objects (feels like *suffix)',
696
            '--with-suffix',
697
            default=''),
698
        add_prefix=ValueArgument('Prefix targets', '--add-prefix', default=''),
699
        add_suffix=ValueArgument('Suffix targets', '--add-suffix', default=''),
700
        prefix_replace=ValueArgument(
701
            'Prefix of src to replace with dst path + add_prefix, if matched',
702
            '--prefix-to-replace',
703
            default=''),
704
        suffix_replace=ValueArgument(
705
            'Suffix of src to replace with add_suffix, if matched',
706
            '--suffix-to-replace',
707
            default=''),
708
        source_version=ValueArgument(
709
            'copy specific version',
710
            ('-S', '--source-version'))
711
    )
712

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

    
739
    def main(
740
            self, source_container___path,
741
            destination_container___path=None):
742
        super(file_copy, self)._run(
743
            source_container___path,
744
            path_is_optional=False)
745
        (dst_cont, dst_path) = self._dest_container_path(
746
            destination_container___path)
747
        self.dst_client.container = dst_cont or self.container
748
        self._run(dst_path=dst_path or '')
749

    
750

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

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

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

    
825
    def main(
826
            self, source_container___path,
827
            destination_container___path=None):
828
        super(self.__class__, self)._run(
829
            source_container___path,
830
            path_is_optional=False)
831
        (dst_cont, dst_path) = self._dest_container_path(
832
            destination_container___path)
833
        (dst_cont, dst_path) = self._dest_container_path(
834
            destination_container___path)
835
        self.dst_client.container = dst_cont or self.container
836
        self._run(dst_path=dst_path or '')
837

    
838

    
839
@command(pithos_cmds)
840
class file_append(_file_container_command, _optional_output_cmd):
841
    """Append local file to (existing) remote object
842
    The remote object should exist.
843
    If the remote object is a directory, it is transformed into a file.
844
    In the later case, objects under the directory remain intact.
845
    """
846

    
847
    arguments = dict(
848
        progress_bar=ProgressBarArgument(
849
            'do not show progress bar',
850
            ('-N', '--no-progress-bar'),
851
            default=False)
852
    )
853

    
854
    @errors.generic.all
855
    @errors.pithos.connection
856
    @errors.pithos.container
857
    @errors.pithos.object_path
858
    def _run(self, local_path):
859
        (progress_bar, upload_cb) = self._safe_progress_bar('Appending')
860
        try:
861
            f = open(local_path, 'rb')
862
            self._optional_output(
863
                self.client.append_object(self.path, f, upload_cb))
864
        except Exception:
865
            self._safe_progress_bar_finish(progress_bar)
866
            raise
867
        finally:
868
            self._safe_progress_bar_finish(progress_bar)
869

    
870
    def main(self, local_path, container___path):
871
        super(self.__class__, self)._run(
872
            container___path,
873
            path_is_optional=False)
874
        self._run(local_path)
875

    
876

    
877
@command(pithos_cmds)
878
class file_truncate(_file_container_command, _optional_output_cmd):
879
    """Truncate remote file up to a size (default is 0)"""
880

    
881
    @errors.generic.all
882
    @errors.pithos.connection
883
    @errors.pithos.container
884
    @errors.pithos.object_path
885
    @errors.pithos.object_size
886
    def _run(self, size=0):
887
        self._optional_output(self.client.truncate_object(self.path, size))
888

    
889
    def main(self, container___path, size=0):
890
        super(self.__class__, self)._run(container___path)
891
        self._run(size=size)
892

    
893

    
894
@command(pithos_cmds)
895
class file_overwrite(_file_container_command, _optional_output_cmd):
896
    """Overwrite part (from start to end) of a remote file
897
    overwrite local-path container 10 20
898
    .   will overwrite bytes from 10 to 20 of a remote file with the same name
899
    .   as local-path basename
900
    overwrite local-path container:path 10 20
901
    .   will overwrite as above, but the remote file is named path
902
    """
903

    
904
    arguments = dict(
905
        progress_bar=ProgressBarArgument(
906
            'do not show progress bar',
907
            ('-N', '--no-progress-bar'),
908
            default=False)
909
    )
910

    
911
    def _open_file(self, local_path, start):
912
        f = open(path.abspath(local_path), 'rb')
913
        f.seek(0, 2)
914
        f_size = f.tell()
915
        f.seek(start, 0)
916
        return (f, f_size)
917

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

    
938
    def main(self, local_path, container___path, start, end):
939
        super(self.__class__, self)._run(
940
            container___path,
941
            path_is_optional=None)
942
        self.path = self.path or path.basename(local_path)
943
        self._run(local_path=local_path, start=start, end=end)
944

    
945

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

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

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

    
992
    def main(self, container___path):
993
        super(self.__class__, self)._run(
994
            container___path,
995
            path_is_optional=False)
996
        self.run()
997

    
998

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

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

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

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

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

    
1173
    def main(self, local_path, container____path__=None):
1174
        super(self.__class__, self)._run(container____path__)
1175
        remote_path = self.path or path.basename(local_path)
1176
        self._run(local_path=local_path, remote_path=remote_path)
1177

    
1178

    
1179
@command(pithos_cmds)
1180
class file_cat(_file_container_command):
1181
    """Print remote file contents to console"""
1182

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

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

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

    
1221

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

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

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

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

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

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

    
1419

    
1420
@command(pithos_cmds)
1421
class file_hashmap(_file_container_command):
1422
    """Get the hash-map of an object"""
1423

    
1424
    arguments = dict(
1425
        if_match=ValueArgument('show output if ETags match', '--if-match'),
1426
        if_none_match=ValueArgument(
1427
            'show output if ETags match',
1428
            '--if-none-match'),
1429
        if_modified_since=DateArgument(
1430
            'show output modified since then',
1431
            '--if-modified-since'),
1432
        if_unmodified_since=DateArgument(
1433
            'show output unmodified since then',
1434
            '--if-unmodified-since'),
1435
        object_version=ValueArgument(
1436
            'get the specific version',
1437
            ('-O', '--object-version')),
1438
        json_output=FlagArgument('show headers in json', ('-j', '--json'))
1439
    )
1440

    
1441
    @errors.generic.all
1442
    @errors.pithos.connection
1443
    @errors.pithos.container
1444
    @errors.pithos.object_path
1445
    def _run(self):
1446
        data = self.client.get_object_hashmap(
1447
            self.path,
1448
            version=self['object_version'],
1449
            if_match=self['if_match'],
1450
            if_none_match=self['if_none_match'],
1451
            if_modified_since=self['if_modified_since'],
1452
            if_unmodified_since=self['if_unmodified_since'])
1453
        printer = print_json if self['json_output'] else print_dict
1454
        printer(data)
1455

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

    
1462

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

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

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

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

    
1521
    def main(self, container____path__=None):
1522
        super(self.__class__, self)._run(container____path__)
1523
        self._run()
1524

    
1525

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

    
1537
    arguments = dict(
1538
        yes=FlagArgument('Do not prompt for permission', '--yes'),
1539
        force=FlagArgument('purge even if not empty', ('-F', '--force'))
1540
    )
1541

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

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

    
1570

    
1571
@command(pithos_cmds)
1572
class file_publish(_file_container_command):
1573
    """Publish the object and print the public url"""
1574

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

    
1583
    def main(self, container___path):
1584
        super(self.__class__, self)._run(
1585
            container___path,
1586
            path_is_optional=False)
1587
        self._run()
1588

    
1589

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

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

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

    
1607

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

    
1616

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

    
1621
    @errors.generic.all
1622
    @errors.pithos.connection
1623
    @errors.pithos.container
1624
    @errors.pithos.object_path
1625
    def _run(self):
1626
        r = self.client.get_object_sharing(self.path)
1627
        print_dict(r)
1628

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

    
1635

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

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

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

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

    
1678

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

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

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

    
1698

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

    
1707
    arguments = dict(
1708
        object_version=ValueArgument(
1709
            'show specific version \ (applies only for objects)',
1710
            ('-O', '--object-version')),
1711
        json_output=FlagArgument('show headers in json', ('-j', '--json'))
1712
    )
1713

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

    
1730
    def main(self, container____path__=None):
1731
        super(self.__class__, self)._run(container____path__)
1732
        self._run()
1733

    
1734

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

    
1741

    
1742
@command(pithos_cmds)
1743
class file_metadata_get(_file_container_command):
1744
    """Get metadata for account, containers or objects"""
1745

    
1746
    arguments = dict(
1747
        detail=FlagArgument('show detailed output', ('-l', '--details')),
1748
        until=DateArgument('show metadata until then', '--until'),
1749
        object_version=ValueArgument(
1750
            'show specific version \ (applies only for objects)',
1751
            ('-O', '--object-version')),
1752
        json_output=FlagArgument('show headers in json', ('-j', '--json'))
1753
    )
1754

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

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

    
1797

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

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

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

    
1819

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

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

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

    
1845

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

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

    
1854
    @errors.generic.all
1855
    @errors.pithos.connection
1856
    def _run(self):
1857
        reply = self.client.get_account_quota()
1858
        if not self['in_bytes']:
1859
            for k in reply:
1860
                reply[k] = format_size(reply[k])
1861
        print_dict(pretty_keys(reply, '-'))
1862

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

    
1867

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

    
1872

    
1873
@command(pithos_cmds)
1874
class file_containerlimit_get(_file_container_command):
1875
    """Get container size limit"""
1876

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

    
1881
    @errors.generic.all
1882
    @errors.pithos.container
1883
    def _run(self):
1884
        reply = self.client.get_container_limit(self.container)
1885
        if not self['in_bytes']:
1886
            for k, v in reply.items():
1887
                reply[k] = 'unlimited' if '0' == v else format_size(v)
1888
        print_dict(pretty_keys(reply, '-'))
1889

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

    
1895

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

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

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

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

    
1944

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

    
1949

    
1950
@command(pithos_cmds)
1951
class file_versioning_get(_file_account_command):
1952
    """Get  versioning for account or container"""
1953

    
1954
    @errors.generic.all
1955
    @errors.pithos.connection
1956
    @errors.pithos.container
1957
    def _run(self):
1958
        if self.container:
1959
            r = self.client.get_container_versioning(self.container)
1960
        else:
1961
            r = self.client.get_account_versioning()
1962
        print_dict(r)
1963

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

    
1969

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

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

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

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

    
1995

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

    
2000

    
2001
@command(pithos_cmds)
2002
class file_group_get(_file_account_command):
2003
    """Get groups and group members"""
2004

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

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

    
2015

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

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

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

    
2032

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

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

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

    
2046

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

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

    
2056
    @errors.generic.all
2057
    @errors.pithos.connection
2058
    def _run(self):
2059
        accounts = self.client.get_sharing_accounts(marker=self['marker'])
2060
        if self['detail']:
2061
            print_items(accounts)
2062
        else:
2063
            print_items([acc['name'] for acc in accounts])
2064

    
2065
    def main(self):
2066
        super(self.__class__, self)._run()
2067
        self._run()
2068

    
2069

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

    
2080
    @errors.generic.all
2081
    @errors.pithos.connection
2082
    @errors.pithos.container
2083
    @errors.pithos.object_path
2084
    def _run(self):
2085
        versions = self.client.get_object_versionlist(self.path)
2086
        print_items([dict(id=vitem[0], created=strftime(
2087
            '%d-%m-%Y %H:%M:%S',
2088
            localtime(float(vitem[1])))) for vitem in versions])
2089

    
2090
    def main(self, container___path):
2091
        super(file_versions, self)._run(
2092
            container___path,
2093
            path_is_optional=False)
2094
        self._run()