Add google appengine to repo
[frozenviper.git] / google_appengine / google / appengine / api / blobstore / blobstore_stub.py
blob032757cc8f44d665c585e89bdc166612adec02ee
1 #!/usr/bin/env python
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.
18 """Datastore backed Blobstore API stub.
20 Class:
21 BlobstoreServiceStub: BlobstoreService stub backed by datastore.
22 """
29 import os
30 import time
32 from google.appengine.api import apiproxy_stub
33 from google.appengine.api import datastore
34 from google.appengine.api import datastore_errors
35 from google.appengine.api import datastore_types
36 from google.appengine.api import users
37 from google.appengine.api import blobstore
38 from google.appengine.api.blobstore import blobstore_service_pb
39 from google.appengine.runtime import apiproxy_errors
42 __all__ = ['BlobStorage',
43 'BlobstoreServiceStub',
44 'ConfigurationError',
45 'CreateUploadSession',
46 'Error',
50 class Error(Exception):
51 """Base blobstore error type."""
54 class ConfigurationError(Error):
55 """Raised when environment is not correctly configured."""
58 _UPLOAD_SESSION_KIND = '__BlobUploadSession__'
61 def CreateUploadSession(creation, success_path, user):
62 """Create upload session in datastore.
64 Creates an upload session and puts it in Datastore to be referenced by
65 upload handler later.
67 Args:
68 creation: Creation timestamp.
69 success_path: Path in users application to call upon success.
70 user: User that initiated this upload, if any.
72 Returns:
73 String encoded key of new Datastore entity.
74 """
75 entity = datastore.Entity(_UPLOAD_SESSION_KIND, namespace='')
76 entity.update({'creation': creation,
77 'success_path': success_path,
78 'user': user,
79 'state': 'init'})
80 datastore.Put(entity)
81 return str(entity.key())
84 class BlobStorage(object):
85 """Base class for defining how blobs are stored.
87 This base class merely defines an interface that all stub blob-storage
88 mechanisms must implement.
89 """
91 def StoreBlob(self, blob_key, blob_stream):
92 """Store blob stream.
94 Implement this method to persist blob data.
96 Args:
97 blob_key: Blob key of blob to store.
98 blob_stream: Stream or stream-like object that will generate blob content.
99 """
100 raise NotImplementedError('Storage class must override StoreBlob method.')
102 def OpenBlob(self, blob_key):
103 """Open blob for streaming.
105 Args:
106 blob_key: Blob-key of existing blob to open for reading.
108 Returns:
109 Open file stream for reading blob. Caller is responsible for closing
110 file.
112 raise NotImplementedError('Storage class must override OpenBlob method.')
114 def DeleteBlob(self, blob_key):
115 """Delete blob data from storage.
117 Args:
118 blob_key: Blob-key of existing blob to delete.
120 raise NotImplementedError('Storage class must override DeleteBlob method.')
123 class BlobstoreServiceStub(apiproxy_stub.APIProxyStub):
124 """Datastore backed Blobstore service stub.
126 This stub stores manages upload sessions in the Datastore and must be
127 provided with a blob_storage object to know where the actual blob
128 records can be found after having been uploaded.
130 This stub does not handle the actual creation of blobs, neither the BlobInfo
131 in the Datastore nor creation of blob data in the blob_storage. It does,
132 however, assume that another part of the system has created these and
133 uses these objects for deletion.
135 An upload session is created when the CreateUploadURL request is handled and
136 put in the Datastore under the __BlobUploadSession__ kind. There is no
137 analog for this kind on a production server. Other than creation, this stub
138 not work with session objects. The URLs created by this service stub are:
140 http://<appserver-host>:<appserver-port>/<uploader-path>/<session-info>
142 This is very similar to what the URL is on a production server. The session
143 info is the string encoded version of the session entity
146 def __init__(self,
147 blob_storage,
148 time_function=time.time,
149 service_name='blobstore',
150 uploader_path='_ah/upload/'):
151 """Constructor.
153 Args:
154 blob_storage: BlobStorage class instance used for blob storage.
155 time_function: Used for dependency injection in tests.
156 service_name: Service name expected for all calls.
157 uploader_path: Path to upload handler pointed to by URLs generated
158 by this service stub.
160 super(BlobstoreServiceStub, self).__init__(service_name)
161 self.__storage = blob_storage
162 self.__time_function = time_function
163 self.__next_session_id = 1
164 self.__uploader_path = uploader_path
166 @property
167 def storage(self):
168 """Access BlobStorage used by service stub.
170 Returns:
171 BlobStorage instance used by blobstore service stub.
173 return self.__storage
175 def _GetEnviron(self, name):
176 """Helper method ensures environment configured as expected.
178 Args:
179 name: Name of environment variable to get.
181 Returns:
182 Environment variable associated with name.
184 Raises:
185 ConfigurationError if required environment variable is not found.
187 try:
188 return os.environ[name]
189 except KeyError:
190 raise ConfigurationError('%s is not set in environment.' % name)
192 def _CreateSession(self, success_path, user):
193 """Create new upload session.
195 Args:
196 success_path: Application path to call upon successful POST.
197 user: User that initiated the upload session.
199 Returns:
200 String encoded key of a new upload session created in the datastore.
202 return CreateUploadSession(self.__time_function(),
203 success_path,
204 user)
206 def _Dynamic_CreateUploadURL(self, request, response):
207 """Create upload URL implementation.
209 Create a new upload session. The upload session key is encoded in the
210 resulting POST URL. This URL is embedded in a POST form by the application
211 which contacts the uploader when the user posts.
213 Args:
214 request: A fully initialized CreateUploadURLRequest instance.
215 response: A CreateUploadURLResponse instance.
217 session = self._CreateSession(request.success_path(),
218 users.get_current_user())
220 response.set_url('http://%s:%s/%s%s' % (self._GetEnviron('SERVER_NAME'),
221 self._GetEnviron('SERVER_PORT'),
222 self.__uploader_path,
223 session))
225 def _Dynamic_DeleteBlob(self, request, response):
226 """Delete a blob by its blob-key.
228 Delete a blob from the blobstore using its blob-key. Deleting blobs that
229 do not exist is a no-op.
231 Args:
232 request: A fully initialized DeleteBlobRequest instance.
233 response: Not used but should be a VoidProto.
235 for blob_key in request.blob_key_list():
236 key = datastore_types.Key.from_path(blobstore.BLOB_INFO_KIND,
237 str(blob_key),
238 namespace='')
240 datastore.Delete(key)
241 self.__storage.DeleteBlob(blob_key)
243 def _Dynamic_FetchData(self, request, response):
244 """Fetch a blob fragment from a blob by its blob-key.
246 Fetches a blob fragment using its blob-key. Start index is inclusive,
247 end index is inclusive. Valid requests for information outside of
248 the range of the blob return a partial string or empty string if entirely
249 out of range.
251 Args:
252 request: A fully initialized FetchDataRequest instance.
253 response: A FetchDataResponse instance.
255 Raises:
256 ApplicationError when application has the following errors:
257 INDEX_OUT_OF_RANGE: Index is negative or end > start.
258 BLOB_FETCH_SIZE_TOO_LARGE: Request blob fragment is larger than
259 MAX_BLOB_FRAGMENT_SIZE.
260 BLOB_NOT_FOUND: If invalid blob-key is provided or is not found.
262 start_index = request.start_index()
263 if start_index < 0:
264 raise apiproxy_errors.ApplicationError(
265 blobstore_service_pb.BlobstoreServiceError.DATA_INDEX_OUT_OF_RANGE)
267 end_index = request.end_index()
268 if end_index < start_index:
269 raise apiproxy_errors.ApplicationError(
270 blobstore_service_pb.BlobstoreServiceError.DATA_INDEX_OUT_OF_RANGE)
272 fetch_size = end_index - start_index + 1
273 if fetch_size > blobstore.MAX_BLOB_FETCH_SIZE:
274 raise apiproxy_errors.ApplicationError(
275 blobstore_service_pb.BlobstoreServiceError.BLOB_FETCH_SIZE_TOO_LARGE)
277 blob_key = request.blob_key()
278 blob_info_key = datastore.Key.from_path(blobstore.BLOB_INFO_KIND,
279 blob_key,
280 namespace='')
281 try:
282 datastore.Get(blob_info_key)
283 except datastore_errors.EntityNotFoundError, err:
284 raise apiproxy_errors.ApplicationError(
285 blobstore_service_pb.BlobstoreServiceError.BLOB_NOT_FOUND)
287 blob_file = self.__storage.OpenBlob(blob_key)
288 blob_file.seek(start_index)
289 response.set_data(blob_file.read(fetch_size))