blob: 9a3ab55659990e4cff851287a142ac1ef3b9d8ef [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.
#
"""Datastore backed Blobstore API stub.
Class:
BlobstoreServiceStub: BlobstoreService stub backed by datastore.
"""
import base64
import os
import time
import urlparse
from google.appengine.api import apiproxy_stub
from google.appengine.api import blobstore
from google.appengine.api import datastore
from google.appengine.api import datastore_errors
from google.appengine.api import datastore_types
from google.appengine.api import users
from google.appengine.api.blobstore import blobstore_service_pb
from google.appengine.runtime import apiproxy_errors
__all__ = ['BlobStorage',
'BlobstoreServiceStub',
'ConfigurationError',
'CreateUploadSession',
'Error',
]
class Error(Exception):
"""Base blobstore error type."""
class ConfigurationError(Error):
"""Raised when environment is not correctly configured."""
_UPLOAD_SESSION_KIND = '__BlobUploadSession__'
_GS_INFO_KIND = '__GsFileInfo__'
def CreateUploadSession(creation,
success_path,
user,
max_bytes_per_blob,
max_bytes_total,
bucket_name=None):
"""Create upload session in datastore.
Creates an upload session and puts it in Datastore to be referenced by
upload handler later.
Args:
creation: Creation timestamp.
success_path: Path in users application to call upon success.
user: User that initiated this upload, if any.
max_bytes_per_blob: Maximum number of bytes for any blob in the upload.
max_bytes_total: Maximum aggregate bytes for all blobs in the upload.
bucket_name: Name of the Google Storage bucket tio upload the files.
Returns:
String encoded key of new Datastore entity.
"""
entity = datastore.Entity(_UPLOAD_SESSION_KIND, namespace='')
entity_dict = {'creation': creation,
'success_path': success_path,
'user': user,
'state': 'init',
'max_bytes_per_blob': max_bytes_per_blob,
'max_bytes_total': max_bytes_total}
if bucket_name:
entity_dict['gs_bucket_name'] = bucket_name
entity.update(entity_dict)
datastore.Put(entity)
return str(entity.key())
class BlobStorage(object):
"""Base class for defining how blobs are stored.
This base class merely defines an interface that all stub blob-storage
mechanisms must implement.
"""
def StoreBlob(self, blob_key, blob_stream):
"""Store blob stream.
Implement this method to persist blob data.
Args:
blob_key: Blob key of blob to store.
blob_stream: Stream or stream-like object that will generate blob content.
"""
raise NotImplementedError('Storage class must override StoreBlob method.')
def OpenBlob(self, blob_key):
"""Open blob for streaming.
Args:
blob_key: Blob-key of existing blob to open for reading.
Returns:
Open file stream for reading blob. Caller is responsible for closing
file.
"""
raise NotImplementedError('Storage class must override OpenBlob method.')
def DeleteBlob(self, blob_key):
"""Delete blob data from storage.
Args:
blob_key: Blob-key of existing blob to delete.
"""
raise NotImplementedError('Storage class must override DeleteBlob method.')
class BlobstoreServiceStub(apiproxy_stub.APIProxyStub):
"""Datastore backed Blobstore service stub.
This stub stores manages upload sessions in the Datastore and must be
provided with a blob_storage object to know where the actual blob
records can be found after having been uploaded.
This stub does not handle the actual creation of blobs, neither the BlobInfo
in the Datastore nor creation of blob data in the blob_storage. It does,
however, assume that another part of the system has created these and
uses these objects for deletion.
An upload session is created when the CreateUploadURL request is handled and
put in the Datastore under the __BlobUploadSession__ kind. There is no
analog for this kind on a production server. Other than creation, this stub
not work with session objects. The URLs created by this service stub are:
http://<appserver-host>:<appserver-port>/<uploader-path>/<session-info>
This is very similar to what the URL is on a production server. The session
info is the string encoded version of the session entity
"""
_ACCEPTS_REQUEST_ID = True
GS_BLOBKEY_PREFIX = 'encoded_gs_file:'
def __init__(self,
blob_storage,
time_function=time.time,
service_name='blobstore',
uploader_path='_ah/upload/',
request_data=None):
"""Constructor.
Args:
blob_storage: BlobStorage class instance used for blob storage.
time_function: Used for dependency injection in tests.
service_name: Service name expected for all calls.
uploader_path: Path to upload handler pointed to by URLs generated
by this service stub.
request_data: A apiproxy_stub.RequestData instance used to look up state
associated with the request that generated an API call.
"""
super(BlobstoreServiceStub, self).__init__(service_name,
request_data=request_data)
self.__storage = blob_storage
self.__time_function = time_function
self.__next_session_id = 1
self.__uploader_path = uploader_path
@classmethod
def ToDatastoreBlobKey(cls, blobkey):
"""Given a string blobkey, return its db.Key."""
kind = blobstore.BLOB_INFO_KIND
if blobkey.startswith(cls.GS_BLOBKEY_PREFIX):
kind = _GS_INFO_KIND
return datastore_types.Key.from_path(kind,
blobkey,
namespace='')
@property
def storage(self):
"""Access BlobStorage used by service stub.
Returns:
BlobStorage instance used by blobstore service stub.
"""
return self.__storage
def _GetEnviron(self, name):
"""Helper method ensures environment configured as expected.
Args:
name: Name of environment variable to get.
Returns:
Environment variable associated with name.
Raises:
ConfigurationError if required environment variable is not found.
"""
try:
return os.environ[name]
except KeyError:
raise ConfigurationError('%s is not set in environment.' % name)
def _CreateSession(self,
success_path,
user,
max_bytes_per_blob=None,
max_bytes_total=None,
bucket_name=None):
"""Create new upload session.
Args:
success_path: Application path to call upon successful POST.
user: User that initiated the upload session.
max_bytes_per_blob: Maximum number of bytes for any blob in the upload.
max_bytes_total: Maximum aggregate bytes for all blobs in the upload.
bucket_name: The name of the Cloud Storage bucket where the files will be
uploaded.
Returns:
String encoded key of a new upload session created in the datastore.
"""
return CreateUploadSession(self.__time_function(),
success_path,
user,
max_bytes_per_blob,
max_bytes_total,
bucket_name)
def _Dynamic_CreateUploadURL(self, request, response, request_id):
"""Create upload URL implementation.
Create a new upload session. The upload session key is encoded in the
resulting POST URL. This URL is embedded in a POST form by the application
which contacts the uploader when the user posts.
Args:
request: A fully initialized CreateUploadURLRequest instance.
response: A CreateUploadURLResponse instance.
request_id: A unique string identifying the request associated with the
API call.
"""
max_bytes_per_blob = None
max_bytes_total = None
bucket_name = None
if request.has_max_upload_size_per_blob_bytes():
max_bytes_per_blob = request.max_upload_size_per_blob_bytes()
if request.has_max_upload_size_bytes():
max_bytes_total = request.max_upload_size_bytes()
if request.has_gs_bucket_name():
bucket_name = request.gs_bucket_name()
session = self._CreateSession(request.success_path(),
users.get_current_user(),
max_bytes_per_blob,
max_bytes_total,
bucket_name)
protocol, host, _, _, _, _ = urlparse.urlparse(
self.request_data.get_request_url(request_id))
response.set_url('%s://%s/%s%s' % (protocol, host, self.__uploader_path,
session))
@classmethod
def DeleteBlob(cls, blobkey, storage):
"""Delete a blob.
Args:
blobkey: blobkey in str.
storage: blobstore storage stub.
"""
datastore.Delete(cls.ToDatastoreBlobKey(blobkey))
blobinfo = datastore_types.Key.from_path(blobstore.BLOB_INFO_KIND,
blobkey,
namespace='')
datastore.Delete(blobinfo)
storage.DeleteBlob(blobkey)
def _Dynamic_DeleteBlob(self, request, response, unused_request_id):
"""Delete a blob by its blob-key.
Delete a blob from the blobstore using its blob-key. Deleting blobs that
do not exist is a no-op.
Args:
request: A fully initialized DeleteBlobRequest instance.
response: Not used but should be a VoidProto.
"""
for blobkey in request.blob_key_list():
self.DeleteBlob(blobkey, self.__storage)
def _Dynamic_FetchData(self, request, response, unused_request_id):
"""Fetch a blob fragment from a blob by its blob-key.
Fetches a blob fragment using its blob-key. Start index is inclusive,
end index is inclusive. Valid requests for information outside of
the range of the blob return a partial string or empty string if entirely
out of range.
Args:
request: A fully initialized FetchDataRequest instance.
response: A FetchDataResponse instance.
Raises:
ApplicationError when application has the following errors:
INDEX_OUT_OF_RANGE: Index is negative or end > start.
BLOB_FETCH_SIZE_TOO_LARGE: Request blob fragment is larger than
MAX_BLOB_FRAGMENT_SIZE.
BLOB_NOT_FOUND: If invalid blob-key is provided or is not found.
"""
start_index = request.start_index()
if start_index < 0:
raise apiproxy_errors.ApplicationError(
blobstore_service_pb.BlobstoreServiceError.DATA_INDEX_OUT_OF_RANGE)
end_index = request.end_index()
if end_index < start_index:
raise apiproxy_errors.ApplicationError(
blobstore_service_pb.BlobstoreServiceError.DATA_INDEX_OUT_OF_RANGE)
fetch_size = end_index - start_index + 1
if fetch_size > blobstore.MAX_BLOB_FETCH_SIZE:
raise apiproxy_errors.ApplicationError(
blobstore_service_pb.BlobstoreServiceError.BLOB_FETCH_SIZE_TOO_LARGE)
blobkey = request.blob_key()
info_key = self.ToDatastoreBlobKey(blobkey)
try:
datastore.Get(info_key)
except datastore_errors.EntityNotFoundError:
raise apiproxy_errors.ApplicationError(
blobstore_service_pb.BlobstoreServiceError.BLOB_NOT_FOUND)
blob_file = self.__storage.OpenBlob(blobkey)
blob_file.seek(start_index)
response.set_data(blob_file.read(fetch_size))
def _Dynamic_DecodeBlobKey(self, request, response, unused_request_id):
"""Decode a given blob key: data is simply base64-decoded.
Args:
request: A fully-initialized DecodeBlobKeyRequest instance
response: A DecodeBlobKeyResponse instance.
"""
for blob_key in request.blob_key_list():
response.add_decoded(blob_key.decode('base64'))
@classmethod
def CreateEncodedGoogleStorageKey(cls, filename):
"""Create an encoded blob key that represents a Google Storage file.
For now we'll just base64 encode the Google Storage filename, APIs that
accept encoded blob keys will need to be able to support Google Storage
files or blobstore files based on decoding this key.
Any stub that creates GS files should use this function to convert
a gs filename to a blobkey. The created blobkey should be used both
as its _GS_FILE_INFO entity's key name and as the storage key to
store its content in blobstore. This ensures the GS files created
can be operated by other APIs.
Note this encoding is easily reversible and is not encryption.
Args:
filename: gs filename of form 'bucket/filename'
Returns:
blobkey string of encoded filename.
"""
return cls.GS_BLOBKEY_PREFIX + base64.urlsafe_b64encode(filename)
def _Dynamic_CreateEncodedGoogleStorageKey(self, request, response,
unused_request_id):
"""Create an encoded blob key that represents a Google Storage file.
For now we'll just base64 encode the Google Storage filename, APIs that
accept encoded blob keys will need to be able to support Google Storage
files or blobstore files based on decoding this key.
Args:
request: A fully-initialized CreateEncodedGoogleStorageKeyRequest
instance.
response: A CreateEncodedGoogleStorageKeyResponse instance.
"""
filename = request.filename()[len(blobstore.GS_PREFIX):]
response.set_blob_key(
self.CreateEncodedGoogleStorageKey(filename))
def CreateBlob(self, blob_key, content):
"""Create new blob and put in storage and Datastore.
This is useful in testing where you have access to the stub.
Args:
blob_key: String blob-key of new blob.
content: Content of new blob as a string.
Returns:
New Datastore entity without blob meta-data fields.
"""
entity = datastore.Entity(blobstore.BLOB_INFO_KIND,
name=blob_key, namespace='')
entity['size'] = len(content)
datastore.Put(entity)
self.storage.CreateBlob(blob_key, content)
return entity