Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / tools / telemetry / third_party / gsutilz / gslib / translation_helper.py
blob91adc83a3720d82b94a2ffc723b35aed49587708
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
19 import datetime
20 import json
21 import re
22 import textwrap
23 import xml.etree.ElementTree
25 from apitools.base.py import encoding
26 import boto
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
46 try:
47 from xml.etree.ElementTree import ParseError as XmlParseError
48 except ImportError:
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>.*)',
62 re.I)
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.
96 Args:
97 headers: Dict of headers passed via gsutil -h
99 Raises:
100 ArgumentException if an invalid header is encountered.
102 Returns:
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):
118 if not value:
119 obj_metadata.contentType = DEFAULT_CONTENT_TYPE
120 else:
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.
125 continue
126 elif GOOG_GENERATION_MATCH_REGEX.match(header):
127 # Preconditions are handled elsewhere, but allow these headers through.
128 continue
129 elif GOOG_METAGENERATION_MATCH_REGEX.match(header):
130 # Preconditions are handled elsewhere, but allow these headers through.
131 continue
132 else:
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)
136 header_key = None
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'))
146 if header_key:
147 if header_key.lower() == 'x-goog-content-language':
148 # Work around content-language being inserted into custom metadata.
149 continue
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))
157 else:
158 raise ArgumentException(
159 'Invalid header specifed: %s:%s' % (header, value))
160 return obj_metadata
163 def HeadersFromObjectMetadata(dst_obj_metadata, provider):
164 """Creates a header dictionary based on existing object metadata.
166 Args:
167 dst_obj_metadata: Object metadata to create the headers from.
168 provider: Provider string ('gs' or 's3')
170 Returns:
171 Headers dictionary.
173 headers = {}
174 if not dst_obj_metadata:
175 return
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
180 else:
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
185 else:
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
191 else:
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
196 else:
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
201 else:
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
206 else:
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
212 # the XML API.
213 if additional_property.key == 'content-language':
214 continue
215 # Don't translate special metadata markers.
216 if additional_property.key in S3_MARKER_GUIDS:
217 continue
218 if provider == 'gs':
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):])
224 else:
225 header_name = 'x-amz-meta-' + additional_property.key
226 else:
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
231 else:
232 headers[header_name] = additional_property.value
233 return headers
236 def CopyObjectMetadata(src_obj_metadata, dst_obj_metadata, override=False):
237 """Copies metadata from src_obj_metadata to dst_obj_metadata.
239 Args:
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
244 exist.
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:
274 if override:
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
278 else:
279 dst_metadata_dict[src_prop.key] = src_prop.value
280 else:
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,
287 value=v))
290 def PreconditionsFromHeaders(headers):
291 """Creates bucket or object preconditions acccording to the provided headers.
293 Args:
294 headers: Dict of headers passed via gsutil -h
296 Returns:
297 gsutil Cloud API Preconditions object fields populated from headers, or None
298 if no precondition headers are present.
300 return_preconditions = Preconditions()
301 try:
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,
321 generation=None):
322 uri_string = '%s://%s/%s' % (provider, bucket_name, object_name)
323 if generation:
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.
334 Args:
335 string_to_convert: ASCII string to convert to a long.
337 Returns:
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.
348 Args:
349 long_to_convert: long to convert to ASCII string. If this is already a
350 string, it is simply returned.
352 Returns:
353 String decoded from the input long.
355 if isinstance(long_to_convert, basestring):
356 # Already converted.
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.
366 Args:
367 url: StorageUrl representing the object.
368 generation: Long or string representing the object's generation or
369 version.
371 Returns:
372 Valid generation string for use in URLs.
374 if url.scheme == 's3' and generation:
375 return _DecodeLongAsString(generation)
376 return generation
379 def CheckForXmlConfigurationAndRaise(config_type_string, json_txt):
380 """Checks a JSON parse exception for provided XML configuration."""
381 try:
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:
391 pass
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.
403 @classmethod
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
429 @classmethod
430 def BotoLifecycleToMessage(cls, boto_lifecycle):
431 """Translates a boto lifecycle object to an apitools message."""
432 lifecycle_message = None
433 if boto_lifecycle:
434 lifecycle_message = apitools_messages.Bucket.LifecycleValue()
435 for boto_rule in boto_lifecycle:
436 lifecycle_rule = (
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(
443 type='Delete'))
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
460 @classmethod
461 def JsonLifecycleFromMessage(cls, lifecycle_message):
462 """Translates an apitools message to lifecycle JSON."""
463 return str(encoding.MessageToJson(lifecycle_message)) + '\n'
465 @classmethod
466 def JsonLifecycleToMessage(cls, json_txt):
467 """Translates lifecycle JSON to an apitools message."""
468 try:
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
473 # {'rule': ...
474 if 'lifecycle' in deserialized_lifecycle:
475 deserialized_lifecycle = deserialized_lifecycle['lifecycle']
476 lifecycle = encoding.DictToMessage(
477 deserialized_lifecycle, apitools_messages.Bucket.LifecycleValue)
478 return lifecycle
479 except ValueError:
480 CheckForXmlConfigurationAndRaise('lifecycle', json_txt)
482 @classmethod
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.
495 @classmethod
496 def BotoCorsFromMessage(cls, cors_message):
497 """Translates an apitools message to a boto Cors object."""
498 cors = boto.gs.cors.Cors()
499 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:
506 method_elements = []
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:
511 origin_elements = []
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:
516 header_elements = []
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)
521 return cors
523 @classmethod
524 def BotoCorsToMessage(cls, boto_cors):
525 """Translates a boto Cors object to an apitools message."""
526 message_cors = []
527 if boto_cors.cors:
528 for cors_collection in boto_cors.cors:
529 if cors_collection:
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)
544 return message_cors
546 @classmethod
547 def JsonCorsToMessageEntries(cls, json_cors):
548 """Translates CORS JSON to an apitools message.
550 Args:
551 json_cors: JSON string representing CORS configuration.
553 Returns:
554 List of apitools Bucket.CorsValueListEntry. An empty list represents
555 no CORS configuration.
557 try:
558 deserialized_cors = json.loads(json_cors)
559 cors = []
560 for cors_entry in deserialized_cors:
561 cors.append(encoding.DictToMessage(
562 cors_entry, apitools_messages.Bucket.CorsValueListEntry))
563 return cors
564 except ValueError:
565 CheckForXmlConfigurationAndRaise('CORS', json_cors)
567 @classmethod
568 def MessageEntriesToJson(cls, cors_message):
569 """Translates an apitools message to CORS JSON."""
570 json_text = ''
571 # Because CORS is a MessageField, serialize/deserialize as JSON list.
572 json_text += '['
573 printed_one = False
574 for cors_entry in cors_message:
575 if printed_one:
576 json_text += ','
577 else:
578 printed_one = True
579 json_text += encoding.MessageToJson(cors_entry)
580 json_text += ']\n'
581 return json_text
584 def S3MarkerAclFromObjectMetadata(object_metadata):
585 """Retrieves GUID-marked S3 ACL from object metadata, if present.
587 Args:
588 object_metadata: Object metadata to check.
590 Returns:
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:
597 return prop.value
600 def AddS3MarkerAclToObjectMetadata(object_metadata, acl_text):
601 """Adds a GUID-marked S3 ACL to the object metadata.
603 Args:
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'}
629 @classmethod
630 def BotoAclFromJson(cls, acl_json):
631 acl = ACL()
632 acl.parent = None
633 acl.entries = cls.BotoEntriesFromJson(acl_json, acl)
634 return acl
636 @classmethod
637 # acl_message is a list of messages, either object or bucketaccesscontrol
638 def BotoAclFromMessage(cls, acl_message):
639 acl_dicts = []
640 for message in acl_message:
641 acl_dicts.append(encoding.MessageToDict(message))
642 return cls.BotoAclFromJson(acl_dicts)
644 @classmethod
645 def BotoAclToJson(cls, acl):
646 if hasattr(acl, 'entries'):
647 return cls.BotoEntriesToJson(acl.entries)
648 return []
650 @classmethod
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'
656 yield message
658 @classmethod
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'
664 yield message
666 @classmethod
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]
672 return entries
674 @classmethod
675 def BotoEntriesToJson(cls, entries):
676 return [cls.BotoEntryToJson(entry) for entry in entries.entry_list]
678 @classmethod
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.')
711 @classmethod
712 def BotoEntryToJson(cls, entry):
713 """Converts a Boto ACL entry to a valid JSON dictionary."""
714 acl_entry_json = {}
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
736 else:
737 raise ArgumentException('ACL contains invalid scope type: %s' %
738 scope_type_lower)
740 acl_entry_json['role'] = cls.XML_TO_JSON_ROLES[entry.permission]
741 return acl_entry_json
743 @classmethod
744 def JsonToMessage(cls, json_data, message_type):
745 """Converts the input JSON data into list of Object/BucketAccessControls.
747 Args:
748 json_data: String of JSON to convert.
749 message_type: Which type of access control entries to return,
750 either ObjectAccessControl or BucketAccessControl.
752 Raises:
753 ArgumentException on invalid JSON data.
755 Returns:
756 List of ObjectAccessControl or BucketAccessControl elements.
758 try:
759 deserialized_acl = json.loads(json_data)
761 acl = []
762 for acl_entry in deserialized_acl:
763 acl.append(encoding.DictToMessage(acl_entry, message_type))
764 return acl
765 except ValueError:
766 CheckForXmlConfigurationAndRaise('ACL', json_data)
768 @classmethod
769 def JsonFromMessage(cls, acl):
770 """Strips unnecessary fields from an ACL message and returns valid JSON.
772 Args:
773 acl: iterable ObjectAccessControl or BucketAccessControl
775 Returns:
776 ACL JSON string.
778 serializable_acl = []
779 if acl is not None:
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
786 acl_entry.id = 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=(',', ': '))