Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / pithos.py @ 94bedc5b

History | View | Annotate | Download (75.6 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
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):
433
    """Create a directory"""
434

    
435
    arguments = dict(
436
        with_output=FlagArgument('show response headers', ('--with-output')),
437
        json_output=FlagArgument('show headers in json', ('-j', '--json'))
438
    )
439

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

    
447
    @errors.generic.all
448
    @errors.pithos.connection
449
    @errors.pithos.container
450
    def _run(self):
451
        r = self.client.create_directory(self.path)
452
        if self['json_output']:
453
            print_json(r)
454
        elif self['with_output']:
455
            print_dict(r)
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):
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
        with_output=FlagArgument('show response headers', ('--with-output')),
476
        json_output=FlagArgument('show headers in json', ('-j', '--json'))
477
    )
478

    
479
    @errors.generic.all
480
    @errors.pithos.connection
481
    @errors.pithos.container
482
    def _run(self):
483
        r = self.client.create_object(self.path, self['content_type'])
484
        if self['json_output']:
485
            print_json(r)
486
        elif self['with_output']:
487
            print_dict(r)
488

    
489
    def main(self, container___path):
490
        super(file_touch, self)._run(
491
            container___path,
492
            path_is_optional=False)
493
        self._run()
494

    
495

    
496
@command(pithos_cmds)
497
class file_create(_file_container_command):
498
    """Create a container"""
499

    
500
    arguments = dict(
501
        versioning=ValueArgument(
502
            'set container versioning (auto/none)',
503
            '--versioning'),
504
        limit=IntArgument('set default container limit', '--limit'),
505
        meta=KeyValueArgument(
506
            'set container metadata (can be repeated)',
507
            '--meta'),
508
        with_output=FlagArgument('show request headers', ('--with-output')),
509
        json_output=FlagArgument('show headers in json', ('-j', '--json'))
510
    )
511

    
512
    @errors.generic.all
513
    @errors.pithos.connection
514
    @errors.pithos.container
515
    def _run(self, container):
516
        r = self.client.create_container(
517
            container=container,
518
            sizelimit=self['limit'],
519
            versioning=self['versioning'],
520
            metadata=self['meta'])
521
        if self['json_output']:
522
            print_json(r)
523
        elif self['with_output']:
524
            print_dict(r)
525

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

    
534

    
535
class _source_destination_command(_file_container_command):
536

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

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

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

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

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

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

590
        :param src_path: (str) source path
591

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

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

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

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

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

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

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

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

    
678

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

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

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

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

    
769

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

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

    
821
    @errors.generic.all
822
    @errors.pithos.connection
823
    @errors.pithos.container
824
    def _run(self, dst_path):
825
        no_source_object = True
826
        src_account = self.client.account if (
827
            self['destination_account']) else None
828
        for src_obj, dst_obj in self.src_dst_pairs(dst_path):
829
            no_source_object = False
830
            self.dst_client.move_object(
831
                src_container=self.container,
832
                src_object=src_obj,
833
                dst_container=self.dst_client.container,
834
                dst_object=dst_obj,
835
                source_account=src_account,
836
                source_version=self['source_version'],
837
                public=self['public'],
838
                content_type=self['content_type'])
839
        if no_source_object:
840
            raiseCLIError('No object %s in container %s' % (
841
                self.path,
842
                self.container))
843

    
844
    def main(
845
            self, source_container___path,
846
            destination_container___path=None):
847
        super(self.__class__, self)._run(
848
            source_container___path,
849
            path_is_optional=False)
850
        (dst_cont, dst_path) = self._dest_container_path(
851
            destination_container___path)
852
        (dst_cont, dst_path) = self._dest_container_path(
853
            destination_container___path)
854
        self.dst_client.container = dst_cont or self.container
855
        self._run(dst_path=dst_path or '')
856

    
857

    
858
@command(pithos_cmds)
859
class file_append(_file_container_command):
860
    """Append local file to (existing) remote object
861
    The remote object should exist.
862
    If the remote object is a directory, it is transformed into a file.
863
    In the later case, objects under the directory remain intact.
864
    """
865

    
866
    arguments = dict(
867
        progress_bar=ProgressBarArgument(
868
            'do not show progress bar',
869
            ('-N', '--no-progress-bar'),
870
            default=False)
871
    )
872

    
873
    @errors.generic.all
874
    @errors.pithos.connection
875
    @errors.pithos.container
876
    @errors.pithos.object_path
877
    def _run(self, local_path):
878
        (progress_bar, upload_cb) = self._safe_progress_bar('Appending')
879
        try:
880
            f = open(local_path, 'rb')
881
            self.client.append_object(self.path, f, upload_cb)
882
        except Exception:
883
            self._safe_progress_bar_finish(progress_bar)
884
            raise
885
        finally:
886
            self._safe_progress_bar_finish(progress_bar)
887

    
888
    def main(self, local_path, container___path):
889
        super(self.__class__, self)._run(
890
            container___path,
891
            path_is_optional=False)
892
        self._run(local_path)
893

    
894

    
895
@command(pithos_cmds)
896
class file_truncate(_file_container_command):
897
    """Truncate remote file up to a size (default is 0)"""
898

    
899
    @errors.generic.all
900
    @errors.pithos.connection
901
    @errors.pithos.container
902
    @errors.pithos.object_path
903
    @errors.pithos.object_size
904
    def _run(self, size=0):
905
        self.client.truncate_object(self.path, size)
906

    
907
    def main(self, container___path, size=0):
908
        super(self.__class__, self)._run(container___path)
909
        self._run(size=size)
910

    
911

    
912
@command(pithos_cmds)
913
class file_overwrite(_file_container_command):
914
    """Overwrite part (from start to end) of a remote file
915
    overwrite local-path container 10 20
916
    .   will overwrite bytes from 10 to 20 of a remote file with the same name
917
    .   as local-path basename
918
    overwrite local-path container:path 10 20
919
    .   will overwrite as above, but the remote file is named path
920
    """
921

    
922
    arguments = dict(
923
        progress_bar=ProgressBarArgument(
924
            'do not show progress bar',
925
            ('-N', '--no-progress-bar'),
926
            default=False)
927
    )
928

    
929
    def _open_file(self, local_path, start):
930
        f = open(path.abspath(local_path), 'rb')
931
        f.seek(0, 2)
932
        f_size = f.tell()
933
        f.seek(start, 0)
934
        return (f, f_size)
935

    
936
    @errors.generic.all
937
    @errors.pithos.connection
938
    @errors.pithos.container
939
    @errors.pithos.object_path
940
    @errors.pithos.object_size
941
    def _run(self, local_path, start, end):
942
        (start, end) = (int(start), int(end))
943
        (f, f_size) = self._open_file(local_path, start)
944
        (progress_bar, upload_cb) = self._safe_progress_bar(
945
            'Overwrite %s bytes' % (end - start))
946
        try:
947
            self.client.overwrite_object(
948
                obj=self.path,
949
                start=start,
950
                end=end,
951
                source_file=f,
952
                upload_cb=upload_cb)
953
        except Exception:
954
            self._safe_progress_bar_finish(progress_bar)
955
            raise
956
        finally:
957
            self._safe_progress_bar_finish(progress_bar)
958

    
959
    def main(self, local_path, container___path, start, end):
960
        super(self.__class__, self)._run(
961
            container___path,
962
            path_is_optional=None)
963
        self.path = self.path or path.basename(local_path)
964
        self._run(local_path=local_path, start=start, end=end)
965

    
966

    
967
@command(pithos_cmds)
968
class file_manifest(_file_container_command):
969
    """Create a remote file of uploaded parts by manifestation
970
    Remains functional for compatibility with OOS Storage. Users are advised
971
    to use the upload command instead.
972
    Manifestation is a compliant process for uploading large files. The files
973
    have to be chunked in smalled files and uploaded as <prefix><increment>
974
    where increment is 1, 2, ...
975
    Finally, the manifest command glues partial files together in one file
976
    named <prefix>
977
    The upload command is faster, easier and more intuitive than manifest
978
    """
979

    
980
    arguments = dict(
981
        etag=ValueArgument('check written data', '--etag'),
982
        content_encoding=ValueArgument(
983
            'set MIME content type',
984
            '--content-encoding'),
985
        content_disposition=ValueArgument(
986
            'the presentation style of the object',
987
            '--content-disposition'),
988
        content_type=ValueArgument(
989
            'specify content type',
990
            '--content-type',
991
            default='application/octet-stream'),
992
        sharing=SharingArgument(
993
            '\n'.join([
994
                'define object sharing policy',
995
                '    ( "read=user1,grp1,user2,... write=user1,grp2,..." )']),
996
            '--sharing'),
997
        public=FlagArgument('make object publicly accessible', '--public')
998
    )
999

    
1000
    @errors.generic.all
1001
    @errors.pithos.connection
1002
    @errors.pithos.container
1003
    @errors.pithos.object_path
1004
    def _run(self):
1005
        self.client.create_object_by_manifestation(
1006
            self.path,
1007
            content_encoding=self['content_encoding'],
1008
            content_disposition=self['content_disposition'],
1009
            content_type=self['content_type'],
1010
            sharing=self['sharing'],
1011
            public=self['public'])
1012

    
1013
    def main(self, container___path):
1014
        super(self.__class__, self)._run(
1015
            container___path,
1016
            path_is_optional=False)
1017
        self.run()
1018

    
1019

    
1020
@command(pithos_cmds)
1021
class file_upload(_file_container_command):
1022
    """Upload a file"""
1023

    
1024
    arguments = dict(
1025
        use_hashes=FlagArgument(
1026
            'provide hashmap file instead of data',
1027
            '--use-hashes'),
1028
        etag=ValueArgument('check written data', '--etag'),
1029
        unchunked=FlagArgument('avoid chunked transfer mode', '--unchunked'),
1030
        content_encoding=ValueArgument(
1031
            'set MIME content type',
1032
            '--content-encoding'),
1033
        content_disposition=ValueArgument(
1034
            'specify objects presentation style',
1035
            '--content-disposition'),
1036
        content_type=ValueArgument('specify content type', '--content-type'),
1037
        sharing=SharingArgument(
1038
            help='\n'.join([
1039
                'define sharing object policy',
1040
                '( "read=user1,grp1,user2,... write=user1,grp2,... )']),
1041
            parsed_name='--sharing'),
1042
        public=FlagArgument('make object publicly accessible', '--public'),
1043
        poolsize=IntArgument('set pool size', '--with-pool-size'),
1044
        progress_bar=ProgressBarArgument(
1045
            'do not show progress bar',
1046
            ('-N', '--no-progress-bar'),
1047
            default=False),
1048
        overwrite=FlagArgument('Force (over)write', ('-f', '--force')),
1049
        recursive=FlagArgument(
1050
            'Recursively upload directory *contents* + subdirectories',
1051
            ('-R', '--recursive')),
1052
        details=FlagArgument(
1053
            'Show a detailed list of uploaded objects at the end',
1054
            ('-l', '--details'))
1055
    )
1056

    
1057
    def _check_container_limit(self, path):
1058
        cl_dict = self.client.get_container_limit()
1059
        container_limit = int(cl_dict['x-container-policy-quota'])
1060
        r = self.client.container_get()
1061
        used_bytes = sum(int(o['bytes']) for o in r.json)
1062
        path_size = get_path_size(path)
1063
        if container_limit and path_size > (container_limit - used_bytes):
1064
            raiseCLIError(
1065
                'Container(%s) (limit(%s) - used(%s)) < size(%s) of %s' % (
1066
                    self.client.container,
1067
                    format_size(container_limit),
1068
                    format_size(used_bytes),
1069
                    format_size(path_size),
1070
                    path),
1071
                importance=1, details=[
1072
                    'Check accound limit: /file quota',
1073
                    'Check container limit:',
1074
                    '\t/file containerlimit get %s' % self.client.container,
1075
                    'Increase container limit:',
1076
                    '\t/file containerlimit set <new limit> %s' % (
1077
                        self.client.container)])
1078

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

    
1143
    @errors.generic.all
1144
    @errors.pithos.connection
1145
    @errors.pithos.container
1146
    @errors.pithos.object_path
1147
    @errors.pithos.local_path
1148
    def _run(self, local_path, remote_path):
1149
        poolsize = self['poolsize']
1150
        if poolsize > 0:
1151
            self.client.MAX_THREADS = int(poolsize)
1152
        params = dict(
1153
            content_encoding=self['content_encoding'],
1154
            content_type=self['content_type'],
1155
            content_disposition=self['content_disposition'],
1156
            sharing=self['sharing'],
1157
            public=self['public'])
1158
        uploaded = []
1159
        container_info_cache = dict()
1160
        for f, rpath in self._path_pairs(local_path, remote_path):
1161
            print('%s --> %s:%s' % (f.name, self.client.container, rpath))
1162
            if self['unchunked']:
1163
                r = self.client.upload_object_unchunked(
1164
                    rpath, f,
1165
                    etag=self['etag'], withHashFile=self['use_hashes'],
1166
                    **params)
1167
                if self['details']:
1168
                    r['name'] = '%s: %s' % (self.client.container, rpath)
1169
                    uploaded.append(r)
1170
            else:
1171
                try:
1172
                    (progress_bar, upload_cb) = self._safe_progress_bar(
1173
                        'Uploading %s' % f.name.split(path.sep)[-1])
1174
                    if progress_bar:
1175
                        hash_bar = progress_bar.clone()
1176
                        hash_cb = hash_bar.get_generator(
1177
                            'Calculating block hashes')
1178
                    else:
1179
                        hash_cb = None
1180
                    r = self.client.upload_object(
1181
                        rpath, f,
1182
                        hash_cb=hash_cb,
1183
                        upload_cb=upload_cb,
1184
                        container_info_cache=container_info_cache,
1185
                        **params)
1186
                    if self['details']:
1187
                        r['name'] = '%s: %s' % (self.client.container, rpath)
1188
                        uploaded.append(r)
1189
                except Exception:
1190
                    self._safe_progress_bar_finish(progress_bar)
1191
                    raise
1192
                finally:
1193
                    self._safe_progress_bar_finish(progress_bar)
1194
        if self['details']:
1195
            print_items(uploaded)
1196
        else:
1197
            print('Upload completed')
1198

    
1199
    def main(self, local_path, container____path__=None):
1200
        super(self.__class__, self)._run(container____path__)
1201
        remote_path = self.path or path.basename(local_path)
1202
        self._run(local_path=local_path, remote_path=remote_path)
1203

    
1204

    
1205
@command(pithos_cmds)
1206
class file_cat(_file_container_command):
1207
    """Print remote file contents to console"""
1208

    
1209
    arguments = dict(
1210
        range=RangeArgument('show range of data', '--range'),
1211
        if_match=ValueArgument('show output if ETags match', '--if-match'),
1212
        if_none_match=ValueArgument(
1213
            'show output if ETags match',
1214
            '--if-none-match'),
1215
        if_modified_since=DateArgument(
1216
            'show output modified since then',
1217
            '--if-modified-since'),
1218
        if_unmodified_since=DateArgument(
1219
            'show output unmodified since then',
1220
            '--if-unmodified-since'),
1221
        object_version=ValueArgument(
1222
            'get the specific version',
1223
            ('-j', '--object-version'))
1224
    )
1225

    
1226
    @errors.generic.all
1227
    @errors.pithos.connection
1228
    @errors.pithos.container
1229
    @errors.pithos.object_path
1230
    def _run(self):
1231
        self.client.download_object(
1232
            self.path,
1233
            stdout,
1234
            range_str=self['range'],
1235
            version=self['object_version'],
1236
            if_match=self['if_match'],
1237
            if_none_match=self['if_none_match'],
1238
            if_modified_since=self['if_modified_since'],
1239
            if_unmodified_since=self['if_unmodified_since'])
1240

    
1241
    def main(self, container___path):
1242
        super(self.__class__, self)._run(
1243
            container___path,
1244
            path_is_optional=False)
1245
        self._run()
1246

    
1247

    
1248
@command(pithos_cmds)
1249
class file_download(_file_container_command):
1250
    """Download remote object as local file
1251
    If local destination is a directory:
1252
    *   download <container>:<path> <local dir> -R
1253
    will download all files on <container> prefixed as <path>,
1254
    to <local dir>/<full path>
1255
    *   download <container>:<path> <local dir> --exact-match
1256
    will download only one file, exactly matching <path>
1257
    ATTENTION: to download cont:dir1/dir2/file there must exist objects
1258
    cont:dir1 and cont:dir1/dir2 of type application/directory
1259
    To create directory objects, use /file mkdir
1260
    """
1261

    
1262
    arguments = dict(
1263
        resume=FlagArgument('Resume instead of overwrite', ('-r', '--resume')),
1264
        range=RangeArgument('show range of data', '--range'),
1265
        if_match=ValueArgument('show output if ETags match', '--if-match'),
1266
        if_none_match=ValueArgument(
1267
            'show output if ETags match',
1268
            '--if-none-match'),
1269
        if_modified_since=DateArgument(
1270
            'show output modified since then',
1271
            '--if-modified-since'),
1272
        if_unmodified_since=DateArgument(
1273
            'show output unmodified since then',
1274
            '--if-unmodified-since'),
1275
        object_version=ValueArgument(
1276
            'get the specific version',
1277
            ('-j', '--object-version')),
1278
        poolsize=IntArgument('set pool size', '--with-pool-size'),
1279
        progress_bar=ProgressBarArgument(
1280
            'do not show progress bar',
1281
            ('-N', '--no-progress-bar'),
1282
            default=False),
1283
        recursive=FlagArgument(
1284
            'Download a remote path and all its contents',
1285
            ('-R', '--recursive'))
1286
    )
1287

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

    
1344
        lprefix = path.abspath(local_path or path.curdir)
1345
        if path.isdir(lprefix):
1346
            for rpath, remote_is_dir in remotes:
1347
                lpath = '/%s/%s' % (lprefix.strip('/'), rpath.strip('/'))
1348
                if remote_is_dir:
1349
                    if path.exists(lpath) and path.isdir(lpath):
1350
                        continue
1351
                    makedirs(lpath)
1352
                elif path.exists(lpath):
1353
                    if not self['resume']:
1354
                        print('File %s exists, aborting...' % lpath)
1355
                        continue
1356
                    with open(lpath, 'rwb+') as f:
1357
                        yield (f, rpath)
1358
                else:
1359
                    with open(lpath, 'wb+') as f:
1360
                        yield (f, rpath)
1361
        elif path.exists(lprefix):
1362
            if len(remotes) > 1:
1363
                raiseCLIError(
1364
                    '%s remote objects cannot be merged in local file %s' % (
1365
                        len(remotes),
1366
                        local_path),
1367
                    details=[
1368
                        'To download multiple objects, local path should be',
1369
                        'a directory, or use download without a local path'])
1370
            (rpath, remote_is_dir) = remotes[0]
1371
            if remote_is_dir:
1372
                raiseCLIError(
1373
                    'Remote directory %s should not replace local file %s' % (
1374
                        rpath,
1375
                        local_path))
1376
            if self['resume']:
1377
                with open(lprefix, 'rwb+') as f:
1378
                    yield (f, rpath)
1379
            else:
1380
                raiseCLIError(
1381
                    'Local file %s already exist' % local_path,
1382
                    details=['Try --resume to overwrite it'])
1383
        else:
1384
            if len(remotes) > 1 or remotes[0][1]:
1385
                raiseCLIError(
1386
                    'Local directory %s does not exist' % local_path)
1387
            with open(lprefix, 'wb+') as f:
1388
                yield (f, remotes[0][0])
1389

    
1390
    @errors.generic.all
1391
    @errors.pithos.connection
1392
    @errors.pithos.container
1393
    @errors.pithos.object_path
1394
    @errors.pithos.local_path
1395
    def _run(self, local_path):
1396
        #outputs = self._outputs(local_path)
1397
        poolsize = self['poolsize']
1398
        if poolsize:
1399
            self.client.MAX_THREADS = int(poolsize)
1400
        progress_bar = None
1401
        try:
1402
            for f, rpath in self._outputs(local_path):
1403
                (
1404
                    progress_bar,
1405
                    download_cb) = self._safe_progress_bar(
1406
                        'Download %s' % rpath)
1407
                self.client.download_object(
1408
                    rpath,
1409
                    f,
1410
                    download_cb=download_cb,
1411
                    range_str=self['range'],
1412
                    version=self['object_version'],
1413
                    if_match=self['if_match'],
1414
                    resume=self['resume'],
1415
                    if_none_match=self['if_none_match'],
1416
                    if_modified_since=self['if_modified_since'],
1417
                    if_unmodified_since=self['if_unmodified_since'])
1418
        except KeyboardInterrupt:
1419
            from threading import activeCount, enumerate as activethreads
1420
            timeout = 0.5
1421
            while activeCount() > 1:
1422
                stdout.write('\nCancel %s threads: ' % (activeCount() - 1))
1423
                stdout.flush()
1424
                for thread in activethreads():
1425
                    try:
1426
                        thread.join(timeout)
1427
                        stdout.write('.' if thread.isAlive() else '*')
1428
                    except RuntimeError:
1429
                        continue
1430
                    finally:
1431
                        stdout.flush()
1432
                        timeout += 0.1
1433

    
1434
            print('\nDownload canceled by user')
1435
            if local_path is not None:
1436
                print('to resume, re-run with --resume')
1437
        except Exception:
1438
            self._safe_progress_bar_finish(progress_bar)
1439
            raise
1440
        finally:
1441
            self._safe_progress_bar_finish(progress_bar)
1442

    
1443
    def main(self, container___path, local_path=None):
1444
        super(self.__class__, self)._run(container___path)
1445
        self._run(local_path=local_path)
1446

    
1447

    
1448
@command(pithos_cmds)
1449
class file_hashmap(_file_container_command):
1450
    """Get the hash-map of an object"""
1451

    
1452
    arguments = dict(
1453
        if_match=ValueArgument('show output if ETags match', '--if-match'),
1454
        if_none_match=ValueArgument(
1455
            'show output if ETags match',
1456
            '--if-none-match'),
1457
        if_modified_since=DateArgument(
1458
            'show output modified since then',
1459
            '--if-modified-since'),
1460
        if_unmodified_since=DateArgument(
1461
            'show output unmodified since then',
1462
            '--if-unmodified-since'),
1463
        object_version=ValueArgument(
1464
            'get the specific version',
1465
            ('-j', '--object-version'))
1466
    )
1467

    
1468
    @errors.generic.all
1469
    @errors.pithos.connection
1470
    @errors.pithos.container
1471
    @errors.pithos.object_path
1472
    def _run(self):
1473
        data = self.client.get_object_hashmap(
1474
            self.path,
1475
            version=self['object_version'],
1476
            if_match=self['if_match'],
1477
            if_none_match=self['if_none_match'],
1478
            if_modified_since=self['if_modified_since'],
1479
            if_unmodified_since=self['if_unmodified_since'])
1480
        print_dict(data)
1481

    
1482
    def main(self, container___path):
1483
        super(self.__class__, self)._run(
1484
            container___path,
1485
            path_is_optional=False)
1486
        self._run()
1487

    
1488

    
1489
@command(pithos_cmds)
1490
class file_delete(_file_container_command):
1491
    """Delete a container [or an object]
1492
    How to delete a non-empty container:
1493
    - empty the container:  /file delete -R <container>
1494
    - delete it:            /file delete <container>
1495
    .
1496
    Semantics of directory deletion:
1497
    .a preserve the contents: /file delete <container>:<directory>
1498
    .    objects of the form dir/filename can exist with a dir object
1499
    .b delete contents:       /file delete -R <container>:<directory>
1500
    .    all dir/* objects are affected, even if dir does not exist
1501
    .
1502
    To restore a deleted object OBJ in a container CONT:
1503
    - get object versions: /file versions CONT:OBJ
1504
    .   and choose the version to be restored
1505
    - restore the object:  /file copy --source-version=<version> CONT:OBJ OBJ
1506
    """
1507

    
1508
    arguments = dict(
1509
        until=DateArgument('remove history until that date', '--until'),
1510
        yes=FlagArgument('Do not prompt for permission', '--yes'),
1511
        recursive=FlagArgument(
1512
            'empty dir or container and delete (if dir)',
1513
            ('-R', '--recursive'))
1514
    )
1515

    
1516
    def __init__(self, arguments={}):
1517
        super(self.__class__, self).__init__(arguments)
1518
        self['delimiter'] = DelimiterArgument(
1519
            self,
1520
            parsed_name='--delimiter',
1521
            help='delete objects prefixed with <object><delimiter>')
1522

    
1523
    @errors.generic.all
1524
    @errors.pithos.connection
1525
    @errors.pithos.container
1526
    @errors.pithos.object_path
1527
    def _run(self):
1528
        if self.path:
1529
            if self['yes'] or ask_user(
1530
                    'Delete %s:%s ?' % (self.container, self.path)):
1531
                self.client.del_object(
1532
                    self.path,
1533
                    until=self['until'],
1534
                    delimiter=self['delimiter'])
1535
            else:
1536
                print('Aborted')
1537
        else:
1538
            if self['recursive']:
1539
                ask_msg = 'Delete container contents'
1540
            else:
1541
                ask_msg = 'Delete container'
1542
            if self['yes'] or ask_user('%s %s ?' % (ask_msg, self.container)):
1543
                self.client.del_container(
1544
                    until=self['until'],
1545
                    delimiter=self['delimiter'])
1546
            else:
1547
                print('Aborted')
1548

    
1549
    def main(self, container____path__=None):
1550
        super(self.__class__, self)._run(container____path__)
1551
        self._run()
1552

    
1553

    
1554
@command(pithos_cmds)
1555
class file_purge(_file_container_command):
1556
    """Delete a container and release related data blocks
1557
    Non-empty containers can not purged.
1558
    To purge a container with content:
1559
    .   /file delete -R <container>
1560
    .      objects are deleted, but data blocks remain on server
1561
    .   /file purge <container>
1562
    .      container and data blocks are released and deleted
1563
    """
1564

    
1565
    arguments = dict(
1566
        yes=FlagArgument('Do not prompt for permission', '--yes'),
1567
        force=FlagArgument('purge even if not empty', ('-F', '--force'))
1568
    )
1569

    
1570
    @errors.generic.all
1571
    @errors.pithos.connection
1572
    @errors.pithos.container
1573
    def _run(self):
1574
        if self['yes'] or ask_user('Purge container %s?' % self.container):
1575
            try:
1576
                self.client.purge_container()
1577
            except ClientError as ce:
1578
                if ce.status in (409,):
1579
                    if self['force']:
1580
                        self.client.del_container(delimiter='/')
1581
                        self.client.purge_container()
1582
                    else:
1583
                        raiseCLIError(ce, details=['Try -F to force-purge'])
1584
                else:
1585
                    raise
1586
        else:
1587
            print('Aborted')
1588

    
1589
    def main(self, container=None):
1590
        super(self.__class__, self)._run(container)
1591
        if container and self.container != container:
1592
            raiseCLIError('Invalid container name %s' % container, details=[
1593
                'Did you mean "%s" ?' % self.container,
1594
                'Use --container for names containing :'])
1595
        self._run()
1596

    
1597

    
1598
@command(pithos_cmds)
1599
class file_publish(_file_container_command):
1600
    """Publish the object and print the public url"""
1601

    
1602
    @errors.generic.all
1603
    @errors.pithos.connection
1604
    @errors.pithos.container
1605
    @errors.pithos.object_path
1606
    def _run(self):
1607
        url = self.client.publish_object(self.path)
1608
        print(url)
1609

    
1610
    def main(self, container___path):
1611
        super(self.__class__, self)._run(
1612
            container___path,
1613
            path_is_optional=False)
1614
        self._run()
1615

    
1616

    
1617
@command(pithos_cmds)
1618
class file_unpublish(_file_container_command):
1619
    """Unpublish 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
            self.client.unpublish_object(self.path)
1627

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

    
1634

    
1635
@command(pithos_cmds)
1636
class file_permissions(_file_container_command):
1637
    """Get read and write permissions of an object
1638
    Permissions are lists of users and user groups. There is read and write
1639
    permissions. Users and groups with write permission have also read
1640
    permission.
1641
    """
1642

    
1643
    @errors.generic.all
1644
    @errors.pithos.connection
1645
    @errors.pithos.container
1646
    @errors.pithos.object_path
1647
    def _run(self):
1648
        r = self.client.get_object_sharing(self.path)
1649
        print_dict(r)
1650

    
1651
    def main(self, container___path):
1652
        super(self.__class__, self)._run(
1653
            container___path,
1654
            path_is_optional=False)
1655
        self._run()
1656

    
1657

    
1658
@command(pithos_cmds)
1659
class file_setpermissions(_file_container_command):
1660
    """Set permissions for an object
1661
    New permissions overwrite existing permissions.
1662
    Permission format:
1663
    -   read=<username>[,usergroup[,...]]
1664
    -   write=<username>[,usegroup[,...]]
1665
    E.g. to give read permissions for file F to users A and B and write for C:
1666
    .       /file setpermissions F read=A,B write=C
1667
    """
1668

    
1669
    @errors.generic.all
1670
    def format_permition_dict(self, permissions):
1671
        read = False
1672
        write = False
1673
        for perms in permissions:
1674
            splstr = perms.split('=')
1675
            if 'read' == splstr[0]:
1676
                read = [ug.strip() for ug in splstr[1].split(',')]
1677
            elif 'write' == splstr[0]:
1678
                write = [ug.strip() for ug in splstr[1].split(',')]
1679
            else:
1680
                msg = 'Usage:\tread=<groups,users> write=<groups,users>'
1681
                raiseCLIError(None, msg)
1682
        return (read, write)
1683

    
1684
    @errors.generic.all
1685
    @errors.pithos.connection
1686
    @errors.pithos.container
1687
    @errors.pithos.object_path
1688
    def _run(self, read, write):
1689
        self.client.set_object_sharing(
1690
            self.path,
1691
            read_permition=read,
1692
            write_permition=write)
1693

    
1694
    def main(self, container___path, *permissions):
1695
        super(self.__class__, self)._run(
1696
            container___path,
1697
            path_is_optional=False)
1698
        (read, write) = self.format_permition_dict(permissions)
1699
        self._run(read, write)
1700

    
1701

    
1702
@command(pithos_cmds)
1703
class file_delpermissions(_file_container_command):
1704
    """Delete all permissions set on object
1705
    To modify permissions, use /file setpermssions
1706
    """
1707

    
1708
    @errors.generic.all
1709
    @errors.pithos.connection
1710
    @errors.pithos.container
1711
    @errors.pithos.object_path
1712
    def _run(self):
1713
        self.client.del_object_sharing(self.path)
1714

    
1715
    def main(self, container___path):
1716
        super(self.__class__, self)._run(
1717
            container___path,
1718
            path_is_optional=False)
1719
        self._run()
1720

    
1721

    
1722
@command(pithos_cmds)
1723
class file_info(_file_container_command):
1724
    """Get detailed information for user account, containers or objects
1725
    to get account info:    /file info
1726
    to get container info:  /file info <container>
1727
    to get object info:     /file info <container>:<path>
1728
    """
1729

    
1730
    arguments = dict(
1731
        object_version=ValueArgument(
1732
            'show specific version \ (applies only for objects)',
1733
            ('-j', '--object-version'))
1734
    )
1735

    
1736
    @errors.generic.all
1737
    @errors.pithos.connection
1738
    @errors.pithos.container
1739
    @errors.pithos.object_path
1740
    def _run(self):
1741
        if self.container is None:
1742
            r = self.client.get_account_info()
1743
        elif self.path is None:
1744
            r = self.client.get_container_info(self.container)
1745
        else:
1746
            r = self.client.get_object_info(
1747
                self.path,
1748
                version=self['object_version'])
1749
        print_dict(r)
1750

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

    
1755

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

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

    
1768
    @errors.generic.all
1769
    @errors.pithos.connection
1770
    @errors.pithos.container
1771
    @errors.pithos.object_path
1772
    def _run(self):
1773
        until = self['until']
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
            if r:
1781
                print(bold(self.client.account))
1782
        elif self.path is None:
1783
            if self['detail']:
1784
                r = self.client.get_container_info(until=until)
1785
            else:
1786
                cmeta = self.client.get_container_meta(until=until)
1787
                ometa = self.client.get_container_object_meta(until=until)
1788
                r = {}
1789
                if cmeta:
1790
                    r['container-meta'] = pretty_keys(cmeta, '-')
1791
                if ometa:
1792
                    r['object-meta'] = pretty_keys(ometa, '-')
1793
        else:
1794
            if self['detail']:
1795
                r = self.client.get_object_info(
1796
                    self.path,
1797
                    version=self['object_version'])
1798
            else:
1799
                r = self.client.get_object_meta(
1800
                    self.path,
1801
                    version=self['object_version'])
1802
            if r:
1803
                r = pretty_keys(pretty_keys(r, '-'))
1804
        if r:
1805
            print_dict(r)
1806

    
1807
    def main(self, container____path__=None):
1808
        super(self.__class__, self)._run(container____path__)
1809
        self._run()
1810

    
1811

    
1812
@command(pithos_cmds)
1813
class file_setmeta(_file_container_command):
1814
    """Set a piece of metadata for account, container or object
1815
    Metadata are formed as key:value pairs
1816
    """
1817

    
1818
    @errors.generic.all
1819
    @errors.pithos.connection
1820
    @errors.pithos.container
1821
    @errors.pithos.object_path
1822
    def _run(self, metakey, metaval):
1823
        if not self.container:
1824
            self.client.set_account_meta({metakey: metaval})
1825
        elif not self.path:
1826
            self.client.set_container_meta({metakey: metaval})
1827
        else:
1828
            self.client.set_object_meta(self.path, {metakey: metaval})
1829

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

    
1834

    
1835
@command(pithos_cmds)
1836
class file_delmeta(_file_container_command):
1837
    """Delete metadata with given key from account, container or object
1838
    Metadata are formed as key:value objects
1839
    - to get metadata of current account:     /file meta
1840
    - to get metadata of a container:         /file meta <container>
1841
    - to get metadata of an object:           /file meta <container>:<path>
1842
    """
1843

    
1844
    @errors.generic.all
1845
    @errors.pithos.connection
1846
    @errors.pithos.container
1847
    @errors.pithos.object_path
1848
    def _run(self, metakey):
1849
        if self.container is None:
1850
            self.client.del_account_meta(metakey)
1851
        elif self.path is None:
1852
            self.client.del_container_meta(metakey)
1853
        else:
1854
            self.client.del_object_meta(self.path, metakey)
1855

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

    
1860

    
1861
@command(pithos_cmds)
1862
class file_quota(_file_account_command):
1863
    """Get account quota"""
1864

    
1865
    arguments = dict(
1866
        in_bytes=FlagArgument('Show result in bytes', ('-b', '--bytes'))
1867
    )
1868

    
1869
    @errors.generic.all
1870
    @errors.pithos.connection
1871
    def _run(self):
1872
        reply = self.client.get_account_quota()
1873
        if not self['in_bytes']:
1874
            for k in reply:
1875
                reply[k] = format_size(reply[k])
1876
        print_dict(pretty_keys(reply, '-'))
1877

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

    
1882

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

    
1887

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

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

    
1896
    @errors.generic.all
1897
    @errors.pithos.container
1898
    def _run(self):
1899
        reply = self.client.get_container_limit(self.container)
1900
        if not self['in_bytes']:
1901
            for k, v in reply.items():
1902
                reply[k] = 'unlimited' if '0' == v else format_size(v)
1903
        print_dict(pretty_keys(reply, '-'))
1904

    
1905
    def main(self, container=None):
1906
        super(self.__class__, self)._run()
1907
        self.container = container
1908
        self._run()
1909

    
1910

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

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

    
1945
    @errors.generic.all
1946
    @errors.pithos.connection
1947
    @errors.pithos.container
1948
    def _run(self, limit):
1949
        if self.container:
1950
            self.client.container = self.container
1951
        self.client.set_container_limit(limit)
1952

    
1953
    def main(self, limit, container=None):
1954
        super(self.__class__, self)._run()
1955
        limit = self._calculate_limit(limit)
1956
        self.container = container
1957
        self._run(limit)
1958

    
1959

    
1960
@command(pithos_cmds)
1961
class file_versioning(_file_account_command):
1962
    """Get  versioning for account or container"""
1963

    
1964
    @errors.generic.all
1965
    @errors.pithos.connection
1966
    @errors.pithos.container
1967
    def _run(self):
1968
        if self.container:
1969
            r = self.client.get_container_versioning(self.container)
1970
        else:
1971
            r = self.client.get_account_versioning()
1972
        print_dict(r)
1973

    
1974
    def main(self, container=None):
1975
        super(self.__class__, self)._run()
1976
        self.container = container
1977
        self._run()
1978

    
1979

    
1980
@command(pithos_cmds)
1981
class file_setversioning(_file_account_command):
1982
    """Set versioning mode (auto, none) for account or container"""
1983

    
1984
    def _check_versioning(self, versioning):
1985
        if versioning and versioning.lower() in ('auto', 'none'):
1986
            return versioning.lower()
1987
        raiseCLIError('Invalid versioning %s' % versioning, details=[
1988
            'Versioning can be auto or none'])
1989

    
1990
    @errors.generic.all
1991
    @errors.pithos.connection
1992
    @errors.pithos.container
1993
    def _run(self, versioning):
1994
        if self.container:
1995
            self.client.container = self.container
1996
            self.client.set_container_versioning(versioning)
1997
        else:
1998
            self.client.set_account_versioning(versioning)
1999

    
2000
    def main(self, versioning, container=None):
2001
        super(self.__class__, self)._run()
2002
        self._run(self._check_versioning(versioning))
2003

    
2004

    
2005
@command(pithos_cmds)
2006
class file_group(_file_account_command):
2007
    """Get groups and group members"""
2008

    
2009
    @errors.generic.all
2010
    @errors.pithos.connection
2011
    def _run(self):
2012
        r = self.client.get_account_group()
2013
        print_dict(pretty_keys(r, '-'))
2014

    
2015
    def main(self):
2016
        super(self.__class__, self)._run()
2017
        self._run()
2018

    
2019

    
2020
@command(pithos_cmds)
2021
class file_setgroup(_file_account_command):
2022
    """Set a user group"""
2023

    
2024
    @errors.generic.all
2025
    @errors.pithos.connection
2026
    def _run(self, groupname, *users):
2027
        self.client.set_account_group(groupname, users)
2028

    
2029
    def main(self, groupname, *users):
2030
        super(self.__class__, self)._run()
2031
        if users:
2032
            self._run(groupname, *users)
2033
        else:
2034
            raiseCLIError('No users to add in group %s' % groupname)
2035

    
2036

    
2037
@command(pithos_cmds)
2038
class file_delgroup(_file_account_command):
2039
    """Delete a user group"""
2040

    
2041
    @errors.generic.all
2042
    @errors.pithos.connection
2043
    def _run(self, groupname):
2044
        self.client.del_account_group(groupname)
2045

    
2046
    def main(self, groupname):
2047
        super(self.__class__, self)._run()
2048
        self._run(groupname)
2049

    
2050

    
2051
@command(pithos_cmds)
2052
class file_sharers(_file_account_command):
2053
    """List the accounts that share objects with current user"""
2054

    
2055
    arguments = dict(
2056
        detail=FlagArgument('show detailed output', ('-l', '--details')),
2057
        marker=ValueArgument('show output greater then marker', '--marker')
2058
    )
2059

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

    
2069
    def main(self):
2070
        super(self.__class__, self)._run()
2071
        self._run()
2072

    
2073

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

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

    
2094
    def main(self, container___path):
2095
        super(file_versions, self)._run(
2096
            container___path,
2097
            path_is_optional=False)
2098
        self._run()