Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / pithos.py @ 14b25e00

History | View | Annotate | Download (72.6 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 logging import getLogger
37
from os import path, makedirs, walk
38

    
39
from kamaki.cli import command
40
from kamaki.cli.command_tree import CommandTree
41
from kamaki.cli.errors import raiseCLIError, CLISyntaxError
42
from kamaki.cli.utils import (
43
    format_size, to_bytes, print_dict, print_items, pretty_keys,
44
    page_hold, bold, ask_user, get_path_size)
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.clients.pithos import PithosClient, ClientError
50
from kamaki.clients.astakos import AstakosClient
51

    
52

    
53
kloger = getLogger('kamaki')
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, newvalue):
137
        if newvalue is None:
138
            self._value = self.default
139
            return
140
        (start, end) = newvalue.split('-')
141
        (start, end) = (int(start), int(end))
142
        self._value = '%s-%s' % (start, end)
143

    
144
# Command specs
145

    
146

    
147
class _pithos_init(_command_init):
148
    """Initialize a pithos+ kamaki client"""
149

    
150
    @staticmethod
151
    def _is_dir(remote_dict):
152
        return 'application/directory' == remote_dict.get(
153
            'content_type',
154
            remote_dict.get('content-type', ''))
155

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

    
173
    def main(self):
174
        self._run()
175

    
176
    def _set_account(self):
177
        user = AstakosClient(self.config.get('user', 'url'), self.token)
178
        self.account = self['account'] or user.term('uuid')
179

    
180
        """Backwards compatibility"""
181
        self.account = self.account\
182
            or self.config.get('file', 'account')\
183
            or self.config.get('global', 'account')
184

    
185

    
186
class _file_account_command(_pithos_init):
187
    """Base class for account level storage commands"""
188

    
189
    def __init__(self, arguments={}):
190
        super(_file_account_command, self).__init__(arguments)
191
        self['account'] = ValueArgument(
192
            'Set user account (not permanent)',
193
            ('-A', '--account'))
194

    
195
    def _run(self, custom_account=None):
196
        super(_file_account_command, self)._run()
197
        if custom_account:
198
            self.client.account = custom_account
199
        elif self['account']:
200
            self.client.account = self['account']
201

    
202
    @errors.generic.all
203
    def main(self):
204
        self._run()
205

    
206

    
207
class _file_container_command(_file_account_command):
208
    """Base class for container level storage commands"""
209

    
210
    container = None
211
    path = None
212

    
213
    def __init__(self, arguments={}):
214
        super(_file_container_command, self).__init__(arguments)
215
        self['container'] = ValueArgument(
216
            'Set container to work with (temporary)',
217
            ('-C', '--container'))
218

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

    
239
        user_cont, sep, userpath = container_with_path.partition(':')
240

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

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

    
292
    def main(self, container_with_path=None, path_is_optional=True):
293
        self._run(container_with_path, path_is_optional)
294

    
295

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

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

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

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

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

    
420
    def main(self, container____path__=None):
421
        super(self.__class__, self)._run(container____path__)
422
        self._run()
423

    
424

    
425
@command(pithos_cmds)
426
class file_mkdir(_file_container_command):
427
    """Create a directory"""
428

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

    
436
    @errors.generic.all
437
    @errors.pithos.connection
438
    @errors.pithos.container
439
    def _run(self):
440
        self.client.create_directory(self.path)
441

    
442
    def main(self, container___directory):
443
        super(self.__class__, self)._run(
444
            container___directory,
445
            path_is_optional=False)
446
        self._run()
447

    
448

    
449
@command(pithos_cmds)
450
class file_touch(_file_container_command):
451
    """Create an empty object (file)
452
    If object exists, this command will reset it to 0 length
453
    """
454

    
455
    arguments = dict(
456
        content_type=ValueArgument(
457
            'Set content type (default: application/octet-stream)',
458
            '--content-type',
459
            default='application/octet-stream')
460
    )
461

    
462
    @errors.generic.all
463
    @errors.pithos.connection
464
    @errors.pithos.container
465
    def _run(self):
466
        self.client.create_object(self.path, self['content_type'])
467

    
468
    def main(self, container___path):
469
        super(file_touch, self)._run(
470
            container___path,
471
            path_is_optional=False)
472
        self._run()
473

    
474

    
475
@command(pithos_cmds)
476
class file_create(_file_container_command):
477
    """Create a container"""
478

    
479
    arguments = dict(
480
        versioning=ValueArgument(
481
            'set container versioning (auto/none)',
482
            '--versioning'),
483
        limit=IntArgument('set default container limit', '--limit'),
484
        meta=KeyValueArgument(
485
            'set container metadata (can be repeated)',
486
            '--meta')
487
    )
488

    
489
    @errors.generic.all
490
    @errors.pithos.connection
491
    @errors.pithos.container
492
    def _run(self):
493
        self.client.container_put(
494
            limit=self['limit'],
495
            versioning=self['versioning'],
496
            metadata=self['meta'])
497

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

    
506

    
507
class _source_destination_command(_file_container_command):
508

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

    
520
    def __init__(self, arguments={}):
521
        self.arguments.update(arguments)
522
        super(_source_destination_command, self).__init__(self.arguments)
523

    
524
    def _run(self, source_container___path, path_is_optional=False):
525
        super(_source_destination_command, self)._run(
526
            source_container___path,
527
            path_is_optional)
528
        self.dst_client = PithosClient(
529
            base_url=self.client.base_url,
530
            token=self.client.token,
531
            account=self['destination_account'] or self.client.account)
532

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

    
556
    def _get_all(self, prefix):
557
        return self.client.container_get(prefix=prefix).json
558

    
559
    def _get_src_objects(self, src_path):
560
        """Get a list of the source objects to be called
561

562
        :param src_path: (str) source path
563

564
        :returns: (method, params) a method that returns a list when called
565
        or (object) if it is a single object
566
        """
567
        if src_path and src_path[-1] == '/':
568
            src_path = src_path[:-1]
569

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

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

    
596
    def src_dst_pairs(self, ds_path):
597
        src_iter = self._get_src_objects(self.path)
598
        src_N = isinstance(src_iter, tuple)
599
        add_prefix = self['add_prefix'].strip('/')
600

    
601
        if dst_path and dst_path.endswith('/'):
602
            dst_path = dst_path[:-1]
603

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

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

    
642
    def _get_new_object(self, obj, add_prefix):
643
        if self['prefix_replace'] and obj.startswith(self['prefix_replace']):
644
            obj = obj[len(self['prefix_replace']):]
645
        if self['suffix_replace'] and obj.endswith(self['suffix_replace']):
646
            obj = obj[:-len(self['suffix_replace'])]
647
        return add_prefix + obj + self['add_suffix']
648

    
649

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

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

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

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

    
740

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

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

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

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

    
832

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

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

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

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

    
869

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

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

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

    
886

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

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

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

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

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

    
941

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

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

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

    
988
    def main(self, container___path):
989
        super(self.__class__, self)._run(
990
            container___path,
991
            path_is_optional=False)
992
        self.run()
993

    
994

    
995
@command(pithos_cmds)
996
class file_upload(_file_container_command):
997
    """Upload a file"""
998

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

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

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

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

    
1147
    def main(self, local_path, container____path__=None):
1148
        super(self.__class__, self)._run(container____path__)
1149
        remote_path = self.path or path.basename(local_path)
1150
        self._run(local_path=local_path, remote_path=remote_path)
1151

    
1152

    
1153
@command(pithos_cmds)
1154
class file_cat(_file_container_command):
1155
    """Print remote file contents to console"""
1156

    
1157
    arguments = dict(
1158
        range=RangeArgument('show range of data', '--range'),
1159
        if_match=ValueArgument('show output if ETags match', '--if-match'),
1160
        if_none_match=ValueArgument(
1161
            'show output if ETags match',
1162
            '--if-none-match'),
1163
        if_modified_since=DateArgument(
1164
            'show output modified since then',
1165
            '--if-modified-since'),
1166
        if_unmodified_since=DateArgument(
1167
            'show output unmodified since then',
1168
            '--if-unmodified-since'),
1169
        object_version=ValueArgument(
1170
            'get the specific version',
1171
            ('-j', '--object-version'))
1172
    )
1173

    
1174
    @errors.generic.all
1175
    @errors.pithos.connection
1176
    @errors.pithos.container
1177
    @errors.pithos.object_path
1178
    def _run(self):
1179
        self.client.download_object(
1180
            self.path,
1181
            stdout,
1182
            range_str=self['range'],
1183
            version=self['object_version'],
1184
            if_match=self['if_match'],
1185
            if_none_match=self['if_none_match'],
1186
            if_modified_since=self['if_modified_since'],
1187
            if_unmodified_since=self['if_unmodified_since'])
1188

    
1189
    def main(self, container___path):
1190
        super(self.__class__, self)._run(
1191
            container___path,
1192
            path_is_optional=False)
1193
        self._run()
1194

    
1195

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

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

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

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

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

    
1382
            print('\nDownload canceled by user')
1383
            if local_path is not None:
1384
                print('to resume, re-run with --resume')
1385
        except Exception:
1386
            self._safe_progress_bar_finish(progress_bar)
1387
            raise
1388
        finally:
1389
            self._safe_progress_bar_finish(progress_bar)
1390

    
1391
    def main(self, container___path, local_path=None):
1392
        super(self.__class__, self)._run(container___path)
1393
        self._run(local_path=local_path)
1394

    
1395

    
1396
@command(pithos_cmds)
1397
class file_hashmap(_file_container_command):
1398
    """Get the hash-map of an object"""
1399

    
1400
    arguments = dict(
1401
        if_match=ValueArgument('show output if ETags match', '--if-match'),
1402
        if_none_match=ValueArgument(
1403
            'show output if ETags match',
1404
            '--if-none-match'),
1405
        if_modified_since=DateArgument(
1406
            'show output modified since then',
1407
            '--if-modified-since'),
1408
        if_unmodified_since=DateArgument(
1409
            'show output unmodified since then',
1410
            '--if-unmodified-since'),
1411
        object_version=ValueArgument(
1412
            'get the specific version',
1413
            ('-j', '--object-version'))
1414
    )
1415

    
1416
    @errors.generic.all
1417
    @errors.pithos.connection
1418
    @errors.pithos.container
1419
    @errors.pithos.object_path
1420
    def _run(self):
1421
        data = self.client.get_object_hashmap(
1422
            self.path,
1423
            version=self['object_version'],
1424
            if_match=self['if_match'],
1425
            if_none_match=self['if_none_match'],
1426
            if_modified_since=self['if_modified_since'],
1427
            if_unmodified_since=self['if_unmodified_since'])
1428
        print_dict(data)
1429

    
1430
    def main(self, container___path):
1431
        super(self.__class__, self)._run(
1432
            container___path,
1433
            path_is_optional=False)
1434
        self._run()
1435

    
1436

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

    
1456
    arguments = dict(
1457
        until=DateArgument('remove history until that date', '--until'),
1458
        yes=FlagArgument('Do not prompt for permission', '--yes'),
1459
        recursive=FlagArgument(
1460
            'empty dir or container and delete (if dir)',
1461
            ('-R', '--recursive'))
1462
    )
1463

    
1464
    def __init__(self, arguments={}):
1465
        super(self.__class__, self).__init__(arguments)
1466
        self['delimiter'] = DelimiterArgument(
1467
            self,
1468
            parsed_name='--delimiter',
1469
            help='delete objects prefixed with <object><delimiter>')
1470

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

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

    
1501

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

    
1513
    arguments = dict(
1514
        yes=FlagArgument('Do not prompt for permission', '--yes'),
1515
    )
1516

    
1517
    @errors.generic.all
1518
    @errors.pithos.connection
1519
    @errors.pithos.container
1520
    def _run(self):
1521
        if self['yes'] or ask_user('Purge container %s?' % self.container):
1522
                self.client.purge_container()
1523
        else:
1524
            print('Aborted')
1525

    
1526
    def main(self, container=None):
1527
        super(self.__class__, self)._run(container)
1528
        if container and self.container != container:
1529
            raiseCLIError('Invalid container name %s' % container, details=[
1530
                'Did you mean "%s" ?' % self.container,
1531
                'Use --container for names containing :'])
1532
        self._run()
1533

    
1534

    
1535
@command(pithos_cmds)
1536
class file_publish(_file_container_command):
1537
    """Publish the object and print the public url"""
1538

    
1539
    @errors.generic.all
1540
    @errors.pithos.connection
1541
    @errors.pithos.container
1542
    @errors.pithos.object_path
1543
    def _run(self):
1544
        url = self.client.publish_object(self.path)
1545
        print(url)
1546

    
1547
    def main(self, container___path):
1548
        super(self.__class__, self)._run(
1549
            container___path,
1550
            path_is_optional=False)
1551
        self._run()
1552

    
1553

    
1554
@command(pithos_cmds)
1555
class file_unpublish(_file_container_command):
1556
    """Unpublish an object"""
1557

    
1558
    @errors.generic.all
1559
    @errors.pithos.connection
1560
    @errors.pithos.container
1561
    @errors.pithos.object_path
1562
    def _run(self):
1563
            self.client.unpublish_object(self.path)
1564

    
1565
    def main(self, container___path):
1566
        super(self.__class__, self)._run(
1567
            container___path,
1568
            path_is_optional=False)
1569
        self._run()
1570

    
1571

    
1572
@command(pithos_cmds)
1573
class file_permissions(_file_container_command):
1574
    """Get read and write permissions of an object
1575
    Permissions are lists of users and user groups. There is read and write
1576
    permissions. Users and groups with write permission have also read
1577
    permission.
1578
    """
1579

    
1580
    @errors.generic.all
1581
    @errors.pithos.connection
1582
    @errors.pithos.container
1583
    @errors.pithos.object_path
1584
    def _run(self):
1585
        r = self.client.get_object_sharing(self.path)
1586
        print_dict(r)
1587

    
1588
    def main(self, container___path):
1589
        super(self.__class__, self)._run(
1590
            container___path,
1591
            path_is_optional=False)
1592
        self._run()
1593

    
1594

    
1595
@command(pithos_cmds)
1596
class file_setpermissions(_file_container_command):
1597
    """Set permissions for an object
1598
    New permissions overwrite existing permissions.
1599
    Permission format:
1600
    -   read=<username>[,usergroup[,...]]
1601
    -   write=<username>[,usegroup[,...]]
1602
    E.g. to give read permissions for file F to users A and B and write for C:
1603
    .       /file setpermissions F read=A,B write=C
1604
    """
1605

    
1606
    @errors.generic.all
1607
    def format_permition_dict(self, permissions):
1608
        read = False
1609
        write = False
1610
        for perms in permissions:
1611
            splstr = perms.split('=')
1612
            if 'read' == splstr[0]:
1613
                read = [ug.strip() for ug in splstr[1].split(',')]
1614
            elif 'write' == splstr[0]:
1615
                write = [ug.strip() for ug in splstr[1].split(',')]
1616
            else:
1617
                msg = 'Usage:\tread=<groups,users> write=<groups,users>'
1618
                raiseCLIError(None, msg)
1619
        return (read, write)
1620

    
1621
    @errors.generic.all
1622
    @errors.pithos.connection
1623
    @errors.pithos.container
1624
    @errors.pithos.object_path
1625
    def _run(self, read, write):
1626
        self.client.set_object_sharing(
1627
            self.path,
1628
            read_permition=read,
1629
            write_permition=write)
1630

    
1631
    def main(self, container___path, *permissions):
1632
        super(self.__class__, self)._run(
1633
            container___path,
1634
            path_is_optional=False)
1635
        (read, write) = self.format_permition_dict(permissions)
1636
        self._run(read, write)
1637

    
1638

    
1639
@command(pithos_cmds)
1640
class file_delpermissions(_file_container_command):
1641
    """Delete all permissions set on object
1642
    To modify permissions, use /file setpermssions
1643
    """
1644

    
1645
    @errors.generic.all
1646
    @errors.pithos.connection
1647
    @errors.pithos.container
1648
    @errors.pithos.object_path
1649
    def _run(self):
1650
        self.client.del_object_sharing(self.path)
1651

    
1652
    def main(self, container___path):
1653
        super(self.__class__, self)._run(
1654
            container___path,
1655
            path_is_optional=False)
1656
        self._run()
1657

    
1658

    
1659
@command(pithos_cmds)
1660
class file_info(_file_container_command):
1661
    """Get detailed information for user account, containers or objects
1662
    to get account info:    /file info
1663
    to get container info:  /file info <container>
1664
    to get object info:     /file info <container>:<path>
1665
    """
1666

    
1667
    arguments = dict(
1668
        object_version=ValueArgument(
1669
            'show specific version \ (applies only for objects)',
1670
            ('-j', '--object-version'))
1671
    )
1672

    
1673
    @errors.generic.all
1674
    @errors.pithos.connection
1675
    @errors.pithos.container
1676
    @errors.pithos.object_path
1677
    def _run(self):
1678
        if self.container is None:
1679
            r = self.client.get_account_info()
1680
        elif self.path is None:
1681
            r = self.client.get_container_info(self.container)
1682
        else:
1683
            r = self.client.get_object_info(
1684
                self.path,
1685
                version=self['object_version'])
1686
        print_dict(r)
1687

    
1688
    def main(self, container____path__=None):
1689
        super(self.__class__, self)._run(container____path__)
1690
        self._run()
1691

    
1692

    
1693
@command(pithos_cmds)
1694
class file_meta(_file_container_command):
1695
    """Get metadata for account, containers or objects"""
1696

    
1697
    arguments = dict(
1698
        detail=FlagArgument('show detailed output', ('-l', '--details')),
1699
        until=DateArgument('show metadata until then', '--until'),
1700
        object_version=ValueArgument(
1701
            'show specific version \ (applies only for objects)',
1702
            ('-j', '--object-version'))
1703
    )
1704

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

    
1744
    def main(self, container____path__=None):
1745
        super(self.__class__, self)._run(container____path__)
1746
        self._run()
1747

    
1748

    
1749
@command(pithos_cmds)
1750
class file_setmeta(_file_container_command):
1751
    """Set a piece of metadata for account, container or object
1752
    Metadata are formed as key:value pairs
1753
    """
1754

    
1755
    @errors.generic.all
1756
    @errors.pithos.connection
1757
    @errors.pithos.container
1758
    @errors.pithos.object_path
1759
    def _run(self, metakey, metaval):
1760
        if not self.container:
1761
            self.client.set_account_meta({metakey: metaval})
1762
        elif not self.path:
1763
            self.client.set_container_meta({metakey: metaval})
1764
        else:
1765
            self.client.set_object_meta(self.path, {metakey: metaval})
1766

    
1767
    def main(self, metakey, metaval, container____path__=None):
1768
        super(self.__class__, self)._run(container____path__)
1769
        self._run(metakey=metakey, metaval=metaval)
1770

    
1771

    
1772
@command(pithos_cmds)
1773
class file_delmeta(_file_container_command):
1774
    """Delete metadata with given key from account, container or object
1775
    Metadata are formed as key:value objects
1776
    - to get metadata of current account:     /file meta
1777
    - to get metadata of a container:         /file meta <container>
1778
    - to get metadata of an object:           /file meta <container>:<path>
1779
    """
1780

    
1781
    @errors.generic.all
1782
    @errors.pithos.connection
1783
    @errors.pithos.container
1784
    @errors.pithos.object_path
1785
    def _run(self, metakey):
1786
        if self.container is None:
1787
            self.client.del_account_meta(metakey)
1788
        elif self.path is None:
1789
            self.client.del_container_meta(metakey)
1790
        else:
1791
            self.client.del_object_meta(self.path, metakey)
1792

    
1793
    def main(self, metakey, container____path__=None):
1794
        super(self.__class__, self)._run(container____path__)
1795
        self._run(metakey)
1796

    
1797

    
1798
@command(pithos_cmds)
1799
class file_quota(_file_account_command):
1800
    """Get account quota"""
1801

    
1802
    arguments = dict(
1803
        in_bytes=FlagArgument('Show result in bytes', ('-b', '--bytes'))
1804
    )
1805

    
1806
    @errors.generic.all
1807
    @errors.pithos.connection
1808
    def _run(self):
1809
        reply = self.client.get_account_quota()
1810
        if not self['in_bytes']:
1811
            for k in reply:
1812
                reply[k] = format_size(reply[k])
1813
        print_dict(pretty_keys(reply, '-'))
1814

    
1815
    def main(self, custom_uuid=None):
1816
        super(self.__class__, self)._run(custom_account=custom_uuid)
1817
        self._run()
1818

    
1819

    
1820
@command(pithos_cmds)
1821
class file_containerlimit(_pithos_init):
1822
    """Container size limit commands"""
1823

    
1824

    
1825
@command(pithos_cmds)
1826
class file_containerlimit_get(_file_container_command):
1827
    """Get container size limit"""
1828

    
1829
    arguments = dict(
1830
        in_bytes=FlagArgument('Show result in bytes', ('-b', '--bytes'))
1831
    )
1832

    
1833
    @errors.generic.all
1834
    @errors.pithos.container
1835
    def _run(self):
1836
        reply = self.client.get_container_limit(self.container)
1837
        if not self['in_bytes']:
1838
            for k in reply:
1839
                reply[k] = format_size(reply[k])
1840
        print_dict(pretty_keys(reply, '-'))
1841

    
1842
    def main(self, container=None):
1843
        super(self.__class__, self)._run()
1844
        self.container = container
1845
        self._run()
1846

    
1847

    
1848
@command(pithos_cmds)
1849
class file_containerlimit_set(_file_account_command):
1850
    """Set new storage limit for a container
1851
    By default, the limit is set in bytes
1852
    Users may specify a different unit, e.g:
1853
    /file containerlimit set 2.3GB mycontainer
1854
    Valid units: B, KiB (1024 B), KB (1000 B), MiB, MB, GiB, GB, TiB, TB
1855
    """
1856

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

    
1881
    @errors.generic.all
1882
    @errors.pithos.connection
1883
    @errors.pithos.container
1884
    def _run(self, limit):
1885
        if self.container:
1886
            self.client.container = self.container
1887
        self.client.set_container_limit(limit)
1888

    
1889
    def main(self, limit, container=None):
1890
        super(self.__class__, self)._run()
1891
        limit = self._calculate_limit(limit)
1892
        self.container = container
1893
        self._run(limit)
1894

    
1895

    
1896
@command(pithos_cmds)
1897
class file_versioning(_file_account_command):
1898
    """Get  versioning for account or container"""
1899

    
1900
    @errors.generic.all
1901
    @errors.pithos.connection
1902
    @errors.pithos.container
1903
    def _run(self):
1904
        if self.container:
1905
            r = self.client.get_container_versioning(self.container)
1906
        else:
1907
            r = self.client.get_account_versioning()
1908
        print_dict(r)
1909

    
1910
    def main(self, container=None):
1911
        super(self.__class__, self)._run()
1912
        self.container = container
1913
        self._run()
1914

    
1915

    
1916
@command(pithos_cmds)
1917
class file_setversioning(_file_account_command):
1918
    """Set versioning mode (auto, none) for account or container"""
1919

    
1920
    def _check_versioning(self, versioning):
1921
        if versioning and versioning.lower() in ('auto', 'none'):
1922
            return versioning.lower()
1923
        raiseCLIError('Invalid versioning %s' % versioning, details=[
1924
            'Versioning can be auto or none'])
1925

    
1926
    @errors.generic.all
1927
    @errors.pithos.connection
1928
    @errors.pithos.container
1929
    def _run(self, versioning):
1930
        if self.container:
1931
            self.client.container = self.container
1932
            self.client.set_container_versioning(versioning)
1933
        else:
1934
            self.client.set_account_versioning(versioning)
1935

    
1936
    def main(self, versioning, container=None):
1937
        super(self.__class__, self)._run()
1938
        self._run(self._check_versioning(versioning))
1939

    
1940

    
1941
@command(pithos_cmds)
1942
class file_group(_file_account_command):
1943
    """Get groups and group members"""
1944

    
1945
    @errors.generic.all
1946
    @errors.pithos.connection
1947
    def _run(self):
1948
        r = self.client.get_account_group()
1949
        print_dict(pretty_keys(r, '-'))
1950

    
1951
    def main(self):
1952
        super(self.__class__, self)._run()
1953
        self._run()
1954

    
1955

    
1956
@command(pithos_cmds)
1957
class file_setgroup(_file_account_command):
1958
    """Set a user group"""
1959

    
1960
    @errors.generic.all
1961
    @errors.pithos.connection
1962
    def _run(self, groupname, *users):
1963
        self.client.set_account_group(groupname, users)
1964

    
1965
    def main(self, groupname, *users):
1966
        super(self.__class__, self)._run()
1967
        if users:
1968
            self._run(groupname, *users)
1969
        else:
1970
            raiseCLIError('No users to add in group %s' % groupname)
1971

    
1972

    
1973
@command(pithos_cmds)
1974
class file_delgroup(_file_account_command):
1975
    """Delete a user group"""
1976

    
1977
    @errors.generic.all
1978
    @errors.pithos.connection
1979
    def _run(self, groupname):
1980
        self.client.del_account_group(groupname)
1981

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

    
1986

    
1987
@command(pithos_cmds)
1988
class file_sharers(_file_account_command):
1989
    """List the accounts that share objects with current user"""
1990

    
1991
    arguments = dict(
1992
        detail=FlagArgument('show detailed output', ('-l', '--details')),
1993
        marker=ValueArgument('show output greater then marker', '--marker')
1994
    )
1995

    
1996
    @errors.generic.all
1997
    @errors.pithos.connection
1998
    def _run(self):
1999
        accounts = self.client.get_sharing_accounts(marker=self['marker'])
2000
        if self['detail']:
2001
            print_items(accounts)
2002
        else:
2003
            print_items([acc['name'] for acc in accounts])
2004

    
2005
    def main(self):
2006
        super(self.__class__, self)._run()
2007
        self._run()
2008

    
2009

    
2010
@command(pithos_cmds)
2011
class file_versions(_file_container_command):
2012
    """Get the list of object versions
2013
    Deleted objects may still have versions that can be used to restore it and
2014
    get information about its previous state.
2015
    The version number can be used in a number of other commands, like info,
2016
    copy, move, meta. See these commands for more information, e.g.
2017
    /file info -h
2018
    """
2019

    
2020
    @errors.generic.all
2021
    @errors.pithos.connection
2022
    @errors.pithos.container
2023
    @errors.pithos.object_path
2024
    def _run(self):
2025
        versions = self.client.get_object_versionlist(self.path)
2026
        print_items([dict(id=vitem[0], created=strftime(
2027
            '%d-%m-%Y %H:%M:%S',
2028
            localtime(float(vitem[1])))) for vitem in versions])
2029

    
2030
    def main(self, container___path):
2031
        super(file_versions, self)._run(
2032
            container___path,
2033
            path_is_optional=False)
2034
        self._run()