Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / pithos.py @ 1d3f006b

History | View | Annotate | Download (74.5 kB)

1
# Copyright 2011-2012 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
#
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
#
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
#
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.command
33

    
34
from sys import stdout
35
from time import localtime, strftime
36
from os import path, makedirs, walk
37

    
38
from kamaki.cli import command
39
from kamaki.cli.command_tree import CommandTree
40
from kamaki.cli.errors import raiseCLIError, CLISyntaxError
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)
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
    )
334

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

    
364
    def print_containers(self, container_list):
365
        limit = int(self['limit']) if self['limit'] > 0\
366
            else len(container_list)
367
        for index, container in enumerate(container_list):
368
            if 'bytes' in container:
369
                size = format_size(container['bytes'])
370
            prfx = ('%s. ' % (index + 1)) if self['enum'] else ''
371
            cname = '%s%s' % (prfx, bold(container['name']))
372
            if self['detail']:
373
                print(cname)
374
                pretty_c = container.copy()
375
                if 'bytes' in container:
376
                    pretty_c['bytes'] = '%s (%s)' % (container['bytes'], size)
377
                print_dict(pretty_keys(pretty_c), exclude=('name'))
378
                print
379
            else:
380
                if 'count' in container and 'bytes' in container:
381
                    print('%s (%s, %s objects)' % (
382
                        cname,
383
                        size,
384
                        container['count']))
385
                else:
386
                    print(cname)
387
            if self['more']:
388
                page_hold(index + 1, limit, len(container_list))
389

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

    
419
    def main(self, container____path__=None):
420
        super(self.__class__, self)._run(container____path__)
421
        self._run()
422

    
423

    
424
@command(pithos_cmds)
425
class file_mkdir(_file_container_command):
426
    """Create a directory"""
427

    
428
    __doc__ += '\n. '.join([
429
        'Kamaki hanldes directories the same way as OOS Storage and Pithos+:',
430
        'A   directory  is   an  object  with  type  "application/directory"',
431
        'An object with path  dir/name can exist even if  dir does not exist',
432
        'or even if dir  is  a non  directory  object.  Users can modify dir',
433
        'without affecting the dir/name object in any way.'])
434

    
435
    @errors.generic.all
436
    @errors.pithos.connection
437
    @errors.pithos.container
438
    def _run(self):
439
        self.client.create_directory(self.path)
440

    
441
    def main(self, container___directory):
442
        super(self.__class__, self)._run(
443
            container___directory,
444
            path_is_optional=False)
445
        self._run()
446

    
447

    
448
@command(pithos_cmds)
449
class file_touch(_file_container_command):
450
    """Create an empty object (file)
451
    If object exists, this command will reset it to 0 length
452
    """
453

    
454
    arguments = dict(
455
        content_type=ValueArgument(
456
            'Set content type (default: application/octet-stream)',
457
            '--content-type',
458
            default='application/octet-stream')
459
    )
460

    
461
    @errors.generic.all
462
    @errors.pithos.connection
463
    @errors.pithos.container
464
    def _run(self):
465
        self.client.create_object(self.path, self['content_type'])
466

    
467
    def main(self, container___path):
468
        super(file_touch, self)._run(
469
            container___path,
470
            path_is_optional=False)
471
        self._run()
472

    
473

    
474
@command(pithos_cmds)
475
class file_create(_file_container_command):
476
    """Create a container"""
477

    
478
    arguments = dict(
479
        versioning=ValueArgument(
480
            'set container versioning (auto/none)',
481
            '--versioning'),
482
        limit=IntArgument('set default container limit', '--limit'),
483
        meta=KeyValueArgument(
484
            'set container metadata (can be repeated)',
485
            '--meta')
486
    )
487

    
488
    @errors.generic.all
489
    @errors.pithos.connection
490
    @errors.pithos.container
491
    def _run(self):
492
        self.client.container_put(
493
            limit=self['limit'],
494
            versioning=self['versioning'],
495
            metadata=self['meta'])
496

    
497
    def main(self, container=None):
498
        super(self.__class__, self)._run(container)
499
        if container and self.container != container:
500
            raiseCLIError('Invalid container name %s' % container, details=[
501
                'Did you mean "%s" ?' % self.container,
502
                'Use --container for names containing :'])
503
        self._run()
504

    
505

    
506
class _source_destination_command(_file_container_command):
507

    
508
    arguments = dict(
509
        destination_account=ValueArgument('', ('a', '--dst-account')),
510
        recursive=FlagArgument('', ('-R', '--recursive')),
511
        prefix=FlagArgument('', '--with-prefix', default=''),
512
        suffix=ValueArgument('', '--with-suffix', default=''),
513
        add_prefix=ValueArgument('', '--add-prefix', default=''),
514
        add_suffix=ValueArgument('', '--add-suffix', default=''),
515
        prefix_replace=ValueArgument('', '--prefix-to-replace', default=''),
516
        suffix_replace=ValueArgument('', '--suffix-to-replace', default=''),
517
    )
518

    
519
    def __init__(self, arguments={}):
520
        self.arguments.update(arguments)
521
        super(_source_destination_command, self).__init__(self.arguments)
522

    
523
    def _run(self, source_container___path, path_is_optional=False):
524
        super(_source_destination_command, self)._run(
525
            source_container___path,
526
            path_is_optional)
527
        self.dst_client = PithosClient(
528
            base_url=self.client.base_url,
529
            token=self.client.token,
530
            account=self['destination_account'] or self.client.account)
531

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

    
555
    def _get_all(self, prefix):
556
        return self.client.container_get(prefix=prefix).json
557

    
558
    def _get_src_objects(self, src_path, source_version=None):
559
        """Get a list of the source objects to be called
560

561
        :param src_path: (str) source path
562

563
        :returns: (method, params) a method that returns a list when called
564
        or (object) if it is a single object
565
        """
566
        if src_path and src_path[-1] == '/':
567
            src_path = src_path[:-1]
568

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

    
585
        if self._is_dir(srcobj):
586
            if not self['recursive']:
587
                raiseCLIError(
588
                    'Object %s of cont. %s is a dir' % (
589
                        src_path,
590
                        self.client.container),
591
                    details=['Use --recursive to access directories'])
592
            return (self._get_all, dict(prefix=src_path))
593
        srcobj['name'] = src_path
594
        return srcobj
595

    
596
    def src_dst_pairs(self, dst_path, source_version=None):
597
        src_iter = self._get_src_objects(self.path, source_version)
598
        src_N = isinstance(src_iter, tuple)
599
        add_prefix = self['add_prefix'].strip('/')
600

    
601
        if dst_path and dst_path.endswith('/'):
602
            dst_path = dst_path[:-1]
603

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

    
628
        if src_N:
629
            (method, kwargs) = src_iter
630
            for obj in method(**kwargs):
631
                name = obj['name']
632
                if name.endswith(self['suffix']):
633
                    yield (name, self._get_new_object(name, add_prefix))
634
        elif src_iter['name'].endswith(self['suffix']):
635
            name = src_iter['name']
636
            yield (name, self._get_new_object(dst_path or name, add_prefix))
637
        else:
638
            raiseCLIError('Source path %s conflicts with suffix %s' % (
639
                src_iter['name'],
640
                self['suffix']))
641

    
642
    def _get_new_object(self, obj, add_prefix):
643
        if self['prefix_replace'] and obj.startswith(self['prefix_replace']):
644
            obj = obj[len(self['prefix_replace']):]
645
        if self['suffix_replace'] and obj.endswith(self['suffix_replace']):
646
            obj = obj[:-len(self['suffix_replace'])]
647
        return add_prefix + obj + self['add_suffix']
648

    
649

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

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

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

    
729
    def main(
730
            self, source_container___path,
731
            destination_container___path=None):
732
        super(file_copy, self)._run(
733
            source_container___path,
734
            path_is_optional=False)
735
        (dst_cont, dst_path) = self._dest_container_path(
736
            destination_container___path)
737
        self.dst_client.container = dst_cont or self.container
738
        self._run(dst_path=dst_path or '')
739

    
740

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

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

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

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

    
828

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

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

    
844
    @errors.generic.all
845
    @errors.pithos.connection
846
    @errors.pithos.container
847
    @errors.pithos.object_path
848
    def _run(self, local_path):
849
        (progress_bar, upload_cb) = self._safe_progress_bar('Appending')
850
        try:
851
            f = open(local_path, 'rb')
852
            self.client.append_object(self.path, f, upload_cb)
853
        except Exception:
854
            self._safe_progress_bar_finish(progress_bar)
855
            raise
856
        finally:
857
            self._safe_progress_bar_finish(progress_bar)
858

    
859
    def main(self, local_path, container___path):
860
        super(self.__class__, self)._run(
861
            container___path,
862
            path_is_optional=False)
863
        self._run(local_path)
864

    
865

    
866
@command(pithos_cmds)
867
class file_truncate(_file_container_command):
868
    """Truncate remote file up to a size (default is 0)"""
869

    
870
    @errors.generic.all
871
    @errors.pithos.connection
872
    @errors.pithos.container
873
    @errors.pithos.object_path
874
    @errors.pithos.object_size
875
    def _run(self, size=0):
876
        self.client.truncate_object(self.path, size)
877

    
878
    def main(self, container___path, size=0):
879
        super(self.__class__, self)._run(container___path)
880
        self._run(size=size)
881

    
882

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

    
893
    arguments = dict(
894
        progress_bar=ProgressBarArgument(
895
            'do not show progress bar',
896
            ('-N', '--no-progress-bar'),
897
            default=False)
898
    )
899

    
900
    def _open_file(self, local_path, start):
901
        f = open(path.abspath(local_path), 'rb')
902
        f.seek(0, 2)
903
        f_size = f.tell()
904
        f.seek(start, 0)
905
        return (f, f_size)
906

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

    
930
    def main(self, local_path, container___path, start, end):
931
        super(self.__class__, self)._run(
932
            container___path,
933
            path_is_optional=None)
934
        self.path = self.path or path.basename(local_path)
935
        self._run(local_path=local_path, start=start, end=end)
936

    
937

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

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

    
971
    @errors.generic.all
972
    @errors.pithos.connection
973
    @errors.pithos.container
974
    @errors.pithos.object_path
975
    def _run(self):
976
        self.client.create_object_by_manifestation(
977
            self.path,
978
            content_encoding=self['content_encoding'],
979
            content_disposition=self['content_disposition'],
980
            content_type=self['content_type'],
981
            sharing=self['sharing'],
982
            public=self['public'])
983

    
984
    def main(self, container___path):
985
        super(self.__class__, self)._run(
986
            container___path,
987
            path_is_optional=False)
988
        self.run()
989

    
990

    
991
@command(pithos_cmds)
992
class file_upload(_file_container_command):
993
    """Upload a file"""
994

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

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

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

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

    
1170
    def main(self, local_path, container____path__=None):
1171
        super(self.__class__, self)._run(container____path__)
1172
        remote_path = self.path or path.basename(local_path)
1173
        self._run(local_path=local_path, remote_path=remote_path)
1174

    
1175

    
1176
@command(pithos_cmds)
1177
class file_cat(_file_container_command):
1178
    """Print remote file contents to console"""
1179

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

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

    
1212
    def main(self, container___path):
1213
        super(self.__class__, self)._run(
1214
            container___path,
1215
            path_is_optional=False)
1216
        self._run()
1217

    
1218

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

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

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

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

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

    
1405
            print('\nDownload canceled by user')
1406
            if local_path is not None:
1407
                print('to resume, re-run with --resume')
1408
        except Exception:
1409
            self._safe_progress_bar_finish(progress_bar)
1410
            raise
1411
        finally:
1412
            self._safe_progress_bar_finish(progress_bar)
1413

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

    
1418

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

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

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

    
1453
    def main(self, container___path):
1454
        super(self.__class__, self)._run(
1455
            container___path,
1456
            path_is_optional=False)
1457
        self._run()
1458

    
1459

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

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

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

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

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

    
1524

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

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

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

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

    
1568

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

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

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

    
1587

    
1588
@command(pithos_cmds)
1589
class file_unpublish(_file_container_command):
1590
    """Unpublish an object"""
1591

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

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

    
1605

    
1606
@command(pithos_cmds)
1607
class file_permissions(_file_container_command):
1608
    """Get read and write permissions of an object
1609
    Permissions are lists of users and user groups. There is read and write
1610
    permissions. Users and groups with write permission have also read
1611
    permission.
1612
    """
1613

    
1614
    @errors.generic.all
1615
    @errors.pithos.connection
1616
    @errors.pithos.container
1617
    @errors.pithos.object_path
1618
    def _run(self):
1619
        r = self.client.get_object_sharing(self.path)
1620
        print_dict(r)
1621

    
1622
    def main(self, container___path):
1623
        super(self.__class__, self)._run(
1624
            container___path,
1625
            path_is_optional=False)
1626
        self._run()
1627

    
1628

    
1629
@command(pithos_cmds)
1630
class file_setpermissions(_file_container_command):
1631
    """Set permissions for an object
1632
    New permissions overwrite existing permissions.
1633
    Permission format:
1634
    -   read=<username>[,usergroup[,...]]
1635
    -   write=<username>[,usegroup[,...]]
1636
    E.g. to give read permissions for file F to users A and B and write for C:
1637
    .       /file setpermissions F read=A,B write=C
1638
    """
1639

    
1640
    @errors.generic.all
1641
    def format_permition_dict(self, permissions):
1642
        read = False
1643
        write = False
1644
        for perms in permissions:
1645
            splstr = perms.split('=')
1646
            if 'read' == splstr[0]:
1647
                read = [ug.strip() for ug in splstr[1].split(',')]
1648
            elif 'write' == splstr[0]:
1649
                write = [ug.strip() for ug in splstr[1].split(',')]
1650
            else:
1651
                msg = 'Usage:\tread=<groups,users> write=<groups,users>'
1652
                raiseCLIError(None, msg)
1653
        return (read, write)
1654

    
1655
    @errors.generic.all
1656
    @errors.pithos.connection
1657
    @errors.pithos.container
1658
    @errors.pithos.object_path
1659
    def _run(self, read, write):
1660
        self.client.set_object_sharing(
1661
            self.path,
1662
            read_permition=read,
1663
            write_permition=write)
1664

    
1665
    def main(self, container___path, *permissions):
1666
        super(self.__class__, self)._run(
1667
            container___path,
1668
            path_is_optional=False)
1669
        (read, write) = self.format_permition_dict(permissions)
1670
        self._run(read, write)
1671

    
1672

    
1673
@command(pithos_cmds)
1674
class file_delpermissions(_file_container_command):
1675
    """Delete all permissions set on object
1676
    To modify permissions, use /file setpermssions
1677
    """
1678

    
1679
    @errors.generic.all
1680
    @errors.pithos.connection
1681
    @errors.pithos.container
1682
    @errors.pithos.object_path
1683
    def _run(self):
1684
        self.client.del_object_sharing(self.path)
1685

    
1686
    def main(self, container___path):
1687
        super(self.__class__, self)._run(
1688
            container___path,
1689
            path_is_optional=False)
1690
        self._run()
1691

    
1692

    
1693
@command(pithos_cmds)
1694
class file_info(_file_container_command):
1695
    """Get detailed information for user account, containers or objects
1696
    to get account info:    /file info
1697
    to get container info:  /file info <container>
1698
    to get object info:     /file info <container>:<path>
1699
    """
1700

    
1701
    arguments = dict(
1702
        object_version=ValueArgument(
1703
            'show specific version \ (applies only for objects)',
1704
            ('-j', '--object-version'))
1705
    )
1706

    
1707
    @errors.generic.all
1708
    @errors.pithos.connection
1709
    @errors.pithos.container
1710
    @errors.pithos.object_path
1711
    def _run(self):
1712
        if self.container is None:
1713
            r = self.client.get_account_info()
1714
        elif self.path is None:
1715
            r = self.client.get_container_info(self.container)
1716
        else:
1717
            r = self.client.get_object_info(
1718
                self.path,
1719
                version=self['object_version'])
1720
        print_dict(r)
1721

    
1722
    def main(self, container____path__=None):
1723
        super(self.__class__, self)._run(container____path__)
1724
        self._run()
1725

    
1726

    
1727
@command(pithos_cmds)
1728
class file_meta(_file_container_command):
1729
    """Get metadata for account, containers or objects"""
1730

    
1731
    arguments = dict(
1732
        detail=FlagArgument('show detailed output', ('-l', '--details')),
1733
        until=DateArgument('show metadata until then', '--until'),
1734
        object_version=ValueArgument(
1735
            'show specific version \ (applies only for objects)',
1736
            ('-j', '--object-version'))
1737
    )
1738

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

    
1778
    def main(self, container____path__=None):
1779
        super(self.__class__, self)._run(container____path__)
1780
        self._run()
1781

    
1782

    
1783
@command(pithos_cmds)
1784
class file_setmeta(_file_container_command):
1785
    """Set a piece of metadata for account, container or object
1786
    Metadata are formed as key:value pairs
1787
    """
1788

    
1789
    @errors.generic.all
1790
    @errors.pithos.connection
1791
    @errors.pithos.container
1792
    @errors.pithos.object_path
1793
    def _run(self, metakey, metaval):
1794
        if not self.container:
1795
            self.client.set_account_meta({metakey: metaval})
1796
        elif not self.path:
1797
            self.client.set_container_meta({metakey: metaval})
1798
        else:
1799
            self.client.set_object_meta(self.path, {metakey: metaval})
1800

    
1801
    def main(self, metakey, metaval, container____path__=None):
1802
        super(self.__class__, self)._run(container____path__)
1803
        self._run(metakey=metakey, metaval=metaval)
1804

    
1805

    
1806
@command(pithos_cmds)
1807
class file_delmeta(_file_container_command):
1808
    """Delete metadata with given key from account, container or object
1809
    Metadata are formed as key:value objects
1810
    - to get metadata of current account:     /file meta
1811
    - to get metadata of a container:         /file meta <container>
1812
    - to get metadata of an object:           /file meta <container>:<path>
1813
    """
1814

    
1815
    @errors.generic.all
1816
    @errors.pithos.connection
1817
    @errors.pithos.container
1818
    @errors.pithos.object_path
1819
    def _run(self, metakey):
1820
        if self.container is None:
1821
            self.client.del_account_meta(metakey)
1822
        elif self.path is None:
1823
            self.client.del_container_meta(metakey)
1824
        else:
1825
            self.client.del_object_meta(self.path, metakey)
1826

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

    
1831

    
1832
@command(pithos_cmds)
1833
class file_quota(_file_account_command):
1834
    """Get account quota"""
1835

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

    
1840
    @errors.generic.all
1841
    @errors.pithos.connection
1842
    def _run(self):
1843
        reply = self.client.get_account_quota()
1844
        if not self['in_bytes']:
1845
            for k in reply:
1846
                reply[k] = format_size(reply[k])
1847
        print_dict(pretty_keys(reply, '-'))
1848

    
1849
    def main(self, custom_uuid=None):
1850
        super(self.__class__, self)._run(custom_account=custom_uuid)
1851
        self._run()
1852

    
1853

    
1854
@command(pithos_cmds)
1855
class file_containerlimit(_pithos_init):
1856
    """Container size limit commands"""
1857

    
1858

    
1859
@command(pithos_cmds)
1860
class file_containerlimit_get(_file_container_command):
1861
    """Get container size limit"""
1862

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

    
1867
    @errors.generic.all
1868
    @errors.pithos.container
1869
    def _run(self):
1870
        reply = self.client.get_container_limit(self.container)
1871
        if not self['in_bytes']:
1872
            for k, v in reply.items():
1873
                reply[k] = 'unlimited' if '0' == v else format_size(v)
1874
        print_dict(pretty_keys(reply, '-'))
1875

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

    
1881

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

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

    
1916
    @errors.generic.all
1917
    @errors.pithos.connection
1918
    @errors.pithos.container
1919
    def _run(self, limit):
1920
        if self.container:
1921
            self.client.container = self.container
1922
        self.client.set_container_limit(limit)
1923

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

    
1930

    
1931
@command(pithos_cmds)
1932
class file_versioning(_file_account_command):
1933
    """Get  versioning for account or container"""
1934

    
1935
    @errors.generic.all
1936
    @errors.pithos.connection
1937
    @errors.pithos.container
1938
    def _run(self):
1939
        if self.container:
1940
            r = self.client.get_container_versioning(self.container)
1941
        else:
1942
            r = self.client.get_account_versioning()
1943
        print_dict(r)
1944

    
1945
    def main(self, container=None):
1946
        super(self.__class__, self)._run()
1947
        self.container = container
1948
        self._run()
1949

    
1950

    
1951
@command(pithos_cmds)
1952
class file_setversioning(_file_account_command):
1953
    """Set versioning mode (auto, none) for account or container"""
1954

    
1955
    def _check_versioning(self, versioning):
1956
        if versioning and versioning.lower() in ('auto', 'none'):
1957
            return versioning.lower()
1958
        raiseCLIError('Invalid versioning %s' % versioning, details=[
1959
            'Versioning can be auto or none'])
1960

    
1961
    @errors.generic.all
1962
    @errors.pithos.connection
1963
    @errors.pithos.container
1964
    def _run(self, versioning):
1965
        if self.container:
1966
            self.client.container = self.container
1967
            self.client.set_container_versioning(versioning)
1968
        else:
1969
            self.client.set_account_versioning(versioning)
1970

    
1971
    def main(self, versioning, container=None):
1972
        super(self.__class__, self)._run()
1973
        self._run(self._check_versioning(versioning))
1974

    
1975

    
1976
@command(pithos_cmds)
1977
class file_group(_file_account_command):
1978
    """Get groups and group members"""
1979

    
1980
    @errors.generic.all
1981
    @errors.pithos.connection
1982
    def _run(self):
1983
        r = self.client.get_account_group()
1984
        print_dict(pretty_keys(r, '-'))
1985

    
1986
    def main(self):
1987
        super(self.__class__, self)._run()
1988
        self._run()
1989

    
1990

    
1991
@command(pithos_cmds)
1992
class file_setgroup(_file_account_command):
1993
    """Set a user group"""
1994

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

    
2000
    def main(self, groupname, *users):
2001
        super(self.__class__, self)._run()
2002
        if users:
2003
            self._run(groupname, *users)
2004
        else:
2005
            raiseCLIError('No users to add in group %s' % groupname)
2006

    
2007

    
2008
@command(pithos_cmds)
2009
class file_delgroup(_file_account_command):
2010
    """Delete a user group"""
2011

    
2012
    @errors.generic.all
2013
    @errors.pithos.connection
2014
    def _run(self, groupname):
2015
        self.client.del_account_group(groupname)
2016

    
2017
    def main(self, groupname):
2018
        super(self.__class__, self)._run()
2019
        self._run(groupname)
2020

    
2021

    
2022
@command(pithos_cmds)
2023
class file_sharers(_file_account_command):
2024
    """List the accounts that share objects with current user"""
2025

    
2026
    arguments = dict(
2027
        detail=FlagArgument('show detailed output', ('-l', '--details')),
2028
        marker=ValueArgument('show output greater then marker', '--marker')
2029
    )
2030

    
2031
    @errors.generic.all
2032
    @errors.pithos.connection
2033
    def _run(self):
2034
        accounts = self.client.get_sharing_accounts(marker=self['marker'])
2035
        if self['detail']:
2036
            print_items(accounts)
2037
        else:
2038
            print_items([acc['name'] for acc in accounts])
2039

    
2040
    def main(self):
2041
        super(self.__class__, self)._run()
2042
        self._run()
2043

    
2044

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

    
2055
    @errors.generic.all
2056
    @errors.pithos.connection
2057
    @errors.pithos.container
2058
    @errors.pithos.object_path
2059
    def _run(self):
2060
        versions = self.client.get_object_versionlist(self.path)
2061
        print_items([dict(id=vitem[0], created=strftime(
2062
            '%d-%m-%Y %H:%M:%S',
2063
            localtime(float(vitem[1])))) for vitem in versions])
2064

    
2065
    def main(self, container___path):
2066
        super(file_versions, self)._run(
2067
            container___path,
2068
            path_is_optional=False)
2069
        self._run()