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 """Blobstore support classes.
22 Rewriter responsible for transforming an application response to one
23 that serves a blob to the user.
25 CreateUploadDispatcher:
26 Creates a dispatcher that is added to dispatcher chain. Handles uploads
27 by storing blobs rewriting requests and returning a redirect.
38 from google
.appengine
.api
import apiproxy_stub_map
39 from google
.appengine
.api
import blobstore
40 from google
.appengine
.api
import datastore
41 from google
.appengine
.api
import datastore_errors
42 from google
.appengine
.api
.files
import file_service_stub
43 from google
.appengine
.tools
import dev_appserver_upload
47 UPLOAD_URL_PATH
= '_ah/upload/'
50 UPLOAD_URL_PATTERN
= '/%s(.*)' % UPLOAD_URL_PATH
53 AUTO_MIME_TYPE
= 'application/vnd.google.appengine.auto'
56 ERROR_RESPONSE_TEMPLATE
= """
59 <title>%(response_code)d %(response_string)s</title>
61 <body text=#000000 bgcolor=#ffffff>
62 <h1>Error: %(response_string)s</h1>
63 <h2>%(response_text)s</h2>
70 """Get blob-storage from api-proxy stub map.
73 BlobStorage instance as registered with blobstore API in stub map.
75 return apiproxy_stub_map
.apiproxy
.GetStub('blobstore').storage
78 def ParseRangeHeader(range_header
):
79 """Parse HTTP Range header.
82 range_header: A str representing the value of a range header as retrived
83 from Range or X-AppEngine-BlobRange.
87 start: Start index of blob to retrieve. May be negative index.
88 end: None or end index. End index is exclusive.
89 (None, None) if there is a parse error.
95 range_type
, ranges
= range_header
.split('=', 1)
96 if range_type
!= 'bytes':
98 ranges
= ranges
.lstrip()
102 if ranges
.startswith('-'):
107 split_range
= ranges
.split('-', 1)
108 start
= int(split_range
[0])
109 if len(split_range
) == 2 and split_range
[1].strip():
110 end
= int(split_range
[1]) + 1
118 def _GetGoogleStorageFileMetadata(blob_key
):
119 """Retreive metadata about a GS blob from the blob_key.
122 blob_key: The BlobKey of the blob.
125 Tuple (size, content_type, open_key):
126 size: The size of the blob.
127 content_type: The content type of the blob.
128 open_key: The key used as an argument to BlobStorage to open the blob
130 (None, None, None) if the blob metadata was not found.
133 gs_info
= datastore
.Get(
134 datastore
.Key
.from_path(file_service_stub
.GS_INFO_KIND
,
137 return gs_info
['size'], gs_info
['content_type'], gs_info
['storage_key']
138 except datastore_errors
.EntityNotFoundError
:
139 return None, None, None
142 def _GetBlobstoreMetadata(blob_key
):
143 """Retreive metadata about a blobstore blob from the blob_key.
146 blob_key: The BlobKey of the blob.
149 Tuple (size, content_type, open_key):
150 size: The size of the blob.
151 content_type: The content type of the blob.
152 open_key: The key used as an argument to BlobStorage to open the blob
154 (None, None, None) if the blob metadata was not found.
157 blob_info
= datastore
.Get(
158 datastore
.Key
.from_path(blobstore
.BLOB_INFO_KIND
,
161 return blob_info
['size'], blob_info
['content_type'], blob_key
162 except datastore_errors
.EntityNotFoundError
:
163 return None, None, None
166 def _GetBlobMetadata(blob_key
):
167 """Retrieve the metadata about a blob from the blob_key.
170 blob_key: The BlobKey of the blob.
173 Tuple (size, content_type, open_key):
174 size: The size of the blob.
175 content_type: The content type of the blob.
176 open_key: The key used as an argument to BlobStorage to open the blob
178 (None, None, None) if the blob metadata was not found.
180 size
, content_type
, open_key
= _GetGoogleStorageFileMetadata(blob_key
)
182 size
, content_type
, open_key
= _GetBlobstoreMetadata(blob_key
)
183 return size
, content_type
, open_key
186 def _SetRangeRequestNotSatisfiable(response
, blob_size
):
187 """Short circuit response and return 416 error.
190 response: Response object to be rewritten.
191 blob_size: The size of the blob.
193 response
.status_code
= 416
194 response
.status_message
= 'Requested Range Not Satisfiable'
195 response
.body
= cStringIO
.StringIO('')
196 response
.headers
['Content-Length'] = '0'
197 response
.headers
['Content-Range'] = '*/%d' % blob_size
198 del response
.headers
['Content-Type']
201 def DownloadRewriter(response
, request_headers
):
202 """Intercepts blob download key and rewrites response with large download.
204 Checks for the X-AppEngine-BlobKey header in the response. If found, it will
205 discard the body of the request and replace it with the blob content
208 If a valid blob is not found, it will send a 404 to the client.
210 If the application itself provides a content-type header, it will override
211 the content-type stored in the action blob.
213 If blobstore.BLOB_RANGE_HEADER header is provided, blob will be partially
214 served. If Range is present, and not blobstore.BLOB_RANGE_HEADER, will use
218 response: Response object to be rewritten.
219 request_headers: Original request headers. Looks for 'Range' header to copy
222 blob_key
= response
.headers
.getheader(blobstore
.BLOB_KEY_HEADER
)
224 del response
.headers
[blobstore
.BLOB_KEY_HEADER
]
226 blob_size
, blob_content_type
, blob_open_key
= _GetBlobMetadata(blob_key
)
228 range_header
= response
.headers
.getheader(blobstore
.BLOB_RANGE_HEADER
)
229 if range_header
is not None:
230 del response
.headers
[blobstore
.BLOB_RANGE_HEADER
]
232 range_header
= request_headers
.getheader('Range')
236 if (blob_size
is not None and blob_content_type
is not None and
237 response
.status_code
== 200):
238 content_length
= blob_size
243 start
, end
= ParseRangeHeader(range_header
)
245 _SetRangeRequestNotSatisfiable(response
, blob_size
)
249 start
= max(blob_size
+ start
, 0)
250 elif start
>= blob_size
:
251 _SetRangeRequestNotSatisfiable(response
, blob_size
)
254 end
= min(end
, blob_size
)
257 content_length
= min(end
, blob_size
) - start
258 end
= start
+ content_length
259 response
.status_code
= 206
260 response
.status_message
= 'Partial Content'
261 response
.headers
['Content-Range'] = 'bytes %d-%d/%d' % (
262 start
, end
- 1, blob_size
)
264 blob_stream
= GetBlobStorage().OpenBlob(blob_open_key
)
265 blob_stream
.seek(start
)
266 response
.body
= cStringIO
.StringIO(blob_stream
.read(content_length
))
267 response
.headers
['Content-Length'] = str(content_length
)
269 content_type
= response
.headers
.getheader('Content-Type')
270 if not content_type
or content_type
== AUTO_MIME_TYPE
:
271 response
.headers
['Content-Type'] = blob_content_type
272 response
.large_response
= True
276 if response
.status_code
!= 200:
277 logging
.error('Blob-serving response with status %d, expected 200.',
278 response
.status_code
)
280 logging
.error('Could not find blob with key %s.', blob_key
)
282 response
.status_code
= 500
283 response
.status_message
= 'Internal Error'
284 response
.body
= cStringIO
.StringIO()
286 if response
.headers
.getheader('content-type'):
287 del response
.headers
['content-type']
288 response
.headers
['Content-Length'] = '0'
291 def CreateUploadDispatcher(get_blob_storage
=GetBlobStorage
):
292 """Function to create upload dispatcher.
295 New dispatcher capable of handling large blob uploads.
299 from google
.appengine
.tools
import old_dev_appserver
301 class UploadDispatcher(old_dev_appserver
.URLDispatcher
):
302 """Dispatcher that handles uploads."""
308 blob_storage: A BlobStorage instance.
310 self
.__cgi
_handler
= dev_appserver_upload
.UploadCGIHandler(
319 """Handle post dispatch.
321 This dispatcher will handle all uploaded files in the POST request, store
322 the results in the blob-storage, close the upload session and transform
323 the original request in to one where the uploaded files have external
327 New AppServerRequest indicating request forward to upload success
331 if base_env_dict
['REQUEST_METHOD'] != 'POST':
332 outfile
.write('Status: 400\n\n')
336 upload_key
= re
.match(UPLOAD_URL_PATTERN
, request
.relative_url
).group(1)
338 upload_session
= datastore
.Get(upload_key
)
339 except datastore_errors
.EntityNotFoundError
:
340 upload_session
= None
343 success_path
= upload_session
['success_path']
344 max_bytes_per_blob
= upload_session
['max_bytes_per_blob']
345 max_bytes_total
= upload_session
['max_bytes_total']
346 bucket_name
= upload_session
.get('gs_bucket_name', None)
348 upload_form
= cgi
.FieldStorage(fp
=request
.infile
,
349 headers
=request
.headers
,
350 environ
=base_env_dict
)
355 mime_message_string
= self
.__cgi
_handler
.GenerateMIMEMessageString(
357 max_bytes_per_blob
=max_bytes_per_blob
,
358 max_bytes_total
=max_bytes_total
,
359 bucket_name
=bucket_name
)
361 datastore
.Delete(upload_session
)
362 self
.current_session
= upload_session
365 header_end
= mime_message_string
.find('\n\n') + 1
366 content_start
= header_end
+ 1
367 header_text
= mime_message_string
[:header_end
].replace('\n', '\r\n')
368 content_text
= mime_message_string
[content_start
:].replace('\n',
372 complete_headers
= ('%s'
373 'Content-Length: %d\r\n'
374 '\r\n') % (header_text
, len(content_text
))
376 return old_dev_appserver
.AppServerRequest(
379 mimetools
.Message(cStringIO
.StringIO(complete_headers
)),
380 cStringIO
.StringIO(content_text
),
382 except dev_appserver_upload
.InvalidMIMETypeFormatError
:
383 outfile
.write('Status: 400\n\n')
384 except dev_appserver_upload
.UploadEntityTooLargeError
:
385 outfile
.write('Status: 413\n\n')
386 response
= ERROR_RESPONSE_TEMPLATE
% {
387 'response_code': 413,
388 'response_string': 'Request Entity Too Large',
389 'response_text': 'Your client issued a request that was too '
391 outfile
.write(response
)
392 except dev_appserver_upload
.FilenameOrContentTypeTooLargeError
, ex
:
393 outfile
.write('Status: 400\n\n')
394 response
= ERROR_RESPONSE_TEMPLATE
% {
395 'response_code': 400,
396 'response_string': 'Bad Request',
397 'response_text': str(ex
)}
398 outfile
.write(response
)
400 logging
.error('Could not find session for %s', upload_key
)
401 outfile
.write('Status: 404\n\n')
404 def EndRedirect(self
, dispatched_output
, original_output
):
405 """Handle the end of upload complete notification.
407 Makes sure the application upload handler returned an appropriate status
410 response
= old_dev_appserver
.RewriteResponse(dispatched_output
)
411 logging
.info('Upload handler returned %d', response
.status_code
)
412 outfile
= cStringIO
.StringIO()
413 outfile
.write('Status: %s\n' % response
.status_code
)
415 if response
.body
and len(response
.body
.read()) > 0:
416 response
.body
.seek(0)
417 outfile
.write(response
.body
.read())
419 outfile
.write(''.join(response
.headers
.headers
))
422 old_dev_appserver
.URLDispatcher
.EndRedirect(self
,
426 return UploadDispatcher()