Statistics
| Branch: | Tag: | Revision:

root / snf-pithos-tools / pithos / tools / lib / client.py @ 6e147ecc

History | View | Annotate | Download (41.1 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, HTTPSConnection, HTTP
35
from sys import stdin
36
from xml.dom import minidom
37
from StringIO import StringIO
38
from urllib import quote, unquote
39
from urlparse import urlparse
40

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

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

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

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

    
943
def _prepare_path(path, format='text', params={}):
944
    full_path = '%s?format=%s' % (quote(path), format)
945
    
946
    for k,v in params.items():
947
        value = quote(str(v)) if v else ''
948
        full_path = '%s&%s=%s' %(full_path, quote(k), value)
949
    return full_path
950

    
951
def _prepare_headers(headers):
952
    for k,v in headers.items():
953
        headers.pop(k)
954
        k = k.replace('_', '-')
955
        headers[quote(k)] = quote(v, safe='/=,:@ *"') if type(v) == types.StringType else v
956
    return headers
957

    
958
def _handle_response(response, verbose=False, debug=False):
959
    headers = response.getheaders()
960
    headers = dict((unquote(h), unquote(v)) for h,v in headers)
961
    
962
    if verbose:
963
        print '%d %s' % (response.status, response.reason)
964
        for key, val in headers.items():
965
            print '%s: %s' % (key.capitalize(), val)
966
        print
967
    
968
    length = response.getheader('content-length', None)
969
    data = response.read(length)
970
    if debug:
971
        print data
972
        print
973
    
974
    if int(response.status) in ERROR_CODES.keys():
975
        raise Fault(data, int(response.status))
976
    
977
    #print '**',  response.status, headers, data, '\n'
978
    return response.status, headers, data