#!/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. # """Blobstore support classes. Classes: DownloadRewriter: Rewriter responsible for transforming an application response to one that serves a blob to the user. CreateUploadDispatcher: Creates a dispatcher that is added to dispatcher chain. Handles uploads by storing blobs rewriting requests and returning a redirect. """ import cgi import cStringIO import logging import mimetools import re from google.appengine.api import apiproxy_stub_map from google.appengine.api import blobstore from google.appengine.api import datastore from google.appengine.api import datastore_errors from google.appengine.tools import dev_appserver_upload UPLOAD_URL_PATH = '_ah/upload/' UPLOAD_URL_PATTERN = '/%s(.*)' % UPLOAD_URL_PATH def GetBlobStorage(): """Get blob-storage from api-proxy stub map. Returns: BlobStorage instance as registered with blobstore API in stub map. """ return apiproxy_stub_map.apiproxy.GetStub('blobstore').storage def DownloadRewriter(response): """Intercepts blob download key and rewrites response with large download. 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. Args: response: Response object to be rewritten. """ blob_key = response.headers.getheader(blobstore.BLOB_KEY_HEADER) if blob_key: del response.headers[blobstore.BLOB_KEY_HEADER] try: blob_info = datastore.Get( datastore.Key.from_path(blobstore.BLOB_INFO_KIND, blob_key)) response.body = GetBlobStorage().OpenBlob(blob_key) response.headers['Content-Length'] = str(blob_info['size']) if not response.headers.getheader('Content-Type'): response.headers['Content-Type'] = blob_info['content_type'] response.large_response = True except datastore_errors.EntityNotFoundError: response.status_code = 500 response.status_message = 'Internal Error' response.body = cStringIO.StringIO() if response.headers.getheader('status'): del response.headers['status'] if response.headers.getheader('location'): del response.headers['location'] if response.headers.getheader('content-type'): del response.headers['content-type'] logging.error('Could not find blob with key %s.', blob_key) def CreateUploadDispatcher(get_blob_storage=GetBlobStorage): """Function to create upload dispatcher. Returns: New dispatcher capable of handling large blob uploads. """ from google.appengine.tools import dev_appserver class UploadDispatcher(dev_appserver.URLDispatcher): """Dispatcher that handles uploads.""" def __init__(self): """Constructor. Args: blob_storage: A BlobStorage instance. """ self.__cgi_handler = dev_appserver_upload.UploadCGIHandler( get_blob_storage()) def Dispatch(self, request, outfile, base_env_dict=None): """Handle post dispatch. This dispatcher will handle all uploaded files in the POST request, store the results in the blob-storage, close the upload session and transform the original request in to one where the uploaded files have external bodies. Returns: New AppServerRequest indicating request forward to upload success handler. """ if base_env_dict['REQUEST_METHOD'] != 'POST': outfile.write('Status: 400\n\n') return upload_key = re.match(UPLOAD_URL_PATTERN, request.relative_url).group(1) try: upload_session = datastore.Get(upload_key) except datastore_errors.EntityNotFoundError: upload_session = None if upload_session: success_path = upload_session['success_path'] upload_form = cgi.FieldStorage(fp=request.infile, headers=request.headers, environ=base_env_dict) try: mime_message_string = self.__cgi_handler.GenerateMIMEMessageString( upload_form) datastore.Delete(upload_session) self.current_session = upload_session header_end = mime_message_string.find('\n\n') + 1 content_start = header_end + 1 header_text = mime_message_string[:header_end] content_text = mime_message_string[content_start:] complete_headers = ('%s' 'Content-Length: %d\n' '\n') % (header_text, len(content_text)) return dev_appserver.AppServerRequest( success_path, None, mimetools.Message(cStringIO.StringIO(complete_headers)), cStringIO.StringIO(content_text), force_admin=True) except dev_appserver_upload.InvalidMIMETypeFormatError: outfile.write('Status: 400\n\n') else: logging.error('Could not find session for %s', upload_key) outfile.write('Status: 404\n\n') def EndRedirect(self, redirected_outfile, original_outfile): """Handle the end of upload complete notification. Makes sure the application upload handler returned an appropriate status code. """ response = dev_appserver.RewriteResponse(redirected_outfile) logging.info('Upload handler returned %d', response.status_code) if (response.status_code in (301, 302, 303) and (not response.body or len(response.body.read()) == 0)): contentless_outfile = cStringIO.StringIO() contentless_outfile.write('Status: %s\n' % response.status_code) contentless_outfile.write(''.join(response.headers.headers)) contentless_outfile.seek(0) dev_appserver.URLDispatcher.EndRedirect(self, contentless_outfile, original_outfile) else: logging.error( 'Invalid upload handler response. Only 301, 302 and 303 ' 'statuses are permitted and it may not have a content body.') original_outfile.write('Status: 500\n\n') return UploadDispatcher()