From 214c67e50157bb414ae63359fde25149c6bb368a Mon Sep 17 00:00:00 2001 From: JJ Geewax Date: Thu, 6 Feb 2014 14:59:56 -0500 Subject: [PATCH] Added support to ACLs on Keys and various other additions. --- gcloud/storage/acl.py | 55 ++++++++--- gcloud/storage/bucket.py | 42 ++++++-- gcloud/storage/key.py | 205 ++++++++++++++++++++++++++++++++++----- 3 files changed, 257 insertions(+), 45 deletions(-) diff --git a/gcloud/storage/acl.py b/gcloud/storage/acl.py index 3349544f221e1..267dcb6a771ba 100644 --- a/gcloud/storage/acl.py +++ b/gcloud/storage/acl.py @@ -176,20 +176,14 @@ def revoke_owner(self): return self.revoke(ACL.Role.Owner) - def __init__(self, bucket): - """ - :type bucket: :class:`gcloud.storage.bucket.Bucket` - :param bucket: The bucket to which this ACL relates. - """ - - self.bucket = bucket + def __init__(self): self.entities = {} def __iter__(self): for entity in self.entities.itervalues(): for role in entity.get_roles(): if role: - yield {'entity': entity, 'role': role} + yield {'entity': str(entity), 'role': role} def entity_from_dict(self, entity_dict): """Build an ACL.Entity object from a dictionary of data. @@ -217,7 +211,7 @@ def entity_from_dict(self, entity_dict): elif entity == 'allAuthenticatedUsers': entity = self.all_authenticated() - if '-' in entity: + elif '-' in entity: type, identifier = entity.split('-', 1) entity = self.entity(type=type, identifier=identifier) @@ -351,16 +345,55 @@ def get_entities(self): return self.entities.values() + def save(self): + """A method to be overridden by subclasses. + + :raises: NotImplementedError + """ + + raise NotImplementedError + + +class BucketACL(ACL): + """An ACL specifically for a bucket.""" + + def __init__(self, bucket): + """ + :type bucket: :class:`gcloud.storage.bucket.Bucket` + :param bucket: The bucket to which this ACL relates. + """ + + super(BucketACL, self).__init__() + self.bucket = bucket + def save(self): """Save this ACL for the current bucket.""" return self.bucket.save_acl(acl=self) -class DefaultObjectACL(ACL): - """A subclass of ACL representing the default object ACL for a bucket.""" +class DefaultObjectACL(BucketACL): + """A subclass of BucketACL representing the default object ACL for a bucket.""" def save(self): """Save this ACL as the default object ACL for the current bucket.""" return self.bucket.save_default_object_acl(acl=self) + + +class ObjectACL(ACL): + """An ACL specifically for a key.""" + + def __init__(self, key): + """ + :type key: :class:`gcloud.storage.key.Key` + :param key: The key that this ACL corresponds to. + """ + + super(ObjectACL, self).__init__() + self.key = key + + def save(self): + """Save this ACL for the current key.""" + + return self.key.save_acl(acl=self) diff --git a/gcloud/storage/bucket.py b/gcloud/storage/bucket.py index ca5ba867c6467..9a1d21ae42cc3 100644 --- a/gcloud/storage/bucket.py +++ b/gcloud/storage/bucket.py @@ -1,5 +1,5 @@ from gcloud.storage import exceptions -from gcloud.storage.acl import ACL +from gcloud.storage.acl import BucketACL from gcloud.storage.acl import DefaultObjectACL from gcloud.storage.iterator import KeyIterator from gcloud.storage.key import Key @@ -247,8 +247,8 @@ def get_metadata(self, field=None, default=None): if field: return self.metadata.get(field, default) - - return self.metadata + else: + return self.metadata def patch_metadata(self, metadata): """Update particular fields of this bucket's metadata. @@ -301,8 +301,7 @@ def reload_acl(self): :returns: The current bucket. """ - self.acl = ACL(bucket=self) - acl = self.get_metadata('acl') + self.acl = BucketACL(bucket=self) for entry in self.get_metadata('acl', []): entity = self.acl.entity_from_dict(entry) @@ -312,9 +311,9 @@ def reload_acl(self): def get_acl(self): # TODO: This might be a VERY long list. Use the specific API endpoint. - """Get ACL metadata as a :class:`gcloud.storage.acl.ACL` object. + """Get ACL metadata as a :class:`gcloud.storage.acl.BucketACL` object. - :rtype: :class:`gcloud.storage.acl.ACL` + :rtype: :class:`gcloud.storage.acl.BucketACL` :returns: An ACL object for the current bucket. """ @@ -331,7 +330,10 @@ def save_acl(self, acl=None): set locally on the bucket. """ - acl = acl or self.acl + # We do things in this weird way because [] and None + # both evaluate to False, but mean very different things. + if acl is None: + acl = self.acl if acl is None: return self @@ -403,3 +405,27 @@ def clear_default_object_acl(self): """Remove the Default Object ACL from this bucket.""" return self.save_default_object_acl(acl=[]) + + def make_public(self, recursive=False, future=False): + """Make a bucket public. + + :type recursive: bool + :param recursive: If True, this will make all keys inside the bucket + public as well. + + :type future: bool + :param future: If True, this will make all objects created in the future + public as well. + """ + + self.get_acl().all().grant_read() + self.save_acl() + + if future: + self.get_default_object_acl().all().grant_read() + self.save_default_object_acl() + + if recursive: + for key in self: + key.get_acl().all().grant_read() + key.save_acl() diff --git a/gcloud/storage/key.py b/gcloud/storage/key.py index 5373b0d3dd48e..87232c407e59d 100644 --- a/gcloud/storage/key.py +++ b/gcloud/storage/key.py @@ -4,24 +4,12 @@ import os from StringIO import StringIO +from gcloud.storage.acl import ObjectACL from gcloud.storage.iterator import KeyDataIterator class Key(object): - """A wrapper around Cloud Storage's concept of an ``Object``. - - :type bucket: :class:`gcloud.storage.bucket.Bucket` - :param bucket: The bucket to which this key belongs. - - :type name: string - :param name: The name of the key. - This corresponds to the unique path of the object - in the bucket. - - :type extra_data: dict - :param extra_data: All the other data provided by Cloud Storage - in case we need to use it at some point. - """ + """A wrapper around Cloud Storage's concept of an ``Object``.""" CHUNK_SIZE = 1024 * 1024 # 1 MB. """The size of a chunk of data whenever iterating (1 MB). @@ -29,10 +17,26 @@ class Key(object): This must be a multiple of 256 KB per the API specification. """ - def __init__(self, bucket=None, name=None, object_data=None): + def __init__(self, bucket=None, name=None, metadata=None): + """ + :type bucket: :class:`gcloud.storage.bucket.Bucket` + :param bucket: The bucket to which this key belongs. + + :type name: string + :param name: The name of the key. + This corresponds to the unique path of the object + in the bucket. + + :type metadata: dict + :param metadata: All the other data provided by Cloud Storage. + """ + self.bucket = bucket self.name = name - self.object_data = object_data or {} + self.metadata = metadata or {} + + # Lazily get the ACL information. + self.acl = None @classmethod def from_dict(cls, key_dict, bucket=None): @@ -50,7 +54,7 @@ def from_dict(cls, key_dict, bucket=None): :returns: A key based on the data provided. """ - return cls(bucket=bucket, name=key_dict['name'], object_data=key_dict) + return cls(bucket=bucket, name=key_dict['name'], metadata=key_dict) def __repr__(self): if self.bucket: @@ -75,6 +79,11 @@ def path(self): return self.bucket.path + '/o/' + self.name + @property + def public_url(self): + return '{storage_base_url}/{self.bucket.name}/{self.name}'.format( + storage_base_url='http://commondatastorage.googleapis.com', self=self) + @property def connection(self): """Getter property for the connection to use with this Key. @@ -88,15 +97,6 @@ def connection(self): if self.bucket and self.bucket.connection: return self.bucket.connection - @property - def size(self): - return self.object_data.get('size') - - def reload_object_data(self): - response = self.connection.api_request(method='GET', path=self.path) - self.object_data = response - return self - def exists(self): """Determines whether or not this key exists. @@ -260,3 +260,156 @@ def set_contents_from_string(self, data, content_type='text/plain'): size=string_buffer.len, content_type=content_type) return self + + def has_metadata(self, field=None): + """Check if metadata is available locally. + + :type field: string + :param field: (optional) the particular field to check for. + + :rtype: bool + :returns: Whether metadata is available locally. + """ + + if not self.metadata: + return False + elif field and field not in self.metadata: + return False + else: + return True + + def reload_metadata(self, full=False): + """Reload metadata from Cloud Storage. + + :type full: bool + :param full: If True, loads all data (include ACL data). + + :rtype: :class:`Key` + :returns: The key you just reloaded data for. + """ + + projection = 'full' if full else 'noAcl' + query_params = {'projection': projection} + self.metadata = self.connection.api_request( + method='GET', path=self.path, query_params=query_params) + return self + + def get_metadata(self, field=None, default=None): + """Get all metadata or a specific field. + + If you request a field that isn't available, + and that field can be retrieved by refreshing data + from Cloud Storage, + this method will reload the data using + :func:`Key.reload_metadata`. + + :type field: string + :param field: (optional) A particular field to retrieve from metadata. + + :type default: anything + :param default: The value to return if the field provided wasn't found. + + :rtype: dict or anything + :returns: All metadata or the value of the specific field. + """ + + if not self.has_metadata(field=field): + full = (field and field == 'acl') + self.reload_metadata(full=full) + + if field: + return self.metadata.get(field, default) + else: + return self.metadata + + def patch_metadata(self, metadata): + """Update particular fields of this key's metadata. + + This method will only update the fields provided + and will not touch the other fields. + + It will also reload the metadata locally + based on the servers response. + + :type metadata: dict + :param metadata: The dictionary of values to update. + + :rtype: :class:`Key` + :returns: The current key. + """ + + self.metadata = self.connection.api_request( + method='PATCH', path=self.path, data=metadata, + query_params={'projection': 'full'}) + return self + + def reload_acl(self): + """Reload the ACL data from Cloud Storage. + + :rtype: :class:`Key` + :returns: The current key. + """ + + self.acl = ObjectACL(key=self) + + for entry in self.get_metadata('acl', []): + entity = self.acl.entity_from_dict(entry) + self.acl.add_entity(entity) + + return self + + def get_acl(self): + # TODO: This might be a VERY long list. Use the specific API endpoint. + """Get ACL metadata as a :class:`gcloud.storage.acl.ObjectACL` object. + + :rtype: :class:`gcloud.storage.acl.ObjectACL` + :returns: An ACL object for the current key. + """ + + if not self.acl: + self.reload_acl() + return self.acl + + def save_acl(self, acl=None): + """Save the ACL data for this key. + + :type acl: :class:`gcloud.storage.acl.ACL` + :param acl: The ACL object to save. + If left blank, this will save the ACL + set locally on the key. + """ + + # We do things in this weird way because [] and None + # both evaluate to False, but mean very different things. + if acl is None: + acl = self.acl + + if acl is None: + return self + + return self.patch_metadata({'acl': list(acl)}) + + def clear_acl(self): + """Remove all ACL rules from the key. + + Note that this won't actually remove *ALL* the rules, + but it will remove all the non-default rules. + In short, + you'll still have access + to a key that you created + even after you clear ACL rules + with this method. + """ + + return self.save_acl(acl=[]) + + def make_public(self): + """Make this key public giving all users read access. + + :rtype: :class:`Key` + :returns: The current key. + """ + + self.get_acl().all().grant_read() + self.save_acl() + return self