Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (66.8 kB)

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

    
34
from time import localtime, strftime
35
from io import StringIO
36
from pydoc import pager
37
from os import path, walk, makedirs
38

    
39
from kamaki.clients.pithos import PithosClient, ClientError
40

    
41
from kamaki.cli import command
42
from kamaki.cli.command_tree import CommandTree
43
from kamaki.cli.commands import (
44
    _command_init, errors, addLogSettings, DontRaiseKeyError, _optional_json,
45
    _name_filter, _optional_output_cmd)
46
from kamaki.cli.errors import (
47
    CLIBaseUrlError, CLIError, CLIInvalidArgument, raiseCLIError,
48
    CLISyntaxError)
49
from kamaki.cli.argument import (
50
    FlagArgument, IntArgument, ValueArgument, DateArgument, KeyValueArgument,
51
    ProgressBarArgument, RepeatableArgument, DataSizeArgument)
52
from kamaki.cli.utils import (
53
    format_size, bold, get_path_size, guess_mime_type)
54

    
55
file_cmds = CommandTree('file', 'Pithos+/Storage object level API commands')
56
container_cmds = CommandTree(
57
    'container', 'Pithos+/Storage container level API commands')
58
sharer_cmds = CommandTree('sharer', 'Pithos+/Storage sharers')
59
group_cmds = CommandTree('group', 'Pithos+/Storage user groups')
60
_commands = [file_cmds, container_cmds, sharer_cmds, group_cmds]
61

    
62

    
63
class _pithos_init(_command_init):
64
    """Initilize a pithos+ client
65
    There is always a default account (current user uuid)
66
    There is always a default container (pithos)
67
    """
68

    
69
    @DontRaiseKeyError
70
    def _custom_container(self):
71
        return self.config.get_cloud(self.cloud, 'pithos_container')
72

    
73
    @DontRaiseKeyError
74
    def _custom_uuid(self):
75
        return self.config.get_cloud(self.cloud, 'pithos_uuid')
76

    
77
    def _set_account(self):
78
        self.account = self._custom_uuid()
79
        if self.account:
80
            return
81
        astakos = getattr(self, 'auth_base', None)
82
        if astakos:
83
            self.account = astakos.user_term('id', self.token)
84
        else:
85
            raise CLIBaseUrlError(service='astakos')
86

    
87
    @errors.generic.all
88
    @addLogSettings
89
    def _run(self):
90
        cloud = getattr(self, 'cloud', None)
91
        if cloud:
92
            self.base_url = self._custom_url('pithos')
93
        else:
94
            self.cloud = 'default'
95
        self.token = self._custom_token('pithos')
96
        self.container = self._custom_container() or 'pithos'
97

    
98
        astakos = getattr(self, 'auth_base', None)
99
        if astakos:
100
            self.token = self.token or astakos.token
101
            if not self.base_url:
102
                pithos_endpoints = astakos.get_service_endpoints(
103
                    self._custom_type('pithos') or 'object-store',
104
                    self._custom_version('pithos') or '')
105
                self.base_url = pithos_endpoints['publicURL']
106
        else:
107
            raise CLIBaseUrlError(service='astakos')
108

    
109
        self._set_account()
110
        self.client = PithosClient(
111
            self.base_url, self.token, self.account, self.container)
112

    
113
    def main(self):
114
        self._run()
115

    
116

    
117
class _pithos_account(_pithos_init):
118
    """Setup account"""
119

    
120
    def __init__(self, arguments={}, auth_base=None, cloud=None):
121
        super(_pithos_account, self).__init__(arguments, auth_base, cloud)
122
        self['account'] = ValueArgument(
123
            'Use (a different) user uuid', ('-A', '--account'))
124

    
125
    def print_objects(self, object_list):
126
        for index, obj in enumerate(object_list):
127
            pretty_obj = obj.copy()
128
            index += 1
129
            empty_space = ' ' * (len(str(len(object_list))) - len(str(index)))
130
            if 'subdir' in obj:
131
                continue
132
            if self._is_dir(obj):
133
                size = 'D'
134
            else:
135
                size = format_size(obj['bytes'])
136
                pretty_obj['bytes'] = '%s (%s)' % (obj['bytes'], size)
137
            oname = obj['name'] if self['more'] else bold(obj['name'])
138
            prfx = ('%s%s. ' % (empty_space, index)) if self['enum'] else ''
139
            if self['detail']:
140
                self.writeln('%s%s' % (prfx, oname))
141
                self.print_dict(pretty_obj, exclude=('name'))
142
                self.writeln()
143
            else:
144
                oname = '%s%9s %s' % (prfx, size, oname)
145
                oname += '/' if self._is_dir(obj) else u''
146
                self.writeln(oname)
147

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

    
153
    def _run(self):
154
        super(_pithos_account, self)._run()
155
        self.client.account = self['account'] or getattr(
156
            self, 'account', getattr(self.client, 'account', None))
157

    
158

    
159
class _pithos_container(_pithos_account):
160
    """Setup container"""
161

    
162
    def __init__(self, arguments={}, auth_base=None, cloud=None):
163
        super(_pithos_container, self).__init__(arguments, auth_base, cloud)
164
        self['container'] = ValueArgument(
165
            'Use this container (default: pithos)', ('-C', '--container'))
166

    
167
    @staticmethod
168
    def _resolve_pithos_url(url):
169
        """Match urls of one of the following formats:
170
        pithos://ACCOUNT/CONTAINER/OBJECT_PATH
171
        /CONTAINER/OBJECT_PATH
172
        return account, container, path
173
        """
174
        account, container, obj_path, prefix = '', '', url, 'pithos://'
175
        if url.startswith(prefix):
176
            account, sep, url = url[len(prefix):].partition('/')
177
            url = '/%s' % url
178
        if url.startswith('/'):
179
            container, sep, obj_path = url[1:].partition('/')
180
        return account, container, obj_path
181

    
182
    def _run(self, url=None):
183
        acc, con, self.path = self._resolve_pithos_url(url or '')
184
        #  self.account = acc or getattr(self, 'account', '')
185
        super(_pithos_container, self)._run()
186
        self.container = con or self['container'] or getattr(
187
            self, 'container', None) or getattr(self.client, 'container', '')
188
        self.client.account = acc or self.client.account
189
        self.client.container = self.container
190

    
191

    
192
@command(file_cmds)
193
class file_info(_pithos_container, _optional_json):
194
    """Get information/details about a file"""
195

    
196
    arguments = dict(
197
        object_version=ValueArgument(
198
            'download a file of a specific version', '--object-version'),
199
        hashmap=FlagArgument(
200
            'Get file hashmap instead of details', '--hashmap'),
201
        matching_etag=ValueArgument(
202
            'show output if ETags match', '--if-match'),
203
        non_matching_etag=ValueArgument(
204
            'show output if ETags DO NOT match', '--if-none-match'),
205
        modified_since_date=DateArgument(
206
            'show output modified since then', '--if-modified-since'),
207
        unmodified_since_date=DateArgument(
208
            'show output unmodified since then', '--if-unmodified-since'),
209
        sharing=FlagArgument(
210
            'show object permissions and sharing information', '--sharing'),
211
        metadata=FlagArgument('show only object metadata', '--metadata'),
212
        versions=FlagArgument(
213
            'show the list of versions for the file', '--object-versions')
214
    )
215

    
216
    def version_print(self, versions):
217
        return {'/%s/%s' % (self.container, self.path): [
218
            dict(version_id=vitem[0], created=strftime(
219
                '%d-%m-%Y %H:%M:%S',
220
                localtime(float(vitem[1])))) for vitem in versions]}
221

    
222
    @errors.generic.all
223
    @errors.pithos.connection
224
    @errors.pithos.container
225
    @errors.pithos.object_path
226
    def _run(self):
227
        if self['hashmap']:
228
            r = self.client.get_object_hashmap(
229
                self.path,
230
                version=self['object_version'],
231
                if_match=self['matching_etag'],
232
                if_none_match=self['non_matching_etag'],
233
                if_modified_since=self['modified_since_date'],
234
                if_unmodified_since=self['unmodified_since_date'])
235
        elif self['sharing']:
236
            r = self.client.get_object_sharing(self.path)
237
            r['public url'] = self.client.get_object_info(
238
                self.path, version=self['object_version']).get(
239
                    'x-object-public', None)
240
        elif self['metadata']:
241
            r, preflen = dict(), len('x-object-meta-')
242
            for k, v in self.client.get_object_meta(self.path).items():
243
                r[k[preflen:]] = v
244
        elif self['versions']:
245
            r = self.version_print(
246
                self.client.get_object_versionlist(self.path))
247
        else:
248
            r = self.client.get_object_info(
249
                self.path, version=self['object_version'])
250
        self._print(r, self.print_dict)
251

    
252
    def main(self, path_or_url):
253
        super(self.__class__, self)._run(path_or_url)
254
        self._run()
255

    
256

    
257
@command(file_cmds)
258
class file_list(_pithos_container, _optional_json, _name_filter):
259
    """List all objects in a container or a directory object"""
260

    
261
    arguments = dict(
262
        detail=FlagArgument('detailed output', ('-l', '--list')),
263
        limit=IntArgument('limit number of listed items', ('-n', '--number')),
264
        marker=ValueArgument('output greater that marker', '--marker'),
265
        delimiter=ValueArgument('show output up to delimiter', '--delimiter'),
266
        meta=ValueArgument(
267
            'show output with specified meta keys', '--meta',
268
            default=[]),
269
        if_modified_since=ValueArgument(
270
            'show output modified since then', '--if-modified-since'),
271
        if_unmodified_since=ValueArgument(
272
            'show output not modified since then', '--if-unmodified-since'),
273
        until=DateArgument('show metadata until then', '--until'),
274
        format=ValueArgument(
275
            'format to parse until data (default: d/m/Y H:M:S )', '--format'),
276
        shared=FlagArgument('show only shared', '--shared'),
277
        more=FlagArgument('read long results', '--more'),
278
        enum=FlagArgument('Enumerate results', '--enumerate'),
279
        recursive=FlagArgument(
280
            'Recursively list containers and their contents',
281
            ('-R', '--recursive'))
282
    )
283

    
284
    @errors.generic.all
285
    @errors.pithos.connection
286
    @errors.pithos.container
287
    @errors.pithos.object_path
288
    def _run(self):
289
        r = self.client.container_get(
290
            limit=False if self['more'] else self['limit'],
291
            marker=self['marker'],
292
            prefix=self['name_pref'],
293
            delimiter=self['delimiter'],
294
            path=self.path or '',
295
            if_modified_since=self['if_modified_since'],
296
            if_unmodified_since=self['if_unmodified_since'],
297
            until=self['until'],
298
            meta=self['meta'],
299
            show_only_shared=self['shared'])
300
        files = self._filter_by_name(r.json)
301
        if self['more']:
302
            outbu, self._out = self._out, StringIO()
303
        try:
304
            if self['json_output'] or self['output_format']:
305
                self._print(files)
306
            else:
307
                self.print_objects(files)
308
        finally:
309
            if self['more']:
310
                pager(self._out.getvalue())
311
                self._out = outbu
312

    
313
    def main(self, path_or_url=''):
314
        super(self.__class__, self)._run(path_or_url)
315
        self._run()
316

    
317

    
318
@command(file_cmds)
319
class file_modify(_pithos_container):
320
    """Modify the attributes of a file or directory object"""
321

    
322
    arguments = dict(
323
        publish=FlagArgument(
324
            'Make an object public (returns the public URL)', '--publish'),
325
        unpublish=FlagArgument(
326
            'Make an object unpublic', '--unpublish'),
327
        uuid_for_read_permission=RepeatableArgument(
328
            'Give read access to user/group (can be repeated, accumulative). '
329
            'Format for users: UUID . Format for groups: UUID:GROUP . '
330
            'Use * for all users/groups', '--read-permission'),
331
        uuid_for_write_permission=RepeatableArgument(
332
            'Give write access to user/group (can be repeated, accumulative). '
333
            'Format for users: UUID . Format for groups: UUID:GROUP . '
334
            'Use * for all users/groups', '--write-permission'),
335
        no_permissions=FlagArgument('Remove permissions', '--no-permissions'),
336
        metadata_to_set=KeyValueArgument(
337
            'Add metadata (KEY=VALUE) to an object (can be repeated)',
338
            '--metadata-add'),
339
        metadata_key_to_delete=RepeatableArgument(
340
            'Delete object metadata (can be repeated)', '--metadata-del'),
341
    )
342
    required = [
343
        'publish', 'unpublish', 'uuid_for_read_permission', 'metadata_to_set',
344
        'uuid_for_write_permission', 'no_permissions',
345
        'metadata_key_to_delete']
346

    
347
    @errors.generic.all
348
    @errors.pithos.connection
349
    @errors.pithos.container
350
    @errors.pithos.object_path
351
    def _run(self):
352
        if self['publish']:
353
            self.writeln(self.client.publish_object(self.path))
354
        if self['unpublish']:
355
            self.client.unpublish_object(self.path)
356
        if self['uuid_for_read_permission'] or self[
357
                'uuid_for_write_permission']:
358
            perms = self.client.get_object_sharing(self.path)
359
            read, write = perms.get('read', ''), perms.get('write', '')
360
            read = read.split(',') if read else []
361
            write = write.split(',') if write else []
362
            read += self['uuid_for_read_permission']
363
            write += self['uuid_for_write_permission']
364
            self.client.set_object_sharing(
365
                self.path, read_permission=read, write_permission=write)
366
            self.print_dict(self.client.get_object_sharing(self.path))
367
        if self['no_permissions']:
368
            self.client.del_object_sharing(self.path)
369
        metadata = self['metadata_to_set'] or dict()
370
        for k in self['metadata_key_to_delete']:
371
            metadata[k] = ''
372
        if metadata:
373
            self.client.set_object_meta(self.path, metadata)
374
            self.print_dict(self.client.get_object_meta(self.path))
375

    
376
    def main(self, path_or_url):
377
        super(self.__class__, self)._run(path_or_url)
378
        if self['publish'] and self['unpublish']:
379
            raise CLIInvalidArgument(
380
                'Arguments %s and %s cannot be used together' % (
381
                    self.arguments['publish'].lvalue,
382
                    self.arguments['publish'].lvalue))
383
        if self['no_permissions'] and (
384
                self['uuid_for_read_permission'] or self[
385
                    'uuid_for_write_permission']):
386
            raise CLIInvalidArgument(
387
                '%s cannot be used with other permission arguments' % (
388
                    self.arguments['no_permissions'].lvalue))
389
        self._run()
390

    
391

    
392
@command(file_cmds)
393
class file_create(_pithos_container, _optional_output_cmd):
394
    """Create an empty file"""
395

    
396
    arguments = dict(
397
        content_type=ValueArgument(
398
            'Set content type (default: application/octet-stream)',
399
            '--content-type',
400
            default='application/octet-stream')
401
    )
402

    
403
    @errors.generic.all
404
    @errors.pithos.connection
405
    @errors.pithos.container
406
    def _run(self):
407
        self._optional_output(
408
            self.client.create_object(self.path, self['content_type']))
409

    
410
    def main(self, path_or_url):
411
        super(self.__class__, self)._run(path_or_url)
412
        self._run()
413

    
414

    
415
@command(file_cmds)
416
class file_mkdir(_pithos_container, _optional_output_cmd):
417
    """Create a directory: /file create --content-type='applcation/directory'
418
    """
419

    
420
    @errors.generic.all
421
    @errors.pithos.connection
422
    @errors.pithos.container
423
    def _run(self):
424
        self._optional_output(self.client.create_directory(self.path))
425

    
426
    def main(self, path_or_url):
427
        super(self.__class__, self)._run(path_or_url)
428
        self._run()
429

    
430

    
431
@command(file_cmds)
432
class file_delete(_pithos_container):
433
    """Delete a file or directory object"""
434

    
435
    arguments = dict(
436
        until_date=DateArgument('remove history until then', '--until'),
437
        yes=FlagArgument('Do not prompt for permission', '--yes'),
438
        recursive=FlagArgument(
439
            'If a directory, empty first', ('-r', '--recursive')),
440
        delimiter=ValueArgument(
441
            'delete objects prefixed with <object><delimiter>', '--delimiter')
442
    )
443

    
444
    @errors.generic.all
445
    @errors.pithos.connection
446
    @errors.pithos.container
447
    @errors.pithos.object_path
448
    def _run(self):
449
        if self.path:
450
            if self['yes'] or self.ask_user(
451
                    'Delete /%s/%s ?' % (self.container, self.path)):
452
                self.client.del_object(
453
                    self.path,
454
                    until=self['until_date'],
455
                    delimiter='/' if self['recursive'] else self['delimiter'])
456
            else:
457
                self.error('Aborted')
458
        else:
459
            if self['yes'] or self.ask_user(
460
                    'Empty container /%s ?' % self.container):
461
                self.client.container_delete(self.container, delimiter='/')
462
            else:
463
                self.error('Aborted')
464

    
465
    def main(self, path_or_url):
466
        super(self.__class__, self)._run(path_or_url)
467
        self._run()
468

    
469

    
470
class _source_destination(_pithos_container, _optional_output_cmd):
471

    
472
    sd_arguments = dict(
473
        destination_user_uuid=ValueArgument(
474
            'default: current user uuid', '--to-account'),
475
        destination_container=ValueArgument(
476
            'default: pithos', '--to-container'),
477
        source_prefix=FlagArgument(
478
            'Transfer all files that are prefixed with SOURCE PATH If the '
479
            'destination path is specified, replace SOURCE_PATH with '
480
            'DESTINATION_PATH',
481
            ('-r', '--recursive')),
482
        force=FlagArgument(
483
            'Overwrite destination objects, if needed', ('-f', '--force')),
484
        source_version=ValueArgument(
485
            'The version of the source object', '--source-version')
486
    )
487

    
488
    def __init__(self, arguments={}, auth_base=None, cloud=None):
489
        self.arguments.update(arguments)
490
        self.arguments.update(self.sd_arguments)
491
        super(_source_destination, self).__init__(
492
            self.arguments, auth_base, cloud)
493

    
494
    def _report_transfer(self, src, dst, transfer_name):
495
        if not dst:
496
            if transfer_name in ('move', ):
497
                self.error('  delete source directory %s' % src)
498
            return
499
        dst_prf = '' if self.account == self.dst_client.account else (
500
                'pithos://%s' % self.dst_client.account)
501
        if src:
502
            src_prf = '' if self.account == self.dst_client.account else (
503
                    'pithos://%s' % self.account)
504
            self.error('  %s %s/%s/%s\n  -->  %s/%s/%s' % (
505
                transfer_name,
506
                src_prf, self.container, src,
507
                dst_prf, self.dst_client.container, dst))
508
        else:
509
            self.error('  mkdir %s/%s/%s' % (
510
                dst_prf, self.dst_client.container, dst))
511

    
512
    @errors.generic.all
513
    @errors.pithos.account
514
    def _src_dst(self, version=None):
515
        """Preconditions:
516
        self.account, self.container, self.path
517
        self.dst_acc, self.dst_con, self.dst_path
518
        They should all be configured properly
519
        :returns: [(src_path, dst_path), ...], if src_path is None, create
520
            destination directory
521
        """
522
        src_objects, dst_objects, pairs = dict(), dict(), []
523
        try:
524
            for obj in self.dst_client.list_objects(
525
                    prefix=self.dst_path or self.path or '/'):
526
                dst_objects[obj['name']] = obj
527
        except ClientError as ce:
528
            if ce.status in (404, ):
529
                raise CLIError(
530
                    'Destination container pithos://%s/%s not found' % (
531
                        self.dst_client.account, self.dst_client.container))
532
            raise ce
533
        if self['source_prefix']:
534
            #  Copy and replace prefixes
535
            for src_obj in self.client.list_objects(prefix=self.path):
536
                src_objects[src_obj['name']] = src_obj
537
            for src_path, src_obj in src_objects.items():
538
                dst_path = '%s%s' % (
539
                    self.dst_path or self.path, src_path[len(self.path):])
540
                dst_obj = dst_objects.get(dst_path, None)
541
                if self['force'] or not dst_obj:
542
                    #  Just do it
543
                    pairs.append((
544
                        None if self._is_dir(src_obj) else src_path, dst_path))
545
                    if self._is_dir(src_obj):
546
                        pairs.append((self.path or dst_path, None))
547
                elif not (self._is_dir(dst_obj) and self._is_dir(src_obj)):
548
                    raise CLIError(
549
                        'Destination object exists', importance=2, details=[
550
                            'Failed while transfering:',
551
                            '    pithos://%s/%s/%s' % (
552
                                    self.account,
553
                                    self.container,
554
                                    src_path),
555
                            '--> pithos://%s/%s/%s' % (
556
                                    self.dst_client.account,
557
                                    self.dst_client.container,
558
                                    dst_path),
559
                            'Use %s to transfer overwrite' % (
560
                                    self.arguments['force'].lvalue)])
561
        else:
562
            #  One object transfer
563
            try:
564
                src_version_arg = self.arguments.get('source_version', None)
565
                src_obj = self.client.get_object_info(
566
                    self.path,
567
                    version=src_version_arg.value if src_version_arg else None)
568
            except ClientError as ce:
569
                if ce.status in (204, ):
570
                    raise CLIError(
571
                        'Missing specific path container %s' % self.container,
572
                        importance=2, details=[
573
                            'To transfer container contents %s' % (
574
                                self.arguments['source_prefix'].lvalue)])
575
                raise
576
            dst_path = self.dst_path or self.path
577
            dst_obj = dst_objects.get(dst_path or self.path, None)
578
            if self['force'] or not dst_obj:
579
                pairs.append(
580
                    (None if self._is_dir(src_obj) else self.path, dst_path))
581
                if self._is_dir(src_obj):
582
                    pairs.append((self.path or dst_path, None))
583
            elif self._is_dir(src_obj):
584
                raise CLIError(
585
                    'Cannot transfer an application/directory object',
586
                    importance=2, details=[
587
                        'The object pithos://%s/%s/%s is a directory' % (
588
                            self.account,
589
                            self.container,
590
                            self.path),
591
                        'To recursively copy a directory, use',
592
                        '  %s' % self.arguments['source_prefix'].lvalue,
593
                        'To create a file, use',
594
                        '  /file create  (general purpose)',
595
                        '  /file mkdir   (a directory object)'])
596
            else:
597
                raise CLIError(
598
                    'Destination object exists',
599
                    importance=2, details=[
600
                        'Failed while transfering:',
601
                        '    pithos://%s/%s/%s' % (
602
                                self.account,
603
                                self.container,
604
                                self.path),
605
                        '--> pithos://%s/%s/%s' % (
606
                                self.dst_client.account,
607
                                self.dst_client.container,
608
                                dst_path),
609
                        'Use %s to transfer overwrite' % (
610
                                self.arguments['force'].lvalue)])
611
        return pairs
612

    
613
    def _run(self, source_path_or_url, destination_path_or_url=''):
614
        super(_source_destination, self)._run(source_path_or_url)
615
        dst_acc, dst_con, dst_path = self._resolve_pithos_url(
616
            destination_path_or_url)
617
        self.dst_client = PithosClient(
618
            base_url=self.client.base_url, token=self.client.token,
619
            container=self[
620
                'destination_container'] or dst_con or self.client.container,
621
            account=self[
622
                'destination_user_uuid'] or dst_acc or self.account)
623
        self.dst_path = dst_path or self.path
624

    
625

    
626
@command(file_cmds)
627
class file_copy(_source_destination):
628
    """Copy objects, even between different accounts or containers"""
629

    
630
    arguments = dict(
631
        public=ValueArgument('publish new object', '--public'),
632
        content_type=ValueArgument(
633
            'change object\'s content type', '--content-type'),
634
        source_version=ValueArgument(
635
            'The version of the source object', '--object-version')
636
    )
637

    
638
    @errors.generic.all
639
    @errors.pithos.connection
640
    @errors.pithos.container
641
    @errors.pithos.account
642
    def _run(self):
643
        for src, dst in self._src_dst(self['source_version']):
644
            self._report_transfer(src, dst, 'copy')
645
            if src and dst:
646
                self.dst_client.copy_object(
647
                    src_container=self.client.container,
648
                    src_object=src,
649
                    dst_container=self.dst_client.container,
650
                    dst_object=dst,
651
                    source_account=self.client.account,
652
                    source_version=self['source_version'],
653
                    public=self['public'],
654
                    content_type=self['content_type'])
655
            elif dst:
656
                self.dst_client.create_directory(dst)
657

    
658
    def main(self, source_path_or_url, destination_path_or_url=None):
659
        super(file_copy, self)._run(
660
            source_path_or_url, destination_path_or_url or '')
661
        self._run()
662

    
663

    
664
@command(file_cmds)
665
class file_move(_source_destination):
666
    """Move objects, even between different accounts or containers"""
667

    
668
    arguments = dict(
669
        public=ValueArgument('publish new object', '--public'),
670
        content_type=ValueArgument(
671
            'change object\'s content type', '--content-type')
672
    )
673

    
674
    @errors.generic.all
675
    @errors.pithos.connection
676
    @errors.pithos.container
677
    @errors.pithos.account
678
    def _run(self):
679
        for src, dst in self._src_dst():
680
            self._report_transfer(src, dst, 'move')
681
            if src and dst:
682
                self.dst_client.move_object(
683
                    src_container=self.client.container,
684
                    src_object=src,
685
                    dst_container=self.dst_client.container,
686
                    dst_object=dst,
687
                    source_account=self.account,
688
                    public=self['public'],
689
                    content_type=self['content_type'])
690
            elif dst:
691
                self.dst_client.create_directory(dst)
692
            else:
693
                self.client.del_object(src)
694

    
695
    def main(self, source_path_or_url, destination_path_or_url=None):
696
        super(file_move, self)._run(
697
            source_path_or_url, destination_path_or_url or '')
698
        self._run()
699

    
700

    
701
@command(file_cmds)
702
class file_append(_pithos_container, _optional_output_cmd):
703
    """Append local file to (existing) remote object
704
    The remote object should exist.
705
    If the remote object is a directory, it is transformed into a file.
706
    In the later case, objects under the directory remain intact.
707
    """
708

    
709
    arguments = dict(
710
        progress_bar=ProgressBarArgument(
711
            'do not show progress bar', ('-N', '--no-progress-bar'),
712
            default=False),
713
        max_threads=IntArgument('default: 1', '--threads'),
714
    )
715

    
716
    @errors.generic.all
717
    @errors.pithos.connection
718
    @errors.pithos.container
719
    @errors.pithos.object_path
720
    def _run(self, local_path):
721
        if self['max_threads'] > 0:
722
            self.client.MAX_THREADS = int(self['max_threads'])
723
        (progress_bar, upload_cb) = self._safe_progress_bar('Appending')
724
        try:
725
            with open(local_path, 'rb') as f:
726
                self._optional_output(
727
                    self.client.append_object(self.path, f, upload_cb))
728
        finally:
729
            self._safe_progress_bar_finish(progress_bar)
730

    
731
    def main(self, local_path, remote_path_or_url):
732
        super(self.__class__, self)._run(remote_path_or_url)
733
        self._run(local_path)
734

    
735

    
736
@command(file_cmds)
737
class file_truncate(_pithos_container, _optional_output_cmd):
738
    """Truncate remote file up to size"""
739

    
740
    arguments = dict(
741
        size_in_bytes=IntArgument('Length of file after truncation', '--size')
742
    )
743
    required = ('size_in_bytes', )
744

    
745
    @errors.generic.all
746
    @errors.pithos.connection
747
    @errors.pithos.container
748
    @errors.pithos.object_path
749
    @errors.pithos.object_size
750
    def _run(self, size):
751
        self._optional_output(self.client.truncate_object(self.path, size))
752

    
753
    def main(self, path_or_url):
754
        super(self.__class__, self)._run(path_or_url)
755
        self._run(size=self['size_in_bytes'])
756

    
757

    
758
@command(file_cmds)
759
class file_overwrite(_pithos_container, _optional_output_cmd):
760
    """Overwrite part of a remote file"""
761

    
762
    arguments = dict(
763
        progress_bar=ProgressBarArgument(
764
            'do not show progress bar', ('-N', '--no-progress-bar'),
765
            default=False),
766
        start_position=IntArgument('File position in bytes', '--from'),
767
        end_position=IntArgument('File position in bytes', '--to')
768
    )
769
    required = ('start_position', 'end_position')
770

    
771
    @errors.generic.all
772
    @errors.pithos.connection
773
    @errors.pithos.container
774
    @errors.pithos.object_path
775
    @errors.pithos.object_size
776
    def _run(self, local_path, start, end):
777
        start, end = int(start), int(end)
778
        (progress_bar, upload_cb) = self._safe_progress_bar(
779
            'Overwrite %s bytes' % (end - start))
780
        try:
781
            with open(path.abspath(local_path), 'rb') as f:
782
                self._optional_output(self.client.overwrite_object(
783
                    obj=self.path,
784
                    start=start,
785
                    end=end,
786
                    source_file=f,
787
                    upload_cb=upload_cb))
788
        finally:
789
            self._safe_progress_bar_finish(progress_bar)
790

    
791
    def main(self, local_path, path_or_url):
792
        super(self.__class__, self)._run(path_or_url)
793
        self.path = self.path or path.basename(local_path)
794
        self._run(
795
            local_path=local_path,
796
            start=self['start_position'],
797
            end=self['end_position'])
798

    
799

    
800
@command(file_cmds)
801
class file_upload(_pithos_container, _optional_output_cmd):
802
    """Upload a file"""
803

    
804
    arguments = dict(
805
        max_threads=IntArgument('default: 5', '--threads'),
806
        content_encoding=ValueArgument(
807
            'set MIME content type', '--content-encoding'),
808
        content_disposition=ValueArgument(
809
            'specify objects presentation style', '--content-disposition'),
810
        content_type=ValueArgument('specify content type', '--content-type'),
811
        uuid_for_read_permission=RepeatableArgument(
812
            'Give read access to a user or group (can be repeated) '
813
            'Use * for all users',
814
            '--read-permission'),
815
        uuid_for_write_permission=RepeatableArgument(
816
            'Give write access to a user or group (can be repeated) '
817
            'Use * for all users',
818
            '--write-permission'),
819
        public=FlagArgument('make object publicly accessible', '--public'),
820
        progress_bar=ProgressBarArgument(
821
            'do not show progress bar',
822
            ('-N', '--no-progress-bar'),
823
            default=False),
824
        overwrite=FlagArgument('Force (over)write', ('-f', '--force')),
825
        recursive=FlagArgument(
826
            'Recursively upload directory *contents* + subdirectories',
827
            ('-r', '--recursive')),
828
        unchunked=FlagArgument(
829
            'Upload file as one block (not recommended)', '--unchunked'),
830
        md5_checksum=ValueArgument(
831
            'Confirm upload with a custom checksum (MD5)', '--etag'),
832
        use_hashes=FlagArgument(
833
            'Source file contains hashmap not data', '--source-is-hashmap'),
834
    )
835

    
836
    def _sharing(self):
837
        sharing = dict()
838
        readlist = self['uuid_for_read_permission']
839
        if readlist:
840
            sharing['read'] = self['uuid_for_read_permission']
841
        writelist = self['uuid_for_write_permission']
842
        if writelist:
843
            sharing['write'] = self['uuid_for_write_permission']
844
        return sharing or None
845

    
846
    def _check_container_limit(self, path):
847
        cl_dict = self.client.get_container_limit()
848
        container_limit = int(cl_dict['x-container-policy-quota'])
849
        r = self.client.container_get()
850
        used_bytes = sum(int(o['bytes']) for o in r.json)
851
        path_size = get_path_size(path)
852
        if container_limit and path_size > (container_limit - used_bytes):
853
            raise CLIError(
854
                'Container %s (limit(%s) - used(%s)) < (size(%s) of %s)' % (
855
                    self.client.container,
856
                    format_size(container_limit),
857
                    format_size(used_bytes),
858
                    format_size(path_size),
859
                    path),
860
                details=[
861
                    'Check accound limit: /file quota',
862
                    'Check container limit:',
863
                    '\t/file containerlimit get %s' % self.client.container,
864
                    'Increase container limit:',
865
                    '\t/file containerlimit set <new limit> %s' % (
866
                        self.client.container)])
867

    
868
    def _src_dst(self, local_path, remote_path, objlist=None):
869
        lpath = path.abspath(local_path)
870
        short_path = path.basename(path.abspath(local_path))
871
        rpath = remote_path or short_path
872
        if path.isdir(lpath):
873
            if not self['recursive']:
874
                raise CLIError('%s is a directory' % lpath, details=[
875
                    'Use %s to upload directories & contents' % (
876
                        self.arguments['recursive'].lvalue)])
877
            robj = self.client.container_get(path=rpath)
878
            if not self['overwrite']:
879
                if robj.json:
880
                    raise CLIError(
881
                        'Objects/files prefixed as %s already exist' % rpath,
882
                        details=['Existing objects:'] + ['\t/%s/\t%s' % (
883
                            o['name'],
884
                            o['content_type'][12:]) for o in robj.json] + [
885
                            'Use -f to add, overwrite or resume'])
886
                else:
887
                    try:
888
                        topobj = self.client.get_object_info(rpath)
889
                        if not self._is_dir(topobj):
890
                            raise CLIError(
891
                                'Object /%s/%s exists but not a directory' % (
892
                                    self.container, rpath),
893
                                details=['Use -f to overwrite'])
894
                    except ClientError as ce:
895
                        if ce.status not in (404, ):
896
                            raise
897
            self._check_container_limit(lpath)
898
            prev = ''
899
            for top, subdirs, files in walk(lpath):
900
                if top != prev:
901
                    prev = top
902
                    try:
903
                        rel_path = rpath + top.split(lpath)[1]
904
                    except IndexError:
905
                        rel_path = rpath
906
                    self.error('mkdir /%s/%s' % (
907
                        self.client.container, rel_path))
908
                    self.client.create_directory(rel_path)
909
                for f in files:
910
                    fpath = path.join(top, f)
911
                    if path.isfile(fpath):
912
                        rel_path = rel_path.replace(path.sep, '/')
913
                        pathfix = f.replace(path.sep, '/')
914
                        yield open(fpath, 'rb'), '%s/%s' % (rel_path, pathfix)
915
                    else:
916
                        self.error('%s is not a regular file' % fpath)
917
        else:
918
            if not path.isfile(lpath):
919
                raise CLIError(('%s is not a regular file' % lpath) if (
920
                    path.exists(lpath)) else '%s does not exist' % lpath)
921
            try:
922
                robj = self.client.get_object_info(rpath)
923
                if remote_path and self._is_dir(robj):
924
                    rpath += '/%s' % (short_path.replace(path.sep, '/'))
925
                    self.client.get_object_info(rpath)
926
                if not self['overwrite']:
927
                    raise CLIError(
928
                        'Object /%s/%s already exists' % (
929
                            self.container, rpath),
930
                        details=['use -f to overwrite / resume'])
931
            except ClientError as ce:
932
                if ce.status not in (404, ):
933
                    raise
934
            self._check_container_limit(lpath)
935
            yield open(lpath, 'rb'), rpath
936

    
937
    def _run(self, local_path, remote_path):
938
        self.client.MAX_THREADS = int(self['max_threads'] or 5)
939
        params = dict(
940
            content_encoding=self['content_encoding'],
941
            content_type=self['content_type'],
942
            content_disposition=self['content_disposition'],
943
            sharing=self._sharing(),
944
            public=self['public'])
945
        uploaded, container_info_cache = list, dict()
946
        rpref = 'pithos://%s' if self['account'] else ''
947
        for f, rpath in self._src_dst(local_path, remote_path):
948
            self.error('%s --> %s/%s/%s' % (
949
                f.name, rpref, self.client.container, rpath))
950
            if not (self['content_type'] and self['content_encoding']):
951
                ctype, cenc = guess_mime_type(f.name)
952
                params['content_type'] = self['content_type'] or ctype
953
                params['content_encoding'] = self['content_encoding'] or cenc
954
            if self['unchunked']:
955
                r = self.client.upload_object_unchunked(
956
                    rpath, f,
957
                    etag=self['md5_checksum'], withHashFile=self['use_hashes'],
958
                    **params)
959
                if self['with_output'] or self['json_output']:
960
                    r['name'] = '/%s/%s' % (self.client.container, rpath)
961
                    uploaded.append(r)
962
            else:
963
                try:
964
                    (progress_bar, upload_cb) = self._safe_progress_bar(
965
                        'Uploading %s' % f.name.split(path.sep)[-1])
966
                    if progress_bar:
967
                        hash_bar = progress_bar.clone()
968
                        hash_cb = hash_bar.get_generator(
969
                            'Calculating block hashes')
970
                    else:
971
                        hash_cb = None
972
                    r = self.client.upload_object(
973
                        rpath, f,
974
                        hash_cb=hash_cb,
975
                        upload_cb=upload_cb,
976
                        container_info_cache=container_info_cache,
977
                        **params)
978
                    if self['with_output'] or self['json_output']:
979
                        r['name'] = '/%s/%s' % (self.client.container, rpath)
980
                        uploaded.append(r)
981
                except Exception:
982
                    self._safe_progress_bar_finish(progress_bar)
983
                    raise
984
                finally:
985
                    self._safe_progress_bar_finish(progress_bar)
986
        self._optional_output(uploaded)
987
        self.error('Upload completed')
988

    
989
    def main(self, local_path, remote_path_or_url):
990
        super(self.__class__, self)._run(remote_path_or_url)
991
        remote_path = self.path or path.basename(path.abspath(local_path))
992
        self._run(local_path=local_path, remote_path=remote_path)
993

    
994

    
995
class RangeArgument(ValueArgument):
996
    """
997
    :value type: string of the form <start>-<end> where <start> and <end> are
998
        integers
999
    :value returns: the input string, after type checking <start> and <end>
1000
    """
1001

    
1002
    @property
1003
    def value(self):
1004
        return getattr(self, '_value', self.default)
1005

    
1006
    @value.setter
1007
    def value(self, newvalues):
1008
        if newvalues:
1009
            self._value = getattr(self, '_value', self.default)
1010
            for newvalue in newvalues.split(','):
1011
                self._value = ('%s,' % self._value) if self._value else ''
1012
                start, sep, end = newvalue.partition('-')
1013
                if sep:
1014
                    if start:
1015
                        start, end = (int(start), int(end))
1016
                        if start > end:
1017
                            raise CLIInvalidArgument(
1018
                                'Invalid range %s' % newvalue, details=[
1019
                                'Valid range formats',
1020
                                '  START-END', '  UP_TO', '  -FROM',
1021
                                'where all values are integers'])
1022
                        self._value += '%s-%s' % (start, end)
1023
                    else:
1024
                        self._value += '-%s' % int(end)
1025
                else:
1026
                    self._value += '%s' % int(start)
1027

    
1028

    
1029
@command(file_cmds)
1030
class file_cat(_pithos_container):
1031
    """Fetch remote file contents"""
1032

    
1033
    arguments = dict(
1034
        range=RangeArgument('show range of data', '--range'),
1035
        if_match=ValueArgument('show output if ETags match', '--if-match'),
1036
        if_none_match=ValueArgument(
1037
            'show output if ETags match', '--if-none-match'),
1038
        if_modified_since=DateArgument(
1039
            'show output modified since then', '--if-modified-since'),
1040
        if_unmodified_since=DateArgument(
1041
            'show output unmodified since then', '--if-unmodified-since'),
1042
        object_version=ValueArgument(
1043
            'Get contents of the chosen version', '--object-version')
1044
    )
1045

    
1046
    @errors.generic.all
1047
    @errors.pithos.connection
1048
    @errors.pithos.container
1049
    @errors.pithos.object_path
1050
    def _run(self):
1051
        self.client.download_object(
1052
            self.path, self._out,
1053
            range_str=self['range'],
1054
            version=self['object_version'],
1055
            if_match=self['if_match'],
1056
            if_none_match=self['if_none_match'],
1057
            if_modified_since=self['if_modified_since'],
1058
            if_unmodified_since=self['if_unmodified_since'])
1059

    
1060
    def main(self, path_or_url):
1061
        super(self.__class__, self)._run(path_or_url)
1062
        self._run()
1063

    
1064

    
1065
@command(file_cmds)
1066
class file_download(_pithos_container):
1067
    """Download a remove file or directory object to local file system"""
1068

    
1069
    arguments = dict(
1070
        resume=FlagArgument(
1071
            'Resume/Overwrite (attempt resume, else overwrite)',
1072
            ('-f', '--resume')),
1073
        range=RangeArgument('Download only that range of data', '--range'),
1074
        matching_etag=ValueArgument('download iff ETag match', '--if-match'),
1075
        non_matching_etag=ValueArgument(
1076
            'download iff ETags DO NOT match', '--if-none-match'),
1077
        modified_since_date=DateArgument(
1078
            'download iff remote file is modified since then',
1079
            '--if-modified-since'),
1080
        unmodified_since_date=DateArgument(
1081
            'show output iff remote file is unmodified since then',
1082
            '--if-unmodified-since'),
1083
        object_version=ValueArgument(
1084
            'download a file of a specific version', '--object-version'),
1085
        max_threads=IntArgument('default: 5', '--threads'),
1086
        progress_bar=ProgressBarArgument(
1087
            'do not show progress bar', ('-N', '--no-progress-bar'),
1088
            default=False),
1089
        recursive=FlagArgument(
1090
            'Download a remote directory object and its contents',
1091
            ('-r', '--recursive'))
1092
        )
1093

    
1094
    def _src_dst(self, local_path):
1095
        """Create a list of (src, dst) where src is a remote location and dst
1096
        is an open file descriptor. Directories are denoted as (None, dirpath)
1097
        and they are pretended to other objects in a very strict order (shorter
1098
        to longer path)."""
1099
        ret = []
1100
        try:
1101
            if self.path:
1102
                obj = self.client.get_object_info(
1103
                    self.path, version=self['object_version'])
1104
                obj.setdefault('name', self.path.strip('/'))
1105
            else:
1106
                obj = None
1107
        except ClientError as ce:
1108
            if ce.status in (404, ):
1109
                raiseCLIError(ce, details=[
1110
                    'To download an object, it must exist either as a file or'
1111
                    ' as a directory.',
1112
                    'For example, to download everything under prefix/ the '
1113
                    'directory "prefix" must exist.',
1114
                    'To see if an remote object is actually there:',
1115
                    '  /file info [/CONTAINER/]OBJECT',
1116
                    'To create a directory object:',
1117
                    '  /file mkdir [/CONTAINER/]OBJECT'])
1118
            if ce.status in (204, ):
1119
                raise CLIError(
1120
                    'No file or directory objects to download',
1121
                    details=[
1122
                        'To download a container (e.g., %s):' % self.container,
1123
                        '  [kamaki] container download %s [LOCAL_PATH]' % (
1124
                            self.container)])
1125
            raise
1126
        rpath = self.path.strip('/')
1127
        if local_path and self.path and local_path.endswith('/'):
1128
            local_path = local_path[-1:]
1129

    
1130
        if (not obj) or self._is_dir(obj):
1131
            if self['recursive']:
1132
                if not (self.path or local_path.endswith('/')):
1133
                    #  Download the whole container
1134
                    local_path = '' if local_path in ('.', ) else local_path
1135
                    local_path = '%s/' % (local_path or self.container)
1136
                obj = obj or dict(
1137
                    name='', content_type='application/directory')
1138
                dirs, files = [obj, ], []
1139
                objects = self.client.container_get(
1140
                    path=self.path,
1141
                    if_modified_since=self['modified_since_date'],
1142
                    if_unmodified_since=self['unmodified_since_date'])
1143
                for o in objects.json:
1144
                    (dirs if self._is_dir(o) else files).append(o)
1145

    
1146
                #  Put the directories on top of the list
1147
                for dpath in sorted(['%s%s' % (
1148
                        local_path, d['name'][len(rpath):]) for d in dirs]):
1149
                    if path.exists(dpath):
1150
                        if path.isdir(dpath):
1151
                            continue
1152
                        raise CLIError(
1153
                            'Cannot replace local file %s with a directory '
1154
                            'of the same name' % dpath,
1155
                            details=[
1156
                                'Either remove the file or specify a'
1157
                                'different target location'])
1158
                    ret.append((None, dpath, None))
1159

    
1160
                #  Append the file objects
1161
                for opath in [o['name'] for o in files]:
1162
                    lpath = '%s%s' % (local_path, opath[len(rpath):])
1163
                    if self['resume']:
1164
                        fxists = path.exists(lpath)
1165
                        if fxists and path.isdir(lpath):
1166
                            raise CLIError(
1167
                                'Cannot change local dir %s info file' % (
1168
                                    lpath),
1169
                                details=[
1170
                                    'Either remove the file or specify a'
1171
                                    'different target location'])
1172
                        ret.append((opath, lpath, fxists))
1173
                    elif path.exists(lpath):
1174
                        raise CLIError(
1175
                            'Cannot overwrite %s' % lpath,
1176
                            details=['To overwrite/resume, use  %s' % (
1177
                                self.arguments['resume'].lvalue)])
1178
                    else:
1179
                        ret.append((opath, lpath, None))
1180
            elif self.path:
1181
                raise CLIError(
1182
                    'Remote object /%s/%s is a directory' % (
1183
                        self.container, local_path),
1184
                    details=['Use %s to download directories' % (
1185
                        self.arguments['recursive'].lvalue)])
1186
            else:
1187
                parsed_name = self.arguments['recursive'].lvalue
1188
                raise CLIError(
1189
                    'Cannot download container %s' % self.container,
1190
                    details=[
1191
                        'Use %s to download containers' % parsed_name,
1192
                        '  [kamaki] file download %s /%s [LOCAL_PATH]' % (
1193
                            parsed_name, self.container)])
1194
        else:
1195
            #  Remote object is just a file
1196
            if path.exists(local_path) and not self['resume']:
1197
                raise CLIError(
1198
                    'Cannot overwrite local file %s' % (lpath),
1199
                    details=['To overwrite/resume, use  %s' % (
1200
                        self.arguments['resume'].lvalue)])
1201
            ret.append((rpath, local_path, self['resume']))
1202
        for r, l, resume in ret:
1203
            if r:
1204
                with open(l, 'rwb+' if resume else 'wb+') as f:
1205
                    yield (r, f)
1206
            else:
1207
                yield (r, l)
1208

    
1209
    @errors.generic.all
1210
    @errors.pithos.connection
1211
    @errors.pithos.container
1212
    @errors.pithos.object_path
1213
    @errors.pithos.local_path
1214
    @errors.pithos.local_path_download
1215
    def _run(self, local_path):
1216
        self.client.MAX_THREADS = int(self['max_threads'] or 5)
1217
        progress_bar = None
1218
        try:
1219
            for rpath, output_file in self._src_dst(local_path):
1220
                if not rpath:
1221
                    self.error('Create local directory %s' % output_file)
1222
                    makedirs(output_file)
1223
                    continue
1224
                self.error('/%s/%s --> %s' % (
1225
                    self.container, rpath, output_file.name))
1226
                progress_bar, download_cb = self._safe_progress_bar(
1227
                    '  download')
1228
                self.client.download_object(
1229
                    rpath, output_file,
1230
                    download_cb=download_cb,
1231
                    range_str=self['range'],
1232
                    version=self['object_version'],
1233
                    if_match=self['matching_etag'],
1234
                    resume=self['resume'],
1235
                    if_none_match=self['non_matching_etag'],
1236
                    if_modified_since=self['modified_since_date'],
1237
                    if_unmodified_since=self['unmodified_since_date'])
1238
        except KeyboardInterrupt:
1239
            from threading import activeCount, enumerate as activethreads
1240
            timeout = 0.5
1241
            while activeCount() > 1:
1242
                self._out.write('\nCancel %s threads: ' % (activeCount() - 1))
1243
                self._out.flush()
1244
                for thread in activethreads():
1245
                    try:
1246
                        thread.join(timeout)
1247
                        self._out.write('.' if thread.isAlive() else '*')
1248
                    except RuntimeError:
1249
                        continue
1250
                    finally:
1251
                        self._out.flush()
1252
                        timeout += 0.1
1253
            self.error('\nDownload canceled by user')
1254
            if local_path is not None:
1255
                self.error('to resume, re-run with --resume')
1256
        except Exception:
1257
            self._safe_progress_bar_finish(progress_bar)
1258
            raise
1259
        finally:
1260
            self._safe_progress_bar_finish(progress_bar)
1261

    
1262
    def main(self, remote_path_or_url, local_path=None):
1263
        super(self.__class__, self)._run(remote_path_or_url)
1264
        local_path = local_path or self.path or '.'
1265
        self._run(local_path=local_path)
1266

    
1267

    
1268
@command(container_cmds)
1269
class container_info(_pithos_account, _optional_json):
1270
    """Get information about a container"""
1271

    
1272
    arguments = dict(
1273
        until_date=DateArgument('show metadata until then', '--until'),
1274
        metadata=FlagArgument('Show only container metadata', '--metadata'),
1275
        sizelimit=FlagArgument(
1276
            'Show the maximum size limit for container', '--size-limit'),
1277
        in_bytes=FlagArgument('Show size limit in bytes', ('-b', '--bytes'))
1278
    )
1279

    
1280
    @errors.generic.all
1281
    @errors.pithos.connection
1282
    @errors.pithos.container
1283
    @errors.pithos.object_path
1284
    def _run(self):
1285
        if self['metadata']:
1286
            r, preflen = dict(), len('x-container-meta-')
1287
            for k, v in self.client.get_container_meta(
1288
                    until=self['until_date']).items():
1289
                r[k[preflen:]] = v
1290
        elif self['sizelimit']:
1291
            r = self.client.get_container_limit(
1292
                self.container)['x-container-policy-quota']
1293
            r = {'size limit': 'unlimited' if r in ('0', ) else (
1294
                int(r) if self['in_bytes'] else format_size(r))}
1295
        else:
1296
            r = self.client.get_container_info(self.container)
1297
        self._print(r, self.print_dict)
1298

    
1299
    def main(self, container):
1300
        super(self.__class__, self)._run()
1301
        self.container, self.client.container = container, container
1302
        self._run()
1303

    
1304

    
1305
class VersioningArgument(ValueArgument):
1306

    
1307
    schemes = ('auto', 'none')
1308

    
1309
    @property
1310
    def value(self):
1311
        return getattr(self, '_value', None)
1312

    
1313
    @value.setter
1314
    def value(self, new_scheme):
1315
        if new_scheme:
1316
            new_scheme = new_scheme.lower()
1317
            if new_scheme not in self.schemes:
1318
                raise CLIInvalidArgument('Invalid versioning value', details=[
1319
                    'Valid versioning values are %s' % ', '.join(
1320
                        self.schemes)])
1321
            self._value = new_scheme
1322

    
1323

    
1324
@command(container_cmds)
1325
class container_modify(_pithos_account, _optional_json):
1326
    """Modify the properties of a container"""
1327

    
1328
    arguments = dict(
1329
        metadata_to_add=KeyValueArgument(
1330
            'Add metadata in the form KEY=VALUE (can be repeated)',
1331
            '--metadata-add'),
1332
        metadata_to_delete=RepeatableArgument(
1333
            'Delete metadata by KEY (can be repeated)', '--metadata-del'),
1334
        sizelimit=DataSizeArgument(
1335
            'Set max size limit (0 for unlimited, '
1336
            'use units B, KiB, KB, etc.)', '--size-limit'),
1337
        versioning=VersioningArgument(
1338
            'Set a versioning scheme (%s)' % ', '.join(
1339
                VersioningArgument.schemes), '--versioning')
1340
    )
1341
    required = ['metadata_to_add', 'metadata_to_delete', 'sizelimit']
1342

    
1343
    @errors.generic.all
1344
    @errors.pithos.connection
1345
    @errors.pithos.container
1346
    def _run(self, container):
1347
        metadata = self['metadata_to_add']
1348
        for k in self['metadata_to_delete']:
1349
            metadata[k] = ''
1350
        if metadata:
1351
            self.client.set_container_meta(metadata)
1352
            self._print(self.client.get_container_meta(), self.print_dict)
1353
        if self['sizelimit'] is not None:
1354
            self.client.set_container_limit(self['sizelimit'])
1355
            r = self.client.get_container_limit()['x-container-policy-quota']
1356
            r = 'unlimited' if r in ('0', ) else format_size(r)
1357
            self.writeln('new size limit: %s' % r)
1358
        if self['versioning']:
1359
            self.client.set_container_versioning(self['versioning'])
1360
            self.writeln('new versioning scheme: %s' % (
1361
                self.client.get_container_versioning(self.container)[
1362
                    'x-container-policy-versioning']))
1363

    
1364
    def main(self, container):
1365
        super(self.__class__, self)._run()
1366
        self.client.container, self.container = container, container
1367
        self._run(container=container)
1368

    
1369

    
1370
@command(container_cmds)
1371
class container_list(_pithos_account, _optional_json, _name_filter):
1372
    """List all containers, or their contents"""
1373

    
1374
    arguments = dict(
1375
        detail=FlagArgument('Containers with details', ('-l', '--list')),
1376
        limit=IntArgument('limit number of listed items', ('-n', '--number')),
1377
        marker=ValueArgument('output greater that marker', '--marker'),
1378
        modified_since_date=ValueArgument(
1379
            'show output modified since then', '--if-modified-since'),
1380
        unmodified_since_date=ValueArgument(
1381
            'show output not modified since then', '--if-unmodified-since'),
1382
        until_date=DateArgument('show metadata until then', '--until'),
1383
        shared=FlagArgument('show only shared', '--shared'),
1384
        more=FlagArgument('read long results', '--more'),
1385
        enum=FlagArgument('Enumerate results', '--enumerate'),
1386
        recursive=FlagArgument(
1387
            'Recursively list containers and their contents',
1388
            ('-r', '--recursive'))
1389
    )
1390

    
1391
    def print_containers(self, container_list):
1392
        for index, container in enumerate(container_list):
1393
            if 'bytes' in container:
1394
                size = format_size(container['bytes'])
1395
            prfx = ('%s. ' % (index + 1)) if self['enum'] else ''
1396
            _cname = container['name'] if (
1397
                self['more']) else bold(container['name'])
1398
            cname = u'%s%s' % (prfx, _cname)
1399
            if self['detail']:
1400
                self.writeln(cname)
1401
                pretty_c = container.copy()
1402
                if 'bytes' in container:
1403
                    pretty_c['bytes'] = '%s (%s)' % (container['bytes'], size)
1404
                self.print_dict(pretty_c, exclude=('name'))
1405
                self.writeln()
1406
            else:
1407
                if 'count' in container and 'bytes' in container:
1408
                    self.writeln('%s (%s, %s objects)' % (
1409
                        cname, size, container['count']))
1410
                else:
1411
                    self.writeln(cname)
1412
            objects = container.get('objects', [])
1413
            if objects:
1414
                self.print_objects(objects)
1415
                self.writeln('')
1416

    
1417
    def _create_object_forest(self, container_list):
1418
        try:
1419
            for container in container_list:
1420
                self.client.container = container['name']
1421
                objects = self.client.container_get(
1422
                    limit=False if self['more'] else self['limit'],
1423
                    if_modified_since=self['modified_since_date'],
1424
                    if_unmodified_since=self['unmodified_since_date'],
1425
                    until=self['until_date'],
1426
                    show_only_shared=self['shared'])
1427
                container['objects'] = objects.json
1428
        finally:
1429
            self.client.container = None
1430

    
1431
    @errors.generic.all
1432
    @errors.pithos.connection
1433
    @errors.pithos.object_path
1434
    @errors.pithos.container
1435
    def _run(self, container):
1436
        if container:
1437
            r = self.client.container_get(
1438
                limit=False if self['more'] else self['limit'],
1439
                marker=self['marker'],
1440
                if_modified_since=self['modified_since_date'],
1441
                if_unmodified_since=self['unmodified_since_date'],
1442
                until=self['until_date'],
1443
                show_only_shared=self['shared'])
1444
        else:
1445
            r = self.client.account_get(
1446
                limit=False if self['more'] else self['limit'],
1447
                marker=self['marker'],
1448
                if_modified_since=self['modified_since_date'],
1449
                if_unmodified_since=self['unmodified_since_date'],
1450
                until=self['until_date'],
1451
                show_only_shared=self['shared'])
1452
        files = self._filter_by_name(r.json)
1453
        if self['recursive'] and not container:
1454
            self._create_object_forest(files)
1455
        if self['more']:
1456
            outbu, self._out = self._out, StringIO()
1457
        try:
1458
            if self['json_output'] or self['output_format']:
1459
                self._print(files)
1460
            else:
1461
                (self.print_objects if container else self.print_containers)(
1462
                    files)
1463
        finally:
1464
            if self['more']:
1465
                pager(self._out.getvalue())
1466
                self._out = outbu
1467

    
1468
    def main(self, container=None):
1469
        super(self.__class__, self)._run()
1470
        self.client.container, self.container = container, container
1471
        self._run(container)
1472

    
1473

    
1474
@command(container_cmds)
1475
class container_create(_pithos_account):
1476
    """Create a new container"""
1477

    
1478
    arguments = dict(
1479
        versioning=ValueArgument(
1480
            'set container versioning (auto/none)', '--versioning'),
1481
        limit=IntArgument('set default container limit', '--limit'),
1482
        meta=KeyValueArgument(
1483
            'set container metadata (can be repeated)', '--meta')
1484
    )
1485

    
1486
    @errors.generic.all
1487
    @errors.pithos.connection
1488
    @errors.pithos.container
1489
    def _run(self, container):
1490
        try:
1491
            self.client.create_container(
1492
                container=container,
1493
                sizelimit=self['limit'],
1494
                versioning=self['versioning'],
1495
                metadata=self['meta'],
1496
                success=(201, ))
1497
        except ClientError as ce:
1498
            if ce.status in (202, ):
1499
                raise CLIError(
1500
                    'Container %s alread exists' % container, details=[
1501
                    'Either delete %s or choose another name' % (container)])
1502
            raise
1503

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

    
1508

    
1509
@command(container_cmds)
1510
class container_delete(_pithos_account):
1511
    """Delete a container"""
1512

    
1513
    arguments = dict(
1514
        yes=FlagArgument('Do not prompt for permission', '--yes'),
1515
        recursive=FlagArgument(
1516
            'delete container even if not empty', ('-r', '--recursive'))
1517
    )
1518

    
1519
    @errors.generic.all
1520
    @errors.pithos.connection
1521
    @errors.pithos.container
1522
    def _run(self, container):
1523
        num_of_contents = int(self.client.get_container_info(container)[
1524
            'x-container-object-count'])
1525
        delimiter, msg = None, 'Delete container %s ?' % container
1526
        if self['recursive']:
1527
            delimiter, msg = '/', 'Empty and d%s' % msg[1:]
1528
        elif num_of_contents:
1529
            raise CLIError('Container %s is not empty' % container, details=[
1530
                'Use %s to delete non-empty containers' % (
1531
                    self.arguments['recursive'].lvalue)])
1532
        if self['yes'] or self.ask_user(msg):
1533
            if num_of_contents:
1534
                self.client.del_container(delimiter=delimiter)
1535
            self.client.purge_container()
1536

    
1537
    def main(self, container):
1538
        super(self.__class__, self)._run()
1539
        self.container, self.client.container = container, container
1540
        self._run(container)
1541

    
1542

    
1543
@command(container_cmds)
1544
class container_empty(_pithos_account):
1545
    """Empty a container"""
1546

    
1547
    arguments = dict(yes=FlagArgument('Do not prompt for permission', '--yes'))
1548

    
1549
    @errors.generic.all
1550
    @errors.pithos.connection
1551
    @errors.pithos.container
1552
    def _run(self, container):
1553
        if self['yes'] or self.ask_user('Empty container %s ?' % container):
1554
            self.client.del_container(delimiter='/')
1555

    
1556
    def main(self, container):
1557
        super(self.__class__, self)._run()
1558
        self.container, self.client.container = container, container
1559
        self._run(container)
1560

    
1561

    
1562
@command(sharer_cmds)
1563
class sharer_list(_pithos_account, _optional_json):
1564
    """List accounts who share file objects with current user"""
1565

    
1566
    arguments = dict(
1567
        detail=FlagArgument('show detailed output', ('-l', '--details')),
1568
        marker=ValueArgument('show output greater then marker', '--marker')
1569
    )
1570

    
1571
    @errors.generic.all
1572
    @errors.pithos.connection
1573
    def _run(self):
1574
        accounts = self.client.get_sharing_accounts(marker=self['marker'])
1575
        if not (self['json_output'] or self['output_format']):
1576
            usernames = self._uuids2usernames(
1577
                [acc['name'] for acc in accounts])
1578
            for item in accounts:
1579
                uuid = item['name']
1580
                item['id'], item['name'] = uuid, usernames[uuid]
1581
                if not self['detail']:
1582
                    item.pop('last_modified')
1583
        self._print(accounts)
1584

    
1585
    def main(self):
1586
        super(self.__class__, self)._run()
1587
        self._run()
1588

    
1589

    
1590
@command(sharer_cmds)
1591
class sharer_info(_pithos_account, _optional_json):
1592
    """Details on a Pithos+ sharer account (default: current account)"""
1593

    
1594
    @errors.generic.all
1595
    @errors.pithos.connection
1596
    def _run(self):
1597
        self._print(self.client.get_account_info(), self.print_dict)
1598

    
1599
    def main(self, account_uuid=None):
1600
        super(self.__class__, self)._run()
1601
        if account_uuid:
1602
            self.client.account, self.account = account_uuid, account_uuid
1603
        self._run()
1604

    
1605

    
1606
class _pithos_group(_pithos_account):
1607
    prefix = 'x-account-group-'
1608
    preflen = len(prefix)
1609

    
1610
    def _groups(self):
1611
        groups = dict()
1612
        for k, v in self.client.get_account_group().items():
1613
            groups[k[self.preflen:]] = v
1614
        return groups
1615

    
1616

    
1617
@command(group_cmds)
1618
class group_list(_pithos_group, _optional_json):
1619
    """list all groups and group members"""
1620

    
1621
    @errors.generic.all
1622
    @errors.pithos.connection
1623
    def _run(self):
1624
        self._print(self._groups(), self.print_dict)
1625

    
1626
    def main(self):
1627
        super(self.__class__, self)._run()
1628
        self._run()
1629

    
1630

    
1631
@command(group_cmds)
1632
class group_create(_pithos_group, _optional_json):
1633
    """Create a group of users"""
1634

    
1635
    arguments = dict(
1636
        user_uuid=RepeatableArgument('Add a user to the group', '--uuid'),
1637
        username=RepeatableArgument('Add a user to the group', '--username')
1638
    )
1639
    required = ['user_uuid', 'user_name']
1640

    
1641
    @errors.generic.all
1642
    @errors.pithos.connection
1643
    def _run(self, groupname, *users):
1644
        if groupname in self._groups() and not self.ask_user(
1645
                'Group %s already exists, overwrite?' % groupname):
1646
            self.error('Aborted')
1647
            return
1648
        self.client.set_account_group(groupname, users)
1649
        self._print(self._groups(), self.print_dict)
1650

    
1651
    def main(self, groupname):
1652
        super(self.__class__, self)._run()
1653
        users = self['user_uuid'] + self._usernames2uuids(
1654
            self['username']).values()
1655
        if users:
1656
            self._run(groupname, *users)
1657
        else:
1658
            raise CLISyntaxError(
1659
                'No valid users specified, use %s or %s' % (
1660
                    self.arguments['user_uuid'].lvalue,
1661
                    self.arguments['username'].lvalue),
1662
                details=[
1663
                    'Check if a username or uuid is valid with',
1664
                    '  user uuid2username', 'OR', '  user username2uuid'])
1665

    
1666

    
1667
@command(group_cmds)
1668
class group_delete(_pithos_group, _optional_json):
1669
    """Delete a user group"""
1670

    
1671
    @errors.generic.all
1672
    @errors.pithos.connection
1673
    def _run(self, groupname):
1674
        self.client.del_account_group(groupname)
1675
        self._print(self._groups(), self.print_dict)
1676

    
1677
    def main(self, groupname):
1678
        super(self.__class__, self)._run()
1679
        self._run(groupname)