Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / pithos.py @ 9c25c741

History | View | Annotate | Download (70.3 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
    UserAccountArgument)
53
from kamaki.cli.utils import (
54
    format_size, bold, get_path_size, guess_mime_type)
55

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

    
63

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

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

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

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

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

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

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

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

    
117

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

    
121
    def __init__(self, arguments={}, auth_base=None, cloud=None):
122
        super(_pithos_account, self).__init__(arguments, auth_base, cloud)
123
        self['account'] = UserAccountArgument(
124
            'A user UUID or name', ('-A', '--account'))
125
        self.arguments['account'].account_client = auth_base
126

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

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

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

    
160

    
161
class _pithos_container(_pithos_account):
162
    """Setup container"""
163

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

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

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

    
193

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

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

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

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

    
254
    def main(self, path_or_url):
255
        super(self.__class__, self)._run(path_or_url)
256
        self._run()
257

    
258

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

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

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

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

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

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

    
328

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

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

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

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

    
394

    
395
@command(file_cmds)
396
class file_publish(_pithos_container):
397
    """Publish an object (creates a public URL)"""
398

    
399
    @errors.generic.all
400
    @errors.pithos.connection
401
    @errors.pithos.container
402
    @errors.pithos.object_path
403
    def _run(self):
404
        self.writeln(self.client.publish_object(self.path))
405

    
406
    def main(self, path_or_url):
407
        super(self.__class__, self)._run(path_or_url)
408
        self._run()
409

    
410

    
411
@command(file_cmds)
412
class file_unpublish(_pithos_container):
413
    """Unpublish an object"""
414

    
415
    @errors.generic.all
416
    @errors.pithos.connection
417
    @errors.pithos.container
418
    @errors.pithos.object_path
419
    def _run(self):
420
        self.client.unpublish_object(self.path)
421

    
422
    def main(self, path_or_url):
423
        super(self.__class__, self)._run(path_or_url)
424
        self._run()
425

    
426

    
427
def _assert_path(self, path_or_url):
428
    if not self.path:
429
        raiseCLIError(
430
            'Directory path is missing in location %s' % path_or_url,
431
            details=['Location format:    [[pithos://UUID]/CONTAINER/]PATH'])
432

    
433

    
434
@command(file_cmds)
435
class file_create(_pithos_container, _optional_output_cmd):
436
    """Create an empty file"""
437

    
438
    arguments = dict(
439
        content_type=ValueArgument(
440
            'Set content type (default: application/octet-stream)',
441
            '--content-type',
442
            default='application/octet-stream')
443
    )
444

    
445
    @errors.generic.all
446
    @errors.pithos.connection
447
    @errors.pithos.container
448
    def _run(self):
449
        self._optional_output(
450
            self.client.create_object(self.path, self['content_type']))
451

    
452
    def main(self, path_or_url):
453
        super(self.__class__, self)._run(path_or_url)
454
        _assert_path(self, path_or_url)
455
        self._run()
456

    
457

    
458
@command(file_cmds)
459
class file_mkdir(_pithos_container, _optional_output_cmd):
460
    """Create a directory: /file create --content-type='application/directory'
461
    """
462

    
463
    @errors.generic.all
464
    @errors.pithos.connection
465
    @errors.pithos.container
466
    def _run(self, path):
467
        self._optional_output(self.client.create_directory(self.path))
468

    
469
    def main(self, path_or_url):
470
        super(self.__class__, self)._run(path_or_url)
471
        _assert_path(self, path_or_url)
472
        self._run(self.path)
473

    
474

    
475
@command(file_cmds)
476
class file_delete(_pithos_container):
477
    """Delete a file or directory object"""
478

    
479
    arguments = dict(
480
        until_date=DateArgument('remove history until then', '--until'),
481
        yes=FlagArgument('Do not prompt for permission', '--yes'),
482
        recursive=FlagArgument(
483
            'If a directory, empty first', ('-r', '--recursive')),
484
        delimiter=ValueArgument(
485
            'delete objects prefixed with <object><delimiter>', '--delimiter')
486
    )
487

    
488
    @errors.generic.all
489
    @errors.pithos.connection
490
    @errors.pithos.container
491
    @errors.pithos.object_path
492
    def _run(self):
493
        if self.path:
494
            if self['yes'] or self.ask_user(
495
                    'Delete /%s/%s ?' % (self.container, self.path)):
496
                self.client.del_object(
497
                    self.path,
498
                    until=self['until_date'],
499
                    delimiter='/' if self['recursive'] else self['delimiter'])
500
            else:
501
                self.error('Aborted')
502
        else:
503
            if self['yes'] or self.ask_user(
504
                    'Empty container /%s ?' % self.container):
505
                self.client.container_delete(self.container, delimiter='/')
506
            else:
507
                self.error('Aborted')
508

    
509
    def main(self, path_or_url):
510
        super(self.__class__, self)._run(path_or_url)
511
        self._run()
512

    
513

    
514
class _source_destination(_pithos_container, _optional_output_cmd):
515

    
516
    sd_arguments = dict(
517
        destination_user=UserAccountArgument(
518
            'UUID or username, default: current user', '--to-account'),
519
        destination_container=ValueArgument(
520
            'default: pithos', '--to-container'),
521
        source_prefix=FlagArgument(
522
            'Transfer all files that are prefixed with SOURCE PATH If the '
523
            'destination path is specified, replace SOURCE_PATH with '
524
            'DESTINATION_PATH',
525
            ('-r', '--recursive')),
526
        force=FlagArgument(
527
            'Overwrite destination objects, if needed', ('-f', '--force')),
528
        source_version=ValueArgument(
529
            'The version of the source object', '--source-version')
530
    )
531

    
532
    def __init__(self, arguments={}, auth_base=None, cloud=None):
533
        self.arguments.update(arguments)
534
        self.arguments.update(self.sd_arguments)
535
        super(_source_destination, self).__init__(
536
            self.arguments, auth_base, cloud)
537
        self.arguments['destination_user'].account_client = self.auth_base
538

    
539
    def _report_transfer(self, src, dst, transfer_name):
540
        if not dst:
541
            if transfer_name in ('move', ):
542
                self.error('  delete source directory %s' % src)
543
            return
544
        dst_prf = '' if self.account == self.dst_client.account else (
545
                'pithos://%s' % self.dst_client.account)
546
        if src:
547
            src_prf = '' if self.account == self.dst_client.account else (
548
                    'pithos://%s' % self.account)
549
            self.error('  %s %s/%s/%s\n  -->  %s/%s/%s' % (
550
                transfer_name,
551
                src_prf, self.container, src,
552
                dst_prf, self.dst_client.container, dst))
553
        else:
554
            self.error('  mkdir %s/%s/%s' % (
555
                dst_prf, self.dst_client.container, dst))
556

    
557
    @errors.generic.all
558
    @errors.pithos.account
559
    def _src_dst(self, version=None):
560
        """Preconditions:
561
        self.account, self.container, self.path
562
        self.dst_acc, self.dst_con, self.dst_path
563
        They should all be configured properly
564
        :returns: [(src_path, dst_path), ...], if src_path is None, create
565
            destination directory
566
        """
567
        src_objects, dst_objects, pairs = dict(), dict(), []
568
        try:
569
            for obj in self.dst_client.list_objects(
570
                    prefix=self.dst_path or self.path or '/'):
571
                dst_objects[obj['name']] = obj
572
        except ClientError as ce:
573
            if ce.status in (404, ):
574
                raise CLIError(
575
                    'Destination container pithos://%s/%s not found' % (
576
                        self.dst_client.account, self.dst_client.container))
577
            raise ce
578
        if self['source_prefix']:
579
            #  Copy and replace prefixes
580
            for src_obj in self.client.list_objects(prefix=self.path):
581
                src_objects[src_obj['name']] = src_obj
582
            for src_path, src_obj in src_objects.items():
583
                dst_path = '%s%s' % (
584
                    self.dst_path or self.path, src_path[len(self.path):])
585
                dst_obj = dst_objects.get(dst_path, None)
586
                if self['force'] or not dst_obj:
587
                    #  Just do it
588
                    pairs.append((
589
                        None if self._is_dir(src_obj) else src_path, dst_path))
590
                    if self._is_dir(src_obj):
591
                        pairs.append((self.path or dst_path, None))
592
                elif not (self._is_dir(dst_obj) and self._is_dir(src_obj)):
593
                    raise CLIError(
594
                        'Destination object exists', importance=2, details=[
595
                            'Failed while transfering:',
596
                            '    pithos://%s/%s/%s' % (
597
                                    self.account,
598
                                    self.container,
599
                                    src_path),
600
                            '--> pithos://%s/%s/%s' % (
601
                                    self.dst_client.account,
602
                                    self.dst_client.container,
603
                                    dst_path),
604
                            'Use %s to transfer overwrite' % (
605
                                    self.arguments['force'].lvalue)])
606
        else:
607
            #  One object transfer
608
            try:
609
                src_version_arg = self.arguments.get('source_version', None)
610
                src_obj = self.client.get_object_info(
611
                    self.path,
612
                    version=src_version_arg.value if src_version_arg else None)
613
            except ClientError as ce:
614
                if ce.status in (204, ):
615
                    raise CLIError(
616
                        'Missing specific path container %s' % self.container,
617
                        importance=2, details=[
618
                            'To transfer container contents %s' % (
619
                                self.arguments['source_prefix'].lvalue)])
620
                raise
621
            dst_path = self.dst_path or self.path
622
            dst_obj = dst_objects.get(dst_path or self.path, None)
623
            if self['force'] or not dst_obj:
624
                pairs.append(
625
                    (None if self._is_dir(src_obj) else self.path, dst_path))
626
                if self._is_dir(src_obj):
627
                    pairs.append((self.path or dst_path, None))
628
            elif self._is_dir(src_obj):
629
                raise CLIError(
630
                    'Cannot transfer an application/directory object',
631
                    importance=2, details=[
632
                        'The object pithos://%s/%s/%s is a directory' % (
633
                            self.account,
634
                            self.container,
635
                            self.path),
636
                        'To recursively copy a directory, use',
637
                        '  %s' % self.arguments['source_prefix'].lvalue,
638
                        'To create a file, use',
639
                        '  /file create  (general purpose)',
640
                        '  /file mkdir   (a directory object)'])
641
            else:
642
                raise CLIError(
643
                    'Destination object exists',
644
                    importance=2, details=[
645
                        'Failed while transfering:',
646
                        '    pithos://%s/%s/%s' % (
647
                                self.account,
648
                                self.container,
649
                                self.path),
650
                        '--> pithos://%s/%s/%s' % (
651
                                self.dst_client.account,
652
                                self.dst_client.container,
653
                                dst_path),
654
                        'Use %s to transfer overwrite' % (
655
                                self.arguments['force'].lvalue)])
656
        return pairs
657

    
658
    def _run(self, source_path_or_url, destination_path_or_url=''):
659
        super(_source_destination, self)._run(source_path_or_url)
660
        dst_acc, dst_con, dst_path = self._resolve_pithos_url(
661
            destination_path_or_url)
662
        self.dst_client = PithosClient(
663
            base_url=self.client.base_url, token=self.client.token,
664
            container=self[
665
                'destination_container'] or dst_con or self.client.container,
666
            account=self['destination_user'] or dst_acc or self.account)
667
        self.dst_path = dst_path or self.path
668

    
669

    
670
@command(file_cmds)
671
class file_copy(_source_destination):
672
    """Copy objects, even between different accounts or containers"""
673

    
674
    arguments = dict(
675
        public=ValueArgument('publish new object', '--public'),
676
        content_type=ValueArgument(
677
            'change object\'s content type', '--content-type'),
678
        source_version=ValueArgument(
679
            'The version of the source object', '--object-version')
680
    )
681

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

    
702
    def main(self, source_path_or_url, destination_path_or_url=None):
703
        super(file_copy, self)._run(
704
            source_path_or_url, destination_path_or_url or '')
705
        self._run()
706

    
707

    
708
@command(file_cmds)
709
class file_move(_source_destination):
710
    """Move objects, even between different accounts or containers"""
711

    
712
    arguments = dict(
713
        public=ValueArgument('publish new object', '--public'),
714
        content_type=ValueArgument(
715
            'change object\'s content type', '--content-type')
716
    )
717

    
718
    @errors.generic.all
719
    @errors.pithos.connection
720
    @errors.pithos.container
721
    @errors.pithos.account
722
    def _run(self):
723
        for src, dst in self._src_dst():
724
            self._report_transfer(src, dst, 'move')
725
            if src and dst:
726
                self.dst_client.move_object(
727
                    src_container=self.client.container,
728
                    src_object=src,
729
                    dst_container=self.dst_client.container,
730
                    dst_object=dst,
731
                    source_account=self.account,
732
                    public=self['public'],
733
                    content_type=self['content_type'])
734
            elif dst:
735
                self.dst_client.create_directory(dst)
736
            else:
737
                self.client.del_object(src)
738

    
739
    def main(self, source_path_or_url, destination_path_or_url=None):
740
        super(file_move, self)._run(
741
            source_path_or_url, destination_path_or_url or '')
742
        self._run()
743

    
744

    
745
@command(file_cmds)
746
class file_append(_pithos_container, _optional_output_cmd):
747
    """Append local file to (existing) remote object
748
    The remote object should exist.
749
    If the remote object is a directory, it is transformed into a file.
750
    In the later case, objects under the directory remain intact.
751
    """
752

    
753
    arguments = dict(
754
        progress_bar=ProgressBarArgument(
755
            'do not show progress bar', ('-N', '--no-progress-bar'),
756
            default=False),
757
        max_threads=IntArgument('default: 1', '--threads'),
758
    )
759

    
760
    @errors.generic.all
761
    @errors.pithos.connection
762
    @errors.pithos.container
763
    @errors.pithos.object_path
764
    def _run(self, local_path):
765
        if self['max_threads'] > 0:
766
            self.client.MAX_THREADS = int(self['max_threads'])
767
        (progress_bar, upload_cb) = self._safe_progress_bar('Appending')
768
        try:
769
            with open(local_path, 'rb') as f:
770
                self._optional_output(
771
                    self.client.append_object(self.path, f, upload_cb))
772
        finally:
773
            self._safe_progress_bar_finish(progress_bar)
774

    
775
    def main(self, local_path, remote_path_or_url):
776
        super(self.__class__, self)._run(remote_path_or_url)
777
        self._run(local_path)
778

    
779

    
780
@command(file_cmds)
781
class file_truncate(_pithos_container, _optional_output_cmd):
782
    """Truncate remote file up to size"""
783

    
784
    arguments = dict(
785
        size_in_bytes=IntArgument('Length of file after truncation', '--size')
786
    )
787
    required = ('size_in_bytes', )
788

    
789
    @errors.generic.all
790
    @errors.pithos.connection
791
    @errors.pithos.container
792
    @errors.pithos.object_path
793
    @errors.pithos.object_size
794
    def _run(self, size):
795
        self._optional_output(self.client.truncate_object(self.path, size))
796

    
797
    def main(self, path_or_url):
798
        super(self.__class__, self)._run(path_or_url)
799
        self._run(size=self['size_in_bytes'])
800

    
801

    
802
@command(file_cmds)
803
class file_overwrite(_pithos_container, _optional_output_cmd):
804
    """Overwrite part of a remote file"""
805

    
806
    arguments = dict(
807
        progress_bar=ProgressBarArgument(
808
            'do not show progress bar', ('-N', '--no-progress-bar'),
809
            default=False),
810
        start_position=IntArgument('File position in bytes', '--from'),
811
        end_position=IntArgument('File position in bytes', '--to'),
812
    )
813
    required = ('start_position', 'end_position')
814

    
815
    @errors.generic.all
816
    @errors.pithos.connection
817
    @errors.pithos.container
818
    @errors.pithos.object_path
819
    @errors.pithos.object_size
820
    def _run(self, local_path, start, end):
821
        start, end = int(start), int(end)
822
        (progress_bar, upload_cb) = self._safe_progress_bar(
823
            'Overwrite %s bytes' % (end - start))
824
        try:
825
            with open(path.abspath(local_path), 'rb') as f:
826
                self._optional_output(self.client.overwrite_object(
827
                    obj=self.path,
828
                    start=start,
829
                    end=end,
830
                    source_file=f,
831
                    upload_cb=upload_cb))
832
        finally:
833
            self._safe_progress_bar_finish(progress_bar)
834

    
835
    def main(self, local_path, path_or_url):
836
        super(self.__class__, self)._run(path_or_url)
837
        self.path = self.path or path.basename(local_path)
838
        self._run(
839
            local_path=local_path,
840
            start=self['start_position'],
841
            end=self['end_position'])
842

    
843

    
844
@command(file_cmds)
845
class file_upload(_pithos_container, _optional_output_cmd):
846
    """Upload a file"""
847

    
848
    arguments = dict(
849
        max_threads=IntArgument('default: 5', '--threads'),
850
        content_encoding=ValueArgument(
851
            'set MIME content type', '--content-encoding'),
852
        content_disposition=ValueArgument(
853
            'specify objects presentation style', '--content-disposition'),
854
        content_type=ValueArgument('specify content type', '--content-type'),
855
        uuid_for_read_permission=RepeatableArgument(
856
            'Give read access to a user or group (can be repeated) '
857
            'Use * for all users',
858
            '--read-permission'),
859
        uuid_for_write_permission=RepeatableArgument(
860
            'Give write access to a user or group (can be repeated) '
861
            'Use * for all users',
862
            '--write-permission'),
863
        public=FlagArgument('make object publicly accessible', '--public'),
864
        progress_bar=ProgressBarArgument(
865
            'do not show progress bar',
866
            ('-N', '--no-progress-bar'),
867
            default=False),
868
        overwrite=FlagArgument('Force (over)write', ('-f', '--force')),
869
        recursive=FlagArgument(
870
            'Recursively upload directory *contents* + subdirectories',
871
            ('-r', '--recursive')),
872
        unchunked=FlagArgument(
873
            'Upload file as one block (not recommended)', '--unchunked'),
874
        md5_checksum=ValueArgument(
875
            'Confirm upload with a custom checksum (MD5)', '--etag'),
876
        use_hashes=FlagArgument(
877
            'Source file contains hashmap not data', '--source-is-hashmap'),
878
    )
879

    
880
    def _sharing(self):
881
        sharing = dict()
882
        readlist = self['uuid_for_read_permission']
883
        if readlist:
884
            sharing['read'] = self['uuid_for_read_permission']
885
        writelist = self['uuid_for_write_permission']
886
        if writelist:
887
            sharing['write'] = self['uuid_for_write_permission']
888
        return sharing or None
889

    
890
    def _check_container_limit(self, path):
891
        cl_dict = self.client.get_container_limit()
892
        container_limit = int(cl_dict['x-container-policy-quota'])
893
        r = self.client.container_get()
894
        used_bytes = sum(int(o['bytes']) for o in r.json)
895
        path_size = get_path_size(path)
896
        if container_limit and path_size > (container_limit - used_bytes):
897
            raise CLIError(
898
                'Container %s (limit(%s) - used(%s)) < (size(%s) of %s)' % (
899
                    self.client.container,
900
                    format_size(container_limit),
901
                    format_size(used_bytes),
902
                    format_size(path_size),
903
                    path),
904
                details=[
905
                    'Check accound limit: /file quota',
906
                    'Check container limit:',
907
                    '\t/file containerlimit get %s' % self.client.container,
908
                    'Increase container limit:',
909
                    '\t/file containerlimit set <new limit> %s' % (
910
                        self.client.container)])
911

    
912
    def _src_dst(self, local_path, remote_path, objlist=None):
913
        lpath = path.abspath(local_path)
914
        short_path = path.basename(path.abspath(local_path))
915
        rpath = remote_path or short_path
916
        if path.isdir(lpath):
917
            if not self['recursive']:
918
                raise CLIError('%s is a directory' % lpath, details=[
919
                    'Use %s to upload directories & contents' % (
920
                        self.arguments['recursive'].lvalue)])
921
            robj = self.client.container_get(path=rpath)
922
            if not self['overwrite']:
923
                if robj.json:
924
                    raise CLIError(
925
                        'Objects/files prefixed as %s already exist' % rpath,
926
                        details=['Existing objects:'] + ['\t/%s/\t%s' % (
927
                            o['name'],
928
                            o['content_type'][12:]) for o in robj.json] + [
929
                            'Use -f to add, overwrite or resume'])
930
                else:
931
                    try:
932
                        topobj = self.client.get_object_info(rpath)
933
                        if not self._is_dir(topobj):
934
                            raise CLIError(
935
                                'Object /%s/%s exists but not a directory' % (
936
                                    self.container, rpath),
937
                                details=['Use -f to overwrite'])
938
                    except ClientError as ce:
939
                        if ce.status not in (404, ):
940
                            raise
941
            self._check_container_limit(lpath)
942
            prev = ''
943
            for top, subdirs, files in walk(lpath):
944
                if top != prev:
945
                    prev = top
946
                    try:
947
                        rel_path = rpath + top.split(lpath)[1]
948
                    except IndexError:
949
                        rel_path = rpath
950
                    self.error('mkdir /%s/%s' % (
951
                        self.client.container, rel_path))
952
                    self.client.create_directory(rel_path)
953
                for f in files:
954
                    fpath = path.join(top, f)
955
                    if path.isfile(fpath):
956
                        rel_path = rel_path.replace(path.sep, '/')
957
                        pathfix = f.replace(path.sep, '/')
958
                        yield open(fpath, 'rb'), '%s/%s' % (rel_path, pathfix)
959
                    else:
960
                        self.error('%s is not a regular file' % fpath)
961
        else:
962
            if not path.isfile(lpath):
963
                raise CLIError(('%s is not a regular file' % lpath) if (
964
                    path.exists(lpath)) else '%s does not exist' % lpath)
965
            try:
966
                robj = self.client.get_object_info(rpath)
967
                if remote_path and self._is_dir(robj):
968
                    rpath += '/%s' % (short_path.replace(path.sep, '/'))
969
                    self.client.get_object_info(rpath)
970
                if not self['overwrite']:
971
                    raise CLIError(
972
                        'Object /%s/%s already exists' % (
973
                            self.container, rpath),
974
                        details=['use -f to overwrite / resume'])
975
            except ClientError as ce:
976
                if ce.status not in (404, ):
977
                    raise
978
            self._check_container_limit(lpath)
979
            yield open(lpath, 'rb'), rpath
980

    
981
    def _run(self, local_path, remote_path):
982
        self.client.MAX_THREADS = int(self['max_threads'] or 5)
983
        params = dict(
984
            content_encoding=self['content_encoding'],
985
            content_type=self['content_type'],
986
            content_disposition=self['content_disposition'],
987
            sharing=self._sharing(),
988
            public=self['public'])
989
        uploaded, container_info_cache = list, dict()
990
        rpref = 'pithos://%s' if self['account'] else ''
991
        for f, rpath in self._src_dst(local_path, remote_path):
992
            self.error('%s --> %s/%s/%s' % (
993
                f.name, rpref, self.client.container, rpath))
994
            if not (self['content_type'] and self['content_encoding']):
995
                ctype, cenc = guess_mime_type(f.name)
996
                params['content_type'] = self['content_type'] or ctype
997
                params['content_encoding'] = self['content_encoding'] or cenc
998
            if self['unchunked']:
999
                r = self.client.upload_object_unchunked(
1000
                    rpath, f,
1001
                    etag=self['md5_checksum'], withHashFile=self['use_hashes'],
1002
                    **params)
1003
                if self['with_output'] or self['json_output']:
1004
                    r['name'] = '/%s/%s' % (self.client.container, rpath)
1005
                    uploaded.append(r)
1006
            else:
1007
                try:
1008
                    (progress_bar, upload_cb) = self._safe_progress_bar(
1009
                        'Uploading %s' % f.name.split(path.sep)[-1])
1010
                    if progress_bar:
1011
                        hash_bar = progress_bar.clone()
1012
                        hash_cb = hash_bar.get_generator(
1013
                            'Calculating block hashes')
1014
                    else:
1015
                        hash_cb = None
1016
                    r = self.client.upload_object(
1017
                        rpath, f,
1018
                        hash_cb=hash_cb,
1019
                        upload_cb=upload_cb,
1020
                        container_info_cache=container_info_cache,
1021
                        **params)
1022
                    if self['with_output'] or self['json_output']:
1023
                        r['name'] = '/%s/%s' % (self.client.container, rpath)
1024
                        uploaded.append(r)
1025
                except Exception:
1026
                    self._safe_progress_bar_finish(progress_bar)
1027
                    raise
1028
                finally:
1029
                    self._safe_progress_bar_finish(progress_bar)
1030
        self._optional_output(uploaded)
1031
        self.error('Upload completed')
1032

    
1033
    def main(self, local_path, remote_path_or_url):
1034
        super(self.__class__, self)._run(remote_path_or_url)
1035
        remote_path = self.path or path.basename(path.abspath(local_path))
1036
        self._run(local_path=local_path, remote_path=remote_path)
1037

    
1038

    
1039
class RangeArgument(ValueArgument):
1040
    """
1041
    :value type: string of the form <start>-<end> where <start> and <end> are
1042
        integers
1043
    :value returns: the input string, after type checking <start> and <end>
1044
    """
1045

    
1046
    @property
1047
    def value(self):
1048
        return getattr(self, '_value', self.default)
1049

    
1050
    @value.setter
1051
    def value(self, newvalues):
1052
        if newvalues:
1053
            self._value = getattr(self, '_value', self.default)
1054
            for newvalue in newvalues.split(','):
1055
                self._value = ('%s,' % self._value) if self._value else ''
1056
                start, sep, end = newvalue.partition('-')
1057
                if sep:
1058
                    if start:
1059
                        start, end = (int(start), int(end))
1060
                        if start > end:
1061
                            raise CLIInvalidArgument(
1062
                                'Invalid range %s' % newvalue, details=[
1063
                                'Valid range formats',
1064
                                '  START-END', '  UP_TO', '  -FROM',
1065
                                'where all values are integers',
1066
                                'OR a compination (csv), e.g.,',
1067
                                '  %s=5,10-20,-5' % self.lvalue])
1068
                        self._value += '%s-%s' % (start, end)
1069
                    else:
1070
                        self._value += '-%s' % int(end)
1071
                else:
1072
                    self._value += '%s' % int(start)
1073

    
1074

    
1075
@command(file_cmds)
1076
class file_cat(_pithos_container):
1077
    """Fetch remote file contents"""
1078

    
1079
    arguments = dict(
1080
        range=RangeArgument('show range of data e.g., 5,10-20,-5', '--range'),
1081
        if_match=ValueArgument('show output if ETags match', '--if-match'),
1082
        if_none_match=ValueArgument(
1083
            'show output if ETags match', '--if-none-match'),
1084
        if_modified_since=DateArgument(
1085
            'show output modified since then', '--if-modified-since'),
1086
        if_unmodified_since=DateArgument(
1087
            'show output unmodified since then', '--if-unmodified-since'),
1088
        object_version=ValueArgument(
1089
            'Get contents of the chosen version', '--object-version')
1090
    )
1091

    
1092
    @errors.generic.all
1093
    @errors.pithos.connection
1094
    @errors.pithos.container
1095
    @errors.pithos.object_path
1096
    def _run(self):
1097
        self.client.download_object(
1098
            self.path, self._out,
1099
            range_str=self['range'],
1100
            version=self['object_version'],
1101
            if_match=self['if_match'],
1102
            if_none_match=self['if_none_match'],
1103
            if_modified_since=self['if_modified_since'],
1104
            if_unmodified_since=self['if_unmodified_since'])
1105
        self._out.flush()
1106

    
1107
    def main(self, path_or_url):
1108
        super(self.__class__, self)._run(path_or_url)
1109
        self._run()
1110

    
1111

    
1112
@command(file_cmds)
1113
class file_download(_pithos_container):
1114
    """Download a remove file or directory object to local file system"""
1115

    
1116
    arguments = dict(
1117
        resume=FlagArgument(
1118
            'Resume/Overwrite (attempt resume, else overwrite)',
1119
            ('-f', '--resume')),
1120
        range=RangeArgument(
1121
            'Download only that range of data e.g., 5,10-20,-5', '--range'),
1122
        matching_etag=ValueArgument('download iff ETag match', '--if-match'),
1123
        non_matching_etag=ValueArgument(
1124
            'download iff ETags DO NOT match', '--if-none-match'),
1125
        modified_since_date=DateArgument(
1126
            'download iff remote file is modified since then',
1127
            '--if-modified-since'),
1128
        unmodified_since_date=DateArgument(
1129
            'show output iff remote file is unmodified since then',
1130
            '--if-unmodified-since'),
1131
        object_version=ValueArgument(
1132
            'download a file of a specific version', '--object-version'),
1133
        max_threads=IntArgument('default: 5', '--threads'),
1134
        progress_bar=ProgressBarArgument(
1135
            'do not show progress bar', ('-N', '--no-progress-bar'),
1136
            default=False),
1137
        recursive=FlagArgument(
1138
            'Download a remote directory object and its contents',
1139
            ('-r', '--recursive'))
1140
        )
1141

    
1142
    def _src_dst(self, local_path):
1143
        """Create a list of (src, dst) where src is a remote location and dst
1144
        is an open file descriptor. Directories are denoted as (None, dirpath)
1145
        and they are pretended to other objects in a very strict order (shorter
1146
        to longer path)."""
1147
        ret = []
1148
        try:
1149
            if self.path:
1150
                obj = self.client.get_object_info(
1151
                    self.path, version=self['object_version'])
1152
                obj.setdefault('name', self.path.strip('/'))
1153
            else:
1154
                obj = None
1155
        except ClientError as ce:
1156
            if ce.status in (404, ):
1157
                raiseCLIError(ce, details=[
1158
                    'To download an object, it must exist either as a file or'
1159
                    ' as a directory.',
1160
                    'For example, to download everything under prefix/ the '
1161
                    'directory "prefix" must exist.',
1162
                    'To see if an remote object is actually there:',
1163
                    '  /file info [/CONTAINER/]OBJECT',
1164
                    'To create a directory object:',
1165
                    '  /file mkdir [/CONTAINER/]OBJECT'])
1166
            if ce.status in (204, ):
1167
                raise CLIError(
1168
                    'No file or directory objects to download',
1169
                    details=[
1170
                        'To download a container (e.g., %s):' % self.container,
1171
                        '  [kamaki] container download %s [LOCAL_PATH]' % (
1172
                            self.container)])
1173
            raise
1174
        rpath = self.path.strip('/')
1175
        if local_path and self.path and local_path.endswith('/'):
1176
            local_path = local_path[-1:]
1177

    
1178
        if (not obj) or self._is_dir(obj):
1179
            if self['recursive']:
1180
                if not (self.path or local_path.endswith('/')):
1181
                    #  Download the whole container
1182
                    local_path = '' if local_path in ('.', ) else local_path
1183
                    local_path = '%s/' % (local_path or self.container)
1184
                obj = obj or dict(
1185
                    name='', content_type='application/directory')
1186
                dirs, files = [obj, ], []
1187
                objects = self.client.container_get(
1188
                    path=self.path,
1189
                    if_modified_since=self['modified_since_date'],
1190
                    if_unmodified_since=self['unmodified_since_date'])
1191
                for o in objects.json:
1192
                    (dirs if self._is_dir(o) else files).append(o)
1193

    
1194
                #  Put the directories on top of the list
1195
                for dpath in sorted(['%s%s' % (
1196
                        local_path, d['name'][len(rpath):]) for d in dirs]):
1197
                    if path.exists(dpath):
1198
                        if path.isdir(dpath):
1199
                            continue
1200
                        raise CLIError(
1201
                            'Cannot replace local file %s with a directory '
1202
                            'of the same name' % dpath,
1203
                            details=[
1204
                                'Either remove the file or specify a'
1205
                                'different target location'])
1206
                    ret.append((None, dpath, None))
1207

    
1208
                #  Append the file objects
1209
                for opath in [o['name'] for o in files]:
1210
                    lpath = '%s%s' % (local_path, opath[len(rpath):])
1211
                    if self['resume']:
1212
                        fxists = path.exists(lpath)
1213
                        if fxists and path.isdir(lpath):
1214
                            raise CLIError(
1215
                                'Cannot change local dir %s info file' % (
1216
                                    lpath),
1217
                                details=[
1218
                                    'Either remove the file or specify a'
1219
                                    'different target location'])
1220
                        ret.append((opath, lpath, fxists))
1221
                    elif path.exists(lpath):
1222
                        raise CLIError(
1223
                            'Cannot overwrite %s' % lpath,
1224
                            details=['To overwrite/resume, use  %s' % (
1225
                                self.arguments['resume'].lvalue)])
1226
                    else:
1227
                        ret.append((opath, lpath, None))
1228
            elif self.path:
1229
                raise CLIError(
1230
                    'Remote object /%s/%s is a directory' % (
1231
                        self.container, local_path),
1232
                    details=['Use %s to download directories' % (
1233
                        self.arguments['recursive'].lvalue)])
1234
            else:
1235
                parsed_name = self.arguments['recursive'].lvalue
1236
                raise CLIError(
1237
                    'Cannot download container %s' % self.container,
1238
                    details=[
1239
                        'Use %s to download containers' % parsed_name,
1240
                        '  [kamaki] file download %s /%s [LOCAL_PATH]' % (
1241
                            parsed_name, self.container)])
1242
        else:
1243
            #  Remote object is just a file
1244
            if path.exists(local_path):
1245
                if not self['resume']:
1246
                    raise CLIError(
1247
                        'Cannot overwrite local file %s' % (local_path),
1248
                        details=['To overwrite/resume, use  %s' % (
1249
                            self.arguments['resume'].lvalue)])
1250
            elif '/' in local_path[1:-1]:
1251
                dirs = [p for p in local_path.split('/') if p]
1252
                pref = '/' if local_path.startswith('/') else ''
1253
                for d in dirs[:-1]:
1254
                    pref += d
1255
                    if not path.exists(pref):
1256
                        ret.append((None, d, None))
1257
                    elif not path.isdir(pref):
1258
                        raise CLIError(
1259
                            'Failed to use %s as a destination' % local_path,
1260
                            importance=3,
1261
                            details=[
1262
                                'Local file %s is not a directory' % pref,
1263
                                'Destination prefix must consist of '
1264
                                'directories or non-existing names',
1265
                                'Either remove the file, or choose another '
1266
                                'destination'])
1267
            ret.append((rpath, local_path, self['resume']))
1268
        for r, l, resume in ret:
1269
            if r:
1270
                with open(l, 'rwb+' if resume else 'wb+') as f:
1271
                    yield (r, f)
1272
            else:
1273
                yield (r, l)
1274

    
1275
    @errors.generic.all
1276
    @errors.pithos.connection
1277
    @errors.pithos.container
1278
    @errors.pithos.object_path
1279
    @errors.pithos.local_path
1280
    @errors.pithos.local_path_download
1281
    def _run(self, local_path):
1282
        self.client.MAX_THREADS = int(self['max_threads'] or 5)
1283
        progress_bar = None
1284
        try:
1285
            for rpath, output_file in self._src_dst(local_path):
1286
                if not rpath:
1287
                    self.error('Create local directory %s' % output_file)
1288
                    makedirs(output_file)
1289
                    continue
1290
                self.error('/%s/%s --> %s' % (
1291
                    self.container, rpath, output_file.name))
1292
                progress_bar, download_cb = self._safe_progress_bar(
1293
                    '  download')
1294
                self.client.download_object(
1295
                    rpath, output_file,
1296
                    download_cb=download_cb,
1297
                    range_str=self['range'],
1298
                    version=self['object_version'],
1299
                    if_match=self['matching_etag'],
1300
                    resume=self['resume'],
1301
                    if_none_match=self['non_matching_etag'],
1302
                    if_modified_since=self['modified_since_date'],
1303
                    if_unmodified_since=self['unmodified_since_date'])
1304
        except KeyboardInterrupt:
1305
            from threading import activeCount, enumerate as activethreads
1306
            timeout = 0.5
1307
            while activeCount() > 1:
1308
                self._out.write('\nCancel %s threads: ' % (activeCount() - 1))
1309
                self._out.flush()
1310
                for thread in activethreads():
1311
                    try:
1312
                        thread.join(timeout)
1313
                        self._out.write('.' if thread.isAlive() else '*')
1314
                    except RuntimeError:
1315
                        continue
1316
                    finally:
1317
                        self._out.flush()
1318
                        timeout += 0.1
1319
            self.error('\nDownload canceled by user')
1320
            if local_path is not None:
1321
                self.error('to resume, re-run with --resume')
1322
        finally:
1323
            self._safe_progress_bar_finish(progress_bar)
1324

    
1325
    def main(self, remote_path_or_url, local_path=None):
1326
        super(self.__class__, self)._run(remote_path_or_url)
1327
        local_path = local_path or self.path or '.'
1328
        self._run(local_path=local_path)
1329

    
1330

    
1331
@command(container_cmds)
1332
class container_info(_pithos_account, _optional_json):
1333
    """Get information about a container"""
1334

    
1335
    arguments = dict(
1336
        until_date=DateArgument('show metadata until then', '--until'),
1337
        metadata=FlagArgument('Show only container metadata', '--metadata'),
1338
        sizelimit=FlagArgument(
1339
            'Show the maximum size limit for container', '--size-limit'),
1340
        in_bytes=FlagArgument('Show size limit in bytes', ('-b', '--bytes'))
1341
    )
1342

    
1343
    @errors.generic.all
1344
    @errors.pithos.connection
1345
    @errors.pithos.container
1346
    @errors.pithos.object_path
1347
    def _run(self):
1348
        if self['metadata']:
1349
            r, preflen = dict(), len('x-container-meta-')
1350
            for k, v in self.client.get_container_meta(
1351
                    until=self['until_date']).items():
1352
                r[k[preflen:]] = v
1353
        elif self['sizelimit']:
1354
            r = self.client.get_container_limit(
1355
                self.container)['x-container-policy-quota']
1356
            r = {'size limit': 'unlimited' if r in ('0', ) else (
1357
                int(r) if self['in_bytes'] else format_size(r))}
1358
        else:
1359
            r = self.client.get_container_info(self.container)
1360
        self._print(r, self.print_dict)
1361

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

    
1367

    
1368
class VersioningArgument(ValueArgument):
1369

    
1370
    schemes = ('auto', 'none')
1371

    
1372
    @property
1373
    def value(self):
1374
        return getattr(self, '_value', None)
1375

    
1376
    @value.setter
1377
    def value(self, new_scheme):
1378
        if new_scheme:
1379
            new_scheme = new_scheme.lower()
1380
            if new_scheme not in self.schemes:
1381
                raise CLIInvalidArgument('Invalid versioning value', details=[
1382
                    'Valid versioning values are %s' % ', '.join(
1383
                        self.schemes)])
1384
            self._value = new_scheme
1385

    
1386

    
1387
@command(container_cmds)
1388
class container_modify(_pithos_account, _optional_json):
1389
    """Modify the properties of a container"""
1390

    
1391
    arguments = dict(
1392
        metadata_to_add=KeyValueArgument(
1393
            'Add metadata in the form KEY=VALUE (can be repeated)',
1394
            '--metadata-add'),
1395
        metadata_to_delete=RepeatableArgument(
1396
            'Delete metadata by KEY (can be repeated)', '--metadata-del'),
1397
        sizelimit=DataSizeArgument(
1398
            'Set max size limit (0 for unlimited, '
1399
            'use units B, KiB, KB, etc.)', '--size-limit'),
1400
        versioning=VersioningArgument(
1401
            'Set a versioning scheme (%s)' % ', '.join(
1402
                VersioningArgument.schemes), '--versioning')
1403
    )
1404
    required = [
1405
        'metadata_to_add', 'metadata_to_delete', 'sizelimit', 'versioning']
1406

    
1407
    @errors.generic.all
1408
    @errors.pithos.connection
1409
    @errors.pithos.container
1410
    def _run(self, container):
1411
        metadata = self['metadata_to_add']
1412
        for k in (self['metadata_to_delete'] or []):
1413
            metadata[k] = ''
1414
        if metadata:
1415
            self.client.set_container_meta(metadata)
1416
            self._print(self.client.get_container_meta(), self.print_dict)
1417
        if self['sizelimit'] is not None:
1418
            self.client.set_container_limit(self['sizelimit'])
1419
            r = self.client.get_container_limit()['x-container-policy-quota']
1420
            r = 'unlimited' if r in ('0', ) else format_size(r)
1421
            self.writeln('new size limit: %s' % r)
1422
        if self['versioning']:
1423
            self.client.set_container_versioning(self['versioning'])
1424
            self.writeln('new versioning scheme: %s' % (
1425
                self.client.get_container_versioning(self.container)[
1426
                    'x-container-policy-versioning']))
1427

    
1428
    def main(self, container):
1429
        super(self.__class__, self)._run()
1430
        self.client.container, self.container = container, container
1431
        self._run(container=container)
1432

    
1433

    
1434
@command(container_cmds)
1435
class container_list(_pithos_account, _optional_json, _name_filter):
1436
    """List all containers, or their contents"""
1437

    
1438
    arguments = dict(
1439
        detail=FlagArgument('Containers with details', ('-l', '--list')),
1440
        limit=IntArgument('limit number of listed items', ('-n', '--number')),
1441
        marker=ValueArgument('output greater that marker', '--marker'),
1442
        modified_since_date=ValueArgument(
1443
            'show output modified since then', '--if-modified-since'),
1444
        unmodified_since_date=ValueArgument(
1445
            'show output not modified since then', '--if-unmodified-since'),
1446
        until_date=DateArgument('show metadata until then', '--until'),
1447
        shared=FlagArgument('show only shared', '--shared'),
1448
        more=FlagArgument('read long results', '--more'),
1449
        enum=FlagArgument('Enumerate results', '--enumerate'),
1450
        recursive=FlagArgument(
1451
            'Recursively list containers and their contents',
1452
            ('-r', '--recursive')),
1453
        shared_by_me=FlagArgument(
1454
            'show only files shared to other users', '--shared-by-me'),
1455
        public=FlagArgument('show only published objects', '--public'),
1456
    )
1457

    
1458
    def print_containers(self, container_list):
1459
        for index, container in enumerate(container_list):
1460
            if 'bytes' in container:
1461
                size = format_size(container['bytes'])
1462
            prfx = ('%s. ' % (index + 1)) if self['enum'] else ''
1463
            _cname = container['name'] if (
1464
                self['more']) else bold(container['name'])
1465
            cname = u'%s%s' % (prfx, _cname)
1466
            if self['detail']:
1467
                self.writeln(cname)
1468
                pretty_c = container.copy()
1469
                if 'bytes' in container:
1470
                    pretty_c['bytes'] = '%s (%s)' % (container['bytes'], size)
1471
                self.print_dict(pretty_c, exclude=('name'))
1472
                self.writeln()
1473
            else:
1474
                if 'count' in container and 'bytes' in container:
1475
                    self.writeln('%s (%s, %s objects)' % (
1476
                        cname, size, container['count']))
1477
                else:
1478
                    self.writeln(cname)
1479
            objects = container.get('objects', [])
1480
            if objects:
1481
                self.print_objects(objects)
1482
                self.writeln('')
1483

    
1484
    def _create_object_forest(self, container_list):
1485
        try:
1486
            for container in container_list:
1487
                self.client.container = container['name']
1488
                objects = self.client.container_get(
1489
                    limit=False if self['more'] else self['limit'],
1490
                    if_modified_since=self['modified_since_date'],
1491
                    if_unmodified_since=self['unmodified_since_date'],
1492
                    until=self['until_date'],
1493
                    show_only_shared=self['shared_by_me'],
1494
                    public=self['public'])
1495
                container['objects'] = objects.json
1496
        finally:
1497
            self.client.container = None
1498

    
1499
    @errors.generic.all
1500
    @errors.pithos.connection
1501
    @errors.pithos.object_path
1502
    @errors.pithos.container
1503
    def _run(self, container):
1504
        if container:
1505
            r = self.client.container_get(
1506
                limit=False if self['more'] else self['limit'],
1507
                marker=self['marker'],
1508
                if_modified_since=self['modified_since_date'],
1509
                if_unmodified_since=self['unmodified_since_date'],
1510
                until=self['until_date'],
1511
                show_only_shared=self['shared_by_me'],
1512
                public=self['public'])
1513
        else:
1514
            r = self.client.account_get(
1515
                limit=False if self['more'] else self['limit'],
1516
                marker=self['marker'],
1517
                if_modified_since=self['modified_since_date'],
1518
                if_unmodified_since=self['unmodified_since_date'],
1519
                until=self['until_date'],
1520
                show_only_shared=self['shared_by_me'],
1521
                public=self['public'])
1522
        files = self._filter_by_name(r.json)
1523
        if self['recursive'] and not container:
1524
            self._create_object_forest(files)
1525
        if self['more']:
1526
            outbu, self._out = self._out, StringIO()
1527
        try:
1528
            if self['json_output'] or self['output_format']:
1529
                self._print(files)
1530
            else:
1531
                (self.print_objects if container else self.print_containers)(
1532
                    files)
1533
        finally:
1534
            if self['more']:
1535
                pager(self._out.getvalue())
1536
                self._out = outbu
1537

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

    
1543

    
1544
@command(container_cmds)
1545
class container_create(_pithos_account):
1546
    """Create a new container"""
1547

    
1548
    arguments = dict(
1549
        versioning=ValueArgument(
1550
            'set container versioning (auto/none)', '--versioning'),
1551
        limit=IntArgument('set default container limit', '--limit'),
1552
        meta=KeyValueArgument(
1553
            'set container metadata (can be repeated)', '--meta'),
1554
        project=ValueArgument('assign the container to project', '--project'),
1555
    )
1556

    
1557
    @errors.generic.all
1558
    @errors.pithos.connection
1559
    @errors.pithos.container
1560
    def _run(self, container):
1561
        try:
1562
            self.client.create_container(
1563
                container=container,
1564
                sizelimit=self['limit'],
1565
                versioning=self['versioning'],
1566
                project=self['project'],
1567
                metadata=self['meta'],
1568
                success=(201, ))
1569
        except ClientError as ce:
1570
            if ce.status in (202, ):
1571
                raise CLIError(
1572
                    'Container %s alread exists' % container, details=[
1573
                    'Either delete %s or choose another name' % (container)])
1574
            raise
1575

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

    
1580

    
1581
@command(container_cmds)
1582
class container_delete(_pithos_account):
1583
    """Delete a container"""
1584

    
1585
    arguments = dict(
1586
        yes=FlagArgument('Do not prompt for permission', '--yes'),
1587
        recursive=FlagArgument(
1588
            'delete container even if not empty', ('-r', '--recursive'))
1589
    )
1590

    
1591
    @errors.generic.all
1592
    @errors.pithos.connection
1593
    @errors.pithos.container
1594
    def _run(self, container):
1595
        num_of_contents = int(self.client.get_container_info(container)[
1596
            'x-container-object-count'])
1597
        delimiter, msg = None, 'Delete container %s ?' % container
1598
        if self['recursive']:
1599
            delimiter, msg = '/', 'Empty and d%s' % msg[1:]
1600
        elif num_of_contents:
1601
            raise CLIError('Container %s is not empty' % container, details=[
1602
                'Use %s to delete non-empty containers' % (
1603
                    self.arguments['recursive'].lvalue)])
1604
        if self['yes'] or self.ask_user(msg):
1605
            if num_of_contents:
1606
                self.client.del_container(delimiter=delimiter)
1607
            self.client.purge_container()
1608

    
1609
    def main(self, container):
1610
        super(self.__class__, self)._run()
1611
        self.container, self.client.container = container, container
1612
        self._run(container)
1613

    
1614

    
1615
@command(container_cmds)
1616
class container_empty(_pithos_account):
1617
    """Empty a container"""
1618

    
1619
    arguments = dict(yes=FlagArgument('Do not prompt for permission', '--yes'))
1620

    
1621
    @errors.generic.all
1622
    @errors.pithos.connection
1623
    @errors.pithos.container
1624
    def _run(self, container):
1625
        if self['yes'] or self.ask_user('Empty container %s ?' % container):
1626
            self.client.del_container(delimiter='/')
1627

    
1628
    def main(self, container):
1629
        super(self.__class__, self)._run()
1630
        self.container, self.client.container = container, container
1631
        self._run(container)
1632

    
1633

    
1634
@command(container_cmds)
1635
class container_reassign(_pithos_account, _optional_output_cmd):
1636
    """Assign a container to a different project
1637
    """
1638

    
1639
    @errors.generic.all
1640
    @errors.pithos.connection
1641
    @errors.pithos.container
1642
    def _run(self, project):
1643
        if self.container:
1644
            self.client.container = self.container
1645
        self._optional_output(self.client.reassign_container(project))
1646

    
1647
    def main(self, container, project):
1648
        super(self.__class__, self)._run()
1649
        self.container = container
1650
        self._run(project)
1651

    
1652

    
1653
@command(sharer_cmds)
1654
class sharer_list(_pithos_account, _optional_json):
1655
    """List accounts who share file objects with current user"""
1656

    
1657
    arguments = dict(
1658
        detail=FlagArgument('show detailed output', ('-l', '--details')),
1659
        marker=ValueArgument('show output greater then marker', '--marker')
1660
    )
1661

    
1662
    @errors.generic.all
1663
    @errors.pithos.connection
1664
    def _run(self):
1665
        accounts = self.client.get_sharing_accounts(marker=self['marker'])
1666
        if not (self['json_output'] or self['output_format']):
1667
            usernames = self._uuids2usernames(
1668
                [acc['name'] for acc in accounts])
1669
            for item in accounts:
1670
                uuid = item['name']
1671
                item['id'], item['name'] = uuid, usernames[uuid]
1672
                if not self['detail']:
1673
                    item.pop('last_modified')
1674
        self._print(accounts)
1675

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

    
1680

    
1681
@command(sharer_cmds)
1682
class sharer_info(_pithos_account, _optional_json):
1683
    """Details on a Pithos+ sharer account (default: current account)"""
1684

    
1685
    @errors.generic.all
1686
    @errors.pithos.connection
1687
    def _run(self):
1688
        self._print(self.client.get_account_info(), self.print_dict)
1689

    
1690
    def main(self, account_uuid_or_name=None):
1691
        super(self.__class__, self)._run()
1692
        if account_uuid_or_name:
1693
            arg = UserAccountArgument('Check', ' ')
1694
            arg.account_client = self.auth_base
1695
            arg.value = account_uuid_or_name
1696
            self.client.account, self.account = arg.value, arg.value
1697
        self._run()
1698

    
1699

    
1700
class _pithos_group(_pithos_account):
1701
    prefix = 'x-account-group-'
1702
    preflen = len(prefix)
1703

    
1704
    def _groups(self):
1705
        groups = dict()
1706
        for k, v in self.client.get_account_group().items():
1707
            groups[k[self.preflen:]] = v
1708
        return groups
1709

    
1710

    
1711
@command(group_cmds)
1712
class group_list(_pithos_group, _optional_json):
1713
    """list all groups and group members"""
1714

    
1715
    @errors.generic.all
1716
    @errors.pithos.connection
1717
    def _run(self):
1718
        self._print(self._groups(), self.print_dict)
1719

    
1720
    def main(self):
1721
        super(self.__class__, self)._run()
1722
        self._run()
1723

    
1724

    
1725
@command(group_cmds)
1726
class group_create(_pithos_group, _optional_json):
1727
    """Create a group of users"""
1728

    
1729
    arguments = dict(
1730
        user_uuid=RepeatableArgument('Add a user to the group', '--uuid'),
1731
        username=RepeatableArgument('Add a user to the group', '--username')
1732
    )
1733
    required = ['user_uuid', 'username']
1734

    
1735
    @errors.generic.all
1736
    @errors.pithos.connection
1737
    def _run(self, groupname, *users):
1738
        if groupname in self._groups() and not self.ask_user(
1739
                'Group %s already exists, overwrite?' % groupname):
1740
            self.error('Aborted')
1741
            return
1742
        self.client.set_account_group(groupname, users)
1743
        self._print(self._groups(), self.print_dict)
1744

    
1745
    def main(self, groupname):
1746
        super(self.__class__, self)._run()
1747
        users = (self['user_uuid'] or []) + self._usernames2uuids(
1748
            self['username'] or []).values()
1749
        if users:
1750
            self._run(groupname, *users)
1751
        else:
1752
            raise CLISyntaxError(
1753
                'No valid users specified, use %s or %s' % (
1754
                    self.arguments['user_uuid'].lvalue,
1755
                    self.arguments['username'].lvalue),
1756
                details=[
1757
                    'Check if a username or uuid is valid with',
1758
                    '  user uuid2username', 'OR', '  user username2uuid'])
1759

    
1760

    
1761
@command(group_cmds)
1762
class group_delete(_pithos_group, _optional_json):
1763
    """Delete a user group"""
1764

    
1765
    @errors.generic.all
1766
    @errors.pithos.connection
1767
    def _run(self, groupname):
1768
        self.client.del_account_group(groupname)
1769
        self._print(self._groups(), self.print_dict)
1770

    
1771
    def main(self, groupname):
1772
        super(self.__class__, self)._run()
1773
        self._run(groupname)