Statistics
| Branch: | Tag: | Revision:

root / snf-pithos-lib / pithos / lib / client.py @ 8c306eab

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

    
919
def _prepare_path(path, api, format='text', params={}):
920
    slash = '/' if api else ''
921
    full_path = '%s%s%s?format=%s' % (slash, api, quote(path), format)
922
    
923
    for k,v in params.items():
924
        value = quote(str(v)) if v else ''
925
        full_path = '%s&%s=%s' %(full_path, quote(k), value)
926
    return full_path
927

    
928
def _prepare_headers(headers):
929
    for k,v in headers.items():
930
        headers.pop(k)
931
        k = k.replace('_', '-')
932
        headers[quote(k)] = quote(v, safe='/=,:@ *"') if type(v) == types.StringType else v
933
    return headers
934

    
935
def _handle_response(response, verbose=False, debug=False):
936
    headers = response.getheaders()
937
    headers = dict((unquote(h), unquote(v)) for h,v in headers)
938
    
939
    if verbose:
940
        print '%d %s' % (response.status, response.reason)
941
        for key, val in headers.items():
942
            print '%s: %s' % (key.capitalize(), val)
943
        print
944
    
945
    length = response.getheader('content-length', None)
946
    data = response.read(length)
947
    if debug:
948
        print data
949
        print
950
    
951
    if int(response.status) in ERROR_CODES.keys():
952
        raise Fault(data, int(response.status))
953
    
954
    #print '**',  response.status, headers, data, '\n'
955
    return response.status, headers, data