Statistics
| Branch: | Tag: | Revision:

root / tools / store @ a2defd86

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

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

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

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

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

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

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

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

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

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

    
413
@cli_command('update')
414
class UpdateObject(Command):
415
    syntax = '<container>/<object> path [key=val] [...]'
416
    description = 'update object metadata/data (default mode: append)'
417
    
418
    def add_options(self, parser):
419
        parser.add_option('-a', action='store_true', dest='append',
420
                          default=True, help='append data')
421
        parser.add_option('--start', action='store',
422
                          dest='start',
423
                          default=None, help='starting offest to be updated')
424
        parser.add_option('--range', action='store', dest='content-range',
425
                          default=None, help='range of data to be updated')
426
        parser.add_option('--chunked', action='store_true', dest='chunked',
427
                          default=False, help='set chunked transfer mode')
428
        parser.add_option('--content-encoding', action='store',
429
                          dest='content-encoding', default=None,
430
                          help='provide the object MIME content type')
431
        parser.add_option('--content-disposition', action='store', type='str',
432
                          dest='content-disposition', default=None,
433
                          help='provide the presentation style of the object')
434
        parser.add_option('--manifest', action='store', type='str',
435
                          dest='manifest', default=None,
436
                          help='use for large file support')        
437
        parser.add_option('--sharing', action='store',
438
                          dest='sharing', default=None,
439
                          help='define sharing object policy')
440
        parser.add_option('--touch', action='store_true',
441
                          dest='touch', default=False,
442
                          help='change file properties')
443
        
444

    
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()