Statistics
| Branch: | Tag: | Revision:

root / tools / store @ 10998c60

History | View | Annotate | Download (24.6 kB)

1
#!/usr/bin/env python
2

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

    
36
from getpass import getuser
37
from optparse import OptionParser
38
from os.path import basename
39
from sys import argv, exit, stdin, stdout
40
from pithos.lib.client import Client, Fault
41
from datetime import datetime
42

    
43
import json
44
import logging
45
import types
46
import re
47
import time as _time
48

    
49
DEFAULT_HOST = 'pithos.dev.grnet.gr'
50
DEFAULT_API = 'v1'
51

    
52
_cli_commands = {}
53

    
54
def cli_command(*args):
55
    def decorator(cls):
56
        cls.commands = args
57
        for name in args:
58
            _cli_commands[name] = cls
59
        return cls
60
    return decorator
61

    
62
def class_for_cli_command(name):
63
    return _cli_commands[name]
64

    
65
class Command(object):
66
    syntax = ''
67
    
68
    def __init__(self, name, argv):
69
        parser = OptionParser('%%prog %s [options] %s' % (name, self.syntax))
70
        parser.add_option('--host', dest='host', metavar='HOST',
71
                          default=DEFAULT_HOST, help='use server HOST')
72
        parser.add_option('--user', dest='user', metavar='USERNAME',
73
                          default=getuser(), help='use account USERNAME')
74
        parser.add_option('--api', dest='api', metavar='API',
75
                          default=DEFAULT_API, help='use api API')
76
        parser.add_option('-v', action='store_true', dest='verbose',
77
                          default=False, help='use verbose output')
78
        parser.add_option('-d', action='store_true', dest='debug',
79
                          default=False, help='use debug output')
80
        self.add_options(parser)
81
        options, args = parser.parse_args(argv)
82
        
83
        # Add options to self
84
        for opt in parser.option_list:
85
            key = opt.dest
86
            if key:
87
                val = getattr(options, key)
88
                setattr(self, key, val)
89
        
90
        self.client = Client(self.host, self.user, self.api, self.verbose,
91
                             self.debug)
92
        
93
        self.parser = parser
94
        self.args = args
95
        
96
    def add_options(self, parser):
97
        pass
98
    
99
    def execute(self, *args):
100
        pass
101

    
102
@cli_command('list', 'ls')
103
class List(Command):
104
    syntax = '[<container>[/<object>]]'
105
    description = 'list containers or objects'
106
    
107
    def add_options(self, parser):
108
        parser.add_option('-l', action='store_true', dest='detail',
109
                          default=False, help='show detailed output')
110
        parser.add_option('-n', action='store', type='int', dest='limit',
111
                          default=1000, help='show limited output')
112
        parser.add_option('--marker', action='store', type='str',
113
                          dest='marker', default=None,
114
                          help='show output greater then marker')
115
        parser.add_option('--prefix', action='store', type='str',
116
                          dest='prefix', default=None,
117
                          help='show output starting with prefix')
118
        parser.add_option('--delimiter', action='store', type='str',
119
                          dest='delimiter', default=None,
120
                          help='show output up to the delimiter')
121
        parser.add_option('--path', action='store', type='str',
122
                          dest='path', default=None,
123
                          help='show output starting with prefix up to /')
124
        parser.add_option('--meta', action='store', type='str',
125
                          dest='meta', default=None,
126
                          help='show output having the specified meta keys')
127
        parser.add_option('--if-modified-since', action='store', type='str',
128
                          dest='if_modified_since', default=None,
129
                          help='show output if modified since then')
130
        parser.add_option('--if-unmodified-since', action='store', type='str',
131
                          dest='if_unmodified_since', default=None,
132
                          help='show output if not modified since then')
133
        parser.add_option('--until', action='store', dest='until',
134
                          default=False, help='show metadata until that date')
135
        parser.add_option('--format', action='store', dest='format',
136
                          default='%d/%m/%Y', help='format to parse until date')
137
    
138
    def execute(self, container=None):
139
        if container:
140
            self.list_objects(container)
141
        else:
142
            self.list_containers()
143
    
144
    def list_containers(self):
145
        params = {'limit':self.limit, 'marker':self.marker}
146
        headers = {'IF_MODIFIED_SINCE':self.if_modified_since,
147
                   'IF_UNMODIFIED_SINCE':self.if_unmodified_since}
148
        
149
        if self.until:
150
            t = _time.strptime(self.until, self.format)
151
            params['until'] = int(_time.mktime(t))
152
        
153
        l = self.client.list_containers(self.detail, params, headers)
154
        print_list(l)
155
    
156
    def list_objects(self, container):
157
        params = {'limit':self.limit, 'marker':self.marker,
158
                  'prefix':self.prefix, 'delimiter':self.delimiter,
159
                  'path':self.path, 'meta':self.meta}
160
        headers = {'IF_MODIFIED_SINCE':self.if_modified_since,
161
                   'IF_UNMODIFIED_SINCE':self.if_unmodified_since}
162
        container, sep, object = container.partition('/')
163
        if object:
164
            return
165
        
166
        if self.until:
167
            t = _time.strptime(self.until, self.format)
168
            params['until'] = int(_time.mktime(t))
169
        
170
        detail = 'json'
171
        #if request with meta quering disable trash filtering
172
        show_trashed = True if self.meta else False
173
        l = self.client.list_objects(container, detail, params, headers,
174
                                     include_trashed = show_trashed)
175
        print_list(l, detail=self.detail)
176

    
177
@cli_command('meta')
178
class Meta(Command):
179
    syntax = '[<container>[/<object>]]'
180
    description = 'get the metadata of an account, a container or an object'
181
    
182
    def add_options(self, parser):
183
        parser.add_option('-r', action='store_true', dest='restricted',
184
                          default=False, help='show only user defined metadata')
185
        parser.add_option('--until', action='store', dest='until',
186
                          default=False, help='show metadata until that date')
187
        parser.add_option('--format', action='store', dest='format',
188
                          default='%d/%m/%Y', help='format to parse until date')
189
        parser.add_option('--version', action='store', dest='version',
190
                          default=None, help='show specific version \
191
                                  (applies only for objects)')
192
    
193
    def execute(self, path=''):
194
        container, sep, object = path.partition('/')
195
        if self.until:
196
            t = _time.strptime(self.until, self.format)
197
            self.until = int(_time.mktime(t))
198
        if object:
199
            meta = self.client.retrieve_object_metadata(container, object,
200
                                                        self.restricted,
201
                                                        self.version)
202
        elif container:
203
            meta = self.client.retrieve_container_metadata(container,
204
                                                           self.restricted,
205
                                                           self.until)
206
        else:
207
            meta = self.client.account_metadata(self.restricted, self.until)
208
        if meta == None:
209
            print 'Entity does not exist'
210
        else:
211
            print_dict(meta, header=None)
212

    
213
@cli_command('create')
214
class CreateContainer(Command):
215
    syntax = '<container> [key=val] [...]'
216
    description = 'create a container'
217
    
218
    def execute(self, container, *args):
219
        headers = {}
220
        for arg in args:
221
            key, sep, val = arg.partition('=')
222
            headers['X_CONTAINER_META_%s' %key.strip().upper()] = val.strip()
223
        ret = self.client.create_container(container, headers)
224
        if not ret:
225
            print 'Container already exists'
226

    
227
@cli_command('delete', 'rm')
228
class Delete(Command):
229
    syntax = '<container>[/<object>]'
230
    description = 'delete a container or an object'
231
    
232
    def execute(self, path):
233
        container, sep, object = path.partition('/')
234
        if object:
235
            self.client.delete_object(container, object)
236
        else:
237
            self.client.delete_container(container)
238

    
239
@cli_command('get')
240
class GetObject(Command):
241
    syntax = '<container>/<object>'
242
    description = 'get the data of an object'
243
    
244
    def add_options(self, parser):
245
        parser.add_option('-l', action='store_true', dest='detail',
246
                          default=False, help='show detailed output')
247
        parser.add_option('--range', action='store', dest='range',
248
                          default=None, help='show range of data')
249
        parser.add_option('--if-match', action='store', dest='if-match',
250
                          default=None, help='show output if ETags match')
251
        parser.add_option('--if-none-match', action='store',
252
                          dest='if-none-match', default=None,
253
                          help='show output if ETags don\'t match')
254
        parser.add_option('--if-modified-since', action='store', type='str',
255
                          dest='if-modified-since', default=None,
256
                          help='show output if modified since then')
257
        parser.add_option('--if-unmodified-since', action='store', type='str',
258
                          dest='if-unmodified-since', default=None,
259
                          help='show output if not modified since then')
260
        parser.add_option('-f', action='store', type='str',
261
                          dest='file', default=None,
262
                          help='save output in file')
263
        parser.add_option('--version', action='store', type='str',
264
                          dest='version', default=None,
265
                          help='get the specific \
266
                               version')
267
        parser.add_option('--versionlist', action='store_true',
268
                          dest='versionlist', default=False,
269
                          help='get the full object version list')
270
    
271
    def execute(self, path):
272
        headers = {}
273
        if self.range:
274
            headers['RANGE'] = 'bytes=%s' %self.range
275
        attrs = ['if-match', 'if-none-match', 'if-modified-since',
276
                 'if-unmodified-since']
277
        attrs = [a for a in attrs if getattr(self, a)]
278
        for a in attrs:
279
            headers[a.replace('-', '_').upper()] = getattr(self, a)
280
        container, sep, object = path.partition('/')
281
        if self.versionlist:
282
            self.version = 'list'
283
            self.detail = True
284
        data = self.client.retrieve_object(container, object, self.detail,
285
                                          headers, self.version)
286
        f = self.file and open(self.file, 'w') or stdout
287
        if self.detail:
288
            data = json.loads(data)
289
            if self.versionlist:
290
                print_versions(data, f=f)
291
            else:
292
                print_dict(data, f=f)
293
        else:
294
            f.write(data)
295
        f.close()
296

    
297
@cli_command('mkdir')
298
class PutMarker(Command):
299
    syntax = '<container>/<directory marker>'
300
    description = 'create a directory marker'
301
    
302
    def execute(self, path):
303
        container, sep, object = path.partition('/')
304
        self.client.create_directory_marker(container, object)
305

    
306
@cli_command('put')
307
class PutObject(Command):
308
    syntax = '<container>/<object> <path> [key=val] [...]'
309
    description = 'create/override object with path contents or standard input'
310
    
311
    def add_options(self, parser):
312
        parser.add_option('--chunked', action='store_true', dest='chunked',
313
                          default=False, help='set chunked transfer mode')
314
        parser.add_option('--etag', action='store', dest='etag',
315
                          default=None, help='check written data')
316
        parser.add_option('--content-encoding', action='store',
317
                          dest='content-encoding', default=None,
318
                          help='provide the object MIME content type')
319
        parser.add_option('--content-disposition', action='store', type='str',
320
                          dest='content-disposition', default=None,
321
                          help='provide the presentation style of the object')
322
        parser.add_option('-S', action='store',
323
                          dest='segment-size', default=False,
324
                          help='use for large file support')
325
        parser.add_option('--manifest', action='store_true',
326
                          dest='manifest', default=None,
327
                          help='upload a manifestation file')
328
        parser.add_option('--type', action='store',
329
                          dest='content-type', default=False,
330
                          help='create object with specific content type')
331
        parser.add_option('--touch', action='store_true',
332
                          dest='touch', default=False,
333
                          help='create object with zero data')
334
        parser.add_option('--sharing', action='store',
335
                          dest='sharing', default=None,
336
                          help='define sharing object policy')
337
        
338
    def execute(self, path, srcpath='-', *args):
339
        headers = {}
340
        if self.manifest:
341
            headers['X_OBJECT_MANIFEST'] = self.manifest
342
        if self.sharing:
343
            headers['X_OBJECT_SHARING'] = self.sharing
344
        
345
        attrs = ['etag', 'content-encoding', 'content-disposition',
346
                 'content-type']
347
        attrs = [a for a in attrs if getattr(self, a)]
348
        for a in attrs:
349
            headers[a.replace('-', '_').upper()] = getattr(self, a)
350
        
351
        #prepare user defined meta
352
        for arg in args:
353
            key, sep, val = arg.partition('=')
354
            headers['X_OBJECT_META_%s' %key.strip().upper()] = val.strip()
355
        
356
        container, sep, object = path.partition('/')
357
        
358
        f = None
359
        chunked = False
360
        if not self.touch:
361
            f = srcpath != '-' and open(srcpath) or stdin
362
            chunked = (self.chunked or f == stdin) and True or False
363
        self.client.create_object(container, object, f, chunked=chunked,
364
                                  headers=headers)
365
        if f:
366
            f.close()
367

    
368
@cli_command('copy', 'cp')
369
class CopyObject(Command):
370
    syntax = '<src container>/<src object> [<dst container>/]<dst object>'
371
    description = 'copies an object to a different location'
372
    
373
    def add_options(self, parser):
374
        parser.add_option('--version', action='store',
375
                          dest='version', default=False,
376
                          help='copy specific version')
377
    
378
    def execute(self, src, dst):
379
        src_container, sep, src_object = src.partition('/')
380
        dst_container, sep, dst_object = dst.partition('/')
381
        if not sep:
382
            dst_container = src_container
383
            dst_object = dst
384
        version = getattr(self, 'version')
385
        if version:
386
            headers = {}
387
            headers['X_SOURCE_VERSION'] = version
388
        self.client.copy_object(src_container, src_object, dst_container,
389
                                dst_object, headers)
390

    
391
@cli_command('set')
392
class SetMeta(Command):
393
    syntax = '[<container>[/<object>]] key=val [key=val] [...]'
394
    description = 'set metadata'
395
    
396
    def execute(self, path, *args):
397
        #in case of account fix the args
398
        if path.find('=') != -1:
399
            args = list(args)
400
            args.append(path)
401
            args = tuple(args)
402
            path = ''
403
        meta = {}
404
        for arg in args:
405
            key, sep, val = arg.partition('=')
406
            meta[key.strip()] = val.strip()
407
        container, sep, object = path.partition('/')
408
        if object:
409
            self.client.update_object_metadata(container, object, **meta)
410
        elif container:
411
            self.client.update_container_metadata(container, **meta)
412
        else:
413
            self.client.update_account_metadata(**meta)
414

    
415
@cli_command('update')
416
class UpdateObject(Command):
417
    syntax = '<container>/<object> path [key=val] [...]'
418
    description = 'update object metadata/data (default mode: append)'
419
    
420
    def add_options(self, parser):
421
        parser.add_option('-a', action='store_true', dest='append',
422
                          default=True, help='append data')
423
        parser.add_option('--start', action='store',
424
                          dest='start',
425
                          default=None, help='starting offest to be updated')
426
        parser.add_option('--range', action='store', dest='content-range',
427
                          default=None, help='range of data to be updated')
428
        parser.add_option('--chunked', action='store_true', dest='chunked',
429
                          default=False, help='set chunked transfer mode')
430
        parser.add_option('--content-encoding', action='store',
431
                          dest='content-encoding', default=None,
432
                          help='provide the object MIME content type')
433
        parser.add_option('--content-disposition', action='store', type='str',
434
                          dest='content-disposition', default=None,
435
                          help='provide the presentation style of the object')
436
        parser.add_option('--manifest', action='store', type='str',
437
                          dest='manifest', default=None,
438
                          help='use for large file support')        
439
        parser.add_option('--sharing', action='store',
440
                          dest='sharing', default=None,
441
                          help='define sharing object policy')
442
        parser.add_option('--touch', action='store_true',
443
                          dest='touch', default=False,
444
                          help='change object properties')
445
    
446
    def execute(self, path, srcpath='-', *args):
447
        headers = {}
448
        if self.manifest:
449
            headers['X_OBJECT_MANIFEST'] = self.manifest
450
        if self.sharing:
451
            headers['X_OBJECT_SHARING'] = self.sharing
452
        
453
        if getattr(self, 'start'):
454
            headers['CONTENT_RANGE'] = 'bytes %s-/*' % getattr(self, 'start')
455
        elif self.append:
456
            headers['CONTENT_RANGE'] = 'bytes */*'
457
        
458
        attrs = ['content-encoding', 'content-disposition']
459
        attrs = [a for a in attrs if getattr(self, a)]
460
        for a in attrs:
461
            headers[a.replace('-', '_').upper()] = getattr(self, a)
462
        
463
        #prepare user defined meta
464
        for arg in args:
465
            key, sep, val = arg.partition('=')
466
            headers['X_OBJECT_META_%s' %key.strip().upper()] = val.strip()
467
        
468
        container, sep, object = path.partition('/')
469
        
470
        f = None
471
        chunked = False
472
        if not self.touch:
473
            f = srcpath != '-' and open(srcpath) or stdin
474
            chunked = (self.chunked or f == stdin) and True or False
475
        self.client.update_object(container, object, f, chunked=chunked,
476
                                  headers=headers)
477
        if f:
478
            f.close()
479

    
480
@cli_command('move', 'mv')
481
class MoveObject(Command):
482
    syntax = '<src container>/<src object> [<dst container>/]<dst object>'
483
    description = 'moves an object to a different location'
484
    
485
    def execute(self, src, dst):
486
        src_container, sep, src_object = src.partition('/')
487
        dst_container, sep, dst_object = dst.partition('/')
488
        if not sep:
489
            dst_container = src_container
490
            dst_object = dst
491
        
492
        self.client.move_object(src_container, src_object, dst_container,
493
                                dst_object, headers)
494

    
495
@cli_command('remove')
496
class TrashObject(Command):
497
    syntax = '<container>/<object>'
498
    description = 'trashes an object'
499
    
500
    def execute(self, src):
501
        src_container, sep, src_object = src.partition('/')
502
        
503
        self.client.trash_object(src_container, src_object)
504

    
505
@cli_command('restore')
506
class TrashObject(Command):
507
    syntax = '<container>/<object>'
508
    description = 'trashes an object'
509
    
510
    def execute(self, src):
511
        src_container, sep, src_object = src.partition('/')
512
        
513
        self.client.restore_object(src_container, src_object)
514

    
515
@cli_command('unset')
516
class TrashObject(Command):
517
    syntax = '<container>/[<object>] key [key] [...]'
518
    description = 'deletes metadata info'
519
    
520
    def execute(self, path, *args):
521
        #in case of account fix the args
522
        if len(args) == 0:
523
            args = list(args)
524
            args.append(path)
525
            args = tuple(args)
526
            path = ''
527
        meta = []
528
        for key in args:
529
            meta.append(key)
530
        container, sep, object = path.partition('/')
531
        if object:
532
            self.client.delete_object_metadata(container, object, meta)
533
        elif container:
534
            self.client.delete_container_metadata(container, meta)
535
        else:
536
            self.client.delete_account_metadata(meta)
537

    
538
@cli_command('group')
539
class SetGroup(Command):
540
    syntax = 'key=val [key=val] [...]'
541
    description = 'sets group account info'
542
    
543
    def execute(self, path='', **args):
544
        if len(args) == 0:
545
            args = list(args)
546
            args.append(path)
547
            args = tuple(args)
548
            path = ''
549
        groups = {}
550
        for arg in args:
551
            key, sep, val = arg.partition('=')
552
            groups[key] = val
553
        self.client.set_account_groups(groups)
554

    
555
def print_usage():
556
    cmd = Command('', [])
557
    parser = cmd.parser
558
    parser.usage = '%prog <command> [options]'
559
    parser.print_help()
560
    
561
    commands = []
562
    for cls in set(_cli_commands.values()):
563
        name = ', '.join(cls.commands)
564
        description = getattr(cls, 'description', '')
565
        commands.append('  %s %s' % (name.ljust(12), description))
566
    print '\nCommands:\n' + '\n'.join(sorted(commands))
567

    
568
def print_dict(d, header='name', f=stdout, detail=True):
569
    header = header in d and header or 'subdir'
570
    if header and header in d:
571
        f.write('%s\n' %d.pop(header))
572
    if detail:
573
        patterns = ['^x_(account|container|object)_meta_(\w+)$']
574
        patterns.append(patterns[0].replace('_', '-'))
575
        for key, val in sorted(d.items()):
576
            for p in patterns:
577
                p = re.compile(p)
578
                m = p.match(key)
579
                if m:
580
                    key = m.group(2)
581
            f.write('%s: %s\n' % (key.rjust(30), val))
582

    
583
def print_list(l, verbose=False, f=stdout, detail=True):
584
    for elem in l:
585
        #if it's empty string continue
586
        if not elem:
587
            continue
588
        if type(elem) == types.DictionaryType:
589
            print_dict(elem, f=f, detail=detail)
590
        elif type(elem) == types.StringType:
591
            if not verbose:
592
                elem = elem.split('Traceback')[0]
593
            f.write('%s\n' % elem)
594
        else:
595
            f.write('%s\n' % elem)
596

    
597
def print_versions(data, f=stdout):
598
    if 'versions' not in data:
599
        f.write('%s\n' %data)
600
        return
601
    f.write('versions:\n')
602
    for id, t in data['versions']:
603
        f.write('%s @ %s\n' % (str(id).rjust(30), datetime.fromtimestamp(t)))
604

    
605
def main():
606
    try:
607
        name = argv[1]
608
        cls = class_for_cli_command(name)
609
    except (IndexError, KeyError):
610
        print_usage()
611
        exit(1)
612
    
613
    cmd = cls(name, argv[2:])
614
    
615
    try:
616
        cmd.execute(*cmd.args)
617
    except TypeError, e:
618
        cmd.parser.print_help()
619
        exit(1)
620
    except Fault, f:
621
        status = f.status and '%s ' % f.status or ''
622
        print '%s%s' % (status, f.data)
623

    
624
if __name__ == '__main__':
625
    main()