blob: 97461f521a9a8746cc05a3ea4a0be6218108d624 [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.
#
"""A Python blobstore API used by app developers.
Contains methods used to interface with Blobstore API. Includes db.Model-like
class representing a reference to a very large BLOB. Imports db.Key-like
class representing a blob-key.
"""
import base64
import email
import email.message
from google.appengine.api import datastore
from google.appengine.api import datastore_errors
from google.appengine.api import datastore_types
from google.appengine.api.blobstore import blobstore
from google.appengine.ext import db
__all__ = ['BLOB_INFO_KIND',
'BLOB_KEY_HEADER',
'BLOB_MIGRATION_KIND',
'BLOB_RANGE_HEADER',
'BlobFetchSizeTooLargeError',
'BlobInfo',
'BlobInfoParseError',
'BlobKey',
'BlobMigrationRecord',
'BlobNotFoundError',
'BlobReferenceProperty',
'BlobReader',
'FileInfo',
'FileInfoParseError',
'DataIndexOutOfRangeError',
'PermissionDeniedError',
'Error',
'InternalError',
'MAX_BLOB_FETCH_SIZE',
'UPLOAD_INFO_CREATION_HEADER',
'CLOUD_STORAGE_OBJECT_HEADER',
'create_rpc',
'create_upload_url',
'create_upload_url_async',
'delete',
'delete_async',
'fetch_data',
'fetch_data_async',
'create_gs_key',
'create_gs_key_async',
'GS_PREFIX',
'get',
'parse_blob_info',
'parse_file_info']
Error = blobstore.Error
InternalError = blobstore.InternalError
BlobFetchSizeTooLargeError = blobstore.BlobFetchSizeTooLargeError
BlobNotFoundError = blobstore.BlobNotFoundError
_CreationFormatError = blobstore._CreationFormatError
DataIndexOutOfRangeError = blobstore.DataIndexOutOfRangeError
PermissionDeniedError = blobstore.PermissionDeniedError
BlobKey = blobstore.BlobKey
create_rpc = blobstore.create_rpc
create_upload_url = blobstore.create_upload_url
create_upload_url_async = blobstore.create_upload_url_async
delete = blobstore.delete
delete_async = blobstore.delete_async
create_gs_key = blobstore.create_gs_key
create_gs_key_async = blobstore.create_gs_key_async
BLOB_INFO_KIND = blobstore.BLOB_INFO_KIND
BLOB_MIGRATION_KIND = blobstore.BLOB_MIGRATION_KIND
BLOB_KEY_HEADER = blobstore.BLOB_KEY_HEADER
BLOB_RANGE_HEADER = blobstore.BLOB_RANGE_HEADER
MAX_BLOB_FETCH_SIZE = blobstore.MAX_BLOB_FETCH_SIZE
UPLOAD_INFO_CREATION_HEADER = blobstore.UPLOAD_INFO_CREATION_HEADER
CLOUD_STORAGE_OBJECT_HEADER = blobstore.CLOUD_STORAGE_OBJECT_HEADER
GS_PREFIX = blobstore.GS_PREFIX
class BlobInfoParseError(Error):
"""CGI parameter does not contain valid BlobInfo record."""
class FileInfoParseError(Error):
"""CGI parameter does not contain valid FileInfo record."""
class _GqlQuery(db.GqlQuery):
"""GqlQuery class that explicitly sets model-class.
This does the same as the original db.GqlQuery class except that it does
not try to find the model class based on the compiled GQL query. The
caller instead provides the query with a model class to use for construction.
This class is required for compatibility with the current db.py query
mechanism but will be removed in the future. DO NOT USE.
"""
def __init__(self, query_string, model_class, *args, **kwds):
"""Constructor.
Args:
query_string: Properly formatted GQL query string.
model_class: Model class from which entities are constructed.
*args: Positional arguments used to bind numeric references in the query.
**kwds: Dictionary-based arguments for named references.
"""
from google.appengine.ext import gql
app = kwds.pop('_app', None)
self._proto_query = gql.GQL(query_string, _app=app, namespace='')
super(db.GqlQuery, self).__init__(model_class)
self.bind(*args, **kwds)
class BlobInfo(object):
"""Information about blobs in Blobstore.
This is a db.Model-like class that contains information about blobs stored
by an application. Like db.Model, this class is backed by an Datastore
entity, however, BlobInfo instances are read-only and have a much more
limited interface.
Each BlobInfo has a key of type BlobKey associated with it. This key is
specific to the Blobstore API and is not compatible with db.get. The key
can be used for quick lookup by passing it to BlobInfo.get. This
key converts easily to a string, which is web safe and can be embedded
in URLs.
Properties:
content_type: Content type of blob.
creation: Creation date of blob, when it was uploaded.
filename: Filename user selected from their machine.
size: Size of uncompressed blob.
md5_hash: The md5 hash value of the uploaded blob.
All properties are read-only. Attempting to assign a value to a property
will raise NotImplementedError.
"""
_unindexed_properties = frozenset([])
_all_properties = frozenset(['content_type', 'creation', 'filename',
'size', 'md5_hash'])
@property
def content_type(self):
return self.__get_value('content_type')
@property
def creation(self):
return self.__get_value('creation')
@property
def filename(self):
return self.__get_value('filename')
@property
def size(self):
return self.__get_value('size')
@property
def md5_hash(self):
return self.__get_value('md5_hash')
def __init__(self, entity_or_blob_key, _values=None):
"""Constructor for wrapping blobstore entity.
The constructor should not be used outside this package and tests.
Args:
entity: Datastore entity that represents the blob reference.
"""
if isinstance(entity_or_blob_key, datastore.Entity):
self.__entity = entity_or_blob_key
self.__key = BlobKey(entity_or_blob_key.key().name())
elif isinstance(entity_or_blob_key, BlobKey):
self.__entity = _values
self.__key = entity_or_blob_key
else:
raise TypeError('Must provide Entity or BlobKey')
@classmethod
def from_entity(cls, entity):
"""Convert entity to BlobInfo.
This method is required for compatibility with the current db.py query
mechanism but will be removed in the future. DO NOT USE.
"""
return BlobInfo(entity)
@classmethod
def properties(cls):
"""Set of properties that belong to BlobInfo.
This method is required for compatibility with the current db.py query
mechanism but will be removed in the future. DO NOT USE.
"""
return set(cls._all_properties)
def __get_value(self, name):
"""Get a BlobInfo value, loading entity if necessary.
This method allows lazy loading of the underlying datastore entity. It
should never be invoked directly.
Args:
name: Name of property to get value for.
Returns:
Value of BlobInfo property from entity.
"""
if self.__entity is None:
self.__entity = datastore.Get(
datastore_types.Key.from_path(
self.kind(), str(self.__key), namespace=''))
try:
return self.__entity[name]
except KeyError:
raise AttributeError(name)
def key(self):
"""Get key for blob.
Returns:
BlobKey instance that identifies this blob.
"""
return self.__key
def delete(self, _token=None):
"""Permanently delete blob from Blobstore."""
delete(self.key(), _token=_token)
def open(self, *args, **kwargs):
"""Returns a BlobReader for this blob.
Args:
*args, **kwargs: Passed to BlobReader constructor.
Returns:
A BlobReader instance.
"""
return BlobReader(self, *args, **kwargs)
@classmethod
def get(cls, blob_keys):
"""Retrieve BlobInfo by key or list of keys.
Args:
blob_keys: A key or a list of keys. Keys may be instances of str,
unicode and BlobKey.
Returns:
A BlobInfo instance associated with provided key or a list of BlobInfo
instances if a list of keys was provided. Keys that are not found in
Blobstore return None as their values.
"""
blob_keys = cls.__normalize_and_convert_keys(blob_keys)
try:
entities = datastore.Get(blob_keys)
except datastore_errors.EntityNotFoundError:
return None
if isinstance(entities, datastore.Entity):
return BlobInfo(entities)
else:
references = []
for entity in entities:
if entity is not None:
references.append(BlobInfo(entity))
else:
references.append(None)
return references
@classmethod
def all(cls):
"""Get query for all Blobs associated with application.
Returns:
A db.Query object querying over BlobInfo's datastore kind.
"""
return db.Query(model_class=cls, namespace='')
@classmethod
def __factory_for_kind(cls, kind):
if kind == BLOB_INFO_KIND:
return BlobInfo
raise ValueError('Cannot query for kind %s' % kind)
@classmethod
def gql(cls, query_string, *args, **kwds):
"""Returns a query using GQL query string.
See appengine/ext/gql for more information about GQL.
Args:
query_string: Properly formatted GQL query string with the
'SELECT * FROM <entity>' part omitted
*args: rest of the positional arguments used to bind numeric references
in the query.
**kwds: dictionary-based arguments (for named parameters).
Returns:
A gql.GqlQuery object querying over BlobInfo's datastore kind.
"""
return _GqlQuery('SELECT * FROM %s %s'
% (cls.kind(), query_string),
cls,
*args,
**kwds)
@classmethod
def kind(self):
"""Get the entity kind for the BlobInfo.
This method is required for compatibility with the current db.py query
mechanism but will be removed in the future. DO NOT USE.
"""
return BLOB_INFO_KIND
@classmethod
def __normalize_and_convert_keys(cls, keys):
"""Normalize and convert all keys to BlobKey type.
This method is based on datastore.NormalizeAndTypeCheck().
Args:
keys: A single key or a list/tuple of keys. Keys may be a string
or BlobKey
Returns:
Single key or list with all strings replaced by BlobKey instances.
"""
if isinstance(keys, (list, tuple)):
multiple = True
keys = list(keys)
else:
multiple = False
keys = [keys]
for index, key in enumerate(keys):
if not isinstance(key, (basestring, BlobKey)):
raise datastore_errors.BadArgumentError(
'Expected str or BlobKey; received %s (a %s)' % (
key,
datastore.typename(key)))
keys[index] = datastore.Key.from_path(cls.kind(), str(key), namespace='')
if multiple:
return keys
else:
return keys[0]
def get(blob_key):
"""Get a BlobInfo record from blobstore.
Does the same as BlobInfo.get.
"""
return BlobInfo.get(blob_key)
def _get_upload_content(field_storage):
"""Returns an email.Message holding the values of the file transfer.
It decodes the content of the field storage and creates a new email.Message.
Args:
field_storage: cgi.FieldStorage that represents uploaded blob.
Returns:
An email.message.Message holding the upload information.
"""
message = email.message.Message()
message.add_header(
'content-transfer-encoding',
field_storage.headers.getheader('Content-Transfer-Encoding', ''))
message.set_payload(field_storage.file.read())
payload = message.get_payload(decode=True)
return email.message_from_string(payload)
def _parse_upload_info(field_storage, error_class):
"""Parse the upload info from file upload field_storage.
Args:
field_storage: cgi.FieldStorage that represents uploaded blob.
error_class: error to raise.
Returns:
A dictionary containing the parsed values. None if there was no
field_storage.
Raises:
error_class when provided a field_storage that does not contain enough
information.
"""
if field_storage is None:
return None
field_name = field_storage.name
def get_value(dict, name):
value = dict.get(name, None)
if value is None:
raise error_class(
'Field %s has no %s.' % (field_name, name))
return value
filename = get_value(field_storage.disposition_options, 'filename')
blob_key = field_storage.type_options.get('blob-key', None)
upload_content = _get_upload_content(field_storage)
field_storage.file.seek(0)
content_type = get_value(upload_content, 'content-type')
size = get_value(upload_content, 'content-length')
creation_string = get_value(upload_content, UPLOAD_INFO_CREATION_HEADER)
md5_hash_encoded = get_value(upload_content, 'content-md5')
md5_hash = base64.urlsafe_b64decode(md5_hash_encoded)
gs_object_name = upload_content.get(CLOUD_STORAGE_OBJECT_HEADER, None)
try:
size = int(size)
except (TypeError, ValueError):
raise error_class(
'%s is not a valid value for %s size.' % (size, field_name))
try:
creation = blobstore._parse_creation(creation_string, field_name)
except blobstore._CreationFormatError, err:
raise error_class(str(err))
return {'blob_key': blob_key,
'content_type': content_type,
'creation': creation,
'filename': filename,
'size': size,
'md5_hash': md5_hash,
'gs_object_name': gs_object_name,
}
def parse_blob_info(field_storage):
"""Parse a BlobInfo record from file upload field_storage.
Args:
field_storage: cgi.FieldStorage that represents uploaded blob.
Returns:
BlobInfo record as parsed from the field-storage instance.
None if there was no field_storage.
Raises:
BlobInfoParseError when provided field_storage does not contain enough
information to construct a BlobInfo object.
"""
info = _parse_upload_info(field_storage, BlobInfoParseError)
if info is None:
return None
key = info.pop('blob_key', None)
if not key:
raise BlobInfoParseError('Field %s has no %s.' % (field_storage.name,
'blob_key'))
info.pop('gs_object_name', None)
return BlobInfo(BlobKey(key), info)
class FileInfo(object):
"""Information about uploaded files.
This is a class that contains information about blobs stored by an
application.
This class is similar to BlobInfo, however this has no key and it is not
persisted in the datastore.
Properties:
content_type: Content type of uploaded file.
creation: Creation date of uploaded file, when it was uploaded.
filename: Filename user selected from their machine.
size: Size of uncompressed file.
md5_hash: The md5 hash value of the uploaded file.
gs_object_name: Name of the file written to Google Cloud Storage or None if
the file was not uploaded to Google Cloud Storage.
All properties are read-only. Attempting to assign a value to a property
will raise AttributeError.
"""
def __init__(self, filename=None, content_type=None, creation=None,
size=None, md5_hash=None, gs_object_name=None):
self.__filename = filename
self.__content_type = content_type
self.__creation = creation
self.__size = size
self.__md5_hash = md5_hash
self.__gs_object_name = gs_object_name
@property
def filename(self):
return self.__filename
@property
def content_type(self):
return self.__content_type
@property
def creation(self):
return self.__creation
@property
def size(self):
return self.__size
@property
def md5_hash(self):
return self.__md5_hash
@property
def gs_object_name(self):
return self.__gs_object_name
def parse_file_info(field_storage):
"""Parse an FileInfo record from file upload field_storage.
Args:
field_storage: cgi.FieldStorage that represents uploaded file.
Returns:
FileInfo record as parsed from the field-storage instance.
None if there was no field_storage.
Raises:
FileInfoParseError when provided a field_storage that does not contain
enough information to construct a FileInfo object.
"""
info = _parse_upload_info(field_storage, FileInfoParseError)
if info is None:
return None
info.pop('blob_key', None)
return FileInfo(**info)
class BlobReferenceProperty(db.Property):
"""Property compatible with db.Model classes.
Add references to blobs to domain models using BlobReferenceProperty:
class Picture(db.Model):
title = db.StringProperty()
image = blobstore.BlobReferenceProperty()
thumbnail = blobstore.BlobReferenceProperty()
To find the size of a picture using this model:
picture = Picture.get(picture_key)
print picture.image.size
BlobInfo objects are lazily loaded so iterating over models with
for BlobKeys is efficient, the following does not need to hit
Datastore for each image key:
list_of_untitled_blobs = []
for picture in Picture.gql("WHERE title=''"):
list_of_untitled_blobs.append(picture.image.key())
"""
data_type = BlobInfo
def get_value_for_datastore(self, model_instance):
"""Translate model property to datastore value."""
blob_info = super(BlobReferenceProperty,
self).get_value_for_datastore(model_instance)
if blob_info is None:
return None
return blob_info.key()
def make_value_from_datastore(self, value):
"""Translate datastore value to BlobInfo."""
if value is None:
return None
return BlobInfo(value)
def validate(self, value):
"""Validate that assigned value is BlobInfo.
Automatically converts from strings and BlobKey instances.
"""
if isinstance(value, (basestring)):
value = BlobInfo(BlobKey(value))
elif isinstance(value, BlobKey):
value = BlobInfo(value)
return super(BlobReferenceProperty, self).validate(value)
def fetch_data(blob, start_index, end_index, rpc=None):
"""Fetch data for blob.
Fetches a fragment of a blob up to MAX_BLOB_FETCH_SIZE in length. Attempting
to fetch a fragment that extends beyond the boundaries of the blob will return
the amount of data from start_index until the end of the blob, which will be
a smaller size than requested. Requesting a fragment which is entirely
outside the boundaries of the blob will return empty string. Attempting
to fetch a negative index will raise an exception.
Args:
blob: BlobInfo, BlobKey, str or unicode representation of BlobKey of
blob to fetch data from.
start_index: Start index of blob data to fetch. May not be negative.
end_index: End index (inclusive) of blob data to fetch. Must be
>= start_index.
rpc: Optional UserRPC object.
Returns:
str containing partial data of blob. If the indexes are legal but outside
the boundaries of the blob, will return empty string.
Raises:
TypeError if start_index or end_index are not indexes. Also when blob
is not a string, BlobKey or BlobInfo.
DataIndexOutOfRangeError when start_index < 0 or end_index < start_index.
BlobFetchSizeTooLargeError when request blob fragment is larger than
MAX_BLOB_FETCH_SIZE.
BlobNotFoundError when blob does not exist.
"""
rpc = fetch_data_async(blob, start_index, end_index, rpc=rpc)
return rpc.get_result()
def fetch_data_async(blob, start_index, end_index, rpc=None):
"""Fetch data for blob -- async version.
Fetches a fragment of a blob up to MAX_BLOB_FETCH_SIZE in length. Attempting
to fetch a fragment that extends beyond the boundaries of the blob will return
the amount of data from start_index until the end of the blob, which will be
a smaller size than requested. Requesting a fragment which is entirely
outside the boundaries of the blob will return empty string. Attempting
to fetch a negative index will raise an exception.
Args:
blob: BlobInfo, BlobKey, str or unicode representation of BlobKey of
blob to fetch data from.
start_index: Start index of blob data to fetch. May not be negative.
end_index: End index (inclusive) of blob data to fetch. Must be
>= start_index.
rpc: Optional UserRPC object.
Returns:
A UserRPC whose result will be a str as returned by fetch_data().
Raises:
TypeError if start_index or end_index are not indexes. Also when blob
is not a string, BlobKey or BlobInfo.
The following exceptions may be raised when rpc.get_result() is
called:
DataIndexOutOfRangeError when start_index < 0 or end_index < start_index.
BlobFetchSizeTooLargeError when request blob fragment is larger than
MAX_BLOB_FETCH_SIZE.
BlobNotFoundError when blob does not exist.
"""
if isinstance(blob, BlobInfo):
blob = blob.key()
return blobstore.fetch_data_async(blob, start_index, end_index, rpc=rpc)
class BlobReader(object):
"""Provides a read-only file-like interface to a blobstore blob."""
SEEK_SET = 0
SEEK_CUR = 1
SEEK_END = 2
def __init__(self, blob, buffer_size=131072, position=0):
"""Constructor.
Args:
blob: The blob key, blob info, or string blob key to read from.
buffer_size: The minimum size to fetch chunks of data from blobstore.
position: The initial position in the file.
Raises:
ValueError if a blob key, blob info or string blob key is not supplied.
"""
if not blob:
raise ValueError('A BlobKey, BlobInfo or string is required.')
if hasattr(blob, 'key'):
self.__blob_key = blob.key()
self.__blob_info = blob
else:
self.__blob_key = blob
self.__blob_info = None
self.__buffer_size = buffer_size
self.__buffer = ""
self.__position = position
self.__buffer_position = 0
self.__eof = False
def __iter__(self):
"""Returns a file iterator for this BlobReader."""
return self
def __getstate__(self):
"""Returns the serialized state for this BlobReader."""
return (self.__blob_key, self.__buffer_size, self.__position)
def __setstate__(self, state):
"""Restores pickled state for this BlobReader."""
self.__init__(*state)
def close(self):
"""Close the file.
A closed file cannot be read or written any more. Any operation which
requires that the file be open will raise a ValueError after the file has
been closed. Calling close() more than once is allowed.
"""
self.__blob_key = None
def flush(self):
raise IOError("BlobReaders are read-only")
def next(self):
"""Returns the next line from the file.
Returns:
A string, terminted by \n. The last line may not be terminated by \n.
If EOF is reached, an empty string will be returned.
"""
line = self.readline()
if not line:
raise StopIteration
return line
def __read_from_buffer(self, size):
"""Reads at most size bytes from the buffer.
Args:
size: Number of bytes to read, or negative to read the entire buffer.
Returns:
Tuple (data, size):
data: The bytes read from the buffer.
size: The remaining unread byte count. Negative when size
is negative. Thus when remaining size != 0, the calling method
may choose to fill the buffer again and keep reading.
"""
if not self.__blob_key:
raise ValueError("File is closed")
if size < 0:
end_pos = len(self.__buffer)
else:
end_pos = self.__buffer_position + size
data = self.__buffer[self.__buffer_position:end_pos]
data_length = len(data)
size -= data_length
self.__position += data_length
self.__buffer_position += data_length
if self.__buffer_position == len(self.__buffer):
self.__buffer = ""
self.__buffer_position = 0
return data, size
def __fill_buffer(self, size=0):
"""Fills the internal buffer.
Args:
size: Number of bytes to read. Will be clamped to
[self.__buffer_size, MAX_BLOB_FETCH_SIZE].
"""
read_size = min(max(size, self.__buffer_size), MAX_BLOB_FETCH_SIZE)
self.__buffer = fetch_data(self.__blob_key, self.__position,
self.__position + read_size - 1)
self.__buffer_position = 0
self.__eof = len(self.__buffer) < read_size
def read(self, size=-1):
"""Read at most size bytes from the file.
Fewer bytes are read if the read hits EOF before obtaining size bytes.
If the size argument is negative or omitted, read all data until EOF is
reached. The bytes are returned as a string object. An empty string is
returned when EOF is encountered immediately.
Calling read() without a size specified is likely to be dangerous, as it
may read excessive amounts of data.
Args:
size: Optional. The maximum number of bytes to read. When omitted, read()
returns all remaining data in the file.
Returns:
The read data, as a string.
"""
data_list = []
while True:
data, size = self.__read_from_buffer(size)
data_list.append(data)
if size == 0 or self.__eof:
return ''.join(data_list)
self.__fill_buffer(size)
def readline(self, size=-1):
"""Read one entire line from the file.
A trailing newline character is kept in the string (but may be absent when a
file ends with an incomplete line). If the size argument is present and
non-negative, it is a maximum byte count (including the trailing newline)
and an incomplete line may be returned. An empty string is returned only
when EOF is encountered immediately.
Args:
size: Optional. The maximum number of bytes to read.
Returns:
The read data, as a string.
"""
data_list = []
while True:
if size < 0:
end_pos = len(self.__buffer)
else:
end_pos = self.__buffer_position + size
newline_pos = self.__buffer.find('\n', self.__buffer_position, end_pos)
if newline_pos != -1:
data_list.append(
self.__read_from_buffer(newline_pos
- self.__buffer_position + 1)[0])
break
else:
data, size = self.__read_from_buffer(size)
data_list.append(data)
if size == 0 or self.__eof:
break
self.__fill_buffer()
return ''.join(data_list)
def readlines(self, sizehint=None):
"""Read until EOF using readline() and return a list of lines thus read.
If the optional sizehint argument is present, instead of reading up to EOF,
whole lines totalling approximately sizehint bytes (possibly after rounding
up to an internal buffer size) are read.
Args:
sizehint: A hint as to the maximum number of bytes to read.
Returns:
A list of strings, each being a single line from the file.
"""
lines = []
while sizehint is None or sizehint > 0:
line = self.readline()
if sizehint:
sizehint -= len(line)
if not line:
break
lines.append(line)
return lines
def seek(self, offset, whence=SEEK_SET):
"""Set the file's current position, like stdio's fseek().
The whence argument is optional and defaults to os.SEEK_SET or 0 (absolute
file positioning); other values are os.SEEK_CUR or 1 (seek relative to the
current position) and os.SEEK_END or 2 (seek relative to the file's end).
Args:
offset: The relative offset to seek to.
whence: Defines what the offset is relative to. See description for
details.
"""
if whence == BlobReader.SEEK_CUR:
offset = self.__position + offset
elif whence == BlobReader.SEEK_END:
offset = self.blob_info.size + offset
self.__buffer = ""
self.__buffer_position = 0
self.__position = offset
self.__eof = False
def tell(self):
"""Return the file's current position, like stdio's ftell()."""
return self.__position
def truncate(self, size):
raise IOError("BlobReaders are read-only")
def write(self, str):
raise IOError("BlobReaders are read-only")
def writelines(self, sequence):
raise IOError("BlobReaders are read-only")
@property
def blob_info(self):
"""Returns the BlobInfo for this file."""
if not self.__blob_info:
self.__blob_info = BlobInfo.get(self.__blob_key)
return self.__blob_info
@property
def closed(self):
"""Returns True if this file is closed, False otherwise."""
return self.__blob_key is None
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, traceback):
self.close()
class BlobMigrationRecord(db.Model):
"""A model that records the result of a blob migration."""
new_blob_ref = BlobReferenceProperty(indexed=False, name='new_blob_key')
@classmethod
def kind(cls):
return blobstore.BLOB_MIGRATION_KIND
@classmethod
def get_by_blob_key(cls, old_blob_key):
"""Fetches the BlobMigrationRecord for the given blob key.
Args:
old_blob_key: The blob key used in the previous app.
Returns:
A instance of blobstore.BlobMigrationRecord or None
"""
return cls.get_by_key_name(str(old_blob_key))
@classmethod
def get_new_blob_key(cls, old_blob_key):
"""Looks up the new key for a blob.
Args:
old_blob_key: The original blob key.
Returns:
The blobstore.BlobKey of the migrated blob.
"""
record = cls.get_by_blob_key(old_blob_key)
if record:
return record.new_blob_ref.key()