from httplib import HTTPConnection, HTTP
from sys import stdin
from xml.dom import minidom
+from StringIO import StringIO
+from urllib import quote, unquote
import json
import types
ERROR_CODES = {304:'Not Modified',
400:'Bad Request',
401:'Unauthorized',
+ 403:'Forbidden',
404:'Not Found',
409:'Conflict',
411:'Length Required',
412:'Precondition Failed',
+ 413:'Request Entity Too Large',
416:'Range Not Satisfiable',
422:'Unprocessable Entity',
- 503:'Service Unavailable'}
+ 503:'Service Unavailable',
+ }
class Fault(Exception):
def __init__(self, data='', status=None):
self.token = token
def _req(self, method, path, body=None, headers={}, format='text', params={}):
- full_path = '/%s%s?format=%s' % (self.api, path, format)
+ slash = '/' if self.api else ''
+ full_path = '%s%s%s?format=%s' % (slash, self.api, quote(path), format)
for k,v in params.items():
if v:
- full_path = '%s&%s=%s' %(full_path, k, v)
+ full_path = '%s&%s=%s' %(full_path, quote(k), quote(str(v)))
else:
- full_path = '%s&%s' %(full_path, k)
+ full_path = '%s&%s=' %(full_path, k)
conn = HTTPConnection(self.host)
- #encode whitespace
- full_path = full_path.replace(' ', '%20')
-
kwargs = {}
for k,v in headers.items():
headers.pop(k)
k = k.replace('_', '-')
- headers[k] = v
+ headers[k] = quote(v, safe='/=,:@ *"') if type(v) == types.StringType else v
kwargs['headers'] = headers
kwargs['headers']['X-Auth-Token'] = self.token
resp = conn.getresponse()
t2 = datetime.datetime.utcnow()
#print 'response time:', str(t2-t1)
- headers = dict(resp.getheaders())
+ headers = resp.getheaders()
+ headers = dict((unquote(h), unquote(v)) for h,v in headers)
if self.verbose:
print '%d %s' % (resp.status, resp.reason)
#print '**', resp.status, headers, data, '\n'
return resp.status, headers, data
+ def _chunked_transfer(self, path, method='PUT', f=stdin, headers=None,
+ blocksize=1024, params={}):
+ """perfomrs a chunked request"""
+ http = HTTPConnection(self.host)
+
+ # write header
+ full_path = '/%s%s?' % (self.api, path)
+
+ for k,v in params.items():
+ if v:
+ full_path = '%s&%s=%s' %(full_path, k, v)
+ else:
+ full_path = '%s&%s=' %(full_path, k)
+
+ full_path = urllib.quote(full_path, '?&:=/')
+
+ http.putrequest(method, full_path)
+ http.putheader('x-auth-token', self.token)
+ http.putheader('content-type', 'application/octet-stream')
+ http.putheader('transfer-encoding', 'chunked')
+ if headers:
+ for header,value in headers.items():
+ http.putheader(header, value)
+ http.endheaders()
+
+ # write body
+ data = ''
+ while True:
+ if f.closed:
+ break
+ block = f.read(blocksize)
+ if block == '':
+ break
+ data = '%x\r\n%s\r\n' % (len(block), block)
+ try:
+ http.send(data)
+ except:
+ #retry
+ http.send(data)
+ data = '0\r\n\r\n'
+ try:
+ http.send(data)
+ except:
+ #retry
+ http.send(data)
+
+ # get response
+ resp = http.getresponse()
+
+ headers = dict(resp.getheaders())
+
+ if self.verbose:
+ print '%d %s' % (resp.status, resp.reason)
+ for key, val in headers.items():
+ print '%s: %s' % (key.capitalize(), val)
+ print
+
+ length = resp.getheader('Content-length', None)
+ data = resp.read(length)
+ if self.debug:
+ print data
+ print
+
+ if int(resp.status) in ERROR_CODES.keys():
+ raise Fault(data, int(resp.status))
+
+ #print '*', resp.status, headers, data
+ return resp.status, headers, data
+
def delete(self, path, format='text', params={}):
return self._req('DELETE', path, format=format, params=params)
return self._req('POST', path, body, headers=headers, format=format,
params=params)
- def put(self, path, body=None, format='text', headers=None):
- return self._req('PUT', path, body, headers=headers, format=format)
+ def put(self, path, body=None, format='text', headers=None, params={}):
+ return self._req('PUT', path, body, headers=headers, format=format,
+ params=params)
def _list(self, path, format='text', params={}, **headers):
status, headers, data = self.get(path, format=format, headers=headers,
elif format == 'xml':
data = minidom.parseString(data)
else:
- data = data.strip().split('\n') if data else ''
+ data = data.split('\n')[:-1] if data else ''
return data
def _get_metadata(self, path, prefix=None, params={}):
def retrieve_object_hashmap(self, container, object, params={},
account=None, **headers):
"""returns the hashmap representing object's data"""
- args = locals()
+ args = locals().copy()
for elem in ['self', 'container', 'object']:
args.pop(elem)
- data = self.retrieve_object(container, object, format='json', **args)
- return data['hashes']
+ return self.retrieve_object(container, object, format='json', **args)
def create_directory_marker(self, container, object, account=None):
"""creates a dierectory marker"""
**h)
def create_object(self, container, object, f=stdin, format='text', meta={},
- etag=None, content_type=None, content_encoding=None,
+ params={}, etag=None, content_type=None, content_encoding=None,
content_disposition=None, account=None, **headers):
"""creates a zero-length object"""
account = account or self.account
path = '/%s/%s/%s' % (account, container, object)
for k, v in headers.items():
- if not v:
+ if v == None:
headers.pop(k)
l = ['etag', 'content_encoding', 'content_disposition', 'content_type']
for k,v in meta.items():
headers['x-object-meta-%s' %k.strip()] = v.strip()
data = f.read() if f else None
- return self.put(path, data, format, headers=headers)
+ return self.put(path, data, format, headers=headers, params=params)
def create_zero_length_object(self, container, object, meta={}, etag=None,
content_type=None, content_encoding=None,
content_disposition=None, account=None,
**headers):
account = account or self.account
- args = locals()
+ args = locals().copy()
for elem in ['self', 'container', 'headers', 'account']:
args.pop(elem)
args.update(headers)
return self.create_object(container, account=account, f=None, **args)
def update_object(self, container, object, f=stdin,
- offset=None, meta={}, content_length=None,
+ offset=None, meta={}, params={}, content_length=None,
content_type=None, content_encoding=None,
content_disposition=None, account=None, **headers):
account = account or self.account
path = '/%s/%s/%s' % (account, container, object)
for k, v in headers.items():
- if not v:
+ if v == None:
headers.pop(k)
l = ['content_encoding', 'content_disposition', 'content_type',
l = [elem for elem in l if eval(elem)]
for elem in l:
headers.update({elem:eval(elem)})
-
+
if 'content_range' not in headers.keys():
if offset != None:
headers['content_range'] = 'bytes %s-/*' % offset
for k,v in meta.items():
headers['x-object-meta-%s' %k.strip()] = v.strip()
data = f.read() if f else None
- return self.post(path, data, headers=headers)
+ return self.post(path, data, headers=headers, params=params)
+
+ def update_object_using_chunks(self, container, object, f=stdin,
+ blocksize=1024, offset=None, meta={},
+ params={}, content_type=None, content_encoding=None,
+ content_disposition=None, account=None, **headers):
+ """updates an object (incremental upload)"""
+ account = account or self.account
+ path = '/%s/%s/%s' % (account, container, object)
+ headers = headers if not headers else {}
+ l = ['content_type', 'content_encoding', 'content_disposition']
+ l = [elem for elem in l if eval(elem)]
+ for elem in l:
+ headers.update({elem:eval(elem)})
+
+ if offset != None:
+ headers['content_range'] = 'bytes %s-/*' % offset
+ else:
+ headers['content_range'] = 'bytes */*'
+
+ for k,v in meta.items():
+ v = v.strip()
+ headers['x-object-meta-%s' %k.strip()] = v
+ return self._chunked_transfer(path, 'POST', f, headers=headers,
+ blocksize=blocksize, params=params)
def _change_obj_location(self, src_container, src_object, dst_container,
dst_object, remove=False, meta={}, account=None,
- **headers):
+ content_type=None, **headers):
account = account or self.account
path = '/%s/%s/%s' % (account, dst_container, dst_object)
headers = {} if not headers else headers
else:
headers['x-copy-from'] = '/%s/%s' % (src_container, src_object)
headers['content_length'] = 0
+ if content_type:
+ headers['content_type'] = content_type
return self.put(path, headers=headers)
def copy_object(self, src_container, src_object, dst_container, dst_object,
- meta={}, account=None, **headers):
+ meta={}, account=None, content_type=None, **headers):
"""copies an object"""
account = account or self.account
return self._change_obj_location(src_container, src_object,
dst_container, dst_object, account=account,
- remove=False, meta=meta, **headers)
+ remove=False, meta=meta,
+ content_type=content_type, **headers)
def move_object(self, src_container, src_object, dst_container,
dst_object, meta={}, account=None,
- **headers):
+ content_type=None, **headers):
"""moves an object"""
account = account or self.account
return self._change_obj_location(src_container, src_object,
dst_container, dst_object,
account=account, remove=True,
- meta=meta, **headers)
+ meta=meta, content_type=content_type,
+ **headers)
def delete_object(self, container, object, params={}, account=None):
"""deletes an object"""
class Pithos_Client(OOS_Client):
"""Pithos Storage Client. Extends OOS_Client"""
- def _chunked_transfer(self, path, method='PUT', f=stdin, headers=None,
- blocksize=1024):
- """perfomrs a chunked request"""
- http = HTTPConnection(self.host)
-
- # write header
- path = '/%s%s' % (self.api, path)
- http.putrequest(method, path)
- http.putheader('x-auth-token', self.token)
- http.putheader('content-type', 'application/octet-stream')
- http.putheader('transfer-encoding', 'chunked')
- if headers:
- for header,value in headers.items():
- http.putheader(header, value)
- http.endheaders()
-
- # write body
- data = ''
- while True:
- if f.closed:
- break
- block = f.read(blocksize)
- if block == '':
- break
- data = '%x\r\n%s\r\n' % (len(block), block)
- try:
- http.send(data)
- except:
- #retry
- http.send(data)
- data = '0\r\n\r\n'
- try:
- http.send(data)
- except:
- #retry
- http.send(data)
-
- # get response
- resp = http.getresponse()
-
- headers = dict(resp.getheaders())
-
- if self.verbose:
- print '%d %s' % (resp.status, resp.reason)
- for key, val in headers.items():
- print '%s: %s' % (key.capitalize(), val)
- print
-
- length = resp.getheader('Content-length', None)
- data = resp.read(length)
- if self.debug:
- print data
- print
-
- if int(resp.status) in ERROR_CODES.keys():
- raise Fault(data, int(resp.status))
-
- #print '*', resp.status, headers, data
- return resp.status, headers, data
-
def _update_metadata(self, path, entity, **meta):
"""
adds new and updates the values of previously set metadata
# Storage Account Services
def list_containers(self, format='text', if_modified_since=None,
- if_unmodified_since=None, limit=1000, marker=None,
+ if_unmodified_since=None, limit=None, marker=None,
shared=False, until=None, account=None):
"""returns a list with the account containers"""
account = account or self.account
params = {'until':until, 'meta':meta}
if shared:
params['shared'] = None
- args = locals()
+ args = locals().copy()
for elem in ['self', 'container', 'params', 'until', 'meta']:
args.pop(elem)
return OOS_Client.list_objects(self, container, params=params, **args)
headers['x-container-policy-%s' % key] = val
return self.post(path, headers=headers)
+ def update_container_data(self, container, f=stdin):
+ """adds blocks of data to the container"""
+ account = self.account
+ path = '/%s/%s' % (account, container)
+ params = {'update': None}
+ headers = {'content_type': 'application/octet-stream'}
+ data = f.read() if f else None
+ headers['content_length'] = len(data)
+ return self.post(path, data, headers=headers, params=params)
+
def delete_container(self, container, until=None, account=None):
"""deletes a container or the container history until the date provided"""
account = account or self.account
l = [elem for elem in l if eval(elem)]
for elem in l:
headers.update({elem:eval(elem)})
+ if format != 'text':
+ params['hashmap'] = None
return OOS_Client.retrieve_object(self, container, object,
account=account, format=format,
params=params, **headers)
account=None):
"""returns a specific object version"""
account = account or self.account
- args = locals()
+ args = locals().copy()
l = ['self', 'container', 'object']
for elem in l:
args.pop(elem)
if_unmodified_since=None, account=None):
"""returns the object version list"""
account = account or self.account
- args = locals()
+ args = locals().copy()
l = ['self', 'container', 'object']
for elem in l:
args.pop(elem)
x_object_public=None, account=None):
"""createas a zero length object"""
account = account or self.account
- args = locals()
+ args = locals().copy()
for elem in ['self', 'container', 'object']:
args.pop(elem)
return OOS_Client.create_zero_length_object(self, container, object,
**args)
- def create_object(self, container, object, f=stdin,
- meta={}, etag=None, content_type=None,
+ def create_object(self, container, object, f=stdin, format='text',
+ meta={}, params={}, etag=None, content_type=None,
content_encoding=None, content_disposition=None,
x_object_manifest=None, x_object_sharing=None,
x_object_public=None, account=None):
"""creates an object"""
account = account or self.account
- args = locals()
+ args = locals().copy()
for elem in ['self', 'container', 'object']:
args.pop(elem)
+ if format != 'text':
+ params.update({'hashmap':None})
return OOS_Client.create_object(self, container, object, **args)
def create_object_using_chunks(self, container, object,
return self._chunked_transfer(path, 'PUT', f, headers=headers,
blocksize=blocksize)
- def create_object_by_hashmap(container, object, f=stdin, format='json',
+ def create_object_by_hashmap(self, container, object, hashmap={},
meta={}, etag=None, content_encoding=None,
content_disposition=None, content_type=None,
x_object_sharing=None, x_object_manifest=None,
x_object_public = None, account=None):
"""creates an object by uploading hashes representing data instead of data"""
account = account or self.account
- args = locals()
- for elem in ['self', 'container', 'object']:
+ args = locals().copy()
+ for elem in ['self', 'container', 'object', 'hashmap']:
args.pop(elem)
- data = f.read() if f else None
- if data and format == 'json':
- try:
- data = eval(data)
- data = json.dumps(data)
- except SyntaxError:
- raise Fault('Invalid formatting')
+ try:
+ data = json.dumps(hashmap)
+ except SyntaxError:
+ raise Fault('Invalid formatting')
+ args['params'] = {'hashmap':None}
+ args['format'] = 'json'
- #TODO check with xml
- return self.create_object(container, object, **args)
+ return self.create_object(container, object, f=StringIO(data), **args)
def create_manifestation(self, container, object, manifest, account=None):
"""creates a manifestation"""
**headers)
def update_object(self, container, object, f=stdin,
- offset=None, meta={}, content_length=None,
+ offset=None, meta={}, replace=False, content_length=None,
content_type=None, content_range=None,
content_encoding=None, content_disposition=None,
x_object_bytes=None, x_object_manifest=None,
x_source_object=None, account=None):
"""updates an object"""
account = account or self.account
- args = locals()
- for elem in ['self', 'container', 'object']:
+ args = locals().copy()
+ for elem in ['self', 'container', 'object', 'replace']:
args.pop(elem)
+ if not replace:
+ args['params'] = {'update':None}
return OOS_Client.update_object(self, container, object, **args)
-
+
def update_object_using_chunks(self, container, object, f=stdin,
blocksize=1024, offset=None, meta={},
- content_type=None, content_encoding=None,
+ replace=False, content_type=None, content_encoding=None,
content_disposition=None, x_object_bytes=None,
x_object_manifest=None, x_object_sharing=None,
x_object_public=None, account=None):
"""updates an object (incremental upload)"""
account = account or self.account
- path = '/%s/%s/%s' % (account, container, object)
- headers = {}
- l = ['content_type', 'content_encoding', 'content_disposition',
- 'x_object_bytes', 'x_object_manifest', 'x_object_sharing',
- 'x_object_public']
- l = [elem for elem in l if eval(elem)]
- for elem in l:
- headers.update({elem:eval(elem)})
-
- if offset != None:
- headers['content_range'] = 'bytes %s-/*' % offset
- else:
- headers['content_range'] = 'bytes */*'
-
- for k,v in meta.items():
- v = v.strip()
- headers['x-object-meta-%s' %k.strip()] = v
- return self._chunked_transfer(path, 'POST', f, headers=headers,
- blocksize=blocksize)
+ args = locals().copy()
+ for elem in ['self', 'container', 'object', 'replace']:
+ args.pop(elem)
+ if not replace:
+ args['params'] = {'update':None}
+ return OOS_Client.update_object_using_chunks(self, container, object, **args)
def update_from_other_source(self, container, object, source,
offset=None, meta={}, content_range=None,
x_object_sharing=None, x_object_public=None, account=None):
"""updates an object"""
account = account or self.account
- args = locals()
+ args = locals().copy()
for elem in ['self', 'container', 'object', 'source']:
args.pop(elem)
def publish_object(self, container, object, account=None):
"""sets a previously created object publicly accessible"""
account = account or self.account
- path = '/%s/%s/%s' % (container, object)
- headers = {'content_range':'bytes */*'}
+ path = '/%s/%s/%s' % (account, container, object)
+ headers = {}
headers['x_object_public'] = True
- return self.post(path, headers=headers)
+ params = {'update':None}
+ return self.post(path, headers=headers, params=params)
def unpublish_object(self, container, object, account=None):
"""unpublish an object"""
account = account or self.account
path = '/%s/%s/%s' % (account, container, object)
- headers = {'content_range':'bytes */*'}
+ headers = {}
headers['x_object_public'] = False
- return self.post(path, headers=headers)
-
- def _change_obj_location(self, src_container, src_object, dst_container,
- dst_object, remove=False,
- meta={}, account=None, **headers):
- account = account or self.account
- path = '/%s/%s/%s' % (account, dst_container, dst_object)
- headers = {} if not headers else headers
- for k, v in meta.items():
- headers['x-object-meta-%s' % k] = v
- if remove:
- headers['x-move-from'] = '/%s/%s' % (src_container, src_object)
- else:
- headers['x-copy-from'] = '/%s/%s' % (src_container, src_object)
- headers['content_length'] = 0
- return self.put(path, headers=headers)
+ params = {'update':None}
+ return self.post(path, headers=headers, params=params)
def copy_object(self, src_container, src_object, dst_container, dst_object,
- meta={}, public=False, version=None, account=None):
+ meta={}, public=False, version=None, account=None,
+ content_type=None):
"""copies an object"""
account = account or self.account
headers = {}
headers['x_object_public'] = public
if version:
- headers['x_object_version'] = version
+ headers['x_source_version'] = version
return OOS_Client.copy_object(self, src_container, src_object,
dst_container, dst_object, meta=meta,
- account=account,**headers)
+ account=account, content_type=content_type,
+ **headers)
def move_object(self, src_container, src_object, dst_container,
- dst_object, meta={}, public=False, version=None,
- account=None):
+ dst_object, meta={}, public=False,
+ account=None, content_type=None):
"""moves an object"""
headers = {}
headers['x_object_public'] = public
- if version:
- headers['x_object_version'] = version
return OOS_Client.move_object(self, src_container, src_object,
dst_container, dst_object, meta=meta,
- account=account, **headers)
+ account=account, content_type=content_type,
+ **headers)
def list_shared_by_others(self, limit=None, marker=None, format='text'):
"""lists other accounts that share objects to the user"""
action = 'read' if read else 'write'
sharing = '%s=%s' % (action, ','.join(l))
self.update_object(container, object, f=None, x_object_sharing=sharing)
-
-def _encode_headers(headers):
- h = {}
- for k, v in headers.items():
- k = urllib.quote(k)
- if v and type(v) == types.StringType:
- v = urllib.quote(v, '/=,-* :"')
- h[k] = v
- return h