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
7from typing import (  # pylint: disable=unused-import
8    Union, Optional, Any, Iterable, Dict, List, Type, Tuple,
9    TYPE_CHECKING
10)
11
12import logging
13from os import fstat
14from io import (SEEK_END, SEEK_SET, UnsupportedOperation)
15
16import isodate
17
18from azure.core.exceptions import raise_with_traceback
19
20
21_LOGGER = logging.getLogger(__name__)
22
23
24def serialize_iso(attr):
25    """Serialize Datetime object into ISO-8601 formatted string.
26
27    :param Datetime attr: Object to be serialized.
28    :rtype: str
29    :raises: ValueError if format invalid.
30    """
31    if not attr:
32        return None
33    if isinstance(attr, str):
34        attr = isodate.parse_datetime(attr)
35    try:
36        utc = attr.utctimetuple()
37        if utc.tm_year > 9999 or utc.tm_year < 1:
38            raise OverflowError("Hit max or min date")
39
40        date = "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}".format(
41            utc.tm_year, utc.tm_mon, utc.tm_mday,
42            utc.tm_hour, utc.tm_min, utc.tm_sec)
43        return date + 'Z'
44    except (ValueError, OverflowError) as err:
45        msg = "Unable to serialize datetime object."
46        raise_with_traceback(ValueError, msg, err)
47    except AttributeError as err:
48        msg = "ISO-8601 object must be valid Datetime object."
49        raise_with_traceback(TypeError, msg, err)
50
51
52def get_length(data):
53    length = None
54    # Check if object implements the __len__ method, covers most input cases such as bytearray.
55    try:
56        length = len(data)
57    except:  # pylint: disable=bare-except
58        pass
59
60    if not length:
61        # Check if the stream is a file-like stream object.
62        # If so, calculate the size using the file descriptor.
63        try:
64            fileno = data.fileno()
65        except (AttributeError, UnsupportedOperation):
66            pass
67        else:
68            try:
69                return fstat(fileno).st_size
70            except OSError:
71                # Not a valid fileno, may be possible requests returned
72                # a socket number?
73                pass
74
75        # If the stream is seekable and tell() is implemented, calculate the stream size.
76        try:
77            current_position = data.tell()
78            data.seek(0, SEEK_END)
79            length = data.tell() - current_position
80            data.seek(current_position, SEEK_SET)
81        except (AttributeError, UnsupportedOperation):
82            pass
83
84    return length
85
86
87def read_length(data):
88    try:
89        if hasattr(data, 'read'):
90            read_data = b''
91            for chunk in iter(lambda: data.read(4096), b""):
92                read_data += chunk
93            return len(read_data), read_data
94        if hasattr(data, '__iter__'):
95            read_data = b''
96            for chunk in data:
97                read_data += chunk
98            return len(read_data), read_data
99    except:  # pylint: disable=bare-except
100        pass
101    raise ValueError("Unable to calculate content length, please specify.")
102
103
104def validate_and_format_range_headers(
105        start_range, end_range, start_range_required=True,
106        end_range_required=True, check_content_md5=False, align_to_page=False):
107    # If end range is provided, start range must be provided
108    if (start_range_required or end_range is not None) and start_range is None:
109        raise ValueError("start_range value cannot be None.")
110    if end_range_required and end_range is None:
111        raise ValueError("end_range value cannot be None.")
112
113    # Page ranges must be 512 aligned
114    if align_to_page:
115        if start_range is not None and start_range % 512 != 0:
116            raise ValueError("Invalid page blob start_range: {0}. "
117                             "The size must be aligned to a 512-byte boundary.".format(start_range))
118        if end_range is not None and end_range % 512 != 511:
119            raise ValueError("Invalid page blob end_range: {0}. "
120                             "The size must be aligned to a 512-byte boundary.".format(end_range))
121
122    # Format based on whether end_range is present
123    range_header = None
124    if end_range is not None:
125        range_header = 'bytes={0}-{1}'.format(start_range, end_range)
126    elif start_range is not None:
127        range_header = "bytes={0}-".format(start_range)
128
129    # Content MD5 can only be provided for a complete range less than 4MB in size
130    range_validation = None
131    if check_content_md5:
132        if start_range is None or end_range is None:
133            raise ValueError("Both start and end range requied for MD5 content validation.")
134        if end_range - start_range > 4 * 1024 * 1024:
135            raise ValueError("Getting content MD5 for a range greater than 4MB is not supported.")
136        range_validation = 'true'
137
138    return range_header, range_validation
139
140
141def add_metadata_headers(metadata=None):
142    # type: (Optional[Dict[str, str]]) -> Dict[str, str]
143    headers = {}
144    if metadata:
145        for key, value in metadata.items():
146            headers['x-ms-meta-{}'.format(key.strip())] = value.strip() if value else value
147    return headers
148