3 # Copyright 2007 Google Inc.
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
9 # http://www.apache.org/licenses/LICENSE-2.0
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
17 """Dispatcher to handle Google Cloud Storage stub requests."""
31 import xml
.etree
.ElementTree
as ET
33 from google
.appengine
.api
import apiproxy_stub_map
34 from google
.appengine
.api
import urlfetch_stub
35 from google
.appengine
.ext
.cloudstorage
import cloudstorage_stub
36 from google
.appengine
.ext
.cloudstorage
import common
40 _MAX_GET_BUCKET_RESULT
= 1000
44 def _urlfetch_to_gcs_stub(url
, payload
, method
, headers
, request
, response
,
45 follow_redirects
=False, deadline
=None,
46 validate_certificate
=None):
48 """Forwards gcs urlfetch requests to gcs_dispatcher.
50 See apphosting.api.urlfetch_stub.URLFetchServiceStub._RetrieveURL.
53 (header
.key().lower(), header
.value()) for header
in headers
)
54 result
= dispatch(method
, headers_map
, url
, payload
)
55 response
.set_statuscode(result
.status_code
)
56 response
.set_content(result
.content
[:urlfetch_stub
.MAX_RESPONSE_SIZE
])
57 for k
, v
in result
.headers
.iteritems():
58 if k
.lower() == 'content-length' and method
!= 'HEAD':
59 v
= len(response
.content())
60 header_proto
= response
.add_header()
61 header_proto
.set_key(k
)
62 header_proto
.set_value(str(v
))
63 if len(result
.content
) > urlfetch_stub
.MAX_RESPONSE_SIZE
:
64 response
.set_contentwastruncated(True)
67 def _urlmatcher_for_gcs_stub(url
):
68 """Determines whether a url should be handled by gcs stub."""
69 _
, host
, _
, _
, _
= urlparse
.urlsplit(url
)
70 return host
== common
.LOCAL_API_HOST
74 URLMATCHERS_TO_FETCH_FUNCTIONS
= [
75 (_urlmatcher_for_gcs_stub
, _urlfetch_to_gcs_stub
)]
78 class _FakeUrlFetchResult(object):
79 def __init__(self
, status
, headers
, content
):
80 self
.status_code
= status
81 self
.headers
= headers
82 self
.content
= content
85 def dispatch(method
, headers
, url
, payload
):
86 """Dispatches incoming request and returns response.
88 In dev appserver or unittest environment, this method is called instead of
92 method: urlfetch method.
93 headers: urlfetch headers.
95 payload: urlfecth payload.
98 A _FakeUrlFetchResult.
101 ValueError: invalid request method.
103 method
, headers
, filename
, param_dict
= _preprocess(method
, headers
, url
)
104 gcs_stub
= cloudstorage_stub
.CloudStorageStub(
105 apiproxy_stub_map
.apiproxy
.GetStub('blobstore').storage
)
108 return _handle_post(gcs_stub
, filename
, headers
)
109 elif method
== 'PUT':
110 return _handle_put(gcs_stub
, filename
, param_dict
, headers
, payload
)
111 elif method
== 'GET':
112 return _handle_get(gcs_stub
, filename
, param_dict
, headers
)
113 elif method
== 'HEAD':
114 return _handle_head(gcs_stub
, filename
)
115 elif method
== 'DELETE':
116 return _handle_delete(gcs_stub
, filename
)
117 raise ValueError('Unrecognized request method %r.' % method
)
120 def _preprocess(method
, headers
, url
):
124 _preprocess('POST', {'Content-Type': 'Foo'}, http://gcs.com/b/f?foo=bar)
125 -> 'POST', {'content-type': 'Foo'}, '/b/f', {'foo':'bar'}
128 method: HTTP method used by the request.
129 headers: HTTP request headers in a dict.
130 url: HTTP request url.
133 method: method in all upper case.
134 headers: headers with keys in all lower case.
135 filename: a google storage filename of form /bucket/filename or
136 a bucket path of form /bucket
137 param_dict: a dict of query parameters.
139 _
, _
, filename
, query
, _
= urlparse
.urlsplit(url
)
140 param_dict
= urlparse
.parse_qs(query
, True)
142 param_dict
[k
] = urllib
.unquote(param_dict
[k
][0])
144 headers
= dict((k
.lower(), v
) for k
, v
in headers
.iteritems())
145 return method
, headers
, filename
, param_dict
148 def _handle_post(gcs_stub
, filename
, headers
):
149 """Handle POST that starts object creation."""
150 content_type
= _ContentType(headers
)
151 token
= gcs_stub
.post_start_creation(filename
, headers
)
153 'location': 'https://storage.googleapis.com/%s?%s' % (
155 urllib
.urlencode({'upload_id': token
})),
156 'content-type': content_type
.value
,
159 return _FakeUrlFetchResult(201, response_headers
, '')
162 def _handle_put(gcs_stub
, filename
, param_dict
, headers
, payload
):
163 """Handle PUT that continues object creation."""
164 token
= _get_param('upload_id', param_dict
)
165 content_range
= _ContentRange(headers
)
167 if not content_range
.value
:
168 raise ValueError('Missing header content-range.')
170 gcs_stub
.put_continue_creation(token
,
174 if content_range
.last
:
175 filestat
= gcs_stub
.head_object(filename
)
177 'content-length': filestat
.st_size
,
179 response_status
= httplib
.OK
181 response_headers
= {}
182 response_status
= 308
184 return _FakeUrlFetchResult(response_status
, response_headers
, '')
187 def _handle_get(gcs_stub
, filename
, param_dict
, headers
):
188 """Handle GET object and GET bucket."""
189 if filename
.rfind('/') == 0:
191 return _handle_get_bucket(gcs_stub
, filename
, param_dict
)
194 result
= _handle_head(gcs_stub
, filename
)
195 if result
.status_code
== 404:
197 start
, end
= _Range(headers
).value
198 st_size
= result
.headers
['content-length']
201 result
.headers
['content-range'] = 'bytes: %d-%d/%d' % (start
,
204 result
.content
= gcs_stub
.get_object(filename
, start
, end
)
208 def _handle_get_bucket(gcs_stub
, bucketpath
, param_dict
):
209 """Handle get bucket request."""
210 prefix
= _get_param('prefix', param_dict
, '')
211 max_keys
= _get_param('max-keys', param_dict
, _MAX_GET_BUCKET_RESULT
)
212 marker
= _get_param('marker', param_dict
, '')
214 stats
= gcs_stub
.get_bucket(bucketpath
,
219 builder
= ET
.TreeBuilder()
220 builder
.start('ListBucketResult', {'xmlns': common
.CS_XML_NS
})
221 last_object_name
= ''
223 builder
.start('Contents', {})
225 builder
.start('Key', {})
226 last_object_name
= stat
.filename
[len(bucketpath
) + 1:]
227 builder
.data(last_object_name
)
230 builder
.start('LastModified', {})
231 builder
.data(common
.posix_to_dt_str(stat
.st_ctime
))
232 builder
.end('LastModified')
234 builder
.start('ETag', {})
235 builder
.data(stat
.etag
)
238 builder
.start('Size', {})
239 builder
.data(str(stat
.st_size
))
242 builder
.end('Contents')
245 builder
.start('NextMarker', {})
246 builder
.data(last_object_name
)
247 builder
.end('NextMarker')
249 max_keys
= _get_param('max-keys', param_dict
)
250 if max_keys
is not None:
251 builder
.start('MaxKeys', {})
252 builder
.data(str(max_keys
))
253 builder
.end('MaxKeys')
255 builder
.end('ListBucketResult')
256 root
= builder
.close()
258 body
= ET
.tostring(root
)
259 response_headers
= {'content-length': len(body
),
260 'content-type': 'application/xml'}
261 return _FakeUrlFetchResult(200, response_headers
, body
)
264 def _handle_head(gcs_stub
, filename
):
265 """Handle HEAD request."""
266 filestat
= gcs_stub
.head_object(filename
)
268 return _FakeUrlFetchResult(404, {}, '')
270 http_time
= common
.posix_time_to_http(filestat
.st_ctime
)
273 'content-length': filestat
.st_size
,
274 'content-type': filestat
.content_type
,
275 'etag': filestat
.etag
,
276 'last-modified': http_time
279 if filestat
.metadata
:
280 response_headers
.update(filestat
.metadata
)
282 return _FakeUrlFetchResult(200, response_headers
, '')
285 def _handle_delete(gcs_stub
, filename
):
286 """Handle DELETE object."""
287 if gcs_stub
.delete_object(filename
):
288 return _FakeUrlFetchResult(204, {}, '')
290 return _FakeUrlFetchResult(404, {}, '')
293 class _Header(object):
294 """Wrapper class for a header.
296 A subclass helps to parse a specific header.
302 def __init__(self
, headers
):
305 Initializes self.value to the value in request header, or DEFAULT if
306 not defined in headers.
309 headers: request headers.
311 self
.value
= self
.DEFAULT
313 if k
.lower() == self
.HEADER
.lower():
314 self
.value
= headers
[k
]
318 class _ContentType(_Header
):
319 """Content-type header."""
321 HEADER
= 'Content-Type'
322 DEFAULT
= 'binary/octet-stream'
325 class _ContentRange(_Header
):
326 """Content-Range header.
328 Used by resumable upload of unknown size. Possible formats:
329 Content-Range: bytes 1-3/* (for uploading of unknown size)
330 Content-Range: bytes */5 (for finalizing with no data)
333 HEADER
= 'Content-Range'
334 RE_PATTERN
= re
.compile(r
'^bytes (([0-9]+)-([0-9]+)|\*)/([0-9]+|\*)$')
336 def __init__(self
, headers
):
337 super(_ContentRange
, self
).__init
__(headers
)
339 result
= self
.RE_PATTERN
.match(self
.value
)
341 raise ValueError('Invalid content-range header %s' % self
.value
)
343 self
.no_data
= result
.group(1) == '*'
344 self
.last
= result
.group(4) != '*'
345 if self
.no_data
and not self
.last
:
346 raise ValueError('Invalid content-range header %s' % self
.value
)
350 self
.range = (long(result
.group(2)), long(result
.group(3)))
353 class _Range(_Header
):
356 Used by read. Format: Range: bytes=1-3.
361 def __init__(self
, headers
):
362 super(_Range
, self
).__init
__(headers
)
364 start
, end
= self
.value
.rsplit('=', 1)[-1].split('-')
365 start
, end
= long(start
), long(end
)
368 self
.value
= start
, end
371 def _get_param(param
, param_dict
, default
=None):
372 """Gets a parameter value from request query parameters.
375 param: name of the parameter to get.
376 param_dict: a dict of request query parameters.
377 default: default value if not defined.
380 Value of the parameter or default if not defined.
382 result
= param_dict
.get(param
, default
)
383 if param
in ['max-keys'] and result
: