#!/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.
This module contains methods that are used to interface with Blobstore API.
The module includes a `db.Model`-like class that represents a reference to a
very large blob. The module imports a `db.Key`-like class that represents 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
[docs]class BlobInfoParseError(Error):
"""The CGI parameter does not contain a valid `BlobInfo` record."""
[docs]class FileInfoParseError(Error):
"""The CGI parameter does not contain a 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.
Note:
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: A properly formatted GQL query string.
model_class: The model class from which entities are constructed.
*args: Positional arguments that are 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)
[docs]class BlobInfo(object):
"""Information about blobs in Blobstore.
This is a `db.Model`-like class that contains information about blobs that are
stored by an application. Like `db.Model`, this class is backed by a
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: The content type of the blob.
- creation: The creation date of the blob, or when it was uploaded.
- filename: The file name that the user selected from their machine.
- size: The size of the uncompressed blob.
- md5_hash: The MD5 hash value of the uploaded blob.
gs_object_name: The name of the object, if the blob is stored in
Google Cloud Storage, in the form /[bucket-name]/[object-name]
All properties are read-only. Attempting to assign a value to a property
will raise a `NotImplementedError`.
"""
_unindexed_properties = frozenset([])
_all_properties = frozenset(['content_type', 'creation', 'filename',
'size', 'md5_hash', 'gs_object_name'])
@property
def content_type(self):
"""Returns the content type of the blob.
Returns:
The content type of the blob.
"""
return self.__get_value('content_type')
@property
def creation(self):
"""Returns the creation date or time of upload of the blob.
Returns:
The creation date or time of upload of the blob.
"""
return self.__get_value('creation')
@property
def filename(self):
"""Returns the file name that the user selected from their machine.
Returns:
The file name that the user selected.
"""
return self.__get_value('filename')
@property
def size(self):
"""Returns the size of the uncompressed blob.
Returns:
The size of the uncompressed blob.
"""
return self.__get_value('size')
@property
def md5_hash(self):
"""Returns the MD5 hash value of the uncompressed blob.
Returns:
The hash value of the uncompressed blob.
"""
return self.__get_value('md5_hash')
@property
def gs_object_name(self):
return self.__get_value('gs_object_name')
def __init__(self, entity_or_blob_key, _values=None):
"""Constructor for wrapping the blobstore entity.
The constructor should not be used outside of this package and tests.
Args:
entity_or_blob_key: The datastore entity or blob key that represents the
blob reference.
_values: Optional; not recommended. This argument passes the associated
entity when `entity_or_blob_key` is a blob key.
"""
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')
[docs] @classmethod
def from_entity(cls, entity):
"""Converts an entity to `BlobInfo`.
Note:
This method is required for compatibility with the current `db.py` query
mechanism but will be removed in the future. DO NOT USE.
Args:
entity: The entity that you are trying to convert.
Returns:
The `BlobInfo` that was converted from the entity.
"""
return BlobInfo(entity)
[docs] @classmethod
def properties(cls):
"""Defines the set of properties that belong to `BlobInfo`.
Note:
This method is required for compatibility with the current `db.py` query
mechanism but will be removed in the future. DO NOT USE.
Returns:
A set of all of the properties that belong to `BlobInfo`.
"""
return set(cls._all_properties)
def __get_value(self, name):
"""Gets a `BlobInfo` value, loading an entity if necessary.
This method allows lazy loading of the underlying datastore entity. It
should never be invoked directly.
Args:
name: The name of property for which you want to retrieve the value.
Returns:
The value of the `BlobInfo` property from the entity.
"""
if self.__entity is None:
self.__entity = datastore.Get(
datastore_types.Key.from_path(
self.kind(), str(self.__key), namespace=''))
return self.__entity.get(name, None)
[docs] def key(self):
"""Gets the key for a blob.
Returns:
The `BlobKey` instance that identifies this blob.
"""
return self.__key
[docs] def delete(self, _token=None):
"""Permanently deletes a blob from Blobstore."""
delete(self.key(), _token=_token)
[docs] def open(self, *args, **kwargs):
"""Returns a `BlobReader` for this blob.
Args:
*args: Arguments to pass to the `BlobReader` constructor.
**kwargs: Keyword arguments to pass to the `BlobReader` constructor.
Returns:
A `BlobReader` instance.
"""
return BlobReader(self, *args, **kwargs)
[docs] @classmethod
def get(cls, blob_keys):
"""Retrieves a `BlobInfo` by key or by a list of keys.
Args:
blob_keys: A key or a list of keys. Keys can be in string, Unicode, or
`BlobKey` format.
Returns:
A `BlobInfo` instance that is associated with the provided key or a list
of `BlobInfo` instances if a list of keys was provided. Keys that are not
found in Blobstore return `None`.
"""
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
[docs] @classmethod
def all(cls):
"""Creates a query for all `BlobInfo` objects associated with the app.
Returns:
A `db.Query` object that queries 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)
[docs] @classmethod
def gql(cls, query_string, *args, **kwds):
"""Returns a query using a GQL query string.
See the `GQL source` for more information about GQL.
Args:
query_string: A properly formatted GQL query string that omits
`SELECT * FROM <entity>`
*args: The remaining positional arguments that are used to bind numeric
references in the query.
**kwds: The dictionary-based arguments for named parameters.
Returns:
A `gql.GqlQuery` object that queries over `BlobInfo`'s datastore kind.
.. _GQL source:
https://cloud.google.com/appengine/docs/python/refdocs/google.appengine.ext.gql
"""
return _GqlQuery('SELECT * FROM %s %s'
% (cls.kind(), query_string),
cls,
*args,
**kwds)
[docs] @classmethod
def kind(self):
"""Gets the entity kind for the `BlobInfo`.
Note:
This method is required for compatibility with the current `db.py` query
mechanism but will be removed in the future. DO NOT USE.
Returns:
The entity kind for `BlobInfo`.
"""
return BLOB_INFO_KIND
@classmethod
def __normalize_and_convert_keys(cls, keys):
"""Normalizes and converts all keys to `BlobKey` type.
This method is based on `datastore.NormalizeAndTypeCheck()`.
Args:
keys: A single key or a list or tuple of keys. Keys can be a string or
a `BlobKey`.
Returns:
A single key or a list with all of the 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 string 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]
[docs]def get(blob_key):
"""Gets a `BlobInfo` record from blobstore.
Does the same as `BlobInfo.get`.
Args:
blob_key: The `BlobKey` of the record you want to retrieve.
Returns:
A `BlobInfo` instance that is associated with the provided key or a list
of `BlobInfo` instances if a list of keys was provided. Keys that are not
found in Blobstore return `None`.
"""
return BlobInfo.get(blob_key)
def _get_upload_content(field_storage):
"""Returns an `email.Message` that contains the values of the file transfer.
This method decodes the content of the field storage and creates a new
`email.Message`.
Args:
field_storage: `cgi.FieldStorage` that represents an uploaded blob.
Returns:
An `email.message.Message` that lists 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):
"""Parses the upload information from the file upload `field_storage`.
Args:
field_storage: `cgi.FieldStorage` that represents the uploaded blob.
error_class: The error to raise if the information cannot be parsed.
Returns:
A dictionary that contains the parsed values. This method will return `None`
if `field_storage` was not defined.
Raises:
`error_class` when provided with 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):
"""Gets the name of the field."""
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,
}
[docs]def parse_blob_info(field_storage):
"""Parses a `BlobInfo` record from file upload `field_storage`.
Args:
field_storage: `cgi.FieldStorage` that represents an uploaded blob.
Returns:
A `BlobInfo` record as parsed from the `field_storage` instance. This
method will return `None` if `field_storage` was not defined.
Raises:
BlobInfoParseError: If the 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'))
return BlobInfo(BlobKey(key), info)
[docs]class FileInfo(object):
"""Details information about uploaded files.
This class contains information about blobs stored by an application.
This class is similar to `BlobInfo`; however, this method does not make use of
a key, and the information is not persisted in the datastore.
Properties:
- content_type: The content type of the uploaded file.
- creation: The creation date of the uploaded file, or when it was
uploaded.
- filename: The file name that the user selected from their machine.
- size: The size of the uncompressed file.
- md5_hash: The MD5 hash value of the uploaded file.
- gs_object_name: The name of the file that was 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 an `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):
"""Returns the file name that the user selected.
Returns:
The file name that the user selected.
"""
return self.__filename
@property
def content_type(self):
"""Returns the content type of the uploaded file.
Returns:
The content type of the file.
"""
return self.__content_type
@property
def creation(self):
"""Returns the creation date or upload time of the file.
Returns:
The creation date or upload time of the file.
"""
return self.__creation
@property
def size(self):
"""Returns the size of the uncompressed file.
Returns:
The size of the uncompressed file.
"""
return self.__size
@property
def md5_hash(self):
"""Returns the MD5 hash of the uploaded file.
Returns:
The hash value for the uploaded file.
"""
return self.__md5_hash
@property
def gs_object_name(self):
"""Returns the name of the file that was written to Cloud Storage.
Returns:
The name of the file that was written to Cloud Storage.
"""
return self.__gs_object_name
[docs]def parse_file_info(field_storage):
"""Parses a `FileInfo` record from file upload `field_storage`.
Args:
field_storage: `cgi.FieldStorage` that represents the uploaded file.
Returns:
`FileInfo` record as parsed from the `field_storage` instance. This method
will return `None` if `field_storage` was not specified.
Raises:
FileInfoParseError: If `field_storage` 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)
[docs]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 for `BlobKeys`
is efficient. The following sample code 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
[docs] def get_value_for_datastore(self, model_instance):
"""Returns a model property translated to a datastore value.
Args:
model_instance: The model property that you want to translate.
Returns:
The model property that was translated from datastore.
"""
blob_info = super(BlobReferenceProperty,
self).get_value_for_datastore(model_instance)
if blob_info is None:
return None
return blob_info.key()
[docs] def make_value_from_datastore(self, value):
"""Returns a datastore value to `BlobInfo`.
Args:
value: The datastore value that you want to translate.
Returns:
A `BlobInfo` that was translated from datastore.
"""
if value is None:
return None
return BlobInfo(value)
[docs] def validate(self, value):
"""Validates that an assigned value is `BlobInfo`.
This method automatically converts from strings and `BlobKey` instances.
Args:
value: The value that you want to validate.
Returns:
Information about whether an assigned value is `BlobInfo`.
"""
if isinstance(value, (basestring)):
value = BlobInfo(BlobKey(value))
elif isinstance(value, BlobKey):
value = BlobInfo(value)
return super(BlobReferenceProperty, self).validate(value)
[docs]def fetch_data(blob, start_index, end_index, rpc=None):
"""Fetches data for a 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 that is entirely outside
the boundaries of the blob will return an empty string. Attempting to fetch a
negative index will raise an exception.
Args:
blob: A `BlobInfo`, `BlobKey`, string, or Unicode representation of
the `BlobKey` of the blob from which you want to fetch data.
start_index: The start index of blob data to fetch. This value must not be
negative.
end_index: The end index (inclusive) of the blob data to fetch. This value
must be greater than or equal to `start_index`.
rpc: Optional UserRPC object.
Returns:
A string that contains partial data of a blob. If the indexes are legal but
outside of the boundaries of the blob, an empty string is returned.
Raises:
TypeError: If `start_index` or `end_index` are not indexes, or if `blob` is
not a string, `BlobKey` or `BlobInfo`.
DataIndexOutOfRangeError: If `start_index` is set to a value that is less
than 0 or `end_index` is less than `start_index`.
BlobFetchSizeTooLargeError: If the requested blob fragment is larger than
`MAX_BLOB_FETCH_SIZE`.
BlobNotFoundError: If the blob does not exist.
"""
rpc = fetch_data_async(blob, start_index, end_index, rpc=rpc)
return rpc.get_result()
[docs]def fetch_data_async(blob, start_index, end_index, rpc=None):
"""Asynchronously fetches data for a 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 that is entirely outside
the boundaries of the blob will return an empty string. Attempting to fetch a
negative index will raise an exception.
Args:
blob: A `BlobInfo`, `BlobKey`, string, or Unicode representation of
the `BlobKey` of the blob from which you want to fetch data.
start_index: The start index of blob data to fetch. This value must not be
negative.
end_index: The end index (inclusive) of the blob data to fetch. This value
must be greater than or equal to `start_index`.
rpc: Optional UserRPC object.
Returns:
A UserRPC whose result will be a string as returned by `fetch_data()`.
Raises:
TypeError: If `start_index` or `end_index` are not indexes, or if `blob` is
not a string, `BlobKey` or `BlobInfo`.
DataIndexOutOfRangeError: If `start_index` is set to a value that is less
than 0 or `end_index` is less than `start_index` when calling
`rpc.get_result()`.
BlobFetchSizeTooLargeError: If the requested blob fragment is larger than
`MAX_BLOB_FETCH_SIZE` when calling `rpc.get_result()`.
BlobNotFoundError: If the blob does not exist when calling
`rpc.get_result()`.
"""
if isinstance(blob, BlobInfo):
blob = blob.key()
return blobstore.fetch_data_async(blob, start_index, end_index, rpc=rpc)
[docs]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 `BlobKey`, `BlobInfo`, 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):
"""Retrieves a file iterator for this `BlobReader`.
Returns:
The file iterator for the `BlobReader`.
"""
return self
def __getstate__(self):
"""Retrieves the serialized state for this `BlobReader`.
Returns:
The serialized state for the `BlobReader`.
"""
return (self.__blob_key, self.__buffer_size, self.__position)
def __setstate__(self, state):
"""Restores pickled state for this BlobReader."""
self.__init__(*state)
[docs] def close(self):
"""Closes the file.
A closed file cannot be read or written to anymore. An operation that
requires that the file to be open will raise a `ValueError` after the file
has been closed. Calling `close()` more than once is allowed.
"""
self.__blob_key = None
[docs] def flush(self):
raise IOError("BlobReaders are read-only")
[docs] def next(self):
r"""Returns the next line from the file.
Returns:
A string, terminated by `\\n`. The last line cannot end with `\\n`. If the
end of the file is reached, an empty string will be returned.
Raises:
StopIteration: If there are no further lines to read.
"""
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 a negative value to read the entire
buffer.
Returns:
Tuple (data, size):
data: The bytes read from the buffer.
size: The remaining unread byte count. This value is negative when
`size` is negative. Thus when remaining size is not equal to 0, the
calling method might 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
[docs] def read(self, size=-1):
"""Reads at most `size` bytes from the file.
Fewer bytes are read if the read hits the end of the file before obtaining
`size` bytes. If the `size` argument is negative or omitted, all data is
read until the end of the file is reached. The bytes are returned as a
string object. An empty string is returned immediately when the end of the
file is reached.
Calling `read()` without specifying a `size` is likely to be dangerous, as
it might 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)
[docs] def readline(self, size=-1):
"""Reads one entire line from the file.
A trailing newline character is kept in the string but can be absent when a
file ends with an incomplete line. If the `size` argument is present and
non-negative, it represents a maximum byte count, including the trailing
newline and an incomplete line might be returned. An empty string is
returned immediately only when the end of the file is reached.
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)
[docs] def readlines(self, sizehint=None):
"""Reads until the end of the file using `readline()`.
A list of lines read is returned.
If the optional `sizehint` argument is present, instead of reading up to the
end of the file, whole lines totalling approximately `sizehint` bytes are
read, possibly after rounding up to an internal buffer size.
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
[docs] def seek(self, offset, whence=SEEK_SET):
"""Sets the file's current position, like stdio's `fseek()`_.
Args:
offset: The relative offset to seek to.
whence: Optional; defines to what value the offset is relative. This
argument defaults to `os.SEEK_SET` or `0` to use absolute file
positioning; other valid values are `os.SEEK_CUR` or `1` to seek
relative to the current position and `os.SEEK_END` or `2` to seek
relative to the end of the file.
.. _fseek():
http://www.cplusplus.com/reference/cstdio/fseek/
"""
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
[docs] def tell(self):
"""Retrieves the file's current position, like stdio's `ftell()`_.
Returns:
The file's current position.
.. _ftell():
http://www.cplusplus.com/reference/cstdio/ftell/
"""
return self.__position
[docs] def truncate(self, size):
"""Raises an error if you attempt to truncate the file.
Args:
size: The size that you are attempting to truncate to.
Raises:
IOError: If you attempt to truncate a file in `BlobReader`.
"""
raise IOError("BlobReaders are read-only")
[docs] def write(self, str):
"""Raises an error if you attempt to write to the file.
Args:
str: The string that you are attempting to write.
Raises:
IOError: If you attempt to write to a file in `BlobReader`.
"""
raise IOError("BlobReaders are read-only")
[docs] def writelines(self, sequence):
"""Raises an error if you attempt to write to the file.
Args:
sequence: The sequence of strings that you are attempting to write.
Raises:
IOError: If you attempt to write lines to a file in `BlobReader`.
"""
raise IOError("BlobReaders are read-only")
@property
def blob_info(self):
"""Returns the `BlobInfo` for this file.
Returns:
A string that contains the `BlobInfo`.
"""
if not self.__blob_info:
self.__blob_info = BlobInfo.get(self.__blob_key)
return self.__blob_info
@property
def closed(self):
"""Determines whether a file is closed.
Returns:
`True` if this file is closed; opened files return `False`.
"""
return self.__blob_key is None
def __enter__(self):
"""Attempts to open the file.
Returns:
The open file.
"""
return self
def __exit__(self, exc_type, exc_value, traceback):
"""Closes the file."""
self.close()
[docs]class BlobMigrationRecord(db.Model):
"""Defines a model that records the result of a blob migration."""
new_blob_ref = BlobReferenceProperty(indexed=False, name='new_blob_key')
[docs] @classmethod
def kind(cls):
"""Specifies the kind of blob that you are migrating.
Returns:
The kind of blob that you are migrating
"""
return blobstore.BLOB_MIGRATION_KIND
[docs] @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 that was used in the previous app.
Returns:
A instance of `blobstore.BlobMigrationRecord` or `None`.
"""
return cls.get_by_key_name(str(old_blob_key))
[docs] @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()