Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / pithos.py @ 0399ac7e

History | View | Annotate | Download (74.7 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
    __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.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):
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.client.create_object(self.path, self['content_type'])
473

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

    
480

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

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

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

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

    
512

    
513
class _source_destination_command(_file_container_command):
514

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

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

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

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

    
562
    def _get_all(self, prefix):
563
        return self.client.container_get(prefix=prefix).json
564

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

568
        :param src_path: (str) source path
569

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

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

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

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

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

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

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

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

    
656

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

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

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

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

    
747

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

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

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

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

    
835

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

    
844
    arguments = dict(
845
        progress_bar=ProgressBarArgument(
846
            'do not show progress bar',
847
            ('-N', '--no-progress-bar'),
848
            default=False)
849
    )
850

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

    
866
    def main(self, local_path, container___path):
867
        super(self.__class__, self)._run(
868
            container___path,
869
            path_is_optional=False)
870
        self._run(local_path)
871

    
872

    
873
@command(pithos_cmds)
874
class file_truncate(_file_container_command):
875
    """Truncate remote file up to a size (default is 0)"""
876

    
877
    @errors.generic.all
878
    @errors.pithos.connection
879
    @errors.pithos.container
880
    @errors.pithos.object_path
881
    @errors.pithos.object_size
882
    def _run(self, size=0):
883
        self.client.truncate_object(self.path, size)
884

    
885
    def main(self, container___path, size=0):
886
        super(self.__class__, self)._run(container___path)
887
        self._run(size=size)
888

    
889

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

    
900
    arguments = dict(
901
        progress_bar=ProgressBarArgument(
902
            'do not show progress bar',
903
            ('-N', '--no-progress-bar'),
904
            default=False)
905
    )
906

    
907
    def _open_file(self, local_path, start):
908
        f = open(path.abspath(local_path), 'rb')
909
        f.seek(0, 2)
910
        f_size = f.tell()
911
        f.seek(start, 0)
912
        return (f, f_size)
913

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

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

    
944

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

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

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

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

    
997

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

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

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

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

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

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

    
1182

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

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

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

    
1219
    def main(self, container___path):
1220
        super(self.__class__, self)._run(
1221
            container___path,
1222
            path_is_optional=False)
1223
        self._run()
1224

    
1225

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

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

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

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

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

    
1412
            print('\nDownload canceled by user')
1413
            if local_path is not None:
1414
                print('to resume, re-run with --resume')
1415
        except Exception:
1416
            self._safe_progress_bar_finish(progress_bar)
1417
            raise
1418
        finally:
1419
            self._safe_progress_bar_finish(progress_bar)
1420

    
1421
    def main(self, container___path, local_path=None):
1422
        super(self.__class__, self)._run(container___path)
1423
        self._run(local_path=local_path)
1424

    
1425

    
1426
@command(pithos_cmds)
1427
class file_hashmap(_file_container_command):
1428
    """Get the hash-map of an object"""
1429

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

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

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

    
1466

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

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

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

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

    
1527
    def main(self, container____path__=None):
1528
        super(self.__class__, self)._run(container____path__)
1529
        self._run()
1530

    
1531

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

    
1543
    arguments = dict(
1544
        yes=FlagArgument('Do not prompt for permission', '--yes'),
1545
        force=FlagArgument('purge even if not empty', ('-F', '--force'))
1546
    )
1547

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

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

    
1575

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

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

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

    
1594

    
1595
@command(pithos_cmds)
1596
class file_unpublish(_file_container_command):
1597
    """Unpublish an object"""
1598

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

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

    
1612

    
1613
@command(pithos_cmds)
1614
class file_permissions(_file_container_command):
1615
    """Get read and write permissions of an object
1616
    Permissions are lists of users and user groups. There is read and write
1617
    permissions. Users and groups with write permission have also read
1618
    permission.
1619
    """
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_setpermissions(_file_container_command):
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 setpermissions F read=A,B write=C
1645
    """
1646

    
1647
    @errors.generic.all
1648
    def format_permition_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.client.set_object_sharing(
1668
            self.path,
1669
            read_permition=read,
1670
            write_permition=write)
1671

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

    
1679

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

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

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

    
1699

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

    
1708
    arguments = dict(
1709
        object_version=ValueArgument(
1710
            'show specific version \ (applies only for objects)',
1711
            ('-j', '--object-version'))
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
        print_dict(r)
1728

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

    
1733

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

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

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

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

    
1789

    
1790
@command(pithos_cmds)
1791
class file_setmeta(_file_container_command):
1792
    """Set a piece of metadata for account, container or object
1793
    Metadata are formed as key:value pairs
1794
    """
1795

    
1796
    @errors.generic.all
1797
    @errors.pithos.connection
1798
    @errors.pithos.container
1799
    @errors.pithos.object_path
1800
    def _run(self, metakey, metaval):
1801
        if not self.container:
1802
            self.client.set_account_meta({metakey: metaval})
1803
        elif not self.path:
1804
            self.client.set_container_meta({metakey: metaval})
1805
        else:
1806
            self.client.set_object_meta(self.path, {metakey: metaval})
1807

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

    
1812

    
1813
@command(pithos_cmds)
1814
class file_delmeta(_file_container_command):
1815
    """Delete metadata with given key from account, container or object
1816
    Metadata are formed as key:value objects
1817
    - to get metadata of current account:     /file meta
1818
    - to get metadata of a container:         /file meta <container>
1819
    - to get metadata of an object:           /file meta <container>:<path>
1820
    """
1821

    
1822
    @errors.generic.all
1823
    @errors.pithos.connection
1824
    @errors.pithos.container
1825
    @errors.pithos.object_path
1826
    def _run(self, metakey):
1827
        if self.container is None:
1828
            self.client.del_account_meta(metakey)
1829
        elif self.path is None:
1830
            self.client.del_container_meta(metakey)
1831
        else:
1832
            self.client.del_object_meta(self.path, metakey)
1833

    
1834
    def main(self, metakey, container____path__=None):
1835
        super(self.__class__, self)._run(container____path__)
1836
        self._run(metakey)
1837

    
1838

    
1839
@command(pithos_cmds)
1840
class file_quota(_file_account_command):
1841
    """Get account quota"""
1842

    
1843
    arguments = dict(
1844
        in_bytes=FlagArgument('Show result in bytes', ('-b', '--bytes'))
1845
    )
1846

    
1847
    @errors.generic.all
1848
    @errors.pithos.connection
1849
    def _run(self):
1850
        reply = self.client.get_account_quota()
1851
        if not self['in_bytes']:
1852
            for k in reply:
1853
                reply[k] = format_size(reply[k])
1854
        print_dict(pretty_keys(reply, '-'))
1855

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

    
1860

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

    
1865

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

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

    
1874
    @errors.generic.all
1875
    @errors.pithos.container
1876
    def _run(self):
1877
        reply = self.client.get_container_limit(self.container)
1878
        if not self['in_bytes']:
1879
            for k, v in reply.items():
1880
                reply[k] = 'unlimited' if '0' == v else format_size(v)
1881
        print_dict(pretty_keys(reply, '-'))
1882

    
1883
    def main(self, container=None):
1884
        super(self.__class__, self)._run()
1885
        self.container = container
1886
        self._run()
1887

    
1888

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

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

    
1923
    @errors.generic.all
1924
    @errors.pithos.connection
1925
    @errors.pithos.container
1926
    def _run(self, limit):
1927
        if self.container:
1928
            self.client.container = self.container
1929
        self.client.set_container_limit(limit)
1930

    
1931
    def main(self, limit, container=None):
1932
        super(self.__class__, self)._run()
1933
        limit = self._calculate_limit(limit)
1934
        self.container = container
1935
        self._run(limit)
1936

    
1937

    
1938
@command(pithos_cmds)
1939
class file_versioning(_file_account_command):
1940
    """Get  versioning for account or container"""
1941

    
1942
    @errors.generic.all
1943
    @errors.pithos.connection
1944
    @errors.pithos.container
1945
    def _run(self):
1946
        if self.container:
1947
            r = self.client.get_container_versioning(self.container)
1948
        else:
1949
            r = self.client.get_account_versioning()
1950
        print_dict(r)
1951

    
1952
    def main(self, container=None):
1953
        super(self.__class__, self)._run()
1954
        self.container = container
1955
        self._run()
1956

    
1957

    
1958
@command(pithos_cmds)
1959
class file_setversioning(_file_account_command):
1960
    """Set versioning mode (auto, none) for account or container"""
1961

    
1962
    def _check_versioning(self, versioning):
1963
        if versioning and versioning.lower() in ('auto', 'none'):
1964
            return versioning.lower()
1965
        raiseCLIError('Invalid versioning %s' % versioning, details=[
1966
            'Versioning can be auto or none'])
1967

    
1968
    @errors.generic.all
1969
    @errors.pithos.connection
1970
    @errors.pithos.container
1971
    def _run(self, versioning):
1972
        if self.container:
1973
            self.client.container = self.container
1974
            self.client.set_container_versioning(versioning)
1975
        else:
1976
            self.client.set_account_versioning(versioning)
1977

    
1978
    def main(self, versioning, container=None):
1979
        super(self.__class__, self)._run()
1980
        self._run(self._check_versioning(versioning))
1981

    
1982

    
1983
@command(pithos_cmds)
1984
class file_group(_file_account_command):
1985
    """Get groups and group members"""
1986

    
1987
    @errors.generic.all
1988
    @errors.pithos.connection
1989
    def _run(self):
1990
        r = self.client.get_account_group()
1991
        print_dict(pretty_keys(r, '-'))
1992

    
1993
    def main(self):
1994
        super(self.__class__, self)._run()
1995
        self._run()
1996

    
1997

    
1998
@command(pithos_cmds)
1999
class file_setgroup(_file_account_command):
2000
    """Set a user group"""
2001

    
2002
    @errors.generic.all
2003
    @errors.pithos.connection
2004
    def _run(self, groupname, *users):
2005
        self.client.set_account_group(groupname, users)
2006

    
2007
    def main(self, groupname, *users):
2008
        super(self.__class__, self)._run()
2009
        if users:
2010
            self._run(groupname, *users)
2011
        else:
2012
            raiseCLIError('No users to add in group %s' % groupname)
2013

    
2014

    
2015
@command(pithos_cmds)
2016
class file_delgroup(_file_account_command):
2017
    """Delete a user group"""
2018

    
2019
    @errors.generic.all
2020
    @errors.pithos.connection
2021
    def _run(self, groupname):
2022
        self.client.del_account_group(groupname)
2023

    
2024
    def main(self, groupname):
2025
        super(self.__class__, self)._run()
2026
        self._run(groupname)
2027

    
2028

    
2029
@command(pithos_cmds)
2030
class file_sharers(_file_account_command):
2031
    """List the accounts that share objects with current user"""
2032

    
2033
    arguments = dict(
2034
        detail=FlagArgument('show detailed output', ('-l', '--details')),
2035
        marker=ValueArgument('show output greater then marker', '--marker')
2036
    )
2037

    
2038
    @errors.generic.all
2039
    @errors.pithos.connection
2040
    def _run(self):
2041
        accounts = self.client.get_sharing_accounts(marker=self['marker'])
2042
        if self['detail']:
2043
            print_items(accounts)
2044
        else:
2045
            print_items([acc['name'] for acc in accounts])
2046

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

    
2051

    
2052
@command(pithos_cmds)
2053
class file_versions(_file_container_command):
2054
    """Get the list of object versions
2055
    Deleted objects may still have versions that can be used to restore it and
2056
    get information about its previous state.
2057
    The version number can be used in a number of other commands, like info,
2058
    copy, move, meta. See these commands for more information, e.g.
2059
    /file info -h
2060
    """
2061

    
2062
    @errors.generic.all
2063
    @errors.pithos.connection
2064
    @errors.pithos.container
2065
    @errors.pithos.object_path
2066
    def _run(self):
2067
        versions = self.client.get_object_versionlist(self.path)
2068
        print_items([dict(id=vitem[0], created=strftime(
2069
            '%d-%m-%Y %H:%M:%S',
2070
            localtime(float(vitem[1])))) for vitem in versions])
2071

    
2072
    def main(self, container___path):
2073
        super(file_versions, self)._run(
2074
            container___path,
2075
            path_is_optional=False)
2076
        self._run()