Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / pithos.py @ ed9af02c

History | View | Annotate | Download (74.4 kB)

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

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

    
38
from kamaki.cli import command
39
from kamaki.cli.command_tree import CommandTree
40
from kamaki.cli.errors import raiseCLIError, CLISyntaxError
41
from kamaki.cli.utils import (
42
    format_size, to_bytes, print_dict, print_items, pretty_keys,
43
    page_hold, bold, ask_user, get_path_size)
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):
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(src_path)
573
        except ClientError as srcerr:
574
            if srcerr.status == 404:
575
                raiseCLIError(
576
                    'Source object %s not in source container %s' % (
577
                        src_path,
578
                        self.client.container),
579
                    details=['Hint: --with-prefix to match multiple objects'])
580
            elif srcerr.status not in (204,):
581
                raise
582
            return (self.client.list_objects, {})
583

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

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

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

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

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

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

    
648

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

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

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

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

    
739

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

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

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

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

    
831

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

    
840
    arguments = dict(
841
        progress_bar=ProgressBarArgument(
842
            'do not show progress bar',
843
            ('-N', '--no-progress-bar'),
844
            default=False)
845
    )
846

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

    
862
    def main(self, local_path, container___path):
863
        super(self.__class__, self)._run(
864
            container___path,
865
            path_is_optional=False)
866
        self._run(local_path)
867

    
868

    
869
@command(pithos_cmds)
870
class file_truncate(_file_container_command):
871
    """Truncate remote file up to a size (default is 0)"""
872

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

    
881
    def main(self, container___path, size=0):
882
        super(self.__class__, self)._run(container___path)
883
        self._run(size=size)
884

    
885

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

    
896
    arguments = dict(
897
        progress_bar=ProgressBarArgument(
898
            'do not show progress bar',
899
            ('-N', '--no-progress-bar'),
900
            default=False)
901
    )
902

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

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

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

    
940

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

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

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

    
987
    def main(self, container___path):
988
        super(self.__class__, self)._run(
989
            container___path,
990
            path_is_optional=False)
991
        self.run()
992

    
993

    
994
@command(pithos_cmds)
995
class file_upload(_file_container_command):
996
    """Upload a file"""
997

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

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

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

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

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

    
1178

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

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

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

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

    
1221

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

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

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

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

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

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

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

    
1421

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

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

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

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

    
1462

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

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

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

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

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

    
1527

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

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

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

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

    
1571

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

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

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

    
1590

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

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

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

    
1608

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

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

    
1625
    def main(self, container___path):
1626
        super(self.__class__, self)._run(
1627
            container___path,
1628
            path_is_optional=False)
1629
        self._run()
1630

    
1631

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

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

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

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

    
1675

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

    
1682
    @errors.generic.all
1683
    @errors.pithos.connection
1684
    @errors.pithos.container
1685
    @errors.pithos.object_path
1686
    def _run(self):
1687
        self.client.del_object_sharing(self.path)
1688

    
1689
    def main(self, container___path):
1690
        super(self.__class__, self)._run(
1691
            container___path,
1692
            path_is_optional=False)
1693
        self._run()
1694

    
1695

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

    
1704
    arguments = dict(
1705
        object_version=ValueArgument(
1706
            'show specific version \ (applies only for objects)',
1707
            ('-j', '--object-version'))
1708
    )
1709

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

    
1725
    def main(self, container____path__=None):
1726
        super(self.__class__, self)._run(container____path__)
1727
        self._run()
1728

    
1729

    
1730
@command(pithos_cmds)
1731
class file_meta(_file_container_command):
1732
    """Get metadata for account, containers or objects"""
1733

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

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

    
1781
    def main(self, container____path__=None):
1782
        super(self.__class__, self)._run(container____path__)
1783
        self._run()
1784

    
1785

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

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

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

    
1808

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

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

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

    
1834

    
1835
@command(pithos_cmds)
1836
class file_quota(_file_account_command):
1837
    """Get account quota"""
1838

    
1839
    arguments = dict(
1840
        in_bytes=FlagArgument('Show result in bytes', ('-b', '--bytes'))
1841
    )
1842

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

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

    
1856

    
1857
@command(pithos_cmds)
1858
class file_containerlimit(_pithos_init):
1859
    """Container size limit commands"""
1860

    
1861

    
1862
@command(pithos_cmds)
1863
class file_containerlimit_get(_file_container_command):
1864
    """Get container size limit"""
1865

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

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

    
1879
    def main(self, container=None):
1880
        super(self.__class__, self)._run()
1881
        self.container = container
1882
        self._run()
1883

    
1884

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

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

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

    
1927
    def main(self, limit, container=None):
1928
        super(self.__class__, self)._run()
1929
        limit = self._calculate_limit(limit)
1930
        self.container = container
1931
        self._run(limit)
1932

    
1933

    
1934
@command(pithos_cmds)
1935
class file_versioning(_file_account_command):
1936
    """Get  versioning for account or container"""
1937

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

    
1948
    def main(self, container=None):
1949
        super(self.__class__, self)._run()
1950
        self.container = container
1951
        self._run()
1952

    
1953

    
1954
@command(pithos_cmds)
1955
class file_setversioning(_file_account_command):
1956
    """Set versioning mode (auto, none) for account or container"""
1957

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

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

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

    
1978

    
1979
@command(pithos_cmds)
1980
class file_group(_file_account_command):
1981
    """Get groups and group members"""
1982

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

    
1989
    def main(self):
1990
        super(self.__class__, self)._run()
1991
        self._run()
1992

    
1993

    
1994
@command(pithos_cmds)
1995
class file_setgroup(_file_account_command):
1996
    """Set a user group"""
1997

    
1998
    @errors.generic.all
1999
    @errors.pithos.connection
2000
    def _run(self, groupname, *users):
2001
        self.client.set_account_group(groupname, users)
2002

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

    
2010

    
2011
@command(pithos_cmds)
2012
class file_delgroup(_file_account_command):
2013
    """Delete a user group"""
2014

    
2015
    @errors.generic.all
2016
    @errors.pithos.connection
2017
    def _run(self, groupname):
2018
        self.client.del_account_group(groupname)
2019

    
2020
    def main(self, groupname):
2021
        super(self.__class__, self)._run()
2022
        self._run(groupname)
2023

    
2024

    
2025
@command(pithos_cmds)
2026
class file_sharers(_file_account_command):
2027
    """List the accounts that share objects with current user"""
2028

    
2029
    arguments = dict(
2030
        detail=FlagArgument('show detailed output', ('-l', '--details')),
2031
        marker=ValueArgument('show output greater then marker', '--marker')
2032
    )
2033

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

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

    
2047

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

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

    
2068
    def main(self, container___path):
2069
        super(file_versions, self)._run(
2070
            container___path,
2071
            path_is_optional=False)
2072
        self._run()