Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / pithos.py @ 9986e569

History | View | Annotate | Download (73.2 kB)

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

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

    
38
from kamaki.cli import command
39
from kamaki.cli.command_tree import CommandTree
40
from kamaki.cli.errors import raiseCLIError, CLISyntaxError
41
from kamaki.cli.utils import (
42
    format_size, to_bytes, print_dict, print_items, pretty_keys,
43
    page_hold, bold, ask_user, get_path_size)
44
from kamaki.cli.argument import FlagArgument, ValueArgument, IntArgument
45
from kamaki.cli.argument import KeyValueArgument, DateArgument
46
from kamaki.cli.argument import ProgressBarArgument
47
from kamaki.cli.commands import _command_init, errors
48
from kamaki.clients.pithos import PithosClient, ClientError
49
from kamaki.clients.astakos import AstakosClient
50

    
51
pithos_cmds = CommandTree('file', 'Pithos+/Storage API commands')
52
_commands = [pithos_cmds]
53

    
54

    
55
# Argument functionality
56

    
57
class DelimiterArgument(ValueArgument):
58
    """
59
    :value type: string
60
    :value returns: given string or /
61
    """
62

    
63
    def __init__(self, caller_obj, help='', parsed_name=None, default=None):
64
        super(DelimiterArgument, self).__init__(help, parsed_name, default)
65
        self.caller_obj = caller_obj
66

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

    
73
    @value.setter
74
    def value(self, newvalue):
75
        self._value = newvalue
76

    
77

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

    
87
    @property
88
    def value(self):
89
        return getattr(self, '_value', self.default)
90

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

    
119

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

    
127
    @property
128
    def value(self):
129
        return getattr(self, '_value', self.default)
130

    
131
    @value.setter
132
    def value(self, newvalue):
133
        if newvalue is None:
134
            self._value = self.default
135
            return
136
        (start, end) = newvalue.split('-')
137
        (start, end) = (int(start), int(end))
138
        self._value = '%s-%s' % (start, end)
139

    
140
# Command specs
141

    
142

    
143
class _pithos_init(_command_init):
144
    """Initialize a pithos+ kamaki client"""
145

    
146
    @staticmethod
147
    def _is_dir(remote_dict):
148
        return 'application/directory' == remote_dict.get(
149
            'content_type',
150
            remote_dict.get('content-type', ''))
151

    
152
    @errors.generic.all
153
    def _run(self):
154
        self.token = self.config.get('file', 'token')\
155
            or self.config.get('global', 'token')
156
        self.base_url = self.config.get('file', 'url')\
157
            or self.config.get('global', 'url')
158
        self._set_account()
159
        self.container = self.config.get('file', 'container')\
160
            or self.config.get('global', 'container')
161
        self.client = PithosClient(
162
            base_url=self.base_url,
163
            token=self.token,
164
            account=self.account,
165
            container=self.container)
166
        self._set_log_params()
167
        self._update_max_threads()
168

    
169
    def main(self):
170
        self._run()
171

    
172
    def _set_account(self):
173
        user = AstakosClient(self.config.get('user', 'url'), self.token)
174
        self.account = self['account'] or user.term('uuid')
175

    
176
        """Backwards compatibility"""
177
        self.account = self.account\
178
            or self.config.get('file', 'account')\
179
            or self.config.get('global', 'account')
180

    
181

    
182
class _file_account_command(_pithos_init):
183
    """Base class for account level storage commands"""
184

    
185
    def __init__(self, arguments={}):
186
        super(_file_account_command, self).__init__(arguments)
187
        self['account'] = ValueArgument(
188
            'Set user account (not permanent)',
189
            ('-A', '--account'))
190

    
191
    def _run(self, custom_account=None):
192
        super(_file_account_command, self)._run()
193
        if custom_account:
194
            self.client.account = custom_account
195
        elif self['account']:
196
            self.client.account = self['account']
197

    
198
    @errors.generic.all
199
    def main(self):
200
        self._run()
201

    
202

    
203
class _file_container_command(_file_account_command):
204
    """Base class for container level storage commands"""
205

    
206
    container = None
207
    path = None
208

    
209
    def __init__(self, arguments={}):
210
        super(_file_container_command, self).__init__(arguments)
211
        self['container'] = ValueArgument(
212
            'Set container to work with (temporary)',
213
            ('-C', '--container'))
214

    
215
    def extract_container_and_path(
216
            self,
217
            container_with_path,
218
            path_is_optional=True):
219
        """Contains all heuristics for deciding what should be used as
220
        container or path. Options are:
221
        * user string of the form container:path
222
        * self.container, self.path variables set by super constructor, or
223
        explicitly by the caller application
224
        Error handling is explicit as these error cases happen only here
225
        """
226
        try:
227
            assert isinstance(container_with_path, str)
228
        except AssertionError as err:
229
            if self['container'] and path_is_optional:
230
                self.container = self['container']
231
                self.client.container = self['container']
232
                return
233
            raiseCLIError(err)
234

    
235
        user_cont, sep, userpath = container_with_path.partition(':')
236

    
237
        if sep:
238
            if not user_cont:
239
                raiseCLIError(CLISyntaxError(
240
                    'Container is missing\n',
241
                    details=errors.pithos.container_howto))
242
            alt_cont = self['container']
243
            if alt_cont and user_cont != alt_cont:
244
                raiseCLIError(CLISyntaxError(
245
                    'Conflict: 2 containers (%s, %s)' % (user_cont, alt_cont),
246
                    details=errors.pithos.container_howto)
247
                )
248
            self.container = user_cont
249
            if not userpath:
250
                raiseCLIError(CLISyntaxError(
251
                    'Path is missing for object in container %s' % user_cont,
252
                    details=errors.pithos.container_howto)
253
                )
254
            self.path = userpath
255
        else:
256
            alt_cont = self['container'] or self.client.container
257
            if alt_cont:
258
                self.container = alt_cont
259
                self.path = user_cont
260
            elif path_is_optional:
261
                self.container = user_cont
262
                self.path = None
263
            else:
264
                self.container = user_cont
265
                raiseCLIError(CLISyntaxError(
266
                    'Both container and path are required',
267
                    details=errors.pithos.container_howto)
268
                )
269

    
270
    @errors.generic.all
271
    def _run(self, container_with_path=None, path_is_optional=True):
272
        super(_file_container_command, self)._run()
273
        if self['container']:
274
            self.client.container = self['container']
275
            if container_with_path:
276
                self.path = container_with_path
277
            elif not path_is_optional:
278
                raise CLISyntaxError(
279
                    'Both container and path are required',
280
                    details=errors.pithos.container_howto)
281
        elif container_with_path:
282
            self.extract_container_and_path(
283
                container_with_path,
284
                path_is_optional)
285
            self.client.container = self.container
286
        self.container = self.client.container
287

    
288
    def main(self, container_with_path=None, path_is_optional=True):
289
        self._run(container_with_path, path_is_optional)
290

    
291

    
292
@command(pithos_cmds)
293
class file_list(_file_container_command):
294
    """List containers, object trees or objects in a directory
295
    Use with:
296
    1 no parameters : containers in current account
297
    2. one parameter (container) or --container : contents of container
298
    3. <container>:<prefix> or --container=<container> <prefix>: objects in
299
    .   container starting with prefix
300
    """
301

    
302
    arguments = dict(
303
        detail=FlagArgument('detailed output', ('-l', '--list')),
304
        limit=IntArgument('limit number of listed items', ('-n', '--number')),
305
        marker=ValueArgument('output greater that marker', '--marker'),
306
        prefix=ValueArgument('output starting with prefix', '--prefix'),
307
        delimiter=ValueArgument('show output up to delimiter', '--delimiter'),
308
        path=ValueArgument(
309
            'show output starting with prefix up to /',
310
            '--path'),
311
        meta=ValueArgument(
312
            'show output with specified meta keys',
313
            '--meta',
314
            default=[]),
315
        if_modified_since=ValueArgument(
316
            'show output modified since then',
317
            '--if-modified-since'),
318
        if_unmodified_since=ValueArgument(
319
            'show output not modified since then',
320
            '--if-unmodified-since'),
321
        until=DateArgument('show metadata until then', '--until'),
322
        format=ValueArgument(
323
            'format to parse until data (default: d/m/Y H:M:S )',
324
            '--format'),
325
        shared=FlagArgument('show only shared', '--shared'),
326
        more=FlagArgument(
327
            'output results in pages (-n to set items per page, default 10)',
328
            '--more'),
329
        exact_match=FlagArgument(
330
            'Show only objects that match exactly with path',
331
            '--exact-match')
332
    )
333

    
334
    def print_objects(self, object_list):
335
        limit = int(self['limit']) if self['limit'] > 0 else len(object_list)
336
        for index, obj in enumerate(object_list):
337
            if self['exact_match'] and self.path and not (
338
                    obj['name'] == self.path or 'content_type' in obj):
339
                continue
340
            pretty_obj = obj.copy()
341
            index += 1
342
            empty_space = ' ' * (len(str(len(object_list))) - len(str(index)))
343
            if obj['content_type'] == 'application/directory':
344
                isDir = True
345
                size = 'D'
346
            else:
347
                isDir = False
348
                size = format_size(obj['bytes'])
349
                pretty_obj['bytes'] = '%s (%s)' % (obj['bytes'], size)
350
            oname = bold(obj['name'])
351
            if self['detail']:
352
                print('%s%s. %s' % (empty_space, index, oname))
353
                print_dict(pretty_keys(pretty_obj), exclude=('name'))
354
                print
355
            else:
356
                oname = '%s%s. %6s %s' % (empty_space, index, size, oname)
357
                oname += '/' if isDir else ''
358
                print(oname)
359
            if self['more']:
360
                page_hold(index, limit, len(object_list))
361

    
362
    def print_containers(self, container_list):
363
        limit = int(self['limit']) if self['limit'] > 0\
364
            else len(container_list)
365
        for index, container in enumerate(container_list):
366
            if 'bytes' in container:
367
                size = format_size(container['bytes'])
368
            cname = '%s. %s' % (index + 1, bold(container['name']))
369
            if self['detail']:
370
                print(cname)
371
                pretty_c = container.copy()
372
                if 'bytes' in container:
373
                    pretty_c['bytes'] = '%s (%s)' % (container['bytes'], size)
374
                print_dict(pretty_keys(pretty_c), exclude=('name'))
375
                print
376
            else:
377
                if 'count' in container and 'bytes' in container:
378
                    print('%s (%s, %s objects)' % (
379
                        cname,
380
                        size,
381
                        container['count']))
382
                else:
383
                    print(cname)
384
            if self['more']:
385
                page_hold(index + 1, limit, len(container_list))
386

    
387
    @errors.generic.all
388
    @errors.pithos.connection
389
    @errors.pithos.object_path
390
    @errors.pithos.container
391
    def _run(self):
392
        if self.container is None:
393
            r = self.client.account_get(
394
                limit=False if self['more'] else self['limit'],
395
                marker=self['marker'],
396
                if_modified_since=self['if_modified_since'],
397
                if_unmodified_since=self['if_unmodified_since'],
398
                until=self['until'],
399
                show_only_shared=self['shared'])
400
            self.print_containers(r.json)
401
        else:
402
            prefix = self.path or self['prefix']
403
            r = self.client.container_get(
404
                limit=False if self['more'] else self['limit'],
405
                marker=self['marker'],
406
                prefix=prefix,
407
                delimiter=self['delimiter'],
408
                path=self['path'],
409
                if_modified_since=self['if_modified_since'],
410
                if_unmodified_since=self['if_unmodified_since'],
411
                until=self['until'],
412
                meta=self['meta'],
413
                show_only_shared=self['shared'])
414
            self.print_objects(r.json)
415

    
416
    def main(self, container____path__=None):
417
        super(self.__class__, self)._run(container____path__)
418
        self._run()
419

    
420

    
421
@command(pithos_cmds)
422
class file_mkdir(_file_container_command):
423
    """Create a directory"""
424

    
425
    __doc__ += '\n. '.join([
426
        'Kamaki hanldes directories the same way as OOS Storage and Pithos+:',
427
        'A   directory  is   an  object  with  type  "application/directory"',
428
        'An object with path  dir/name can exist even if  dir does not exist',
429
        'or even if dir  is  a non  directory  object.  Users can modify dir',
430
        'without affecting the dir/name object in any way.'])
431

    
432
    @errors.generic.all
433
    @errors.pithos.connection
434
    @errors.pithos.container
435
    def _run(self):
436
        self.client.create_directory(self.path)
437

    
438
    def main(self, container___directory):
439
        super(self.__class__, self)._run(
440
            container___directory,
441
            path_is_optional=False)
442
        self._run()
443

    
444

    
445
@command(pithos_cmds)
446
class file_touch(_file_container_command):
447
    """Create an empty object (file)
448
    If object exists, this command will reset it to 0 length
449
    """
450

    
451
    arguments = dict(
452
        content_type=ValueArgument(
453
            'Set content type (default: application/octet-stream)',
454
            '--content-type',
455
            default='application/octet-stream')
456
    )
457

    
458
    @errors.generic.all
459
    @errors.pithos.connection
460
    @errors.pithos.container
461
    def _run(self):
462
        self.client.create_object(self.path, self['content_type'])
463

    
464
    def main(self, container___path):
465
        super(file_touch, self)._run(
466
            container___path,
467
            path_is_optional=False)
468
        self._run()
469

    
470

    
471
@command(pithos_cmds)
472
class file_create(_file_container_command):
473
    """Create a container"""
474

    
475
    arguments = dict(
476
        versioning=ValueArgument(
477
            'set container versioning (auto/none)',
478
            '--versioning'),
479
        limit=IntArgument('set default container limit', '--limit'),
480
        meta=KeyValueArgument(
481
            'set container metadata (can be repeated)',
482
            '--meta')
483
    )
484

    
485
    @errors.generic.all
486
    @errors.pithos.connection
487
    @errors.pithos.container
488
    def _run(self):
489
        self.client.container_put(
490
            limit=self['limit'],
491
            versioning=self['versioning'],
492
            metadata=self['meta'])
493

    
494
    def main(self, container=None):
495
        super(self.__class__, self)._run(container)
496
        if container and self.container != container:
497
            raiseCLIError('Invalid container name %s' % container, details=[
498
                'Did you mean "%s" ?' % self.container,
499
                'Use --container for names containing :'])
500
        self._run()
501

    
502

    
503
class _source_destination_command(_file_container_command):
504

    
505
    arguments = dict(
506
        destination_account=ValueArgument('', ('a', '--dst-account')),
507
        recursive=FlagArgument('', ('-R', '--recursive')),
508
        prefix=FlagArgument('', '--with-prefix', default=''),
509
        suffix=ValueArgument('', '--with-suffix', default=''),
510
        add_prefix=ValueArgument('', '--add-prefix', default=''),
511
        add_suffix=ValueArgument('', '--add-suffix', default=''),
512
        prefix_replace=ValueArgument('', '--prefix-to-replace', default=''),
513
        suffix_replace=ValueArgument('', '--suffix-to-replace', default='')
514
    )
515

    
516
    def __init__(self, arguments={}):
517
        self.arguments.update(arguments)
518
        super(_source_destination_command, self).__init__(self.arguments)
519

    
520
    def _run(self, source_container___path, path_is_optional=False):
521
        super(_source_destination_command, self)._run(
522
            source_container___path,
523
            path_is_optional)
524
        self.dst_client = PithosClient(
525
            base_url=self.client.base_url,
526
            token=self.client.token,
527
            account=self['destination_account'] or self.client.account)
528

    
529
    @errors.generic.all
530
    @errors.pithos.account
531
    def _dest_container_path(self, dest_container_path):
532
        if self['destination_container']:
533
            self.dst_client.container = self['destination_container']
534
            return (self['destination_container'], dest_container_path)
535
        if dest_container_path:
536
            dst = dest_container_path.split(':')
537
            if len(dst) > 1:
538
                try:
539
                    self.dst_client.container = dst[0]
540
                    self.dst_client.get_container_info(dst[0])
541
                except ClientError as err:
542
                    if err.status in (404, 204):
543
                        raiseCLIError(
544
                            'Destination container %s not found' % dst[0])
545
                    raise
546
                else:
547
                    self.dst_client.container = dst[0]
548
                return (dst[0], dst[1])
549
            return(None, dst[0])
550
        raiseCLIError('No destination container:path provided')
551

    
552
    def _get_all(self, prefix):
553
        return self.client.container_get(prefix=prefix).json
554

    
555
    def _get_src_objects(self, src_path):
556
        """Get a list of the source objects to be called
557

558
        :param src_path: (str) source path
559

560
        :returns: (method, params) a method that returns a list when called
561
        or (object) if it is a single object
562
        """
563
        if src_path and src_path[-1] == '/':
564
            src_path = src_path[:-1]
565

    
566
        if self['prefix']:
567
            return (self._get_all, dict(prefix=src_path))
568
        try:
569
            srcobj = self.client.get_object_info(src_path)
570
        except ClientError as srcerr:
571
            if srcerr.status == 404:
572
                raiseCLIError(
573
                    'Source object %s not in source container %s' % (
574
                        src_path,
575
                        self.client.container),
576
                    details=['Hint: --with-prefix to match multiple objects'])
577
            elif srcerr.status not in (204,):
578
                raise
579
            return (self.client.list_objects, {})
580

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

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

    
597
        if dst_path and dst_path.endswith('/'):
598
            dst_path = dst_path[:-1]
599

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

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

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

    
645

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

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

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

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

    
736

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

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

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

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

    
828

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

    
837
    arguments = dict(
838
        progress_bar=ProgressBarArgument(
839
            'do not show progress bar',
840
            ('-N', '--no-progress-bar'),
841
            default=False)
842
    )
843

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

    
859
    def main(self, local_path, container___path):
860
        super(self.__class__, self)._run(
861
            container___path,
862
            path_is_optional=False)
863
        self._run(local_path)
864

    
865

    
866
@command(pithos_cmds)
867
class file_truncate(_file_container_command):
868
    """Truncate remote file up to a size (default is 0)"""
869

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

    
878
    def main(self, container___path, size=0):
879
        super(self.__class__, self)._run(container___path)
880
        self._run(size=size)
881

    
882

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

    
893
    arguments = dict(
894
        progress_bar=ProgressBarArgument(
895
            'do not show progress bar',
896
            ('-N', '--no-progress-bar'),
897
            default=False)
898
    )
899

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

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

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

    
937

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

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

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

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

    
990

    
991
@command(pithos_cmds)
992
class file_upload(_file_container_command):
993
    """Upload a file"""
994

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

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

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

    
1111
    @errors.generic.all
1112
    @errors.pithos.connection
1113
    @errors.pithos.container
1114
    @errors.pithos.object_path
1115
    @errors.pithos.local_path
1116
    def _run(self, local_path, remote_path):
1117
        poolsize = self['poolsize']
1118
        if poolsize > 0:
1119
            self.client.MAX_THREADS = int(poolsize)
1120
        params = dict(
1121
            content_encoding=self['content_encoding'],
1122
            content_type=self['content_type'],
1123
            content_disposition=self['content_disposition'],
1124
            sharing=self['sharing'],
1125
            public=self['public'])
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 self['unchunked']:
1129
                self.client.upload_object_unchunked(
1130
                    rpath, f,
1131
                    etag=self['etag'], withHashFile=self['use_hashes'],
1132
                    **params)
1133
            else:
1134
                try:
1135
                    (progress_bar, upload_cb) = self._safe_progress_bar(
1136
                        'Uploading %s' % f.name.split(path.sep)[-1])
1137
                    if progress_bar:
1138
                        hash_bar = progress_bar.clone()
1139
                        hash_cb = hash_bar.get_generator(
1140
                            'Calculating block hashes')
1141
                    else:
1142
                        hash_cb = None
1143
                    self.client.upload_object(
1144
                        rpath, f,
1145
                        hash_cb=hash_cb, upload_cb=upload_cb,
1146
                        **params)
1147
                except Exception:
1148
                    self._safe_progress_bar_finish(progress_bar)
1149
                    raise
1150
                finally:
1151
                    self._safe_progress_bar_finish(progress_bar)
1152
        print 'Upload completed'
1153

    
1154
    def main(self, local_path, container____path__=None):
1155
        super(self.__class__, self)._run(container____path__)
1156
        remote_path = self.path or path.basename(local_path)
1157
        self._run(local_path=local_path, remote_path=remote_path)
1158

    
1159

    
1160
@command(pithos_cmds)
1161
class file_cat(_file_container_command):
1162
    """Print remote file contents to console"""
1163

    
1164
    arguments = dict(
1165
        range=RangeArgument('show range of data', '--range'),
1166
        if_match=ValueArgument('show output if ETags match', '--if-match'),
1167
        if_none_match=ValueArgument(
1168
            'show output if ETags match',
1169
            '--if-none-match'),
1170
        if_modified_since=DateArgument(
1171
            'show output modified since then',
1172
            '--if-modified-since'),
1173
        if_unmodified_since=DateArgument(
1174
            'show output unmodified since then',
1175
            '--if-unmodified-since'),
1176
        object_version=ValueArgument(
1177
            'get the specific version',
1178
            ('-j', '--object-version'))
1179
    )
1180

    
1181
    @errors.generic.all
1182
    @errors.pithos.connection
1183
    @errors.pithos.container
1184
    @errors.pithos.object_path
1185
    def _run(self):
1186
        self.client.download_object(
1187
            self.path,
1188
            stdout,
1189
            range_str=self['range'],
1190
            version=self['object_version'],
1191
            if_match=self['if_match'],
1192
            if_none_match=self['if_none_match'],
1193
            if_modified_since=self['if_modified_since'],
1194
            if_unmodified_since=self['if_unmodified_since'])
1195

    
1196
    def main(self, container___path):
1197
        super(self.__class__, self)._run(
1198
            container___path,
1199
            path_is_optional=False)
1200
        self._run()
1201

    
1202

    
1203
@command(pithos_cmds)
1204
class file_download(_file_container_command):
1205
    """Download remote object as local file
1206
    If local destination is a directory:
1207
    *   download <container>:<path> <local dir> -R
1208
    will download all files on <container> prefixed as <path>,
1209
    to <local dir>/<full path>
1210
    *   download <container>:<path> <local dir> --exact-match
1211
    will download only one file, exactly matching <path>
1212
    ATTENTION: to download cont:dir1/dir2/file there must exist objects
1213
    cont:dir1 and cont:dir1/dir2 of type application/directory
1214
    To create directory objects, use /file mkdir
1215
    """
1216

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

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

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

    
1345
    @errors.generic.all
1346
    @errors.pithos.connection
1347
    @errors.pithos.container
1348
    @errors.pithos.object_path
1349
    @errors.pithos.local_path
1350
    def _run(self, local_path):
1351
        #outputs = self._outputs(local_path)
1352
        poolsize = self['poolsize']
1353
        if poolsize:
1354
            self.client.MAX_THREADS = int(poolsize)
1355
        progress_bar = None
1356
        try:
1357
            for f, rpath in self._outputs(local_path):
1358
                (
1359
                    progress_bar,
1360
                    download_cb) = self._safe_progress_bar(
1361
                        'Download %s' % rpath)
1362
                self.client.download_object(
1363
                    rpath,
1364
                    f,
1365
                    download_cb=download_cb,
1366
                    range_str=self['range'],
1367
                    version=self['object_version'],
1368
                    if_match=self['if_match'],
1369
                    resume=self['resume'],
1370
                    if_none_match=self['if_none_match'],
1371
                    if_modified_since=self['if_modified_since'],
1372
                    if_unmodified_since=self['if_unmodified_since'])
1373
        except KeyboardInterrupt:
1374
            from threading import activeCount, enumerate as activethreads
1375
            timeout = 0.5
1376
            while activeCount() > 1:
1377
                stdout.write('\nCancel %s threads: ' % (activeCount() - 1))
1378
                stdout.flush()
1379
                for thread in activethreads():
1380
                    try:
1381
                        thread.join(timeout)
1382
                        stdout.write('.' if thread.isAlive() else '*')
1383
                    except RuntimeError:
1384
                        continue
1385
                    finally:
1386
                        stdout.flush()
1387
                        timeout += 0.1
1388

    
1389
            print('\nDownload canceled by user')
1390
            if local_path is not None:
1391
                print('to resume, re-run with --resume')
1392
        except Exception:
1393
            self._safe_progress_bar_finish(progress_bar)
1394
            raise
1395
        finally:
1396
            self._safe_progress_bar_finish(progress_bar)
1397

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

    
1402

    
1403
@command(pithos_cmds)
1404
class file_hashmap(_file_container_command):
1405
    """Get the hash-map of an object"""
1406

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

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

    
1437
    def main(self, container___path):
1438
        super(self.__class__, self)._run(
1439
            container___path,
1440
            path_is_optional=False)
1441
        self._run()
1442

    
1443

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

    
1463
    arguments = dict(
1464
        until=DateArgument('remove history until that date', '--until'),
1465
        yes=FlagArgument('Do not prompt for permission', '--yes'),
1466
        recursive=FlagArgument(
1467
            'empty dir or container and delete (if dir)',
1468
            ('-R', '--recursive'))
1469
    )
1470

    
1471
    def __init__(self, arguments={}):
1472
        super(self.__class__, self).__init__(arguments)
1473
        self['delimiter'] = DelimiterArgument(
1474
            self,
1475
            parsed_name='--delimiter',
1476
            help='delete objects prefixed with <object><delimiter>')
1477

    
1478
    @errors.generic.all
1479
    @errors.pithos.connection
1480
    @errors.pithos.container
1481
    @errors.pithos.object_path
1482
    def _run(self):
1483
        if self.path:
1484
            if self['yes'] or ask_user(
1485
                    'Delete %s:%s ?' % (self.container, self.path)):
1486
                self.client.del_object(
1487
                    self.path,
1488
                    until=self['until'],
1489
                    delimiter=self['delimiter'])
1490
            else:
1491
                print('Aborted')
1492
        else:
1493
            if self['recursive']:
1494
                ask_msg = 'Delete container contents'
1495
            else:
1496
                ask_msg = 'Delete container'
1497
            if self['yes'] or ask_user('%s %s ?' % (ask_msg, self.container)):
1498
                self.client.del_container(
1499
                    until=self['until'],
1500
                    delimiter=self['delimiter'])
1501
            else:
1502
                print('Aborted')
1503

    
1504
    def main(self, container____path__=None):
1505
        super(self.__class__, self)._run(container____path__)
1506
        self._run()
1507

    
1508

    
1509
@command(pithos_cmds)
1510
class file_purge(_file_container_command):
1511
    """Delete a container and release related data blocks
1512
    Non-empty containers can not purged.
1513
    To purge a container with content:
1514
    .   /file delete -R <container>
1515
    .      objects are deleted, but data blocks remain on server
1516
    .   /file purge <container>
1517
    .      container and data blocks are released and deleted
1518
    """
1519

    
1520
    arguments = dict(
1521
        yes=FlagArgument('Do not prompt for permission', '--yes'),
1522
    )
1523

    
1524
    @errors.generic.all
1525
    @errors.pithos.connection
1526
    @errors.pithos.container
1527
    def _run(self):
1528
        if self['yes'] or ask_user('Purge container %s?' % self.container):
1529
                self.client.purge_container()
1530
        else:
1531
            print('Aborted')
1532

    
1533
    def main(self, container=None):
1534
        super(self.__class__, self)._run(container)
1535
        if container and self.container != container:
1536
            raiseCLIError('Invalid container name %s' % container, details=[
1537
                'Did you mean "%s" ?' % self.container,
1538
                'Use --container for names containing :'])
1539
        self._run()
1540

    
1541

    
1542
@command(pithos_cmds)
1543
class file_publish(_file_container_command):
1544
    """Publish the object and print the public url"""
1545

    
1546
    @errors.generic.all
1547
    @errors.pithos.connection
1548
    @errors.pithos.container
1549
    @errors.pithos.object_path
1550
    def _run(self):
1551
        url = self.client.publish_object(self.path)
1552
        print(url)
1553

    
1554
    def main(self, container___path):
1555
        super(self.__class__, self)._run(
1556
            container___path,
1557
            path_is_optional=False)
1558
        self._run()
1559

    
1560

    
1561
@command(pithos_cmds)
1562
class file_unpublish(_file_container_command):
1563
    """Unpublish an object"""
1564

    
1565
    @errors.generic.all
1566
    @errors.pithos.connection
1567
    @errors.pithos.container
1568
    @errors.pithos.object_path
1569
    def _run(self):
1570
            self.client.unpublish_object(self.path)
1571

    
1572
    def main(self, container___path):
1573
        super(self.__class__, self)._run(
1574
            container___path,
1575
            path_is_optional=False)
1576
        self._run()
1577

    
1578

    
1579
@command(pithos_cmds)
1580
class file_permissions(_file_container_command):
1581
    """Get read and write permissions of an object
1582
    Permissions are lists of users and user groups. There is read and write
1583
    permissions. Users and groups with write permission have also read
1584
    permission.
1585
    """
1586

    
1587
    @errors.generic.all
1588
    @errors.pithos.connection
1589
    @errors.pithos.container
1590
    @errors.pithos.object_path
1591
    def _run(self):
1592
        r = self.client.get_object_sharing(self.path)
1593
        print_dict(r)
1594

    
1595
    def main(self, container___path):
1596
        super(self.__class__, self)._run(
1597
            container___path,
1598
            path_is_optional=False)
1599
        self._run()
1600

    
1601

    
1602
@command(pithos_cmds)
1603
class file_setpermissions(_file_container_command):
1604
    """Set permissions for an object
1605
    New permissions overwrite existing permissions.
1606
    Permission format:
1607
    -   read=<username>[,usergroup[,...]]
1608
    -   write=<username>[,usegroup[,...]]
1609
    E.g. to give read permissions for file F to users A and B and write for C:
1610
    .       /file setpermissions F read=A,B write=C
1611
    """
1612

    
1613
    @errors.generic.all
1614
    def format_permition_dict(self, permissions):
1615
        read = False
1616
        write = False
1617
        for perms in permissions:
1618
            splstr = perms.split('=')
1619
            if 'read' == splstr[0]:
1620
                read = [ug.strip() for ug in splstr[1].split(',')]
1621
            elif 'write' == splstr[0]:
1622
                write = [ug.strip() for ug in splstr[1].split(',')]
1623
            else:
1624
                msg = 'Usage:\tread=<groups,users> write=<groups,users>'
1625
                raiseCLIError(None, msg)
1626
        return (read, write)
1627

    
1628
    @errors.generic.all
1629
    @errors.pithos.connection
1630
    @errors.pithos.container
1631
    @errors.pithos.object_path
1632
    def _run(self, read, write):
1633
        self.client.set_object_sharing(
1634
            self.path,
1635
            read_permition=read,
1636
            write_permition=write)
1637

    
1638
    def main(self, container___path, *permissions):
1639
        super(self.__class__, self)._run(
1640
            container___path,
1641
            path_is_optional=False)
1642
        (read, write) = self.format_permition_dict(permissions)
1643
        self._run(read, write)
1644

    
1645

    
1646
@command(pithos_cmds)
1647
class file_delpermissions(_file_container_command):
1648
    """Delete all permissions set on object
1649
    To modify permissions, use /file setpermssions
1650
    """
1651

    
1652
    @errors.generic.all
1653
    @errors.pithos.connection
1654
    @errors.pithos.container
1655
    @errors.pithos.object_path
1656
    def _run(self):
1657
        self.client.del_object_sharing(self.path)
1658

    
1659
    def main(self, container___path):
1660
        super(self.__class__, self)._run(
1661
            container___path,
1662
            path_is_optional=False)
1663
        self._run()
1664

    
1665

    
1666
@command(pithos_cmds)
1667
class file_info(_file_container_command):
1668
    """Get detailed information for user account, containers or objects
1669
    to get account info:    /file info
1670
    to get container info:  /file info <container>
1671
    to get object info:     /file info <container>:<path>
1672
    """
1673

    
1674
    arguments = dict(
1675
        object_version=ValueArgument(
1676
            'show specific version \ (applies only for objects)',
1677
            ('-j', '--object-version'))
1678
    )
1679

    
1680
    @errors.generic.all
1681
    @errors.pithos.connection
1682
    @errors.pithos.container
1683
    @errors.pithos.object_path
1684
    def _run(self):
1685
        if self.container is None:
1686
            r = self.client.get_account_info()
1687
        elif self.path is None:
1688
            r = self.client.get_container_info(self.container)
1689
        else:
1690
            r = self.client.get_object_info(
1691
                self.path,
1692
                version=self['object_version'])
1693
        print_dict(r)
1694

    
1695
    def main(self, container____path__=None):
1696
        super(self.__class__, self)._run(container____path__)
1697
        self._run()
1698

    
1699

    
1700
@command(pithos_cmds)
1701
class file_meta(_file_container_command):
1702
    """Get metadata for account, containers or objects"""
1703

    
1704
    arguments = dict(
1705
        detail=FlagArgument('show detailed output', ('-l', '--details')),
1706
        until=DateArgument('show metadata until then', '--until'),
1707
        object_version=ValueArgument(
1708
            'show specific version \ (applies only for objects)',
1709
            ('-j', '--object-version'))
1710
    )
1711

    
1712
    @errors.generic.all
1713
    @errors.pithos.connection
1714
    @errors.pithos.container
1715
    @errors.pithos.object_path
1716
    def _run(self):
1717
        until = self['until']
1718
        if self.container is None:
1719
            if self['detail']:
1720
                r = self.client.get_account_info(until=until)
1721
            else:
1722
                r = self.client.get_account_meta(until=until)
1723
                r = pretty_keys(r, '-')
1724
            if r:
1725
                print(bold(self.client.account))
1726
        elif self.path is None:
1727
            if self['detail']:
1728
                r = self.client.get_container_info(until=until)
1729
            else:
1730
                cmeta = self.client.get_container_meta(until=until)
1731
                ometa = self.client.get_container_object_meta(until=until)
1732
                r = {}
1733
                if cmeta:
1734
                    r['container-meta'] = pretty_keys(cmeta, '-')
1735
                if ometa:
1736
                    r['object-meta'] = pretty_keys(ometa, '-')
1737
        else:
1738
            if self['detail']:
1739
                r = self.client.get_object_info(
1740
                    self.path,
1741
                    version=self['object_version'])
1742
            else:
1743
                r = self.client.get_object_meta(
1744
                    self.path,
1745
                    version=self['object_version'])
1746
            if r:
1747
                r = pretty_keys(pretty_keys(r, '-'))
1748
        if r:
1749
            print_dict(r)
1750

    
1751
    def main(self, container____path__=None):
1752
        super(self.__class__, self)._run(container____path__)
1753
        self._run()
1754

    
1755

    
1756
@command(pithos_cmds)
1757
class file_setmeta(_file_container_command):
1758
    """Set a piece of metadata for account, container or object
1759
    Metadata are formed as key:value pairs
1760
    """
1761

    
1762
    @errors.generic.all
1763
    @errors.pithos.connection
1764
    @errors.pithos.container
1765
    @errors.pithos.object_path
1766
    def _run(self, metakey, metaval):
1767
        if not self.container:
1768
            self.client.set_account_meta({metakey: metaval})
1769
        elif not self.path:
1770
            self.client.set_container_meta({metakey: metaval})
1771
        else:
1772
            self.client.set_object_meta(self.path, {metakey: metaval})
1773

    
1774
    def main(self, metakey, metaval, container____path__=None):
1775
        super(self.__class__, self)._run(container____path__)
1776
        self._run(metakey=metakey, metaval=metaval)
1777

    
1778

    
1779
@command(pithos_cmds)
1780
class file_delmeta(_file_container_command):
1781
    """Delete metadata with given key from account, container or object
1782
    Metadata are formed as key:value objects
1783
    - to get metadata of current account:     /file meta
1784
    - to get metadata of a container:         /file meta <container>
1785
    - to get metadata of an object:           /file meta <container>:<path>
1786
    """
1787

    
1788
    @errors.generic.all
1789
    @errors.pithos.connection
1790
    @errors.pithos.container
1791
    @errors.pithos.object_path
1792
    def _run(self, metakey):
1793
        if self.container is None:
1794
            self.client.del_account_meta(metakey)
1795
        elif self.path is None:
1796
            self.client.del_container_meta(metakey)
1797
        else:
1798
            self.client.del_object_meta(self.path, metakey)
1799

    
1800
    def main(self, metakey, container____path__=None):
1801
        super(self.__class__, self)._run(container____path__)
1802
        self._run(metakey)
1803

    
1804

    
1805
@command(pithos_cmds)
1806
class file_quota(_file_account_command):
1807
    """Get account quota"""
1808

    
1809
    arguments = dict(
1810
        in_bytes=FlagArgument('Show result in bytes', ('-b', '--bytes'))
1811
    )
1812

    
1813
    @errors.generic.all
1814
    @errors.pithos.connection
1815
    def _run(self):
1816
        reply = self.client.get_account_quota()
1817
        if not self['in_bytes']:
1818
            for k in reply:
1819
                reply[k] = format_size(reply[k])
1820
        print_dict(pretty_keys(reply, '-'))
1821

    
1822
    def main(self, custom_uuid=None):
1823
        super(self.__class__, self)._run(custom_account=custom_uuid)
1824
        self._run()
1825

    
1826

    
1827
@command(pithos_cmds)
1828
class file_containerlimit(_pithos_init):
1829
    """Container size limit commands"""
1830

    
1831

    
1832
@command(pithos_cmds)
1833
class file_containerlimit_get(_file_container_command):
1834
    """Get container size limit"""
1835

    
1836
    arguments = dict(
1837
        in_bytes=FlagArgument('Show result in bytes', ('-b', '--bytes'))
1838
    )
1839

    
1840
    @errors.generic.all
1841
    @errors.pithos.container
1842
    def _run(self):
1843
        reply = self.client.get_container_limit(self.container)
1844
        if not self['in_bytes']:
1845
            for k, v in reply.items():
1846
                reply[k] = 'unlimited' if '0' == v else format_size(v)
1847
        print_dict(pretty_keys(reply, '-'))
1848

    
1849
    def main(self, container=None):
1850
        super(self.__class__, self)._run()
1851
        self.container = container
1852
        self._run()
1853

    
1854

    
1855
@command(pithos_cmds)
1856
class file_containerlimit_set(_file_account_command):
1857
    """Set new storage limit for a container
1858
    By default, the limit is set in bytes
1859
    Users may specify a different unit, e.g:
1860
    /file containerlimit set 2.3GB mycontainer
1861
    Valid units: B, KiB (1024 B), KB (1000 B), MiB, MB, GiB, GB, TiB, TB
1862
    To set container limit to "unlimited", use 0
1863
    """
1864

    
1865
    @errors.generic.all
1866
    def _calculate_limit(self, user_input):
1867
        limit = 0
1868
        try:
1869
            limit = int(user_input)
1870
        except ValueError:
1871
            index = 0
1872
            digits = [str(num) for num in range(0, 10)] + ['.']
1873
            while user_input[index] in digits:
1874
                index += 1
1875
            limit = user_input[:index]
1876
            format = user_input[index:]
1877
            try:
1878
                return to_bytes(limit, format)
1879
            except Exception as qe:
1880
                msg = 'Failed to convert %s to bytes' % user_input,
1881
                raiseCLIError(qe, msg, details=[
1882
                    'Syntax: containerlimit set <limit>[format] [container]',
1883
                    'e.g.: containerlimit set 2.3GB mycontainer',
1884
                    'Valid formats:',
1885
                    '(*1024): B, KiB, MiB, GiB, TiB',
1886
                    '(*1000): B, KB, MB, GB, TB'])
1887
        return limit
1888

    
1889
    @errors.generic.all
1890
    @errors.pithos.connection
1891
    @errors.pithos.container
1892
    def _run(self, limit):
1893
        if self.container:
1894
            self.client.container = self.container
1895
        self.client.set_container_limit(limit)
1896

    
1897
    def main(self, limit, container=None):
1898
        super(self.__class__, self)._run()
1899
        limit = self._calculate_limit(limit)
1900
        self.container = container
1901
        self._run(limit)
1902

    
1903

    
1904
@command(pithos_cmds)
1905
class file_versioning(_file_account_command):
1906
    """Get  versioning for account or container"""
1907

    
1908
    @errors.generic.all
1909
    @errors.pithos.connection
1910
    @errors.pithos.container
1911
    def _run(self):
1912
        if self.container:
1913
            r = self.client.get_container_versioning(self.container)
1914
        else:
1915
            r = self.client.get_account_versioning()
1916
        print_dict(r)
1917

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

    
1923

    
1924
@command(pithos_cmds)
1925
class file_setversioning(_file_account_command):
1926
    """Set versioning mode (auto, none) for account or container"""
1927

    
1928
    def _check_versioning(self, versioning):
1929
        if versioning and versioning.lower() in ('auto', 'none'):
1930
            return versioning.lower()
1931
        raiseCLIError('Invalid versioning %s' % versioning, details=[
1932
            'Versioning can be auto or none'])
1933

    
1934
    @errors.generic.all
1935
    @errors.pithos.connection
1936
    @errors.pithos.container
1937
    def _run(self, versioning):
1938
        if self.container:
1939
            self.client.container = self.container
1940
            self.client.set_container_versioning(versioning)
1941
        else:
1942
            self.client.set_account_versioning(versioning)
1943

    
1944
    def main(self, versioning, container=None):
1945
        super(self.__class__, self)._run()
1946
        self._run(self._check_versioning(versioning))
1947

    
1948

    
1949
@command(pithos_cmds)
1950
class file_group(_file_account_command):
1951
    """Get groups and group members"""
1952

    
1953
    @errors.generic.all
1954
    @errors.pithos.connection
1955
    def _run(self):
1956
        r = self.client.get_account_group()
1957
        print_dict(pretty_keys(r, '-'))
1958

    
1959
    def main(self):
1960
        super(self.__class__, self)._run()
1961
        self._run()
1962

    
1963

    
1964
@command(pithos_cmds)
1965
class file_setgroup(_file_account_command):
1966
    """Set a user group"""
1967

    
1968
    @errors.generic.all
1969
    @errors.pithos.connection
1970
    def _run(self, groupname, *users):
1971
        self.client.set_account_group(groupname, users)
1972

    
1973
    def main(self, groupname, *users):
1974
        super(self.__class__, self)._run()
1975
        if users:
1976
            self._run(groupname, *users)
1977
        else:
1978
            raiseCLIError('No users to add in group %s' % groupname)
1979

    
1980

    
1981
@command(pithos_cmds)
1982
class file_delgroup(_file_account_command):
1983
    """Delete a user group"""
1984

    
1985
    @errors.generic.all
1986
    @errors.pithos.connection
1987
    def _run(self, groupname):
1988
        self.client.del_account_group(groupname)
1989

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

    
1994

    
1995
@command(pithos_cmds)
1996
class file_sharers(_file_account_command):
1997
    """List the accounts that share objects with current user"""
1998

    
1999
    arguments = dict(
2000
        detail=FlagArgument('show detailed output', ('-l', '--details')),
2001
        marker=ValueArgument('show output greater then marker', '--marker')
2002
    )
2003

    
2004
    @errors.generic.all
2005
    @errors.pithos.connection
2006
    def _run(self):
2007
        accounts = self.client.get_sharing_accounts(marker=self['marker'])
2008
        if self['detail']:
2009
            print_items(accounts)
2010
        else:
2011
            print_items([acc['name'] for acc in accounts])
2012

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

    
2017

    
2018
@command(pithos_cmds)
2019
class file_versions(_file_container_command):
2020
    """Get the list of object versions
2021
    Deleted objects may still have versions that can be used to restore it and
2022
    get information about its previous state.
2023
    The version number can be used in a number of other commands, like info,
2024
    copy, move, meta. See these commands for more information, e.g.
2025
    /file info -h
2026
    """
2027

    
2028
    @errors.generic.all
2029
    @errors.pithos.connection
2030
    @errors.pithos.container
2031
    @errors.pithos.object_path
2032
    def _run(self):
2033
        versions = self.client.get_object_versionlist(self.path)
2034
        print_items([dict(id=vitem[0], created=strftime(
2035
            '%d-%m-%Y %H:%M:%S',
2036
            localtime(float(vitem[1])))) for vitem in versions])
2037

    
2038
    def main(self, container___path):
2039
        super(file_versions, self)._run(
2040
            container___path,
2041
            path_is_optional=False)
2042
        self._run()