Skip to content
This repository has been archived by the owner on May 10, 2024. It is now read-only.

Commit

Permalink
Merge pull request #3635 from houglum/develop
Browse files Browse the repository at this point in the history
Add gs support for object-level storage class features.
  • Loading branch information
mfschwartz committed Oct 21, 2016
2 parents 5e176f5 + f4ad3df commit dc4bf34
Show file tree
Hide file tree
Showing 5 changed files with 119 additions and 82 deletions.
14 changes: 13 additions & 1 deletion boto/gs/bucket.py
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -44,11 +44,14 @@
STANDARD_ACL = 'acl' STANDARD_ACL = 'acl'
CORS_ARG = 'cors' CORS_ARG = 'cors'
LIFECYCLE_ARG = 'lifecycle' LIFECYCLE_ARG = 'lifecycle'
STORAGE_CLASS_ARG='storageClass'
ERROR_DETAILS_REGEX = re.compile(r'<Details>(?P<details>.*)</Details>') ERROR_DETAILS_REGEX = re.compile(r'<Details>(?P<details>.*)</Details>')


class Bucket(S3Bucket): class Bucket(S3Bucket):
"""Represents a Google Cloud Storage bucket.""" """Represents a Google Cloud Storage bucket."""


StorageClassBody = ('<?xml version="1.0" encoding="UTF-8"?>\n'
'<StorageClass>%s</StorageClass>')
VersioningBody = ('<?xml version="1.0" encoding="UTF-8"?>\n' VersioningBody = ('<?xml version="1.0" encoding="UTF-8"?>\n'
'<VersioningConfiguration><Status>%s</Status>' '<VersioningConfiguration><Status>%s</Status>'
'</VersioningConfiguration>') '</VersioningConfiguration>')
Expand Down Expand Up @@ -599,7 +602,7 @@ def get_storage_class(self):
:return: The StorageClass for the bucket. :return: The StorageClass for the bucket.
""" """
response = self.connection.make_request('GET', self.name, response = self.connection.make_request('GET', self.name,
query_args='storageClass') query_args=STORAGE_CLASS_ARG)
body = response.read() body = response.read()
if response.status == 200: if response.status == 200:
rs = ResultSet(self) rs = ResultSet(self)
Expand All @@ -610,6 +613,15 @@ def get_storage_class(self):
raise self.connection.provider.storage_response_error( raise self.connection.provider.storage_response_error(
response.status, response.reason, body) response.status, response.reason, body)


def set_storage_class(self, storage_class, headers=None):
"""
Sets a bucket's storage class.
:param str storage_class: A string containing the storage class.
:param dict headers: Additional headers to send with the request.
"""
req_body = self.StorageClassBody % (get_utf8_value(storage_class))
self.set_subresource(STORAGE_CLASS_ARG, req_body, headers=headers)


# Method with same signature as boto.s3.bucket.Bucket.add_email_grant(), # Method with same signature as boto.s3.bucket.Bucket.add_email_grant(),
# to allow polymorphic treatment at application layer. # to allow polymorphic treatment at application layer.
Expand Down
2 changes: 2 additions & 0 deletions boto/gs/key.py
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -131,6 +131,8 @@ def handle_addl_headers(self, headers):
self.content_encoding = value self.content_encoding = value
elif key == 'x-goog-stored-content-length': elif key == 'x-goog-stored-content-length':
self.size = int(value) self.size = int(value)
elif key == 'x-goog-storage-class':
self.storage_class = value


def open_read(self, headers=None, query_args='', def open_read(self, headers=None, query_args='',
override_num_retries=None, response_headers=None): override_num_retries=None, response_headers=None):
Expand Down
137 changes: 67 additions & 70 deletions boto/gs/lifecycle.py
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -22,44 +22,42 @@
from boto.exception import InvalidLifecycleConfigError from boto.exception import InvalidLifecycleConfigError


# Relevant tags for the lifecycle configuration XML document. # Relevant tags for the lifecycle configuration XML document.
LIFECYCLE_CONFIG = 'LifecycleConfiguration' LIFECYCLE_CONFIG = 'LifecycleConfiguration'
RULE = 'Rule' RULE = 'Rule'
ACTION = 'Action' ACTION = 'Action'
DELETE = 'Delete' DELETE = 'Delete'
CONDITION = 'Condition' SET_STORAGE_CLASS = 'SetStorageClass'
AGE = 'Age' CONDITION = 'Condition'
CREATED_BEFORE = 'CreatedBefore' AGE = 'Age'
NUM_NEWER_VERSIONS = 'NumberOfNewerVersions' CREATED_BEFORE = 'CreatedBefore'
IS_LIVE = 'IsLive' NUM_NEWER_VERSIONS = 'NumberOfNewerVersions'
IS_LIVE = 'IsLive'
MATCHES_STORAGE_CLASS = 'MatchesStorageClass'


# List of all action elements. # List of all action elements.
LEGAL_ACTIONS = [DELETE] LEGAL_ACTIONS = [DELETE, SET_STORAGE_CLASS]
# List of all action parameter elements.
LEGAL_ACTION_PARAMS = []
# List of all condition elements. # List of all condition elements.
LEGAL_CONDITIONS = [AGE, CREATED_BEFORE, NUM_NEWER_VERSIONS, IS_LIVE] LEGAL_CONDITIONS = [AGE, CREATED_BEFORE, NUM_NEWER_VERSIONS, IS_LIVE,
# Dictionary mapping actions to supported action parameters for each action. MATCHES_STORAGE_CLASS]
LEGAL_ACTION_ACTION_PARAMS = { # List of conditions elements that may be repeated.
DELETE: [], LEGAL_REPEATABLE_CONDITIONS = [MATCHES_STORAGE_CLASS]
}


class Rule(object): class Rule(object):
""" """
A lifecycle rule for a bucket. A lifecycle rule for a bucket.
:ivar action: Action to be taken. :ivar action: Action to be taken.
:ivar action_params: A dictionary of action specific parameters. Each item :ivar action_text: The text value for the specified action, if any.
in the dictionary represents the name and value of an action parameter.
:ivar conditions: A dictionary of conditions that specify when the action :ivar conditions: A dictionary of conditions that specify when the action
should be taken. Each item in the dictionary represents the name and value should be taken. Each item in the dictionary represents the name and
of a condition. value (or a list of multiple values, if applicable) of a condition.
""" """


def __init__(self, action=None, action_params=None, conditions=None): def __init__(self, action=None, action_text=None, conditions=None):
self.action = action self.action = action
self.action_params = action_params or {} self.action_text = action_text
self.conditions = conditions or {} self.conditions = conditions or {}


# Name of the current enclosing tag (used to validate the schema). # Name of the current enclosing tag (used to validate the schema).
Expand Down Expand Up @@ -88,23 +86,15 @@ def startElement(self, name, attrs, connection):
raise InvalidLifecycleConfigError( raise InvalidLifecycleConfigError(
'Only one action tag is allowed in each rule') 'Only one action tag is allowed in each rule')
self.action = name self.action = name
elif name in LEGAL_ACTION_PARAMS:
# Make sure this tag is found in an action tag.
if self.current_tag not in LEGAL_ACTIONS:
raise InvalidLifecycleConfigError(
'Tag %s found outside of action' % name)
# Make sure this tag is allowed for the current action tag.
if name not in LEGAL_ACTION_ACTION_PARAMS[self.action]:
raise InvalidLifecycleConfigError(
'Tag %s not allowed in action %s' % (name, self.action))
elif name == CONDITION: elif name == CONDITION:
self.validateStartTag(name, RULE) self.validateStartTag(name, RULE)
elif name in LEGAL_CONDITIONS: elif name in LEGAL_CONDITIONS:
self.validateStartTag(name, CONDITION) self.validateStartTag(name, CONDITION)
# Verify there is no duplicate conditions. # Verify there is no duplicate conditions.
if name in self.conditions: if (name in self.conditions and
name not in LEGAL_REPEATABLE_CONDITIONS):
raise InvalidLifecycleConfigError( raise InvalidLifecycleConfigError(
'Found duplicate conditions %s' % name) 'Found duplicate non-repeatable conditions %s' % name)
else: else:
raise InvalidLifecycleConfigError('Unsupported tag ' + name) raise InvalidLifecycleConfigError('Unsupported tag ' + name)
self.current_tag = name self.current_tag = name
Expand All @@ -118,17 +108,20 @@ def endElement(self, name, value, connection):
elif name == ACTION: elif name == ACTION:
self.current_tag = RULE self.current_tag = RULE
elif name in LEGAL_ACTIONS: elif name in LEGAL_ACTIONS:
if name == SET_STORAGE_CLASS and value is not None:
self.action_text = value.strip()
self.current_tag = ACTION self.current_tag = ACTION
elif name in LEGAL_ACTION_PARAMS:
self.current_tag = self.action
# Add the action parameter name and value to the dictionary.
self.action_params[name] = value.strip()
elif name == CONDITION: elif name == CONDITION:
self.current_tag = RULE self.current_tag = RULE
elif name in LEGAL_CONDITIONS: elif name in LEGAL_CONDITIONS:
self.current_tag = CONDITION self.current_tag = CONDITION
# Add the condition name and value to the dictionary. # Some conditions specify a list of values.
self.conditions[name] = value.strip() if name in LEGAL_REPEATABLE_CONDITIONS:
if name not in self.conditions:
self.conditions[name] = []
self.conditions[name].append(value.strip())
else:
self.conditions[name] = value.strip()
else: else:
raise InvalidLifecycleConfigError('Unsupported end tag ' + name) raise InvalidLifecycleConfigError('Unsupported end tag ' + name)


Expand All @@ -143,26 +136,32 @@ def validate(self):


def to_xml(self): def to_xml(self):
"""Convert the rule into XML string representation.""" """Convert the rule into XML string representation."""
s = '<' + RULE + '>' s = ['<' + RULE + '>']
s += '<' + ACTION + '>' s.append('<' + ACTION + '>')
if self.action_params: if self.action_text:
s += '<' + self.action + '>' s.extend(['<' + self.action + '>',
for param in LEGAL_ACTION_PARAMS: self.action_text,
if param in self.action_params: '</' + self.action + '>'])
s += ('<' + param + '>' + self.action_params[param] + '</'
+ param + '>')
s += '</' + self.action + '>'
else: else:
s += '<' + self.action + '/>' s.append('<' + self.action + '/>')
s += '</' + ACTION + '>' s.append('</' + ACTION + '>')
s += '<' + CONDITION + '>' s.append('<' + CONDITION + '>')
for condition in LEGAL_CONDITIONS: for condition_name in self.conditions:
if condition in self.conditions: if condition_name not in LEGAL_CONDITIONS:
s += ('<' + condition + '>' + self.conditions[condition] + '</' continue
+ condition + '>') if condition_name in LEGAL_REPEATABLE_CONDITIONS:
s += '</' + CONDITION + '>' condition_values = self.conditions[condition_name]
s += '</' + RULE + '>' else:
return s # Wrap condition value in a list, allowing us to iterate over
# all condition values using the same logic.
condition_values = [self.conditions[condition_name]]
for condition_value in condition_values:
s.extend(['<' + condition_name + '>',
condition_value,
'</' + condition_name + '>'])
s.append('</' + CONDITION + '>')
s.append('</' + RULE + '>')
return ''.join(s)


class LifecycleConfig(list): class LifecycleConfig(list):
""" """
Expand Down Expand Up @@ -196,14 +195,14 @@ def endElement(self, name, value, connection):


def to_xml(self): def to_xml(self):
"""Convert LifecycleConfig object into XML string representation.""" """Convert LifecycleConfig object into XML string representation."""
s = '<?xml version="1.0" encoding="UTF-8"?>' s = ['<?xml version="1.0" encoding="UTF-8"?>']
s += '<' + LIFECYCLE_CONFIG + '>' s.append('<' + LIFECYCLE_CONFIG + '>')
for rule in self: for rule in self:
s += rule.to_xml() s.append(rule.to_xml())
s += '</' + LIFECYCLE_CONFIG + '>' s.append('</' + LIFECYCLE_CONFIG + '>')
return s return ''.join(s)


def add_rule(self, action, action_params, conditions): def add_rule(self, action, action_text, conditions):
""" """
Add a rule to this Lifecycle configuration. This only adds the rule to Add a rule to this Lifecycle configuration. This only adds the rule to
the local copy. To install the new rule(s) on the bucket, you need to the local copy. To install the new rule(s) on the bucket, you need to
Expand All @@ -213,15 +212,13 @@ def add_rule(self, action, action_params, conditions):
:type action: str :type action: str
:param action: Action to be taken. :param action: Action to be taken.
:type action_params: dict :type action_text: str
:param action_params: A dictionary of action specific parameters. Each :param action_text: Value for the specified action.
item in the dictionary represents the name and value of an action
parameter.
:type conditions: dict :type conditions: dict
:param conditions: A dictionary of conditions that specify when the :param conditions: A dictionary of conditions that specify when the
action should be taken. Each item in the dictionary represents the name action should be taken. Each item in the dictionary represents the name
and value of a condition. and value of a condition.
""" """
rule = Rule(action, action_params, conditions) rule = Rule(action, action_text, conditions)
self.append(rule) self.append(rule)
15 changes: 13 additions & 2 deletions boto/storage_uri.py
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -444,14 +444,25 @@ def get_location(self, validate=False, headers=None):


def get_storage_class(self, validate=False, headers=None): def get_storage_class(self, validate=False, headers=None):
self._check_bucket_uri('get_storage_class') self._check_bucket_uri('get_storage_class')
# StorageClass is defined as a bucket param for GCS, but as a key # StorageClass is defined as a bucket and object param for GCS, but
# param for S3. # only as a key param for S3.
if self.scheme != 'gs': if self.scheme != 'gs':
raise ValueError('get_storage_class() not supported for %s ' raise ValueError('get_storage_class() not supported for %s '
'URIs.' % self.scheme) 'URIs.' % self.scheme)
bucket = self.get_bucket(validate, headers) bucket = self.get_bucket(validate, headers)
return bucket.get_storage_class() return bucket.get_storage_class()


def set_storage_class(self, storage_class, validate=False, headers=None):
"""Updates a bucket's storage class."""
self._check_bucket_uri('set_storage_class')
# StorageClass is defined as a bucket and object param for GCS, but
# only as a key param for S3.
if self.scheme != 'gs':
raise ValueError('set_storage_class() not supported for %s '
'URIs.' % self.scheme)
bucket = self.get_bucket(validate, headers)
bucket.set_storage_class(storage_class, headers)

def get_subresource(self, subresource, validate=False, headers=None, def get_subresource(self, subresource, validate=False, headers=None,
version_id=None): version_id=None):
self._check_bucket_uri('get_subresource') self._check_bucket_uri('get_subresource')
Expand Down
33 changes: 24 additions & 9 deletions tests/integration/gs/test_basic.py
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -56,15 +56,22 @@
LIFECYCLE_DOC = ('<?xml version="1.0" encoding="UTF-8"?>' LIFECYCLE_DOC = ('<?xml version="1.0" encoding="UTF-8"?>'
'<LifecycleConfiguration><Rule>' '<LifecycleConfiguration><Rule>'
'<Action><Delete/></Action>' '<Action><Delete/></Action>'
'<Condition><Age>365</Age>' '<Condition>''<IsLive>true</IsLive>'
'<MatchesStorageClass>STANDARD</MatchesStorageClass>'
'<Age>365</Age>'
'<CreatedBefore>2013-01-15</CreatedBefore>' '<CreatedBefore>2013-01-15</CreatedBefore>'
'<NumberOfNewerVersions>3</NumberOfNewerVersions>' '<NumberOfNewerVersions>3</NumberOfNewerVersions>'
'<IsLive>true</IsLive></Condition>' '</Condition></Rule><Rule>'
'</Rule></LifecycleConfiguration>') '<Action><SetStorageClass>NEARLINE</SetStorageClass></Action>'
LIFECYCLE_CONDITIONS = {'Age': '365', '<Condition><Age>366</Age>'
'CreatedBefore': '2013-01-15', '</Condition></Rule></LifecycleConfiguration>')
'NumberOfNewerVersions': '3', LIFECYCLE_CONDITIONS_FOR_DELETE_RULE = {
'IsLive': 'true'} 'Age': '365',
'CreatedBefore': '2013-01-15',
'NumberOfNewerVersions': '3',
'IsLive': 'true',
'MatchesStorageClass': ['STANDARD']}
LIFECYCLE_CONDITIONS_FOR_SET_STORAGE_CLASS_RULE = {'Age': '366'}


# Regexp for matching project-private default object ACL. # Regexp for matching project-private default object ACL.
PROJECT_PRIVATE_RE = ('\s*<AccessControlList>\s*<Entries>\s*<Entry>' PROJECT_PRIVATE_RE = ('\s*<AccessControlList>\s*<Entries>\s*<Entry>'
Expand Down Expand Up @@ -412,7 +419,11 @@ def test_lifecycle_config_bucket(self):
self.assertEqual(xml, LIFECYCLE_EMPTY) self.assertEqual(xml, LIFECYCLE_EMPTY)
# set lifecycle config # set lifecycle config
lifecycle_config = LifecycleConfig() lifecycle_config = LifecycleConfig()
lifecycle_config.add_rule('Delete', None, LIFECYCLE_CONDITIONS) lifecycle_config.add_rule(
'Delete', None, LIFECYCLE_CONDITIONS_FOR_DELETE_RULE)
lifecycle_config.add_rule(
'SetStorageClass', 'NEARLINE',
LIFECYCLE_CONDITIONS_FOR_SET_STORAGE_CLASS_RULE)
bucket.configure_lifecycle(lifecycle_config) bucket.configure_lifecycle(lifecycle_config)
xml = bucket.get_lifecycle_config().to_xml() xml = bucket.get_lifecycle_config().to_xml()
self.assertEqual(xml, LIFECYCLE_DOC) self.assertEqual(xml, LIFECYCLE_DOC)
Expand All @@ -428,7 +439,11 @@ def test_lifecycle_config_storage_uri(self):
self.assertEqual(xml, LIFECYCLE_EMPTY) self.assertEqual(xml, LIFECYCLE_EMPTY)
# set lifecycle config # set lifecycle config
lifecycle_config = LifecycleConfig() lifecycle_config = LifecycleConfig()
lifecycle_config.add_rule('Delete', None, LIFECYCLE_CONDITIONS) lifecycle_config.add_rule(
'Delete', None, LIFECYCLE_CONDITIONS_FOR_DELETE_RULE)
lifecycle_config.add_rule(
'SetStorageClass', 'NEARLINE',
LIFECYCLE_CONDITIONS_FOR_SET_STORAGE_CLASS_RULE)
uri.configure_lifecycle(lifecycle_config) uri.configure_lifecycle(lifecycle_config)
xml = uri.get_lifecycle_config().to_xml() xml = uri.get_lifecycle_config().to_xml()
self.assertEqual(xml, LIFECYCLE_DOC) self.assertEqual(xml, LIFECYCLE_DOC)

0 comments on commit dc4bf34

Please sign in to comment.