Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (78.3 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
from io import StringIO
38

    
39
from kamaki.cli import command
40
from kamaki.cli.command_tree import CommandTree
41
from kamaki.cli.errors import raiseCLIError, CLISyntaxError, CLIBaseUrlError
42
from kamaki.cli.utils import (
43
    format_size, to_bytes, print_dict, print_items, pager, bold, ask_user,
44
    get_path_size, print_json, guess_mime_type)
45
from kamaki.cli.argument import FlagArgument, ValueArgument, IntArgument
46
from kamaki.cli.argument import KeyValueArgument, DateArgument
47
from kamaki.cli.argument import ProgressBarArgument
48
from kamaki.cli.commands import _command_init, errors
49
from kamaki.cli.commands import addLogSettings, DontRaiseKeyError
50
from kamaki.cli.commands import (
51
    _optional_output_cmd, _optional_json, _name_filter)
52
from kamaki.clients.pithos import PithosClient, ClientError
53
from kamaki.clients.astakos import AstakosClient
54

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

    
58

    
59
# Argument functionality
60

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

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

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

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

    
81

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

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

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

    
123

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

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

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

    
154

    
155
# Command specs
156

    
157

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

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

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

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

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

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

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

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

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

    
219

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

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

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

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

    
240

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

    
244
    container = None
245
    path = None
246

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

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

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

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

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

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

    
329

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

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

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

    
399
    def print_containers(self, container_list):
400
        if self['json_output']:
401
            print_json(container_list)
402
            return
403
        out = StringIO() if self['more'] else stdout
404
        for index, container in enumerate(container_list):
405
            if 'bytes' in container:
406
                size = format_size(container['bytes'])
407
            prfx = ('%s. ' % (index + 1)) if self['enum'] else ''
408
            _cname = container['name'] if (
409
                self['more']) else bold(container['name'])
410
            cname = u'%s%s' % (prfx, _cname)
411
            if self['detail']:
412
                out.writelines(cname + u'\n')
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'), out=out)
417
                out.writelines(u'\n')
418
            else:
419
                if 'count' in container and 'bytes' in container:
420
                    out.writelines(u'%s (%s, %s objects)\n' % (
421
                        cname, size, container['count']))
422
                else:
423
                    out.writelines(cname + '\n')
424
        if self['more']:
425
            pager(out.getvalue())
426

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

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

    
462

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

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

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

    
485

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

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

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

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

    
512

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

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

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

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

    
543

    
544
class _source_destination_command(_file_container_command):
545

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

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

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

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

    
594
    def _get_all(self, prefix):
595
        return self.client.container_get(prefix=prefix).json
596

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

600
        :param src_path: (str) source path
601

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

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

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

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

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

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

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

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

    
685

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

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

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

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

    
771

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

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

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

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

    
855

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

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

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

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

    
892

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

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

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

    
909

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

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

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

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

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

    
960

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

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

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

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

    
1010

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

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

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

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

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

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

    
1194

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

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

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

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

    
1232

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

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

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

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

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

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

    
1426

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

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

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

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

    
1462

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

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

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

    
1497
    @errors.generic.all
1498
    @errors.pithos.connection
1499
    @errors.pithos.container
1500
    @errors.pithos.object_path
1501
    def _run(self):
1502
        if self.path:
1503
            if self['yes'] or ask_user(
1504
                    'Delete %s:%s ?' % (self.container, self.path)):
1505
                self._optional_output(self.client.del_object(
1506
                    self.path,
1507
                    until=self['until'], delimiter=self['delimiter']))
1508
            else:
1509
                print('Aborted')
1510
        elif self.container:
1511
            if self['recursive']:
1512
                ask_msg = 'Delete container contents'
1513
            else:
1514
                ask_msg = 'Delete container'
1515
            if self['yes'] or ask_user('%s %s ?' % (ask_msg, self.container)):
1516
                self._optional_output(self.client.del_container(
1517
                    until=self['until'], delimiter=self['delimiter']))
1518
            else:
1519
                print('Aborted')
1520
        else:
1521
            raiseCLIError('Nothing to delete, please provide container[:path]')
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
        if not self['json_output']:
2058
            usernames = self._uuids2usernames(
2059
                [acc['name'] for acc in accounts])
2060
            for item in accounts:
2061
                uuid = item['name']
2062
                item['id'], item['name'] = uuid, usernames[uuid]
2063
                if not self['detail']:
2064
                    item.pop('last_modified')
2065
        self._print(accounts)
2066

    
2067
    def main(self):
2068
        super(self.__class__, self)._run()
2069
        self._run()
2070

    
2071

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

    
2077

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

    
2088
    @errors.generic.all
2089
    @errors.pithos.connection
2090
    @errors.pithos.container
2091
    @errors.pithos.object_path
2092
    def _run(self):
2093
        self._print(
2094
            self.client.get_object_versionlist(self.path), version_print)
2095

    
2096
    def main(self, container___path):
2097
        super(file_versions, self)._run(
2098
            container___path,
2099
            path_is_optional=False)
2100
        self._run()