Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / pithos.py @ 1757c616

History | View | Annotate | Download (77.7 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 time import localtime, strftime
35
from os import path, makedirs, walk
36
from io import StringIO
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, pager, bold, ask_user,
43
    get_path_size, 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

    
61
class SharingArgument(ValueArgument):
62
    """Set sharing (read and/or write) groups
63
    .
64
    :value type: "read=term1,term2,... write=term1,term2,..."
65
    .
66
    :value returns: {'read':['term1', 'term2', ...],
67
    .   'write':['term1', 'term2', ...]}
68
    """
69

    
70
    @property
71
    def value(self):
72
        return getattr(self, '_value', self.default)
73

    
74
    @value.setter
75
    def value(self, newvalue):
76
        perms = {}
77
        try:
78
            permlist = newvalue.split(' ')
79
        except AttributeError:
80
            return
81
        for p in permlist:
82
            try:
83
                (key, val) = p.split('=')
84
            except ValueError as err:
85
                raiseCLIError(
86
                    err,
87
                    'Error in --sharing',
88
                    details='Incorrect format',
89
                    importance=1)
90
            if key.lower() not in ('read', 'write'):
91
                msg = 'Error in --sharing'
92
                raiseCLIError(err, msg, importance=1, details=[
93
                    'Invalid permission key %s' % key])
94
            val_list = val.split(',')
95
            if not key in perms:
96
                perms[key] = []
97
            for item in val_list:
98
                if item not in perms[key]:
99
                    perms[key].append(item)
100
        self._value = perms
101

    
102

    
103
class RangeArgument(ValueArgument):
104
    """
105
    :value type: string of the form <start>-<end> where <start> and <end> are
106
        integers
107
    :value returns: the input string, after type checking <start> and <end>
108
    """
109

    
110
    @property
111
    def value(self):
112
        return getattr(self, '_value', self.default)
113

    
114
    @value.setter
115
    def value(self, newvalues):
116
        if not newvalues:
117
            self._value = self.default
118
            return
119
        self._value = ''
120
        for newvalue in newvalues.split(','):
121
            self._value = ('%s,' % self._value) if self._value else ''
122
            start, sep, end = newvalue.partition('-')
123
            if sep:
124
                if start:
125
                    start, end = (int(start), int(end))
126
                    assert start <= end, 'Invalid range value %s' % newvalue
127
                    self._value += '%s-%s' % (int(start), int(end))
128
                else:
129
                    self._value += '-%s' % int(end)
130
            else:
131
                self._value += '%s' % int(start)
132

    
133

    
134
# Command specs
135

    
136

    
137
class _pithos_init(_command_init):
138
    """Initialize a pithos+ kamaki client"""
139

    
140
    @staticmethod
141
    def _is_dir(remote_dict):
142
        return 'application/directory' == remote_dict.get(
143
            'content_type', remote_dict.get('content-type', ''))
144

    
145
    @DontRaiseKeyError
146
    def _custom_container(self):
147
        return self.config.get_cloud(self.cloud, 'pithos_container')
148

    
149
    @DontRaiseKeyError
150
    def _custom_uuid(self):
151
        return self.config.get_cloud(self.cloud, 'pithos_uuid')
152

    
153
    def _set_account(self):
154
        self.account = self._custom_uuid()
155
        if self.account:
156
            return
157
        if getattr(self, 'auth_base', False):
158
            self.account = self.auth_base.user_term('id', self.token)
159
        else:
160
            astakos_url = self._custom_url('astakos')
161
            astakos_token = self._custom_token('astakos') or self.token
162
            if not astakos_url:
163
                raise CLIBaseUrlError(service='astakos')
164
            astakos = AstakosClient(astakos_url, astakos_token)
165
            self.account = astakos.user_term('id')
166

    
167
    @errors.generic.all
168
    @addLogSettings
169
    def _run(self):
170
        self.base_url = None
171
        if getattr(self, 'cloud', None):
172
            self.base_url = self._custom_url('pithos')
173
        else:
174
            self.cloud = 'default'
175
        self.token = self._custom_token('pithos')
176
        self.container = self._custom_container()
177

    
178
        if getattr(self, 'auth_base', False):
179
            self.token = self.token or self.auth_base.token
180
            if not self.base_url:
181
                pithos_endpoints = self.auth_base.get_service_endpoints(
182
                    self._custom_type('pithos') or 'object-store',
183
                    self._custom_version('pithos') or '')
184
                self.base_url = pithos_endpoints['publicURL']
185
        elif not self.base_url:
186
            raise CLIBaseUrlError(service='pithos')
187

    
188
        self._set_account()
189
        self.client = PithosClient(
190
            base_url=self.base_url,
191
            token=self.token,
192
            account=self.account,
193
            container=self.container)
194

    
195
    def main(self):
196
        self._run()
197

    
198

    
199
class _file_account_command(_pithos_init):
200
    """Base class for account level storage commands"""
201

    
202
    def __init__(self, arguments={}, auth_base=None, cloud=None):
203
        super(_file_account_command, self).__init__(
204
            arguments, auth_base, cloud)
205
        self['account'] = ValueArgument(
206
            'Set user account (not permanent)', ('-A', '--account'))
207

    
208
    def _run(self, custom_account=None):
209
        super(_file_account_command, self)._run()
210
        if custom_account:
211
            self.client.account = custom_account
212
        elif self['account']:
213
            self.client.account = self['account']
214

    
215
    @errors.generic.all
216
    def main(self):
217
        self._run()
218

    
219

    
220
class _file_container_command(_file_account_command):
221
    """Base class for container level storage commands"""
222

    
223
    container = None
224
    path = None
225

    
226
    def __init__(self, arguments={}, auth_base=None, cloud=None):
227
        super(_file_container_command, self).__init__(
228
            arguments, auth_base, cloud)
229
        self['container'] = ValueArgument(
230
            'Set container to work with (temporary)', ('-C', '--container'))
231

    
232
    def extract_container_and_path(
233
            self,
234
            container_with_path,
235
            path_is_optional=True):
236
        """Contains all heuristics for deciding what should be used as
237
        container or path. Options are:
238
        * user string of the form container:path
239
        * self.container, self.path variables set by super constructor, or
240
        explicitly by the caller application
241
        Error handling is explicit as these error cases happen only here
242
        """
243
        try:
244
            assert isinstance(container_with_path, str)
245
        except AssertionError as err:
246
            if self['container'] and path_is_optional:
247
                self.container = self['container']
248
                self.client.container = self['container']
249
                return
250
            raiseCLIError(err)
251

    
252
        user_cont, sep, userpath = container_with_path.partition(':')
253

    
254
        if sep:
255
            if not user_cont:
256
                raiseCLIError(CLISyntaxError(
257
                    'Container is missing\n',
258
                    details=errors.pithos.container_howto))
259
            alt_cont = self['container']
260
            if alt_cont and user_cont != alt_cont:
261
                raiseCLIError(CLISyntaxError(
262
                    'Conflict: 2 containers (%s, %s)' % (user_cont, alt_cont),
263
                    details=errors.pithos.container_howto)
264
                )
265
            self.container = user_cont
266
            if not userpath:
267
                raiseCLIError(CLISyntaxError(
268
                    'Path is missing for object in container %s' % user_cont,
269
                    details=errors.pithos.container_howto)
270
                )
271
            self.path = userpath
272
        else:
273
            alt_cont = self['container'] or self.client.container
274
            if alt_cont:
275
                self.container = alt_cont
276
                self.path = user_cont
277
            elif path_is_optional:
278
                self.container = user_cont
279
                self.path = None
280
            else:
281
                self.container = user_cont
282
                raiseCLIError(CLISyntaxError(
283
                    'Both container and path are required',
284
                    details=errors.pithos.container_howto)
285
                )
286

    
287
    @errors.generic.all
288
    def _run(self, container_with_path=None, path_is_optional=True):
289
        super(_file_container_command, self)._run()
290
        if self['container']:
291
            self.client.container = self['container']
292
            if container_with_path:
293
                self.path = container_with_path
294
            elif not path_is_optional:
295
                raise CLISyntaxError(
296
                    'Both container and path are required',
297
                    details=errors.pithos.container_howto)
298
        elif container_with_path:
299
            self.extract_container_and_path(
300
                container_with_path,
301
                path_is_optional)
302
            self.client.container = self.container
303
        self.container = self.client.container
304

    
305
    def main(self, container_with_path=None, path_is_optional=True):
306
        self._run(container_with_path, path_is_optional)
307

    
308

    
309
@command(pithos_cmds)
310
class file_list(_file_container_command, _optional_json, _name_filter):
311
    """List containers, object trees or objects in a directory
312
    Use with:
313
    1 no parameters : containers in current account
314
    2. one parameter (container) or --container : contents of container
315
    3. <container>:<prefix> or --container=<container> <prefix>: objects in
316
    .   container starting with prefix
317
    """
318

    
319
    arguments = dict(
320
        detail=FlagArgument('detailed output', ('-l', '--list')),
321
        limit=IntArgument('limit number of listed items', ('-n', '--number')),
322
        marker=ValueArgument('output greater that marker', '--marker'),
323
        delimiter=ValueArgument('show output up to delimiter', '--delimiter'),
324
        path=ValueArgument(
325
            'show output starting with prefix up to /', '--path'),
326
        meta=ValueArgument(
327
            'show output with specified meta keys', '--meta',
328
            default=[]),
329
        if_modified_since=ValueArgument(
330
            'show output modified since then', '--if-modified-since'),
331
        if_unmodified_since=ValueArgument(
332
            'show output not modified since then', '--if-unmodified-since'),
333
        until=DateArgument('show metadata until then', '--until'),
334
        format=ValueArgument(
335
            'format to parse until data (default: d/m/Y H:M:S )', '--format'),
336
        shared=FlagArgument('show only shared', '--shared'),
337
        more=FlagArgument('read long results', '--more'),
338
        exact_match=FlagArgument(
339
            'Show only objects that match exactly with path',
340
            '--exact-match'),
341
        enum=FlagArgument('Enumerate results', '--enumerate')
342
    )
343

    
344
    def print_objects(self, object_list):
345
        for index, obj in enumerate(object_list):
346
            if self['exact_match'] and self.path and not (
347
                    obj['name'] == self.path or 'content_type' in obj):
348
                continue
349
            pretty_obj = obj.copy()
350
            index += 1
351
            empty_space = ' ' * (len(str(len(object_list))) - len(str(index)))
352
            if 'subdir' in obj:
353
                continue
354
            if obj['content_type'] == 'application/directory':
355
                isDir = True
356
                size = 'D'
357
            else:
358
                isDir = False
359
                size = format_size(obj['bytes'])
360
                pretty_obj['bytes'] = '%s (%s)' % (obj['bytes'], size)
361
            oname = obj['name'] if self['more'] else bold(obj['name'])
362
            prfx = (
363
                '%s%s. ' % (empty_space, index)) if self['enum'] else ''
364
            if self['detail']:
365
                self._out.writelines(u'%s%s\n' % (prfx, oname))
366
                print_dict(pretty_obj, exclude=('name'), out=self._out)
367
                self._out.writelines(u'\n')
368
            else:
369
                oname = u'%s%9s %s' % (prfx, size, oname)
370
                oname += u'/' if isDir else u''
371
                self._out.writelines(oname + u'\n')
372

    
373
    def print_containers(self, container_list):
374
        for index, container in enumerate(container_list):
375
            if 'bytes' in container:
376
                size = format_size(container['bytes'])
377
            prfx = ('%s. ' % (index + 1)) if self['enum'] else ''
378
            _cname = container['name'] if (
379
                self['more']) else bold(container['name'])
380
            cname = u'%s%s' % (prfx, _cname)
381
            if self['detail']:
382
                self._out.writelines(cname + u'\n')
383
                pretty_c = container.copy()
384
                if 'bytes' in container:
385
                    pretty_c['bytes'] = '%s (%s)' % (container['bytes'], size)
386
                print_dict(pretty_c, exclude=('name'), out=self._out)
387
                self._out.writelines(u'\n')
388
            else:
389
                if 'count' in container and 'bytes' in container:
390
                    self._out.writelines(u'%s (%s, %s objects)\n' % (
391
                        cname, size, container['count']))
392
                else:
393
                    self._out.writelines(cname + '\n')
394

    
395
    @errors.generic.all
396
    @errors.pithos.connection
397
    @errors.pithos.object_path
398
    @errors.pithos.container
399
    def _run(self):
400
        files, prnt = None, None
401
        if self.container is None:
402
            r = self.client.account_get(
403
                limit=False if self['more'] else self['limit'],
404
                marker=self['marker'],
405
                if_modified_since=self['if_modified_since'],
406
                if_unmodified_since=self['if_unmodified_since'],
407
                until=self['until'],
408
                show_only_shared=self['shared'])
409
            files, prnt = self._filter_by_name(r.json), self.print_containers
410
        else:
411
            prefix = (self.path and not self['name']) or self['name_pref']
412
            r = self.client.container_get(
413
                limit=False if self['more'] else self['limit'],
414
                marker=self['marker'],
415
                prefix=prefix,
416
                delimiter=self['delimiter'],
417
                path=self['path'],
418
                if_modified_since=self['if_modified_since'],
419
                if_unmodified_since=self['if_unmodified_since'],
420
                until=self['until'],
421
                meta=self['meta'],
422
                show_only_shared=self['shared'])
423
            files, prnt = self._filter_by_name(r.json), self.print_objects
424
        if self['more']:
425
            outbu, self._out = self._out, StringIO()
426
        try:
427
            if self['json_output']:
428
                self._print(files)
429
            else:
430
                prnt(files)
431
        finally:
432
            if self['more']:
433
                pager(self._out.getvalue())
434
                self._out = outbu
435

    
436
    def main(self, container____path__=None):
437
        super(self.__class__, self)._run(container____path__)
438
        self._run()
439

    
440

    
441
@command(pithos_cmds)
442
class file_mkdir(_file_container_command, _optional_output_cmd):
443
    """Create a directory
444
    Kamaki hanldes directories the same way as OOS Storage and Pithos+:
445
    A directory  is   an  object  with  type  "application/directory"
446
    An object with path  dir/name can exist even if  dir does not exist
447
    or even if dir  is  a non  directory  object.  Users can modify dir '
448
    without affecting the dir/name object in any way.
449
    """
450

    
451
    @errors.generic.all
452
    @errors.pithos.connection
453
    @errors.pithos.container
454
    def _run(self):
455
        self._optional_output(self.client.create_directory(self.path))
456

    
457
    def main(self, container___directory):
458
        super(self.__class__, self)._run(
459
            container___directory,
460
            path_is_optional=False)
461
        self._run()
462

    
463

    
464
@command(pithos_cmds)
465
class file_touch(_file_container_command, _optional_output_cmd):
466
    """Create an empty object (file)
467
    If object exists, this command will reset it to 0 length
468
    """
469

    
470
    arguments = dict(
471
        content_type=ValueArgument(
472
            'Set content type (default: application/octet-stream)',
473
            '--content-type',
474
            default='application/octet-stream')
475
    )
476

    
477
    @errors.generic.all
478
    @errors.pithos.connection
479
    @errors.pithos.container
480
    def _run(self):
481
        self._optional_output(
482
            self.client.create_object(self.path, self['content_type']))
483

    
484
    def main(self, container___path):
485
        super(file_touch, self)._run(
486
            container___path,
487
            path_is_optional=False)
488
        self._run()
489

    
490

    
491
@command(pithos_cmds)
492
class file_create(_file_container_command, _optional_output_cmd):
493
    """Create a container"""
494

    
495
    arguments = dict(
496
        versioning=ValueArgument(
497
            'set container versioning (auto/none)', '--versioning'),
498
        limit=IntArgument('set default container limit', '--limit'),
499
        meta=KeyValueArgument(
500
            'set container metadata (can be repeated)', '--meta')
501
    )
502

    
503
    @errors.generic.all
504
    @errors.pithos.connection
505
    @errors.pithos.container
506
    def _run(self, container):
507
        self._optional_output(self.client.create_container(
508
            container=container,
509
            sizelimit=self['limit'],
510
            versioning=self['versioning'],
511
            metadata=self['meta']))
512

    
513
    def main(self, container=None):
514
        super(self.__class__, self)._run(container)
515
        if container and self.container != container:
516
            raiseCLIError('Invalid container name %s' % container, details=[
517
                'Did you mean "%s" ?' % self.container,
518
                'Use --container for names containing :'])
519
        self._run(container)
520

    
521

    
522
class _source_destination_command(_file_container_command):
523

    
524
    arguments = dict(
525
        destination_account=ValueArgument('', ('-a', '--dst-account')),
526
        recursive=FlagArgument('', ('-R', '--recursive')),
527
        prefix=FlagArgument('', '--with-prefix', default=''),
528
        suffix=ValueArgument('', '--with-suffix', default=''),
529
        add_prefix=ValueArgument('', '--add-prefix', default=''),
530
        add_suffix=ValueArgument('', '--add-suffix', default=''),
531
        prefix_replace=ValueArgument('', '--prefix-to-replace', default=''),
532
        suffix_replace=ValueArgument('', '--suffix-to-replace', default=''),
533
    )
534

    
535
    def __init__(self, arguments={}, auth_base=None, cloud=None):
536
        self.arguments.update(arguments)
537
        super(_source_destination_command, self).__init__(
538
            self.arguments, auth_base, cloud)
539

    
540
    def _run(self, source_container___path, path_is_optional=False):
541
        super(_source_destination_command, self)._run(
542
            source_container___path,
543
            path_is_optional)
544
        self.dst_client = PithosClient(
545
            base_url=self.client.base_url,
546
            token=self.client.token,
547
            account=self['destination_account'] or self.client.account)
548

    
549
    @errors.generic.all
550
    @errors.pithos.account
551
    def _dest_container_path(self, dest_container_path):
552
        if self['destination_container']:
553
            self.dst_client.container = self['destination_container']
554
            return (self['destination_container'], dest_container_path)
555
        if dest_container_path:
556
            dst = dest_container_path.split(':')
557
            if len(dst) > 1:
558
                try:
559
                    self.dst_client.container = dst[0]
560
                    self.dst_client.get_container_info(dst[0])
561
                except ClientError as err:
562
                    if err.status in (404, 204):
563
                        raiseCLIError(
564
                            'Destination container %s not found' % dst[0])
565
                    raise
566
                else:
567
                    self.dst_client.container = dst[0]
568
                return (dst[0], dst[1])
569
            return(None, dst[0])
570
        raiseCLIError('No destination container:path provided')
571

    
572
    def _get_all(self, prefix):
573
        return self.client.container_get(prefix=prefix).json
574

    
575
    def _get_src_objects(self, src_path, source_version=None):
576
        """Get a list of the source objects to be called
577

578
        :param src_path: (str) source path
579

580
        :returns: (method, params) a method that returns a list when called
581
        or (object) if it is a single object
582
        """
583
        if src_path and src_path[-1] == '/':
584
            src_path = src_path[:-1]
585

    
586
        if self['prefix']:
587
            return (self._get_all, dict(prefix=src_path))
588
        try:
589
            srcobj = self.client.get_object_info(
590
                src_path, version=source_version)
591
        except ClientError as srcerr:
592
            if srcerr.status == 404:
593
                raiseCLIError(
594
                    'Source object %s not in source container %s' % (
595
                        src_path, self.client.container),
596
                    details=['Hint: --with-prefix to match multiple objects'])
597
            elif srcerr.status not in (204,):
598
                raise
599
            return (self.client.list_objects, {})
600

    
601
        if self._is_dir(srcobj):
602
            if not self['recursive']:
603
                raiseCLIError(
604
                    'Object %s of cont. %s is a dir' % (
605
                        src_path, self.client.container),
606
                    details=['Use --recursive to access directories'])
607
            return (self._get_all, dict(prefix=src_path))
608
        srcobj['name'] = src_path
609
        return srcobj
610

    
611
    def src_dst_pairs(self, dst_path, source_version=None):
612
        src_iter = self._get_src_objects(self.path, source_version)
613
        src_N = isinstance(src_iter, tuple)
614
        add_prefix = self['add_prefix'].strip('/')
615

    
616
        if dst_path and dst_path.endswith('/'):
617
            dst_path = dst_path[:-1]
618

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

    
643
        if src_N:
644
            (method, kwargs) = src_iter
645
            for obj in method(**kwargs):
646
                name = obj['name']
647
                if name.endswith(self['suffix']):
648
                    yield (name, self._get_new_object(name, add_prefix))
649
        elif src_iter['name'].endswith(self['suffix']):
650
            name = src_iter['name']
651
            yield (name, self._get_new_object(dst_path or name, add_prefix))
652
        else:
653
            raiseCLIError('Source path %s conflicts with suffix %s' % (
654
                src_iter['name'], self['suffix']))
655

    
656
    def _get_new_object(self, obj, add_prefix):
657
        if self['prefix_replace'] and obj.startswith(self['prefix_replace']):
658
            obj = obj[len(self['prefix_replace']):]
659
        if self['suffix_replace'] and obj.endswith(self['suffix_replace']):
660
            obj = obj[:-len(self['suffix_replace'])]
661
        return add_prefix + obj + self['add_suffix']
662

    
663

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

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

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

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

    
749

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

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

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

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

    
833

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

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

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

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

    
870

    
871
@command(pithos_cmds)
872
class file_truncate(_file_container_command, _optional_output_cmd):
873
    """Truncate remote file up to a size (default is 0)"""
874

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

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

    
887

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

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

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

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

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

    
938

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

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

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

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

    
988

    
989
@command(pithos_cmds)
990
class file_upload(_file_container_command, _optional_output_cmd):
991
    """Upload a file"""
992

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

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

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

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

    
1167
    def main(self, local_path, container____path__=None):
1168
        super(self.__class__, self)._run(container____path__)
1169
        remote_path = self.path or path.basename(path.abspath(local_path))
1170
        self._run(local_path=local_path, remote_path=remote_path)
1171

    
1172

    
1173
@command(pithos_cmds)
1174
class file_cat(_file_container_command):
1175
    """Print remote file contents to console"""
1176

    
1177
    arguments = dict(
1178
        range=RangeArgument('show range of data', '--range'),
1179
        if_match=ValueArgument('show output if ETags match', '--if-match'),
1180
        if_none_match=ValueArgument(
1181
            'show output if ETags match', '--if-none-match'),
1182
        if_modified_since=DateArgument(
1183
            'show output modified since then', '--if-modified-since'),
1184
        if_unmodified_since=DateArgument(
1185
            'show output unmodified since then', '--if-unmodified-since'),
1186
        object_version=ValueArgument(
1187
            'get the specific version', ('-O', '--object-version'))
1188
    )
1189

    
1190
    @errors.generic.all
1191
    @errors.pithos.connection
1192
    @errors.pithos.container
1193
    @errors.pithos.object_path
1194
    def _run(self):
1195
        self.client.download_object(
1196
            self.path,
1197
            self._out,
1198
            range_str=self['range'],
1199
            version=self['object_version'],
1200
            if_match=self['if_match'],
1201
            if_none_match=self['if_none_match'],
1202
            if_modified_since=self['if_modified_since'],
1203
            if_unmodified_since=self['if_unmodified_since'])
1204

    
1205
    def main(self, container___path):
1206
        super(self.__class__, self)._run(
1207
            container___path, path_is_optional=False)
1208
        self._run()
1209

    
1210

    
1211
@command(pithos_cmds)
1212
class file_download(_file_container_command):
1213
    """Download remote object as local file
1214
    If local destination is a directory:
1215
    *   download <container>:<path> <local dir> -R
1216
    will download all files on <container> prefixed as <path>,
1217
    to <local dir>/<full path> (or <local dir>\<full path> in windows)
1218
    *   download <container>:<path> <local dir>
1219
    will download only one file<path>
1220
    ATTENTION: to download cont:dir1/dir2/file there must exist objects
1221
    cont:dir1 and cont:dir1/dir2 of type application/directory
1222
    To create directory objects, use /file mkdir
1223
    """
1224

    
1225
    arguments = dict(
1226
        resume=FlagArgument('Resume instead of overwrite', ('-r', '--resume')),
1227
        range=RangeArgument('show range of data', '--range'),
1228
        if_match=ValueArgument('show output if ETags match', '--if-match'),
1229
        if_none_match=ValueArgument(
1230
            'show output if ETags match', '--if-none-match'),
1231
        if_modified_since=DateArgument(
1232
            'show output modified since then', '--if-modified-since'),
1233
        if_unmodified_since=DateArgument(
1234
            'show output unmodified since then', '--if-unmodified-since'),
1235
        object_version=ValueArgument(
1236
            'get the specific version', ('-O', '--object-version')),
1237
        poolsize=IntArgument('set pool size', '--with-pool-size'),
1238
        progress_bar=ProgressBarArgument(
1239
            'do not show progress bar',
1240
            ('-N', '--no-progress-bar'),
1241
            default=False),
1242
        recursive=FlagArgument(
1243
            'Download a remote path and all its contents',
1244
            ('-R', '--recursive'))
1245
    )
1246

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

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

    
1350
    @errors.generic.all
1351
    @errors.pithos.connection
1352
    @errors.pithos.container
1353
    @errors.pithos.object_path
1354
    @errors.pithos.local_path
1355
    def _run(self, local_path):
1356
        poolsize = self['poolsize']
1357
        if poolsize:
1358
            self.client.MAX_THREADS = int(poolsize)
1359
        progress_bar = None
1360
        try:
1361
            for f, rpath in self._outputs(local_path):
1362
                (
1363
                    progress_bar,
1364
                    download_cb) = self._safe_progress_bar(
1365
                        'Download %s' % rpath)
1366
                self.client.download_object(
1367
                    rpath, f,
1368
                    download_cb=download_cb,
1369
                    range_str=self['range'],
1370
                    version=self['object_version'],
1371
                    if_match=self['if_match'],
1372
                    resume=self['resume'],
1373
                    if_none_match=self['if_none_match'],
1374
                    if_modified_since=self['if_modified_since'],
1375
                    if_unmodified_since=self['if_unmodified_since'])
1376
        except KeyboardInterrupt:
1377
            from threading import activeCount, enumerate as activethreads
1378
            timeout = 0.5
1379
            while activeCount() > 1:
1380
                self._out.write('\nCancel %s threads: ' % (activeCount() - 1))
1381
                self._out.flush()
1382
                for thread in activethreads():
1383
                    try:
1384
                        thread.join(timeout)
1385
                        self._out.write('.' if thread.isAlive() else '*')
1386
                    except RuntimeError:
1387
                        continue
1388
                    finally:
1389
                        self._out.flush()
1390
                        timeout += 0.1
1391
            print('\nDownload canceled by user')
1392
            if local_path is not None:
1393
                print('to resume, re-run with --resume')
1394
        except Exception:
1395
            self._safe_progress_bar_finish(progress_bar)
1396
            raise
1397
        finally:
1398
            self._safe_progress_bar_finish(progress_bar)
1399

    
1400
    def main(self, container___path, local_path=None):
1401
        super(self.__class__, self)._run(container___path)
1402
        self._run(local_path=local_path)
1403

    
1404

    
1405
@command(pithos_cmds)
1406
class file_hashmap(_file_container_command, _optional_json):
1407
    """Get the hash-map of an object"""
1408

    
1409
    arguments = dict(
1410
        if_match=ValueArgument('show output if ETags match', '--if-match'),
1411
        if_none_match=ValueArgument(
1412
            'show output if ETags match', '--if-none-match'),
1413
        if_modified_since=DateArgument(
1414
            'show output modified since then', '--if-modified-since'),
1415
        if_unmodified_since=DateArgument(
1416
            'show output unmodified since then', '--if-unmodified-since'),
1417
        object_version=ValueArgument(
1418
            'get the specific version', ('-O', '--object-version'))
1419
    )
1420

    
1421
    @errors.generic.all
1422
    @errors.pithos.connection
1423
    @errors.pithos.container
1424
    @errors.pithos.object_path
1425
    def _run(self):
1426
        self._print(self.client.get_object_hashmap(
1427
            self.path,
1428
            version=self['object_version'],
1429
            if_match=self['if_match'],
1430
            if_none_match=self['if_none_match'],
1431
            if_modified_since=self['if_modified_since'],
1432
            if_unmodified_since=self['if_unmodified_since']), print_dict)
1433

    
1434
    def main(self, container___path):
1435
        super(self.__class__, self)._run(
1436
            container___path,
1437
            path_is_optional=False)
1438
        self._run()
1439

    
1440

    
1441
@command(pithos_cmds)
1442
class file_delete(_file_container_command, _optional_output_cmd):
1443
    """Delete a container [or an object]
1444
    How to delete a non-empty container:
1445
    - empty the container:  /file delete -R <container>
1446
    - delete it:            /file delete <container>
1447
    .
1448
    Semantics of directory deletion:
1449
    .a preserve the contents: /file delete <container>:<directory>
1450
    .    objects of the form dir/filename can exist with a dir object
1451
    .b delete contents:       /file delete -R <container>:<directory>
1452
    .    all dir/* objects are affected, even if dir does not exist
1453
    .
1454
    To restore a deleted object OBJ in a container CONT:
1455
    - get object versions: /file versions CONT:OBJ
1456
    .   and choose the version to be restored
1457
    - restore the object:  /file copy --source-version=<version> CONT:OBJ OBJ
1458
    """
1459

    
1460
    arguments = dict(
1461
        until=DateArgument('remove history until that date', '--until'),
1462
        yes=FlagArgument('Do not prompt for permission', '--yes'),
1463
        recursive=FlagArgument(
1464
            'empty dir or container and delete (if dir)',
1465
            ('-R', '--recursive')),
1466
        delimiter=ValueArgument(
1467
            'delete objects prefixed with <object><delimiter>', '--delimiter')
1468
    )
1469

    
1470
    @errors.generic.all
1471
    @errors.pithos.connection
1472
    @errors.pithos.container
1473
    @errors.pithos.object_path
1474
    def _run(self):
1475
        if self.path:
1476
            if self['yes'] or ask_user(
1477
                    'Delete %s:%s ?' % (self.container, self.path)):
1478
                self._optional_output(self.client.del_object(
1479
                    self.path,
1480
                    until=self['until'],
1481
                    delimiter='/' if self['recursive'] else self['delimiter']))
1482
            else:
1483
                print('Aborted')
1484
        elif self.container:
1485
            if self['recursive']:
1486
                ask_msg = 'Delete container contents'
1487
            else:
1488
                ask_msg = 'Delete container'
1489
            if self['yes'] or ask_user('%s %s ?' % (ask_msg, self.container)):
1490
                self._optional_output(self.client.del_container(
1491
                    until=self['until'],
1492
                    delimiter='/' if self['recursive'] else self['delimiter']))
1493
            else:
1494
                print('Aborted')
1495
        else:
1496
            raiseCLIError('Nothing to delete, please provide container[:path]')
1497

    
1498
    def main(self, container____path__=None):
1499
        super(self.__class__, self)._run(container____path__)
1500
        self._run()
1501

    
1502

    
1503
@command(pithos_cmds)
1504
class file_purge(_file_container_command, _optional_output_cmd):
1505
    """Delete a container and release related data blocks
1506
    Non-empty containers can not purged.
1507
    To purge a container with content:
1508
    .   /file delete -R <container>
1509
    .      objects are deleted, but data blocks remain on server
1510
    .   /file purge <container>
1511
    .      container and data blocks are released and deleted
1512
    """
1513

    
1514
    arguments = dict(
1515
        yes=FlagArgument('Do not prompt for permission', '--yes'),
1516
        force=FlagArgument('purge even if not empty', ('-F', '--force'))
1517
    )
1518

    
1519
    @errors.generic.all
1520
    @errors.pithos.connection
1521
    @errors.pithos.container
1522
    def _run(self):
1523
        if self['yes'] or ask_user('Purge container %s?' % self.container):
1524
            try:
1525
                r = self.client.purge_container()
1526
            except ClientError as ce:
1527
                if ce.status in (409,):
1528
                    if self['force']:
1529
                        self.client.del_container(delimiter='/')
1530
                        r = self.client.purge_container()
1531
                    else:
1532
                        raiseCLIError(ce, details=['Try -F to force-purge'])
1533
                else:
1534
                    raise
1535
            self._optional_output(r)
1536
        else:
1537
            print('Aborted')
1538

    
1539
    def main(self, container=None):
1540
        super(self.__class__, self)._run(container)
1541
        if container and self.container != container:
1542
            raiseCLIError('Invalid container name %s' % container, details=[
1543
                'Did you mean "%s" ?' % self.container,
1544
                'Use --container for names containing :'])
1545
        self._run()
1546

    
1547

    
1548
@command(pithos_cmds)
1549
class file_publish(_file_container_command):
1550
    """Publish the object and print the public url"""
1551

    
1552
    @errors.generic.all
1553
    @errors.pithos.connection
1554
    @errors.pithos.container
1555
    @errors.pithos.object_path
1556
    def _run(self):
1557
        print self.client.publish_object(self.path)
1558

    
1559
    def main(self, container___path):
1560
        super(self.__class__, self)._run(
1561
            container___path, path_is_optional=False)
1562
        self._run()
1563

    
1564

    
1565
@command(pithos_cmds)
1566
class file_unpublish(_file_container_command, _optional_output_cmd):
1567
    """Unpublish an object"""
1568

    
1569
    @errors.generic.all
1570
    @errors.pithos.connection
1571
    @errors.pithos.container
1572
    @errors.pithos.object_path
1573
    def _run(self):
1574
            self._optional_output(self.client.unpublish_object(self.path))
1575

    
1576
    def main(self, container___path):
1577
        super(self.__class__, self)._run(
1578
            container___path, path_is_optional=False)
1579
        self._run()
1580

    
1581

    
1582
@command(pithos_cmds)
1583
class file_permissions(_pithos_init):
1584
    """Manage user and group accessibility for objects
1585
    Permissions are lists of users and user groups. There are read and write
1586
    permissions. Users and groups with write permission have also read
1587
    permission.
1588
    """
1589

    
1590

    
1591
def print_permissions(permissions_dict, out):
1592
    expected_keys = ('read', 'write')
1593
    if set(permissions_dict).issubset(expected_keys):
1594
        print_dict(permissions_dict, out=out)
1595
    else:
1596
        invalid_keys = set(permissions_dict.keys()).difference(expected_keys)
1597
        raiseCLIError(
1598
            'Illegal permission keys: %s' % ', '.join(invalid_keys),
1599
            importance=1, details=[
1600
                'Valid permission types: %s' % ' '.join(expected_keys)])
1601

    
1602

    
1603
@command(pithos_cmds)
1604
class file_permissions_get(_file_container_command, _optional_json):
1605
    """Get read and write permissions of an object"""
1606

    
1607
    @errors.generic.all
1608
    @errors.pithos.connection
1609
    @errors.pithos.container
1610
    @errors.pithos.object_path
1611
    def _run(self):
1612
        self._print(
1613
            self.client.get_object_sharing(self.path), print_permissions)
1614

    
1615
    def main(self, container___path):
1616
        super(self.__class__, self)._run(
1617
            container___path, path_is_optional=False)
1618
        self._run()
1619

    
1620

    
1621
@command(pithos_cmds)
1622
class file_permissions_set(_file_container_command, _optional_output_cmd):
1623
    """Set permissions for an object
1624
    New permissions overwrite existing permissions.
1625
    Permission format:
1626
    -   read=<username>[,usergroup[,...]]
1627
    -   write=<username>[,usegroup[,...]]
1628
    E.g. to give read permissions for file F to users A and B and write for C:
1629
    .       /file permissions set F read=A,B write=C
1630
    """
1631

    
1632
    @errors.generic.all
1633
    def format_permission_dict(self, permissions):
1634
        read = False
1635
        write = False
1636
        for perms in permissions:
1637
            splstr = perms.split('=')
1638
            if 'read' == splstr[0]:
1639
                read = [ug.strip() for ug in splstr[1].split(',')]
1640
            elif 'write' == splstr[0]:
1641
                write = [ug.strip() for ug in splstr[1].split(',')]
1642
            else:
1643
                msg = 'Usage:\tread=<groups,users> write=<groups,users>'
1644
                raiseCLIError(None, msg)
1645
        return (read, write)
1646

    
1647
    @errors.generic.all
1648
    @errors.pithos.connection
1649
    @errors.pithos.container
1650
    @errors.pithos.object_path
1651
    def _run(self, read, write):
1652
        self._optional_output(self.client.set_object_sharing(
1653
            self.path, read_permission=read, write_permission=write))
1654

    
1655
    def main(self, container___path, *permissions):
1656
        super(self.__class__, self)._run(
1657
            container___path, path_is_optional=False)
1658
        read, write = self.format_permission_dict(permissions)
1659
        self._run(read, write)
1660

    
1661

    
1662
@command(pithos_cmds)
1663
class file_permissions_delete(_file_container_command, _optional_output_cmd):
1664
    """Delete all permissions set on object
1665
    To modify permissions, use /file permissions set
1666
    """
1667

    
1668
    @errors.generic.all
1669
    @errors.pithos.connection
1670
    @errors.pithos.container
1671
    @errors.pithos.object_path
1672
    def _run(self):
1673
        self._optional_output(self.client.del_object_sharing(self.path))
1674

    
1675
    def main(self, container___path):
1676
        super(self.__class__, self)._run(
1677
            container___path, path_is_optional=False)
1678
        self._run()
1679

    
1680

    
1681
@command(pithos_cmds)
1682
class file_info(_file_container_command, _optional_json):
1683
    """Get detailed information for user account, containers or objects
1684
    to get account info:    /file info
1685
    to get container info:  /file info <container>
1686
    to get object info:     /file info <container>:<path>
1687
    """
1688

    
1689
    arguments = dict(
1690
        object_version=ValueArgument(
1691
            'show specific version \ (applies only for objects)',
1692
            ('-O', '--object-version'))
1693
    )
1694

    
1695
    @errors.generic.all
1696
    @errors.pithos.connection
1697
    @errors.pithos.container
1698
    @errors.pithos.object_path
1699
    def _run(self):
1700
        if self.container is None:
1701
            r = self.client.get_account_info()
1702
        elif self.path is None:
1703
            r = self.client.get_container_info(self.container)
1704
        else:
1705
            r = self.client.get_object_info(
1706
                self.path, version=self['object_version'])
1707
        self._print(r, print_dict)
1708

    
1709
    def main(self, container____path__=None):
1710
        super(self.__class__, self)._run(container____path__)
1711
        self._run()
1712

    
1713

    
1714
@command(pithos_cmds)
1715
class file_metadata(_pithos_init):
1716
    """Metadata are attached on objects. They are formed as key:value pairs.
1717
    They can have arbitary values.
1718
    """
1719

    
1720

    
1721
@command(pithos_cmds)
1722
class file_metadata_get(_file_container_command, _optional_json):
1723
    """Get metadata for account, containers or objects"""
1724

    
1725
    arguments = dict(
1726
        detail=FlagArgument('show detailed output', ('-l', '--details')),
1727
        until=DateArgument('show metadata until then', '--until'),
1728
        object_version=ValueArgument(
1729
            'show specific version (applies only for objects)',
1730
            ('-O', '--object-version'))
1731
    )
1732

    
1733
    @errors.generic.all
1734
    @errors.pithos.connection
1735
    @errors.pithos.container
1736
    @errors.pithos.object_path
1737
    def _run(self):
1738
        until = self['until']
1739
        r = None
1740
        if self.container is None:
1741
            r = self.client.get_account_info(until=until)
1742
        elif self.path is None:
1743
            if self['detail']:
1744
                r = self.client.get_container_info(until=until)
1745
            else:
1746
                cmeta = self.client.get_container_meta(until=until)
1747
                ometa = self.client.get_container_object_meta(until=until)
1748
                r = {}
1749
                if cmeta:
1750
                    r['container-meta'] = cmeta
1751
                if ometa:
1752
                    r['object-meta'] = ometa
1753
        else:
1754
            if self['detail']:
1755
                r = self.client.get_object_info(
1756
                    self.path,
1757
                    version=self['object_version'])
1758
            else:
1759
                r = self.client.get_object_meta(
1760
                    self.path,
1761
                    version=self['object_version'])
1762
        if r:
1763
            self._print(r, print_dict)
1764

    
1765
    def main(self, container____path__=None):
1766
        super(self.__class__, self)._run(container____path__)
1767
        self._run()
1768

    
1769

    
1770
@command(pithos_cmds)
1771
class file_metadata_set(_file_container_command, _optional_output_cmd):
1772
    """Set a piece of metadata for account, container or object"""
1773

    
1774
    @errors.generic.all
1775
    @errors.pithos.connection
1776
    @errors.pithos.container
1777
    @errors.pithos.object_path
1778
    def _run(self, metakey, metaval):
1779
        if not self.container:
1780
            r = self.client.set_account_meta({metakey: metaval})
1781
        elif not self.path:
1782
            r = self.client.set_container_meta({metakey: metaval})
1783
        else:
1784
            r = self.client.set_object_meta(self.path, {metakey: metaval})
1785
        self._optional_output(r)
1786

    
1787
    def main(self, metakey, metaval, container____path__=None):
1788
        super(self.__class__, self)._run(container____path__)
1789
        self._run(metakey=metakey, metaval=metaval)
1790

    
1791

    
1792
@command(pithos_cmds)
1793
class file_metadata_delete(_file_container_command, _optional_output_cmd):
1794
    """Delete metadata with given key from account, container or object
1795
    - to get metadata of current account: /file metadata get
1796
    - to get metadata of a container:     /file metadata get <container>
1797
    - to get metadata of an object:       /file metadata get <container>:<path>
1798
    """
1799

    
1800
    @errors.generic.all
1801
    @errors.pithos.connection
1802
    @errors.pithos.container
1803
    @errors.pithos.object_path
1804
    def _run(self, metakey):
1805
        if self.container is None:
1806
            r = self.client.del_account_meta(metakey)
1807
        elif self.path is None:
1808
            r = self.client.del_container_meta(metakey)
1809
        else:
1810
            r = self.client.del_object_meta(self.path, metakey)
1811
        self._optional_output(r)
1812

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

    
1817

    
1818
@command(pithos_cmds)
1819
class file_quota(_file_account_command, _optional_json):
1820
    """Get account quota"""
1821

    
1822
    arguments = dict(
1823
        in_bytes=FlagArgument('Show result in bytes', ('-b', '--bytes'))
1824
    )
1825

    
1826
    @errors.generic.all
1827
    @errors.pithos.connection
1828
    def _run(self):
1829

    
1830
        def pretty_print(output):
1831
            if not self['in_bytes']:
1832
                for k in output:
1833
                    output[k] = format_size(output[k])
1834
            print_dict(output, '-')
1835

    
1836
        self._print(self.client.get_account_quota(), pretty_print)
1837

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

    
1842

    
1843
@command(pithos_cmds)
1844
class file_containerlimit(_pithos_init):
1845
    """Container size limit commands"""
1846

    
1847

    
1848
@command(pithos_cmds)
1849
class file_containerlimit_get(_file_container_command, _optional_json):
1850
    """Get container size limit"""
1851

    
1852
    arguments = dict(
1853
        in_bytes=FlagArgument('Show result in bytes', ('-b', '--bytes'))
1854
    )
1855

    
1856
    @errors.generic.all
1857
    @errors.pithos.container
1858
    def _run(self):
1859

    
1860
        def pretty_print(output):
1861
            if not self['in_bytes']:
1862
                for k, v in output.items():
1863
                    output[k] = 'unlimited' if '0' == v else format_size(v)
1864
            print_dict(output, '-')
1865

    
1866
        self._print(
1867
            self.client.get_container_limit(self.container), pretty_print)
1868

    
1869
    def main(self, container=None):
1870
        super(self.__class__, self)._run()
1871
        self.container = container
1872
        self._run()
1873

    
1874

    
1875
@command(pithos_cmds)
1876
class file_containerlimit_set(_file_account_command, _optional_output_cmd):
1877
    """Set new storage limit for a container
1878
    By default, the limit is set in bytes
1879
    Users may specify a different unit, e.g:
1880
    /file containerlimit set 2.3GB mycontainer
1881
    Valid units: B, KiB (1024 B), KB (1000 B), MiB, MB, GiB, GB, TiB, TB
1882
    To set container limit to "unlimited", use 0
1883
    """
1884

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

    
1909
    @errors.generic.all
1910
    @errors.pithos.connection
1911
    @errors.pithos.container
1912
    def _run(self, limit):
1913
        if self.container:
1914
            self.client.container = self.container
1915
        self._optional_output(self.client.set_container_limit(limit))
1916

    
1917
    def main(self, limit, container=None):
1918
        super(self.__class__, self)._run()
1919
        limit = self._calculate_limit(limit)
1920
        self.container = container
1921
        self._run(limit)
1922

    
1923

    
1924
@command(pithos_cmds)
1925
class file_versioning(_pithos_init):
1926
    """Manage the versioning scheme of current pithos user account"""
1927

    
1928

    
1929
@command(pithos_cmds)
1930
class file_versioning_get(_file_account_command, _optional_json):
1931
    """Get  versioning for account or container"""
1932

    
1933
    @errors.generic.all
1934
    @errors.pithos.connection
1935
    @errors.pithos.container
1936
    def _run(self):
1937
        self._print(
1938
            self.client.get_container_versioning(self.container), print_dict)
1939

    
1940
    def main(self, container):
1941
        super(self.__class__, self)._run()
1942
        self.container = container
1943
        self._run()
1944

    
1945

    
1946
@command(pithos_cmds)
1947
class file_versioning_set(_file_account_command, _optional_output_cmd):
1948
    """Set versioning mode (auto, none) for account or container"""
1949

    
1950
    def _check_versioning(self, versioning):
1951
        if versioning and versioning.lower() in ('auto', 'none'):
1952
            return versioning.lower()
1953
        raiseCLIError('Invalid versioning %s' % versioning, details=[
1954
            'Versioning can be auto or none'])
1955

    
1956
    @errors.generic.all
1957
    @errors.pithos.connection
1958
    @errors.pithos.container
1959
    def _run(self, versioning):
1960
        self.client.container = self.container
1961
        r = self.client.set_container_versioning(versioning)
1962
        self._optional_output(r)
1963

    
1964
    def main(self, versioning, container):
1965
        super(self.__class__, self)._run()
1966
        self._run(self._check_versioning(versioning))
1967

    
1968

    
1969
@command(pithos_cmds)
1970
class file_group(_pithos_init):
1971
    """Manage access groups and group members"""
1972

    
1973

    
1974
@command(pithos_cmds)
1975
class file_group_list(_file_account_command, _optional_json):
1976
    """list all groups and group members"""
1977

    
1978
    @errors.generic.all
1979
    @errors.pithos.connection
1980
    def _run(self):
1981
        self._print(self.client.get_account_group(), print_dict, delim='-')
1982

    
1983
    def main(self):
1984
        super(self.__class__, self)._run()
1985
        self._run()
1986

    
1987

    
1988
@command(pithos_cmds)
1989
class file_group_set(_file_account_command, _optional_output_cmd):
1990
    """Set a user group"""
1991

    
1992
    @errors.generic.all
1993
    @errors.pithos.connection
1994
    def _run(self, groupname, *users):
1995
        self._optional_output(self.client.set_account_group(groupname, users))
1996

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

    
2004

    
2005
@command(pithos_cmds)
2006
class file_group_delete(_file_account_command, _optional_output_cmd):
2007
    """Delete a user group"""
2008

    
2009
    @errors.generic.all
2010
    @errors.pithos.connection
2011
    def _run(self, groupname):
2012
        self._optional_output(self.client.del_account_group(groupname))
2013

    
2014
    def main(self, groupname):
2015
        super(self.__class__, self)._run()
2016
        self._run(groupname)
2017

    
2018

    
2019
@command(pithos_cmds)
2020
class file_sharers(_file_account_command, _optional_json):
2021
    """List the accounts that share objects with current user"""
2022

    
2023
    arguments = dict(
2024
        detail=FlagArgument('show detailed output', ('-l', '--details')),
2025
        marker=ValueArgument('show output greater then marker', '--marker')
2026
    )
2027

    
2028
    @errors.generic.all
2029
    @errors.pithos.connection
2030
    def _run(self):
2031
        accounts = self.client.get_sharing_accounts(marker=self['marker'])
2032
        if not self['json_output']:
2033
            usernames = self._uuids2usernames(
2034
                [acc['name'] for acc in accounts])
2035
            for item in accounts:
2036
                uuid = item['name']
2037
                item['id'], item['name'] = uuid, usernames[uuid]
2038
                if not self['detail']:
2039
                    item.pop('last_modified')
2040
        self._print(accounts)
2041

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

    
2046

    
2047
def version_print(versions, out):
2048
    print_items(
2049
        [dict(id=vitem[0], created=strftime(
2050
            '%d-%m-%Y %H:%M:%S',
2051
            localtime(float(vitem[1])))) for vitem in versions],
2052
        out=out)
2053

    
2054

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

    
2065
    @errors.generic.all
2066
    @errors.pithos.connection
2067
    @errors.pithos.container
2068
    @errors.pithos.object_path
2069
    def _run(self):
2070
        self._print(
2071
            self.client.get_object_versionlist(self.path), version_print)
2072

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