Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / pithos.py @ 6d190dd1

History | View | Annotate | Download (78.4 kB)

1
# Copyright 2011-2013 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, CLIBaseUrlError
41
from kamaki.cli.utils import (
42
    format_size, to_bytes, print_dict, print_items, page_hold, bold, ask_user,
43
    get_path_size, print_json, guess_mime_type)
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.cli.commands import addLogSettings, DontRaiseKeyError
49
from kamaki.cli.commands import (
50
    _optional_output_cmd, _optional_json, _name_filter)
51
from kamaki.clients.pithos import PithosClient, ClientError
52
from kamaki.clients.astakos import AstakosClient
53

    
54
pithos_cmds = CommandTree('file', 'Pithos+/Storage API commands')
55
_commands = [pithos_cmds]
56

    
57

    
58
# Argument functionality
59

    
60
class DelimiterArgument(ValueArgument):
61
    """
62
    :value type: string
63
    :value returns: given string or /
64
    """
65

    
66
    def __init__(self, caller_obj, help='', parsed_name=None, default=None):
67
        super(DelimiterArgument, self).__init__(help, parsed_name, default)
68
        self.caller_obj = caller_obj
69

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

    
76
    @value.setter
77
    def value(self, newvalue):
78
        self._value = newvalue
79

    
80

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

    
90
    @property
91
    def value(self):
92
        return getattr(self, '_value', self.default)
93

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

    
122

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

    
130
    @property
131
    def value(self):
132
        return getattr(self, '_value', self.default)
133

    
134
    @value.setter
135
    def value(self, newvalues):
136
        if not newvalues:
137
            self._value = self.default
138
            return
139
        self._value = ''
140
        for newvalue in newvalues.split(','):
141
            self._value = ('%s,' % self._value) if self._value else ''
142
            start, sep, end = newvalue.partition('-')
143
            if sep:
144
                if start:
145
                    start, end = (int(start), int(end))
146
                    assert start <= end, 'Invalid range value %s' % newvalue
147
                    self._value += '%s-%s' % (int(start), int(end))
148
                else:
149
                    self._value += '-%s' % int(end)
150
            else:
151
                self._value += '%s' % int(start)
152

    
153

    
154
# Command specs
155

    
156

    
157
class _pithos_init(_command_init):
158
    """Initialize a pithos+ kamaki client"""
159

    
160
    @staticmethod
161
    def _is_dir(remote_dict):
162
        return 'application/directory' == remote_dict.get(
163
            'content_type', remote_dict.get('content-type', ''))
164

    
165
    @DontRaiseKeyError
166
    def _custom_container(self):
167
        return self.config.get_cloud(self.cloud, 'pithos_container')
168

    
169
    @DontRaiseKeyError
170
    def _custom_uuid(self):
171
        return self.config.get_cloud(self.cloud, 'pithos_uuid')
172

    
173
    def _set_account(self):
174
        self.account = self._custom_uuid()
175
        if self.account:
176
            return
177
        if getattr(self, 'auth_base', False):
178
            self.account = self.auth_base.user_term('id', self.token)
179
        else:
180
            astakos_url = self._custom_url('astakos')
181
            astakos_token = self._custom_token('astakos') or self.token
182
            if not astakos_url:
183
                raise CLIBaseUrlError(service='astakos')
184
            astakos = AstakosClient(astakos_url, astakos_token)
185
            self.account = astakos.user_term('id')
186

    
187
    @errors.generic.all
188
    @addLogSettings
189
    def _run(self):
190
        self.base_url = None
191
        if getattr(self, 'cloud', None):
192
            self.base_url = self._custom_url('pithos')
193
        else:
194
            self.cloud = 'default'
195
        self.token = self._custom_token('pithos')
196
        self.container = self._custom_container()
197

    
198
        if getattr(self, 'auth_base', False):
199
            self.token = self.token or self.auth_base.token
200
            if not self.base_url:
201
                pithos_endpoints = self.auth_base.get_service_endpoints(
202
                    self._custom_type('pithos') or 'object-store',
203
                    self._custom_version('pithos') or '')
204
                self.base_url = pithos_endpoints['publicURL']
205
        elif not self.base_url:
206
            raise CLIBaseUrlError(service='pithos')
207

    
208
        self._set_account()
209
        self.client = PithosClient(
210
            base_url=self.base_url,
211
            token=self.token,
212
            account=self.account,
213
            container=self.container)
214

    
215
    def main(self):
216
        self._run()
217

    
218

    
219
class _file_account_command(_pithos_init):
220
    """Base class for account level storage commands"""
221

    
222
    def __init__(self, arguments={}, auth_base=None, cloud=None):
223
        super(_file_account_command, self).__init__(
224
            arguments, auth_base, cloud)
225
        self['account'] = ValueArgument(
226
            'Set user account (not permanent)', ('-A', '--account'))
227

    
228
    def _run(self, custom_account=None):
229
        super(_file_account_command, self)._run()
230
        if custom_account:
231
            self.client.account = custom_account
232
        elif self['account']:
233
            self.client.account = self['account']
234

    
235
    @errors.generic.all
236
    def main(self):
237
        self._run()
238

    
239

    
240
class _file_container_command(_file_account_command):
241
    """Base class for container level storage commands"""
242

    
243
    container = None
244
    path = None
245

    
246
    def __init__(self, arguments={}, auth_base=None, cloud=None):
247
        super(_file_container_command, self).__init__(
248
            arguments, auth_base, cloud)
249
        self['container'] = ValueArgument(
250
            'Set container to work with (temporary)', ('-C', '--container'))
251

    
252
    def extract_container_and_path(
253
            self,
254
            container_with_path,
255
            path_is_optional=True):
256
        """Contains all heuristics for deciding what should be used as
257
        container or path. Options are:
258
        * user string of the form container:path
259
        * self.container, self.path variables set by super constructor, or
260
        explicitly by the caller application
261
        Error handling is explicit as these error cases happen only here
262
        """
263
        try:
264
            assert isinstance(container_with_path, str)
265
        except AssertionError as err:
266
            if self['container'] and path_is_optional:
267
                self.container = self['container']
268
                self.client.container = self['container']
269
                return
270
            raiseCLIError(err)
271

    
272
        user_cont, sep, userpath = container_with_path.partition(':')
273

    
274
        if sep:
275
            if not user_cont:
276
                raiseCLIError(CLISyntaxError(
277
                    'Container is missing\n',
278
                    details=errors.pithos.container_howto))
279
            alt_cont = self['container']
280
            if alt_cont and user_cont != alt_cont:
281
                raiseCLIError(CLISyntaxError(
282
                    'Conflict: 2 containers (%s, %s)' % (user_cont, alt_cont),
283
                    details=errors.pithos.container_howto)
284
                )
285
            self.container = user_cont
286
            if not userpath:
287
                raiseCLIError(CLISyntaxError(
288
                    'Path is missing for object in container %s' % user_cont,
289
                    details=errors.pithos.container_howto)
290
                )
291
            self.path = userpath
292
        else:
293
            alt_cont = self['container'] or self.client.container
294
            if alt_cont:
295
                self.container = alt_cont
296
                self.path = user_cont
297
            elif path_is_optional:
298
                self.container = user_cont
299
                self.path = None
300
            else:
301
                self.container = user_cont
302
                raiseCLIError(CLISyntaxError(
303
                    'Both container and path are required',
304
                    details=errors.pithos.container_howto)
305
                )
306

    
307
    @errors.generic.all
308
    def _run(self, container_with_path=None, path_is_optional=True):
309
        super(_file_container_command, self)._run()
310
        if self['container']:
311
            self.client.container = self['container']
312
            if container_with_path:
313
                self.path = container_with_path
314
            elif not path_is_optional:
315
                raise CLISyntaxError(
316
                    'Both container and path are required',
317
                    details=errors.pithos.container_howto)
318
        elif container_with_path:
319
            self.extract_container_and_path(
320
                container_with_path,
321
                path_is_optional)
322
            self.client.container = self.container
323
        self.container = self.client.container
324

    
325
    def main(self, container_with_path=None, path_is_optional=True):
326
        self._run(container_with_path, path_is_optional)
327

    
328

    
329
@command(pithos_cmds)
330
class file_list(_file_container_command, _optional_json, _name_filter):
331
    """List containers, object trees or objects in a directory
332
    Use with:
333
    1 no parameters : containers in current account
334
    2. one parameter (container) or --container : contents of container
335
    3. <container>:<prefix> or --container=<container> <prefix>: objects in
336
    .   container starting with prefix
337
    """
338

    
339
    arguments = dict(
340
        detail=FlagArgument('detailed output', ('-l', '--list')),
341
        limit=IntArgument('limit number of listed items', ('-n', '--number')),
342
        marker=ValueArgument('output greater that marker', '--marker'),
343
        prefix=ValueArgument('output starting with prefix', '--prefix'),
344
        delimiter=ValueArgument('show output up to delimiter', '--delimiter'),
345
        path=ValueArgument(
346
            'show output starting with prefix up to /', '--path'),
347
        meta=ValueArgument(
348
            'show output with specified meta keys', '--meta',
349
            default=[]),
350
        if_modified_since=ValueArgument(
351
            'show output modified since then', '--if-modified-since'),
352
        if_unmodified_since=ValueArgument(
353
            'show output not modified since then', '--if-unmodified-since'),
354
        until=DateArgument('show metadata until then', '--until'),
355
        format=ValueArgument(
356
            'format to parse until data (default: d/m/Y H:M:S )', '--format'),
357
        shared=FlagArgument('show only shared', '--shared'),
358
        more=FlagArgument(
359
            'output results in pages (-n to set items per page, default 10)',
360
            '--more'),
361
        exact_match=FlagArgument(
362
            'Show only objects that match exactly with path',
363
            '--exact-match'),
364
        enum=FlagArgument('Enumerate results', '--enumerate')
365
    )
366

    
367
    def print_objects(self, object_list):
368
        if self['json_output']:
369
            print_json(object_list)
370
            return
371
        limit = int(self['limit']) if self['limit'] > 0 else len(object_list)
372
        for index, obj in enumerate(object_list):
373
            if self['exact_match'] and self.path and not (
374
                    obj['name'] == self.path or 'content_type' in obj):
375
                continue
376
            pretty_obj = obj.copy()
377
            index += 1
378
            empty_space = ' ' * (len(str(len(object_list))) - len(str(index)))
379
            if 'subdir' in obj:
380
                continue
381
            if obj['content_type'] == 'application/directory':
382
                isDir = True
383
                size = 'D'
384
            else:
385
                isDir = False
386
                size = format_size(obj['bytes'])
387
                pretty_obj['bytes'] = '%s (%s)' % (obj['bytes'], size)
388
            oname = bold(obj['name'])
389
            prfx = ('%s%s. ' % (empty_space, index)) if self['enum'] else ''
390
            if self['detail']:
391
                print('%s%s' % (prfx, oname))
392
                print_dict(pretty_obj, exclude=('name'))
393
                print
394
            else:
395
                oname = '%s%9s %s' % (prfx, size, oname)
396
                oname += '/' if isDir else ''
397
                print(oname)
398
            if self['more']:
399
                page_hold(index, limit, len(object_list))
400

    
401
    def print_containers(self, container_list):
402
        if self['json_output']:
403
            print_json(container_list)
404
            return
405
        limit = int(self['limit']) if self['limit'] > 0\
406
            else len(container_list)
407
        for index, container in enumerate(container_list):
408
            if 'bytes' in container:
409
                size = format_size(container['bytes'])
410
            prfx = ('%s. ' % (index + 1)) if self['enum'] else ''
411
            cname = '%s%s' % (prfx, bold(container['name']))
412
            if self['detail']:
413
                print(cname)
414
                pretty_c = container.copy()
415
                if 'bytes' in container:
416
                    pretty_c['bytes'] = '%s (%s)' % (container['bytes'], size)
417
                print_dict(pretty_c, exclude=('name'))
418
                print
419
            else:
420
                if 'count' in container and 'bytes' in container:
421
                    print('%s (%s, %s objects)' % (
422
                        cname,
423
                        size,
424
                        container['count']))
425
                else:
426
                    print(cname)
427
            if self['more']:
428
                page_hold(index + 1, limit, len(container_list))
429

    
430
    @errors.generic.all
431
    @errors.pithos.connection
432
    @errors.pithos.object_path
433
    @errors.pithos.container
434
    def _run(self):
435
        if self.container is None:
436
            r = self.client.account_get(
437
                limit=False if self['more'] else self['limit'],
438
                marker=self['marker'],
439
                if_modified_since=self['if_modified_since'],
440
                if_unmodified_since=self['if_unmodified_since'],
441
                until=self['until'],
442
                show_only_shared=self['shared'])
443
            files = self._filter_by_name(r.json)
444
            self._print(files, self.print_containers)
445
        else:
446
            prefix = self.path or self['prefix'] or self['name_prefix']
447
            r = self.client.container_get(
448
                limit=False if self['more'] else self['limit'],
449
                marker=self['marker'],
450
                prefix=prefix,
451
                delimiter=self['delimiter'],
452
                path=self['path'],
453
                if_modified_since=self['if_modified_since'],
454
                if_unmodified_since=self['if_unmodified_since'],
455
                until=self['until'],
456
                meta=self['meta'],
457
                show_only_shared=self['shared'])
458
            files = self._filter_by_name(r.json)
459
            self._print(files, self.print_objects)
460

    
461
    def main(self, container____path__=None):
462
        super(self.__class__, self)._run(container____path__)
463
        self._run()
464

    
465

    
466
@command(pithos_cmds)
467
class file_mkdir(_file_container_command, _optional_output_cmd):
468
    """Create a directory
469
    Kamaki hanldes directories the same way as OOS Storage and Pithos+:
470
    A directory  is   an  object  with  type  "application/directory"
471
    An object with path  dir/name can exist even if  dir does not exist
472
    or even if dir  is  a non  directory  object.  Users can modify dir '
473
    without affecting the dir/name object in any way.
474
    """
475

    
476
    @errors.generic.all
477
    @errors.pithos.connection
478
    @errors.pithos.container
479
    def _run(self):
480
        self._optional_output(self.client.create_directory(self.path))
481

    
482
    def main(self, container___directory):
483
        super(self.__class__, self)._run(
484
            container___directory,
485
            path_is_optional=False)
486
        self._run()
487

    
488

    
489
@command(pithos_cmds)
490
class file_touch(_file_container_command, _optional_output_cmd):
491
    """Create an empty object (file)
492
    If object exists, this command will reset it to 0 length
493
    """
494

    
495
    arguments = dict(
496
        content_type=ValueArgument(
497
            'Set content type (default: application/octet-stream)',
498
            '--content-type',
499
            default='application/octet-stream')
500
    )
501

    
502
    @errors.generic.all
503
    @errors.pithos.connection
504
    @errors.pithos.container
505
    def _run(self):
506
        self._optional_output(
507
            self.client.create_object(self.path, self['content_type']))
508

    
509
    def main(self, container___path):
510
        super(file_touch, self)._run(
511
            container___path,
512
            path_is_optional=False)
513
        self._run()
514

    
515

    
516
@command(pithos_cmds)
517
class file_create(_file_container_command, _optional_output_cmd):
518
    """Create a container"""
519

    
520
    arguments = dict(
521
        versioning=ValueArgument(
522
            'set container versioning (auto/none)', '--versioning'),
523
        limit=IntArgument('set default container limit', '--limit'),
524
        meta=KeyValueArgument(
525
            'set container metadata (can be repeated)', '--meta')
526
    )
527

    
528
    @errors.generic.all
529
    @errors.pithos.connection
530
    @errors.pithos.container
531
    def _run(self, container):
532
        self._optional_output(self.client.create_container(
533
            container=container,
534
            sizelimit=self['limit'],
535
            versioning=self['versioning'],
536
            metadata=self['meta']))
537

    
538
    def main(self, container=None):
539
        super(self.__class__, self)._run(container)
540
        if container and self.container != container:
541
            raiseCLIError('Invalid container name %s' % container, details=[
542
                'Did you mean "%s" ?' % self.container,
543
                'Use --container for names containing :'])
544
        self._run(container)
545

    
546

    
547
class _source_destination_command(_file_container_command):
548

    
549
    arguments = dict(
550
        destination_account=ValueArgument('', ('-a', '--dst-account')),
551
        recursive=FlagArgument('', ('-R', '--recursive')),
552
        prefix=FlagArgument('', '--with-prefix', default=''),
553
        suffix=ValueArgument('', '--with-suffix', default=''),
554
        add_prefix=ValueArgument('', '--add-prefix', default=''),
555
        add_suffix=ValueArgument('', '--add-suffix', default=''),
556
        prefix_replace=ValueArgument('', '--prefix-to-replace', default=''),
557
        suffix_replace=ValueArgument('', '--suffix-to-replace', default=''),
558
    )
559

    
560
    def __init__(self, arguments={}, auth_base=None, cloud=None):
561
        self.arguments.update(arguments)
562
        super(_source_destination_command, self).__init__(
563
            self.arguments, auth_base, cloud)
564

    
565
    def _run(self, source_container___path, path_is_optional=False):
566
        super(_source_destination_command, self)._run(
567
            source_container___path,
568
            path_is_optional)
569
        self.dst_client = PithosClient(
570
            base_url=self.client.base_url,
571
            token=self.client.token,
572
            account=self['destination_account'] or self.client.account)
573

    
574
    @errors.generic.all
575
    @errors.pithos.account
576
    def _dest_container_path(self, dest_container_path):
577
        if self['destination_container']:
578
            self.dst_client.container = self['destination_container']
579
            return (self['destination_container'], dest_container_path)
580
        if dest_container_path:
581
            dst = dest_container_path.split(':')
582
            if len(dst) > 1:
583
                try:
584
                    self.dst_client.container = dst[0]
585
                    self.dst_client.get_container_info(dst[0])
586
                except ClientError as err:
587
                    if err.status in (404, 204):
588
                        raiseCLIError(
589
                            'Destination container %s not found' % dst[0])
590
                    raise
591
                else:
592
                    self.dst_client.container = dst[0]
593
                return (dst[0], dst[1])
594
            return(None, dst[0])
595
        raiseCLIError('No destination container:path provided')
596

    
597
    def _get_all(self, prefix):
598
        return self.client.container_get(prefix=prefix).json
599

    
600
    def _get_src_objects(self, src_path, source_version=None):
601
        """Get a list of the source objects to be called
602

603
        :param src_path: (str) source path
604

605
        :returns: (method, params) a method that returns a list when called
606
        or (object) if it is a single object
607
        """
608
        if src_path and src_path[-1] == '/':
609
            src_path = src_path[:-1]
610

    
611
        if self['prefix']:
612
            return (self._get_all, dict(prefix=src_path))
613
        try:
614
            srcobj = self.client.get_object_info(
615
                src_path, version=source_version)
616
        except ClientError as srcerr:
617
            if srcerr.status == 404:
618
                raiseCLIError(
619
                    'Source object %s not in source container %s' % (
620
                        src_path, self.client.container),
621
                    details=['Hint: --with-prefix to match multiple objects'])
622
            elif srcerr.status not in (204,):
623
                raise
624
            return (self.client.list_objects, {})
625

    
626
        if self._is_dir(srcobj):
627
            if not self['recursive']:
628
                raiseCLIError(
629
                    'Object %s of cont. %s is a dir' % (
630
                        src_path, self.client.container),
631
                    details=['Use --recursive to access directories'])
632
            return (self._get_all, dict(prefix=src_path))
633
        srcobj['name'] = src_path
634
        return srcobj
635

    
636
    def src_dst_pairs(self, dst_path, source_version=None):
637
        src_iter = self._get_src_objects(self.path, source_version)
638
        src_N = isinstance(src_iter, tuple)
639
        add_prefix = self['add_prefix'].strip('/')
640

    
641
        if dst_path and dst_path.endswith('/'):
642
            dst_path = dst_path[:-1]
643

    
644
        try:
645
            dstobj = self.dst_client.get_object_info(dst_path)
646
        except ClientError as trgerr:
647
            if trgerr.status in (404,):
648
                if src_N:
649
                    raiseCLIError(
650
                        'Cannot merge multiple paths to path %s' % dst_path,
651
                        details=[
652
                            'Try to use / or a directory as destination',
653
                            'or create the destination dir (/file mkdir)',
654
                            'or use a single object as source'])
655
            elif trgerr.status not in (204,):
656
                raise
657
        else:
658
            if self._is_dir(dstobj):
659
                add_prefix = '%s/%s' % (dst_path.strip('/'), add_prefix)
660
            elif src_N:
661
                raiseCLIError(
662
                    'Cannot merge multiple paths to path' % dst_path,
663
                    details=[
664
                        'Try to use / or a directory as destination',
665
                        'or create the destination dir (/file mkdir)',
666
                        'or use a single object as source'])
667

    
668
        if src_N:
669
            (method, kwargs) = src_iter
670
            for obj in method(**kwargs):
671
                name = obj['name']
672
                if name.endswith(self['suffix']):
673
                    yield (name, self._get_new_object(name, add_prefix))
674
        elif src_iter['name'].endswith(self['suffix']):
675
            name = src_iter['name']
676
            yield (name, self._get_new_object(dst_path or name, add_prefix))
677
        else:
678
            raiseCLIError('Source path %s conflicts with suffix %s' % (
679
                src_iter['name'], self['suffix']))
680

    
681
    def _get_new_object(self, obj, add_prefix):
682
        if self['prefix_replace'] and obj.startswith(self['prefix_replace']):
683
            obj = obj[len(self['prefix_replace']):]
684
        if self['suffix_replace'] and obj.endswith(self['suffix_replace']):
685
            obj = obj[:-len(self['suffix_replace'])]
686
        return add_prefix + obj + self['add_suffix']
687

    
688

    
689
@command(pithos_cmds)
690
class file_copy(_source_destination_command, _optional_output_cmd):
691
    """Copy objects from container to (another) container
692
    Semantics:
693
    copy cont:path dir
694
    .   transfer path as dir/path
695
    copy cont:path cont2:
696
    .   trasnfer all <obj> prefixed with path to container cont2
697
    copy cont:path [cont2:]path2
698
    .   transfer path to path2
699
    Use options:
700
    1. <container1>:<path1> [container2:]<path2> : if container2 is not given,
701
    destination is container1:path2
702
    2. <container>:<path1> <path2> : make a copy in the same container
703
    3. Can use --container= instead of <container1>
704
    """
705

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

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

    
763
    def main(
764
            self, source_container___path,
765
            destination_container___path=None):
766
        super(file_copy, self)._run(
767
            source_container___path,
768
            path_is_optional=False)
769
        (dst_cont, dst_path) = self._dest_container_path(
770
            destination_container___path)
771
        self.dst_client.container = dst_cont or self.container
772
        self._run(dst_path=dst_path or '')
773

    
774

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

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

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

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

    
858

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

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

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

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

    
895

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

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

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

    
912

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

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

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

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

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

    
963

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

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

    
994
    @errors.generic.all
995
    @errors.pithos.connection
996
    @errors.pithos.container
997
    @errors.pithos.object_path
998
    def _run(self):
999
        ctype, cenc = guess_mime_type(self.path)
1000
        self._optional_output(self.client.create_object_by_manifestation(
1001
            self.path,
1002
            content_encoding=self['content_encoding'] or cenc,
1003
            content_disposition=self['content_disposition'],
1004
            content_type=self['content_type'] or ctype,
1005
            sharing=self['sharing'],
1006
            public=self['public']))
1007

    
1008
    def main(self, container___path):
1009
        super(self.__class__, self)._run(
1010
            container___path, path_is_optional=False)
1011
        self.run()
1012

    
1013

    
1014
@command(pithos_cmds)
1015
class file_upload(_file_container_command, _optional_output_cmd):
1016
    """Upload a file"""
1017

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

    
1045
    def _check_container_limit(self, path):
1046
        cl_dict = self.client.get_container_limit()
1047
        container_limit = int(cl_dict['x-container-policy-quota'])
1048
        r = self.client.container_get()
1049
        used_bytes = sum(int(o['bytes']) for o in r.json)
1050
        path_size = get_path_size(path)
1051
        if container_limit and path_size > (container_limit - used_bytes):
1052
            raiseCLIError(
1053
                'Container(%s) (limit(%s) - used(%s)) < size(%s) of %s' % (
1054
                    self.client.container,
1055
                    format_size(container_limit),
1056
                    format_size(used_bytes),
1057
                    format_size(path_size),
1058
                    path),
1059
                importance=1, details=[
1060
                    'Check accound limit: /file quota',
1061
                    'Check container limit:',
1062
                    '\t/file containerlimit get %s' % self.client.container,
1063
                    'Increase container limit:',
1064
                    '\t/file containerlimit set <new limit> %s' % (
1065
                        self.client.container)])
1066

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

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

    
1192
    def main(self, local_path, container____path__=None):
1193
        super(self.__class__, self)._run(container____path__)
1194
        remote_path = self.path or path.basename(path.abspath(local_path))
1195
        self._run(local_path=local_path, remote_path=remote_path)
1196

    
1197

    
1198
@command(pithos_cmds)
1199
class file_cat(_file_container_command):
1200
    """Print remote file contents to console"""
1201

    
1202
    arguments = dict(
1203
        range=RangeArgument('show range of data', '--range'),
1204
        if_match=ValueArgument('show output if ETags match', '--if-match'),
1205
        if_none_match=ValueArgument(
1206
            'show output if ETags match', '--if-none-match'),
1207
        if_modified_since=DateArgument(
1208
            'show output modified since then', '--if-modified-since'),
1209
        if_unmodified_since=DateArgument(
1210
            'show output unmodified since then', '--if-unmodified-since'),
1211
        object_version=ValueArgument(
1212
            'get the specific version', ('-O', '--object-version'))
1213
    )
1214

    
1215
    @errors.generic.all
1216
    @errors.pithos.connection
1217
    @errors.pithos.container
1218
    @errors.pithos.object_path
1219
    def _run(self):
1220
        self.client.download_object(
1221
            self.path,
1222
            stdout,
1223
            range_str=self['range'],
1224
            version=self['object_version'],
1225
            if_match=self['if_match'],
1226
            if_none_match=self['if_none_match'],
1227
            if_modified_since=self['if_modified_since'],
1228
            if_unmodified_since=self['if_unmodified_since'])
1229

    
1230
    def main(self, container___path):
1231
        super(self.__class__, self)._run(
1232
            container___path, path_is_optional=False)
1233
        self._run()
1234

    
1235

    
1236
@command(pithos_cmds)
1237
class file_download(_file_container_command):
1238
    """Download remote object as local file
1239
    If local destination is a directory:
1240
    *   download <container>:<path> <local dir> -R
1241
    will download all files on <container> prefixed as <path>,
1242
    to <local dir>/<full path> (or <local dir>\<full path> in windows)
1243
    *   download <container>:<path> <local dir> --exact-match
1244
    will download only one file, exactly matching <path>
1245
    ATTENTION: to download cont:dir1/dir2/file there must exist objects
1246
    cont:dir1 and cont:dir1/dir2 of type application/directory
1247
    To create directory objects, use /file mkdir
1248
    """
1249

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

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

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

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

    
1425
    def main(self, container___path, local_path=None):
1426
        super(self.__class__, self)._run(container___path)
1427
        self._run(local_path=local_path)
1428

    
1429

    
1430
@command(pithos_cmds)
1431
class file_hashmap(_file_container_command, _optional_json):
1432
    """Get the hash-map of an object"""
1433

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

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

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

    
1465

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

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

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

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

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

    
1528

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

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

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

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

    
1573

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

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

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

    
1590

    
1591
@command(pithos_cmds)
1592
class file_unpublish(_file_container_command, _optional_output_cmd):
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._optional_output(self.client.unpublish_object(self.path))
1601

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

    
1607

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

    
1616

    
1617
def print_permissions(permissions_dict):
1618
    expected_keys = ('read', 'write')
1619
    if set(permissions_dict).issubset(expected_keys):
1620
        print_dict(permissions_dict)
1621
    else:
1622
        invalid_keys = set(permissions_dict.keys()).difference(expected_keys)
1623
        raiseCLIError(
1624
            'Illegal permission keys: %s' % ', '.join(invalid_keys),
1625
            importance=1, details=[
1626
                'Valid permission types: %s' % ' '.join(expected_keys)])
1627

    
1628

    
1629
@command(pithos_cmds)
1630
class file_permissions_get(_file_container_command, _optional_json):
1631
    """Get read and write permissions of an object"""
1632

    
1633
    @errors.generic.all
1634
    @errors.pithos.connection
1635
    @errors.pithos.container
1636
    @errors.pithos.object_path
1637
    def _run(self):
1638
        self._print(
1639
            self.client.get_object_sharing(self.path), print_permissions)
1640

    
1641
    def main(self, container___path):
1642
        super(self.__class__, self)._run(
1643
            container___path, path_is_optional=False)
1644
        self._run()
1645

    
1646

    
1647
@command(pithos_cmds)
1648
class file_permissions_set(_file_container_command, _optional_output_cmd):
1649
    """Set permissions for an object
1650
    New permissions overwrite existing permissions.
1651
    Permission format:
1652
    -   read=<username>[,usergroup[,...]]
1653
    -   write=<username>[,usegroup[,...]]
1654
    E.g. to give read permissions for file F to users A and B and write for C:
1655
    .       /file permissions set F read=A,B write=C
1656
    """
1657

    
1658
    @errors.generic.all
1659
    def format_permission_dict(self, permissions):
1660
        read = False
1661
        write = False
1662
        for perms in permissions:
1663
            splstr = perms.split('=')
1664
            if 'read' == splstr[0]:
1665
                read = [ug.strip() for ug in splstr[1].split(',')]
1666
            elif 'write' == splstr[0]:
1667
                write = [ug.strip() for ug in splstr[1].split(',')]
1668
            else:
1669
                msg = 'Usage:\tread=<groups,users> write=<groups,users>'
1670
                raiseCLIError(None, msg)
1671
        return (read, write)
1672

    
1673
    @errors.generic.all
1674
    @errors.pithos.connection
1675
    @errors.pithos.container
1676
    @errors.pithos.object_path
1677
    def _run(self, read, write):
1678
        self._optional_output(self.client.set_object_sharing(
1679
            self.path, read_permission=read, write_permission=write))
1680

    
1681
    def main(self, container___path, *permissions):
1682
        super(self.__class__, self)._run(
1683
            container___path, path_is_optional=False)
1684
        read, write = self.format_permission_dict(permissions)
1685
        self._run(read, write)
1686

    
1687

    
1688
@command(pithos_cmds)
1689
class file_permissions_delete(_file_container_command, _optional_output_cmd):
1690
    """Delete all permissions set on object
1691
    To modify permissions, use /file permissions set
1692
    """
1693

    
1694
    @errors.generic.all
1695
    @errors.pithos.connection
1696
    @errors.pithos.container
1697
    @errors.pithos.object_path
1698
    def _run(self):
1699
        self._optional_output(self.client.del_object_sharing(self.path))
1700

    
1701
    def main(self, container___path):
1702
        super(self.__class__, self)._run(
1703
            container___path, path_is_optional=False)
1704
        self._run()
1705

    
1706

    
1707
@command(pithos_cmds)
1708
class file_info(_file_container_command, _optional_json):
1709
    """Get detailed information for user account, containers or objects
1710
    to get account info:    /file info
1711
    to get container info:  /file info <container>
1712
    to get object info:     /file info <container>:<path>
1713
    """
1714

    
1715
    arguments = dict(
1716
        object_version=ValueArgument(
1717
            'show specific version \ (applies only for objects)',
1718
            ('-O', '--object-version'))
1719
    )
1720

    
1721
    @errors.generic.all
1722
    @errors.pithos.connection
1723
    @errors.pithos.container
1724
    @errors.pithos.object_path
1725
    def _run(self):
1726
        if self.container is None:
1727
            r = self.client.get_account_info()
1728
        elif self.path is None:
1729
            r = self.client.get_container_info(self.container)
1730
        else:
1731
            r = self.client.get_object_info(
1732
                self.path, version=self['object_version'])
1733
        self._print(r, print_dict)
1734

    
1735
    def main(self, container____path__=None):
1736
        super(self.__class__, self)._run(container____path__)
1737
        self._run()
1738

    
1739

    
1740
@command(pithos_cmds)
1741
class file_metadata(_pithos_init):
1742
    """Metadata are attached on objects. They are formed as key:value pairs.
1743
    They can have arbitary values.
1744
    """
1745

    
1746

    
1747
@command(pithos_cmds)
1748
class file_metadata_get(_file_container_command, _optional_json):
1749
    """Get metadata for account, containers or objects"""
1750

    
1751
    arguments = dict(
1752
        detail=FlagArgument('show detailed output', ('-l', '--details')),
1753
        until=DateArgument('show metadata until then', '--until'),
1754
        object_version=ValueArgument(
1755
            'show specific version (applies only for objects)',
1756
            ('-O', '--object-version'))
1757
    )
1758

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

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

    
1795

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

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

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

    
1817

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

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

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

    
1843

    
1844
@command(pithos_cmds)
1845
class file_quota(_file_account_command, _optional_json):
1846
    """Get account quota"""
1847

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

    
1852
    @errors.generic.all
1853
    @errors.pithos.connection
1854
    def _run(self):
1855

    
1856
        def pretty_print(output):
1857
            if not self['in_bytes']:
1858
                for k in output:
1859
                    output[k] = format_size(output[k])
1860
            print_dict(output, '-')
1861

    
1862
        self._print(self.client.get_account_quota(), pretty_print)
1863

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

    
1868

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

    
1873

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

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

    
1882
    @errors.generic.all
1883
    @errors.pithos.container
1884
    def _run(self):
1885

    
1886
        def pretty_print(output):
1887
            if not self['in_bytes']:
1888
                for k, v in output.items():
1889
                    output[k] = 'unlimited' if '0' == v else format_size(v)
1890
            print_dict(output, '-')
1891

    
1892
        self._print(
1893
            self.client.get_container_limit(self.container), pretty_print)
1894

    
1895
    def main(self, container=None):
1896
        super(self.__class__, self)._run()
1897
        self.container = container
1898
        self._run()
1899

    
1900

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

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

    
1935
    @errors.generic.all
1936
    @errors.pithos.connection
1937
    @errors.pithos.container
1938
    def _run(self, limit):
1939
        if self.container:
1940
            self.client.container = self.container
1941
        self._optional_output(self.client.set_container_limit(limit))
1942

    
1943
    def main(self, limit, container=None):
1944
        super(self.__class__, self)._run()
1945
        limit = self._calculate_limit(limit)
1946
        self.container = container
1947
        self._run(limit)
1948

    
1949

    
1950
@command(pithos_cmds)
1951
class file_versioning(_pithos_init):
1952
    """Manage the versioning scheme of current pithos user account"""
1953

    
1954

    
1955
@command(pithos_cmds)
1956
class file_versioning_get(_file_account_command, _optional_json):
1957
    """Get  versioning for account or container"""
1958

    
1959
    @errors.generic.all
1960
    @errors.pithos.connection
1961
    @errors.pithos.container
1962
    def _run(self):
1963
        self._print(
1964
            self.client.get_container_versioning(self.container), print_dict)
1965

    
1966
    def main(self, container):
1967
        super(self.__class__, self)._run()
1968
        self.container = container
1969
        self._run()
1970

    
1971

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

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

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

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

    
1994

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

    
1999

    
2000
@command(pithos_cmds)
2001
class file_group_list(_file_account_command, _optional_json):
2002
    """list all groups and group members"""
2003

    
2004
    @errors.generic.all
2005
    @errors.pithos.connection
2006
    def _run(self):
2007
        self._print(self.client.get_account_group(), print_dict, delim='-')
2008

    
2009
    def main(self):
2010
        super(self.__class__, self)._run()
2011
        self._run()
2012

    
2013

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

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

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

    
2030

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

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

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

    
2044

    
2045
@command(pithos_cmds)
2046
class file_sharers(_file_account_command, _optional_json):
2047
    """List the accounts that share objects with current user"""
2048

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

    
2054
    @errors.generic.all
2055
    @errors.pithos.connection
2056
    def _run(self):
2057
        accounts = self.client.get_sharing_accounts(marker=self['marker'])
2058
        uuids = [acc['name'] for acc in accounts]
2059
        try:
2060
            astakos_responce = self.auth_base.post_user_catalogs(uuids)
2061
            usernames = astakos_responce.json
2062
            r = usernames['uuid_catalog']
2063
        except Exception as e:
2064
            print 'WARNING: failed to call user_catalogs, %s' % e
2065
            r = dict(sharer_uuid=uuids)
2066
            usernames = accounts
2067
        if self['json_output'] or self['detail']:
2068
            self._print(usernames)
2069
        else:
2070
            self._print(r, print_dict)
2071

    
2072
    def main(self):
2073
        super(self.__class__, self)._run()
2074
        self._run()
2075

    
2076

    
2077
def version_print(versions):
2078
    print_items([dict(id=vitem[0], created=strftime(
2079
        '%d-%m-%Y %H:%M:%S',
2080
        localtime(float(vitem[1])))) for vitem in versions])
2081

    
2082

    
2083
@command(pithos_cmds)
2084
class file_versions(_file_container_command, _optional_json):
2085
    """Get the list of object versions
2086
    Deleted objects may still have versions that can be used to restore it and
2087
    get information about its previous state.
2088
    The version number can be used in a number of other commands, like info,
2089
    copy, move, meta. See these commands for more information, e.g.
2090
    /file info -h
2091
    """
2092

    
2093
    @errors.generic.all
2094
    @errors.pithos.connection
2095
    @errors.pithos.container
2096
    @errors.pithos.object_path
2097
    def _run(self):
2098
        self._print(
2099
            self.client.get_object_versionlist(self.path), version_print)
2100

    
2101
    def main(self, container___path):
2102
        super(file_versions, self)._run(
2103
            container___path,
2104
            path_is_optional=False)
2105
        self._run()