winazurestorageのフォーク
Revision | daaf7ce6bbab25bf8718aee876ab288f4db0f549 (tree) |
---|---|
Zeit | 2008-11-22 14:07:50 |
Autor | Steve Marx <Steve.Marx@micr...> |
Commiter | Steve Marx |
Add early table support, support for non-path-style-URIs, and move to urllib2 for proxy support.
@@ -7,22 +7,24 @@ Sriram Krishnan <sriramk@microsoft.com> | ||
7 | 7 | |
8 | 8 | import base64 |
9 | 9 | import hmac |
10 | -import httplib | |
11 | 10 | import hashlib |
12 | 11 | import time |
13 | -import urllib | |
14 | -import urlparse | |
15 | 12 | import sys |
16 | 13 | import os |
17 | 14 | from xml.dom import minidom #TODO: Use a faster way of processing XML |
15 | +import re | |
16 | +from urllib2 import Request, urlopen | |
17 | +from urlparse import urlsplit | |
18 | +from datetime import datetime, timedelta | |
18 | 19 | |
19 | 20 | DEVSTORE_ACCOUNT = "devstoreaccount1" |
20 | 21 | DEVSTORE_SECRET_KEY = "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==" |
21 | 22 | |
23 | +DEVSTORE_BLOB_HOST = "127.0.0.1:10000" | |
24 | +DEVSTORE_TABLE_HOST = "127.0.0.1:10002" | |
22 | 25 | |
23 | -DEVSTORE_HOST="127.0.0.1:10000" | |
24 | -CLOUD_HOST = "blob.core.windows.net" | |
25 | - | |
26 | +CLOUD_BLOB_HOST = "blob.core.windows.net" | |
27 | +CLOUD_TABLE_HOST = "table.core.windows.net" | |
26 | 28 | |
27 | 29 | PREFIX_PROPERTIES = "x-ms-prop-" |
28 | 30 | PREFIX_METADATA = "x-ms-meta-" |
@@ -34,36 +36,157 @@ DEBUG = False | ||
34 | 36 | |
35 | 37 | TIME_FORMAT ="%a, %d %b %Y %H:%M:%S %Z" |
36 | 38 | |
39 | +def parse_edm_datetime(input): | |
40 | + d = datetime.strptime(input[:input.find('.')], "%Y-%m-%dT%H:%M:%S") | |
41 | + if input[:input.find('.')] != -1: | |
42 | + d += timedelta(0, 0, int(round(float(input[input.index('.'):-1])*1000000))) | |
43 | + return d | |
37 | 44 | |
38 | -class WAStorageConnection: | |
39 | - def __init__(self, server_url = DEVSTORE_HOST, account_name=DEVSTORE_ACCOUNT,secret_key = DEVSTORE_SECRET_KEY): | |
40 | - self.server_url = server_url | |
41 | - self.account_name = account_name | |
42 | - self.secret_key = base64.decodestring(secret_key) | |
43 | - | |
44 | - | |
45 | - def create_container(self,container_name, is_public): | |
46 | - headers ={} | |
47 | - if is_public: | |
48 | - headers[PREFIX_PROPERTIES+ "publicaccess"] = "true" | |
45 | +def parse_edm_int32(input): | |
46 | + return int(input) | |
47 | + | |
48 | +class SharedKeyCredentials(object): | |
49 | + def __init__(self, account_name, account_key, use_path_style_uris = None): | |
50 | + self._account = account_name | |
51 | + self._key = base64.decodestring(account_key) | |
52 | + | |
53 | + def _sign_request_impl(self, request, for_tables = False, use_path_style_uris = None): | |
54 | + (scheme, host, path, query, fragment) = urlsplit(request.get_full_url()) | |
55 | + if use_path_style_uris: | |
56 | + path = path[path.index('/'):] | |
57 | + | |
58 | + canonicalized_resource = "/" + self._account + path | |
59 | + match = re.search(r'comp=[^&]*', query) | |
60 | + if match is not None: | |
61 | + canonicalized_resource += "?" + match.group(0) | |
49 | 62 | |
50 | - return self._do_store_request(container_name, None, 'PUT', headers) | |
51 | - | |
52 | - def put_blob(self, container_name, key, data, content_type=None): | |
53 | - return self._do_store_request(container_name, key, 'PUT', {}, data, content_type ) | |
54 | - | |
55 | - def get_blob(self, container_name, key): | |
56 | - response = self._do_store_request(container_name, key, 'GET' ) | |
57 | - return response.read() | |
58 | - | |
59 | - def list_containers(self): | |
60 | - #TODO: Deal with nextmarker for large requests (only 5K containers returned by default) | |
61 | - #TODO: Use a different XML parsing scheme | |
62 | - | |
63 | - response = self._do_store_request(query_string = "?comp=list") | |
64 | - | |
63 | + if use_path_style_uris is None: | |
64 | + use_path_style_uris = re.match('^[\d.:]+$', host) is not None | |
65 | + | |
66 | + request.add_header(PREFIX_STORAGE_HEADER + 'date', time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime())) #RFC 1123 | |
67 | + canonicalized_headers = NEW_LINE.join(('%s:%s' % (k.lower(), request.get_header(k).strip()) for k in sorted(request.headers.keys(), lambda x,y: cmp(x.lower(), y.lower())) if k.lower().startswith(PREFIX_STORAGE_HEADER))) | |
68 | + | |
69 | + string_to_sign = request.get_method().upper() + NEW_LINE # verb | |
70 | + string_to_sign += NEW_LINE # MD5 not required | |
71 | + if request.get_header('Content-type') is not None: # Content-Type | |
72 | + string_to_sign += request.get_header('Content-type') | |
73 | + string_to_sign += NEW_LINE | |
74 | + if for_tables: string_to_sign += request.get_header(PREFIX_STORAGE_HEADER.capitalize() + 'date') + NEW_LINE | |
75 | + else: string_to_sign += NEW_LINE # Date | |
76 | + if not for_tables: | |
77 | + string_to_sign += canonicalized_headers + NEW_LINE # Canonicalized headers | |
78 | + string_to_sign += canonicalized_resource # Canonicalized resource | |
79 | + | |
80 | + request.add_header('Authorization', 'SharedKey ' + self._account + ':' + base64.encodestring(hmac.new(self._key, unicode(string_to_sign).encode("utf-8"), hashlib.sha256).digest()).strip()) | |
81 | + return request | |
82 | + | |
83 | + def sign_request(self, request, use_path_style_uris = None): | |
84 | + return self._sign_request_impl(request, use_path_style_uris) | |
85 | + | |
86 | + def sign_table_request(self, request, use_path_style_uris = None): | |
87 | + return self._sign_request_impl(request, for_tables = True, use_path_style_uris = use_path_style_uris) | |
88 | + | |
89 | +class RequestWithMethod(Request): | |
90 | + '''Subclass urllib2.Request to add the capability of using methods other than GET and POST. | |
91 | + Thanks to http://benjamin.smedbergs.us/blog/2008-10-21/putting-and-deleteing-in-python-urllib2/''' | |
92 | + def __init__(self, method, *args, **kwargs): | |
93 | + self._method = method | |
94 | + Request.__init__(self, *args, **kwargs) | |
95 | + | |
96 | + def get_method(self): | |
97 | + return self._method | |
98 | + | |
99 | +class Table(object): | |
100 | + def __init__(self, url, name): | |
101 | + self.url = url | |
102 | + self.name = name | |
103 | + | |
104 | +class Storage(object): | |
105 | + def __init__(self, host, account_name, secret_key, use_path_style_uris): | |
106 | + self._host = host | |
107 | + self._account = account_name | |
108 | + self._key = secret_key | |
109 | + if use_path_style_uris is None: | |
110 | + use_path_style_uris = re.match(r'^[^:]*[\d:]+$', self._host) | |
111 | + self._use_path_style_uris = use_path_style_uris | |
112 | + self._credentials = SharedKeyCredentials(self._account, self._key) | |
113 | + | |
114 | + def get_base_url(self): | |
115 | + if self._use_path_style_uris: | |
116 | + return "http://%s/%s" % (self._host, self._account) | |
117 | + else: | |
118 | + return "http://%s.%s" % (self._account, self._host) | |
119 | + | |
120 | +class TableEntity(object): pass | |
121 | + | |
122 | +class TableStorage(Storage): | |
123 | + '''Due to local development storage not supporting SharedKeyLite authentication, this class | |
124 | + will only work against cloud storage.''' | |
125 | + def __init__(self, host, account_name, secret_key, use_path_style_uris = None): | |
126 | + super(TableStorage, self).__init__(host, account_name, secret_key, use_path_style_uris) | |
127 | + | |
128 | + def list_tables(self): | |
129 | + req = Request("%s/Tables" % self.get_base_url()) | |
130 | + self._credentials.sign_table_request(req) | |
131 | + response = urlopen(req) | |
132 | + | |
65 | 133 | dom = minidom.parseString(response.read()) |
66 | 134 | |
135 | + entries = dom.getElementsByTagName("entry") | |
136 | + for entry in entries: | |
137 | + table_url = entry.getElementsByTagName("id")[0].firstChild.data | |
138 | + table_name = entry.getElementsByTagName("content")[0].getElementsByTagName("m:properties")[0].getElementsByTagName("d:TableName")[0].firstChild.data | |
139 | + yield Table(table_url, table_name) | |
140 | + dom.unlink() | |
141 | + | |
142 | + def get_entity(self, table_name, partition_key, row_key): | |
143 | + dom = minidom.parseString(urlopen(self._credentials.sign_table_request(Request("%s/%s(PartitionKey='%s',RowKey='%s')" % (self.get_base_url(), table_name, partition_key, row_key)))).read()) | |
144 | + entity = self._parse_entity(dom.getElementsByTagName("entry")[0]) | |
145 | + dom.unlink() | |
146 | + return entity | |
147 | + | |
148 | + def _parse_entity(self, entry): | |
149 | + entity = TableEntity() | |
150 | + for property in (p for p in entry.getElementsByTagName("m:properties")[0].childNodes if p.nodeType == minidom.Node.ELEMENT_NODE): | |
151 | + key = property.tagName[2:] | |
152 | + if property.hasAttribute('m:type'): | |
153 | + t = property.getAttribute('m:type') | |
154 | + if t.lower() == 'edm.datetime': value = parse_edm_datetime(property.firstChild.data) | |
155 | + elif t.lower() == 'edm.int32': value = parse_edm_int32(property.firstChild.data) | |
156 | + else: raise Exception(t.lower()) | |
157 | + else: value = property.firstChild.data | |
158 | + setattr(entity, key, value) | |
159 | + return entity | |
160 | + | |
161 | + def get_all(self, table_name): | |
162 | + dom = minidom.parseString(urlopen(self._credentials.sign_table_request(Request("%s/%s" % (self.get_base_url(), table_name)))).read()) | |
163 | + entries = dom.getElementsByTagName("entry") | |
164 | + entities = [] | |
165 | + for entry in entries: | |
166 | + entities.append(self._parse_entity(entry)) | |
167 | + dom.unlink() | |
168 | + return entities | |
169 | + | |
170 | +class BlobStorage(Storage): | |
171 | + def __init__(self, host = DEVSTORE_BLOB_HOST, account_name = DEVSTORE_ACCOUNT, secret_key = DEVSTORE_SECRET_KEY, use_path_style_uris = None): | |
172 | + super(BlobStorage, self).__init__(host, account_name, secret_key, use_path_style_uris) | |
173 | + | |
174 | + def create_container(self, container_name, is_public = False): | |
175 | + req = RequestWithMethod("PUT", "%s/%s" % (self.get_base_url(), container_name)) | |
176 | + req.add_header("Content-Length", "0") | |
177 | + self._credentials.sign_request(req) | |
178 | + if is_public: req.add_header(PREFIX_PROPERTIES + "publicaccess", "true") | |
179 | + return urlopen(req) | |
180 | + | |
181 | + def delete_container(self, container_name): | |
182 | + req = RequestWithMethod("DELETE", "%s/%s" % (self.get_base_url(), container_name)) | |
183 | + self._credentials.sign_request(req) | |
184 | + return urlopen(req) | |
185 | + | |
186 | + def list_containers(self): | |
187 | + req = Request("%s/?comp=list" % self.get_base_url()) | |
188 | + self._credentials.sign_request(req) | |
189 | + dom = minidom.parseString(urlopen(req).read()) | |
67 | 190 | containers = dom.getElementsByTagName("Container") |
68 | 191 | for container in containers: |
69 | 192 | container_name = container.getElementsByTagName("Name")[0].firstChild.data |
@@ -72,85 +195,21 @@ class WAStorageConnection: | ||
72 | 195 | yield (container_name, etag, last_modified) |
73 | 196 | |
74 | 197 | dom.unlink() #Docs say to do this to force GC. Ugh. |
75 | - | |
76 | - def _get_auth_header(self, http_method, path, data, headers): | |
77 | - string_to_sign ="" | |
78 | - | |
79 | - #First element is the method | |
80 | - string_to_sign += http_method + NEW_LINE | |
81 | - | |
82 | - #Second is the optional content MD5 | |
83 | - string_to_sign += NEW_LINE | |
84 | - | |
85 | - #content type - this should have been initialized atleast to a blank value | |
86 | - if headers.has_key("content-type"): | |
87 | - string_to_sign += headers["content-type"] | |
88 | - string_to_sign += NEW_LINE | |
89 | - | |
90 | - # date - we don't need to add header here since the special date storage header | |
91 | - # always exists in our implementation | |
92 | - string_to_sign += NEW_LINE | |
93 | - | |
94 | - # Construct canonicalized headers. | |
95 | - # TODO: Note that this doesn't implement parts of the spec - combining header fields with same name, | |
96 | - # unfolding long lines and trimming white spaces around the colon | |
97 | - | |
98 | - ms_headers =[header_key for header_key in headers.keys() if header_key.startswith(PREFIX_STORAGE_HEADER)] | |
99 | - ms_headers.sort() | |
100 | - for header_key in ms_headers: | |
101 | - string_to_sign += "%s:%s%s" % (header_key, headers[header_key], NEW_LINE) | |
102 | - | |
103 | - # Add canonicalized resource | |
104 | - string_to_sign += "/" + self.account_name + path | |
105 | - utf8_string_to_sign = unicode(string_to_sign).encode("utf-8") | |
106 | - hmac_digest = hmac.new(self.secret_key, utf8_string_to_sign, hashlib.sha256).digest() | |
107 | - return base64.encodestring(hmac_digest).strip() | |
108 | - | |
109 | - | |
110 | - | |
111 | - def _do_store_request(self, container=None, blob_name=None, http_method="GET", headers = {},data = "", | |
112 | - content_type=None, query_string = None, signed=True): | |
113 | - connection = httplib.HTTPConnection(self.server_url) | |
114 | - | |
115 | - # Construct right path based on account name , container name and blob name if any | |
116 | - path = "/" + self.account_name + "/" | |
117 | - if container!= None: | |
118 | - path = path + container +"/" | |
119 | - if blob_name != None: | |
120 | - path = path + blob_name | |
121 | - | |
122 | - if query_string != None: | |
123 | - path = path + query_string | |
124 | - | |
125 | - | |
126 | - headers[PREFIX_STORAGE_HEADER + "date"] = time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime()) #RFC 1123 | |
127 | - if content_type != None: | |
128 | - headers["content-type"] = content_type | |
129 | - | |
130 | - if signed: | |
131 | - auth_header = self._get_auth_header(http_method,path, data, headers) | |
132 | - headers["Authorization"] = "SharedKey " + self.account_name + ":" + auth_header | |
133 | - headers["content-length"] = len(data) | |
134 | - | |
135 | - connection.request(http_method, path, data, headers) | |
136 | - response = connection.getresponse() | |
137 | - if DEBUG: | |
138 | - print response.status | |
139 | - return response | |
140 | - | |
141 | - | |
142 | -def main(): | |
143 | - conn = WAStorageConnection() | |
144 | - for (container_name,etag, last_modified ) in conn.list_containers(): | |
145 | - print container_name | |
146 | - print etag | |
147 | - print last_modified | |
148 | - | |
149 | - conn.create_container("testcontainer", False) | |
150 | - conn.put_blob("testcontainer","test","Hello World!" ) | |
151 | - print conn.get_blob("testcontainer", "test") | |
152 | 198 | |
199 | + def put_blob(self, container_name, blob_name, data, content_type = None): | |
200 | + req = RequestWithMethod("PUT", "%s/%s/%s" % (self.get_base_url(), container_name, blob_name), data=data) | |
201 | + req.add_header("Content-Length", "%d" % len(data)) | |
202 | + if content_type is not None: req.add_header("Content-Type", content_type) | |
203 | + self._credentials.sign_request(req) | |
204 | + return urlopen(req) | |
205 | + | |
206 | + def get_blob(self, container_name, blob_name): | |
207 | + req = Request("%s/%s/%s" % (self.get_base_url(), container_name, blob_name)) | |
208 | + self._credentials.sign_request(req) | |
209 | + return urlopen(req).read() | |
210 | + | |
211 | +def main(): | |
212 | + pass | |
153 | 213 | |
154 | 214 | if __name__ == '__main__': |
155 | - main() | |
156 | - | |
215 | + main() | |
\ No newline at end of file |