Skip to content

Commit c586a7c

Browse files
committed
Add CORS support to buckets.
See: http://www.w3.org/TR/cors/ and https://cloud.google.com/storage/docs/json_api/v1/buckets Addresses 'cors' part of 314.
1 parent 826ea69 commit c586a7c

2 files changed

Lines changed: 144 additions & 3 deletions

File tree

gcloud/storage/bucket.py

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ class Bucket(_MetadataMixin):
2525
'acl': 'get_acl',
2626
'defaultObjectAcl': 'get_default_object_acl',
2727
'lifecycle': 'get_lifecycle',
28+
'cors': 'get_cors',
2829
}
2930
"""Mapping of field name -> accessor for fields w/ custom accessors."""
3031

@@ -443,13 +444,13 @@ def make_public(self, recursive=False, future=False):
443444
key.save_acl()
444445

445446
def get_lifecycle(self):
446-
"""Retrieve CORS policies configured for this bucket.
447+
"""Retrieve lifecycle rules configured for this bucket.
447448
448449
See: https://cloud.google.com/storage/docs/lifecycle and
449450
https://cloud.google.com/storage/docs/json_api/v1/buckets
450451
451452
:rtype: list(dict)
452-
:returns: A sequence of mappings describing each CORS policy.
453+
:returns: A sequence of mappings describing each lifecycle rule.
453454
"""
454455
if not self.has_metadata('lifecycle'):
455456
self.reload_metadata()
@@ -467,10 +468,62 @@ def update_lifecycle(self, rules):
467468
https://cloud.google.com/storage/docs/json_api/v1/buckets
468469
469470
:type rules: list(dict)
470-
:param rules: A sequence of mappings describing each lifecycle policy.
471+
:param rules: A sequence of mappings describing each lifecycle rule.
471472
"""
472473
self.patch_metadata({'lifecycle': {'rule': rules}})
473474

475+
def get_cors(self):
476+
"""Retrieve CORS policies configured for this bucket.
477+
478+
See: http://www.w3.org/TR/cors/ and
479+
https://cloud.google.com/storage/docs/json_api/v1/buckets
480+
481+
:rtype: list(dict)
482+
:returns: A sequence of mappings describing each CORS policy.
483+
Keys include 'max_age', 'methods', 'origins', and
484+
'headers'.
485+
"""
486+
if not self.has_metadata('cors'):
487+
self.reload_metadata()
488+
result = []
489+
for entry in self.metadata.get('cors', ()):
490+
entry = entry.copy()
491+
result.append(entry)
492+
if 'maxAgeSeconds' in entry:
493+
entry['max_age'] = entry.pop('maxAgeSeconds')
494+
if 'method' in entry:
495+
entry['methods'] = entry.pop('method')
496+
if 'origin' in entry:
497+
entry['origins'] = entry.pop('origin')
498+
if 'responseHeader' in entry:
499+
entry['headers'] = entry.pop('responseHeader')
500+
return result
501+
502+
def update_cors(self, entries):
503+
"""Update CORS policies configured for this bucket.
504+
505+
See: http://www.w3.org/TR/cors/ and
506+
https://cloud.google.com/storage/docs/json_api/v1/buckets
507+
508+
:type entries: list(dict)
509+
:param entries: A sequence of mappings describing each CORS policy.
510+
Keys include 'max_age', 'methods', 'origins', and
511+
'headers'.
512+
"""
513+
to_patch = []
514+
for entry in entries:
515+
entry = entry.copy()
516+
to_patch.append(entry)
517+
if 'max_age' in entry:
518+
entry['maxAgeSeconds'] = entry.pop('max_age')
519+
if 'methods' in entry:
520+
entry['method'] = entry.pop('methods')
521+
if 'origins' in entry:
522+
entry['origin'] = entry.pop('origins')
523+
if 'headers' in entry:
524+
entry['responseHeader'] = entry.pop('headers')
525+
self.patch_metadata({'cors': to_patch})
526+
474527

475528
class BucketIterator(Iterator):
476529
"""An iterator listing all buckets.

gcloud/storage/test_bucket.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,23 @@ def test_get_metadata_lifecycle_w_default(self):
510510
kw = connection._requested
511511
self.assertEqual(len(kw), 0)
512512

513+
def test_get_metadata_cors_no_default(self):
514+
NAME = 'name'
515+
connection = _Connection()
516+
bucket = self._makeOne(connection, NAME)
517+
self.assertRaises(KeyError, bucket.get_metadata, 'cors')
518+
kw = connection._requested
519+
self.assertEqual(len(kw), 0)
520+
521+
def test_get_metadata_none_set_cors_w_default(self):
522+
NAME = 'name'
523+
connection = _Connection()
524+
bucket = self._makeOne(connection, NAME)
525+
default = object()
526+
self.assertRaises(KeyError, bucket.get_metadata, 'cors', default)
527+
kw = connection._requested
528+
self.assertEqual(len(kw), 0)
529+
513530
def test_get_metadata_miss(self):
514531
NAME = 'name'
515532
before = {'bar': 'Bar'}
@@ -781,6 +798,77 @@ def test_update_lifecycle(self):
781798
self.assertEqual(entries[0]['action']['type'], 'Delete')
782799
self.assertEqual(entries[0]['condition']['age'], 42)
783800

801+
def test_get_cors_eager(self):
802+
NAME = 'name'
803+
CORS_ENTRY = {
804+
'maxAgeSeconds': 1234,
805+
'method': ['OPTIONS', 'GET'],
806+
'origin': ['127.0.0.1'],
807+
'responseHeader': ['Content-Type'],
808+
}
809+
before = {'cors': [CORS_ENTRY, {}]}
810+
connection = _Connection()
811+
bucket = self._makeOne(connection, NAME, before)
812+
entries = bucket.get_cors()
813+
self.assertEqual(len(entries), 2)
814+
self.assertEqual(entries[0]['max_age'], CORS_ENTRY['maxAgeSeconds'])
815+
self.assertEqual(entries[0]['methods'], CORS_ENTRY['method'])
816+
self.assertEqual(entries[0]['origins'], CORS_ENTRY['origin'])
817+
self.assertEqual(entries[0]['headers'], CORS_ENTRY['responseHeader'])
818+
self.assertEqual(entries[1], {})
819+
kw = connection._requested
820+
self.assertEqual(len(kw), 0)
821+
822+
def test_get_cors_lazy(self):
823+
NAME = 'name'
824+
CORS_ENTRY = {
825+
'maxAgeSeconds': 1234,
826+
'method': ['OPTIONS', 'GET'],
827+
'origin': ['127.0.0.1'],
828+
'responseHeader': ['Content-Type'],
829+
}
830+
after = {'cors': [CORS_ENTRY]}
831+
connection = _Connection(after)
832+
bucket = self._makeOne(connection, NAME)
833+
entries = bucket.get_cors()
834+
self.assertEqual(len(entries), 1)
835+
self.assertEqual(entries[0]['max_age'], CORS_ENTRY['maxAgeSeconds'])
836+
self.assertEqual(entries[0]['methods'], CORS_ENTRY['method'])
837+
self.assertEqual(entries[0]['origins'], CORS_ENTRY['origin'])
838+
self.assertEqual(entries[0]['headers'], CORS_ENTRY['responseHeader'])
839+
kw = connection._requested
840+
self.assertEqual(len(kw), 1)
841+
self.assertEqual(kw[0]['method'], 'GET')
842+
self.assertEqual(kw[0]['path'], '/b/%s' % NAME)
843+
self.assertEqual(kw[0]['query_params'], {'projection': 'noAcl'})
844+
845+
def test_update_cors(self):
846+
NAME = 'name'
847+
CORS_ENTRY = {
848+
'maxAgeSeconds': 1234,
849+
'method': ['OPTIONS', 'GET'],
850+
'origin': ['127.0.0.1'],
851+
'responseHeader': ['Content-Type'],
852+
}
853+
MAPPED = {
854+
'max_age': 1234,
855+
'methods': ['OPTIONS', 'GET'],
856+
'origins': ['127.0.0.1'],
857+
'headers': ['Content-Type'],
858+
}
859+
after = {'cors': [CORS_ENTRY, {}]}
860+
connection = _Connection(after)
861+
bucket = self._makeOne(connection, NAME)
862+
bucket.update_cors([MAPPED, {}])
863+
kw = connection._requested
864+
self.assertEqual(len(kw), 1)
865+
self.assertEqual(kw[0]['method'], 'PATCH')
866+
self.assertEqual(kw[0]['path'], '/b/%s' % NAME)
867+
self.assertEqual(kw[0]['data'], after)
868+
self.assertEqual(kw[0]['query_params'], {'projection': 'full'})
869+
entries = bucket.get_cors()
870+
self.assertEqual(entries, [MAPPED, {}])
871+
784872

785873
class TestBucketIterator(unittest2.TestCase):
786874

0 commit comments

Comments
 (0)