diff --git a/gcloud/storage/bucket.py b/gcloud/storage/bucket.py index 4d553a45c6ca..7dac8d91cbeb 100644 --- a/gcloud/storage/bucket.py +++ b/gcloud/storage/bucket.py @@ -400,7 +400,6 @@ def etag(self): https://cloud.google.com/storage/docs/json_api/v1/buckets :rtype: string - :returns: a unique identifier for the bucket and current metadata. """ return self.properties['etag'] @@ -411,7 +410,6 @@ def id(self): See: https://cloud.google.com/storage/docs/json_api/v1/buckets :rtype: string - :returns: a unique identifier for the bucket. """ return self.properties['id'] @@ -501,8 +499,6 @@ def metageneration(self): See: https://cloud.google.com/storage/docs/json_api/v1/buckets :rtype: integer - :returns: count of times since creation the bucket's metadata has - been updated. """ return self.properties['metageneration'] @@ -524,7 +520,6 @@ def project_number(self): See: https://cloud.google.com/storage/docs/json_api/v1/buckets :rtype: integer - :returns: a unique identifier for the bucket. """ return self.properties['projectNumber'] @@ -535,7 +530,6 @@ def self_link(self): See: https://cloud.google.com/storage/docs/json_api/v1/buckets :rtype: string - :returns: URI of the bucket. """ return self.properties['selfLink'] @@ -547,8 +541,7 @@ def storage_class(self): https://cloud.google.com/storage/docs/durable-reduced-availability :rtype: string - :returns: the storage class for the bucket (currently one of - ``STANDARD``, ``DURABLE_REDUCED_AVAILABILITY``) + :returns: Currently one of "STANDARD", "DURABLE_REDUCED_AVAILABILITY" """ return self.properties['storageClass'] @@ -559,7 +552,7 @@ def time_created(self): See: https://cloud.google.com/storage/docs/json_api/v1/buckets :rtype: string - :returns: timestamp for the bucket's creation, in RFC 3339 format. + :returns: timestamp in RFC 3339 format. """ return self.properties['timeCreated'] diff --git a/gcloud/storage/key.py b/gcloud/storage/key.py index 1c09d5c7fef1..86d018e66b11 100644 --- a/gcloud/storage/key.py +++ b/gcloud/storage/key.py @@ -1,5 +1,6 @@ """Create / interact with gcloud storage keys.""" +import copy import mimetypes import os from StringIO import StringIO @@ -10,11 +11,43 @@ from gcloud.storage.iterator import Iterator +def _scalar_property(fieldname): + """Create a property descriptor around the :class:`_PropertyMixin` helpers. + """ + def _getter(self): + """Scalar property getter.""" + return self.properties[fieldname] + + def _setter(self, value): + """Scalar property setter.""" + self._patch_properties({fieldname: value}) + + return property(_getter, _setter) + + class Key(_PropertyMixin): """A wrapper around Cloud Storage's concept of an ``Object``.""" CUSTOM_PROPERTY_ACCESSORS = { 'acl': 'get_acl()', + 'cacheControl': 'cache_control', + 'contentDisposition': 'content_disposition', + 'contentEncoding': 'content_encoding', + 'contentLanguage': 'content_language', + 'contentType': 'content_type', + 'componentCount': 'component_count', + 'etag': 'etag', + 'generation': 'generation', + 'id': 'id', + 'mediaLink': 'media_link', + 'metageneration': 'metageneration', + 'name': 'name', + 'owner': 'owner', + 'selfLink': 'self_link', + 'size': 'size', + 'storageClass': 'storage_class', + 'timeDeleted': 'time_deleted', + 'updated': 'updated', } """Map field name -> accessor for fields w/ custom accessors.""" @@ -359,6 +392,216 @@ def make_public(self): self.acl.save() return self + cache_control = _scalar_property('cacheControl') + """HTTP 'Cache-Control' header for this object. + + See: https://tools.ietf.org/html/rfc7234#section-5.2 and + https://cloud.google.com/storage/docs/json_api/v1/objects + + :rtype: string + """ + + content_disposition = _scalar_property('contentDisposition') + """HTTP 'Content-Disposition' header for this object. + + See: https://tools.ietf.org/html/rfc6266 and + https://cloud.google.com/storage/docs/json_api/v1/objects + + :rtype: string + """ + + content_encoding = _scalar_property('contentEncoding') + """HTTP 'Content-Encoding' header for this object. + + See: https://tools.ietf.org/html/rfc7231#section-3.1.2.2 and + https://cloud.google.com/storage/docs/json_api/v1/objects + + :rtype: string + """ + + content_language = _scalar_property('contentLanguage') + """HTTP 'Content-Language' header for this object. + + See: http://tools.ietf.org/html/bcp47 and + https://cloud.google.com/storage/docs/json_api/v1/objects + + :rtype: string + """ + + content_type = _scalar_property('contentType') + """HTTP 'Content-Type' header for this object. + + See: https://tools.ietf.org/html/rfc2616#section-14.17 and + https://cloud.google.com/storage/docs/json_api/v1/objects + + :rtype: string + """ + + crc32c = _scalar_property('crc32c') + """CRC32C checksum for this object. + + See: http://tools.ietf.org/html/rfc4960#appendix-B and + https://cloud.google.com/storage/docs/json_api/v1/objects + + :rtype: string + """ + + @property + def component_count(self): + """Number of underlying components that make up this object. + + See: https://cloud.google.com/storage/docs/json_api/v1/objects + + :rtype: integer + """ + return self.properties['componentCount'] + + @property + def etag(self): + """Retrieve the ETag for the object. + + See: http://tools.ietf.org/html/rfc2616#section-3.11 and + https://cloud.google.com/storage/docs/json_api/v1/objects + + :rtype: string + """ + return self.properties['etag'] + + @property + def generation(self): + """Retrieve the generation for the object. + + See: https://cloud.google.com/storage/docs/json_api/v1/objects + + :rtype: integer + """ + return self.properties['generation'] + + @property + def id(self): + """Retrieve the ID for the object. + + See: https://cloud.google.com/storage/docs/json_api/v1/objects + + :rtype: string + """ + return self.properties['id'] + + md5_hash = _scalar_property('md5Hash') + """MD5 hash for this object. + + See: http://tools.ietf.org/html/rfc4960#appendix-B and + https://cloud.google.com/storage/docs/json_api/v1/objects + + :rtype: string + """ + + @property + def media_link(self): + """Retrieve the media download URI for the object. + + See: https://cloud.google.com/storage/docs/json_api/v1/objects + + :rtype: string + """ + return self.properties['mediaLink'] + + @property + def metadata(self): + """Retrieve arbitrary/application specific metadata for the object. + + See: https://cloud.google.com/storage/docs/json_api/v1/objects + + :rtype: dict + """ + return copy.deepcopy(self.properties['metadata']) + + @metadata.setter + def metadata(self, value): + """Update arbitrary/application specific metadata for the object. + + See: https://cloud.google.com/storage/docs/json_api/v1/objects + + :type value: dict + """ + self._patch_properties({'metadata': value}) + + @property + def metageneration(self): + """Retrieve the metageneration for the object. + + See: https://cloud.google.com/storage/docs/json_api/v1/objects + + :rtype: integer + """ + return self.properties['metageneration'] + + @property + def owner(self): + """Retrieve info about the owner of the object. + + See: https://cloud.google.com/storage/docs/json_api/v1/objects + + :rtype: dict + :returns: mapping of owner's role/ID. + """ + return self.properties['owner'].copy() + + @property + def self_link(self): + """Retrieve the URI for the object. + + See: https://cloud.google.com/storage/docs/json_api/v1/objects + + :rtype: string + """ + return self.properties['selfLink'] + + @property + def size(self): + """Size of the object, in bytes. + + See: https://cloud.google.com/storage/docs/json_api/v1/objects + + :rtype: integer + """ + return self.properties['size'] + + @property + def storage_class(self): + """Retrieve the storage class for the object. + + See: https://cloud.google.com/storage/docs/json_api/v1/objects and + https://cloud.google.com/storage/docs/durable-reduced-availability#_DRA_Bucket + + :rtype: string + :returns: Currently one of "STANDARD", "DURABLE_REDUCED_AVAILABILITY" + """ + return self.properties['storageClass'] + + @property + def time_deleted(self): + """Retrieve the timestamp at which the object was deleted. + + See: https://cloud.google.com/storage/docs/json_api/v1/objects + + :rtype: string or None + :returns: timestamp in RFC 3339 format, or None if the object + has a "live" version. + """ + return self.properties.get('timeDeleted') + + @property + def updated(self): + """Retrieve the timestamp at which the object was updated. + + See: https://cloud.google.com/storage/docs/json_api/v1/objects + + :rtype: string + :returns: timestamp in RFC 3339 format. + """ + return self.properties['updated'] + class _KeyIterator(Iterator): """An iterator listing keys. diff --git a/gcloud/storage/test_key.py b/gcloud/storage/test_key.py index 7fdeb2733efe..be6e4adbabc7 100644 --- a/gcloud/storage/test_key.py +++ b/gcloud/storage/test_key.py @@ -344,6 +344,323 @@ def test_make_public(self): self.assertEqual(kw[0]['data'], {'acl': permissive}) self.assertEqual(kw[0]['query_params'], {'projection': 'full'}) + def test_cache_control_getter(self): + KEY = 'key' + connection = _Connection() + bucket = _Bucket(connection) + CACHE_CONTROL = 'no-cache' + properties = {'cacheControl': CACHE_CONTROL} + key = self._makeOne(bucket, KEY, properties) + self.assertEqual(key.cache_control, CACHE_CONTROL) + + def test_cache_control_setter(self): + KEY = 'key' + CACHE_CONTROL = 'no-cache' + after = {'cacheControl': CACHE_CONTROL} + connection = _Connection(after) + bucket = _Bucket(connection) + key = self._makeOne(bucket, KEY) + key.cache_control = CACHE_CONTROL + self.assertEqual(key.cache_control, CACHE_CONTROL) + kw = connection._requested + self.assertEqual(len(kw), 1) + self.assertEqual(kw[0]['method'], 'PATCH') + self.assertEqual(kw[0]['path'], '/b/name/o/%s' % KEY) + self.assertEqual(kw[0]['data'], {'cacheControl': CACHE_CONTROL}) + self.assertEqual(kw[0]['query_params'], {'projection': 'full'}) + + def test_component_count(self): + KEY = 'key' + connection = _Connection() + bucket = _Bucket(connection) + COMPONENT_COUNT = 42 + properties = {'componentCount': COMPONENT_COUNT} + key = self._makeOne(bucket, KEY, properties) + self.assertEqual(key.component_count, COMPONENT_COUNT) + + def test_content_disposition_getter(self): + KEY = 'key' + connection = _Connection() + bucket = _Bucket(connection) + CONTENT_DISPOSITION = 'Attachment; filename=example.jpg' + properties = {'contentDisposition': CONTENT_DISPOSITION} + key = self._makeOne(bucket, KEY, properties) + self.assertEqual(key.content_disposition, CONTENT_DISPOSITION) + + def test_content_disposition_setter(self): + KEY = 'key' + CONTENT_DISPOSITION = 'Attachment; filename=example.jpg' + after = {'contentDisposition': CONTENT_DISPOSITION} + connection = _Connection(after) + bucket = _Bucket(connection) + key = self._makeOne(bucket, KEY) + key.content_disposition = CONTENT_DISPOSITION + self.assertEqual(key.content_disposition, CONTENT_DISPOSITION) + kw = connection._requested + self.assertEqual(len(kw), 1) + self.assertEqual(kw[0]['method'], 'PATCH') + self.assertEqual(kw[0]['path'], '/b/name/o/%s' % KEY) + self.assertEqual(kw[0]['data'], + {'contentDisposition': CONTENT_DISPOSITION}) + self.assertEqual(kw[0]['query_params'], {'projection': 'full'}) + + def test_content_encoding_getter(self): + KEY = 'key' + connection = _Connection() + bucket = _Bucket(connection) + CONTENT_ENCODING = 'gzip' + properties = {'contentEncoding': CONTENT_ENCODING} + key = self._makeOne(bucket, KEY, properties) + self.assertEqual(key.content_encoding, CONTENT_ENCODING) + + def test_content_encoding_setter(self): + KEY = 'key' + CONTENT_ENCODING = 'gzip' + after = {'contentEncoding': CONTENT_ENCODING} + connection = _Connection(after) + bucket = _Bucket(connection) + key = self._makeOne(bucket, KEY) + key.content_encoding = CONTENT_ENCODING + self.assertEqual(key.content_encoding, CONTENT_ENCODING) + kw = connection._requested + self.assertEqual(len(kw), 1) + self.assertEqual(kw[0]['method'], 'PATCH') + self.assertEqual(kw[0]['path'], '/b/name/o/%s' % KEY) + self.assertEqual(kw[0]['data'], + {'contentEncoding': CONTENT_ENCODING}) + self.assertEqual(kw[0]['query_params'], {'projection': 'full'}) + + def test_content_language_getter(self): + KEY = 'key' + connection = _Connection() + bucket = _Bucket(connection) + CONTENT_LANGUAGE = 'pt-BR' + properties = {'contentLanguage': CONTENT_LANGUAGE} + key = self._makeOne(bucket, KEY, properties) + self.assertEqual(key.content_language, CONTENT_LANGUAGE) + + def test_content_language_setter(self): + KEY = 'key' + CONTENT_LANGUAGE = 'pt-BR' + after = {'contentLanguage': CONTENT_LANGUAGE} + connection = _Connection(after) + bucket = _Bucket(connection) + key = self._makeOne(bucket, KEY) + key.content_language = CONTENT_LANGUAGE + self.assertEqual(key.content_language, CONTENT_LANGUAGE) + kw = connection._requested + self.assertEqual(len(kw), 1) + self.assertEqual(kw[0]['method'], 'PATCH') + self.assertEqual(kw[0]['path'], '/b/name/o/%s' % KEY) + self.assertEqual(kw[0]['data'], + {'contentLanguage': CONTENT_LANGUAGE}) + self.assertEqual(kw[0]['query_params'], {'projection': 'full'}) + + def test_content_type_getter(self): + KEY = 'key' + connection = _Connection() + bucket = _Bucket(connection) + CONTENT_TYPE = 'image/jpeg' + properties = {'contentType': CONTENT_TYPE} + key = self._makeOne(bucket, KEY, properties) + self.assertEqual(key.content_type, CONTENT_TYPE) + + def test_content_type_setter(self): + KEY = 'key' + CONTENT_TYPE = 'image/jpeg' + after = {'contentType': CONTENT_TYPE} + connection = _Connection(after) + bucket = _Bucket(connection) + key = self._makeOne(bucket, KEY) + key.content_type = CONTENT_TYPE + self.assertEqual(key.content_type, CONTENT_TYPE) + kw = connection._requested + self.assertEqual(len(kw), 1) + self.assertEqual(kw[0]['method'], 'PATCH') + self.assertEqual(kw[0]['path'], '/b/name/o/%s' % KEY) + self.assertEqual(kw[0]['data'], + {'contentType': CONTENT_TYPE}) + self.assertEqual(kw[0]['query_params'], {'projection': 'full'}) + + def test_crc32c_getter(self): + KEY = 'key' + connection = _Connection() + bucket = _Bucket(connection) + CRC32C = 'DEADBEEF' + properties = {'crc32c': CRC32C} + key = self._makeOne(bucket, KEY, properties) + self.assertEqual(key.crc32c, CRC32C) + + def test_crc32c_setter(self): + KEY = 'key' + CRC32C = 'DEADBEEF' + after = {'crc32c': CRC32C} + connection = _Connection(after) + bucket = _Bucket(connection) + key = self._makeOne(bucket, KEY) + key.crc32c = CRC32C + self.assertEqual(key.crc32c, CRC32C) + kw = connection._requested + self.assertEqual(len(kw), 1) + self.assertEqual(kw[0]['method'], 'PATCH') + self.assertEqual(kw[0]['path'], '/b/name/o/%s' % KEY) + self.assertEqual(kw[0]['data'], + {'crc32c': CRC32C}) + self.assertEqual(kw[0]['query_params'], {'projection': 'full'}) + + def test_etag(self): + KEY = 'key' + connection = _Connection() + bucket = _Bucket(connection) + ETAG = 'ETAG' + properties = {'etag': ETAG} + key = self._makeOne(bucket, KEY, properties) + self.assertEqual(key.etag, ETAG) + + def test_generation(self): + KEY = 'key' + connection = _Connection() + bucket = _Bucket(connection) + GENERATION = 42 + properties = {'generation': GENERATION} + key = self._makeOne(bucket, KEY, properties) + self.assertEqual(key.generation, GENERATION) + + def test_id(self): + KEY = 'key' + connection = _Connection() + bucket = _Bucket(connection) + ID = 'ID' + properties = {'id': ID} + key = self._makeOne(bucket, KEY, properties) + self.assertEqual(key.id, ID) + + def test_md5_hash_getter(self): + KEY = 'key' + connection = _Connection() + bucket = _Bucket(connection) + MD5_HASH = 'DEADBEEF' + properties = {'md5Hash': MD5_HASH} + key = self._makeOne(bucket, KEY, properties) + self.assertEqual(key.md5_hash, MD5_HASH) + + def test_md5_hash_setter(self): + KEY = 'key' + MD5_HASH = 'DEADBEEF' + after = {'md5Hash': MD5_HASH} + connection = _Connection(after) + bucket = _Bucket(connection) + key = self._makeOne(bucket, KEY) + key.md5_hash = MD5_HASH + self.assertEqual(key.md5_hash, MD5_HASH) + kw = connection._requested + self.assertEqual(len(kw), 1) + self.assertEqual(kw[0]['method'], 'PATCH') + self.assertEqual(kw[0]['path'], '/b/name/o/%s' % KEY) + self.assertEqual(kw[0]['data'], + {'md5Hash': MD5_HASH}) + self.assertEqual(kw[0]['query_params'], {'projection': 'full'}) + + def test_media_link(self): + KEY = 'key' + connection = _Connection() + bucket = _Bucket(connection) + MEDIA_LINK = 'http://example.com/media/' + properties = {'mediaLink': MEDIA_LINK} + key = self._makeOne(bucket, KEY, properties) + self.assertEqual(key.media_link, MEDIA_LINK) + + def test_metadata_getter(self): + KEY = 'key' + connection = _Connection() + bucket = _Bucket(connection) + METADATA = {'foo': 'Foo'} + properties = {'metadata': METADATA} + key = self._makeOne(bucket, KEY, properties) + self.assertEqual(key.metadata, METADATA) + + def test_metadata_setter(self): + KEY = 'key' + METADATA = {'foo': 'Foo'} + after = {'metadata': METADATA} + connection = _Connection(after) + bucket = _Bucket(connection) + key = self._makeOne(bucket, KEY) + key.metadata = METADATA + self.assertEqual(key.metadata, METADATA) + kw = connection._requested + self.assertEqual(len(kw), 1) + self.assertEqual(kw[0]['method'], 'PATCH') + self.assertEqual(kw[0]['path'], '/b/name/o/%s' % KEY) + self.assertEqual(kw[0]['data'], + {'metadata': METADATA}) + self.assertEqual(kw[0]['query_params'], {'projection': 'full'}) + + def test_metageneration(self): + KEY = 'key' + connection = _Connection() + bucket = _Bucket(connection) + METAGENERATION = 42 + properties = {'metageneration': METAGENERATION} + key = self._makeOne(bucket, KEY, properties) + self.assertEqual(key.metageneration, METAGENERATION) + + def test_owner(self): + KEY = 'key' + connection = _Connection() + bucket = _Bucket(connection) + OWNER = {'entity': 'project-owner-12345', 'entityId': '23456'} + properties = {'owner': OWNER} + key = self._makeOne(bucket, KEY, properties) + owner = key.owner + self.assertEqual(owner['entity'], 'project-owner-12345') + self.assertEqual(owner['entityId'], '23456') + + def test_self_link(self): + KEY = 'key' + connection = _Connection() + bucket = _Bucket(connection) + SELF_LINK = 'http://example.com/self/' + properties = {'selfLink': SELF_LINK} + key = self._makeOne(bucket, KEY, properties) + self.assertEqual(key.self_link, SELF_LINK) + + def test_size(self): + KEY = 'key' + connection = _Connection() + bucket = _Bucket(connection) + SIZE = 42 + properties = {'size': SIZE} + key = self._makeOne(bucket, KEY, properties) + self.assertEqual(key.size, SIZE) + + def test_storage_class(self): + KEY = 'key' + connection = _Connection() + bucket = _Bucket(connection) + STORAGE_CLASS = 'http://example.com/self/' + properties = {'storageClass': STORAGE_CLASS} + key = self._makeOne(bucket, KEY, properties) + self.assertEqual(key.storage_class, STORAGE_CLASS) + + def test_time_deleted(self): + KEY = 'key' + connection = _Connection() + bucket = _Bucket(connection) + TIME_DELETED = '2014-11-05T20:34:37Z' + properties = {'timeDeleted': TIME_DELETED} + key = self._makeOne(bucket, KEY, properties) + self.assertEqual(key.time_deleted, TIME_DELETED) + + def test_updated(self): + KEY = 'key' + connection = _Connection() + bucket = _Bucket(connection) + UPDATED = '2014-11-05T20:34:37Z' + properties = {'updated': UPDATED} + key = self._makeOne(bucket, KEY, properties) + self.assertEqual(key.updated, UPDATED) + class Test__KeyIterator(unittest2.TestCase):