libcloud-notifications mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From to...@apache.org
Subject svn commit: r1443958 [1/2] - in /libcloud/trunk: ./ libcloud/common/ libcloud/storage/ libcloud/storage/drivers/ libcloud/test/ libcloud/test/storage/ libcloud/test/storage/fixtures/azure_blobs/
Date Fri, 08 Feb 2013 11:48:21 GMT
Author: tomaz
Date: Fri Feb  8 11:48:21 2013
New Revision: 1443958

URL: http://svn.apache.org/r1443958
Log:
Add new driver for Windows Azure Storage with support for block and page
blobs.

Contributed by Mahendra M., part of LIBCLOUD-80.

Added:
    libcloud/trunk/libcloud/common/azure.py
    libcloud/trunk/libcloud/storage/drivers/azure_blobs.py
    libcloud/trunk/libcloud/test/storage/fixtures/azure_blobs/
    libcloud/trunk/libcloud/test/storage/fixtures/azure_blobs/list_containers_1.xml
    libcloud/trunk/libcloud/test/storage/fixtures/azure_blobs/list_containers_2.xml
    libcloud/trunk/libcloud/test/storage/fixtures/azure_blobs/list_containers_empty.xml
    libcloud/trunk/libcloud/test/storage/fixtures/azure_blobs/list_objects_1.xml
    libcloud/trunk/libcloud/test/storage/fixtures/azure_blobs/list_objects_2.xml
    libcloud/trunk/libcloud/test/storage/fixtures/azure_blobs/list_objects_empty.xml
    libcloud/trunk/libcloud/test/storage/test_azure_blobs.py
Modified:
    libcloud/trunk/CHANGES
    libcloud/trunk/libcloud/storage/base.py
    libcloud/trunk/libcloud/storage/providers.py
    libcloud/trunk/libcloud/storage/types.py
    libcloud/trunk/libcloud/test/secrets.py-dist

Modified: libcloud/trunk/CHANGES
URL: http://svn.apache.org/viewvc/libcloud/trunk/CHANGES?rev=1443958&r1=1443957&r2=1443958&view=diff
==============================================================================
--- libcloud/trunk/CHANGES (original)
+++ libcloud/trunk/CHANGES Fri Feb  8 11:48:21 2013
@@ -201,6 +201,10 @@ Changes with Apache Libcloud in developm
       (LIBCLOUD-269)
       [Mahendra M]
 
+    - Add new driver for Windows Azure Storage with support for block and page
+      blobs. (LIBCLOUD-80)
+      [Mahendra M]
+
   *) DNS
 
     - Update 'if type' checks in the update_record methods to behave correctly

Added: libcloud/trunk/libcloud/common/azure.py
URL: http://svn.apache.org/viewvc/libcloud/trunk/libcloud/common/azure.py?rev=1443958&view=auto
==============================================================================
--- libcloud/trunk/libcloud/common/azure.py (added)
+++ libcloud/trunk/libcloud/common/azure.py Fri Feb  8 11:48:21 2013
@@ -0,0 +1,186 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import copy
+import time
+import base64
+import hmac
+
+from hashlib import sha256
+
+from libcloud.utils.py3 import PY3
+from libcloud.utils.py3 import httplib
+from libcloud.utils.py3 import b
+from libcloud.utils.xml import fixxpath, findtext
+from xml.etree          import ElementTree
+
+from libcloud.common.types import InvalidCredsError
+from libcloud.common.types import LibcloudError, MalformedResponseError
+from libcloud.common.base import ConnectionUserAndKey, RawResponse
+from libcloud.common.base import XmlResponse
+
+# Azure API version
+API_VERSION = '2012-02-12'
+
+# The time format for headers in Azure requests
+AZURE_TIME_FORMAT = '%a, %d %b %Y %H:%M:%S GMT'
+
+
+class AzureResponse(XmlResponse):
+
+    valid_response_codes = [httplib.NOT_FOUND, httplib.CONFLICT,
+                            httplib.BAD_REQUEST]
+
+    def success(self):
+        i = int(self.status)
+        return i >= 200 and i <= 299 or i in self.valid_response_codes
+
+    def parse_error(self, msg=None):
+        error_msg = 'Unknown error'
+
+        try:
+            # Azure does give some meaningful errors, but is inconsistent
+            # Some APIs respond with an XML error. Others just dump HTML
+            body = self.parse_body()
+
+            if type(body) == ElementTree.Element:
+                code = body.findtext(fixxpath(xpath='Code'))
+                message = body.findtext(fixxpath(xpath='Message'))
+                message = message.split('\n')[0]
+                error_msg = '%s: %s' % (code, message)
+
+        except MalformedResponseError:
+            pass
+
+        if msg:
+            error_msg = '%s - %s' % (msg, error_msg)
+
+        if self.status in [httplib.UNAUTHORIZED, httplib.FORBIDDEN]:
+            raise InvalidCredsError(error_msg)
+
+        raise LibcloudError('%s Status code: %d.' % (error_msg, self.status),
+                            driver=self)
+
+
+class AzureRawResponse(RawResponse):
+    pass
+
+
+class AzureConnection(ConnectionUserAndKey):
+    """
+    Represents a single connection to Azure
+    """
+
+    responseCls = AzureResponse
+    rawResponseCls = AzureRawResponse
+
+    def add_default_params(self, params):
+        return params
+
+    def pre_connect_hook(self, params, headers):
+        headers = copy.deepcopy(headers)
+
+        # We have to add a date header in GMT
+        headers['x-ms-date'] = time.strftime(AZURE_TIME_FORMAT, time.gmtime())
+        headers['x-ms-version'] = API_VERSION
+
+        # Add the authorization header
+        headers['Authorization'] = self._get_azure_auth_signature(
+            method=self.method, headers=headers, params=params,
+            account=self.user_id, secret_key=self.key, path=self.action)
+
+        # Azure cribs about this in 'raw' connections
+        headers.pop('Host', None)
+
+        return params, headers
+
+    def _get_azure_auth_signature(self, method, headers, params,
+                                  account, secret_key, path='/'):
+        """
+        Signature = Base64( HMAC-SHA1( YourSecretAccessKeyID,
+                            UTF-8-Encoding-Of( StringToSign ) ) ) );
+
+        StringToSign = HTTP-VERB + "\n" +
+            Content-Encoding + "\n" +
+            Content-Language + "\n" +
+            Content-Length + "\n" +
+            Content-MD5 + "\n" +
+            Content-Type + "\n" +
+            Date + "\n" +
+            If-Modified-Since + "\n" +
+            If-Match + "\n" +
+            If-None-Match + "\n" +
+            If-Unmodified-Since + "\n" +
+            Range + "\n" +
+            CanonicalizedHeaders +
+            CanonicalizedResource;
+        """
+        special_header_values = []
+        xms_header_values = []
+        param_list = []
+        special_header_keys = ['content-encoding', 'content-language',
+                               'content-length', 'content-md5',
+                               'content-type', 'date', 'if-modified-since',
+                               'if-match', 'if-none-match',
+                               'if-unmodified-since', 'range']
+
+        # Split the x-ms headers and normal headers and make everything
+        # lower case
+        headers_copy = {}
+        for header, value in headers.items():
+            header = header.lower()
+            value = str(value).strip()
+            if header.startswith('x-ms-'):
+                xms_header_values.append((header, value))
+            else:
+                headers_copy[header] = value
+
+        # Get the values for the headers in the specific order
+        for header in special_header_keys:
+            header = header.lower()  # Just for safety
+            if header in headers_copy:
+                special_header_values.append(headers_copy[header])
+            else:
+                special_header_values.append('')
+
+        # Prepare the first section of the string to be signed
+        values_to_sign = [method] + special_header_values
+        # string_to_sign = '\n'.join([method] + special_header_values)
+
+        # The x-ms-* headers have to be in lower case and sorted
+        xms_header_values.sort()
+
+        for header, value in xms_header_values:
+            values_to_sign.append('%s:%s' % (header, value))
+
+        # Add the canonicalized path
+        values_to_sign.append('/%s%s' % (account, path))
+
+        # URL query parameters (sorted and lower case)
+        for key, value in params.items():
+            param_list.append((key.lower(), str(value).strip()))
+
+        param_list.sort()
+
+        for key, value in param_list:
+            values_to_sign.append('%s:%s' % (key, value))
+
+        string_to_sign = b('\n'.join(values_to_sign))
+        secret_key = b(secret_key)
+        b64_hmac = base64.b64encode(
+            hmac.new(secret_key, string_to_sign, digestmod=sha256).digest()
+        )
+
+        return 'SharedKey %s:%s' % (self.user_id, b64_hmac.decode('utf-8'))

Modified: libcloud/trunk/libcloud/storage/base.py
URL: http://svn.apache.org/viewvc/libcloud/trunk/libcloud/storage/base.py?rev=1443958&r1=1443957&r2=1443958&view=diff
==============================================================================
--- libcloud/trunk/libcloud/storage/base.py (original)
+++ libcloud/trunk/libcloud/storage/base.py Fri Feb  8 11:48:21 2013
@@ -134,13 +134,14 @@ class Container(object):
         return self.driver.get_object(container_name=self.name,
                                       object_name=object_name)
 
-    def upload_object(self, file_path, object_name, extra=None):
+    def upload_object(self, file_path, object_name, extra=None, **kwargs):
         return self.driver.upload_object(
-            file_path, self, object_name, extra)
+            file_path, self, object_name, extra=extra, **kwargs)
 
-    def upload_object_via_stream(self, iterator, object_name, extra=None):
+    def upload_object_via_stream(self, iterator, object_name, extra=None,
+                                 **kwargs):
         return self.driver.upload_object_via_stream(
-            iterator, self, object_name, extra)
+            iterator, self, object_name, extra=extra, **kwargs)
 
     def download_object(self, obj, destination_path, overwrite_existing=False,
                         delete_on_failure=True):
@@ -615,7 +616,7 @@ class StorageDriver(BaseDriver):
             file_size = os.path.getsize(file_path)
             upload_func_kwargs['chunked'] = False
 
-        if file_size is not None:
+        if file_size is not None and 'Content-Length' not in headers:
             headers['Content-Length'] = file_size
 
         headers['Content-Type'] = content_type
@@ -676,7 +677,7 @@ class StorageDriver(BaseDriver):
         return True, data_hash, bytes_transferred
 
     def _stream_data(self, response, iterator, chunked=False,
-                     calculate_hash=True, chunk_size=None):
+                     calculate_hash=True, chunk_size=None, data=None):
         """
         Stream a data over an http connection.
 

Added: libcloud/trunk/libcloud/storage/drivers/azure_blobs.py
URL: http://svn.apache.org/viewvc/libcloud/trunk/libcloud/storage/drivers/azure_blobs.py?rev=1443958&view=auto
==============================================================================
--- libcloud/trunk/libcloud/storage/drivers/azure_blobs.py (added)
+++ libcloud/trunk/libcloud/storage/drivers/azure_blobs.py Fri Feb  8 11:48:21 2013
@@ -0,0 +1,979 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import time
+import base64
+import hmac
+import re
+import os
+import binascii
+
+from hashlib import sha256
+from xml.etree.ElementTree import Element, SubElement
+
+from libcloud.utils.py3 import PY3
+from libcloud.utils.py3 import httplib
+from libcloud.utils.py3 import urlquote
+from libcloud.utils.py3 import tostring
+from libcloud.utils.py3 import b
+
+from libcloud.utils.xml import fixxpath, findtext
+from libcloud.utils.files import read_in_chunks
+from libcloud.common.types import LibcloudError
+from libcloud.common.azure import AzureConnection
+
+from libcloud.storage.base import Object, Container, StorageDriver
+from libcloud.storage.types import ContainerIsNotEmptyError
+from libcloud.storage.types import ContainerAlreadyExistsError
+from libcloud.storage.types import InvalidContainerNameError
+from libcloud.storage.types import ContainerDoesNotExistError
+from libcloud.storage.types import ObjectDoesNotExistError
+from libcloud.storage.types import ObjectHashMismatchError
+
+if PY3:
+    from io import FileIO as file
+
+# Desired number of items in each response inside a paginated request
+RESPONSES_PER_REQUEST = 100
+
+# As per the Azure documentation, if the upload file size is less than
+# 64MB, we can upload it in a single request. However, in real life azure
+# servers seem to disconnect randomly after around 5 MB or 200s of upload.
+# So, it is better that for file sizes greater than 4MB, we upload it in
+# chunks.
+# Also, with large sizes, if we use a lease, the lease will timeout after
+# 60 seconds, but the upload might still be in progress. This can be
+# handled in code, but if we use chunked uploads, the lease renewal will
+# happen automatically.
+AZURE_BLOCK_MAX_SIZE = 4 * 1024 * 1024
+
+# Azure block blocks must be maximum 4MB
+# Azure page blobs must be aligned in 512 byte boundaries (4MB fits that)
+AZURE_CHUNK_SIZE = 4 * 1024 * 1024
+
+# Azure page blob must be aligned in 512 byte boundaries
+AZURE_PAGE_CHUNK_SIZE = 512
+
+# The time period (in seconds) for which a lease must be obtained.
+# If set as -1, we get an infinite lease, but that is a bad idea. If
+# after getting an infinite lease, there was an issue in releasing the
+# lease, the object will remain 'locked' forever, unless the lease is
+# released using the lease_id (which is not exposed to the user)
+AZURE_LEASE_PERIOD = 60
+
+
+class AzureBlobLease(object):
+    """
+    A class to help in leasing an azure blob and renewing the lease
+    """
+    def __init__(self, driver, object_path, use_lease):
+        """
+        @param driver: The Azure storage driver that is being used
+        @type driver: L{AzureStorageDriver}
+
+        @param object_path: The path of the object we need to lease
+        @type object_path: C{str}
+
+        @param use_lease: Indicates if we must take a lease or not
+        @type use_lease: C{bool}
+        """
+        self.object_path = object_path
+        self.driver = driver
+        self.use_lease = use_lease
+        self.lease_id = None
+        self.params = {'comp': 'lease'}
+
+    def renew(self):
+        """
+        Renew the lease if it is older than a predefined time period
+        """
+        if self.lease_id is None:
+            return
+
+        headers = {'x-ms-lease-action': 'renew',
+                   'x-ms-lease-id': self.lease_id,
+                   'x-ms-lease-duration': '60'}
+
+        response = self.driver.connection.request(self.object_path,
+                                                  headers=headers,
+                                                  params=self.params,
+                                                  method='PUT')
+
+        if response.status != httplib.OK:
+            raise LibcloudError('Unable to obtain lease', driver=self)
+
+    def update_headers(self, headers):
+        """
+        Update the lease id in the headers
+        """
+        if self.lease_id:
+            headers['x-ms-lease-id'] = self.lease_id
+
+    def __enter__(self):
+        if not self.use_lease:
+            return self
+
+        headers = {'x-ms-lease-action': 'acquire',
+                   'x-ms-lease-duration': '60'}
+
+        response = self.driver.connection.request(self.object_path,
+                                                  headers=headers,
+                                                  params=self.params,
+                                                  method='PUT')
+
+        if response.status == httplib.NOT_FOUND:
+            return self
+        elif response.status != httplib.CREATED:
+            raise LibcloudError('Unable to obtain lease', driver=self)
+
+        self.lease_id = response.headers['x-ms-lease-id']
+        return self
+
+    def __exit__(self, type, value, traceback):
+        if self.lease_id is None:
+            return
+
+        headers = {'x-ms-lease-action': 'release',
+                   'x-ms-lease-id': self.lease_id}
+        response = self.driver.connection.request(self.object_path,
+                                                  headers=headers,
+                                                  params=self.params,
+                                                  method='PUT')
+
+        if response.status != httplib.OK:
+            raise LibcloudError('Unable to release lease', driver=self)
+
+
+class AzureBlobsConnection(AzureConnection):
+    """
+    Represents a single connection to Azure Blobs
+    """
+    host = 'blob.core.windows.net'
+
+
+class AzureBlobsStorageDriver(StorageDriver):
+    name = 'Microsoft Azure (blobs)'
+    website = 'http://windows.azure.com/'
+    connectionCls = AzureBlobsConnection
+    hash_type = 'md5'
+    supports_chunked_encoding = False
+    ex_blob_type = 'BlockBlob'
+
+    def __init__(self, key, secret=None, secure=True, host=None, port=None,
+                 **kwargs):
+
+        # The hostname must be 'account.blobs.core.windows.net'
+        self.connectionCls.host = '%s.%s' % (key, self.connectionCls.host)
+
+        # B64decode() this key and keep it, so that we don't have to do
+        # so for every request. Minor performance improvement
+        secret = base64.b64decode(b(secret))
+
+        super(AzureBlobsStorageDriver, self).__init__(
+                                        key=key, secret=secret,
+                                        secure=secure, host=host,
+                                        port=port, **kwargs)
+
+    def _xml_to_container(self, node):
+        """
+        Converts a container XML node to a container instance
+
+        @param node: XML info of the container
+        @type node: L{xml.etree.ElementTree.Element}
+
+        @return: A container instance
+        @rtype: L{Container}
+        """
+
+        name = node.findtext(fixxpath(xpath='Name'))
+        props = node.find(fixxpath(xpath='Properties'))
+        metadata = node.find(fixxpath(xpath='Metadata'))
+
+        extra = {
+            'url': node.findtext(fixxpath(xpath='Url')),
+            'last_modified': node.findtext(fixxpath(xpath='Last-Modified')),
+            'etag': props.findtext(fixxpath(xpath='Etag')),
+            'lease': {
+                'status': props.findtext(fixxpath(xpath='LeaseStatus')),
+                'state': props.findtext(fixxpath(xpath='LeaseState')),
+                'duration': props.findtext(fixxpath(xpath='LeaseDuration')),
+            },
+            'meta_data': {}
+        }
+
+        for meta in metadata.getchildren():
+            extra['meta_data'][meta.tag] = meta.text
+
+        return Container(name=name, extra=extra, driver=self)
+
+    def _response_to_container(self, container_name, response):
+        """
+        Converts a HTTP response to a container instance
+
+        @param container_name: Name of the container
+        @type container_name: C{str}
+
+        @param response: HTTP Response
+        @type node: L{}
+
+        @return: A container instance
+        @rtype: L{Container}
+        """
+
+        headers = response.headers
+        extra = {
+            'url': 'http://%s%s' % (response.connection.host,
+                                    response.connection.action),
+            'etag': headers['etag'],
+            'last_modified': headers['last-modified'],
+            'lease': {
+                'status': headers.get('x-ms-lease-status', None),
+                'state': headers.get('x-ms-lease-state', None),
+                'duration': headers.get('x-ms-lease-duration', None),
+            },
+            'meta_data': {}
+        }
+
+        for key, value in response.headers.items():
+            if key.startswith('x-ms-meta-'):
+                key = key.split('x-ms-meta-')[1]
+                extra['meta_data'][key] = value
+
+        return Container(name=container_name, extra=extra, driver=self)
+
+    def _xml_to_object(self, container, blob):
+        """
+        Converts a BLOB XML node to an object instance
+
+        @param container: Instance of the container holding the blob
+        @type: L{Container}
+
+        @param blob: XML info of the blob
+        @type blob: L{}
+
+        @return: An object instance
+        @rtype: L{Object}
+        """
+
+        name = blob.findtext(fixxpath(xpath='Name'))
+        props = blob.find(fixxpath(xpath='Properties'))
+        metadata = blob.find(fixxpath(xpath='Metadata'))
+        etag = props.findtext(fixxpath(xpath='Etag'))
+        size = int(props.findtext(fixxpath(xpath='Content-Length')))
+
+        extra = {
+            'content_type': props.findtext(fixxpath(xpath='Content-Type')),
+            'etag': etag,
+            'md5_hash': props.findtext(fixxpath(xpath='Content-MD5')),
+            'last_modified': props.findtext(fixxpath(xpath='Last-Modified')),
+            'url': blob.findtext(fixxpath(xpath='Url')),
+            'hash': props.findtext(fixxpath(xpath='Etag')),
+            'lease': {
+                'status': props.findtext(fixxpath(xpath='LeaseStatus')),
+                'state': props.findtext(fixxpath(xpath='LeaseState')),
+                'duration': props.findtext(fixxpath(xpath='LeaseDuration')),
+            },
+            'content_encoding': props.findtext(fixxpath(
+                                             xpath='Content-Encoding')),
+            'content_language': props.findtext(fixxpath(
+                                             xpath='Content-Language')),
+            'blob_type': props.findtext(fixxpath(xpath='BlobType'))
+        }
+
+        if extra['md5_hash']:
+            extra['md5_hash'] = binascii.hexlify(
+                            base64.b64decode(b(extra['md5_hash'])))
+
+        meta_data = {}
+        for meta in metadata.getchildren():
+            meta_data[meta.tag] = meta.text
+
+        return Object(name=name, size=size, hash=etag, meta_data=meta_data,
+                      extra=extra, container=container, driver=self)
+
+    def _response_to_object(self, object_name, container, response):
+        """
+        Converts a HTTP response to an object (from headers)
+
+        @param object_name: Name of the object
+        @type object_name: C{str}
+
+        @param container: Instance of the container holding the blob
+        @type: L{Container}
+
+        @param response: HTTP Response
+        @type node: L{}
+
+        @return: An object instance
+        @rtype: L{Object}
+        """
+
+        headers = response.headers
+        size = int(headers['content-length'])
+        etag = headers['etag']
+
+        extra = {
+            'url': 'http://%s%s' % (response.connection.host,
+                                    response.connection.action),
+            'etag': etag,
+            'md5_hash': headers.get('content-md5', None),
+            'content_type': headers.get('content-type', None),
+            'content_language': headers.get('content-language', None),
+            'content_encoding': headers.get('content-encoding', None),
+            'last_modified': headers['last-modified'],
+            'lease': {
+                'status': headers.get('x-ms-lease-status', None),
+                'state': headers.get('x-ms-lease-state', None),
+                'duration': headers.get('x-ms-lease-duration', None),
+            },
+            'blob_type': headers['x-ms-blob-type']
+        }
+
+        if extra['md5_hash']:
+            extra['md5_hash'] = binascii.hexlify(
+                            base64.b64decode(b(extra['md5_hash'])))
+
+        meta_data = {}
+        for key, value in response.headers.items():
+            if key.startswith('x-ms-meta-'):
+                key = key.split('x-ms-meta-')[1]
+                meta_data[key] = value
+
+        return Object(name=object_name, size=size, hash=etag, extra=extra,
+                      meta_data=meta_data, container=container, driver=self)
+
+    def iterate_containers(self):
+        """
+        @inherits: L{StorageDriver.iterate_containers}
+        """
+        params = {'comp': 'list',
+                  'maxresults': RESPONSES_PER_REQUEST,
+                  'include': 'metadata'}
+
+        while True:
+            response = self.connection.request('/', params)
+            if response.status != httplib.OK:
+                raise LibcloudError('Unexpected status code: %s' %
+                                    (response.status), driver=self)
+
+            body = response.parse_body()
+            containers = body.find(fixxpath(xpath='Containers'))
+            containers = containers.findall(fixxpath(xpath='Container'))
+
+            for container in containers:
+                yield self._xml_to_container(container)
+
+            params['marker'] = body.findtext('NextMarker')
+            if not params['marker']:
+                break
+
+    def iterate_container_objects(self, container):
+        """
+        @inherits: L{StorageDriver.iterate_container_objects}
+        """
+        params = {'restype': 'container',
+                  'comp': 'list',
+                  'maxresults': RESPONSES_PER_REQUEST,
+                  'include': 'metadata'}
+
+        container_path = self._get_container_path(container)
+
+        while True:
+            response = self.connection.request(container_path,
+                                               params=params)
+
+            if response.status == httplib.NOT_FOUND:
+                raise ContainerDoesNotExistError(value=None,
+                                                 driver=self,
+                                                 container_name=container.name)
+
+            elif response.status != httplib.OK:
+                raise LibcloudError('Unexpected status code: %s' %
+                                    (response.status), driver=self)
+
+            body = response.parse_body()
+            blobs = body.find(fixxpath(xpath='Blobs'))
+            blobs = blobs.findall(fixxpath(xpath='Blob'))
+
+            for blob in blobs:
+                yield self._xml_to_object(container, blob)
+
+            params['marker'] = body.findtext('NextMarker')
+            if not params['marker']:
+                break
+
+    def get_container(self, container_name):
+        """
+        @inherits: L{StorageDriver.get_container}
+        """
+        params = {'restype': 'container'}
+
+        container_path = '/%s' % (container_name)
+
+        response = self.connection.request(container_path, params=params,
+                                           method='HEAD')
+
+        if response.status == httplib.NOT_FOUND:
+            raise ContainerDoesNotExistError('Container %s does not exist' %
+                                             (container_name), driver=self,
+                                             container_name=container_name)
+        elif response.status != httplib.OK:
+            raise LibcloudError('Unexpected status code: %s' %
+                                (response.status), driver=self)
+
+        return self._response_to_container(container_name, response)
+
+    def get_object(self, container_name, object_name):
+        """
+        @inherits: L{StorageDriver.get_object}
+        """
+
+        container = self.get_container(container_name=container_name)
+        object_path = self._get_object_path(container, object_name)
+
+        response = self.connection.request(object_path, method='HEAD')
+
+        if response.status == httplib.OK:
+            obj = self._response_to_object(object_name, container, response)
+            return obj
+
+        raise ObjectDoesNotExistError(value=None, driver=self,
+                                      object_name=object_name)
+
+    def _get_container_path(self, container):
+        """
+        Return a container path
+
+        @param container: Container instance
+        @type  container: L{Container}
+
+        @return: A path for this container.
+        @rtype: C{str}
+        """
+        return '/%s' % (container.name)
+
+    def _get_object_path(self, container, object_name):
+        """
+        Return an object's CDN path.
+
+        @param container: Container instance
+        @type  container: L{Container}
+
+        @param object_name: Object name
+        @type  object_name: L{str}
+
+        @return: A  path for this object.
+        @rtype: C{str}
+        """
+        container_url = self._get_container_path(container)
+        object_name_cleaned = urlquote(object_name)
+        object_path = '%s/%s' % (container_url, object_name_cleaned)
+        return object_path
+
+    def create_container(self, container_name):
+        """
+        @inherits: L{StorageDriver.create_container}
+        """
+        params = {'restype': 'container'}
+
+        container_path = '/%s' % (container_name)
+        response = self.connection.request(container_path, params=params,
+                                           method='PUT')
+
+        if response.status == httplib.CREATED:
+            return self._response_to_container(container_name, response)
+        elif response.status == httplib.CONFLICT:
+            raise ContainerAlreadyExistsError(
+                value='Container with this name already exists. The name must '
+                      'be unique among all the containers in the system',
+                container_name=container_name, driver=self)
+        elif response.status == httplib.BAD_REQUEST:
+            raise InvalidContainerNameError(value='Container name contains ' +
+                                            'invalid characters.',
+                                            container_name=container_name,
+                                            driver=self)
+
+        raise LibcloudError('Unexpected status code: %s' % (response.status),
+                            driver=self)
+
+    def delete_container(self, container):
+        """
+        @inherits: L{StorageDriver.delete_container}
+        """
+        # Azure does not check if the container is empty. So, we will do
+        # a check to ensure that the behaviour is similar to other drivers
+        for obj in container.iterate_objects():
+            raise ContainerIsNotEmptyError(
+                value='Container must be empty before it can be deleted.',
+                container_name=container.name, driver=self)
+
+        params = {'restype': 'container'}
+        container_path = self._get_container_path(container)
+
+        # Note: All the objects in the container must be deleted first
+        response = self.connection.request(container_path, params=params,
+                                           method='DELETE')
+
+        if response.status == httplib.ACCEPTED:
+            return True
+        elif response.status == httplib.NOT_FOUND:
+            raise ContainerDoesNotExistError(value=None,
+                                             driver=self,
+                                             container_name=container.name)
+
+        return False
+
+    def download_object(self, obj, destination_path, overwrite_existing=False,
+                        delete_on_failure=True):
+        """
+        @inherits: L{StorageDriver.download_object}
+        """
+        obj_path = self._get_object_path(obj.container, obj.name)
+        response = self.connection.request(obj_path, raw=True, data=None)
+
+        return self._get_object(obj=obj, callback=self._save_object,
+                                response=response,
+                                callback_kwargs={
+                                    'obj': obj,
+                                    'response': response.response,
+                                    'destination_path': destination_path,
+                                    'overwrite_existing': overwrite_existing,
+                                    'delete_on_failure': delete_on_failure},
+                                success_status_code=httplib.OK)
+
+    def download_object_as_stream(self, obj, chunk_size=None):
+        """
+        @inherits: L{StorageDriver.download_object_as_stream}
+        """
+        obj_path = self._get_object_path(obj.container, obj.name)
+        response = self.connection.request(obj_path, raw=True, data=None)
+
+        return self._get_object(obj=obj, callback=read_in_chunks,
+                                response=response,
+                                callback_kwargs={'iterator': response.response,
+                                                 'chunk_size': chunk_size},
+                                success_status_code=httplib.OK)
+
+    def _upload_in_chunks(self, response, data, iterator, object_path,
+                          blob_type, lease, calculate_hash=True):
+        """
+        Uploads data from an interator in fixed sized chunks to S3
+
+        @param response: Response object from the initial POST request
+        @type response: L{RawResponse}
+
+        @param data: Any data from the initial POST request
+        @type data: C{str}
+
+        @param iterator: The generator for fetching the upload data
+        @type iterator: C{generator}
+
+        @param object_path: The path of the object to which we are uploading
+        @type object_name: C{str}
+
+        @param blob_type: The blob type being uploaded
+        @type blob_type: C{str}
+
+        @param lease: The lease object to be used for renewal
+        @type lease: L{AzureBlobLease}
+
+        @keyword calculate_hash: Indicates if we must calculate the data hash
+        @type calculate_hash: C{bool}
+
+        @return: A tuple of (status, checksum, bytes transferred)
+        @rtype: C{tuple}
+        """
+
+        # Get the upload id from the response xml
+        if response.status != httplib.CREATED:
+            raise LibcloudError('Error initializing upload. Code: %d' %
+                                (response.status), driver=self)
+
+        data_hash = None
+        if calculate_hash:
+            data_hash = self._get_hash_function()
+
+        bytes_transferred = 0
+        count = 1
+        chunks = []
+        headers = {}
+
+        lease.update_headers(headers)
+
+        if blob_type == 'BlockBlob':
+            params = {'comp': 'block'}
+        else:
+            params = {'comp': 'page'}
+
+        # Read the input data in chunk sizes suitable for AWS
+        for data in read_in_chunks(iterator, AZURE_CHUNK_SIZE):
+            data = b(data)
+            content_length = len(data)
+            offset = bytes_transferred
+            bytes_transferred += content_length
+
+            if calculate_hash:
+                data_hash.update(data)
+
+            chunk_hash = self._get_hash_function()
+            chunk_hash.update(data)
+            chunk_hash = base64.b64encode(b(chunk_hash.digest()))
+
+            headers['Content-MD5'] = chunk_hash.decode('utf-8')
+            headers['Content-Length'] = content_length
+
+            if blob_type == 'BlockBlob':
+                # Block id can be any unique string that is base64 encoded
+                # A 10 digit number can hold the max value of 50000 blocks
+                # that are allowed for azure
+                block_id = base64.b64encode(b('%10d' % (count)))
+                block_id = block_id.decode('utf-8')
+                params['blockid'] = block_id
+
+                # Keep this data for a later commit
+                chunks.append(block_id)
+            else:
+                headers['x-ms-page-write'] = 'update'
+                headers['x-ms-range'] = 'bytes=%d-%d' % \
+                                            (offset, bytes_transferred-1)
+
+            # Renew lease before updating
+            lease.renew()
+
+            resp = self.connection.request(object_path, method='PUT',
+                                           data=data, headers=headers,
+                                           params=params)
+
+            if resp.status != httplib.CREATED:
+                resp.parse_error()
+                raise LibcloudError('Error uploading chunk %d. Code: %d' %
+                                    (count, resp.status), driver=self)
+
+            count += 1
+
+        if calculate_hash:
+            data_hash = data_hash.hexdigest()
+
+        if blob_type == 'BlockBlob':
+            self._commit_blocks(object_path, chunks, lease)
+
+        # The Azure service does not return a hash immediately for
+        # chunked uploads. It takes some time for the data to get synced
+        response.headers['content-md5'] = None
+
+        return (True, data_hash, bytes_transferred)
+
+    def _commit_blocks(self, object_path, chunks, lease):
+        """
+        Makes a final commit of the data.
+
+        @param object_path: Server side object path.
+        @type object_path: C{str}
+
+        @param upload_id: A list of (chunk_number, chunk_hash) tuples.
+        @type upload_id: C{list}
+        """
+
+        root = Element('BlockList')
+
+        for block_id in chunks:
+            part = SubElement(root, 'Uncommitted')
+            part.text = str(block_id)
+
+        data = tostring(root)
+        params = {'comp': 'blocklist'}
+        headers = {}
+
+        lease.update_headers(headers)
+        lease.renew()
+
+        response = self.connection.request(object_path, data=data,
+                                           params=params, headers=headers,
+                                           method='PUT')
+
+        if response.status != httplib.CREATED:
+            raise LibcloudError('Error in blocklist commit', driver=self)
+
+    def _check_values(self, blob_type, object_size):
+        """
+        Checks if extension arguments are valid
+
+        @param blob_type: The blob type that is being uploaded
+        @type blob_type: C{str}
+
+        @param object_size: The (max) size of the object being uploaded
+        @type object_size: C{int}
+        """
+
+        if blob_type not in ['BlockBlob', 'PageBlob']:
+            raise LibcloudError('Invalid blob type', driver=self)
+
+        if blob_type == 'PageBlob':
+            if not object_size:
+                raise LibcloudError('Max blob size is mandatory for page blob',
+                                    driver=self)
+
+            if object_size % AZURE_PAGE_CHUNK_SIZE:
+                raise LibcloudError('Max blob size is not aligned to '
+                                    'page boundary', driver=self)
+
+    def upload_object(self, file_path, container, object_name, extra=None,
+                      verify_hash=True, ex_blob_type=None, ex_use_lease=False):
+        """
+        Upload an object currently located on a disk.
+
+        @inherits: L{StorageDriver.upload_object}
+
+        @param ex_blob_type: Storage class
+        @type ex_blob_type: C{str}
+
+        @param ex_use_lease: Indicates if we must take a lease before upload
+        @type ex_use_lease: C{bool}
+        """
+
+        if ex_blob_type is None:
+            ex_blob_type = self.ex_blob_type
+
+        # Get the size of the file
+        file_size = os.stat(file_path).st_size
+
+        # The presumed size of the object
+        object_size = file_size
+
+        self._check_values(ex_blob_type, file_size)
+
+        with file(file_path, 'rb') as file_handle:
+            iterator = iter(file_handle)
+
+            # If size is greater than 64MB or type is Page, upload in chunks
+            if ex_blob_type == 'PageBlob' or file_size > AZURE_BLOCK_MAX_SIZE:
+                # For chunked upload of block blobs, the initial size must
+                # be 0.
+                if ex_blob_type == 'BlockBlob':
+                    object_size = None
+
+                object_path = self._get_object_path(container, object_name)
+
+                upload_func = self._upload_in_chunks
+                upload_func_kwargs = {'iterator': iterator,
+                                      'object_path': object_path,
+                                      'blob_type': ex_blob_type,
+                                      'lease': None}
+            else:
+                upload_func = self._stream_data
+                upload_func_kwargs = {'iterator': iterator,
+                                      'chunked': False,
+                                      'calculate_hash': verify_hash}
+
+            return self._put_object(container=container,
+                                    object_name=object_name,
+                                    object_size=object_size,
+                                    upload_func=upload_func,
+                                    upload_func_kwargs=upload_func_kwargs,
+                                    file_path=file_path, extra=extra,
+                                    verify_hash=verify_hash,
+                                    blob_type=ex_blob_type,
+                                    use_lease=ex_use_lease)
+
+    def upload_object_via_stream(self, iterator, container, object_name,
+                                 verify_hash=False, extra=None,
+                                 ex_use_lease=False, ex_blob_type=None,
+                                 ex_page_blob_size=None):
+        """
+        @inherits: L{StorageDriver.upload_object_via_stream}
+
+        @param ex_blob_type: Storage class
+        @type ex_blob_type: C{str}
+
+        @param ex_page_blob_size: The maximum size to which the
+            page blob can grow to
+        @type ex_page_blob_size: C{int}
+
+        @param ex_use_lease: Indicates if we must take a lease before upload
+        @type ex_use_lease: C{bool}
+        """
+
+        if ex_blob_type is None:
+            ex_blob_type = self.ex_blob_type
+
+        self._check_values(ex_blob_type, ex_page_blob_size)
+
+        object_path = self._get_object_path(container, object_name)
+
+        upload_func = self._upload_in_chunks
+        upload_func_kwargs = {'iterator': iterator,
+                              'object_path': object_path,
+                              'blob_type': ex_blob_type,
+                              'lease': None}
+
+        return self._put_object(container=container,
+                                object_name=object_name,
+                                object_size=ex_page_blob_size,
+                                upload_func=upload_func,
+                                upload_func_kwargs=upload_func_kwargs,
+                                extra=extra, verify_hash=verify_hash,
+                                blob_type=ex_blob_type,
+                                use_lease=ex_use_lease)
+
+    def delete_object(self, obj):
+        """
+        @inherits: L{StorageDriver.delete_object}
+        """
+        object_path = self._get_object_path(obj.container, obj.name)
+        response = self.connection.request(object_path, method='DELETE')
+
+        if response.status == httplib.ACCEPTED:
+            return True
+        elif response.status == httplib.NOT_FOUND:
+            raise ObjectDoesNotExistError(value=None, driver=self,
+                                          object_name=obj.name)
+
+        return False
+
+    def _update_metadata(self, headers, meta_data):
+        """
+        Update the given metadata in the headers
+
+        @param headers: The headers dictionary to be updated
+        @type headers: C{dict}
+
+        @param meta_data: Metadata key value pairs
+        @type meta_data: C{dict}
+        """
+        for key, value in list(meta_data.items()):
+            key = 'x-ms-meta-%s' % (key)
+            headers[key] = value
+
+    def _prepare_upload_headers(self, object_name, object_size,
+                                extra, meta_data, blob_type):
+        """
+        Prepare headers for uploading an object
+
+        @param object_name: The full name of the object being updated
+        @type object_name: C{str}
+
+        @param object_size: The size of the object. In case of PageBlobs,
+            this indicates the maximum size the blob can grow to
+        @type object_size: C{int}
+
+        @param extra: Extra control data for the upload
+        @type extra: C{dict}
+
+        @param meta_data: Metadata key value pairs
+        @type meta_data: C{dict}
+
+        @param blob_type: Page or Block blob type
+        @type blob_type: C{str}
+        """
+        headers = {}
+
+        if blob_type is None:
+            blob_type = self.ex_blob_type
+
+        headers['x-ms-blob-type'] = blob_type
+
+        self._update_metadata(headers, meta_data)
+
+        if object_size is not None:
+            headers['Content-Length'] = object_size
+
+        if blob_type == 'PageBlob':
+            headers['Content-Length'] = 0
+            headers['x-ms-blob-content-length'] = object_size
+
+        return headers
+
+    def _put_object(self, container, object_name, object_size, upload_func,
+                    upload_func_kwargs, file_path=None, extra=None,
+                    verify_hash=True, blob_type=None, use_lease=False):
+        """
+        Control function that does the real job of uploading data to a blob
+        """
+        extra = extra or {}
+        meta_data = extra.get('meta_data', {})
+        content_type = extra.get('content_type', None)
+
+        headers = self._prepare_upload_headers(object_name, object_size,
+                                               extra, meta_data, blob_type)
+
+        object_path = self._get_object_path(container, object_name)
+
+        # Get a lease if required and do the operations
+        with AzureBlobLease(self, object_path, use_lease) as lease:
+            if 'lease' in upload_func_kwargs:
+                upload_func_kwargs['lease'] = lease
+
+            lease.update_headers(headers)
+
+            iterator = iter('')
+            result_dict = self._upload_object(object_name, content_type,
+                                              upload_func, upload_func_kwargs,
+                                              object_path, headers=headers,
+                                              file_path=file_path,
+                                              iterator=iterator)
+
+            response = result_dict['response']
+            bytes_transferred = result_dict['bytes_transferred']
+            data_hash = result_dict['data_hash']
+            headers = response.headers
+            response = response.response
+
+        if response.status != httplib.CREATED:
+            raise LibcloudError(
+                'Unexpected status code, status_code=%s' % (response.status),
+                driver=self)
+
+        server_hash = headers['content-md5']
+
+        if server_hash:
+            server_hash = binascii.hexlify(base64.b64decode(b(server_hash)))
+            server_hash = server_hash.decode('utf-8')
+        else:
+            # TODO: HACK - We could poll the object for a while and get
+            # the hash
+            pass
+
+        if (verify_hash and server_hash and data_hash != server_hash):
+            raise ObjectHashMismatchError(
+                value='MD5 hash checksum does not match',
+                object_name=object_name, driver=self)
+
+        return Object(name=object_name, size=bytes_transferred,
+                      hash=headers['etag'], extra=None,
+                      meta_data=meta_data, container=container,
+                      driver=self)
+
+    def ex_set_object_metadata(self, obj, meta_data):
+        """
+        Set metadata for an object
+
+        @param obj: The blob object
+        @type obj: L{Object}
+
+        @param meta_data: Metadata key value pairs
+        @type meta_data: C{dict}
+        """
+        object_path = self._get_object_path(obj.container, obj.name)
+        params = {'comp': 'metadata'}
+        headers = {}
+
+        self._update_metadata(headers, meta_data)
+
+        response = self.connection.request(object_path, method='PUT',
+                                           params=params,
+                                           headers=headers)
+
+        if response.status != httplib.OK:
+            response.parse_error('Setting metadata')

Modified: libcloud/trunk/libcloud/storage/providers.py
URL: http://svn.apache.org/viewvc/libcloud/trunk/libcloud/storage/providers.py?rev=1443958&r1=1443957&r2=1443958&view=diff
==============================================================================
--- libcloud/trunk/libcloud/storage/providers.py (original)
+++ libcloud/trunk/libcloud/storage/providers.py Fri Feb  8 11:48:21 2013
@@ -45,6 +45,8 @@ DRIVERS = {
         ('libcloud.storage.drivers.nimbus', 'NimbusStorageDriver'),
     Provider.LOCAL:
         ('libcloud.storage.drivers.local', 'LocalStorageDriver'),
+    Provider.AZURE_BLOBS:
+        ('libcloud.storage.drivers.azure_blobs', 'AzureBlobsStorageDriver'),
 
     # Deprecated
     Provider.CLOUDFILES_US:

Modified: libcloud/trunk/libcloud/storage/types.py
URL: http://svn.apache.org/viewvc/libcloud/trunk/libcloud/storage/types.py?rev=1443958&r1=1443957&r2=1443958&view=diff
==============================================================================
--- libcloud/trunk/libcloud/storage/types.py (original)
+++ libcloud/trunk/libcloud/storage/types.py Fri Feb  8 11:48:21 2013
@@ -57,6 +57,7 @@ class Provider(object):
     NIMBUS = 'nimbus'
     LOCAL = 'local'
     CLOUDFILES = 'cloudfiles'
+    AZURE_BLOBS = 'azure_blobs'
 
     # Deperecated
     CLOUDFILES_US = 'cloudfiles_us'

Modified: libcloud/trunk/libcloud/test/secrets.py-dist
URL: http://svn.apache.org/viewvc/libcloud/trunk/libcloud/test/secrets.py-dist?rev=1443958&r1=1443957&r2=1443958&view=diff
==============================================================================
--- libcloud/trunk/libcloud/test/secrets.py-dist (original)
+++ libcloud/trunk/libcloud/test/secrets.py-dist Fri Feb  8 11:48:21 2013
@@ -44,6 +44,9 @@ HOSTVIRTUAL_PARAMS = ('key',)
 STORAGE_S3_PARAMS = ('key', 'secret')
 STORAGE_GOOGLE_STORAGE_PARAMS = ('key', 'secret')
 
+# Azure key is b64 encoded and must be decoded before signing requests
+STORAGE_AZURE_BLOBS_PARAMS = ('account', 'cGFzc3dvcmQ=')
+
 # Loadbalancer
 LB_BRIGHTBOX_PARAMS = ('user', 'key')
 LB_ELB_PARAMS = ('access_id', 'secret', 'region')

Added: libcloud/trunk/libcloud/test/storage/fixtures/azure_blobs/list_containers_1.xml
URL: http://svn.apache.org/viewvc/libcloud/trunk/libcloud/test/storage/fixtures/azure_blobs/list_containers_1.xml?rev=1443958&view=auto
==============================================================================
--- libcloud/trunk/libcloud/test/storage/fixtures/azure_blobs/list_containers_1.xml (added)
+++ libcloud/trunk/libcloud/test/storage/fixtures/azure_blobs/list_containers_1.xml Fri Feb  8 11:48:21 2013
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<EnumerationResults AccountName="https://account.blob.core.windows.net/">
+    <MaxResults>2</MaxResults>
+    <Containers>
+        <Container>
+            <Name>container1</Name>
+            <Url>https://account.blob.core.windows.net/container1</Url>
+            <Properties>
+                <Last-Modified>Mon, 07 Jan 2013 06:31:06 GMT</Last-Modified>
+                <Etag>"0x8CFBAB7B4F23346"</Etag>
+                <LeaseStatus>unlocked</LeaseStatus>
+                <LeaseState>available</LeaseState>
+            </Properties>
+            <Metadata />
+        </Container>
+        <Container>
+            <Name>container2</Name>
+            <Url>https://account.blob.core.windows.net/container2</Url>
+            <Properties>
+                <Last-Modified>Mon, 07 Jan 2013 06:31:07 GMT</Last-Modified>
+                <Etag>"0x8CFBAB7B5B82D8E"</Etag>
+                <LeaseStatus>unlocked</LeaseStatus>
+                <LeaseState>available</LeaseState>
+            </Properties>
+            <Metadata />
+        </Container>
+    </Containers>
+    <NextMarker>/account/container3</NextMarker>
+</EnumerationResults>

Added: libcloud/trunk/libcloud/test/storage/fixtures/azure_blobs/list_containers_2.xml
URL: http://svn.apache.org/viewvc/libcloud/trunk/libcloud/test/storage/fixtures/azure_blobs/list_containers_2.xml?rev=1443958&view=auto
==============================================================================
--- libcloud/trunk/libcloud/test/storage/fixtures/azure_blobs/list_containers_2.xml (added)
+++ libcloud/trunk/libcloud/test/storage/fixtures/azure_blobs/list_containers_2.xml Fri Feb  8 11:48:21 2013
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<EnumerationResults AccountName="https://account.blob.core.windows.net/">
+    <Marker>/account/container3</Marker>
+    <MaxResults>2</MaxResults>
+    <Containers>
+        <Container>
+            <Name>container3</Name>
+            <Url>https://account.blob.core.windows.net/container3</Url>
+            <Properties>
+                <Last-Modified>Mon, 07 Jan 2013 06:31:08 GMT</Last-Modified>
+                <Etag>"0x8CFBAB7B6452A71"</Etag>
+                <LeaseStatus>unlocked</LeaseStatus>
+                <LeaseState>available</LeaseState>
+            </Properties>
+            <Metadata />
+        </Container>
+        <Container>
+            <Name>container4</Name>
+            <Url>https://account.blob.core.windows.net/container4</Url>
+            <Properties>
+                <Last-Modified>Fri, 04 Jan 2013 08:32:41 GMT</Last-Modified>
+                <Etag>"0x8CFB86D32305484"</Etag>
+                <LeaseStatus>unlocked</LeaseStatus>
+                <LeaseState>available</LeaseState>
+            </Properties>
+            <Metadata />
+        </Container>
+    </Containers>
+    <NextMarker />
+</EnumerationResults>

Added: libcloud/trunk/libcloud/test/storage/fixtures/azure_blobs/list_containers_empty.xml
URL: http://svn.apache.org/viewvc/libcloud/trunk/libcloud/test/storage/fixtures/azure_blobs/list_containers_empty.xml?rev=1443958&view=auto
==============================================================================
--- libcloud/trunk/libcloud/test/storage/fixtures/azure_blobs/list_containers_empty.xml (added)
+++ libcloud/trunk/libcloud/test/storage/fixtures/azure_blobs/list_containers_empty.xml Fri Feb  8 11:48:21 2013
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<EnumerationResults AccountName="http://account.blob.core.windows.net">
+  <Prefix></Prefix>
+  <Marker></Marker>
+  <MaxResults>100</MaxResults>
+  <Containers />
+  <NextMarker />
+</EnumerationResults>

Added: libcloud/trunk/libcloud/test/storage/fixtures/azure_blobs/list_objects_1.xml
URL: http://svn.apache.org/viewvc/libcloud/trunk/libcloud/test/storage/fixtures/azure_blobs/list_objects_1.xml?rev=1443958&view=auto
==============================================================================
--- libcloud/trunk/libcloud/test/storage/fixtures/azure_blobs/list_objects_1.xml (added)
+++ libcloud/trunk/libcloud/test/storage/fixtures/azure_blobs/list_objects_1.xml Fri Feb  8 11:48:21 2013
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="utf-8"?>
+<EnumerationResults ContainerName="https://account.blob.core.windows.net/test_container">
+    <MaxResults>2</MaxResults>
+    <Blobs>
+        <Blob>
+            <Name>object1.txt</Name>
+            <Url>https://account.blob.core.windows.net/test_container/object1.txt</Url>
+            <Properties>
+                <Last-Modified>Fri, 04 Jan 2013 09:48:06 GMT</Last-Modified>
+                <Etag>0x8CFB877BB56A6FB</Etag>
+                <Content-Length>0</Content-Length>
+                <Content-Type>application/octet-stream</Content-Type>
+                <Content-Encoding />
+                <Content-Language />
+                <Content-MD5>1B2M2Y8AsgTpgAmY7PhCfg==</Content-MD5>
+                <Cache-Control />
+                <BlobType>BlockBlob</BlobType>
+                <LeaseStatus>unlocked</LeaseStatus>
+                <LeaseState>available</LeaseState>
+            </Properties>
+            <Metadata>
+                <meta1>value1</meta1>
+                <meta2>value2</meta2>
+            </Metadata>
+        </Blob>
+        <Blob>
+            <Name>object2.txt</Name>
+            <Url>https://account.blob.core.windows.net/test_container/object2.txt</Url>
+            <Properties>
+                <Last-Modified>Sat, 05 Jan 2013 03:51:42 GMT</Last-Modified>
+                <Etag>0x8CFB90F1BA8CD8F</Etag>
+                <Content-Length>1048576</Content-Length>
+                <Content-Type>application/octet-stream</Content-Type>
+                <Content-Encoding />
+                <Content-Language />
+                <Content-MD5>ttgbNgpWctgMJ0MPORU+LA==</Content-MD5>
+                <Cache-Control />
+                <BlobType>BlockBlob</BlobType>
+                <LeaseStatus>unlocked</LeaseStatus>
+                <LeaseState>available</LeaseState>
+            </Properties>
+            <Metadata>
+                <meta1>value1</meta1>
+                <meta2>value2</meta2>
+            </Metadata>
+        </Blob>
+    </Blobs>
+    <NextMarker>2!76!MDAwMDExIXNvbWUxMTcudHh0ITAwMDAyOCE5OTk5LTEyLTMxVDIzOjU5OjU5Ljk5OTk5OTlaIQ--</NextMarker>
+</EnumerationResults>

Added: libcloud/trunk/libcloud/test/storage/fixtures/azure_blobs/list_objects_2.xml
URL: http://svn.apache.org/viewvc/libcloud/trunk/libcloud/test/storage/fixtures/azure_blobs/list_objects_2.xml?rev=1443958&view=auto
==============================================================================
--- libcloud/trunk/libcloud/test/storage/fixtures/azure_blobs/list_objects_2.xml (added)
+++ libcloud/trunk/libcloud/test/storage/fixtures/azure_blobs/list_objects_2.xml Fri Feb  8 11:48:21 2013
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<EnumerationResults ContainerName="https://account.blob.core.windows.net/test_container">
+    <Marker>object3.txt</Marker>
+    <MaxResults>2</MaxResults>
+    <Blobs>
+        <Blob>
+            <Name>object3.txt</Name>
+            <Url>https://account.blob.core.windows.net/test_container/object3.txt</Url>
+            <Properties>
+                <Last-Modified>Sat, 05 Jan 2013 03:52:08 GMT</Last-Modified>
+                <Etag>0x8CFB90F2B6FC022</Etag>
+                <Content-Length>1048576</Content-Length>
+                <Content-Type>application/octet-stream</Content-Type>
+                <Content-Encoding />
+                <Content-Language />
+                <Content-MD5>ttgbNgpWctgMJ0MPORU+LA==</Content-MD5>
+                <Cache-Control />
+                <BlobType>BlockBlob</BlobType>
+                <LeaseStatus>unlocked</LeaseStatus>
+                <LeaseState>available</LeaseState>
+            </Properties>
+            <Metadata />
+        </Blob>
+        <Blob>
+            <Name>object4.txt</Name>
+            <Url>https://account.blob.core.windows.net/test_container/object4.txt</Url>
+            <Properties>
+                <Last-Modified>Fri, 04 Jan 2013 10:20:14 GMT</Last-Modified>
+                <Etag>0x8CFB87C38717450</Etag>
+                <Content-Length>0</Content-Length>
+                <Content-Type>application/octet-stream</Content-Type>
+                <Content-Encoding /><Content-Language />
+                <Content-MD5>1B2M2Y8AsgTpgAmY7PhCfg==</Content-MD5>
+                <Cache-Control />
+                <BlobType>BlockBlob</BlobType>
+                <LeaseStatus>unlocked</LeaseStatus>
+                <LeaseState>available</LeaseState>
+            </Properties>
+            <Metadata />
+        </Blob>
+    </Blobs>
+    <NextMarker />
+</EnumerationResults>

Added: libcloud/trunk/libcloud/test/storage/fixtures/azure_blobs/list_objects_empty.xml
URL: http://svn.apache.org/viewvc/libcloud/trunk/libcloud/test/storage/fixtures/azure_blobs/list_objects_empty.xml?rev=1443958&view=auto
==============================================================================
--- libcloud/trunk/libcloud/test/storage/fixtures/azure_blobs/list_objects_empty.xml (added)
+++ libcloud/trunk/libcloud/test/storage/fixtures/azure_blobs/list_objects_empty.xml Fri Feb  8 11:48:21 2013
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<EnumerationResults ContainerName="https://account.blob.core.windows.net/test_container">
+    <MaxResults>2</MaxResults>
+    <Blobs />
+    <NextMarker />
+</EnumerationResults>



Mime
View raw message