1 # -*- coding: utf-8 -*-
2 # Copyright 2014 Google Inc. All Rights Reserved.
4 # Licensed under the Apache License, Version 2.0 (the "License");
5 # you may not use this file except in compliance with the License.
6 # You may obtain a copy of the License at
8 # http://www.apache.org/licenses/LICENSE-2.0
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS,
12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # See the License for the specific language governing permissions and
14 # limitations under the License.
15 """Utility module for translating XML API objects to/from JSON objects."""
17 from __future__
import absolute_import
23 import xml
.etree
.ElementTree
25 from apitools
.base
.py
import encoding
27 from boto
.gs
.acl
import ACL
28 from boto
.gs
.acl
import ALL_AUTHENTICATED_USERS
29 from boto
.gs
.acl
import ALL_USERS
30 from boto
.gs
.acl
import Entries
31 from boto
.gs
.acl
import Entry
32 from boto
.gs
.acl
import GROUP_BY_DOMAIN
33 from boto
.gs
.acl
import GROUP_BY_EMAIL
34 from boto
.gs
.acl
import GROUP_BY_ID
35 from boto
.gs
.acl
import USER_BY_EMAIL
36 from boto
.gs
.acl
import USER_BY_ID
38 from gslib
.cloud_api
import ArgumentException
39 from gslib
.cloud_api
import NotFoundException
40 from gslib
.cloud_api
import Preconditions
41 from gslib
.exception
import CommandException
42 from gslib
.third_party
.storage_apitools
import storage_v1_messages
as apitools_messages
44 # In Python 2.6, ElementTree raises ExpatError instead of ParseError.
45 # pylint: disable=g-import-not-at-top
47 from xml
.etree
.ElementTree
import ParseError
as XmlParseError
49 from xml
.parsers
.expat
import ExpatError
as XmlParseError
51 CACHE_CONTROL_REGEX
= re
.compile(r
'^cache-control', re
.I
)
52 CONTENT_DISPOSITION_REGEX
= re
.compile(r
'^content-disposition', re
.I
)
53 CONTENT_ENCODING_REGEX
= re
.compile(r
'^content-encoding', re
.I
)
54 CONTENT_LANGUAGE_REGEX
= re
.compile(r
'^content-language', re
.I
)
55 CONTENT_MD5_REGEX
= re
.compile(r
'^content-md5', re
.I
)
56 CONTENT_TYPE_REGEX
= re
.compile(r
'^content-type', re
.I
)
57 GOOG_API_VERSION_REGEX
= re
.compile(r
'^x-goog-api-version', re
.I
)
58 GOOG_GENERATION_MATCH_REGEX
= re
.compile(r
'^x-goog-if-generation-match', re
.I
)
59 GOOG_METAGENERATION_MATCH_REGEX
= re
.compile(
60 r
'^x-goog-if-metageneration-match', re
.I
)
61 CUSTOM_GOOG_METADATA_REGEX
= re
.compile(r
'^x-goog-meta-(?P<header_key>.*)',
63 CUSTOM_AMZ_METADATA_REGEX
= re
.compile(r
'^x-amz-meta-(?P<header_key>.*)', re
.I
)
64 CUSTOM_AMZ_HEADER_REGEX
= re
.compile(r
'^x-amz-(?P<header_key>.*)', re
.I
)
66 # gsutil-specific GUIDs for marking special metadata for S3 compatibility.
67 S3_ACL_MARKER_GUID
= '3b89a6b5-b55a-4900-8c44-0b0a2f5eab43-s3-AclMarker'
68 S3_DELETE_MARKER_GUID
= 'eadeeee8-fa8c-49bb-8a7d-0362215932d8-s3-DeleteMarker'
69 S3_MARKER_GUIDS
= [S3_ACL_MARKER_GUID
, S3_DELETE_MARKER_GUID
]
70 # This distinguishes S3 custom headers from S3 metadata on objects.
71 S3_HEADER_PREFIX
= 'custom-amz-header'
73 DEFAULT_CONTENT_TYPE
= 'application/octet-stream'
75 # Because CORS is just a list in apitools, we need special handling or blank
76 # CORS lists will get sent with other configuration commands such as lifecycle,
77 # commands, which would cause CORS configuration to be unintentionally removed.
78 # Protorpc defaults list values to an empty list, and won't allow us to set the
79 # value to None like other configuration fields, so there is no way to
80 # distinguish the default value from when we actually want to remove the CORS
81 # configuration. To work around this, we create a dummy CORS entry that
82 # signifies that we should nullify the CORS configuration.
83 # A value of [] means don't modify the CORS configuration.
84 # A value of REMOVE_CORS_CONFIG means remove the CORS configuration.
85 REMOVE_CORS_CONFIG
= [apitools_messages
.Bucket
.CorsValueListEntry(
86 maxAgeSeconds
=-1, method
=['REMOVE_CORS_CONFIG'])]
89 def ObjectMetadataFromHeaders(headers
):
90 """Creates object metadata according to the provided headers.
92 gsutil -h allows specifiying various headers (originally intended
93 to be passed to boto in gsutil v3). For the JSON API to be compatible with
94 this option, we need to parse these headers into gsutil_api Object fields.
97 headers: Dict of headers passed via gsutil -h
100 ArgumentException if an invalid header is encountered.
103 apitools Object with relevant fields populated from headers.
105 obj_metadata
= apitools_messages
.Object()
106 for header
, value
in headers
.items():
107 if CACHE_CONTROL_REGEX
.match(header
):
108 obj_metadata
.cacheControl
= value
.strip()
109 elif CONTENT_DISPOSITION_REGEX
.match(header
):
110 obj_metadata
.contentDisposition
= value
.strip()
111 elif CONTENT_ENCODING_REGEX
.match(header
):
112 obj_metadata
.contentEncoding
= value
.strip()
113 elif CONTENT_MD5_REGEX
.match(header
):
114 obj_metadata
.md5Hash
= value
.strip()
115 elif CONTENT_LANGUAGE_REGEX
.match(header
):
116 obj_metadata
.contentLanguage
= value
.strip()
117 elif CONTENT_TYPE_REGEX
.match(header
):
119 obj_metadata
.contentType
= DEFAULT_CONTENT_TYPE
121 obj_metadata
.contentType
= value
.strip()
122 elif GOOG_API_VERSION_REGEX
.match(header
):
123 # API version is only relevant for XML, ignore and rely on the XML API
124 # to add the appropriate version.
126 elif GOOG_GENERATION_MATCH_REGEX
.match(header
):
127 # Preconditions are handled elsewhere, but allow these headers through.
129 elif GOOG_METAGENERATION_MATCH_REGEX
.match(header
):
130 # Preconditions are handled elsewhere, but allow these headers through.
133 custom_goog_metadata_match
= CUSTOM_GOOG_METADATA_REGEX
.match(header
)
134 custom_amz_metadata_match
= CUSTOM_AMZ_METADATA_REGEX
.match(header
)
135 custom_amz_header_match
= CUSTOM_AMZ_HEADER_REGEX
.match(header
)
137 if custom_goog_metadata_match
:
138 header_key
= custom_goog_metadata_match
.group('header_key')
139 elif custom_amz_metadata_match
:
140 header_key
= custom_amz_metadata_match
.group('header_key')
141 elif custom_amz_header_match
:
142 # If we got here we are guaranteed by the prior statement that this is
143 # not an x-amz-meta- header.
144 header_key
= (S3_HEADER_PREFIX
+
145 custom_amz_header_match
.group('header_key'))
147 if header_key
.lower() == 'x-goog-content-language':
148 # Work around content-language being inserted into custom metadata.
150 if not obj_metadata
.metadata
:
151 obj_metadata
.metadata
= apitools_messages
.Object
.MetadataValue()
152 if not obj_metadata
.metadata
.additionalProperties
:
153 obj_metadata
.metadata
.additionalProperties
= []
154 obj_metadata
.metadata
.additionalProperties
.append(
155 apitools_messages
.Object
.MetadataValue
.AdditionalProperty(
156 key
=header_key
, value
=value
))
158 raise ArgumentException(
159 'Invalid header specifed: %s:%s' % (header
, value
))
163 def HeadersFromObjectMetadata(dst_obj_metadata
, provider
):
164 """Creates a header dictionary based on existing object metadata.
167 dst_obj_metadata: Object metadata to create the headers from.
168 provider: Provider string ('gs' or 's3')
174 if not dst_obj_metadata
:
176 # Metadata values of '' mean suppress/remove this header.
177 if dst_obj_metadata
.cacheControl
is not None:
178 if not dst_obj_metadata
.cacheControl
:
179 headers
['cache-control'] = None
181 headers
['cache-control'] = dst_obj_metadata
.cacheControl
.strip()
182 if dst_obj_metadata
.contentDisposition
:
183 if not dst_obj_metadata
.contentDisposition
:
184 headers
['content-disposition'] = None
186 headers
['content-disposition'] = (
187 dst_obj_metadata
.contentDisposition
.strip())
188 if dst_obj_metadata
.contentEncoding
:
189 if not dst_obj_metadata
.contentEncoding
:
190 headers
['content-encoding'] = None
192 headers
['content-encoding'] = dst_obj_metadata
.contentEncoding
.strip()
193 if dst_obj_metadata
.contentLanguage
:
194 if not dst_obj_metadata
.contentLanguage
:
195 headers
['content-language'] = None
197 headers
['content-language'] = dst_obj_metadata
.contentLanguage
.strip()
198 if dst_obj_metadata
.md5Hash
:
199 if not dst_obj_metadata
.md5Hash
:
200 headers
['Content-MD5'] = None
202 headers
['Content-MD5'] = dst_obj_metadata
.md5Hash
.strip()
203 if dst_obj_metadata
.contentType
is not None:
204 if not dst_obj_metadata
.contentType
:
205 headers
['content-type'] = None
207 headers
['content-type'] = dst_obj_metadata
.contentType
.strip()
208 if (dst_obj_metadata
.metadata
and
209 dst_obj_metadata
.metadata
.additionalProperties
):
210 for additional_property
in dst_obj_metadata
.metadata
.additionalProperties
:
211 # Work around content-language being inserted into custom metadata by
213 if additional_property
.key
== 'content-language':
215 # Don't translate special metadata markers.
216 if additional_property
.key
in S3_MARKER_GUIDS
:
219 header_name
= 'x-goog-meta-' + additional_property
.key
220 elif provider
== 's3':
221 if additional_property
.key
.startswith(S3_HEADER_PREFIX
):
222 header_name
= ('x-amz-' +
223 additional_property
.key
[len(S3_HEADER_PREFIX
):])
225 header_name
= 'x-amz-meta-' + additional_property
.key
227 raise ArgumentException('Invalid provider specified: %s' % provider
)
228 if (additional_property
.value
is not None and
229 not additional_property
.value
):
230 headers
[header_name
] = None
232 headers
[header_name
] = additional_property
.value
236 def CopyObjectMetadata(src_obj_metadata
, dst_obj_metadata
, override
=False):
237 """Copies metadata from src_obj_metadata to dst_obj_metadata.
240 src_obj_metadata: Metadata from source object
241 dst_obj_metadata: Initialized metadata for destination object
242 override: If true, will overwrite metadata in destination object.
243 If false, only writes metadata for values that don't already
246 if override
or not dst_obj_metadata
.cacheControl
:
247 dst_obj_metadata
.cacheControl
= src_obj_metadata
.cacheControl
248 if override
or not dst_obj_metadata
.contentDisposition
:
249 dst_obj_metadata
.contentDisposition
= src_obj_metadata
.contentDisposition
250 if override
or not dst_obj_metadata
.contentEncoding
:
251 dst_obj_metadata
.contentEncoding
= src_obj_metadata
.contentEncoding
252 if override
or not dst_obj_metadata
.contentLanguage
:
253 dst_obj_metadata
.contentLanguage
= src_obj_metadata
.contentLanguage
254 if override
or not dst_obj_metadata
.contentType
:
255 dst_obj_metadata
.contentType
= src_obj_metadata
.contentType
256 if override
or not dst_obj_metadata
.md5Hash
:
257 dst_obj_metadata
.md5Hash
= src_obj_metadata
.md5Hash
259 # TODO: Apitools should ideally treat metadata like a real dictionary instead
260 # of a list of key/value pairs (with an O(N^2) lookup). In practice the
261 # number of values is typically small enough not to matter.
262 # Work around this by creating our own dictionary.
263 if (src_obj_metadata
.metadata
and
264 src_obj_metadata
.metadata
.additionalProperties
):
265 if not dst_obj_metadata
.metadata
:
266 dst_obj_metadata
.metadata
= apitools_messages
.Object
.MetadataValue()
267 if not dst_obj_metadata
.metadata
.additionalProperties
:
268 dst_obj_metadata
.metadata
.additionalProperties
= []
269 dst_metadata_dict
= {}
270 for dst_prop
in dst_obj_metadata
.metadata
.additionalProperties
:
271 dst_metadata_dict
[dst_prop
.key
] = dst_prop
.value
272 for src_prop
in src_obj_metadata
.metadata
.additionalProperties
:
273 if src_prop
.key
in dst_metadata_dict
:
275 # Metadata values of '' mean suppress/remove this header.
276 if src_prop
.value
is not None and not src_prop
.value
:
277 dst_metadata_dict
[src_prop
.key
] = None
279 dst_metadata_dict
[src_prop
.key
] = src_prop
.value
281 dst_metadata_dict
[src_prop
.key
] = src_prop
.value
282 # Rewrite the list with our updated dict.
283 dst_obj_metadata
.metadata
.additionalProperties
= []
284 for k
, v
in dst_metadata_dict
.iteritems():
285 dst_obj_metadata
.metadata
.additionalProperties
.append(
286 apitools_messages
.Object
.MetadataValue
.AdditionalProperty(key
=k
,
290 def PreconditionsFromHeaders(headers
):
291 """Creates bucket or object preconditions acccording to the provided headers.
294 headers: Dict of headers passed via gsutil -h
297 gsutil Cloud API Preconditions object fields populated from headers, or None
298 if no precondition headers are present.
300 return_preconditions
= Preconditions()
302 for header
, value
in headers
.items():
303 if GOOG_GENERATION_MATCH_REGEX
.match(header
):
304 return_preconditions
.gen_match
= long(value
)
305 if GOOG_METAGENERATION_MATCH_REGEX
.match(header
):
306 return_preconditions
.meta_gen_match
= long(value
)
307 except ValueError, _
:
308 raise ArgumentException('Invalid precondition header specified. '
309 'x-goog-if-generation-match and '
310 'x-goog-if-metageneration match must be specified '
311 'with a positive integer value.')
312 return return_preconditions
315 def CreateBucketNotFoundException(code
, provider
, bucket_name
):
316 return NotFoundException('%s://%s bucket does not exist.' %
317 (provider
, bucket_name
), status
=code
)
320 def CreateObjectNotFoundException(code
, provider
, bucket_name
, object_name
,
322 uri_string
= '%s://%s/%s' % (provider
, bucket_name
, object_name
)
324 uri_string
+= '#%s' % str(generation
)
325 return NotFoundException('%s does not exist.' % uri_string
, status
=code
)
328 def EncodeStringAsLong(string_to_convert
):
329 """Encodes an ASCII string as a python long.
331 This is used for modeling S3 version_id's as apitools generation. Because
332 python longs can be arbitrarily large, this works.
335 string_to_convert: ASCII string to convert to a long.
338 Long that represents the input string.
340 return long(string_to_convert
.encode('hex'), 16)
343 def _DecodeLongAsString(long_to_convert
):
344 """Decodes an encoded python long into an ASCII string.
346 This is used for modeling S3 version_id's as apitools generation.
349 long_to_convert: long to convert to ASCII string. If this is already a
350 string, it is simply returned.
353 String decoded from the input long.
355 if isinstance(long_to_convert
, basestring
):
357 return long_to_convert
358 return hex(long_to_convert
)[2:-1].decode('hex')
361 def GenerationFromUrlAndString(url
, generation
):
362 """Decodes a generation from a StorageURL and a generation string.
364 This is used to represent gs and s3 versioning.
367 url: StorageUrl representing the object.
368 generation: Long or string representing the object's generation or
372 Valid generation string for use in URLs.
374 if url
.scheme
== 's3' and generation
:
375 return _DecodeLongAsString(generation
)
379 def CheckForXmlConfigurationAndRaise(config_type_string
, json_txt
):
380 """Checks a JSON parse exception for provided XML configuration."""
382 xml
.etree
.ElementTree
.fromstring(str(json_txt
))
383 raise ArgumentException('\n'.join(textwrap
.wrap(
384 'XML {0} data provided; Google Cloud Storage {0} configuration '
385 'now uses JSON format. To convert your {0}, set the desired XML '
386 'ACL using \'gsutil {1} set ...\' with gsutil version 3.x. Then '
387 'use \'gsutil {1} get ...\' with gsutil version 4 or greater to '
388 'get the corresponding JSON {0}.'.format(config_type_string
,
389 config_type_string
.lower()))))
390 except XmlParseError
:
392 raise ArgumentException('JSON %s data could not be loaded '
393 'from: %s' % (config_type_string
, json_txt
))
396 class LifecycleTranslation(object):
397 """Functions for converting between various lifecycle formats.
399 This class handles conversation to and from Boto Cors objects, JSON text,
400 and apitools Message objects.
404 def BotoLifecycleFromMessage(cls
, lifecycle_message
):
405 """Translates an apitools message to a boto lifecycle object."""
406 boto_lifecycle
= boto
.gs
.lifecycle
.LifecycleConfig()
407 if lifecycle_message
:
408 for rule_message
in lifecycle_message
.rule
:
409 boto_rule
= boto
.gs
.lifecycle
.Rule()
410 if (rule_message
.action
and rule_message
.action
.type and
411 rule_message
.action
.type.lower() == 'delete'):
412 boto_rule
.action
= boto
.gs
.lifecycle
.DELETE
413 if rule_message
.condition
:
414 if rule_message
.condition
.age
:
415 boto_rule
.conditions
[boto
.gs
.lifecycle
.AGE
] = (
416 str(rule_message
.condition
.age
))
417 if rule_message
.condition
.createdBefore
:
418 boto_rule
.conditions
[boto
.gs
.lifecycle
.CREATED_BEFORE
] = (
419 str(rule_message
.condition
.createdBefore
))
420 if rule_message
.condition
.isLive
:
421 boto_rule
.conditions
[boto
.gs
.lifecycle
.IS_LIVE
] = (
422 str(rule_message
.condition
.isLive
))
423 if rule_message
.condition
.numNewerVersions
:
424 boto_rule
.conditions
[boto
.gs
.lifecycle
.NUM_NEWER_VERSIONS
] = (
425 str(rule_message
.condition
.numNewerVersions
))
426 boto_lifecycle
.append(boto_rule
)
427 return boto_lifecycle
430 def BotoLifecycleToMessage(cls
, boto_lifecycle
):
431 """Translates a boto lifecycle object to an apitools message."""
432 lifecycle_message
= None
434 lifecycle_message
= apitools_messages
.Bucket
.LifecycleValue()
435 for boto_rule
in boto_lifecycle
:
437 apitools_messages
.Bucket
.LifecycleValue
.RuleValueListEntry())
438 lifecycle_rule
.condition
= (apitools_messages
.Bucket
.LifecycleValue
.
439 RuleValueListEntry
.ConditionValue())
440 if boto_rule
.action
and boto_rule
.action
== boto
.gs
.lifecycle
.DELETE
:
441 lifecycle_rule
.action
= (apitools_messages
.Bucket
.LifecycleValue
.
442 RuleValueListEntry
.ActionValue(
444 if boto
.gs
.lifecycle
.AGE
in boto_rule
.conditions
:
445 lifecycle_rule
.condition
.age
= int(
446 boto_rule
.conditions
[boto
.gs
.lifecycle
.AGE
])
447 if boto
.gs
.lifecycle
.CREATED_BEFORE
in boto_rule
.conditions
:
448 lifecycle_rule
.condition
.createdBefore
= (
449 LifecycleTranslation
.TranslateBotoLifecycleTimestamp(
450 boto_rule
.conditions
[boto
.gs
.lifecycle
.CREATED_BEFORE
]))
451 if boto
.gs
.lifecycle
.IS_LIVE
in boto_rule
.conditions
:
452 lifecycle_rule
.condition
.isLive
= bool(
453 boto_rule
.conditions
[boto
.gs
.lifecycle
.IS_LIVE
])
454 if boto
.gs
.lifecycle
.NUM_NEWER_VERSIONS
in boto_rule
.conditions
:
455 lifecycle_rule
.condition
.numNewerVersions
= int(
456 boto_rule
.conditions
[boto
.gs
.lifecycle
.NUM_NEWER_VERSIONS
])
457 lifecycle_message
.rule
.append(lifecycle_rule
)
458 return lifecycle_message
461 def JsonLifecycleFromMessage(cls
, lifecycle_message
):
462 """Translates an apitools message to lifecycle JSON."""
463 return str(encoding
.MessageToJson(lifecycle_message
)) + '\n'
466 def JsonLifecycleToMessage(cls
, json_txt
):
467 """Translates lifecycle JSON to an apitools message."""
469 deserialized_lifecycle
= json
.loads(json_txt
)
470 # If lifecycle JSON is the in the following format
471 # {'lifecycle': {'rule': ... then strip out the 'lifecycle' key
472 # and reduce it to the following format
474 if 'lifecycle' in deserialized_lifecycle
:
475 deserialized_lifecycle
= deserialized_lifecycle
['lifecycle']
476 lifecycle
= encoding
.DictToMessage(
477 deserialized_lifecycle
, apitools_messages
.Bucket
.LifecycleValue
)
480 CheckForXmlConfigurationAndRaise('lifecycle', json_txt
)
483 def TranslateBotoLifecycleTimestamp(cls
, lifecycle_datetime
):
484 """Parses the timestamp from the boto lifecycle into a datetime object."""
485 return datetime
.datetime
.strptime(lifecycle_datetime
, '%Y-%m-%d').date()
488 class CorsTranslation(object):
489 """Functions for converting between various CORS formats.
491 This class handles conversation to and from Boto Cors objects, JSON text,
492 and apitools Message objects.
496 def BotoCorsFromMessage(cls
, cors_message
):
497 """Translates an apitools message to a boto Cors object."""
498 cors
= boto
.gs
.cors
.Cors()
500 for collection_message
in cors_message
:
501 collection_elements
= []
502 if collection_message
.maxAgeSeconds
:
503 collection_elements
.append((boto
.gs
.cors
.MAXAGESEC
,
504 str(collection_message
.maxAgeSeconds
)))
505 if collection_message
.method
:
507 for method
in collection_message
.method
:
508 method_elements
.append((boto
.gs
.cors
.METHOD
, method
))
509 collection_elements
.append((boto
.gs
.cors
.METHODS
, method_elements
))
510 if collection_message
.origin
:
512 for origin
in collection_message
.origin
:
513 origin_elements
.append((boto
.gs
.cors
.ORIGIN
, origin
))
514 collection_elements
.append((boto
.gs
.cors
.ORIGINS
, origin_elements
))
515 if collection_message
.responseHeader
:
517 for header
in collection_message
.responseHeader
:
518 header_elements
.append((boto
.gs
.cors
.HEADER
, header
))
519 collection_elements
.append((boto
.gs
.cors
.HEADERS
, header_elements
))
520 cors
.cors
.append(collection_elements
)
524 def BotoCorsToMessage(cls
, boto_cors
):
525 """Translates a boto Cors object to an apitools message."""
528 for cors_collection
in boto_cors
.cors
:
530 collection_message
= apitools_messages
.Bucket
.CorsValueListEntry()
531 for element_tuple
in cors_collection
:
532 if element_tuple
[0] == boto
.gs
.cors
.MAXAGESEC
:
533 collection_message
.maxAgeSeconds
= int(element_tuple
[1])
534 if element_tuple
[0] == boto
.gs
.cors
.METHODS
:
535 for method_tuple
in element_tuple
[1]:
536 collection_message
.method
.append(method_tuple
[1])
537 if element_tuple
[0] == boto
.gs
.cors
.ORIGINS
:
538 for origin_tuple
in element_tuple
[1]:
539 collection_message
.origin
.append(origin_tuple
[1])
540 if element_tuple
[0] == boto
.gs
.cors
.HEADERS
:
541 for header_tuple
in element_tuple
[1]:
542 collection_message
.responseHeader
.append(header_tuple
[1])
543 message_cors
.append(collection_message
)
547 def JsonCorsToMessageEntries(cls
, json_cors
):
548 """Translates CORS JSON to an apitools message.
551 json_cors: JSON string representing CORS configuration.
554 List of apitools Bucket.CorsValueListEntry. An empty list represents
555 no CORS configuration.
558 deserialized_cors
= json
.loads(json_cors
)
560 for cors_entry
in deserialized_cors
:
561 cors
.append(encoding
.DictToMessage(
562 cors_entry
, apitools_messages
.Bucket
.CorsValueListEntry
))
565 CheckForXmlConfigurationAndRaise('CORS', json_cors
)
568 def MessageEntriesToJson(cls
, cors_message
):
569 """Translates an apitools message to CORS JSON."""
571 # Because CORS is a MessageField, serialize/deserialize as JSON list.
574 for cors_entry
in cors_message
:
579 json_text
+= encoding
.MessageToJson(cors_entry
)
584 def S3MarkerAclFromObjectMetadata(object_metadata
):
585 """Retrieves GUID-marked S3 ACL from object metadata, if present.
588 object_metadata: Object metadata to check.
591 S3 ACL text, if present, None otherwise.
593 if (object_metadata
and object_metadata
.metadata
and
594 object_metadata
.metadata
.additionalProperties
):
595 for prop
in object_metadata
.metadata
.additionalProperties
:
596 if prop
.key
== S3_ACL_MARKER_GUID
:
600 def AddS3MarkerAclToObjectMetadata(object_metadata
, acl_text
):
601 """Adds a GUID-marked S3 ACL to the object metadata.
604 object_metadata: Object metadata to add the acl to.
605 acl_text: S3 ACL text to add.
607 if not object_metadata
.metadata
:
608 object_metadata
.metadata
= apitools_messages
.Object
.MetadataValue()
609 if not object_metadata
.metadata
.additionalProperties
:
610 object_metadata
.metadata
.additionalProperties
= []
612 object_metadata
.metadata
.additionalProperties
.append(
613 apitools_messages
.Object
.MetadataValue
.AdditionalProperty(
614 key
=S3_ACL_MARKER_GUID
, value
=acl_text
))
617 class AclTranslation(object):
618 """Functions for converting between various ACL formats.
620 This class handles conversion to and from Boto ACL objects, JSON text,
621 and apitools Message objects.
624 JSON_TO_XML_ROLES
= {'READER': 'READ', 'WRITER': 'WRITE',
625 'OWNER': 'FULL_CONTROL'}
626 XML_TO_JSON_ROLES
= {'READ': 'READER', 'WRITE': 'WRITER',
627 'FULL_CONTROL': 'OWNER'}
630 def BotoAclFromJson(cls
, acl_json
):
633 acl
.entries
= cls
.BotoEntriesFromJson(acl_json
, acl
)
637 # acl_message is a list of messages, either object or bucketaccesscontrol
638 def BotoAclFromMessage(cls
, acl_message
):
640 for message
in acl_message
:
641 acl_dicts
.append(encoding
.MessageToDict(message
))
642 return cls
.BotoAclFromJson(acl_dicts
)
645 def BotoAclToJson(cls
, acl
):
646 if hasattr(acl
, 'entries'):
647 return cls
.BotoEntriesToJson(acl
.entries
)
651 def BotoObjectAclToMessage(cls
, acl
):
652 for entry
in cls
.BotoAclToJson(acl
):
653 message
= encoding
.DictToMessage(entry
,
654 apitools_messages
.ObjectAccessControl
)
655 message
.kind
= u
'storage#objectAccessControl'
659 def BotoBucketAclToMessage(cls
, acl
):
660 for entry
in cls
.BotoAclToJson(acl
):
661 message
= encoding
.DictToMessage(entry
,
662 apitools_messages
.BucketAccessControl
)
663 message
.kind
= u
'storage#bucketAccessControl'
667 def BotoEntriesFromJson(cls
, acl_json
, parent
):
668 entries
= Entries(parent
)
669 entries
.parent
= parent
670 entries
.entry_list
= [cls
.BotoEntryFromJson(entry_json
)
671 for entry_json
in acl_json
]
675 def BotoEntriesToJson(cls
, entries
):
676 return [cls
.BotoEntryToJson(entry
) for entry
in entries
.entry_list
]
679 def BotoEntryFromJson(cls
, entry_json
):
680 """Converts a JSON entry into a Boto ACL entry."""
681 entity
= entry_json
['entity']
682 permission
= cls
.JSON_TO_XML_ROLES
[entry_json
['role']]
683 if entity
.lower() == ALL_USERS
.lower():
684 return Entry(type=ALL_USERS
, permission
=permission
)
685 elif entity
.lower() == ALL_AUTHENTICATED_USERS
.lower():
686 return Entry(type=ALL_AUTHENTICATED_USERS
, permission
=permission
)
687 elif entity
.startswith('project'):
688 raise CommandException('XML API does not support project scopes, '
689 'cannot translate ACL.')
690 elif 'email' in entry_json
:
691 if entity
.startswith('user'):
692 scope_type
= USER_BY_EMAIL
693 elif entity
.startswith('group'):
694 scope_type
= GROUP_BY_EMAIL
695 return Entry(type=scope_type
, email_address
=entry_json
['email'],
696 permission
=permission
)
697 elif 'entityId' in entry_json
:
698 if entity
.startswith('user'):
699 scope_type
= USER_BY_ID
700 elif entity
.startswith('group'):
701 scope_type
= GROUP_BY_ID
702 return Entry(type=scope_type
, id=entry_json
['entityId'],
703 permission
=permission
)
704 elif 'domain' in entry_json
:
705 if entity
.startswith('domain'):
706 scope_type
= GROUP_BY_DOMAIN
707 return Entry(type=scope_type
, domain
=entry_json
['domain'],
708 permission
=permission
)
709 raise CommandException('Failed to translate JSON ACL to XML.')
712 def BotoEntryToJson(cls
, entry
):
713 """Converts a Boto ACL entry to a valid JSON dictionary."""
715 # JSON API documentation uses camel case.
716 scope_type_lower
= entry
.scope
.type.lower()
717 if scope_type_lower
== ALL_USERS
.lower():
718 acl_entry_json
['entity'] = 'allUsers'
719 elif scope_type_lower
== ALL_AUTHENTICATED_USERS
.lower():
720 acl_entry_json
['entity'] = 'allAuthenticatedUsers'
721 elif scope_type_lower
== USER_BY_EMAIL
.lower():
722 acl_entry_json
['entity'] = 'user-%s' % entry
.scope
.email_address
723 acl_entry_json
['email'] = entry
.scope
.email_address
724 elif scope_type_lower
== USER_BY_ID
.lower():
725 acl_entry_json
['entity'] = 'user-%s' % entry
.scope
.id
726 acl_entry_json
['entityId'] = entry
.scope
.id
727 elif scope_type_lower
== GROUP_BY_EMAIL
.lower():
728 acl_entry_json
['entity'] = 'group-%s' % entry
.scope
.email_address
729 acl_entry_json
['email'] = entry
.scope
.email_address
730 elif scope_type_lower
== GROUP_BY_ID
.lower():
731 acl_entry_json
['entity'] = 'group-%s' % entry
.scope
.id
732 acl_entry_json
['entityId'] = entry
.scope
.id
733 elif scope_type_lower
== GROUP_BY_DOMAIN
.lower():
734 acl_entry_json
['entity'] = 'domain-%s' % entry
.scope
.domain
735 acl_entry_json
['domain'] = entry
.scope
.domain
737 raise ArgumentException('ACL contains invalid scope type: %s' %
740 acl_entry_json
['role'] = cls
.XML_TO_JSON_ROLES
[entry
.permission
]
741 return acl_entry_json
744 def JsonToMessage(cls
, json_data
, message_type
):
745 """Converts the input JSON data into list of Object/BucketAccessControls.
748 json_data: String of JSON to convert.
749 message_type: Which type of access control entries to return,
750 either ObjectAccessControl or BucketAccessControl.
753 ArgumentException on invalid JSON data.
756 List of ObjectAccessControl or BucketAccessControl elements.
759 deserialized_acl
= json
.loads(json_data
)
762 for acl_entry
in deserialized_acl
:
763 acl
.append(encoding
.DictToMessage(acl_entry
, message_type
))
766 CheckForXmlConfigurationAndRaise('ACL', json_data
)
769 def JsonFromMessage(cls
, acl
):
770 """Strips unnecessary fields from an ACL message and returns valid JSON.
773 acl: iterable ObjectAccessControl or BucketAccessControl
778 serializable_acl
= []
780 for acl_entry
in acl
:
781 if acl_entry
.kind
== u
'storage#objectAccessControl':
782 acl_entry
.object = None
783 acl_entry
.generation
= None
784 acl_entry
.kind
= None
785 acl_entry
.bucket
= None
787 acl_entry
.selfLink
= None
788 acl_entry
.etag
= None
789 serializable_acl
.append(encoding
.MessageToDict(acl_entry
))
790 return json
.dumps(serializable_acl
, sort_keys
=True,
791 indent
=2, separators
=(',', ': '))