blob: 32c726988dc840819e35ccd46aae1c7f00893b2a [file] [log] [blame]
#!/usr/bin/env python
#
# Copyright 2007 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""Rewrites blob download headers in the response with full blob contents."""
import logging
from google.appengine.api import apiproxy_stub_map
from google.appengine.api import datastore
from google.appengine.api import datastore_errors
from google.appengine.api.blobstore import blobstore_stub
from google.appengine.ext import blobstore
# The MIME type from apps to tell Blobstore to select the mime type.
_AUTO_MIME_TYPE = 'application/vnd.google.appengine.auto'
def _get_blob_storage():
"""Gets the BlobStorage instance from the API proxy stub map.
Returns:
The BlobStorage instance as registered with blobstore API in stub map.
"""
return apiproxy_stub_map.apiproxy.GetStub('blobstore').storage
def _parse_range_header(range_header):
"""Parse HTTP Range header.
Args:
range_header: A str representing the value of a range header as retrived
from Range or X-AppEngine-BlobRange.
Returns:
Tuple (start, end):
start: Start index of blob to retrieve. May be negative index.
end: None or end index. End index is exclusive.
(None, None) if there is a parse error.
"""
if not range_header:
return None, None
try:
# ValueError if <1 split.
range_type, ranges = range_header.split('=', 1)
if range_type != 'bytes':
return None, None
ranges = ranges.lstrip()
if ',' in ranges:
return None, None
end = None
if ranges.startswith('-'):
start = int(ranges)
if start == 0:
return None, None
else:
split_range = ranges.split('-', 1)
start = int(split_range[0])
if len(split_range) == 2 and split_range[1].strip():
end = int(split_range[1]) + 1
if start > end:
return None, None
return start, end
except ValueError:
return None, None
def _get_blob_metadata(blob_key):
"""Retrieve the metadata about a blob from the blob_key.
Args:
blob_key: The BlobKey of the blob.
Returns:
Tuple (size, content_type, open_key):
size: The size of the blob.
content_type: The content type of the blob.
open_key: The key used as an argument to BlobStorage to open the blob
for reading. Same as blob_key
(None, None, None) if the blob metadata was not found.
"""
key = blobstore_stub.BlobstoreServiceStub.ToDatastoreBlobKey(blob_key)
try:
info = datastore.Get(key)
return info['size'], info['content_type'], blob_key
except datastore_errors.EntityNotFoundError:
return None, None, None
def blobstore_download_rewriter(state):
"""Rewrite a response with blobstore download bodies.
Checks for the X-AppEngine-BlobKey header in the response. If found, it will
discard the body of the request and replace it with the blob content
indicated.
If a valid blob is not found, it will send a 404 to the client.
If the application itself provides a content-type header, it will override
the content-type stored in the action blob.
If blobstore.BLOB_RANGE_HEADER header is provided, blob will be partially
served. If Range is present, and not blobstore.BLOB_RANGE_HEADER, will use
Range instead.
Args:
state: A request_rewriter.RewriterState to modify.
"""
blob_key = state.headers.get(blobstore.BLOB_KEY_HEADER)
if not blob_key:
# Pass the download through unchanged.
return
def set_range_request_not_satisfiable(blob_size):
"""Short circuit response and return 416 error.
Changes state into the error response.
Args:
blob_size: The size of the blob.
"""
state.status = '416 Requested Range Not Satisfiable'
state.headers['Content-Length'] = '0'
state.headers['Content-Range'] = '*/%d' % blob_size
del state.headers['Content-Type']
# Rewrite the response to include the blob contents.
state.body = []
del state.headers[blobstore.BLOB_KEY_HEADER]
blob_size, blob_content_type, blob_open_key = _get_blob_metadata(blob_key)
if isinstance(blob_content_type, unicode):
blob_content_type = blob_content_type.encode('ascii')
range_header = state.headers.get(blobstore.BLOB_RANGE_HEADER)
if range_header is not None:
del state.headers[blobstore.BLOB_RANGE_HEADER]
else:
range_header = state.environ.get('HTTP_RANGE')
status_code = state.status_code
# If we found a blob, serve it.
# It is an error if the response code returned by the user is not 200.
if (blob_size is not None and blob_content_type is not None and
status_code == 200):
content_length = blob_size
start = 0
end = content_length
if range_header:
start, end = _parse_range_header(range_header)
if start is None:
set_range_request_not_satisfiable(blob_size)
return
else:
if start < 0:
start = max(blob_size + start, 0) # Start is negative.
elif start >= blob_size:
set_range_request_not_satisfiable(blob_size)
return
if end is not None:
end = min(end, blob_size)
else:
end = blob_size
content_length = min(end, blob_size) - start
end = start + content_length
state.status = '206 Partial Content'
state.headers['Content-Range'] = 'bytes %d-%d/%d' % (start, end - 1,
blob_size)
blob_stream = _get_blob_storage().OpenBlob(blob_open_key)
blob_stream.seek(start)
state.body = [blob_stream.read(content_length)]
state.headers['Content-Length'] = str(content_length)
content_type = state.headers.get('Content-Type')
if not content_type or content_type == _AUTO_MIME_TYPE:
state.headers['Content-Type'] = blob_content_type
# Allow responses beyond the maximum dynamic response size
state.allow_large_response = True
else:
# Missing blobs should have been handled by application.
if status_code != 200:
logging.error('Blob-serving response with status %d, expected 200.',
status_code)
else:
logging.error('Could not find blob with key %s.', blob_key)
state.status = '500 Internal Server Error'
state.headers['Content-Length'] = '0'
del state.headers['Content-Type']