diff --git a/sdk/storage/azure-storage-file/azure/storage/file/_share_utils.py b/sdk/storage/azure-storage-file/azure/storage/file/_share_utils.py new file mode 100644 index 000000000000..4032ec44c2c2 --- /dev/null +++ b/sdk/storage/azure-storage-file/azure/storage/file/_share_utils.py @@ -0,0 +1,20 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- + +from .models import ShareProperties + + +def deserialize_metadata(response, obj, headers): + raw_metadata = {k: v for k, v in response.headers.items() if k.startswith("x-ms-meta-")} + return {k[10:]: v for k, v in raw_metadata.items()} + +def deserialize_share_properties(response, obj, headers): + metadata = deserialize_metadata(response, obj, headers) + share_properties = ShareProperties( + metadata=metadata, + **headers + ) + return share_properties \ No newline at end of file diff --git a/sdk/storage/azure-storage-file/azure/storage/file/_shared/__init__.py b/sdk/storage/azure-storage-file/azure/storage/file/_shared/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/sdk/storage/azure-storage-file/azure/storage/file/_shared/authentication.py b/sdk/storage/azure-storage-file/azure/storage/file/_shared/authentication.py index 92d84b7b520f..e8db39745d12 100644 --- a/sdk/storage/azure-storage-file/azure/storage/file/_shared/authentication.py +++ b/sdk/storage/azure-storage-file/azure/storage/file/_shared/authentication.py @@ -18,8 +18,6 @@ from azure.core.exceptions import AzureError from azure.core.pipeline.policies import SansIOHTTPPolicy -from .constants import DEV_ACCOUNT_NAME, DEV_ACCOUNT_SECONDARY_NAME - if sys.version_info < (3,): _unicode_type = unicode # pylint: disable=undefined-variable else: @@ -82,13 +80,12 @@ class AzureSigningError(AzureError): # pylint: disable=no-self-use -class SharedKeyCredentials(SansIOHTTPPolicy): +class SharedKeyCredentialPolicy(SansIOHTTPPolicy): - def __init__(self, account_name, account_key, is_emulated=False): + def __init__(self, account_name, account_key): self.account_name = account_name self.account_key = account_key - self.is_emulated = is_emulated - super(SharedKeyCredentials, self).__init__() + super(SharedKeyCredentialPolicy, self).__init__() def _get_headers(self, request, headers_to_sign): headers = dict((name.lower(), value) for name, value in request.http_request.headers.items() if value) @@ -101,13 +98,6 @@ def _get_verb(self, request): def _get_canonicalized_resource(self, request): uri_path = urlparse(request.http_request.url).path - - # for emulator, use the DEV_ACCOUNT_NAME instead of DEV_ACCOUNT_SECONDARY_NAME - # as this is how the emulator works - if self.is_emulated and uri_path.find(DEV_ACCOUNT_SECONDARY_NAME) == 1: - # only replace the first instance - uri_path = uri_path.replace(DEV_ACCOUNT_SECONDARY_NAME, DEV_ACCOUNT_NAME, 1) - return '/' + self.account_name + uri_path def _get_canonicalized_headers(self, request): @@ -141,7 +131,6 @@ def _add_authorization_header(self, request, string_to_sign): except Exception as ex: # Wrap any error that occurred as signing error # Doing so will clarify/locate the source of problem - # TODO: AzureSigningError raise _wrap_exception(ex, AzureSigningError) def on_request(self, request, **kwargs): @@ -160,4 +149,4 @@ def on_request(self, request, **kwargs): self._get_canonicalized_resource_query(request) self._add_authorization_header(request, string_to_sign) - logger.debug("String_to_sign=%s", string_to_sign) + #logger.debug("String_to_sign=%s", string_to_sign) diff --git a/sdk/storage/azure-storage-file/azure/storage/file/_shared/models.py b/sdk/storage/azure-storage-file/azure/storage/file/_shared/models.py index 203330997b7c..f2ad58791420 100644 --- a/sdk/storage/azure-storage-file/azure/storage/file/_shared/models.py +++ b/sdk/storage/azure-storage-file/azure/storage/file/_shared/models.py @@ -176,3 +176,14 @@ def get(self, key, default=None): if key in self.__dict__: return self.__dict__[key] return default + + +class LocationMode(object): + """ + Specifies the location the request should be sent to. This mode only applies + for RA-GRS accounts which allow secondary read access. All other account types + must use PRIMARY. + """ + + PRIMARY = 'primary' #: Requests should be sent to the primary location. + SECONDARY = 'secondary' #: Requests should be sent to the secondary location, if possible. diff --git a/sdk/storage/azure-storage-file/azure/storage/file/_shared/utils.py b/sdk/storage/azure-storage-file/azure/storage/file/_shared/utils.py index b9bd9fcd6e65..b84935285d54 100644 --- a/sdk/storage/azure-storage-file/azure/storage/file/_shared/utils.py +++ b/sdk/storage/azure-storage-file/azure/storage/file/_shared/utils.py @@ -43,10 +43,10 @@ ClientAuthenticationError, DecodeError) -from .constants import STORAGE_OAUTH_SCOPE, SERVICE_HOST_BASE, DEFAULT_SOCKET_TIMEOUT -from .models import LocationMode, StorageErrorCode -from .authentication import SharedKeyCredentialPolicy -from .policies import ( +from azure.storage.file._shared.constants import STORAGE_OAUTH_SCOPE, SERVICE_HOST_BASE, DEFAULT_SOCKET_TIMEOUT +from azure.storage.file._shared.models import LocationMode, StorageErrorCode +from azure.storage.file._shared.authentication import SharedKeyCredentialPolicy +from azure.storage.file._shared.policies import ( StorageFileSettings, StorageHeadersPolicy, StorageContentValidation, diff --git a/sdk/storage/azure-storage-file/azure/storage/file/directory_client.py b/sdk/storage/azure-storage-file/azure/storage/file/directory_client.py index 667924e833a6..e09756b00b33 100644 --- a/sdk/storage/azure-storage-file/azure/storage/file/directory_client.py +++ b/sdk/storage/azure-storage-file/azure/storage/file/directory_client.py @@ -13,7 +13,7 @@ class DirectoryClient(): def __init__( self, share_name=None, # type: Optional[Union[str, ShareProperties]] directory_path=None, # type: Optional[str] - credentials=None, # type: Optional[Any] + credential=None, # type: Optional[Any] configuration=None # type: Optional[Configuration] ): # type: (...) -> DirectoryClient diff --git a/sdk/storage/azure-storage-file/azure/storage/file/share_client.py b/sdk/storage/azure-storage-file/azure/storage/file/share_client.py index 5e8480d64fc1..913c022cfd3b 100644 --- a/sdk/storage/azure-storage-file/azure/storage/file/share_client.py +++ b/sdk/storage/azure-storage-file/azure/storage/file/share_client.py @@ -4,8 +4,31 @@ # license information. # -------------------------------------------------------------------------- +try: + from urllib.parse import urlparse, quote, unquote +except ImportError: + from urlparse import urlparse + from urllib2 import quote, unquote -class ShareClient(): +from .directory_client import DirectoryClient + +from ._generated import AzureFileStorage +from ._generated.version import VERSION +from ._generated.models import StorageErrorException, SignedIdentifier +from ._shared.utils import ( + StorageAccountHostsMixin, + serialize_iso, + return_headers_and_deserialized, + parse_query, + return_response_headers, + add_metadata_headers, + process_storage_error, + parse_connection_str) + +from ._share_utils import deserialize_share_properties + + +class ShareClient(StorageAccountHostsMixin): """ A client to interact with the share. """ @@ -28,11 +51,43 @@ def __init__( :param configuration: A optional pipeline configuration. This can be retrieved with :func:`ShareClient.create_configuration()` """ - + try: + if not share_url.lower().startswith('http'): + share_url = "https://" + share_url + except AttributeError: + raise ValueError("Share URL must be a string.") + parsed_url = urlparse(share_url.rstrip('/')) + if not parsed_url.path and not (share_name): + raise ValueError("Please specify a share name.") + if not parsed_url.netloc: + raise ValueError("Invalid URL: {}".format(share_url)) + + path_share = "" + path_snapshot = None + if parsed_url.path: + path_share, _, _ = parsed_url.path.lstrip('/').partition('/') + path_snapshot, sas_token = parse_query(parsed_url.query) + + try: + self.snapshot = snapshot.snapshot + except AttributeError: + try: + self.snapshot = snapshot['snapshot'] + except TypeError: + self.snapshot = snapshot or path_snapshot + try: + self.share_name = share_name + except AttributeError: + self.share_name = share_name or unquote(path_share) + self._query_str, credential = self._format_query_string(sas_token, credential, self.snapshot) + super(ShareClient, self).__init__(parsed_url, credential, configuration, **kwargs) + self._client = AzureFileStorage(version=VERSION, url=self.url, pipeline=self._pipeline) + @classmethod def from_connection_string( cls, conn_str, # type: str share_name, # type: Union[str, ShareProperties] + credential=None, # type: Optional[Any] configuration=None, # type: Optional[Configuration] **kwargs # type: Any ): @@ -40,6 +95,10 @@ def from_connection_string( """ Create ShareClient from a Connection String. """ + account_url, credential = parse_connection_str(conn_str, credential) + return cls( + share_url=account_url, share_name=share_name, credential=credential, + configuration=configuration, **kwargs) def get_directory_client(self, directory_name=""): """Get a client to interact with the specified directory. @@ -51,11 +110,13 @@ def get_directory_client(self, directory_name=""): :returns: A Directory Client. :rtype: ~azure.core.file.directory_client.DirectoryClient """ - + return DirectoryClient(self.share_name, directory_name, self.credential, self._config) + def create_share( self, metadata=None, # type: Optional[Dict[str, str]] quota=None, # type: Optional[int] - timeout=None # type: Optional[int] + timeout=None, # type: Optional[int] + **kwargs # type: Optional[Any] ): # type: (...) -> Dict[str, Any] """Creates a new Share. @@ -69,6 +130,20 @@ def create_share( :returns: Share-updated property dict (Etag and last modified). :rtype: dict(str, Any) """ + if self.require_encryption and not self.key_encryption_key: + raise ValueError("Encryption required but no key was provided.") + headers = kwargs.pop('headers', {}) + headers.update(add_metadata_headers(metadata)) + + try: + return self._client.share.create( + timeout=timeout, + metadata=metadata, + quota=quota, + cls=return_response_headers, + **kwargs) + except StorageErrorException as error: + process_storage_error(error) def create_snapshot( self, metadata=None, # type: Optional[Dict[str, str]] @@ -78,8 +153,56 @@ def create_snapshot( ): # type: (...) -> SnapshotProperties """ - :returns: SnapshotProperties + Creates a snapshot of the share. + A snapshot is a read-only version of a share that's taken at a point in time. + It can be read, copied, or deleted, but not modified. Snapshots provide a way + to back up a share as it appears at a moment in time. + A snapshot of a share has the same name as the base share from which the snapshot + is taken, with a DateTime value appended to indicate the time at which the + snapshot was taken. + :param metadata: + Name-value pairs associated with the share as metadata. + :type metadata: dict(str, str) + :param datetime if_modified_since: + A DateTime value. Azure expects the date value passed in to be UTC. + If timezone is included, any non-UTC datetimes will be converted to UTC. + If a date is passed in without timezone info, it is assumed to be UTC. + Specify this header to perform the operation only + if the resource has been modified since the specified time. + :param datetime if_unmodified_since: + A DateTime value. Azure expects the date value passed in to be UTC. + If timezone is included, any non-UTC datetimes will be converted to UTC. + If a date is passed in without timezone info, it is assumed to be UTC. + Specify this header to perform the operation only if + the resource has not been modified since the specified date/time. + :param str if_match: + An ETag value, or the wildcard character (*). Specify this header to perform + the operation only if the resource's ETag matches the value specified. + :param str if_none_match: + An ETag value, or the wildcard character (*). Specify this header + to perform the operation only if the resource's ETag does not match + the value specified. Specify the wildcard character (*) to perform + the operation only if the resource does not exist, and fail the + operation if it does exist. + :param lease: + Required if the share has an active lease. Value can be a LeaseClient object + or the lease ID as a string. + :type lease: ~azure.storage.share.lease.LeaseClient or str + :param int timeout: + The timeout parameter is expressed in seconds. + :returns: share-updated property dict (Snapshot ID, Etag, and last modified). + :rtype: dict[str, Any] """ + headers = kwargs.pop('headers', {}) + headers.update(add_metadata_headers(metadata)) + try: + return self._client.share.create_snapshot( + timeout=timeout, + cls=return_response_headers, + headers=headers, + **kwargs) + except StorageErrorException as error: + process_storage_error(error) def delete_share( self, delete_snapshots=False, # type: Optional[bool] @@ -97,12 +220,32 @@ def delete_share( The timeout parameter is expressed in seconds. :rtype: None """ - + if delete_snapshots: + delete_snapshots = "include" + try: + self._client.share.delete( + timeout=timeout, + sharesnapshot=self.snapshot, + delete_snapshots=delete_snapshots, + **kwargs) + except StorageErrorException as error: + process_storage_error(error) + def get_share_properties(self, timeout=None, **kwargs): # type: (Optional[int], Any) -> ShareProperties """ :returns: ShareProperties """ + try: + props = self._client.share.get_properties( + timeout=timeout, + sharesnapshot=self.snapshot, + cls=deserialize_share_properties, + **kwargs) + except StorageErrorException as error: + process_storage_error(error) + props.name = self.share_name + return props def set_share_quota(self, quota, timeout=None, **kwargs): # type: (int, Optional[int], Any) -> Dict[str, Any] @@ -114,7 +257,15 @@ def set_share_quota(self, quota, timeout=None, **kwargs): :returns: Share-updated property dict (Etag and last modified). :rtype: dict(str, Any) """ - + try: + return self._client.share.set_quota( + timeout=timeout, + quota=quota, + cls=return_response_headers, + **kwargs) + except StorageErrorException as error: + process_storage_error(error) + def set_share_metadata(self, metadata, timeout=None, **kwargs): # type: (Dict[str, Any], Optional[int], Any) -> Dict[str, Any] """ Sets the metadata for the share. @@ -126,33 +277,86 @@ def set_share_metadata(self, metadata, timeout=None, **kwargs): :returns: Share-updated property dict (Etag and last modified). :rtype: dict(str, Any) """ + try: + return self._client.share.set_metadata( + timeout=timeout, + cls=return_response_headers, + metadata=metadata, + **kwargs) + except StorageErrorException as error: + process_storage_error(error) def get_share_acl(self, timeout=None, **kwargs): # type: (Optional[int]) -> Dict[str, str] """ :returns: Access policy information in a dict. """ - + try: + response, identifiers = self._client.share.get_access_policy( + timeout=timeout, + cls=return_headers_and_deserialized, + **kwargs) + except StorageErrorException as error: + process_storage_error(error) + return { + 'public_access': response.get('share_public_access'), + 'signed_identifiers': identifiers or [] + } + def set_share_acl(self, signed_identifiers=None, timeout=None, **kwargs): # type: (Optional[Dict[str, Optional[AccessPolicy]]], Optional[int]) -> Dict[str, str] """ :returns: None. """ + if signed_identifiers: + if len(signed_identifiers) > 5: + raise ValueError( + 'Too many access policies provided. The server does not support setting ' + 'more than 5 access policies on a single resource.') + identifiers = [] + for key, value in signed_identifiers.items(): + if value: + value.start = serialize_iso(value.start) + value.expiry = serialize_iso(value.expiry) + identifiers.append(SignedIdentifier(id=key, access_policy=value)) + signed_identifiers = identifiers + + try: + return self._client.share.set_access_policy( + share_acl=signed_identifiers or None, + timeout=timeout, + cls=return_response_headers, + **kwargs) + except StorageErrorException as error: + process_storage_error(error) def get_share_stats(self, timeout=None, **kwargs): # type: (Optional[int]) -> Dict[str, str] """ :returns: ShareStats in a dict. """ + try: + return self._client.share.get_statistics( + timeout=timeout, + cls=return_response_headers, + **kwargs) + except StorageErrorException as error: + process_storage_error(error) - def list_directies_and_files(self, prefix=None, timeout=None, **kwargs): + + def list_directies_and_files(self, directory_name, prefix=None, timeout=None, **kwargs): # type: (Optional[str], Optional[int]) -> DirectoryProperties """ :returns: An auto-paging iterable of dict-like DirectoryProperties and FileProperties """ - - def create_directory(self, directory_name, metadata=None, timeout=None): - # type: (str, Optional[Dict[str, Any]], Optional[int]) -> DirectoryClient + directory = self.get_directory_client(directory_name) + return directory.list_directies_and_files(prefix, timeout, **kwargs) + + def create_directory(self, directory_name, metadata=None, timeout=None, **kwargs): + # type: (str, Optional[Dict[str, Any]], Optional[int], Any) -> DirectoryClient """ :returns: DirectoryClient """ + directory = self.get_directory_client(directory_name) + directory.create_directory(metadata, timeout, **kwargs) + return directory