Fix download in transfer lib.
[pithos] / tools / lib / client.py
index f105929..a18bbc3 100644 (file)
@@ -34,6 +34,8 @@
 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
@@ -44,13 +46,16 @@ import datetime
 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):
@@ -72,23 +77,21 @@ class Client(object):
         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
@@ -104,7 +107,8 @@ class Client(object):
         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)
@@ -124,6 +128,75 @@ class Client(object):
         #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)
     
@@ -138,8 +211,9 @@ class Client(object):
         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,
@@ -149,7 +223,7 @@ class Client(object):
         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={}):
@@ -333,11 +407,10 @@ class OOS_Client(Client):
     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"""
@@ -349,13 +422,13 @@ class OOS_Client(Client):
                                               **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']
@@ -367,27 +440,27 @@ class OOS_Client(Client):
         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',
@@ -395,7 +468,7 @@ class OOS_Client(Client):
         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
@@ -405,11 +478,35 @@ class OOS_Client(Client):
         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
@@ -420,25 +517,29 @@ class OOS_Client(Client):
         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"""
@@ -477,66 +578,6 @@ class OOS_Client(Client):
 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
@@ -563,7 +604,7 @@ class Pithos_Client(OOS_Client):
     # 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
@@ -647,7 +688,7 @@ class Pithos_Client(OOS_Client):
         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)
@@ -672,6 +713,16 @@ class Pithos_Client(OOS_Client):
             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
@@ -694,6 +745,8 @@ class Pithos_Client(OOS_Client):
         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)
@@ -705,7 +758,7 @@ class Pithos_Client(OOS_Client):
                                 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)
@@ -718,7 +771,7 @@ class Pithos_Client(OOS_Client):
                                     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)
@@ -734,22 +787,24 @@ class Pithos_Client(OOS_Client):
                                   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,
@@ -776,27 +831,25 @@ class Pithos_Client(OOS_Client):
         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"""
@@ -806,7 +859,7 @@ class Pithos_Client(OOS_Client):
                                   **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,
@@ -814,38 +867,27 @@ class Pithos_Client(OOS_Client):
                       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,
@@ -854,7 +896,7 @@ class Pithos_Client(OOS_Client):
                       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)
         
@@ -882,57 +924,45 @@ class Pithos_Client(OOS_Client):
     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"""
@@ -947,12 +977,3 @@ class Pithos_Client(OOS_Client):
         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