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