Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / pithos.py @ 4e25b350

History | View | Annotate | Download (68.4 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_by_me=FlagArgument(
277
            'show only files shared to other users', '--shared-by-me'),
278
        public=FlagArgument('show only published objects', '--public'),
279
        more=FlagArgument('read long results', '--more'),
280
        enum=FlagArgument('Enumerate results', '--enumerate'),
281
        recursive=FlagArgument(
282
            'Recursively list containers and their contents',
283
            ('-R', '--recursive'))
284
    )
285

    
286
    @errors.generic.all
287
    @errors.pithos.connection
288
    @errors.pithos.container
289
    @errors.pithos.object_path
290
    def _run(self):
291
        r = self.client.container_get(
292
            limit=False if self['more'] else self['limit'],
293
            marker=self['marker'],
294
            prefix=self['name_pref'],
295
            delimiter=self['delimiter'],
296
            path=self.path or '',
297
            show_only_shared=self['shared_by_me'],
298
            public=self['public'],
299
            if_modified_since=self['if_modified_since'],
300
            if_unmodified_since=self['if_unmodified_since'],
301
            until=self['until'],
302
            meta=self['meta'])
303

    
304
        #  REMOVE THIS if version >> 0.12
305
        if not r.json:
306
            self.error('  NOTE: Since v0.12, use / for containers e.g.,')
307
            self.error('    [kamaki] file list /pithos')
308

    
309
        files = self._filter_by_name(r.json)
310
        if self['more']:
311
            outbu, self._out = self._out, StringIO()
312
        try:
313
            if self['json_output'] or self['output_format']:
314
                self._print(files)
315
            else:
316
                self.print_objects(files)
317
        finally:
318
            if self['more']:
319
                pager(self._out.getvalue())
320
                self._out = outbu
321

    
322
    def main(self, path_or_url=''):
323
        super(self.__class__, self)._run(path_or_url)
324
        self._run()
325

    
326

    
327
@command(file_cmds)
328
class file_modify(_pithos_container):
329
    """Modify the attributes of a file or directory object"""
330

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

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

    
385
    def main(self, path_or_url):
386
        super(self.__class__, self)._run(path_or_url)
387
        if self['publish'] and self['unpublish']:
388
            raise CLIInvalidArgument(
389
                'Arguments %s and %s cannot be used together' % (
390
                    self.arguments['publish'].lvalue,
391
                    self.arguments['publish'].lvalue))
392
        if self['no_permissions'] and (
393
                self['uuid_for_read_permission'] or self[
394
                    'uuid_for_write_permission']):
395
            raise CLIInvalidArgument(
396
                '%s cannot be used with other permission arguments' % (
397
                    self.arguments['no_permissions'].lvalue))
398
        self._run()
399

    
400

    
401
@command(file_cmds)
402
class file_create(_pithos_container, _optional_output_cmd):
403
    """Create an empty file"""
404

    
405
    arguments = dict(
406
        content_type=ValueArgument(
407
            'Set content type (default: application/octet-stream)',
408
            '--content-type',
409
            default='application/octet-stream')
410
    )
411

    
412
    @errors.generic.all
413
    @errors.pithos.connection
414
    @errors.pithos.container
415
    def _run(self):
416
        self._optional_output(
417
            self.client.create_object(self.path, self['content_type']))
418

    
419
    def main(self, path_or_url):
420
        super(self.__class__, self)._run(path_or_url)
421
        self._run()
422

    
423

    
424
@command(file_cmds)
425
class file_mkdir(_pithos_container, _optional_output_cmd):
426
    """Create a directory: /file create --content-type='applcation/directory'
427
    """
428

    
429
    @errors.generic.all
430
    @errors.pithos.connection
431
    @errors.pithos.container
432
    def _run(self):
433
        self._optional_output(self.client.create_directory(self.path))
434

    
435
    def main(self, path_or_url):
436
        super(self.__class__, self)._run(path_or_url)
437
        self._run()
438

    
439

    
440
@command(file_cmds)
441
class file_delete(_pithos_container):
442
    """Delete a file or directory object"""
443

    
444
    arguments = dict(
445
        until_date=DateArgument('remove history until then', '--until'),
446
        yes=FlagArgument('Do not prompt for permission', '--yes'),
447
        recursive=FlagArgument(
448
            'If a directory, empty first', ('-r', '--recursive')),
449
        delimiter=ValueArgument(
450
            'delete objects prefixed with <object><delimiter>', '--delimiter')
451
    )
452

    
453
    @errors.generic.all
454
    @errors.pithos.connection
455
    @errors.pithos.container
456
    @errors.pithos.object_path
457
    def _run(self):
458
        if self.path:
459
            if self['yes'] or self.ask_user(
460
                    'Delete /%s/%s ?' % (self.container, self.path)):
461
                self.client.del_object(
462
                    self.path,
463
                    until=self['until_date'],
464
                    delimiter='/' if self['recursive'] else self['delimiter'])
465
            else:
466
                self.error('Aborted')
467
        else:
468
            if self['yes'] or self.ask_user(
469
                    'Empty container /%s ?' % self.container):
470
                self.client.container_delete(self.container, delimiter='/')
471
            else:
472
                self.error('Aborted')
473

    
474
    def main(self, path_or_url):
475
        super(self.__class__, self)._run(path_or_url)
476
        self._run()
477

    
478

    
479
class _source_destination(_pithos_container, _optional_output_cmd):
480

    
481
    sd_arguments = dict(
482
        destination_user_uuid=ValueArgument(
483
            'default: current user uuid', '--to-account'),
484
        destination_container=ValueArgument(
485
            'default: pithos', '--to-container'),
486
        source_prefix=FlagArgument(
487
            'Transfer all files that are prefixed with SOURCE PATH If the '
488
            'destination path is specified, replace SOURCE_PATH with '
489
            'DESTINATION_PATH',
490
            ('-r', '--recursive')),
491
        force=FlagArgument(
492
            'Overwrite destination objects, if needed', ('-f', '--force')),
493
        source_version=ValueArgument(
494
            'The version of the source object', '--source-version')
495
    )
496

    
497
    def __init__(self, arguments={}, auth_base=None, cloud=None):
498
        self.arguments.update(arguments)
499
        self.arguments.update(self.sd_arguments)
500
        super(_source_destination, self).__init__(
501
            self.arguments, auth_base, cloud)
502

    
503
    def _report_transfer(self, src, dst, transfer_name):
504
        if not dst:
505
            if transfer_name in ('move', ):
506
                self.error('  delete source directory %s' % src)
507
            return
508
        dst_prf = '' if self.account == self.dst_client.account else (
509
                'pithos://%s' % self.dst_client.account)
510
        if src:
511
            src_prf = '' if self.account == self.dst_client.account else (
512
                    'pithos://%s' % self.account)
513
            self.error('  %s %s/%s/%s\n  -->  %s/%s/%s' % (
514
                transfer_name,
515
                src_prf, self.container, src,
516
                dst_prf, self.dst_client.container, dst))
517
        else:
518
            self.error('  mkdir %s/%s/%s' % (
519
                dst_prf, self.dst_client.container, dst))
520

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

    
622
    def _run(self, source_path_or_url, destination_path_or_url=''):
623
        super(_source_destination, self)._run(source_path_or_url)
624
        dst_acc, dst_con, dst_path = self._resolve_pithos_url(
625
            destination_path_or_url)
626
        self.dst_client = PithosClient(
627
            base_url=self.client.base_url, token=self.client.token,
628
            container=self[
629
                'destination_container'] or dst_con or self.client.container,
630
            account=self[
631
                'destination_user_uuid'] or dst_acc or self.account)
632
        self.dst_path = dst_path or self.path
633

    
634

    
635
@command(file_cmds)
636
class file_copy(_source_destination):
637
    """Copy objects, even between different accounts or containers"""
638

    
639
    arguments = dict(
640
        public=ValueArgument('publish new object', '--public'),
641
        content_type=ValueArgument(
642
            'change object\'s content type', '--content-type'),
643
        source_version=ValueArgument(
644
            'The version of the source object', '--object-version')
645
    )
646

    
647
    @errors.generic.all
648
    @errors.pithos.connection
649
    @errors.pithos.container
650
    @errors.pithos.account
651
    def _run(self):
652
        for src, dst in self._src_dst(self['source_version']):
653
            self._report_transfer(src, dst, 'copy')
654
            if src and dst:
655
                self.dst_client.copy_object(
656
                    src_container=self.client.container,
657
                    src_object=src,
658
                    dst_container=self.dst_client.container,
659
                    dst_object=dst,
660
                    source_account=self.client.account,
661
                    source_version=self['source_version'],
662
                    public=self['public'],
663
                    content_type=self['content_type'])
664
            elif dst:
665
                self.dst_client.create_directory(dst)
666

    
667
    def main(self, source_path_or_url, destination_path_or_url=None):
668
        super(file_copy, self)._run(
669
            source_path_or_url, destination_path_or_url or '')
670
        self._run()
671

    
672

    
673
@command(file_cmds)
674
class file_move(_source_destination):
675
    """Move objects, even between different accounts or containers"""
676

    
677
    arguments = dict(
678
        public=ValueArgument('publish new object', '--public'),
679
        content_type=ValueArgument(
680
            'change object\'s content type', '--content-type')
681
    )
682

    
683
    @errors.generic.all
684
    @errors.pithos.connection
685
    @errors.pithos.container
686
    @errors.pithos.account
687
    def _run(self):
688
        for src, dst in self._src_dst():
689
            self._report_transfer(src, dst, 'move')
690
            if src and dst:
691
                self.dst_client.move_object(
692
                    src_container=self.client.container,
693
                    src_object=src,
694
                    dst_container=self.dst_client.container,
695
                    dst_object=dst,
696
                    source_account=self.account,
697
                    public=self['public'],
698
                    content_type=self['content_type'])
699
            elif dst:
700
                self.dst_client.create_directory(dst)
701
            else:
702
                self.client.del_object(src)
703

    
704
    def main(self, source_path_or_url, destination_path_or_url=None):
705
        super(file_move, self)._run(
706
            source_path_or_url, destination_path_or_url or '')
707
        self._run()
708

    
709

    
710
@command(file_cmds)
711
class file_append(_pithos_container, _optional_output_cmd):
712
    """Append local file to (existing) remote object
713
    The remote object should exist.
714
    If the remote object is a directory, it is transformed into a file.
715
    In the later case, objects under the directory remain intact.
716
    """
717

    
718
    arguments = dict(
719
        progress_bar=ProgressBarArgument(
720
            'do not show progress bar', ('-N', '--no-progress-bar'),
721
            default=False),
722
        max_threads=IntArgument('default: 1', '--threads'),
723
    )
724

    
725
    @errors.generic.all
726
    @errors.pithos.connection
727
    @errors.pithos.container
728
    @errors.pithos.object_path
729
    def _run(self, local_path):
730
        if self['max_threads'] > 0:
731
            self.client.MAX_THREADS = int(self['max_threads'])
732
        (progress_bar, upload_cb) = self._safe_progress_bar('Appending')
733
        try:
734
            with open(local_path, 'rb') as f:
735
                self._optional_output(
736
                    self.client.append_object(self.path, f, upload_cb))
737
        finally:
738
            self._safe_progress_bar_finish(progress_bar)
739

    
740
    def main(self, local_path, remote_path_or_url):
741
        super(self.__class__, self)._run(remote_path_or_url)
742
        self._run(local_path)
743

    
744

    
745
@command(file_cmds)
746
class file_truncate(_pithos_container, _optional_output_cmd):
747
    """Truncate remote file up to size"""
748

    
749
    arguments = dict(
750
        size_in_bytes=IntArgument('Length of file after truncation', '--size')
751
    )
752
    required = ('size_in_bytes', )
753

    
754
    @errors.generic.all
755
    @errors.pithos.connection
756
    @errors.pithos.container
757
    @errors.pithos.object_path
758
    @errors.pithos.object_size
759
    def _run(self, size):
760
        self._optional_output(self.client.truncate_object(self.path, size))
761

    
762
    def main(self, path_or_url):
763
        super(self.__class__, self)._run(path_or_url)
764
        self._run(size=self['size_in_bytes'])
765

    
766

    
767
@command(file_cmds)
768
class file_overwrite(_pithos_container, _optional_output_cmd):
769
    """Overwrite part of a remote file"""
770

    
771
    arguments = dict(
772
        progress_bar=ProgressBarArgument(
773
            'do not show progress bar', ('-N', '--no-progress-bar'),
774
            default=False),
775
        start_position=IntArgument('File position in bytes', '--from'),
776
        end_position=IntArgument('File position in bytes', '--to')
777
    )
778
    required = ('start_position', 'end_position')
779

    
780
    @errors.generic.all
781
    @errors.pithos.connection
782
    @errors.pithos.container
783
    @errors.pithos.object_path
784
    @errors.pithos.object_size
785
    def _run(self, local_path, start, end):
786
        start, end = int(start), int(end)
787
        (progress_bar, upload_cb) = self._safe_progress_bar(
788
            'Overwrite %s bytes' % (end - start))
789
        try:
790
            with open(path.abspath(local_path), 'rb') as f:
791
                self._optional_output(self.client.overwrite_object(
792
                    obj=self.path,
793
                    start=start,
794
                    end=end,
795
                    source_file=f,
796
                    upload_cb=upload_cb))
797
        finally:
798
            self._safe_progress_bar_finish(progress_bar)
799

    
800
    def main(self, local_path, path_or_url):
801
        super(self.__class__, self)._run(path_or_url)
802
        self.path = self.path or path.basename(local_path)
803
        self._run(
804
            local_path=local_path,
805
            start=self['start_position'],
806
            end=self['end_position'])
807

    
808

    
809
@command(file_cmds)
810
class file_upload(_pithos_container, _optional_output_cmd):
811
    """Upload a file"""
812

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

    
845
    def _sharing(self):
846
        sharing = dict()
847
        readlist = self['uuid_for_read_permission']
848
        if readlist:
849
            sharing['read'] = self['uuid_for_read_permission']
850
        writelist = self['uuid_for_write_permission']
851
        if writelist:
852
            sharing['write'] = self['uuid_for_write_permission']
853
        return sharing or None
854

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

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

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

    
998
    def main(self, local_path, remote_path_or_url):
999
        super(self.__class__, self)._run(remote_path_or_url)
1000
        remote_path = self.path or path.basename(path.abspath(local_path))
1001
        self._run(local_path=local_path, remote_path=remote_path)
1002

    
1003

    
1004
class RangeArgument(ValueArgument):
1005
    """
1006
    :value type: string of the form <start>-<end> where <start> and <end> are
1007
        integers
1008
    :value returns: the input string, after type checking <start> and <end>
1009
    """
1010

    
1011
    @property
1012
    def value(self):
1013
        return getattr(self, '_value', self.default)
1014

    
1015
    @value.setter
1016
    def value(self, newvalues):
1017
        if newvalues:
1018
            self._value = getattr(self, '_value', self.default)
1019
            for newvalue in newvalues.split(','):
1020
                self._value = ('%s,' % self._value) if self._value else ''
1021
                start, sep, end = newvalue.partition('-')
1022
                if sep:
1023
                    if start:
1024
                        start, end = (int(start), int(end))
1025
                        if start > end:
1026
                            raise CLIInvalidArgument(
1027
                                'Invalid range %s' % newvalue, details=[
1028
                                'Valid range formats',
1029
                                '  START-END', '  UP_TO', '  -FROM',
1030
                                'where all values are integers',
1031
                                'OR a compination (csv), e.g.,',
1032
                                '  %s=5,10-20,-5' % self.lvalue])
1033
                        self._value += '%s-%s' % (start, end)
1034
                    else:
1035
                        self._value += '-%s' % int(end)
1036
                else:
1037
                    self._value += '%s' % int(start)
1038

    
1039

    
1040
@command(file_cmds)
1041
class file_cat(_pithos_container):
1042
    """Fetch remote file contents"""
1043

    
1044
    arguments = dict(
1045
        range=RangeArgument('show range of data e.g., 5,10-20,-5', '--range'),
1046
        if_match=ValueArgument('show output if ETags match', '--if-match'),
1047
        if_none_match=ValueArgument(
1048
            'show output if ETags match', '--if-none-match'),
1049
        if_modified_since=DateArgument(
1050
            'show output modified since then', '--if-modified-since'),
1051
        if_unmodified_since=DateArgument(
1052
            'show output unmodified since then', '--if-unmodified-since'),
1053
        object_version=ValueArgument(
1054
            'Get contents of the chosen version', '--object-version')
1055
    )
1056

    
1057
    @errors.generic.all
1058
    @errors.pithos.connection
1059
    @errors.pithos.container
1060
    @errors.pithos.object_path
1061
    def _run(self):
1062
        r = self.client.download_object(
1063
            self.path, self._out,
1064
            range_str=self['range'],
1065
            version=self['object_version'],
1066
            if_match=self['if_match'],
1067
            if_none_match=self['if_none_match'],
1068
            if_modified_since=self['if_modified_since'],
1069
            if_unmodified_since=self['if_unmodified_since'])
1070
        print r
1071

    
1072
    def main(self, path_or_url):
1073
        super(self.__class__, self)._run(path_or_url)
1074
        self._run()
1075

    
1076

    
1077
@command(file_cmds)
1078
class file_download(_pithos_container):
1079
    """Download a remove file or directory object to local file system"""
1080

    
1081
    arguments = dict(
1082
        resume=FlagArgument(
1083
            'Resume/Overwrite (attempt resume, else overwrite)',
1084
            ('-f', '--resume')),
1085
        range=RangeArgument(
1086
            'Download only that range of data e.g., 5,10-20,-5', '--range'),
1087
        matching_etag=ValueArgument('download iff ETag match', '--if-match'),
1088
        non_matching_etag=ValueArgument(
1089
            'download iff ETags DO NOT match', '--if-none-match'),
1090
        modified_since_date=DateArgument(
1091
            'download iff remote file is modified since then',
1092
            '--if-modified-since'),
1093
        unmodified_since_date=DateArgument(
1094
            'show output iff remote file is unmodified since then',
1095
            '--if-unmodified-since'),
1096
        object_version=ValueArgument(
1097
            'download a file of a specific version', '--object-version'),
1098
        max_threads=IntArgument('default: 5', '--threads'),
1099
        progress_bar=ProgressBarArgument(
1100
            'do not show progress bar', ('-N', '--no-progress-bar'),
1101
            default=False),
1102
        recursive=FlagArgument(
1103
            'Download a remote directory object and its contents',
1104
            ('-r', '--recursive'))
1105
        )
1106

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

    
1143
        if (not obj) or self._is_dir(obj):
1144
            if self['recursive']:
1145
                if not (self.path or local_path.endswith('/')):
1146
                    #  Download the whole container
1147
                    local_path = '' if local_path in ('.', ) else local_path
1148
                    local_path = '%s/' % (local_path or self.container)
1149
                obj = obj or dict(
1150
                    name='', content_type='application/directory')
1151
                dirs, files = [obj, ], []
1152
                objects = self.client.container_get(
1153
                    path=self.path,
1154
                    if_modified_since=self['modified_since_date'],
1155
                    if_unmodified_since=self['unmodified_since_date'])
1156
                for o in objects.json:
1157
                    (dirs if self._is_dir(o) else files).append(o)
1158

    
1159
                #  Put the directories on top of the list
1160
                for dpath in sorted(['%s%s' % (
1161
                        local_path, d['name'][len(rpath):]) for d in dirs]):
1162
                    if path.exists(dpath):
1163
                        if path.isdir(dpath):
1164
                            continue
1165
                        raise CLIError(
1166
                            'Cannot replace local file %s with a directory '
1167
                            'of the same name' % dpath,
1168
                            details=[
1169
                                'Either remove the file or specify a'
1170
                                'different target location'])
1171
                    ret.append((None, dpath, None))
1172

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

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

    
1275
    def main(self, remote_path_or_url, local_path=None):
1276
        super(self.__class__, self)._run(remote_path_or_url)
1277
        local_path = local_path or self.path or '.'
1278
        self._run(local_path=local_path)
1279

    
1280

    
1281
@command(container_cmds)
1282
class container_info(_pithos_account, _optional_json):
1283
    """Get information about a container"""
1284

    
1285
    arguments = dict(
1286
        until_date=DateArgument('show metadata until then', '--until'),
1287
        metadata=FlagArgument('Show only container metadata', '--metadata'),
1288
        sizelimit=FlagArgument(
1289
            'Show the maximum size limit for container', '--size-limit'),
1290
        in_bytes=FlagArgument('Show size limit in bytes', ('-b', '--bytes'))
1291
    )
1292

    
1293
    @errors.generic.all
1294
    @errors.pithos.connection
1295
    @errors.pithos.container
1296
    @errors.pithos.object_path
1297
    def _run(self):
1298
        if self['metadata']:
1299
            r, preflen = dict(), len('x-container-meta-')
1300
            for k, v in self.client.get_container_meta(
1301
                    until=self['until_date']).items():
1302
                r[k[preflen:]] = v
1303
        elif self['sizelimit']:
1304
            r = self.client.get_container_limit(
1305
                self.container)['x-container-policy-quota']
1306
            r = {'size limit': 'unlimited' if r in ('0', ) else (
1307
                int(r) if self['in_bytes'] else format_size(r))}
1308
        else:
1309
            r = self.client.get_container_info(self.container)
1310
        self._print(r, self.print_dict)
1311

    
1312
    def main(self, container):
1313
        super(self.__class__, self)._run()
1314
        self.container, self.client.container = container, container
1315
        self._run()
1316

    
1317

    
1318
class VersioningArgument(ValueArgument):
1319

    
1320
    schemes = ('auto', 'none')
1321

    
1322
    @property
1323
    def value(self):
1324
        return getattr(self, '_value', None)
1325

    
1326
    @value.setter
1327
    def value(self, new_scheme):
1328
        if new_scheme:
1329
            new_scheme = new_scheme.lower()
1330
            if new_scheme not in self.schemes:
1331
                raise CLIInvalidArgument('Invalid versioning value', details=[
1332
                    'Valid versioning values are %s' % ', '.join(
1333
                        self.schemes)])
1334
            self._value = new_scheme
1335

    
1336

    
1337
@command(container_cmds)
1338
class container_modify(_pithos_account, _optional_json):
1339
    """Modify the properties of a container"""
1340

    
1341
    arguments = dict(
1342
        metadata_to_add=KeyValueArgument(
1343
            'Add metadata in the form KEY=VALUE (can be repeated)',
1344
            '--metadata-add'),
1345
        metadata_to_delete=RepeatableArgument(
1346
            'Delete metadata by KEY (can be repeated)', '--metadata-del'),
1347
        sizelimit=DataSizeArgument(
1348
            'Set max size limit (0 for unlimited, '
1349
            'use units B, KiB, KB, etc.)', '--size-limit'),
1350
        versioning=VersioningArgument(
1351
            'Set a versioning scheme (%s)' % ', '.join(
1352
                VersioningArgument.schemes), '--versioning')
1353
    )
1354
    required = [
1355
        'metadata_to_add', 'metadata_to_delete', 'sizelimit', 'versioning']
1356

    
1357
    @errors.generic.all
1358
    @errors.pithos.connection
1359
    @errors.pithos.container
1360
    def _run(self, container):
1361
        metadata = self['metadata_to_add']
1362
        for k in (self['metadata_to_delete'] or []):
1363
            metadata[k] = ''
1364
        if metadata:
1365
            self.client.set_container_meta(metadata)
1366
            self._print(self.client.get_container_meta(), self.print_dict)
1367
        if self['sizelimit'] is not None:
1368
            self.client.set_container_limit(self['sizelimit'])
1369
            r = self.client.get_container_limit()['x-container-policy-quota']
1370
            r = 'unlimited' if r in ('0', ) else format_size(r)
1371
            self.writeln('new size limit: %s' % r)
1372
        if self['versioning']:
1373
            self.client.set_container_versioning(self['versioning'])
1374
            self.writeln('new versioning scheme: %s' % (
1375
                self.client.get_container_versioning(self.container)[
1376
                    'x-container-policy-versioning']))
1377

    
1378
    def main(self, container):
1379
        super(self.__class__, self)._run()
1380
        self.client.container, self.container = container, container
1381
        self._run(container=container)
1382

    
1383

    
1384
@command(container_cmds)
1385
class container_list(_pithos_account, _optional_json, _name_filter):
1386
    """List all containers, or their contents"""
1387

    
1388
    arguments = dict(
1389
        detail=FlagArgument('Containers with details', ('-l', '--list')),
1390
        limit=IntArgument('limit number of listed items', ('-n', '--number')),
1391
        marker=ValueArgument('output greater that marker', '--marker'),
1392
        modified_since_date=ValueArgument(
1393
            'show output modified since then', '--if-modified-since'),
1394
        unmodified_since_date=ValueArgument(
1395
            'show output not modified since then', '--if-unmodified-since'),
1396
        until_date=DateArgument('show metadata until then', '--until'),
1397
        shared=FlagArgument('show only shared', '--shared'),
1398
        more=FlagArgument('read long results', '--more'),
1399
        enum=FlagArgument('Enumerate results', '--enumerate'),
1400
        recursive=FlagArgument(
1401
            'Recursively list containers and their contents',
1402
            ('-r', '--recursive')),
1403
        shared_by_me=FlagArgument(
1404
            'show only files shared to other users', '--shared-by-me'),
1405
        public=FlagArgument('show only published objects', '--public'),
1406
    )
1407

    
1408
    def print_containers(self, container_list):
1409
        for index, container in enumerate(container_list):
1410
            if 'bytes' in container:
1411
                size = format_size(container['bytes'])
1412
            prfx = ('%s. ' % (index + 1)) if self['enum'] else ''
1413
            _cname = container['name'] if (
1414
                self['more']) else bold(container['name'])
1415
            cname = u'%s%s' % (prfx, _cname)
1416
            if self['detail']:
1417
                self.writeln(cname)
1418
                pretty_c = container.copy()
1419
                if 'bytes' in container:
1420
                    pretty_c['bytes'] = '%s (%s)' % (container['bytes'], size)
1421
                self.print_dict(pretty_c, exclude=('name'))
1422
                self.writeln()
1423
            else:
1424
                if 'count' in container and 'bytes' in container:
1425
                    self.writeln('%s (%s, %s objects)' % (
1426
                        cname, size, container['count']))
1427
                else:
1428
                    self.writeln(cname)
1429
            objects = container.get('objects', [])
1430
            if objects:
1431
                self.print_objects(objects)
1432
                self.writeln('')
1433

    
1434
    def _create_object_forest(self, container_list):
1435
        try:
1436
            for container in container_list:
1437
                self.client.container = container['name']
1438
                objects = self.client.container_get(
1439
                    limit=False if self['more'] else self['limit'],
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_by_me'],
1444
                    public=self['public'])
1445
                container['objects'] = objects.json
1446
        finally:
1447
            self.client.container = None
1448

    
1449
    @errors.generic.all
1450
    @errors.pithos.connection
1451
    @errors.pithos.object_path
1452
    @errors.pithos.container
1453
    def _run(self, container):
1454
        if container:
1455
            r = self.client.container_get(
1456
                limit=False if self['more'] else self['limit'],
1457
                marker=self['marker'],
1458
                if_modified_since=self['modified_since_date'],
1459
                if_unmodified_since=self['unmodified_since_date'],
1460
                until=self['until_date'],
1461
                show_only_shared=self['shared_by_me'],
1462
                public=self['public'])
1463
        else:
1464
            r = self.client.account_get(
1465
                limit=False if self['more'] else self['limit'],
1466
                marker=self['marker'],
1467
                if_modified_since=self['modified_since_date'],
1468
                if_unmodified_since=self['unmodified_since_date'],
1469
                until=self['until_date'],
1470
                show_only_shared=self['shared_by_me'],
1471
                public=self['public'])
1472
        files = self._filter_by_name(r.json)
1473
        if self['recursive'] and not container:
1474
            self._create_object_forest(files)
1475
        if self['more']:
1476
            outbu, self._out = self._out, StringIO()
1477
        try:
1478
            if self['json_output'] or self['output_format']:
1479
                self._print(files)
1480
            else:
1481
                (self.print_objects if container else self.print_containers)(
1482
                    files)
1483
        finally:
1484
            if self['more']:
1485
                pager(self._out.getvalue())
1486
                self._out = outbu
1487

    
1488
    def main(self, container=None):
1489
        super(self.__class__, self)._run()
1490
        self.client.container, self.container = container, container
1491
        self._run(container)
1492

    
1493

    
1494
@command(container_cmds)
1495
class container_create(_pithos_account):
1496
    """Create a new container"""
1497

    
1498
    arguments = dict(
1499
        versioning=ValueArgument(
1500
            'set container versioning (auto/none)', '--versioning'),
1501
        limit=IntArgument('set default container limit', '--limit'),
1502
        meta=KeyValueArgument(
1503
            'set container metadata (can be repeated)', '--meta')
1504
    )
1505

    
1506
    @errors.generic.all
1507
    @errors.pithos.connection
1508
    @errors.pithos.container
1509
    def _run(self, container):
1510
        try:
1511
            self.client.create_container(
1512
                container=container,
1513
                sizelimit=self['limit'],
1514
                versioning=self['versioning'],
1515
                metadata=self['meta'],
1516
                success=(201, ))
1517
        except ClientError as ce:
1518
            if ce.status in (202, ):
1519
                raise CLIError(
1520
                    'Container %s alread exists' % container, details=[
1521
                    'Either delete %s or choose another name' % (container)])
1522
            raise
1523

    
1524
    def main(self, new_container):
1525
        super(self.__class__, self)._run()
1526
        self._run(container=new_container)
1527

    
1528

    
1529
@command(container_cmds)
1530
class container_delete(_pithos_account):
1531
    """Delete a container"""
1532

    
1533
    arguments = dict(
1534
        yes=FlagArgument('Do not prompt for permission', '--yes'),
1535
        recursive=FlagArgument(
1536
            'delete container even if not empty', ('-r', '--recursive'))
1537
    )
1538

    
1539
    @errors.generic.all
1540
    @errors.pithos.connection
1541
    @errors.pithos.container
1542
    def _run(self, container):
1543
        num_of_contents = int(self.client.get_container_info(container)[
1544
            'x-container-object-count'])
1545
        delimiter, msg = None, 'Delete container %s ?' % container
1546
        if self['recursive']:
1547
            delimiter, msg = '/', 'Empty and d%s' % msg[1:]
1548
        elif num_of_contents:
1549
            raise CLIError('Container %s is not empty' % container, details=[
1550
                'Use %s to delete non-empty containers' % (
1551
                    self.arguments['recursive'].lvalue)])
1552
        if self['yes'] or self.ask_user(msg):
1553
            if num_of_contents:
1554
                self.client.del_container(delimiter=delimiter)
1555
            self.client.purge_container()
1556

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

    
1562

    
1563
@command(container_cmds)
1564
class container_empty(_pithos_account):
1565
    """Empty a container"""
1566

    
1567
    arguments = dict(yes=FlagArgument('Do not prompt for permission', '--yes'))
1568

    
1569
    @errors.generic.all
1570
    @errors.pithos.connection
1571
    @errors.pithos.container
1572
    def _run(self, container):
1573
        if self['yes'] or self.ask_user('Empty container %s ?' % container):
1574
            self.client.del_container(delimiter='/')
1575

    
1576
    def main(self, container):
1577
        super(self.__class__, self)._run()
1578
        self.container, self.client.container = container, container
1579
        self._run(container)
1580

    
1581

    
1582
@command(sharer_cmds)
1583
class sharer_list(_pithos_account, _optional_json):
1584
    """List accounts who share file objects with current user"""
1585

    
1586
    arguments = dict(
1587
        detail=FlagArgument('show detailed output', ('-l', '--details')),
1588
        marker=ValueArgument('show output greater then marker', '--marker')
1589
    )
1590

    
1591
    @errors.generic.all
1592
    @errors.pithos.connection
1593
    def _run(self):
1594
        accounts = self.client.get_sharing_accounts(marker=self['marker'])
1595
        if not (self['json_output'] or self['output_format']):
1596
            usernames = self._uuids2usernames(
1597
                [acc['name'] for acc in accounts])
1598
            for item in accounts:
1599
                uuid = item['name']
1600
                item['id'], item['name'] = uuid, usernames[uuid]
1601
                if not self['detail']:
1602
                    item.pop('last_modified')
1603
        self._print(accounts)
1604

    
1605
    def main(self):
1606
        super(self.__class__, self)._run()
1607
        self._run()
1608

    
1609

    
1610
@command(sharer_cmds)
1611
class sharer_info(_pithos_account, _optional_json):
1612
    """Details on a Pithos+ sharer account (default: current account)"""
1613

    
1614
    @errors.generic.all
1615
    @errors.pithos.connection
1616
    def _run(self):
1617
        self._print(self.client.get_account_info(), self.print_dict)
1618

    
1619
    def main(self, account_uuid=None):
1620
        super(self.__class__, self)._run()
1621
        if account_uuid:
1622
            self.client.account, self.account = account_uuid, account_uuid
1623
        self._run()
1624

    
1625

    
1626
class _pithos_group(_pithos_account):
1627
    prefix = 'x-account-group-'
1628
    preflen = len(prefix)
1629

    
1630
    def _groups(self):
1631
        groups = dict()
1632
        for k, v in self.client.get_account_group().items():
1633
            groups[k[self.preflen:]] = v
1634
        return groups
1635

    
1636

    
1637
@command(group_cmds)
1638
class group_list(_pithos_group, _optional_json):
1639
    """list all groups and group members"""
1640

    
1641
    @errors.generic.all
1642
    @errors.pithos.connection
1643
    def _run(self):
1644
        self._print(self._groups(), self.print_dict)
1645

    
1646
    def main(self):
1647
        super(self.__class__, self)._run()
1648
        self._run()
1649

    
1650

    
1651
@command(group_cmds)
1652
class group_create(_pithos_group, _optional_json):
1653
    """Create a group of users"""
1654

    
1655
    arguments = dict(
1656
        user_uuid=RepeatableArgument('Add a user to the group', '--uuid'),
1657
        username=RepeatableArgument('Add a user to the group', '--username')
1658
    )
1659
    required = ['user_uuid', 'user_name']
1660

    
1661
    @errors.generic.all
1662
    @errors.pithos.connection
1663
    def _run(self, groupname, *users):
1664
        if groupname in self._groups() and not self.ask_user(
1665
                'Group %s already exists, overwrite?' % groupname):
1666
            self.error('Aborted')
1667
            return
1668
        self.client.set_account_group(groupname, users)
1669
        self._print(self._groups(), self.print_dict)
1670

    
1671
    def main(self, groupname):
1672
        super(self.__class__, self)._run()
1673
        users = (self['user_uuid'] or []) + self._usernames2uuids(
1674
            self['username'] or []).values()
1675
        if users:
1676
            self._run(groupname, *users)
1677
        else:
1678
            raise CLISyntaxError(
1679
                'No valid users specified, use %s or %s' % (
1680
                    self.arguments['user_uuid'].lvalue,
1681
                    self.arguments['username'].lvalue),
1682
                details=[
1683
                    'Check if a username or uuid is valid with',
1684
                    '  user uuid2username', 'OR', '  user username2uuid'])
1685

    
1686

    
1687
@command(group_cmds)
1688
class group_delete(_pithos_group, _optional_json):
1689
    """Delete a user group"""
1690

    
1691
    @errors.generic.all
1692
    @errors.pithos.connection
1693
    def _run(self, groupname):
1694
        self.client.del_account_group(groupname)
1695
        self._print(self._groups(), self.print_dict)
1696

    
1697
    def main(self, groupname):
1698
        super(self.__class__, self)._run()
1699
        self._run(groupname)
1700

    
1701

    
1702
#  Deprecated commands
1703

    
1704
@command(file_cmds)
1705
class file_publish(_pithos_init):
1706
    """DEPRECATED, replaced by [kamaki] file modify OBJECT --publish"""
1707

    
1708
    def main(self, *args):
1709
        raise CLISyntaxError('DEPRECATED', details=[
1710
            'This command is replaced by:',
1711
            '  [kamaki] file modify OBJECT --publish'])
1712

    
1713

    
1714
@command(file_cmds)
1715
class file_unpublish(_pithos_init):
1716
    """DEPRECATED, replaced by [kamaki] file modify OBJECT --unpublish"""
1717

    
1718
    def main(self, *args):
1719
        raise CLISyntaxError('DEPRECATED', details=[
1720
            'This command is replaced by:',
1721
            '  [kamaki] file modify OBJECT --unpublish'])