Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / pithos.py @ 602888f4

History | View | Annotate | Download (69.4 kB)

1
# Copyright 2011-2014 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
        if not r.json:
307
            self.error('Container "%s" is empty' % self.client.container)
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
        uuid_for_read_permission=RepeatableArgument(
333
            'Give read access to user/group (can be repeated, accumulative). '
334
            'Format for users: UUID . Format for groups: UUID:GROUP . '
335
            'Use * for all users/groups', '--read-permission'),
336
        uuid_for_write_permission=RepeatableArgument(
337
            'Give write access to user/group (can be repeated, accumulative). '
338
            'Format for users: UUID . Format for groups: UUID:GROUP . '
339
            'Use * for all users/groups', '--write-permission'),
340
        no_permissions=FlagArgument('Remove permissions', '--no-permissions'),
341
        metadata_to_set=KeyValueArgument(
342
            'Add metadata (KEY=VALUE) to an object (can be repeated)',
343
            '--metadata-add'),
344
        metadata_key_to_delete=RepeatableArgument(
345
            'Delete object metadata (can be repeated)', '--metadata-del'),
346
    )
347
    required = [
348
        'uuid_for_read_permission', 'metadata_to_set',
349
        'uuid_for_write_permission', 'no_permissions',
350
        'metadata_key_to_delete']
351

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

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

    
387

    
388
@command(file_cmds)
389
class file_publish(_pithos_container):
390
    """Publish an object (creates a public URL)"""
391

    
392
    @errors.generic.all
393
    @errors.pithos.connection
394
    @errors.pithos.container
395
    @errors.pithos.object_path
396
    def _run(self):
397
        self.writeln(self.client.publish_object(self.path))
398

    
399
    def main(self, path_or_url):
400
        super(self.__class__, self)._run(path_or_url)
401
        self._run()
402

    
403

    
404
@command(file_cmds)
405
class file_unpublish(_pithos_container):
406
    """Unpublish an object"""
407

    
408
    @errors.generic.all
409
    @errors.pithos.connection
410
    @errors.pithos.container
411
    @errors.pithos.object_path
412
    def _run(self):
413
        self.client.unpublish_object(self.path)
414

    
415
    def main(self, path_or_url):
416
        super(self.__class__, self)._run(path_or_url)
417
        self._run()
418

    
419

    
420
def _assert_path(self, path_or_url):
421
    if not self.path:
422
        raiseCLIError(
423
            'Directory path is missing in location %s' % path_or_url,
424
            details=['Location format:    [[pithos://UUID]/CONTAINER/]PATH'])
425

    
426

    
427
@command(file_cmds)
428
class file_create(_pithos_container, _optional_output_cmd):
429
    """Create an empty file"""
430

    
431
    arguments = dict(
432
        content_type=ValueArgument(
433
            'Set content type (default: application/octet-stream)',
434
            '--content-type',
435
            default='application/octet-stream')
436
    )
437

    
438
    @errors.generic.all
439
    @errors.pithos.connection
440
    @errors.pithos.container
441
    def _run(self):
442
        self._optional_output(
443
            self.client.create_object(self.path, self['content_type']))
444

    
445
    def main(self, path_or_url):
446
        super(self.__class__, self)._run(path_or_url)
447
        _assert_path(self, path_or_url)
448
        self._run()
449

    
450

    
451
@command(file_cmds)
452
class file_mkdir(_pithos_container, _optional_output_cmd):
453
    """Create a directory: /file create --content-type='application/directory'
454
    """
455

    
456
    @errors.generic.all
457
    @errors.pithos.connection
458
    @errors.pithos.container
459
    def _run(self, path):
460
        self._optional_output(self.client.create_directory(self.path))
461

    
462
    def main(self, path_or_url):
463
        super(self.__class__, self)._run(path_or_url)
464
        _assert_path(self, path_or_url)
465
        self._run(self.path)
466

    
467

    
468
@command(file_cmds)
469
class file_delete(_pithos_container):
470
    """Delete a file or directory object"""
471

    
472
    arguments = dict(
473
        until_date=DateArgument('remove history until then', '--until'),
474
        yes=FlagArgument('Do not prompt for permission', '--yes'),
475
        recursive=FlagArgument(
476
            'If a directory, empty first', ('-r', '--recursive')),
477
        delimiter=ValueArgument(
478
            'delete objects prefixed with <object><delimiter>', '--delimiter')
479
    )
480

    
481
    @errors.generic.all
482
    @errors.pithos.connection
483
    @errors.pithos.container
484
    @errors.pithos.object_path
485
    def _run(self):
486
        if self.path:
487
            if self['yes'] or self.ask_user(
488
                    'Delete /%s/%s ?' % (self.container, self.path)):
489
                self.client.del_object(
490
                    self.path,
491
                    until=self['until_date'],
492
                    delimiter='/' if self['recursive'] else self['delimiter'])
493
            else:
494
                self.error('Aborted')
495
        else:
496
            if self['yes'] or self.ask_user(
497
                    'Empty container /%s ?' % self.container):
498
                self.client.container_delete(self.container, delimiter='/')
499
            else:
500
                self.error('Aborted')
501

    
502
    def main(self, path_or_url):
503
        super(self.__class__, self)._run(path_or_url)
504
        self._run()
505

    
506

    
507
class _source_destination(_pithos_container, _optional_output_cmd):
508

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

    
525
    def __init__(self, arguments={}, auth_base=None, cloud=None):
526
        self.arguments.update(arguments)
527
        self.arguments.update(self.sd_arguments)
528
        super(_source_destination, self).__init__(
529
            self.arguments, auth_base, cloud)
530
        self.arguments['destination_user'].account_client = self.auth_base
531

    
532
    def _report_transfer(self, src, dst, transfer_name):
533
        if not dst:
534
            if transfer_name in ('move', ):
535
                self.error('  delete source directory %s' % src)
536
            return
537
        dst_prf = '' if self.account == self.dst_client.account else (
538
                'pithos://%s' % self.dst_client.account)
539
        if src:
540
            src_prf = '' if self.account == self.dst_client.account else (
541
                    'pithos://%s' % self.account)
542
            self.error('  %s %s/%s/%s\n  -->  %s/%s/%s' % (
543
                transfer_name,
544
                src_prf, self.container, src,
545
                dst_prf, self.dst_client.container, dst))
546
        else:
547
            self.error('  mkdir %s/%s/%s' % (
548
                dst_prf, self.dst_client.container, dst))
549

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

    
651
    def _run(self, source_path_or_url, destination_path_or_url=''):
652
        super(_source_destination, self)._run(source_path_or_url)
653
        dst_acc, dst_con, dst_path = self._resolve_pithos_url(
654
            destination_path_or_url)
655
        self.dst_client = PithosClient(
656
            base_url=self.client.base_url, token=self.client.token,
657
            container=self[
658
                'destination_container'] or dst_con or self.client.container,
659
            account=self['destination_user'] or dst_acc or self.account)
660
        self.dst_path = dst_path or self.path
661

    
662

    
663
@command(file_cmds)
664
class file_copy(_source_destination):
665
    """Copy objects, even between different accounts or containers"""
666

    
667
    arguments = dict(
668
        public=ValueArgument('publish new object', '--public'),
669
        content_type=ValueArgument(
670
            'change object\'s content type', '--content-type'),
671
        source_version=ValueArgument(
672
            'The version of the source object', '--object-version')
673
    )
674

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

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

    
700

    
701
@command(file_cmds)
702
class file_move(_source_destination):
703
    """Move objects, even between different accounts or containers"""
704

    
705
    arguments = dict(
706
        public=ValueArgument('publish new object', '--public'),
707
        content_type=ValueArgument(
708
            'change object\'s content type', '--content-type')
709
    )
710

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

    
732
    def main(self, source_path_or_url, destination_path_or_url=None):
733
        super(file_move, self)._run(
734
            source_path_or_url, destination_path_or_url or '')
735
        self._run()
736

    
737

    
738
@command(file_cmds)
739
class file_append(_pithos_container, _optional_output_cmd):
740
    """Append local file to (existing) remote object
741
    The remote object should exist.
742
    If the remote object is a directory, it is transformed into a file.
743
    In the later case, objects under the directory remain intact.
744
    """
745

    
746
    arguments = dict(
747
        progress_bar=ProgressBarArgument(
748
            'do not show progress bar', ('-N', '--no-progress-bar'),
749
            default=False),
750
        max_threads=IntArgument('default: 1', '--threads'),
751
    )
752

    
753
    @errors.generic.all
754
    @errors.pithos.connection
755
    @errors.pithos.container
756
    @errors.pithos.object_path
757
    def _run(self, local_path):
758
        if self['max_threads'] > 0:
759
            self.client.MAX_THREADS = int(self['max_threads'])
760
        (progress_bar, upload_cb) = self._safe_progress_bar('Appending')
761
        try:
762
            with open(local_path, 'rb') as f:
763
                self._optional_output(
764
                    self.client.append_object(self.path, f, upload_cb))
765
        finally:
766
            self._safe_progress_bar_finish(progress_bar)
767

    
768
    def main(self, local_path, remote_path_or_url):
769
        super(self.__class__, self)._run(remote_path_or_url)
770
        self._run(local_path)
771

    
772

    
773
@command(file_cmds)
774
class file_truncate(_pithos_container, _optional_output_cmd):
775
    """Truncate remote file up to size"""
776

    
777
    arguments = dict(
778
        size_in_bytes=IntArgument('Length of file after truncation', '--size')
779
    )
780
    required = ('size_in_bytes', )
781

    
782
    @errors.generic.all
783
    @errors.pithos.connection
784
    @errors.pithos.container
785
    @errors.pithos.object_path
786
    @errors.pithos.object_size
787
    def _run(self, size):
788
        self._optional_output(self.client.truncate_object(self.path, size))
789

    
790
    def main(self, path_or_url):
791
        super(self.__class__, self)._run(path_or_url)
792
        self._run(size=self['size_in_bytes'])
793

    
794

    
795
@command(file_cmds)
796
class file_overwrite(_pithos_container, _optional_output_cmd):
797
    """Overwrite part of a remote file"""
798

    
799
    arguments = dict(
800
        progress_bar=ProgressBarArgument(
801
            'do not show progress bar', ('-N', '--no-progress-bar'),
802
            default=False),
803
        start_position=IntArgument('File position in bytes', '--from'),
804
        end_position=IntArgument('File position in bytes', '--to'),
805
        object_version=ValueArgument('File to overwrite', '--object-version'),
806
    )
807
    required = ('start_position', 'end_position')
808

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

    
830
    def main(self, local_path, path_or_url):
831
        super(self.__class__, self)._run(path_or_url)
832
        self.path = self.path or path.basename(local_path)
833
        self._run(
834
            local_path=local_path,
835
            start=self['start_position'],
836
            end=self['end_position'])
837

    
838

    
839
@command(file_cmds)
840
class file_upload(_pithos_container, _optional_output_cmd):
841
    """Upload a file"""
842

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

    
875
    def _sharing(self):
876
        sharing = dict()
877
        readlist = self['uuid_for_read_permission']
878
        if readlist:
879
            sharing['read'] = self['uuid_for_read_permission']
880
        writelist = self['uuid_for_write_permission']
881
        if writelist:
882
            sharing['write'] = self['uuid_for_write_permission']
883
        return sharing or None
884

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

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

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

    
1028
    def main(self, local_path, remote_path_or_url):
1029
        super(self.__class__, self)._run(remote_path_or_url)
1030
        remote_path = self.path or path.basename(path.abspath(local_path))
1031
        self._run(local_path=local_path, remote_path=remote_path)
1032

    
1033

    
1034
class RangeArgument(ValueArgument):
1035
    """
1036
    :value type: string of the form <start>-<end> where <start> and <end> are
1037
        integers
1038
    :value returns: the input string, after type checking <start> and <end>
1039
    """
1040

    
1041
    @property
1042
    def value(self):
1043
        return getattr(self, '_value', self.default)
1044

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

    
1069

    
1070
@command(file_cmds)
1071
class file_cat(_pithos_container):
1072
    """Fetch remote file contents"""
1073

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

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

    
1102
    def main(self, path_or_url):
1103
        super(self.__class__, self)._run(path_or_url)
1104
        self._run()
1105

    
1106

    
1107
@command(file_cmds)
1108
class file_download(_pithos_container):
1109
    """Download a remove file or directory object to local file system"""
1110

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

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

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

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

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

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

    
1320
    def main(self, remote_path_or_url, local_path=None):
1321
        super(self.__class__, self)._run(remote_path_or_url)
1322
        local_path = local_path or self.path or '.'
1323
        self._run(local_path=local_path)
1324

    
1325

    
1326
@command(container_cmds)
1327
class container_info(_pithos_account, _optional_json):
1328
    """Get information about a container"""
1329

    
1330
    arguments = dict(
1331
        until_date=DateArgument('show metadata until then', '--until'),
1332
        metadata=FlagArgument('Show only container metadata', '--metadata'),
1333
        sizelimit=FlagArgument(
1334
            'Show the maximum size limit for container', '--size-limit'),
1335
        in_bytes=FlagArgument('Show size limit in bytes', ('-b', '--bytes'))
1336
    )
1337

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

    
1357
    def main(self, container):
1358
        super(self.__class__, self)._run()
1359
        self.container, self.client.container = container, container
1360
        self._run()
1361

    
1362

    
1363
class VersioningArgument(ValueArgument):
1364

    
1365
    schemes = ('auto', 'none')
1366

    
1367
    @property
1368
    def value(self):
1369
        return getattr(self, '_value', None)
1370

    
1371
    @value.setter
1372
    def value(self, new_scheme):
1373
        if new_scheme:
1374
            new_scheme = new_scheme.lower()
1375
            if new_scheme not in self.schemes:
1376
                raise CLIInvalidArgument('Invalid versioning value', details=[
1377
                    'Valid versioning values are %s' % ', '.join(
1378
                        self.schemes)])
1379
            self._value = new_scheme
1380

    
1381

    
1382
@command(container_cmds)
1383
class container_modify(_pithos_account, _optional_json):
1384
    """Modify the properties of a container"""
1385

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

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

    
1423
    def main(self, container):
1424
        super(self.__class__, self)._run()
1425
        self.client.container, self.container = container, container
1426
        self._run(container=container)
1427

    
1428

    
1429
@command(container_cmds)
1430
class container_list(_pithos_account, _optional_json, _name_filter):
1431
    """List all containers, or their contents"""
1432

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

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

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

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

    
1533
    def main(self, container=None):
1534
        super(self.__class__, self)._run()
1535
        self.client.container, self.container = container, container
1536
        self._run(container)
1537

    
1538

    
1539
@command(container_cmds)
1540
class container_create(_pithos_account):
1541
    """Create a new container"""
1542

    
1543
    arguments = dict(
1544
        versioning=ValueArgument(
1545
            'set container versioning (auto/none)', '--versioning'),
1546
        limit=IntArgument('set default container limit', '--limit'),
1547
        meta=KeyValueArgument(
1548
            'set container metadata (can be repeated)', '--meta')
1549
    )
1550

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

    
1569
    def main(self, new_container):
1570
        super(self.__class__, self)._run()
1571
        self._run(container=new_container)
1572

    
1573

    
1574
@command(container_cmds)
1575
class container_delete(_pithos_account):
1576
    """Delete a container"""
1577

    
1578
    arguments = dict(
1579
        yes=FlagArgument('Do not prompt for permission', '--yes'),
1580
        recursive=FlagArgument(
1581
            'delete container even if not empty', ('-r', '--recursive'))
1582
    )
1583

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

    
1602
    def main(self, container):
1603
        super(self.__class__, self)._run()
1604
        self.container, self.client.container = container, container
1605
        self._run(container)
1606

    
1607

    
1608
@command(container_cmds)
1609
class container_empty(_pithos_account):
1610
    """Empty a container"""
1611

    
1612
    arguments = dict(yes=FlagArgument('Do not prompt for permission', '--yes'))
1613

    
1614
    @errors.generic.all
1615
    @errors.pithos.connection
1616
    @errors.pithos.container
1617
    def _run(self, container):
1618
        if self['yes'] or self.ask_user('Empty container %s ?' % container):
1619
            self.client.del_container(delimiter='/')
1620

    
1621
    def main(self, container):
1622
        super(self.__class__, self)._run()
1623
        self.container, self.client.container = container, container
1624
        self._run(container)
1625

    
1626

    
1627
@command(sharer_cmds)
1628
class sharer_list(_pithos_account, _optional_json):
1629
    """List accounts who share file objects with current user"""
1630

    
1631
    arguments = dict(
1632
        detail=FlagArgument('show detailed output', ('-l', '--details')),
1633
        marker=ValueArgument('show output greater then marker', '--marker')
1634
    )
1635

    
1636
    @errors.generic.all
1637
    @errors.pithos.connection
1638
    def _run(self):
1639
        accounts = self.client.get_sharing_accounts(marker=self['marker'])
1640
        if not (self['json_output'] or self['output_format']):
1641
            usernames = self._uuids2usernames(
1642
                [acc['name'] for acc in accounts])
1643
            for item in accounts:
1644
                uuid = item['name']
1645
                item['id'], item['name'] = uuid, usernames[uuid]
1646
                if not self['detail']:
1647
                    item.pop('last_modified')
1648
        self._print(accounts)
1649

    
1650
    def main(self):
1651
        super(self.__class__, self)._run()
1652
        self._run()
1653

    
1654

    
1655
@command(sharer_cmds)
1656
class sharer_info(_pithos_account, _optional_json):
1657
    """Details on a Pithos+ sharer account (default: current account)"""
1658

    
1659
    @errors.generic.all
1660
    @errors.pithos.connection
1661
    def _run(self):
1662
        self._print(self.client.get_account_info(), self.print_dict)
1663

    
1664
    def main(self, account_uuid_or_name=None):
1665
        super(self.__class__, self)._run()
1666
        if account_uuid_or_name:
1667
            arg = UserAccountArgument('Check', ' ')
1668
            arg.account_client = self.auth_base
1669
            arg.value = account_uuid_or_name
1670
            self.client.account, self.account = arg.value, arg.value
1671
        self._run()
1672

    
1673

    
1674
class _pithos_group(_pithos_account):
1675
    prefix = 'x-account-group-'
1676
    preflen = len(prefix)
1677

    
1678
    def _groups(self):
1679
        groups = dict()
1680
        for k, v in self.client.get_account_group().items():
1681
            groups[k[self.preflen:]] = v
1682
        return groups
1683

    
1684

    
1685
@command(group_cmds)
1686
class group_list(_pithos_group, _optional_json):
1687
    """list all groups and group members"""
1688

    
1689
    @errors.generic.all
1690
    @errors.pithos.connection
1691
    def _run(self):
1692
        self._print(self._groups(), self.print_dict)
1693

    
1694
    def main(self):
1695
        super(self.__class__, self)._run()
1696
        self._run()
1697

    
1698

    
1699
@command(group_cmds)
1700
class group_create(_pithos_group, _optional_json):
1701
    """Create a group of users"""
1702

    
1703
    arguments = dict(
1704
        user_uuid=RepeatableArgument('Add a user to the group', '--uuid'),
1705
        username=RepeatableArgument('Add a user to the group', '--username')
1706
    )
1707
    required = ['user_uuid', 'username']
1708

    
1709
    @errors.generic.all
1710
    @errors.pithos.connection
1711
    def _run(self, groupname, *users):
1712
        if groupname in self._groups() and not self.ask_user(
1713
                'Group %s already exists, overwrite?' % groupname):
1714
            self.error('Aborted')
1715
            return
1716
        self.client.set_account_group(groupname, users)
1717
        self._print(self._groups(), self.print_dict)
1718

    
1719
    def main(self, groupname):
1720
        super(self.__class__, self)._run()
1721
        users = (self['user_uuid'] or []) + self._usernames2uuids(
1722
            self['username'] or []).values()
1723
        if users:
1724
            self._run(groupname, *users)
1725
        else:
1726
            raise CLISyntaxError(
1727
                'No valid users specified, use %s or %s' % (
1728
                    self.arguments['user_uuid'].lvalue,
1729
                    self.arguments['username'].lvalue),
1730
                details=[
1731
                    'Check if a username or uuid is valid with',
1732
                    '  user uuid2username', 'OR', '  user username2uuid'])
1733

    
1734

    
1735
@command(group_cmds)
1736
class group_delete(_pithos_group, _optional_json):
1737
    """Delete a user group"""
1738

    
1739
    @errors.generic.all
1740
    @errors.pithos.connection
1741
    def _run(self, groupname):
1742
        self.client.del_account_group(groupname)
1743
        self._print(self._groups(), self.print_dict)
1744

    
1745
    def main(self, groupname):
1746
        super(self.__class__, self)._run()
1747
        self._run(groupname)