Statistics
| Branch: | Tag: | Revision:

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

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
        delimiter=ValueArgument('show output up to delimiter', '--delimiter'),
344
        path=ValueArgument(
345
            'show output starting with prefix up to /', '--path'),
346
        meta=ValueArgument(
347
            'show output with specified meta keys', '--meta',
348
            default=[]),
349
        if_modified_since=ValueArgument(
350
            'show output modified since then', '--if-modified-since'),
351
        if_unmodified_since=ValueArgument(
352
            'show output not modified since then', '--if-unmodified-since'),
353
        until=DateArgument('show metadata until then', '--until'),
354
        format=ValueArgument(
355
            'format to parse until data (default: d/m/Y H:M:S )', '--format'),
356
        shared=FlagArgument('show only shared', '--shared'),
357
        more=FlagArgument(
358
            'output results in pages (-n to set items per page, default 10)',
359
            '--more'),
360
        exact_match=FlagArgument(
361
            'Show only objects that match exactly with path',
362
            '--exact-match'),
363
        enum=FlagArgument('Enumerate results', '--enumerate')
364
    )
365

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

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

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

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

    
464

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

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

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

    
487

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

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

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

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

    
514

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

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

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

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

    
545

    
546
class _source_destination_command(_file_container_command):
547

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

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

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

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

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

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

602
        :param src_path: (str) source path
603

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

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

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

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

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

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

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

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

    
687

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

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

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

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

    
773

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

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

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

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

    
857

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

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

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

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

    
894

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

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

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

    
911

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

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

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

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

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

    
962

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

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

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

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

    
1012

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

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

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

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

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

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

    
1196

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

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

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

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

    
1234

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

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

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

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

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

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

    
1428

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

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

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

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

    
1464

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

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

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

    
1499
    @errors.generic.all
1500
    @errors.pithos.connection
1501
    @errors.pithos.container
1502
    @errors.pithos.object_path
1503
    def _run(self):
1504
        if self.path:
1505
            if self['yes'] or ask_user(
1506
                    'Delete %s:%s ?' % (self.container, self.path)):
1507
                self._optional_output(self.client.del_object(
1508
                    self.path,
1509
                    until=self['until'], delimiter=self['delimiter']))
1510
            else:
1511
                print('Aborted')
1512
        else:
1513
            if self['recursive']:
1514
                ask_msg = 'Delete container contents'
1515
            else:
1516
                ask_msg = 'Delete container'
1517
            if self['yes'] or ask_user('%s %s ?' % (ask_msg, self.container)):
1518
                self._optional_output(self.client.del_container(
1519
                    until=self['until'], 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, _optional_output_cmd):
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
                r = 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
                        r = self.client.purge_container()
1556
                    else:
1557
                        raiseCLIError(ce, details=['Try -F to force-purge'])
1558
                else:
1559
                    raise
1560
            self._optional_output(r)
1561
        else:
1562
            print('Aborted')
1563

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

    
1572

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

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

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

    
1589

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

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

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

    
1606

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

    
1615

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

    
1627

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

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

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

    
1645

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

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

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

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

    
1686

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

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

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

    
1705

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

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

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

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

    
1738

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

    
1745

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

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

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

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

    
1794

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

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

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

    
1816

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

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

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

    
1842

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

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

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

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

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

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

    
1867

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

    
1872

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

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

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

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

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

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

    
1899

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

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

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

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

    
1948

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

    
1953

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

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

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

    
1970

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

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

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

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

    
1993

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

    
1998

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

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

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

    
2012

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

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

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

    
2029

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

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

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

    
2043

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

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

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

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

    
2075

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

    
2081

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

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

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