Statistics
| Branch: | Tag: | Revision:

root / pithos / lib / client.py @ 53cff70c

History | View | Annotate | Download (40.4 kB)

1
# Copyright 2011-2012 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
               500:'Internal Server Error',
58
               501:'Not Implemented'}
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
        params = {}
452
        for k, v in meta.items():
453
            headers['x-object-meta-%s' % k] = v
454
        if remove:
455
            headers['x-move-from'] = '/%s/%s' % (src_container, src_object)
456
        else:
457
            headers['x-copy-from'] = '/%s/%s' % (src_container, src_object)
458
        headers['content_length'] = 0
459
        if content_type:
460
            headers['content_type'] = content_type
461
        else:
462
            params['ignore_content_type'] = ''
463
        return self.put(path, headers=headers, params=params)
464
    
465
    def copy_object(self, src_container, src_object, dst_container, dst_object,
466
                   meta={}, account=None, content_type=None, **headers):
467
        """copies an object"""
468
        account = account or self.account
469
        return self._change_obj_location(src_container, src_object,
470
                                   dst_container, dst_object, account=account,
471
                                   remove=False, meta=meta,
472
                                   content_type=content_type, **headers)
473
    
474
    def move_object(self, src_container, src_object, dst_container,
475
                             dst_object, meta={}, account=None,
476
                             content_type=None, **headers):
477
        """moves an object"""
478
        account = account or self.account
479
        return self._change_obj_location(src_container, src_object,
480
                                         dst_container, dst_object,
481
                                         account=account, remove=True,
482
                                         meta=meta, content_type=content_type,
483
                                         **headers)
484
    
485
    def delete_object(self, container, object, params={}, account=None):
486
        """deletes an object"""
487
        account = account or self.account
488
        return self.delete('/%s/%s/%s' % (account, container, object),
489
                           params=params)
490
    
491
    def retrieve_object_metadata(self, container, object, restricted=False,
492
                                 version=None, account=None):
493
        """
494
        set restricted to True to get only user defined metadata
495
        """
496
        account = account or self.account
497
        path = '/%s/%s/%s' % (account, container, object)
498
        prefix = 'x-object-meta-' if restricted else None
499
        params = {'version':version} if version else {}
500
        return self._get_metadata(path, prefix, params=params)
501
    
502
    def update_object_metadata(self, container, object, account=None,
503
                               **meta):
504
        """
505
        updates object's metadata
506
        """
507
        account = account or self.account
508
        path = '/%s/%s/%s' % (account, container, object)
509
        return self._update_metadata(path, 'object', **meta)
510
    
511
    def delete_object_metadata(self, container, object, meta=[], account=None):
512
        """
513
        deletes object's metadata
514
        """
515
        account = account or self.account
516
        path = '/%s/%s' % (account, container, object)
517
        return self._delete_metadata(path, 'object', meta)
518
    
519
class Pithos_Client(OOS_Client):
520
    """Pithos Storage Client. Extends OOS_Client"""
521
    
522
    def _update_metadata(self, path, entity, **meta):
523
        """
524
        adds new and updates the values of previously set metadata
525
        """
526
        params = {'update':None}
527
        headers = {}
528
        prefix = 'x-%s-meta-' % entity
529
        for k,v in meta.items():
530
            k = '%s%s' % (prefix, k)
531
            headers[k] = v
532
        return self.post(path, headers=headers, params=params)
533
    
534
    def _delete_metadata(self, path, entity, meta=[]):
535
        """
536
        delete previously set metadata
537
        """
538
        params = {'update':None}
539
        headers = {}
540
        prefix = 'x-%s-meta-' % entity
541
        for m in meta:
542
            headers['%s%s' % (prefix, m)] = ''
543
        return self.post(path, headers=headers, params=params)
544
    
545
    # Storage Account Services
546
    
547
    def list_containers(self, format='text', if_modified_since=None,
548
                        if_unmodified_since=None, limit=None, marker=None,
549
                        shared=False, until=None, account=None):
550
        """returns a list with the account containers"""
551
        account = account or self.account
552
        params = {'until':until} if until else {}
553
        if shared:
554
            params['shared'] = None
555
        headers = {'if-modified-since':if_modified_since,
556
                   'if-unmodified-since':if_unmodified_since}
557
        return OOS_Client.list_containers(self, account=account, format=format,
558
                                          limit=limit, marker=marker,
559
                                          params=params, **headers)
560
    
561
    def retrieve_account_metadata(self, restricted=False, until=None,
562
                                  account=None):
563
        """returns the account metadata"""
564
        account = account or self.account
565
        params = {'until':until} if until else {}
566
        return OOS_Client.retrieve_account_metadata(self, account=account,
567
                                                    restricted=restricted,
568
                                                    **params)
569
    
570
    def set_account_groups(self, account=None, **groups):
571
        """create account groups"""
572
        account = account or self.account
573
        path = '/%s' % account
574
        headers = {}
575
        for k, v in groups.items():
576
            headers['x-account-group-%s' % k] = v
577
        params = {'update':None}
578
        return self.post(path, headers=headers, params=params)
579
    
580
    def retrieve_account_groups(self, account=None):
581
        """returns the account groups"""
582
        account = account or self.account
583
        meta = self.retrieve_account_metadata(account=account)
584
        prefix = 'x-account-group-'
585
        prefixlen = len(prefix)
586
        groups = {}
587
        for key, val in meta.items():
588
            if prefix and not key.startswith(prefix):
589
                continue
590
            elif prefix and key.startswith(prefix):
591
                key = key[prefixlen:]
592
            groups[key] = val
593
        return groups
594
    
595
    def unset_account_groups(self, groups=[], account=None):
596
        """delete account groups"""
597
        account = account or self.account
598
        path = '/%s' % account
599
        headers = {}
600
        for elem in groups:
601
            headers['x-account-group-%s' % elem] = ''
602
        params = {'update':None}
603
        return self.post(path, headers=headers, params=params)
604
    
605
    def reset_account_groups(self, account=None, **groups):
606
        """overrides account groups"""
607
        account = account or self.account
608
        path = '/%s' % account
609
        headers = {}
610
        for k, v in groups.items():
611
            v = v.strip()
612
            headers['x-account-group-%s' % k] = v
613
        meta = self.retrieve_account_metadata(restricted=True)
614
        prefix = 'x-account-meta-'
615
        for k,v in meta.items():
616
            k = '%s%s' % (prefix, k)
617
            headers[k] = v
618
        return self.post(path, headers=headers)
619
    
620
    # Storage Container Services
621
    
622
    def list_objects(self, container, format='text',
623
                     limit=None, marker=None, prefix=None, delimiter=None,
624
                     path=None, shared=False, include_trashed=False, params={},
625
                     if_modified_since=None, if_unmodified_since=None, meta='',
626
                     until=None, account=None):
627
        """returns a list with the container objects"""
628
        account = account or self.account
629
        params = {'until':until, 'meta':meta}
630
        if shared:
631
            params['shared'] = None
632
        args = locals().copy()
633
        for elem in ['self', 'container', 'params', 'until', 'meta']:
634
            args.pop(elem)
635
        return OOS_Client.list_objects(self, container, params=params, **args)
636
    
637
    def retrieve_container_metadata(self, container, restricted=False,
638
                                    until=None, account=None):
639
        """returns container's metadata"""
640
        account = account or self.account
641
        params = {'until':until} if until else {}
642
        return OOS_Client.retrieve_container_metadata(self, container,
643
                                                      account=account,
644
                                                      restricted=restricted,
645
                                                      **params)
646
    
647
    def set_container_policies(self, container, account=None,
648
                               **policies):
649
        """sets containers policies"""
650
        account = account or self.account
651
        path = '/%s/%s' % (account, container)
652
        headers = {}
653
        for key, val in policies.items():
654
            headers['x-container-policy-%s' % key] = val
655
        return self.post(path, headers=headers)
656
    
657
    def update_container_data(self, container, f=stdin):
658
        """adds blocks of data to the container"""
659
        account = self.account
660
        path = '/%s/%s' % (account, container)
661
        params = {'update': None}
662
        headers = {'content_type': 'application/octet-stream'}
663
        data = f.read() if f else None
664
        headers['content_length'] = len(data)
665
        return self.post(path, data, headers=headers, params=params)
666
    
667
    def delete_container(self, container, until=None, account=None):
668
        """deletes a container or the container history until the date provided"""
669
        account = account or self.account
670
        params = {'until':until} if until else {}
671
        return OOS_Client.delete_container(self, container, account=account,
672
                                           params=params)
673
    
674
    # Storage Object Services
675
    
676
    def retrieve_object(self, container, object, params={}, format='text',
677
                        range=None, if_range=None,
678
                        if_match=None, if_none_match=None,
679
                        if_modified_since=None, if_unmodified_since=None,
680
                        account=None, **headers):
681
        """returns an object"""
682
        account = account or self.account
683
        headers={}
684
        l = ['range', 'if_range', 'if_match', 'if_none_match',
685
             'if_modified_since', 'if_unmodified_since']
686
        l = [elem for elem in l if eval(elem)]
687
        for elem in l:
688
            headers.update({elem:eval(elem)})
689
        if format != 'text':
690
            params['hashmap'] = None
691
        return OOS_Client.retrieve_object(self, container, object,
692
                                          account=account, format=format,
693
                                          params=params, **headers)
694
    
695
    def retrieve_object_version(self, container, object, version,
696
                                format='text', range=None, if_range=None,
697
                                if_match=None, if_none_match=None,
698
                                if_modified_since=None, if_unmodified_since=None,
699
                                account=None):
700
        """returns a specific object version"""
701
        account = account or self.account
702
        args = locals().copy()
703
        l = ['self', 'container', 'object']
704
        for elem in l:
705
            args.pop(elem)
706
        params = {'version':version}
707
        return self.retrieve_object(container, object, params=params, **args)
708
    
709
    def retrieve_object_versionlist(self, container, object, range=None,
710
                                    if_range=None, if_match=None,
711
                                    if_none_match=None, if_modified_since=None,
712
                                    if_unmodified_since=None, account=None):
713
        """returns the object version list"""
714
        account = account or self.account
715
        args = locals().copy()
716
        l = ['self', 'container', 'object']
717
        for elem in l:
718
            args.pop(elem)
719
        
720
        return self.retrieve_object_version(container, object, version='list',
721
                                            format='json', **args)
722
    
723
    def create_zero_length_object(self, container, object,
724
                                  meta={}, etag=None, content_type=None,
725
                                  content_encoding=None,
726
                                  content_disposition=None,
727
                                  x_object_manifest=None, x_object_sharing=None,
728
                                  x_object_public=None, account=None):
729
        """createas a zero length object"""
730
        account = account or self.account
731
        args = locals().copy()
732
        for elem in ['self', 'container', 'object']:
733
            args.pop(elem)
734
        return OOS_Client.create_zero_length_object(self, container, object,
735
                                                    **args)
736
    
737
    def create_object(self, container, object, f=stdin, format='text',
738
                      meta={}, params={}, etag=None, content_type=None,
739
                      content_encoding=None, content_disposition=None,
740
                      x_object_manifest=None, x_object_sharing=None,
741
                      x_object_public=None, account=None):
742
        """creates an object"""
743
        account = account or self.account
744
        args = locals().copy()
745
        for elem in ['self', 'container', 'object']:
746
            args.pop(elem)
747
        if format != 'text':
748
            params.update({'hashmap':None})
749
        return OOS_Client.create_object(self, container, object, **args)
750
        
751
    def create_object_using_chunks(self, container, object,
752
                                   f=stdin, blocksize=1024, meta={}, etag=None,
753
                                   content_type=None, content_encoding=None,
754
                                   content_disposition=None,
755
                                   x_object_sharing=None, x_object_manifest=None,
756
                                   x_object_public=None, account=None):
757
        """creates an object (incremental upload)"""
758
        account = account or self.account
759
        path = '/%s/%s/%s' % (account, container, object)
760
        headers = {}
761
        l = ['etag', 'content_type', 'content_encoding', 'content_disposition', 
762
             'x_object_sharing', 'x_object_manifest', 'x_object_public']
763
        l = [elem for elem in l if eval(elem)]
764
        for elem in l:
765
            headers.update({elem:eval(elem)})
766
        headers.setdefault('content-type', 'application/octet-stream')
767
        
768
        for k,v in meta.items():
769
            v = v.strip()
770
            headers['x-object-meta-%s' %k.strip()] = v
771
        
772
        return self._chunked_transfer(path, 'PUT', f, headers=headers,
773
                                      blocksize=blocksize)
774
    
775
    def create_object_by_hashmap(self, container, object, hashmap={},
776
                                 meta={}, etag=None, content_encoding=None,
777
                                 content_disposition=None, content_type=None,
778
                                 x_object_sharing=None, x_object_manifest=None,
779
                                 x_object_public = None, account=None):
780
        """creates an object by uploading hashes representing data instead of data"""
781
        account = account or self.account
782
        args = locals().copy()
783
        for elem in ['self', 'container', 'object', 'hashmap']:
784
            args.pop(elem)
785
            
786
        try:
787
            data = json.dumps(hashmap)
788
        except SyntaxError:
789
            raise Fault('Invalid formatting')
790
        args['params'] = {'hashmap':None}
791
        args['format'] = 'json'
792
        
793
        return self.create_object(container, object, f=StringIO(data), **args)
794
    
795
    def create_manifestation(self, container, object, manifest, account=None):
796
        """creates a manifestation"""
797
        account = account or self.account
798
        headers={'x_object_manifest':manifest}
799
        return self.create_object(container, object, f=None, account=account,
800
                                  **headers)
801
    
802
    def update_object(self, container, object, f=stdin,
803
                      offset=None, meta={}, replace=False, content_length=None,
804
                      content_type=None, content_range=None,
805
                      content_encoding=None, content_disposition=None,
806
                      x_object_bytes=None, x_object_manifest=None,
807
                      x_object_sharing=None, x_object_public=None,
808
                      x_source_object=None, account=None):
809
        """updates an object"""
810
        account = account or self.account
811
        args = locals().copy()
812
        for elem in ['self', 'container', 'object', 'replace']:
813
            args.pop(elem)
814
        if not replace:
815
            args['params'] = {'update':None}
816
        return OOS_Client.update_object(self, container, object, **args)
817
    
818
    def update_object_using_chunks(self, container, object, f=stdin,
819
                                   blocksize=1024, offset=None, meta={},
820
                                   replace=False, content_type=None, content_encoding=None,
821
                                   content_disposition=None, x_object_bytes=None,
822
                                   x_object_manifest=None, x_object_sharing=None,
823
                                   x_object_public=None, account=None):
824
        """updates an object (incremental upload)"""
825
        account = account or self.account
826
        args = locals().copy()
827
        for elem in ['self', 'container', 'object', 'replace']:
828
            args.pop(elem)
829
        if not replace:
830
            args['params'] = {'update':None}
831
        return OOS_Client.update_object_using_chunks(self, container, object, **args)
832
    
833
    def update_from_other_source(self, container, object, source,
834
                      offset=None, meta={}, content_range=None,
835
                      content_encoding=None, content_disposition=None,
836
                      x_object_bytes=None, x_object_manifest=None,
837
                      x_object_sharing=None, x_object_public=None, account=None):
838
        """updates an object"""
839
        account = account or self.account
840
        args = locals().copy()
841
        for elem in ['self', 'container', 'object', 'source']:
842
            args.pop(elem)
843
        
844
        args['x_source_object'] = source
845
        return self.update_object(container, object, f=None, **args)
846
    
847
    def delete_object(self, container, object, until=None, account=None):
848
        """deletes an object or the object history until the date provided"""
849
        account = account or self.account
850
        params = {'until':until} if until else {}
851
        return OOS_Client.delete_object(self, container, object, params, account)
852
    
853
    def trash_object(self, container, object):
854
        """trashes an object"""
855
        account = account or self.account
856
        path = '/%s/%s' % (container, object)
857
        meta = {'trash':'true'}
858
        return self._update_metadata(path, 'object', **meta)
859
    
860
    def restore_object(self, container, object, account=None):
861
        """restores a trashed object"""
862
        account = account or self.account
863
        return self.delete_object_metadata(container, object, account, ['trash'])
864
    
865
    def publish_object(self, container, object, account=None):
866
        """sets a previously created object publicly accessible"""
867
        account = account or self.account
868
        path = '/%s/%s/%s' % (account, container, object)
869
        headers = {}
870
        headers['x_object_public'] = True
871
        params = {'update':None}
872
        return self.post(path, headers=headers, params=params)
873
    
874
    def unpublish_object(self, container, object, account=None):
875
        """unpublish an object"""
876
        account = account or self.account
877
        path = '/%s/%s/%s' % (account, container, object)
878
        headers = {}
879
        headers['x_object_public'] = False
880
        params = {'update':None}
881
        return self.post(path, headers=headers, params=params)
882
    
883
    def copy_object(self, src_container, src_object, dst_container, dst_object,
884
                    meta={}, public=False, version=None, account=None,
885
                    content_type=None):
886
        """copies an object"""
887
        account = account or self.account
888
        headers = {}
889
        headers['x_object_public'] = public
890
        if version:
891
            headers['x_source_version'] = version
892
        return OOS_Client.copy_object(self, src_container, src_object,
893
                                      dst_container, dst_object, meta=meta,
894
                                      account=account, content_type=content_type,
895
                                      **headers)
896
    
897
    def move_object(self, src_container, src_object, dst_container,
898
                             dst_object, meta={}, public=False,
899
                             account=None, content_type=None):
900
        """moves an object"""
901
        headers = {}
902
        headers['x_object_public'] = public
903
        return OOS_Client.move_object(self, src_container, src_object,
904
                                      dst_container, dst_object, meta=meta,
905
                                      account=account, content_type=content_type,
906
                                      **headers)
907
    
908
    def list_shared_by_others(self, limit=None, marker=None, format='text'):
909
        """lists other accounts that share objects to the user"""
910
        l = ['limit', 'marker']
911
        params = {}
912
        for elem in [elem for elem in l if eval(elem)]:
913
            params[elem] = eval(elem)
914
        return self._list('', format, params)
915
    
916
    def share_object(self, container, object, l, read=True):
917
        """gives access(read by default) to an object to a user/group list"""
918
        action = 'read' if read else 'write'
919
        sharing = '%s=%s' % (action, ','.join(l))
920
        self.update_object(container, object, f=None, x_object_sharing=sharing)
921

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

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

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