Statistics
| Branch: | Tag: | Revision:

root / tools / lib / client.py @ 423edf28

History | View | Annotate | Download (41.1 kB)

1
# Copyright 2011 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.
33

    
34
from httplib import HTTPConnection, HTTP
35
from sys import stdin
36
from xml.dom import minidom
37
from StringIO import StringIO
38
from urllib import quote
39

    
40
import json
41
import types
42
import socket
43
import urllib
44
import datetime
45

    
46
ERROR_CODES = {304:'Not Modified',
47
               400:'Bad Request',
48
               401:'Unauthorized',
49
               403:'Forbidden',
50
               404:'Not Found',
51
               409:'Conflict',
52
               411:'Length Required',
53
               412:'Precondition Failed',
54
               413:'Request Entity Too Large',
55
               416:'Range Not Satisfiable',
56
               422:'Unprocessable Entity',
57
               503:'Service Unavailable',
58
               }
59

    
60
class Fault(Exception):
61
    def __init__(self, data='', status=None):
62
        if data == '' and status in ERROR_CODES.keys():
63
            data = ERROR_CODES[status]
64
        Exception.__init__(self, data)
65
        self.data = data
66
        self.status = status
67

    
68
class Client(object):
69
    def __init__(self, host, token, account, api='v1', verbose=False, debug=False):
70
        """`host` can also include a port, e.g '127.0.0.1:8000'."""
71
        
72
        self.host = host
73
        self.account = account
74
        self.api = api
75
        self.verbose = verbose or debug
76
        self.debug = debug
77
        self.token = token
78
    
79
    def _req(self, method, path, body=None, headers={}, format='text', params={}):
80
        slash = '/' if self.api else ''
81
        full_path = '%s%s%s?format=%s' % (slash, self.api, quote(path), format)
82
        
83
        for k,v in params.items():
84
            if v:
85
                full_path = '%s&%s=%s' %(full_path, quote(k), quote(unicode(v)))
86
            else:
87
                full_path = '%s&%s=' %(full_path, k)
88
        conn = HTTPConnection(self.host)
89
        
90
        kwargs = {}
91
        for k,v in headers.items():
92
            headers.pop(k)
93
            k = k.replace('_', '-')
94
            headers[k] = v
95
        
96
        kwargs['headers'] = headers
97
        kwargs['headers']['X-Auth-Token'] = self.token
98
        if body:
99
            kwargs['body'] = body
100
            kwargs['headers'].setdefault('content-type', 'application/octet-stream')
101
        kwargs['headers'].setdefault('content-length', len(body) if body else 0)
102
        
103
        #print '#', method, full_path, kwargs
104
        t1 = datetime.datetime.utcnow()
105
        conn.request(method, full_path, **kwargs)
106
        
107
        resp = conn.getresponse()
108
        t2 = datetime.datetime.utcnow()
109
        #print 'response time:', str(t2-t1)
110
        headers = dict(resp.getheaders())
111
        
112
        if self.verbose:
113
            print '%d %s' % (resp.status, resp.reason)
114
            for key, val in headers.items():
115
                print '%s: %s' % (key.capitalize(), val)
116
            print
117
        
118
        length = resp.getheader('content-length', None)
119
        data = resp.read(length)
120
        if self.debug:
121
            print data
122
            print
123
        
124
        if int(resp.status) in ERROR_CODES.keys():
125
            raise Fault(data, int(resp.status))
126
        
127
        #print '**',  resp.status, headers, data, '\n'
128
        return resp.status, headers, data
129
    
130
    def _chunked_transfer(self, path, method='PUT', f=stdin, headers=None,
131
                          blocksize=1024, params={}):
132
        """perfomrs a chunked request"""
133
        http = HTTPConnection(self.host)
134
        
135
        # write header
136
        full_path = '/%s%s?' % (self.api, path)
137
        
138
        for k,v in params.items():
139
            if v:
140
                full_path = '%s&%s=%s' %(full_path, k, v)
141
            else:
142
                full_path = '%s&%s=' %(full_path, k)
143
        
144
        full_path = urllib.quote(full_path, '?&:=/')
145
        
146
        http.putrequest(method, full_path)
147
        http.putheader('x-auth-token', self.token)
148
        http.putheader('content-type', 'application/octet-stream')
149
        http.putheader('transfer-encoding', 'chunked')
150
        if headers:
151
            for header,value in headers.items():
152
                http.putheader(header, value)
153
        http.endheaders()
154
        
155
        # write body
156
        data = ''
157
        while True:
158
            if f.closed:
159
                break
160
            block = f.read(blocksize)
161
            if block == '':
162
                break
163
            data = '%x\r\n%s\r\n' % (len(block), block)
164
            try:
165
                http.send(data)
166
            except:
167
                #retry
168
                http.send(data)
169
        data = '0\r\n\r\n'
170
        try:
171
            http.send(data)
172
        except:
173
            #retry
174
            http.send(data)
175
        
176
        # get response
177
        resp = http.getresponse()
178
        
179
        headers = dict(resp.getheaders())
180
        
181
        if self.verbose:
182
            print '%d %s' % (resp.status, resp.reason)
183
            for key, val in headers.items():
184
                print '%s: %s' % (key.capitalize(), val)
185
            print
186
        
187
        length = resp.getheader('Content-length', None)
188
        data = resp.read(length)
189
        if self.debug:
190
            print data
191
            print
192
        
193
        if int(resp.status) in ERROR_CODES.keys():
194
            raise Fault(data, int(resp.status))
195
        
196
        #print '*',  resp.status, headers, data
197
        return resp.status, headers, data
198
    
199
    def delete(self, path, format='text', params={}):
200
        return self._req('DELETE', path, format=format, params=params)
201
    
202
    def get(self, path, format='text', headers={}, params={}):
203
        return self._req('GET', path, headers=headers, format=format,
204
                        params=params)
205
    
206
    def head(self, path, format='text', params={}):
207
         return self._req('HEAD', path, format=format, params=params)
208
    
209
    def post(self, path, body=None, format='text', headers=None, params={}):
210
        return self._req('POST', path, body, headers=headers, format=format,
211
                        params=params)
212
    
213
    def put(self, path, body=None, format='text', headers=None, params={}):
214
        return self._req('PUT', path, body, headers=headers, format=format,
215
                         params=params)
216
    
217
    def _list(self, path, format='text', params={}, **headers):
218
        status, headers, data = self.get(path, format=format, headers=headers,
219
                                         params=params)
220
        if format == 'json':
221
            data = json.loads(data) if data else ''
222
        elif format == 'xml':
223
            data = minidom.parseString(data)
224
        else:
225
            data = data.split('\n')[:-1] if data else ''
226
        return data
227
    
228
    def _get_metadata(self, path, prefix=None, params={}):
229
        status, headers, data = self.head(path, params=params)
230
        prefixlen = len(prefix) if prefix else 0
231
        meta = {}
232
        for key, val in headers.items():
233
            if prefix and not key.startswith(prefix):
234
                continue
235
            elif prefix and key.startswith(prefix):
236
                key = key[prefixlen:]
237
            meta[key] = val
238
        return meta
239
    
240
    def _filter(self, l, d):
241
        """
242
        filter out from l elements having the metadata values provided
243
        """
244
        ll = l
245
        for elem in l:
246
            if type(elem) == types.DictionaryType:
247
                for key in d.keys():
248
                    k = 'x_object_meta_%s' % key
249
                    if k in elem.keys() and elem[k] == d[key]:
250
                        ll.remove(elem)
251
                        break
252
        return ll
253
    
254
class OOS_Client(Client):
255
    """Openstack Object Storage Client"""
256
    
257
    def _update_metadata(self, path, entity, **meta):
258
        """adds new and updates the values of previously set metadata"""
259
        ex_meta = self.retrieve_account_metadata(restricted=True)
260
        ex_meta.update(meta)
261
        headers = {}
262
        prefix = 'x-%s-meta-' % entity
263
        for k,v in ex_meta.items():
264
            k = '%s%s' % (prefix, k)
265
            headers[k] = v
266
        return self.post(path, headers=headers)
267
    
268
    def _reset_metadata(self, path, entity, **meta):
269
        """
270
        overwrites all user defined metadata
271
        """
272
        headers = {}
273
        prefix = 'x-%s-meta-' % entity
274
        for k,v in meta.items():
275
            k = '%s%s' % (prefix, k)
276
            headers[k] = v
277
        return self.post(path, headers=headers)
278
    
279
    def _delete_metadata(self, path, entity, meta=[]):
280
        """delete previously set metadata"""
281
        ex_meta = self.retrieve_account_metadata(restricted=True)
282
        headers = {}
283
        prefix = 'x-%s-meta-' % entity
284
        for k in ex_meta.keys():
285
            if k in meta:
286
                headers['%s%s' % (prefix, k)] = ex_meta[k]
287
        return self.post(path, headers=headers)
288
    
289
    # Storage Account Services
290
    
291
    def list_containers(self, format='text', limit=None,
292
                        marker=None, params={}, account=None, **headers):
293
        """lists containers"""
294
        account = account or self.account
295
        path = '/%s' % account
296
        params.update({'limit':limit, 'marker':marker})
297
        return self._list(path, format, params, **headers)
298
    
299
    def retrieve_account_metadata(self, restricted=False, account=None, **params):
300
        """returns the account metadata"""
301
        account = account or self.account
302
        path = '/%s' % account
303
        prefix = 'x-account-meta-' if restricted else None
304
        return self._get_metadata(path, prefix, params)
305
    
306
    def update_account_metadata(self, account=None, **meta):
307
        """updates the account metadata"""
308
        account = account or self.account
309
        path = '/%s' % account
310
        return self._update_metadata(path, 'account', **meta)
311
        
312
    def delete_account_metadata(self, meta=[], account=None):
313
        """deletes the account metadata"""
314
        account = account or self.account
315
        path = '/%s' % account
316
        return self._delete_metadata(path, 'account', meta)
317
    
318
    def reset_account_metadata(self, account=None, **meta):
319
        """resets account metadata"""
320
        account = account or self.account
321
        path = '/%s' % account
322
        return self._reset_metadata(path, 'account', **meta)
323
    
324
    # Storage Container Services
325
    
326
    def _filter_trashed(self, l):
327
        return self._filter(l, {'trash':'true'})
328
    
329
    def list_objects(self, container, format='text',
330
                     limit=None, marker=None, prefix=None, delimiter=None,
331
                     path=None, include_trashed=False, params={}, account=None,
332
                     **headers):
333
        """returns a list with the container objects"""
334
        account = account or self.account
335
        params.update({'limit':limit, 'marker':marker, 'prefix':prefix,
336
                       'delimiter':delimiter, 'path':path})
337
        l = self._list('/%s/%s' % (account, container), format, params,
338
                       **headers)
339
        #TODO support filter trashed with xml also
340
        if format != 'xml' and not include_trashed:
341
            l = self._filter_trashed(l)
342
        return l
343
    
344
    def create_container(self, container, account=None, **meta):
345
        """creates a container"""
346
        account = account or self.account
347
        headers = {}
348
        for k,v in meta.items():
349
            headers['x-container-meta-%s' %k.strip().upper()] = v.strip()
350
        status, header, data = self.put('/%s/%s' % (account, container),
351
                                        headers=headers)
352
        if status == 202:
353
            return False
354
        elif status != 201:
355
            raise Fault(data, int(status))
356
        return True
357
    
358
    def delete_container(self, container, params={}, account=None):
359
        """deletes a container"""
360
        account = account or self.account
361
        return self.delete('/%s/%s' % (account, container), params=params)
362
    
363
    def retrieve_container_metadata(self, container, restricted=False,
364
                                    account=None, **params):
365
        """returns the container metadata"""
366
        account = account or self.account
367
        prefix = 'x-container-meta-' if restricted else None
368
        return self._get_metadata('/%s/%s' % (account, container), prefix,
369
                                  params)
370
    
371
    def update_container_metadata(self, container, account=None, **meta):
372
        """unpdates the container metadata"""
373
        account = account or self.account
374
        return self._update_metadata('/%s/%s' % (account, container),
375
                                     'container', **meta)
376
        
377
    def delete_container_metadata(self, container, meta=[], account=None):
378
        """deletes the container metadata"""
379
        account = account or self.account
380
        path = '/%s/%s' % (account, container)
381
        return self._delete_metadata(path, 'container', meta)
382
    
383
    # Storage Object Services
384
    
385
    def request_object(self, container, object, format='text', params={},
386
                       account=None, **headers):
387
        """returns tuple containing the status, headers and data response for an object request"""
388
        account = account or self.account
389
        path = '/%s/%s/%s' % (account, container, object)
390
        status, headers, data = self.get(path, format, headers, params)
391
        return status, headers, data
392
    
393
    def retrieve_object(self, container, object, format='text', params={},
394
                        account=None, **headers):
395
        """returns an object's data"""
396
        account = account or self.account
397
        t = self.request_object(container, object, format, params, account,
398
                                **headers)
399
        data = t[2]
400
        if format == 'json':
401
            data = json.loads(data) if data else ''
402
        elif format == 'xml':
403
            data = minidom.parseString(data)
404
        return data
405
    
406
    def retrieve_object_hashmap(self, container, object, params={},
407
                        account=None, **headers):
408
        """returns the hashmap representing object's data"""
409
        args = locals().copy()
410
        for elem in ['self', 'container', 'object']:
411
            args.pop(elem)
412
        data = self.retrieve_object(container, object, format='json', **args)
413
        return data['hashes']
414
    
415
    def create_directory_marker(self, container, object, account=None):
416
        """creates a dierectory marker"""
417
        account = account or self.account
418
        if not object:
419
            raise Fault('Directory markers have to be nested in a container')
420
        h = {'content_type':'application/directory'}
421
        return self.create_zero_length_object(container, object, account=account,
422
                                              **h)
423
    
424
    def create_object(self, container, object, f=stdin, format='text', meta={},
425
                      params={}, etag=None, content_type=None, content_encoding=None,
426
                      content_disposition=None, account=None, **headers):
427
        """creates a zero-length object"""
428
        account = account or self.account
429
        path = '/%s/%s/%s' % (account, container, object)
430
        for k, v  in headers.items():
431
            if v == None:
432
                headers.pop(k)
433
        
434
        l = ['etag', 'content_encoding', 'content_disposition', 'content_type']
435
        l = [elem for elem in l if eval(elem)]
436
        for elem in l:
437
            headers.update({elem:eval(elem)})
438
        headers.setdefault('content-type', 'application/octet-stream')
439
        
440
        for k,v in meta.items():
441
            headers['x-object-meta-%s' %k.strip()] = v.strip()
442
        data = f.read() if f else None
443
        return self.put(path, data, format, headers=headers, params=params)
444
    
445
    def create_zero_length_object(self, container, object, meta={}, etag=None,
446
                                  content_type=None, content_encoding=None,
447
                                  content_disposition=None, account=None,
448
                                  **headers):
449
        account = account or self.account
450
        args = locals().copy()
451
        for elem in ['self', 'container', 'headers', 'account']:
452
            args.pop(elem)
453
        args.update(headers)
454
        return self.create_object(container, account=account, f=None, **args)
455
    
456
    def update_object(self, container, object, f=stdin,
457
                      offset=None, meta={}, params={}, content_length=None,
458
                      content_type=None, content_encoding=None,
459
                      content_disposition=None,  account=None, **headers):
460
        account = account or self.account
461
        path = '/%s/%s/%s' % (account, container, object)
462
        for k, v  in headers.items():
463
            if v == None:
464
                headers.pop(k)
465
        
466
        l = ['content_encoding', 'content_disposition', 'content_type',
467
             'content_length']
468
        l = [elem for elem in l if eval(elem)]
469
        for elem in l:
470
            headers.update({elem:eval(elem)})
471
        
472
        if 'content_range' not in headers.keys():
473
            if offset != None:
474
                headers['content_range'] = 'bytes %s-/*' % offset
475
            else:
476
                headers['content_range'] = 'bytes */*'
477
            
478
        for k,v in meta.items():
479
            headers['x-object-meta-%s' %k.strip()] = v.strip()
480
        data = f.read() if f else None
481
        return self.post(path, data, headers=headers, params=params)
482
    
483
    def update_object_using_chunks(self, container, object, f=stdin,
484
                                   blocksize=1024, offset=None, meta={},
485
                                   params={}, content_type=None, content_encoding=None,
486
                                   content_disposition=None, account=None, **headers):
487
        """updates an object (incremental upload)"""
488
        account = account or self.account
489
        path = '/%s/%s/%s' % (account, container, object)
490
        headers = headers if not headers else {}
491
        l = ['content_type', 'content_encoding', 'content_disposition']
492
        l = [elem for elem in l if eval(elem)]
493
        for elem in l:
494
            headers.update({elem:eval(elem)})
495
        
496
        if offset != None:
497
            headers['content_range'] = 'bytes %s-/*' % offset
498
        else:
499
            headers['content_range'] = 'bytes */*'
500
        
501
        for k,v in meta.items():
502
            v = v.strip()
503
            headers['x-object-meta-%s' %k.strip()] = v
504
        return self._chunked_transfer(path, 'POST', f, headers=headers,
505
                                      blocksize=blocksize, params=params)
506
    
507
    def _change_obj_location(self, src_container, src_object, dst_container,
508
                             dst_object, remove=False, meta={}, account=None,
509
                             content_type=None, **headers):
510
        account = account or self.account
511
        path = '/%s/%s/%s' % (account, dst_container, dst_object)
512
        headers = {} if not headers else headers
513
        for k, v in meta.items():
514
            headers['x-object-meta-%s' % k] = v
515
        if remove:
516
            headers['x-move-from'] = '/%s/%s' % (src_container, src_object)
517
        else:
518
            headers['x-copy-from'] = '/%s/%s' % (src_container, src_object)
519
        headers['content_length'] = 0
520
        if content_type:
521
            headers['content_type'] = content_type 
522
        return self.put(path, headers=headers)
523
    
524
    def copy_object(self, src_container, src_object, dst_container, dst_object,
525
                   meta={}, account=None, content_type=None, **headers):
526
        """copies an object"""
527
        account = account or self.account
528
        return self._change_obj_location(src_container, src_object,
529
                                   dst_container, dst_object, account=account,
530
                                   remove=False, meta=meta,
531
                                   content_type=content_type, **headers)
532
    
533
    def move_object(self, src_container, src_object, dst_container,
534
                             dst_object, meta={}, account=None,
535
                             content_type=None, **headers):
536
        """moves an object"""
537
        account = account or self.account
538
        return self._change_obj_location(src_container, src_object,
539
                                         dst_container, dst_object,
540
                                         account=account, remove=True,
541
                                         meta=meta, content_type=content_type,
542
                                         **headers)
543
    
544
    def delete_object(self, container, object, params={}, account=None):
545
        """deletes an object"""
546
        account = account or self.account
547
        return self.delete('/%s/%s/%s' % (account, container, object),
548
                           params=params)
549
    
550
    def retrieve_object_metadata(self, container, object, restricted=False,
551
                                 version=None, account=None):
552
        """
553
        set restricted to True to get only user defined metadata
554
        """
555
        account = account or self.account
556
        path = '/%s/%s/%s' % (account, container, object)
557
        prefix = 'x-object-meta-' if restricted else None
558
        params = {'version':version} if version else {}
559
        return self._get_metadata(path, prefix, params=params)
560
    
561
    def update_object_metadata(self, container, object, account=None,
562
                               **meta):
563
        """
564
        updates object's metadata
565
        """
566
        account = account or self.account
567
        path = '/%s/%s/%s' % (account, container, object)
568
        return self._update_metadata(path, 'object', **meta)
569
    
570
    def delete_object_metadata(self, container, object, meta=[], account=None):
571
        """
572
        deletes object's metadata
573
        """
574
        account = account or self.account
575
        path = '/%s/%s' % (account, container, object)
576
        return self._delete_metadata(path, 'object', meta)
577
    
578
class Pithos_Client(OOS_Client):
579
    """Pithos Storage Client. Extends OOS_Client"""
580
    
581
    def _update_metadata(self, path, entity, **meta):
582
        """
583
        adds new and updates the values of previously set metadata
584
        """
585
        params = {'update':None}
586
        headers = {}
587
        prefix = 'x-%s-meta-' % entity
588
        for k,v in meta.items():
589
            k = '%s%s' % (prefix, k)
590
            headers[k] = v
591
        return self.post(path, headers=headers, params=params)
592
    
593
    def _delete_metadata(self, path, entity, meta=[]):
594
        """
595
        delete previously set metadata
596
        """
597
        params = {'update':None}
598
        headers = {}
599
        prefix = 'x-%s-meta-' % entity
600
        for m in meta:
601
            headers['%s%s' % (prefix, m)] = ''
602
        return self.post(path, headers=headers, params=params)
603
    
604
    # Storage Account Services
605
    
606
    def list_containers(self, format='text', if_modified_since=None,
607
                        if_unmodified_since=None, limit=None, marker=None,
608
                        shared=False, until=None, account=None):
609
        """returns a list with the account containers"""
610
        account = account or self.account
611
        params = {'until':until} if until else {}
612
        if shared:
613
            params['shared'] = None
614
        headers = {'if-modified-since':if_modified_since,
615
                   'if-unmodified-since':if_unmodified_since}
616
        return OOS_Client.list_containers(self, account=account, format=format,
617
                                          limit=limit, marker=marker,
618
                                          params=params, **headers)
619
    
620
    def retrieve_account_metadata(self, restricted=False, until=None,
621
                                  account=None):
622
        """returns the account metadata"""
623
        account = account or self.account
624
        params = {'until':until} if until else {}
625
        return OOS_Client.retrieve_account_metadata(self, account=account,
626
                                                    restricted=restricted,
627
                                                    **params)
628
    
629
    def set_account_groups(self, account=None, **groups):
630
        """create account groups"""
631
        account = account or self.account
632
        path = '/%s' % account
633
        headers = {}
634
        for k, v in groups.items():
635
            headers['x-account-group-%s' % k] = v
636
        params = {'update':None}
637
        return self.post(path, headers=headers, params=params)
638
    
639
    def retrieve_account_groups(self, account=None):
640
        """returns the account groups"""
641
        account = account or self.account
642
        meta = self.retrieve_account_metadata(account=account)
643
        prefix = 'x-account-group-'
644
        prefixlen = len(prefix)
645
        groups = {}
646
        for key, val in meta.items():
647
            if prefix and not key.startswith(prefix):
648
                continue
649
            elif prefix and key.startswith(prefix):
650
                key = key[prefixlen:]
651
            groups[key] = val
652
        return groups
653
    
654
    def unset_account_groups(self, groups=[], account=None):
655
        """delete account groups"""
656
        account = account or self.account
657
        path = '/%s' % account
658
        headers = {}
659
        for elem in groups:
660
            headers['x-account-group-%s' % elem] = ''
661
        params = {'update':None}
662
        return self.post(path, headers=headers, params=params)
663
    
664
    def reset_account_groups(self, account=None, **groups):
665
        """overrides account groups"""
666
        account = account or self.account
667
        path = '/%s' % account
668
        headers = {}
669
        for k, v in groups.items():
670
            v = v.strip()
671
            headers['x-account-group-%s' % k] = v
672
        meta = self.retrieve_account_metadata(restricted=True)
673
        prefix = 'x-account-meta-'
674
        for k,v in meta.items():
675
            k = '%s%s' % (prefix, k)
676
            headers[k] = v
677
        return self.post(path, headers=headers)
678
    
679
    # Storage Container Services
680
    
681
    def list_objects(self, container, format='text',
682
                     limit=None, marker=None, prefix=None, delimiter=None,
683
                     path=None, shared=False, include_trashed=False, params={},
684
                     if_modified_since=None, if_unmodified_since=None, meta='',
685
                     until=None, account=None):
686
        """returns a list with the container objects"""
687
        account = account or self.account
688
        params = {'until':until, 'meta':meta}
689
        if shared:
690
            params['shared'] = None
691
        args = locals().copy()
692
        for elem in ['self', 'container', 'params', 'until', 'meta']:
693
            args.pop(elem)
694
        return OOS_Client.list_objects(self, container, params=params, **args)
695
    
696
    def retrieve_container_metadata(self, container, restricted=False,
697
                                    until=None, account=None):
698
        """returns container's metadata"""
699
        account = account or self.account
700
        params = {'until':until} if until else {}
701
        return OOS_Client.retrieve_container_metadata(self, container,
702
                                                      account=account,
703
                                                      restricted=restricted,
704
                                                      **params)
705
    
706
    def set_container_policies(self, container, account=None,
707
                               **policies):
708
        """sets containers policies"""
709
        account = account or self.account
710
        path = '/%s/%s' % (account, container)
711
        headers = {}
712
        for key, val in policies.items():
713
            headers['x-container-policy-%s' % key] = val
714
        return self.post(path, headers=headers)
715
    
716
    def update_container_data(self, container, f=stdin):
717
        """adds blocks of data to the container"""
718
        account = self.account
719
        path = '/%s/%s' % (account, container)
720
        params = {'update': None}
721
        headers = {'content_type': 'application/octet-stream'}
722
        data = f.read() if f else None
723
        headers['content_length'] = len(data)
724
        return self.post(path, data, headers=headers, params=params)
725
    
726
    def delete_container(self, container, until=None, account=None):
727
        """deletes a container or the container history until the date provided"""
728
        account = account or self.account
729
        params = {'until':until} if until else {}
730
        return OOS_Client.delete_container(self, container, account=account,
731
                                           params=params)
732
    
733
    # Storage Object Services
734
    
735
    def retrieve_object(self, container, object, params={}, format='text',
736
                        range=None, if_range=None,
737
                        if_match=None, if_none_match=None,
738
                        if_modified_since=None, if_unmodified_since=None,
739
                        account=None, **headers):
740
        """returns an object"""
741
        account = account or self.account
742
        headers={}
743
        l = ['range', 'if_range', 'if_match', 'if_none_match',
744
             'if_modified_since', 'if_unmodified_since']
745
        l = [elem for elem in l if eval(elem)]
746
        for elem in l:
747
            headers.update({elem:eval(elem)})
748
        if format != 'text':
749
            params['hashmap'] = None
750
        return OOS_Client.retrieve_object(self, container, object,
751
                                          account=account, format=format,
752
                                          params=params, **headers)
753
    
754
    def retrieve_object_version(self, container, object, version,
755
                                format='text', range=None, if_range=None,
756
                                if_match=None, if_none_match=None,
757
                                if_modified_since=None, if_unmodified_since=None,
758
                                account=None):
759
        """returns a specific object version"""
760
        account = account or self.account
761
        args = locals().copy()
762
        l = ['self', 'container', 'object']
763
        for elem in l:
764
            args.pop(elem)
765
        params = {'version':version}
766
        return self.retrieve_object(container, object, params=params, **args)
767
    
768
    def retrieve_object_versionlist(self, container, object, range=None,
769
                                    if_range=None, if_match=None,
770
                                    if_none_match=None, if_modified_since=None,
771
                                    if_unmodified_since=None, account=None):
772
        """returns the object version list"""
773
        account = account or self.account
774
        args = locals().copy()
775
        l = ['self', 'container', 'object']
776
        for elem in l:
777
            args.pop(elem)
778
        
779
        return self.retrieve_object_version(container, object, version='list',
780
                                            format='json', **args)
781
    
782
    def create_zero_length_object(self, container, object,
783
                                  meta={}, etag=None, content_type=None,
784
                                  content_encoding=None,
785
                                  content_disposition=None,
786
                                  x_object_manifest=None, x_object_sharing=None,
787
                                  x_object_public=None, account=None):
788
        """createas a zero length object"""
789
        account = account or self.account
790
        args = locals().copy()
791
        for elem in ['self', 'container', 'object']:
792
            args.pop(elem)
793
        return OOS_Client.create_zero_length_object(self, container, object,
794
                                                    **args)
795
    
796
    def create_object(self, container, object, f=stdin, format='text',
797
                      meta={}, params={}, etag=None, content_type=None,
798
                      content_encoding=None, content_disposition=None,
799
                      x_object_manifest=None, x_object_sharing=None,
800
                      x_object_public=None, account=None):
801
        """creates an object"""
802
        account = account or self.account
803
        args = locals().copy()
804
        for elem in ['self', 'container', 'object']:
805
            args.pop(elem)
806
        if format != 'text':
807
            params.update({'hashmap':None})
808
        return OOS_Client.create_object(self, container, object, **args)
809
        
810
    def create_object_using_chunks(self, container, object,
811
                                   f=stdin, blocksize=1024, meta={}, etag=None,
812
                                   content_type=None, content_encoding=None,
813
                                   content_disposition=None,
814
                                   x_object_sharing=None, x_object_manifest=None,
815
                                   x_object_public=None, account=None):
816
        """creates an object (incremental upload)"""
817
        account = account or self.account
818
        path = '/%s/%s/%s' % (account, container, object)
819
        headers = {}
820
        l = ['etag', 'content_type', 'content_encoding', 'content_disposition', 
821
             'x_object_sharing', 'x_object_manifest', 'x_object_public']
822
        l = [elem for elem in l if eval(elem)]
823
        for elem in l:
824
            headers.update({elem:eval(elem)})
825
        headers.setdefault('content-type', 'application/octet-stream')
826
        
827
        for k,v in meta.items():
828
            v = v.strip()
829
            headers['x-object-meta-%s' %k.strip()] = v
830
        
831
        return self._chunked_transfer(path, 'PUT', f, headers=headers,
832
                                      blocksize=blocksize)
833
    
834
    def create_object_by_hashmap(self, container, object, hashmap={},
835
                                 meta={}, etag=None, content_encoding=None,
836
                                 content_disposition=None, content_type=None,
837
                                 x_object_sharing=None, x_object_manifest=None,
838
                                 x_object_public = None, account=None):
839
        """creates an object by uploading hashes representing data instead of data"""
840
        account = account or self.account
841
        args = locals().copy()
842
        for elem in ['self', 'container', 'object', 'hashmap']:
843
            args.pop(elem)
844
            
845
        try:
846
            data = json.dumps(hashmap)
847
        except SyntaxError:
848
            raise Fault('Invalid formatting')
849
        args['params'] = {'hashmap':None}
850
        args['format'] = 'json'
851
        
852
        return self.create_object(container, object, f=StringIO(data), **args)
853
    
854
    def create_manifestation(self, container, object, manifest, account=None):
855
        """creates a manifestation"""
856
        account = account or self.account
857
        headers={'x_object_manifest':manifest}
858
        return self.create_object(container, object, f=None, account=account,
859
                                  **headers)
860
    
861
    def update_object(self, container, object, f=stdin,
862
                      offset=None, meta={}, replace=False, content_length=None,
863
                      content_type=None, content_range=None,
864
                      content_encoding=None, content_disposition=None,
865
                      x_object_bytes=None, x_object_manifest=None,
866
                      x_object_sharing=None, x_object_public=None,
867
                      x_source_object=None, account=None):
868
        """updates an object"""
869
        account = account or self.account
870
        args = locals().copy()
871
        for elem in ['self', 'container', 'object', 'replace']:
872
            args.pop(elem)
873
        if not replace:
874
            args['params'] = {'update':None}
875
        return OOS_Client.update_object(self, container, object, **args)
876
    
877
    def update_object_using_chunks(self, container, object, f=stdin,
878
                                   blocksize=1024, offset=None, meta={},
879
                                   replace=False, content_type=None, content_encoding=None,
880
                                   content_disposition=None, x_object_bytes=None,
881
                                   x_object_manifest=None, x_object_sharing=None,
882
                                   x_object_public=None, account=None):
883
        """updates an object (incremental upload)"""
884
        account = account or self.account
885
        args = locals().copy()
886
        for elem in ['self', 'container', 'object', 'replace']:
887
            args.pop(elem)
888
        if not replace:
889
            args['params'] = {'update':None}
890
        return OOS_Client.update_object_using_chunks(self, container, object, **args)
891
    
892
    def update_from_other_source(self, container, object, source,
893
                      offset=None, meta={}, content_range=None,
894
                      content_encoding=None, content_disposition=None,
895
                      x_object_bytes=None, x_object_manifest=None,
896
                      x_object_sharing=None, x_object_public=None, account=None):
897
        """updates an object"""
898
        account = account or self.account
899
        args = locals().copy()
900
        for elem in ['self', 'container', 'object', 'source']:
901
            args.pop(elem)
902
        
903
        args['x_source_object'] = source
904
        return self.update_object(container, object, f=None, **args)
905
    
906
    def delete_object(self, container, object, until=None, account=None):
907
        """deletes an object or the object history until the date provided"""
908
        account = account or self.account
909
        params = {'until':until} if until else {}
910
        return OOS_Client.delete_object(self, container, object, params, account)
911
    
912
    def trash_object(self, container, object):
913
        """trashes an object"""
914
        account = account or self.account
915
        path = '/%s/%s' % (container, object)
916
        meta = {'trash':'true'}
917
        return self._update_metadata(path, 'object', **meta)
918
    
919
    def restore_object(self, container, object, account=None):
920
        """restores a trashed object"""
921
        account = account or self.account
922
        return self.delete_object_metadata(container, object, account, ['trash'])
923
    
924
    def publish_object(self, container, object, account=None):
925
        """sets a previously created object publicly accessible"""
926
        account = account or self.account
927
        path = '/%s/%s/%s' % (account, container, object)
928
        headers = {}
929
        headers['x_object_public'] = True
930
        params = {'update':None}
931
        return self.post(path, headers=headers, params=params)
932
    
933
    def unpublish_object(self, container, object, account=None):
934
        """unpublish an object"""
935
        account = account or self.account
936
        path = '/%s/%s/%s' % (account, container, object)
937
        headers = {}
938
        headers['x_object_public'] = False
939
        params = {'update':None}
940
        return self.post(path, headers=headers, params=params)
941
    
942
    def copy_object(self, src_container, src_object, dst_container, dst_object,
943
                    meta={}, public=False, version=None, account=None,
944
                    content_type=None):
945
        """copies an object"""
946
        account = account or self.account
947
        headers = {}
948
        headers['x_object_public'] = public
949
        if version:
950
            headers['x_source_version'] = version
951
        return OOS_Client.copy_object(self, src_container, src_object,
952
                                      dst_container, dst_object, meta=meta,
953
                                      account=account, content_type=content_type,
954
                                      **headers)
955
    
956
    def move_object(self, src_container, src_object, dst_container,
957
                             dst_object, meta={}, public=False,
958
                             account=None, content_type=None):
959
        """moves an object"""
960
        headers = {}
961
        headers['x_object_public'] = public
962
        return OOS_Client.move_object(self, src_container, src_object,
963
                                      dst_container, dst_object, meta=meta,
964
                                      account=account, content_type=content_type,
965
                                      **headers)
966
    
967
    def list_shared_by_others(self, limit=None, marker=None, format='text'):
968
        """lists other accounts that share objects to the user"""
969
        l = ['limit', 'marker']
970
        params = {}
971
        for elem in [elem for elem in l if eval(elem)]:
972
            params[elem] = eval(elem)
973
        return self._list('', format, params)
974
    
975
    def share_object(self, container, object, l, read=True):
976
        """gives access(read by default) to an object to a user/group list"""
977
        action = 'read' if read else 'write'
978
        sharing = '%s=%s' % (action, ','.join(l))
979
        self.update_object(container, object, f=None, x_object_sharing=sharing)
980

    
981
def _encode_headers(headers):
982
    h = {}
983
    for k, v in headers.items():
984
        k = urllib.quote(k)
985
        if v and type(v) == types.StringType:
986
            v = urllib.quote(v, '/=,-* :"')
987
        h[k] = v
988
    return h