Skip to content

Commit b25cfb6

Browse files
committed
Refactoring duplicate code between storage.key and storage.bucket.
1 parent abe3d7c commit b25cfb6

5 files changed

Lines changed: 241 additions & 188 deletions

File tree

gcloud/storage/_helpers.py

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
"""Helper functions for Cloud Storage utility classes.
2+
3+
These are *not* part of the API.
4+
"""
5+
6+
7+
class _MetadataMixin(object):
8+
"""Abstract mixin for cloud storage classes with associated metadata.
9+
10+
Non-abstract subclasses should implement:
11+
- METADATA_ACL_FIELDS
12+
- ACL_CLASS
13+
- connection
14+
- path
15+
"""
16+
17+
METADATA_ACL_FIELDS = None
18+
"""Tuple of fields which pertain to metadata.
19+
20+
Expected to be set by subclasses. Fields in this tuple will cause
21+
`get_metadata()` to raise a KeyError with a message to use get_acl()
22+
methods.
23+
"""
24+
25+
ACL_CLASS = type(None)
26+
"""Class which holds ACL data for a given type.
27+
28+
Expected to be set by subclasses.
29+
"""
30+
31+
def __init__(self, name=None, metadata=None):
32+
"""_MetadataMixin constructor.
33+
34+
:type name: string
35+
:param name: The name of the object.
36+
37+
:type metadata: dict
38+
:param metadata: All the other data provided by Cloud Storage.
39+
"""
40+
self.name = name
41+
self.metadata = metadata
42+
43+
@property
44+
def connection(self):
45+
"""Abstract getter for the connection to use."""
46+
raise NotImplementedError
47+
48+
@property
49+
def path(self):
50+
"""Abstract getter for the object path."""
51+
raise NotImplementedError
52+
53+
def has_metadata(self, field=None):
54+
"""Check if metadata is available.
55+
56+
:type field: string
57+
:param field: (optional) the particular field to check for.
58+
59+
:rtype: bool
60+
:returns: Whether metadata is available locally.
61+
"""
62+
if not self.metadata:
63+
return False
64+
elif field and field not in self.metadata:
65+
return False
66+
else:
67+
return True
68+
69+
def reload_metadata(self):
70+
"""Reload metadata from Cloud Storage.
71+
72+
:rtype: :class:`_MetadataMixin`
73+
:returns: The object you just reloaded data for.
74+
"""
75+
# Pass only '?projection=noAcl' here because 'acl' and related
76+
# are handled via 'get_acl()' etc.
77+
query_params = {'projection': 'noAcl'}
78+
self.metadata = self.connection.api_request(
79+
method='GET', path=self.path, query_params=query_params)
80+
return self
81+
82+
def get_metadata(self, field=None, default=None):
83+
"""Get all metadata or a specific field.
84+
85+
If you request a field that isn't available, and that field can
86+
be retrieved by refreshing data from Cloud Storage, this method
87+
will reload the data using :func:`_MetadataMixin.reload_metadata`.
88+
89+
:type field: string
90+
:param field: (optional) A particular field to retrieve from metadata.
91+
92+
:type default: anything
93+
:param default: The value to return if the field provided wasn't found.
94+
95+
:rtype: dict or anything
96+
:returns: All metadata or the value of the specific field.
97+
98+
:raises: :class:`KeyError` if the field is in METADATA_ACL_FIELDS.
99+
"""
100+
# We ignore 'acl' and related fields because they are meant to be
101+
# handled via 'get_acl()' and related methods.
102+
if field in self.METADATA_ACL_FIELDS:
103+
raise KeyError((field, 'Use get_acl() or related methods instead.'))
104+
105+
if not self.has_metadata(field=field):
106+
self.reload_metadata()
107+
108+
if field:
109+
return self.metadata.get(field, default)
110+
else:
111+
return self.metadata
112+
113+
def patch_metadata(self, metadata):
114+
"""Update particular fields of this object's metadata.
115+
116+
This method will only update the fields provided and will not
117+
touch the other fields.
118+
119+
It will also reload the metadata locally based on the server's
120+
response.
121+
122+
:type metadata: dict
123+
:param metadata: The dictionary of values to update.
124+
125+
:rtype: :class:`_MetadataMixin`
126+
:returns: The current object.
127+
"""
128+
self.metadata = self.connection.api_request(
129+
method='PATCH', path=self.path, data=metadata,
130+
query_params={'projection': 'full'})
131+
return self
132+
133+
134+
class _ACLMetadataMixin(_MetadataMixin):
135+
"""Abstract mixin for cloud storage classes with metadata and ACLs.
136+
137+
Expected to be subclassed by :class:`gcloud.storage.bucket.Bucket`
138+
and :class:`gcloud.storage.key.Key`.
139+
140+
Non-abstract subclasses should implement:
141+
- METADATA_ACL_FIELDS
142+
- ACL_CLASS
143+
- connection
144+
- path
145+
"""
146+
147+
@property
148+
def connection(self):
149+
"""Abstract getter for the connection to use."""
150+
raise NotImplementedError
151+
152+
@property
153+
def path(self):
154+
"""Abstract getter for the object path."""
155+
raise NotImplementedError
156+
157+
def get_acl(self):
158+
"""Get ACL metadata as an object of type `ACL_CLASS`.
159+
160+
:returns: An ACL object for the current object.
161+
"""
162+
if not self.acl.loaded:
163+
self.acl.reload()
164+
return self.acl
165+
166+
def make_public(self):
167+
"""Make this object public giving all users read access.
168+
169+
:returns: The current object.
170+
"""
171+
self.get_acl().all().grant_read()
172+
self.acl.save()
173+
return self

gcloud/storage/acl.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,10 @@
4949
>>> acl.save()
5050
5151
You can alternatively save any existing :class:`gcloud.storage.acl.ACL`
52-
object (whether it was created by a factory method or not) with the
53-
:func:`gcloud.storage.bucket.Bucket.save_acl` method::
52+
object (whether it was created by a factory method or not) from a
53+
:class:`gcloud.storage.bucket.Bucket`::
5454
55-
>>> bucket.save_acl(acl)
55+
>>> bucket.acl.save(acl=acl)
5656
5757
To get the list of ``entity`` and ``role`` for each unique pair, the
5858
:class:`ACL` class is iterable::

gcloud/storage/bucket.py

Lines changed: 21 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import os
44

5+
from gcloud.storage._helpers import _ACLMetadataMixin
56
from gcloud.storage import exceptions
67
from gcloud.storage.acl import BucketACL
78
from gcloud.storage.acl import DefaultObjectACL
@@ -10,7 +11,7 @@
1011
from gcloud.storage.key import _KeyIterator
1112

1213

13-
class Bucket(object):
14+
class Bucket(_ACLMetadataMixin):
1415
"""A class representing a Bucket on Cloud Storage.
1516
1617
:type connection: :class:`gcloud.storage.connection.Connection`
@@ -19,13 +20,19 @@ class Bucket(object):
1920
:type name: string
2021
:param name: The name of the bucket.
2122
"""
23+
24+
METADATA_ACL_FIELDS = ('acl', 'defaultObjectAcl')
25+
"""Tuple of metadata fields pertaining to bucket ACLs."""
26+
27+
ACL_CLASS = BucketACL
28+
"""Class which holds ACL data for buckets."""
29+
2230
# ACL rules are lazily retrieved.
2331
_acl = _default_object_acl = None
2432

2533
def __init__(self, connection=None, name=None, metadata=None):
26-
self.connection = connection
27-
self.name = name
28-
self.metadata = metadata
34+
super(Bucket, self).__init__(name=name, metadata=metadata)
35+
self._connection = connection
2936

3037
@property
3138
def acl(self):
@@ -63,6 +70,15 @@ def __iter__(self):
6370
def __contains__(self, key):
6471
return self.get_key(key) is not None
6572

73+
@property
74+
def connection(self):
75+
"""Getter property for the connection to use with this Bucket.
76+
77+
:rtype: :class:`gcloud.storage.connection.Connection`
78+
:returns: The connection to use.
79+
"""
80+
return self._connection
81+
6682
@property
6783
def path(self):
6884
"""The URL path to this bucket."""
@@ -326,85 +342,6 @@ def upload_file_object(self, file_obj, key=None):
326342
key = self.new_key(os.path.basename(file_obj.name))
327343
return key.set_contents_from_file(file_obj)
328344

329-
def has_metadata(self, field=None):
330-
"""Check if metadata is available locally.
331-
332-
:type field: string
333-
:param field: (optional) the particular field to check for.
334-
335-
:rtype: bool
336-
:returns: Whether metadata is available locally.
337-
"""
338-
if not self.metadata:
339-
return False
340-
elif field and field not in self.metadata:
341-
return False
342-
else:
343-
return True
344-
345-
def reload_metadata(self):
346-
"""Reload metadata from Cloud Storage.
347-
348-
:rtype: :class:`Bucket`
349-
:returns: The bucket you just reloaded data for.
350-
"""
351-
# Pass only '?projection=noAcl' here because 'acl'/'defaultObjectAcl'
352-
# are handled via 'get_acl()'/'get_default_object_acl()'
353-
query_params = {'projection': 'noAcl'}
354-
self.metadata = self.connection.api_request(
355-
method='GET', path=self.path, query_params=query_params)
356-
return self
357-
358-
def get_metadata(self, field=None, default=None):
359-
"""Get all metadata or a specific field.
360-
361-
If you request a field that isn't available, and that field can
362-
be retrieved by refreshing data from Cloud Storage, this method
363-
will reload the data using :func:`Bucket.reload_metadata`.
364-
365-
:type field: string
366-
:param field: (optional) A particular field to retrieve from metadata.
367-
368-
:type default: anything
369-
:param default: The value to return if the field provided wasn't found.
370-
371-
:rtype: dict or anything
372-
:returns: All metadata or the value of the specific field.
373-
"""
374-
if field == 'acl':
375-
raise KeyError("Use 'get_acl()'")
376-
377-
if field == 'defaultObjectAcl':
378-
raise KeyError("Use 'get_default_object_acl()'")
379-
380-
if not self.has_metadata(field=field):
381-
self.reload_metadata()
382-
383-
if field:
384-
return self.metadata.get(field, default)
385-
else:
386-
return self.metadata
387-
388-
def patch_metadata(self, metadata):
389-
"""Update particular fields of this bucket's metadata.
390-
391-
This method will only update the fields provided and will not
392-
touch the other fields.
393-
394-
It will also reload the metadata locally based on the servers
395-
response.
396-
397-
:type metadata: dict
398-
:param metadata: The dictionary of values to update.
399-
400-
:rtype: :class:`Bucket`
401-
:returns: The current bucket.
402-
"""
403-
self.metadata = self.connection.api_request(
404-
method='PATCH', path=self.path, data=metadata,
405-
query_params={'projection': 'full'})
406-
return self
407-
408345
def configure_website(self, main_page_suffix=None, not_found_page=None):
409346
"""Configure website-related metadata.
410347
@@ -491,8 +428,7 @@ def make_public(self, recursive=False, future=False):
491428
:param future: If True, this will make all objects created in the
492429
future public as well.
493430
"""
494-
self.get_acl().all().grant_read()
495-
self.acl.save()
431+
super(Bucket, self).make_public()
496432

497433
if future:
498434
doa = self.get_default_object_acl()

0 commit comments

Comments
 (0)