1# -------------------------------------------------------------------------
2# Copyright (c) Microsoft Corporation. All rights reserved.
3# Licensed under the MIT License. See License.txt in the project root for
4# license information.
5# --------------------------------------------------------------------------
6
7import functools
8from typing import (  # pylint: disable=unused-import
9    Union, Optional, Any, Iterable, Dict, List,
10    TYPE_CHECKING
11)
12
13try:
14    from urllib.parse import urlparse
15except ImportError:
16    from urlparse import urlparse # type: ignore
17
18from azure.core.paging import ItemPaged
19from azure.core.pipeline import Pipeline
20from azure.core.tracing.decorator import distributed_trace
21
22from ._shared.models import LocationMode
23from ._shared.base_client import StorageAccountHostsMixin, TransportWrapper, parse_connection_str, parse_query
24from ._shared.parser import _to_utc_datetime
25from ._shared.response_handlers import return_response_headers, process_storage_error, \
26    parse_to_internal_user_delegation_key
27from ._generated import AzureBlobStorage
28from ._generated.models import StorageErrorException, StorageServiceProperties, KeyInfo
29from ._container_client import ContainerClient
30from ._blob_client import BlobClient
31from ._models import (
32    ContainerPropertiesPaged,
33    service_stats_deserialize,
34    service_properties_deserialize
35)
36
37if TYPE_CHECKING:
38    from datetime import datetime
39    from azure.core.pipeline.transport import HttpTransport
40    from azure.core.pipeline.policies import HTTPPolicy
41    from ._shared.models import UserDelegationKey
42    from ._lease import BlobLeaseClient
43    from ._models import (
44        BlobProperties,
45        ContainerProperties,
46        PublicAccess,
47        BlobAnalyticsLogging,
48        Metrics,
49        CorsRule,
50        RetentionPolicy,
51        StaticWebsite,
52    )
53
54
55class BlobServiceClient(StorageAccountHostsMixin):
56    """A client to interact with the Blob Service at the account level.
57
58    This client provides operations to retrieve and configure the account properties
59    as well as list, create and delete containers within the account.
60    For operations relating to a specific container or blob, clients for those entities
61    can also be retrieved using the `get_client` functions.
62
63    :param str account_url:
64        The URL to the blob storage account. Any other entities included
65        in the URL path (e.g. container or blob) will be discarded. This URL can be optionally
66        authenticated with a SAS token.
67    :param credential:
68        The credentials with which to authenticate. This is optional if the
69        account URL already has a SAS token. The value can be a SAS token string, an account
70        shared access key, or an instance of a TokenCredentials class from azure.identity.
71        If the URL already has a SAS token, specifying an explicit credential will take priority.
72    :keyword str secondary_hostname:
73        The hostname of the secondary endpoint.
74    :keyword int max_block_size: The maximum chunk size for uploading a block blob in chunks.
75        Defaults to 4*1024*1024, or 4MB.
76    :keyword int max_single_put_size: If the blob size is less than max_single_put_size, then the blob will be
77        uploaded with only one http PUT request. If the blob size is larger than max_single_put_size,
78        the blob will be uploaded in chunks. Defaults to 64*1024*1024, or 64MB.
79    :keyword int min_large_block_upload_threshold: The minimum chunk size required to use the memory efficient
80        algorithm when uploading a block blob. Defaults to 4*1024*1024+1.
81    :keyword bool use_byte_buffer: Use a byte buffer for block blob uploads. Defaults to False.
82    :keyword int max_page_size: The maximum chunk size for uploading a page blob. Defaults to 4*1024*1024, or 4MB.
83    :keyword int max_single_get_size: The maximum size for a blob to be downloaded in a single call,
84        the exceeded part will be downloaded in chunks (could be parallel). Defaults to 32*1024*1024, or 32MB.
85    :keyword int max_chunk_get_size: The maximum chunk size used for downloading a blob. Defaults to 4*1024*1024,
86        or 4MB.
87
88    .. admonition:: Example:
89
90        .. literalinclude:: ../samples/blob_samples_authentication.py
91            :start-after: [START create_blob_service_client]
92            :end-before: [END create_blob_service_client]
93            :language: python
94            :dedent: 8
95            :caption: Creating the BlobServiceClient with account url and credential.
96
97        .. literalinclude:: ../samples/blob_samples_authentication.py
98            :start-after: [START create_blob_service_client_oauth]
99            :end-before: [END create_blob_service_client_oauth]
100            :language: python
101            :dedent: 8
102            :caption: Creating the BlobServiceClient with Azure Identity credentials.
103    """
104
105    def __init__(
106            self, account_url,  # type: str
107            credential=None,  # type: Optional[Any]
108            **kwargs  # type: Any
109        ):
110        # type: (...) -> None
111        try:
112            if not account_url.lower().startswith('http'):
113                account_url = "https://" + account_url
114        except AttributeError:
115            raise ValueError("Account URL must be a string.")
116        parsed_url = urlparse(account_url.rstrip('/'))
117        if not parsed_url.netloc:
118            raise ValueError("Invalid URL: {}".format(account_url))
119
120        _, sas_token = parse_query(parsed_url.query)
121        self._query_str, credential = self._format_query_string(sas_token, credential)
122        super(BlobServiceClient, self).__init__(parsed_url, service='blob', credential=credential, **kwargs)
123        self._client = AzureBlobStorage(self.url, pipeline=self._pipeline)
124
125    def _format_url(self, hostname):
126        """Format the endpoint URL according to the current location
127        mode hostname.
128        """
129        return "{}://{}/{}".format(self.scheme, hostname, self._query_str)
130
131    @classmethod
132    def from_connection_string(
133            cls, conn_str,  # type: str
134            credential=None,  # type: Optional[Any]
135            **kwargs  # type: Any
136        ):  # type: (...) -> BlobServiceClient
137        """Create BlobServiceClient from a Connection String.
138
139        :param str conn_str:
140            A connection string to an Azure Storage account.
141        :param credential:
142            The credentials with which to authenticate. This is optional if the
143            account URL already has a SAS token, or the connection string already has shared
144            access key values. The value can be a SAS token string, an account shared access
145            key, or an instance of a TokenCredentials class from azure.identity.
146            Credentials provided here will take precedence over those in the connection string.
147        :returns: A Blob service client.
148        :rtype: ~azure.storage.blob.BlobServiceClient
149
150        .. admonition:: Example:
151
152            .. literalinclude:: ../samples/blob_samples_authentication.py
153                :start-after: [START auth_from_connection_string]
154                :end-before: [END auth_from_connection_string]
155                :language: python
156                :dedent: 8
157                :caption: Creating the BlobServiceClient from a connection string.
158        """
159        account_url, secondary, credential = parse_connection_str(conn_str, credential, 'blob')
160        if 'secondary_hostname' not in kwargs:
161            kwargs['secondary_hostname'] = secondary
162        return cls(account_url, credential=credential, **kwargs)
163
164    @distributed_trace
165    def get_user_delegation_key(self, key_start_time,  # type: datetime
166                                key_expiry_time,  # type: datetime
167                                **kwargs  # type: Any
168                                ):
169        # type: (...) -> UserDelegationKey
170        """
171        Obtain a user delegation key for the purpose of signing SAS tokens.
172        A token credential must be present on the service object for this request to succeed.
173
174        :param ~datetime.datetime key_start_time:
175            A DateTime value. Indicates when the key becomes valid.
176        :param ~datetime.datetime key_expiry_time:
177            A DateTime value. Indicates when the key stops being valid.
178        :keyword int timeout:
179            The timeout parameter is expressed in seconds.
180        :return: The user delegation key.
181        :rtype: ~azure.storage.blob.UserDelegationKey
182        """
183        key_info = KeyInfo(start=_to_utc_datetime(key_start_time), expiry=_to_utc_datetime(key_expiry_time))
184        timeout = kwargs.pop('timeout', None)
185        try:
186            user_delegation_key = self._client.service.get_user_delegation_key(key_info=key_info,
187                                                                               timeout=timeout,
188                                                                               **kwargs)  # type: ignore
189        except StorageErrorException as error:
190            process_storage_error(error)
191
192        return parse_to_internal_user_delegation_key(user_delegation_key)  # type: ignore
193
194    @distributed_trace
195    def get_account_information(self, **kwargs):
196        # type: (Any) -> Dict[str, str]
197        """Gets information related to the storage account.
198
199        The information can also be retrieved if the user has a SAS to a container or blob.
200        The keys in the returned dictionary include 'sku_name' and 'account_kind'.
201
202        :returns: A dict of account information (SKU and account type).
203        :rtype: dict(str, str)
204
205        .. admonition:: Example:
206
207            .. literalinclude:: ../samples/blob_samples_service.py
208                :start-after: [START get_blob_service_account_info]
209                :end-before: [END get_blob_service_account_info]
210                :language: python
211                :dedent: 8
212                :caption: Getting account information for the blob service.
213        """
214        try:
215            return self._client.service.get_account_info(cls=return_response_headers, **kwargs) # type: ignore
216        except StorageErrorException as error:
217            process_storage_error(error)
218
219    @distributed_trace
220    def get_service_stats(self, **kwargs):
221        # type: (**Any) -> Dict[str, Any]
222        """Retrieves statistics related to replication for the Blob service.
223
224        It is only available when read-access geo-redundant replication is enabled for
225        the storage account.
226
227        With geo-redundant replication, Azure Storage maintains your data durable
228        in two locations. In both locations, Azure Storage constantly maintains
229        multiple healthy replicas of your data. The location where you read,
230        create, update, or delete data is the primary storage account location.
231        The primary location exists in the region you choose at the time you
232        create an account via the Azure Management Azure classic portal, for
233        example, North Central US. The location to which your data is replicated
234        is the secondary location. The secondary location is automatically
235        determined based on the location of the primary; it is in a second data
236        center that resides in the same region as the primary location. Read-only
237        access is available from the secondary location, if read-access geo-redundant
238        replication is enabled for your storage account.
239
240        :keyword int timeout:
241            The timeout parameter is expressed in seconds.
242        :return: The blob service stats.
243        :rtype: Dict[str, Any]
244
245        .. admonition:: Example:
246
247            .. literalinclude:: ../samples/blob_samples_service.py
248                :start-after: [START get_blob_service_stats]
249                :end-before: [END get_blob_service_stats]
250                :language: python
251                :dedent: 8
252                :caption: Getting service stats for the blob service.
253        """
254        timeout = kwargs.pop('timeout', None)
255        try:
256            stats = self._client.service.get_statistics( # type: ignore
257                timeout=timeout, use_location=LocationMode.SECONDARY, **kwargs)
258            return service_stats_deserialize(stats)
259        except StorageErrorException as error:
260            process_storage_error(error)
261
262    @distributed_trace
263    def get_service_properties(self, **kwargs):
264        # type: (Any) -> Dict[str, Any]
265        """Gets the properties of a storage account's Blob service, including
266        Azure Storage Analytics.
267
268        :keyword int timeout:
269            The timeout parameter is expressed in seconds.
270        :returns: An object containing blob service properties such as
271            analytics logging, hour/minute metrics, cors rules, etc.
272        :rtype: Dict[str, Any]
273
274        .. admonition:: Example:
275
276            .. literalinclude:: ../samples/blob_samples_service.py
277                :start-after: [START get_blob_service_properties]
278                :end-before: [END get_blob_service_properties]
279                :language: python
280                :dedent: 8
281                :caption: Getting service properties for the blob service.
282        """
283        timeout = kwargs.pop('timeout', None)
284        try:
285            service_props = self._client.service.get_properties(timeout=timeout, **kwargs)
286            return service_properties_deserialize(service_props)
287        except StorageErrorException as error:
288            process_storage_error(error)
289
290    @distributed_trace
291    def set_service_properties(
292            self, analytics_logging=None,  # type: Optional[BlobAnalyticsLogging]
293            hour_metrics=None,  # type: Optional[Metrics]
294            minute_metrics=None,  # type: Optional[Metrics]
295            cors=None,  # type: Optional[List[CorsRule]]
296            target_version=None,  # type: Optional[str]
297            delete_retention_policy=None,  # type: Optional[RetentionPolicy]
298            static_website=None,  # type: Optional[StaticWebsite]
299            **kwargs
300        ):
301        # type: (...) -> None
302        """Sets the properties of a storage account's Blob service, including
303        Azure Storage Analytics.
304
305        If an element (e.g. analytics_logging) is left as None, the
306        existing settings on the service for that functionality are preserved.
307
308        :param analytics_logging:
309            Groups the Azure Analytics Logging settings.
310        :type analytics_logging: ~azure.storage.blob.BlobAnalyticsLogging
311        :param hour_metrics:
312            The hour metrics settings provide a summary of request
313            statistics grouped by API in hourly aggregates for blobs.
314        :type hour_metrics: ~azure.storage.blob.Metrics
315        :param minute_metrics:
316            The minute metrics settings provide request statistics
317            for each minute for blobs.
318        :type minute_metrics: ~azure.storage.blob.Metrics
319        :param cors:
320            You can include up to five CorsRule elements in the
321            list. If an empty list is specified, all CORS rules will be deleted,
322            and CORS will be disabled for the service.
323        :type cors: list[~azure.storage.blob.CorsRule]
324        :param str target_version:
325            Indicates the default version to use for requests if an incoming
326            request's version is not specified.
327        :param delete_retention_policy:
328            The delete retention policy specifies whether to retain deleted blobs.
329            It also specifies the number of days and versions of blob to keep.
330        :type delete_retention_policy: ~azure.storage.blob.RetentionPolicy
331        :param static_website:
332            Specifies whether the static website feature is enabled,
333            and if yes, indicates the index document and 404 error document to use.
334        :type static_website: ~azure.storage.blob.StaticWebsite
335        :keyword int timeout:
336            The timeout parameter is expressed in seconds.
337        :rtype: None
338
339        .. admonition:: Example:
340
341            .. literalinclude:: ../samples/blob_samples_service.py
342                :start-after: [START set_blob_service_properties]
343                :end-before: [END set_blob_service_properties]
344                :language: python
345                :dedent: 8
346                :caption: Setting service properties for the blob service.
347        """
348        props = StorageServiceProperties(
349            logging=analytics_logging,
350            hour_metrics=hour_metrics,
351            minute_metrics=minute_metrics,
352            cors=cors,
353            default_service_version=target_version,
354            delete_retention_policy=delete_retention_policy,
355            static_website=static_website
356        )
357        timeout = kwargs.pop('timeout', None)
358        try:
359            self._client.service.set_properties(props, timeout=timeout, **kwargs)
360        except StorageErrorException as error:
361            process_storage_error(error)
362
363    @distributed_trace
364    def list_containers(
365            self, name_starts_with=None,  # type: Optional[str]
366            include_metadata=False,  # type: Optional[bool]
367            **kwargs
368        ):
369        # type: (...) -> ItemPaged[ContainerProperties]
370        """Returns a generator to list the containers under the specified account.
371
372        The generator will lazily follow the continuation tokens returned by
373        the service and stop when all containers have been returned.
374
375        :param str name_starts_with:
376            Filters the results to return only containers whose names
377            begin with the specified prefix.
378        :param bool include_metadata:
379            Specifies that container metadata to be returned in the response.
380            The default value is `False`.
381        :keyword int results_per_page:
382            The maximum number of container names to retrieve per API
383            call. If the request does not specify the server will return up to 5,000 items.
384        :keyword int timeout:
385            The timeout parameter is expressed in seconds.
386        :returns: An iterable (auto-paging) of ContainerProperties.
387        :rtype: ~azure.core.paging.ItemPaged[~azure.storage.blob.ContainerProperties]
388
389        .. admonition:: Example:
390
391            .. literalinclude:: ../samples/blob_samples_service.py
392                :start-after: [START bsc_list_containers]
393                :end-before: [END bsc_list_containers]
394                :language: python
395                :dedent: 12
396                :caption: Listing the containers in the blob service.
397        """
398        include = 'metadata' if include_metadata else None
399        timeout = kwargs.pop('timeout', None)
400        results_per_page = kwargs.pop('results_per_page', None)
401        command = functools.partial(
402            self._client.service.list_containers_segment,
403            prefix=name_starts_with,
404            include=include,
405            timeout=timeout,
406            **kwargs)
407        return ItemPaged(
408                command,
409                prefix=name_starts_with,
410                results_per_page=results_per_page,
411                page_iterator_class=ContainerPropertiesPaged
412            )
413
414    @distributed_trace
415    def create_container(
416            self, name,  # type: str
417            metadata=None,  # type: Optional[Dict[str, str]]
418            public_access=None,  # type: Optional[Union[PublicAccess, str]]
419            **kwargs
420        ):
421        # type: (...) -> ContainerClient
422        """Creates a new container under the specified account.
423
424        If the container with the same name already exists, a ResourceExistsError will
425        be raised. This method returns a client with which to interact with the newly
426        created container.
427
428        :param str name: The name of the container to create.
429        :param metadata:
430            A dict with name-value pairs to associate with the
431            container as metadata. Example: `{'Category':'test'}`
432        :type metadata: dict(str, str)
433        :param public_access:
434            Possible values include: 'container', 'blob'.
435        :type public_access: str or ~azure.storage.blob.PublicAccess
436        :keyword int timeout:
437            The timeout parameter is expressed in seconds.
438        :rtype: ~azure.storage.blob.ContainerClient
439
440        .. admonition:: Example:
441
442            .. literalinclude:: ../samples/blob_samples_service.py
443                :start-after: [START bsc_create_container]
444                :end-before: [END bsc_create_container]
445                :language: python
446                :dedent: 12
447                :caption: Creating a container in the blob service.
448        """
449        container = self.get_container_client(name)
450        kwargs.setdefault('merge_span', True)
451        timeout = kwargs.pop('timeout', None)
452        container.create_container(
453            metadata=metadata, public_access=public_access, timeout=timeout, **kwargs)
454        return container
455
456    @distributed_trace
457    def delete_container(
458            self, container,  # type: Union[ContainerProperties, str]
459            lease=None,  # type: Optional[Union[BlobLeaseClient, str]]
460            **kwargs
461        ):
462        # type: (...) -> None
463        """Marks the specified container for deletion.
464
465        The container and any blobs contained within it are later deleted during garbage collection.
466        If the container is not found, a ResourceNotFoundError will be raised.
467
468        :param container:
469            The container to delete. This can either be the name of the container,
470            or an instance of ContainerProperties.
471        :type container: str or ~azure.storage.blob.ContainerProperties
472        :param lease:
473            If specified, delete_container only succeeds if the
474            container's lease is active and matches this ID.
475            Required if the container has an active lease.
476        :paramtype lease: ~azure.storage.blob.BlobLeaseClient or str
477        :keyword ~datetime.datetime if_modified_since:
478            A DateTime value. Azure expects the date value passed in to be UTC.
479            If timezone is included, any non-UTC datetimes will be converted to UTC.
480            If a date is passed in without timezone info, it is assumed to be UTC.
481            Specify this header to perform the operation only
482            if the resource has been modified since the specified time.
483        :keyword ~datetime.datetime if_unmodified_since:
484            A DateTime value. Azure expects the date value passed in to be UTC.
485            If timezone is included, any non-UTC datetimes will be converted to UTC.
486            If a date is passed in without timezone info, it is assumed to be UTC.
487            Specify this header to perform the operation only if
488            the resource has not been modified since the specified date/time.
489        :keyword str etag:
490            An ETag value, or the wildcard character (*). Used to check if the resource has changed,
491            and act according to the condition specified by the `match_condition` parameter.
492        :keyword ~azure.core.MatchConditions match_condition:
493            The match condition to use upon the etag.
494        :keyword int timeout:
495            The timeout parameter is expressed in seconds.
496        :rtype: None
497
498        .. admonition:: Example:
499
500            .. literalinclude:: ../samples/blob_samples_service.py
501                :start-after: [START bsc_delete_container]
502                :end-before: [END bsc_delete_container]
503                :language: python
504                :dedent: 12
505                :caption: Deleting a container in the blob service.
506        """
507        container = self.get_container_client(container) # type: ignore
508        kwargs.setdefault('merge_span', True)
509        timeout = kwargs.pop('timeout', None)
510        container.delete_container( # type: ignore
511            lease=lease,
512            timeout=timeout,
513            **kwargs)
514
515    def get_container_client(self, container):
516        # type: (Union[ContainerProperties, str]) -> ContainerClient
517        """Get a client to interact with the specified container.
518
519        The container need not already exist.
520
521        :param container:
522            The container. This can either be the name of the container,
523            or an instance of ContainerProperties.
524        :type container: str or ~azure.storage.blob.ContainerProperties
525        :returns: A ContainerClient.
526        :rtype: ~azure.storage.blob.ContainerClient
527
528        .. admonition:: Example:
529
530            .. literalinclude:: ../samples/blob_samples_service.py
531                :start-after: [START bsc_get_container_client]
532                :end-before: [END bsc_get_container_client]
533                :language: python
534                :dedent: 8
535                :caption: Getting the container client to interact with a specific container.
536        """
537        try:
538            container_name = container.name
539        except AttributeError:
540            container_name = container
541        _pipeline = Pipeline(
542            transport=TransportWrapper(self._pipeline._transport), # pylint: disable = protected-access
543            policies=self._pipeline._impl_policies # pylint: disable = protected-access
544        )
545        return ContainerClient(
546            self.url, container_name=container_name,
547            credential=self.credential, _configuration=self._config,
548            _pipeline=_pipeline, _location_mode=self._location_mode, _hosts=self._hosts,
549            require_encryption=self.require_encryption, key_encryption_key=self.key_encryption_key,
550            key_resolver_function=self.key_resolver_function)
551
552    def get_blob_client(
553            self, container,  # type: Union[ContainerProperties, str]
554            blob,  # type: Union[BlobProperties, str]
555            snapshot=None  # type: Optional[Union[Dict[str, Any], str]]
556        ):
557        # type: (...) -> BlobClient
558        """Get a client to interact with the specified blob.
559
560        The blob need not already exist.
561
562        :param container:
563            The container that the blob is in. This can either be the name of the container,
564            or an instance of ContainerProperties.
565        :type container: str or ~azure.storage.blob.ContainerProperties
566        :param blob:
567            The blob with which to interact. This can either be the name of the blob,
568            or an instance of BlobProperties.
569        :type blob: str or ~azure.storage.blob.BlobProperties
570        :param snapshot:
571            The optional blob snapshot on which to operate. This can either be the ID of the snapshot,
572            or a dictionary output returned by :func:`~azure.storage.blob.BlobClient.create_snapshot()`.
573        :type snapshot: str or dict(str, Any)
574        :returns: A BlobClient.
575        :rtype: ~azure.storage.blob.BlobClient
576
577        .. admonition:: Example:
578
579            .. literalinclude:: ../samples/blob_samples_service.py
580                :start-after: [START bsc_get_blob_client]
581                :end-before: [END bsc_get_blob_client]
582                :language: python
583                :dedent: 12
584                :caption: Getting the blob client to interact with a specific blob.
585        """
586        try:
587            container_name = container.name
588        except AttributeError:
589            container_name = container
590        try:
591            blob_name = blob.name
592        except AttributeError:
593            blob_name = blob
594        _pipeline = Pipeline(
595            transport=TransportWrapper(self._pipeline._transport), # pylint: disable = protected-access
596            policies=self._pipeline._impl_policies # pylint: disable = protected-access
597        )
598        return BlobClient( # type: ignore
599            self.url, container_name=container_name, blob_name=blob_name, snapshot=snapshot,
600            credential=self.credential, _configuration=self._config,
601            _pipeline=_pipeline, _location_mode=self._location_mode, _hosts=self._hosts,
602            require_encryption=self.require_encryption, key_encryption_key=self.key_encryption_key,
603            key_resolver_function=self.key_resolver_function)
604