Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / pithos.py @ 326a79b9

History | View | Annotate | Download (69.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 logging import getLogger
37
from os import path, makedirs
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,
44
    to_bytes,
45
    print_dict,
46
    print_items,
47
    pretty_keys,
48
    page_hold,
49
    bold,
50
    ask_user)
51
from kamaki.cli.argument import FlagArgument, ValueArgument, IntArgument
52
from kamaki.cli.argument import KeyValueArgument, DateArgument
53
from kamaki.cli.argument import ProgressBarArgument
54
from kamaki.cli.commands import _command_init, errors
55
from kamaki.clients.pithos import PithosClient, ClientError
56
from kamaki.clients.astakos import AstakosClient
57

    
58

    
59
kloger = getLogger('kamaki')
60

    
61
pithos_cmds = CommandTree('file', 'Pithos+/Storage API commands')
62
_commands = [pithos_cmds]
63

    
64

    
65
# Argument functionality
66

    
67
class DelimiterArgument(ValueArgument):
68
    """
69
    :value type: string
70
    :value returns: given string or /
71
    """
72

    
73
    def __init__(self, caller_obj, help='', parsed_name=None, default=None):
74
        super(DelimiterArgument, self).__init__(help, parsed_name, default)
75
        self.caller_obj = caller_obj
76

    
77
    @property
78
    def value(self):
79
        if self.caller_obj['recursive']:
80
            return '/'
81
        return getattr(self, '_value', self.default)
82

    
83
    @value.setter
84
    def value(self, newvalue):
85
        self._value = newvalue
86

    
87

    
88
class SharingArgument(ValueArgument):
89
    """Set sharing (read and/or write) groups
90
    .
91
    :value type: "read=term1,term2,... write=term1,term2,..."
92
    .
93
    :value returns: {'read':['term1', 'term2', ...],
94
    .   'write':['term1', 'term2', ...]}
95
    """
96

    
97
    @property
98
    def value(self):
99
        return getattr(self, '_value', self.default)
100

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

    
129

    
130
class RangeArgument(ValueArgument):
131
    """
132
    :value type: string of the form <start>-<end> where <start> and <end> are
133
        integers
134
    :value returns: the input string, after type checking <start> and <end>
135
    """
136

    
137
    @property
138
    def value(self):
139
        return getattr(self, '_value', self.default)
140

    
141
    @value.setter
142
    def value(self, newvalue):
143
        if newvalue is None:
144
            self._value = self.default
145
            return
146
        (start, end) = newvalue.split('-')
147
        (start, end) = (int(start), int(end))
148
        self._value = '%s-%s' % (start, end)
149

    
150
# Command specs
151

    
152

    
153
class _pithos_init(_command_init):
154
    """Initialize a pithos+ kamaki client"""
155

    
156
    @staticmethod
157
    def _is_dir(remote_dict):
158
        return 'application/directory' == remote_dict.get(
159
            'content_type',
160
            remote_dict.get('content-type', ''))
161

    
162
    @errors.generic.all
163
    def _run(self):
164
        self.token = self.config.get('file', 'token')\
165
            or self.config.get('global', 'token')
166
        self.base_url = self.config.get('file', 'url')\
167
            or self.config.get('global', 'url')
168
        self._set_account()
169
        self.container = self.config.get('file', 'container')\
170
            or self.config.get('global', 'container')
171
        self.client = PithosClient(
172
            base_url=self.base_url,
173
            token=self.token,
174
            account=self.account,
175
            container=self.container)
176
        self._set_log_params()
177
        self._update_max_threads()
178

    
179
    def main(self):
180
        self._run()
181

    
182
    def _set_account(self):
183
        user = AstakosClient(self.config.get('user', 'url'), self.token)
184
        self.account = self['account'] or user.term('uuid')
185

    
186
        """Backwards compatibility"""
187
        self.account = self.account\
188
            or self.config.get('file', 'account')\
189
            or self.config.get('global', 'account')
190

    
191

    
192
class _file_account_command(_pithos_init):
193
    """Base class for account level storage commands"""
194

    
195
    def __init__(self, arguments={}):
196
        super(_file_account_command, self).__init__(arguments)
197
        self['account'] = ValueArgument(
198
            'Set user account (not permanent)',
199
            ('-A', '--account'))
200

    
201
    def _run(self):
202
        super(_file_account_command, self)._run()
203
        if self['account']:
204
            self.client.account = self['account']
205

    
206
    @errors.generic.all
207
    def main(self):
208
        self._run()
209

    
210

    
211
class _file_container_command(_file_account_command):
212
    """Base class for container level storage commands"""
213

    
214
    container = None
215
    path = None
216

    
217
    def __init__(self, arguments={}):
218
        super(_file_container_command, self).__init__(arguments)
219
        self['container'] = ValueArgument(
220
            'Set container to work with (temporary)',
221
            ('-C', '--container'))
222

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

    
243
        user_cont, sep, userpath = container_with_path.partition(':')
244

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

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

    
296
    def main(self, container_with_path=None, path_is_optional=True):
297
        self._run(container_with_path, path_is_optional)
298

    
299

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

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

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

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

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

    
424
    def main(self, container____path__=None):
425
        super(self.__class__, self)._run(container____path__)
426
        self._run()
427

    
428

    
429
@command(pithos_cmds)
430
class file_mkdir(_file_container_command):
431
    """Create a directory"""
432

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

    
440
    @errors.generic.all
441
    @errors.pithos.connection
442
    @errors.pithos.container
443
    def _run(self):
444
        self.client.create_directory(self.path)
445

    
446
    def main(self, container___directory):
447
        super(self.__class__, self)._run(
448
            container___directory,
449
            path_is_optional=False)
450
        self._run()
451

    
452

    
453
@command(pithos_cmds)
454
class file_touch(_file_container_command):
455
    """Create an empty object (file)
456
    If object exists, this command will reset it to 0 length
457
    """
458

    
459
    arguments = dict(
460
        content_type=ValueArgument(
461
            'Set content type (default: application/octet-stream)',
462
            '--content-type',
463
            default='application/octet-stream')
464
    )
465

    
466
    @errors.generic.all
467
    @errors.pithos.connection
468
    @errors.pithos.container
469
    def _run(self):
470
        self.client.create_object(self.path, self['content_type'])
471

    
472
    def main(self, container___path):
473
        super(file_touch, self)._run(
474
            container___path,
475
            path_is_optional=False)
476
        self._run()
477

    
478

    
479
@command(pithos_cmds)
480
class file_create(_file_container_command):
481
    """Create a container"""
482

    
483
    arguments = dict(
484
        versioning=ValueArgument(
485
            'set container versioning (auto/none)',
486
            '--versioning'),
487
        quota=IntArgument('set default container quota', '--quota'),
488
        meta=KeyValueArgument(
489
            'set container metadata (can be repeated)',
490
            '--meta')
491
    )
492

    
493
    @errors.generic.all
494
    @errors.pithos.connection
495
    @errors.pithos.container
496
    def _run(self):
497
        self.client.container_put(
498
            quota=self['quota'],
499
            versioning=self['versioning'],
500
            metadata=self['meta'])
501

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

    
510

    
511
class _source_destination_command(_file_container_command):
512

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

    
524
    def __init__(self, arguments={}):
525
        self.arguments.update(arguments)
526
        super(_source_destination_command, self).__init__(self.arguments)
527

    
528
    def _run(self, source_container___path, path_is_optional=False):
529
        super(_source_destination_command, self)._run(
530
            source_container___path,
531
            path_is_optional)
532
        self.dst_client = PithosClient(
533
            base_url=self.client.base_url,
534
            token=self.client.token,
535
            account=self['destination_account'] or self.client.account)
536

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

    
560
    def _get_all(self, prefix):
561
        return self.client.container_get(prefix=prefix).json
562

    
563
    def _get_src_objects(self, src_path):
564
        """Get a list of the source objects to be called
565

566
        :param src_path: (str) source path
567

568
        :returns: (method, params) a method that returns a list when called
569
        or (object) if it is a single object
570
        """
571
        if src_path and src_path[-1] == '/':
572
            src_path = src_path[:-1]
573

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

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

    
600
    def src_dst_pairs(self, dst_path):
601
        src_iter = self._get_src_objects(self.path)
602
        src_N = isinstance(src_iter, tuple)
603
        add_prefix = self['add_prefix'].strip('/')
604

    
605
        if dst_path and dst_path.endswith('/'):
606
            dst_path = dst_path[:-1]
607

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

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

    
646
    def _get_new_object(self, obj, add_prefix):
647
        if self['prefix_replace'] and obj.startswith(self['prefix_replace']):
648
            obj = obj[len(self['prefix_replace']):]
649
        if self['suffix_replace'] and obj.endswith(self['suffix_replace']):
650
            obj = obj[:-len(self['suffix_replace'])]
651
        return add_prefix + obj + self['add_suffix']
652

    
653

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

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

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

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

    
744

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

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

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

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

    
836

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

    
845
    arguments = dict(
846
        progress_bar=ProgressBarArgument(
847
            'do not show progress bar',
848
            ('-N', '--no-progress-bar'),
849
            default=False)
850
    )
851

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

    
867
    def main(self, local_path, container___path):
868
        super(self.__class__, self)._run(
869
            container___path,
870
            path_is_optional=False)
871
        self._run(local_path)
872

    
873

    
874
@command(pithos_cmds)
875
class file_truncate(_file_container_command):
876
    """Truncate remote file up to a size (default is 0)"""
877

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

    
886
    def main(self, container___path, size=0):
887
        super(self.__class__, self)._run(container___path)
888
        self._run(size=size)
889

    
890

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

    
901
    arguments = dict(
902
        progress_bar=ProgressBarArgument(
903
            'do not show progress bar',
904
            ('-N', '--no-progress-bar'),
905
            default=False)
906
    )
907

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

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

    
938
    def main(self, local_path, container___path, start, end):
939
        super(self.__class__, self)._run(
940
            container___path,
941
            path_is_optional=None)
942
        self.path = self.path or path.basename(local_path)
943
        self._run(local_path=local_path, start=start, end=end)
944

    
945

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

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

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

    
992
    def main(self, container___path):
993
        super(self.__class__, self)._run(
994
            container___path,
995
            path_is_optional=False)
996
        self.run()
997

    
998

    
999
@command(pithos_cmds)
1000
class file_upload(_file_container_command):
1001
    """Upload a file"""
1002

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

    
1030
    def _remote_path(self, remote_path, local_path=''):
1031
        if self['overwrite']:
1032
            return remote_path
1033
        try:
1034
            r = self.client.get_object_info(remote_path)
1035
        except ClientError as ce:
1036
            if ce.status == 404:
1037
                return remote_path
1038
            raise ce
1039
        ctype = r.get('content-type', '')
1040
        if 'application/directory' == ctype.lower():
1041
            ret = '%s/%s' % (remote_path, local_path)
1042
            return self._remote_path(ret) if local_path else ret
1043
        raiseCLIError(
1044
            'Object %s already exists' % remote_path,
1045
            importance=1,
1046
            details=['use -f to overwrite or resume'])
1047

    
1048
    @errors.generic.all
1049
    @errors.pithos.connection
1050
    @errors.pithos.container
1051
    @errors.pithos.object_path
1052
    @errors.pithos.local_path
1053
    def _run(self, local_path, remote_path):
1054
        poolsize = self['poolsize']
1055
        if poolsize > 0:
1056
            self.client.MAX_THREADS = int(poolsize)
1057
        params = dict(
1058
            content_encoding=self['content_encoding'],
1059
            content_type=self['content_type'],
1060
            content_disposition=self['content_disposition'],
1061
            sharing=self['sharing'],
1062
            public=self['public'])
1063
        remote_path = self._remote_path(remote_path, local_path)
1064
        with open(path.abspath(local_path), 'rb') as f:
1065
            if self['unchunked']:
1066
                self.client.upload_object_unchunked(
1067
                    remote_path,
1068
                    f,
1069
                    etag=self['etag'],
1070
                    withHashFile=self['use_hashes'],
1071
                    **params)
1072
            else:
1073
                try:
1074
                    (progress_bar, upload_cb) = self._safe_progress_bar(
1075
                        'Uploading')
1076
                    if progress_bar:
1077
                        hash_bar = progress_bar.clone()
1078
                        hash_cb = hash_bar.get_generator(
1079
                            'Calculating block hashes')
1080
                    else:
1081
                        hash_cb = None
1082
                    self.client.upload_object(
1083
                        remote_path,
1084
                        f,
1085
                        hash_cb=hash_cb,
1086
                        upload_cb=upload_cb,
1087
                        **params)
1088
                except Exception:
1089
                    self._safe_progress_bar_finish(progress_bar)
1090
                    raise
1091
                finally:
1092
                    self._safe_progress_bar_finish(progress_bar)
1093
        print 'Upload completed'
1094

    
1095
    def main(self, local_path, container____path__=None):
1096
        super(self.__class__, self)._run(container____path__)
1097
        remote_path = self.path or path.basename(local_path)
1098
        self._run(local_path=local_path, remote_path=remote_path)
1099

    
1100

    
1101
@command(pithos_cmds)
1102
class file_cat(_file_container_command):
1103
    """Print remote file contents to console"""
1104

    
1105
    arguments = dict(
1106
        range=RangeArgument('show range of data', '--range'),
1107
        if_match=ValueArgument('show output if ETags match', '--if-match'),
1108
        if_none_match=ValueArgument(
1109
            'show output if ETags match',
1110
            '--if-none-match'),
1111
        if_modified_since=DateArgument(
1112
            'show output modified since then',
1113
            '--if-modified-since'),
1114
        if_unmodified_since=DateArgument(
1115
            'show output unmodified since then',
1116
            '--if-unmodified-since'),
1117
        object_version=ValueArgument(
1118
            'get the specific version',
1119
            ('-j', '--object-version'))
1120
    )
1121

    
1122
    @errors.generic.all
1123
    @errors.pithos.connection
1124
    @errors.pithos.container
1125
    @errors.pithos.object_path
1126
    def _run(self):
1127
        self.client.download_object(
1128
            self.path,
1129
            stdout,
1130
            range_str=self['range'],
1131
            version=self['object_version'],
1132
            if_match=self['if_match'],
1133
            if_none_match=self['if_none_match'],
1134
            if_modified_since=self['if_modified_since'],
1135
            if_unmodified_since=self['if_unmodified_since'])
1136

    
1137
    def main(self, container___path):
1138
        super(self.__class__, self)._run(
1139
            container___path,
1140
            path_is_optional=False)
1141
        self._run()
1142

    
1143

    
1144
@command(pithos_cmds)
1145
class file_download(_file_container_command):
1146
    """Download remote object as local file
1147
    If local destination is a directory:
1148
    *   download <container>:<path> <local dir> -R
1149
    will download all files on <container> prefixed as <path>,
1150
    to <local dir>/<full path>
1151
    *   download <container>:<path> <local dir> --exact-match
1152
    will download only one file, exactly matching <path>
1153
    ATTENTION: to download cont:dir1/dir2/file there must exist objects
1154
    cont:dir1 and cont:dir1/dir2 of type application/directory
1155
    To create directory objects, use /file mkdir
1156
    """
1157

    
1158
    arguments = dict(
1159
        resume=FlagArgument('Resume instead of overwrite', ('-r', '--resume')),
1160
        range=RangeArgument('show range of data', '--range'),
1161
        if_match=ValueArgument('show output if ETags match', '--if-match'),
1162
        if_none_match=ValueArgument(
1163
            'show output if ETags match',
1164
            '--if-none-match'),
1165
        if_modified_since=DateArgument(
1166
            'show output modified since then',
1167
            '--if-modified-since'),
1168
        if_unmodified_since=DateArgument(
1169
            'show output unmodified since then',
1170
            '--if-unmodified-since'),
1171
        object_version=ValueArgument(
1172
            'get the specific version',
1173
            ('-j', '--object-version')),
1174
        poolsize=IntArgument('set pool size', '--with-pool-size'),
1175
        progress_bar=ProgressBarArgument(
1176
            'do not show progress bar',
1177
            ('-N', '--no-progress-bar'),
1178
            default=False),
1179
        recursive=FlagArgument(
1180
            'Download a remote path and all its contents',
1181
            ('-R', '--recursive'))
1182
    )
1183

    
1184
    def _outputs(self, local_path):
1185
        """:returns: (local_file, remote_path)"""
1186
        remotes = []
1187
        if self['recursive']:
1188
            r = self.client.container_get(
1189
                prefix=self.path or '/',
1190
                if_modified_since=self['if_modified_since'],
1191
                if_unmodified_since=self['if_unmodified_since'])
1192
            dirlist = dict()
1193
            for remote in r.json:
1194
                rname = remote['name'].strip('/')
1195
                tmppath = ''
1196
                for newdir in rname.strip('/').split('/')[:-1]:
1197
                    tmppath = '/'.join([tmppath, newdir])
1198
                    dirlist.update({tmppath.strip('/'): True})
1199
                remotes.append((rname, file_download._is_dir(remote)))
1200
            dir_remotes = [r[0] for r in remotes if r[1]]
1201
            if not set(dirlist).issubset(dir_remotes):
1202
                badguys = [bg.strip('/') for bg in set(
1203
                    dirlist).difference(dir_remotes)]
1204
                raiseCLIError(
1205
                    'Some remote paths contain non existing directories',
1206
                    details=['Missing remote directories:'] + badguys)
1207
        elif self.path:
1208
            r = self.client.get_object_info(
1209
                self.path,
1210
                version=self['object_version'])
1211
            if file_download._is_dir(r):
1212
                raiseCLIError(
1213
                    'Illegal download: Remote object %s is a directory' % (
1214
                        self.path),
1215
                    details=['To download a directory, try --recursive'])
1216
            if '/' in self.path.strip('/') and not local_path:
1217
                raiseCLIError(
1218
                    'Illegal download: remote object %s contains "/"' % (
1219
                        self.path),
1220
                    details=[
1221
                        'To download an object containing "/" characters',
1222
                        'either create the remote directories or',
1223
                        'specify a non-directory local path for this object'])
1224
            remotes = [(self.path, False)]
1225
        if not remotes:
1226
            if self.path:
1227
                raiseCLIError(
1228
                    'No matching path %s on container %s' % (
1229
                        self.path,
1230
                        self.container),
1231
                    details=[
1232
                        'To list the contents of %s, try:' % self.container,
1233
                        '   /file list %s' % self.container])
1234
            raiseCLIError(
1235
                'Illegal download of container %s' % self.container,
1236
                details=[
1237
                    'To download a whole container, try:',
1238
                    '   /file download --recursive <container>'])
1239

    
1240
        lprefix = path.abspath(local_path or path.curdir)
1241
        if path.isdir(lprefix):
1242
            for rpath, remote_is_dir in remotes:
1243
                lpath = '/%s/%s' % (lprefix.strip('/'), rpath.strip('/'))
1244
                if remote_is_dir:
1245
                    if path.exists(lpath) and path.isdir(lpath):
1246
                        continue
1247
                    makedirs(lpath)
1248
                elif path.exists(lpath):
1249
                    if not self['resume']:
1250
                        print('File %s exists, aborting...' % lpath)
1251
                        continue
1252
                    with open(lpath, 'rwb+') as f:
1253
                        yield (f, rpath)
1254
                else:
1255
                    with open(lpath, 'wb+') as f:
1256
                        yield (f, rpath)
1257
        elif path.exists(lprefix):
1258
            if len(remotes) > 1:
1259
                raiseCLIError(
1260
                    '%s remote objects cannot be merged in local file %s' % (
1261
                        len(remotes),
1262
                        local_path),
1263
                    details=[
1264
                        'To download multiple objects, local path should be',
1265
                        'a directory, or use download without a local path'])
1266
            (rpath, remote_is_dir) = remotes[0]
1267
            if remote_is_dir:
1268
                raiseCLIError(
1269
                    'Remote directory %s should not replace local file %s' % (
1270
                        rpath,
1271
                        local_path))
1272
            if self['resume']:
1273
                with open(lprefix, 'rwb+') as f:
1274
                    yield (f, rpath)
1275
            else:
1276
                raiseCLIError(
1277
                    'Local file %s already exist' % local_path,
1278
                    details=['Try --resume to overwrite it'])
1279
        else:
1280
            if len(remotes) > 1 or remotes[0][1]:
1281
                raiseCLIError(
1282
                    'Local directory %s does not exist' % local_path)
1283
            with open(lprefix, 'wb+') as f:
1284
                yield (f, remotes[0][0])
1285

    
1286
    @errors.generic.all
1287
    @errors.pithos.connection
1288
    @errors.pithos.container
1289
    @errors.pithos.object_path
1290
    @errors.pithos.local_path
1291
    def _run(self, local_path):
1292
        #outputs = self._outputs(local_path)
1293
        poolsize = self['poolsize']
1294
        if poolsize:
1295
            self.client.MAX_THREADS = int(poolsize)
1296
        progress_bar = None
1297
        try:
1298
            for f, rpath in self._outputs(local_path):
1299
                (
1300
                    progress_bar,
1301
                    download_cb) = self._safe_progress_bar(
1302
                        'Download %s' % rpath)
1303
                self.client.download_object(
1304
                    rpath,
1305
                    f,
1306
                    download_cb=download_cb,
1307
                    range_str=self['range'],
1308
                    version=self['object_version'],
1309
                    if_match=self['if_match'],
1310
                    resume=self['resume'],
1311
                    if_none_match=self['if_none_match'],
1312
                    if_modified_since=self['if_modified_since'],
1313
                    if_unmodified_since=self['if_unmodified_since'])
1314
        except KeyboardInterrupt:
1315
            from threading import activeCount, enumerate as activethreads
1316
            timeout = 0.5
1317
            while activeCount() > 1:
1318
                stdout.write('\nCancel %s threads: ' % (activeCount() - 1))
1319
                stdout.flush()
1320
                for thread in activethreads():
1321
                    try:
1322
                        thread.join(timeout)
1323
                        stdout.write('.' if thread.isAlive() else '*')
1324
                    except RuntimeError:
1325
                        continue
1326
                    finally:
1327
                        stdout.flush()
1328
                        timeout += 0.1
1329

    
1330
            print('\nDownload canceled by user')
1331
            if local_path is not None:
1332
                print('to resume, re-run with --resume')
1333
        except Exception:
1334
            self._safe_progress_bar_finish(progress_bar)
1335
            raise
1336
        finally:
1337
            self._safe_progress_bar_finish(progress_bar)
1338

    
1339
    def main(self, container___path, local_path=None):
1340
        super(self.__class__, self)._run(container___path)
1341
        self._run(local_path=local_path)
1342

    
1343

    
1344
@command(pithos_cmds)
1345
class file_hashmap(_file_container_command):
1346
    """Get the hash-map of an object"""
1347

    
1348
    arguments = dict(
1349
        if_match=ValueArgument('show output if ETags match', '--if-match'),
1350
        if_none_match=ValueArgument(
1351
            'show output if ETags match',
1352
            '--if-none-match'),
1353
        if_modified_since=DateArgument(
1354
            'show output modified since then',
1355
            '--if-modified-since'),
1356
        if_unmodified_since=DateArgument(
1357
            'show output unmodified since then',
1358
            '--if-unmodified-since'),
1359
        object_version=ValueArgument(
1360
            'get the specific version',
1361
            ('-j', '--object-version'))
1362
    )
1363

    
1364
    @errors.generic.all
1365
    @errors.pithos.connection
1366
    @errors.pithos.container
1367
    @errors.pithos.object_path
1368
    def _run(self):
1369
        data = self.client.get_object_hashmap(
1370
            self.path,
1371
            version=self['object_version'],
1372
            if_match=self['if_match'],
1373
            if_none_match=self['if_none_match'],
1374
            if_modified_since=self['if_modified_since'],
1375
            if_unmodified_since=self['if_unmodified_since'])
1376
        print_dict(data)
1377

    
1378
    def main(self, container___path):
1379
        super(self.__class__, self)._run(
1380
            container___path,
1381
            path_is_optional=False)
1382
        self._run()
1383

    
1384

    
1385
@command(pithos_cmds)
1386
class file_delete(_file_container_command):
1387
    """Delete a container [or an object]
1388
    How to delete a non-empty container:
1389
    - empty the container:  /file delete -R <container>
1390
    - delete it:            /file delete <container>
1391
    .
1392
    Semantics of directory deletion:
1393
    .a preserve the contents: /file delete <container>:<directory>
1394
    .    objects of the form dir/filename can exist with a dir object
1395
    .b delete contents:       /file delete -R <container>:<directory>
1396
    .    all dir/* objects are affected, even if dir does not exist
1397
    .
1398
    To restore a deleted object OBJ in a container CONT:
1399
    - get object versions: /file versions CONT:OBJ
1400
    .   and choose the version to be restored
1401
    - restore the object:  /file copy --source-version=<version> CONT:OBJ OBJ
1402
    """
1403

    
1404
    arguments = dict(
1405
        until=DateArgument('remove history until that date', '--until'),
1406
        yes=FlagArgument('Do not prompt for permission', '--yes'),
1407
        recursive=FlagArgument(
1408
            'empty dir or container and delete (if dir)',
1409
            ('-R', '--recursive'))
1410
    )
1411

    
1412
    def __init__(self, arguments={}):
1413
        super(self.__class__, self).__init__(arguments)
1414
        self['delimiter'] = DelimiterArgument(
1415
            self,
1416
            parsed_name='--delimiter',
1417
            help='delete objects prefixed with <object><delimiter>')
1418

    
1419
    @errors.generic.all
1420
    @errors.pithos.connection
1421
    @errors.pithos.container
1422
    @errors.pithos.object_path
1423
    def _run(self):
1424
        if self.path:
1425
            if self['yes'] or ask_user(
1426
                    'Delete %s:%s ?' % (self.container, self.path)):
1427
                self.client.del_object(
1428
                    self.path,
1429
                    until=self['until'],
1430
                    delimiter=self['delimiter'])
1431
            else:
1432
                print('Aborted')
1433
        else:
1434
            if self['recursive']:
1435
                ask_msg = 'Delete container contents'
1436
            else:
1437
                ask_msg = 'Delete container'
1438
            if self['yes'] or ask_user('%s %s ?' % (ask_msg, self.container)):
1439
                self.client.del_container(
1440
                    until=self['until'],
1441
                    delimiter=self['delimiter'])
1442
            else:
1443
                print('Aborted')
1444

    
1445
    def main(self, container____path__=None):
1446
        super(self.__class__, self)._run(container____path__)
1447
        self._run()
1448

    
1449

    
1450
@command(pithos_cmds)
1451
class file_purge(_file_container_command):
1452
    """Delete a container and release related data blocks
1453
    Non-empty containers can not purged.
1454
    To purge a container with content:
1455
    .   /file delete -R <container>
1456
    .      objects are deleted, but data blocks remain on server
1457
    .   /file purge <container>
1458
    .      container and data blocks are released and deleted
1459
    """
1460

    
1461
    arguments = dict(
1462
        yes=FlagArgument('Do not prompt for permission', '--yes'),
1463
    )
1464

    
1465
    @errors.generic.all
1466
    @errors.pithos.connection
1467
    @errors.pithos.container
1468
    def _run(self):
1469
        if self['yes'] or ask_user('Purge container %s?' % self.container):
1470
                self.client.purge_container()
1471
        else:
1472
            print('Aborted')
1473

    
1474
    def main(self, container=None):
1475
        super(self.__class__, self)._run(container)
1476
        if container and self.container != container:
1477
            raiseCLIError('Invalid container name %s' % container, details=[
1478
                'Did you mean "%s" ?' % self.container,
1479
                'Use --container for names containing :'])
1480
        self._run()
1481

    
1482

    
1483
@command(pithos_cmds)
1484
class file_publish(_file_container_command):
1485
    """Publish the object and print the public url"""
1486

    
1487
    @errors.generic.all
1488
    @errors.pithos.connection
1489
    @errors.pithos.container
1490
    @errors.pithos.object_path
1491
    def _run(self):
1492
        url = self.client.publish_object(self.path)
1493
        print(url)
1494

    
1495
    def main(self, container___path):
1496
        super(self.__class__, self)._run(
1497
            container___path,
1498
            path_is_optional=False)
1499
        self._run()
1500

    
1501

    
1502
@command(pithos_cmds)
1503
class file_unpublish(_file_container_command):
1504
    """Unpublish an object"""
1505

    
1506
    @errors.generic.all
1507
    @errors.pithos.connection
1508
    @errors.pithos.container
1509
    @errors.pithos.object_path
1510
    def _run(self):
1511
            self.client.unpublish_object(self.path)
1512

    
1513
    def main(self, container___path):
1514
        super(self.__class__, self)._run(
1515
            container___path,
1516
            path_is_optional=False)
1517
        self._run()
1518

    
1519

    
1520
@command(pithos_cmds)
1521
class file_permissions(_file_container_command):
1522
    """Get read and write permissions of an object
1523
    Permissions are lists of users and user groups. There is read and write
1524
    permissions. Users and groups with write permission have also read
1525
    permission.
1526
    """
1527

    
1528
    @errors.generic.all
1529
    @errors.pithos.connection
1530
    @errors.pithos.container
1531
    @errors.pithos.object_path
1532
    def _run(self):
1533
        r = self.client.get_object_sharing(self.path)
1534
        print_dict(r)
1535

    
1536
    def main(self, container___path):
1537
        super(self.__class__, self)._run(
1538
            container___path,
1539
            path_is_optional=False)
1540
        self._run()
1541

    
1542

    
1543
@command(pithos_cmds)
1544
class file_setpermissions(_file_container_command):
1545
    """Set permissions for an object
1546
    New permissions overwrite existing permissions.
1547
    Permission format:
1548
    -   read=<username>[,usergroup[,...]]
1549
    -   write=<username>[,usegroup[,...]]
1550
    E.g. to give read permissions for file F to users A and B and write for C:
1551
    .       /file setpermissions F read=A,B write=C
1552
    """
1553

    
1554
    @errors.generic.all
1555
    def format_permition_dict(self, permissions):
1556
        read = False
1557
        write = False
1558
        for perms in permissions:
1559
            splstr = perms.split('=')
1560
            if 'read' == splstr[0]:
1561
                read = [ug.strip() for ug in splstr[1].split(',')]
1562
            elif 'write' == splstr[0]:
1563
                write = [ug.strip() for ug in splstr[1].split(',')]
1564
            else:
1565
                msg = 'Usage:\tread=<groups,users> write=<groups,users>'
1566
                raiseCLIError(None, msg)
1567
        return (read, write)
1568

    
1569
    @errors.generic.all
1570
    @errors.pithos.connection
1571
    @errors.pithos.container
1572
    @errors.pithos.object_path
1573
    def _run(self, read, write):
1574
        self.client.set_object_sharing(
1575
            self.path,
1576
            read_permition=read,
1577
            write_permition=write)
1578

    
1579
    def main(self, container___path, *permissions):
1580
        super(self.__class__, self)._run(
1581
            container___path,
1582
            path_is_optional=False)
1583
        (read, write) = self.format_permition_dict(permissions)
1584
        self._run(read, write)
1585

    
1586

    
1587
@command(pithos_cmds)
1588
class file_delpermissions(_file_container_command):
1589
    """Delete all permissions set on object
1590
    To modify permissions, use /file setpermssions
1591
    """
1592

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

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

    
1606

    
1607
@command(pithos_cmds)
1608
class file_info(_file_container_command):
1609
    """Get detailed information for user account, containers or objects
1610
    to get account info:    /file info
1611
    to get container info:  /file info <container>
1612
    to get object info:     /file info <container>:<path>
1613
    """
1614

    
1615
    arguments = dict(
1616
        object_version=ValueArgument(
1617
            'show specific version \ (applies only for objects)',
1618
            ('-j', '--object-version'))
1619
    )
1620

    
1621
    @errors.generic.all
1622
    @errors.pithos.connection
1623
    @errors.pithos.container
1624
    @errors.pithos.object_path
1625
    def _run(self):
1626
        if self.container is None:
1627
            r = self.client.get_account_info()
1628
        elif self.path is None:
1629
            r = self.client.get_container_info(self.container)
1630
        else:
1631
            r = self.client.get_object_info(
1632
                self.path,
1633
                version=self['object_version'])
1634
        print_dict(r)
1635

    
1636
    def main(self, container____path__=None):
1637
        super(self.__class__, self)._run(container____path__)
1638
        self._run()
1639

    
1640

    
1641
@command(pithos_cmds)
1642
class file_meta(_file_container_command):
1643
    """Get metadata for account, containers or objects"""
1644

    
1645
    arguments = dict(
1646
        detail=FlagArgument('show detailed output', ('-l', '--details')),
1647
        until=DateArgument('show metadata until then', '--until'),
1648
        object_version=ValueArgument(
1649
            'show specific version \ (applies only for objects)',
1650
            ('-j', '--object-version'))
1651
    )
1652

    
1653
    @errors.generic.all
1654
    @errors.pithos.connection
1655
    @errors.pithos.container
1656
    @errors.pithos.object_path
1657
    def _run(self):
1658
        until = self['until']
1659
        if self.container is None:
1660
            if self['detail']:
1661
                r = self.client.get_account_info(until=until)
1662
            else:
1663
                r = self.client.get_account_meta(until=until)
1664
                r = pretty_keys(r, '-')
1665
            if r:
1666
                print(bold(self.client.account))
1667
        elif self.path is None:
1668
            if self['detail']:
1669
                r = self.client.get_container_info(until=until)
1670
            else:
1671
                cmeta = self.client.get_container_meta(until=until)
1672
                ometa = self.client.get_container_object_meta(until=until)
1673
                r = {}
1674
                if cmeta:
1675
                    r['container-meta'] = pretty_keys(cmeta, '-')
1676
                if ometa:
1677
                    r['object-meta'] = pretty_keys(ometa, '-')
1678
        else:
1679
            if self['detail']:
1680
                r = self.client.get_object_info(
1681
                    self.path,
1682
                    version=self['object_version'])
1683
            else:
1684
                r = self.client.get_object_meta(
1685
                    self.path,
1686
                    version=self['object_version'])
1687
            if r:
1688
                r = pretty_keys(pretty_keys(r, '-'))
1689
        if r:
1690
            print_dict(r)
1691

    
1692
    def main(self, container____path__=None):
1693
        super(self.__class__, self)._run(container____path__)
1694
        self._run()
1695

    
1696

    
1697
@command(pithos_cmds)
1698
class file_setmeta(_file_container_command):
1699
    """Set a piece of metadata for account, container or object
1700
    Metadata are formed as key:value pairs
1701
    """
1702

    
1703
    @errors.generic.all
1704
    @errors.pithos.connection
1705
    @errors.pithos.container
1706
    @errors.pithos.object_path
1707
    def _run(self, metakey, metaval):
1708
        if not self.container:
1709
            self.client.set_account_meta({metakey: metaval})
1710
        elif not self.path:
1711
            self.client.set_container_meta({metakey: metaval})
1712
        else:
1713
            self.client.set_object_meta(self.path, {metakey: metaval})
1714

    
1715
    def main(self, metakey, metaval, container____path__=None):
1716
        super(self.__class__, self)._run(container____path__)
1717
        self._run(metakey=metakey, metaval=metaval)
1718

    
1719

    
1720
@command(pithos_cmds)
1721
class file_delmeta(_file_container_command):
1722
    """Delete metadata with given key from account, container or object
1723
    Metadata are formed as key:value objects
1724
    - to get metadata of current account:     /file meta
1725
    - to get metadata of a container:         /file meta <container>
1726
    - to get metadata of an object:           /file meta <container>:<path>
1727
    """
1728

    
1729
    @errors.generic.all
1730
    @errors.pithos.connection
1731
    @errors.pithos.container
1732
    @errors.pithos.object_path
1733
    def _run(self, metakey):
1734
        if self.container is None:
1735
            self.client.del_account_meta(metakey)
1736
        elif self.path is None:
1737
            self.client.del_container_meta(metakey)
1738
        else:
1739
            self.client.del_object_meta(self.path, metakey)
1740

    
1741
    def main(self, metakey, container____path__=None):
1742
        super(self.__class__, self)._run(container____path__)
1743
        self._run(metakey)
1744

    
1745

    
1746
@command(pithos_cmds)
1747
class file_quota(_file_account_command):
1748
    """Get quota for account or container"""
1749

    
1750
    arguments = dict(
1751
        in_bytes=FlagArgument('Show result in bytes', ('-b', '--bytes'))
1752
    )
1753

    
1754
    @errors.generic.all
1755
    @errors.pithos.connection
1756
    @errors.pithos.container
1757
    def _run(self):
1758
        if self.container:
1759
            reply = self.client.get_container_quota(self.container)
1760
        else:
1761
            reply = self.client.get_account_quota()
1762
        if not self['in_bytes']:
1763
            for k in reply:
1764
                reply[k] = format_size(reply[k])
1765
        print_dict(pretty_keys(reply, '-'))
1766

    
1767
    def main(self, container=None):
1768
        super(self.__class__, self)._run()
1769
        self.container = container
1770
        self._run()
1771

    
1772

    
1773
@command(pithos_cmds)
1774
class file_containerlimit(_pithos_init):
1775
    """Container size limit commands"""
1776

    
1777

    
1778
@command(pithos_cmds)
1779
class file_containerlimit_set(_file_account_command):
1780
    """Set new storage limit for a container
1781
    By default, the limit is set in bytes
1782
    Users may specify a different unit, e.g:
1783
    /file containerlimit set 2.3GB mycontainer
1784
    Valide units: B, KiB (1024 B), KB (1000 B), MiB, MB, GiB, GB, TiB, TB
1785
    """
1786

    
1787
    @errors.generic.all
1788
    def _calculate_limit(self, user_input):
1789
        limit = 0
1790
        try:
1791
            limit = int(user_input)
1792
        except ValueError:
1793
            index = 0
1794
            digits = [str(num) for num in range(0, 10)] + ['.']
1795
            while user_input[index] in digits:
1796
                index += 1
1797
            limit = user_input[:index]
1798
            format = user_input[index:]
1799
            try:
1800
                return to_bytes(limit, format)
1801
            except Exception as qe:
1802
                msg = 'Failed to convert %s to bytes' % user_input,
1803
                raiseCLIError(qe, msg, details=[
1804
                    'Syntax: containerlimit set <limit>[format] [container]',
1805
                    'e.g.: containerlimit set 2.3GB mycontainer',
1806
                    'Valid formats:',
1807
                    '(*1024): B, KiB, MiB, GiB, TiB',
1808
                    '(*1000): B, KB, MB, GB, TB'])
1809
        return limit
1810

    
1811
    @errors.generic.all
1812
    @errors.pithos.connection
1813
    @errors.pithos.container
1814
    def _run(self, limit):
1815
        if self.container:
1816
            self.client.container = self.container
1817
        self.client.set_container_limit(limit)
1818

    
1819
    def main(self, limit, container=None):
1820
        super(self.__class__, self)._run()
1821
        limit = self._calculate_limit(limit)
1822
        self.container = container
1823
        self._run(limit)
1824

    
1825

    
1826
@command(pithos_cmds)
1827
class file_versioning(_file_account_command):
1828
    """Get  versioning for account or container"""
1829

    
1830
    @errors.generic.all
1831
    @errors.pithos.connection
1832
    @errors.pithos.container
1833
    def _run(self):
1834
        if self.container:
1835
            r = self.client.get_container_versioning(self.container)
1836
        else:
1837
            r = self.client.get_account_versioning()
1838
        print_dict(r)
1839

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

    
1845

    
1846
@command(pithos_cmds)
1847
class file_setversioning(_file_account_command):
1848
    """Set versioning mode (auto, none) for account or container"""
1849

    
1850
    def _check_versioning(self, versioning):
1851
        if versioning and versioning.lower() in ('auto', 'none'):
1852
            return versioning.lower()
1853
        raiseCLIError('Invalid versioning %s' % versioning, details=[
1854
            'Versioning can be auto or none'])
1855

    
1856
    @errors.generic.all
1857
    @errors.pithos.connection
1858
    @errors.pithos.container
1859
    def _run(self, versioning):
1860
        if self.container:
1861
            self.client.container = self.container
1862
            self.client.set_container_versioning(versioning)
1863
        else:
1864
            self.client.set_account_versioning(versioning)
1865

    
1866
    def main(self, versioning, container=None):
1867
        super(self.__class__, self)._run()
1868
        self._run(self._check_versioning(versioning))
1869

    
1870

    
1871
@command(pithos_cmds)
1872
class file_group(_file_account_command):
1873
    """Get groups and group members"""
1874

    
1875
    @errors.generic.all
1876
    @errors.pithos.connection
1877
    def _run(self):
1878
        r = self.client.get_account_group()
1879
        print_dict(pretty_keys(r, '-'))
1880

    
1881
    def main(self):
1882
        super(self.__class__, self)._run()
1883
        self._run()
1884

    
1885

    
1886
@command(pithos_cmds)
1887
class file_setgroup(_file_account_command):
1888
    """Set a user group"""
1889

    
1890
    @errors.generic.all
1891
    @errors.pithos.connection
1892
    def _run(self, groupname, *users):
1893
        self.client.set_account_group(groupname, users)
1894

    
1895
    def main(self, groupname, *users):
1896
        super(self.__class__, self)._run()
1897
        if users:
1898
            self._run(groupname, *users)
1899
        else:
1900
            raiseCLIError('No users to add in group %s' % groupname)
1901

    
1902

    
1903
@command(pithos_cmds)
1904
class file_delgroup(_file_account_command):
1905
    """Delete a user group"""
1906

    
1907
    @errors.generic.all
1908
    @errors.pithos.connection
1909
    def _run(self, groupname):
1910
        self.client.del_account_group(groupname)
1911

    
1912
    def main(self, groupname):
1913
        super(self.__class__, self)._run()
1914
        self._run(groupname)
1915

    
1916

    
1917
@command(pithos_cmds)
1918
class file_sharers(_file_account_command):
1919
    """List the accounts that share objects with current user"""
1920

    
1921
    arguments = dict(
1922
        detail=FlagArgument('show detailed output', ('-l', '--details')),
1923
        marker=ValueArgument('show output greater then marker', '--marker')
1924
    )
1925

    
1926
    @errors.generic.all
1927
    @errors.pithos.connection
1928
    def _run(self):
1929
        accounts = self.client.get_sharing_accounts(marker=self['marker'])
1930
        if self['detail']:
1931
            print_items(accounts)
1932
        else:
1933
            print_items([acc['name'] for acc in accounts])
1934

    
1935
    def main(self):
1936
        super(self.__class__, self)._run()
1937
        self._run()
1938

    
1939

    
1940
@command(pithos_cmds)
1941
class file_versions(_file_container_command):
1942
    """Get the list of object versions
1943
    Deleted objects may still have versions that can be used to restore it and
1944
    get information about its previous state.
1945
    The version number can be used in a number of other commands, like info,
1946
    copy, move, meta. See these commands for more information, e.g.
1947
    /file info -h
1948
    """
1949

    
1950
    @errors.generic.all
1951
    @errors.pithos.connection
1952
    @errors.pithos.container
1953
    @errors.pithos.object_path
1954
    def _run(self):
1955
        versions = self.client.get_object_versionlist(self.path)
1956
        print_items([dict(id=vitem[0], created=strftime(
1957
            '%d-%m-%Y %H:%M:%S',
1958
            localtime(float(vitem[1])))) for vitem in versions])
1959

    
1960
    def main(self, container___path):
1961
        super(file_versions, self)._run(
1962
            container___path,
1963
            path_is_optional=False)
1964
        self._run()