OLD | NEW |
| (Empty) |
1 # -*- coding: utf-8 -*- | |
2 # Copyright 2014 Google Inc. All Rights Reserved. | |
3 # | |
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 | |
7 # | |
8 # http://www.apache.org/licenses/LICENSE-2.0 | |
9 # | |
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.""" | |
16 | |
17 from __future__ import absolute_import | |
18 | |
19 import datetime | |
20 import json | |
21 import re | |
22 import textwrap | |
23 import xml.etree.ElementTree | |
24 | |
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 | |
37 | |
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_m
essages | |
43 | |
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 | |
50 | |
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) | |
65 | |
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' | |
72 | |
73 DEFAULT_CONTENT_TYPE = 'application/octet-stream' | |
74 | |
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'])] | |
87 | |
88 | |
89 def ObjectMetadataFromHeaders(headers): | |
90 """Creates object metadata according to the provided headers. | |
91 | |
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. | |
95 | |
96 Args: | |
97 headers: Dict of headers passed via gsutil -h | |
98 | |
99 Raises: | |
100 ArgumentException if an invalid header is encountered. | |
101 | |
102 Returns: | |
103 apitools Object with relevant fields populated from headers. | |
104 """ | |
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 | |
161 | |
162 | |
163 def HeadersFromObjectMetadata(dst_obj_metadata, provider): | |
164 """Creates a header dictionary based on existing object metadata. | |
165 | |
166 Args: | |
167 dst_obj_metadata: Object metadata to create the headers from. | |
168 provider: Provider string ('gs' or 's3') | |
169 | |
170 Returns: | |
171 Headers dictionary. | |
172 """ | |
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 | |
234 | |
235 | |
236 def CopyObjectMetadata(src_obj_metadata, dst_obj_metadata, override=False): | |
237 """Copies metadata from src_obj_metadata to dst_obj_metadata. | |
238 | |
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. | |
245 """ | |
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 | |
258 | |
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)) | |
288 | |
289 | |
290 def PreconditionsFromHeaders(headers): | |
291 """Creates bucket or object preconditions acccording to the provided headers. | |
292 | |
293 Args: | |
294 headers: Dict of headers passed via gsutil -h | |
295 | |
296 Returns: | |
297 gsutil Cloud API Preconditions object fields populated from headers, or None | |
298 if no precondition headers are present. | |
299 """ | |
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 | |
313 | |
314 | |
315 def CreateBucketNotFoundException(code, provider, bucket_name): | |
316 return NotFoundException('%s://%s bucket does not exist.' % | |
317 (provider, bucket_name), status=code) | |
318 | |
319 | |
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) | |
326 | |
327 | |
328 def EncodeStringAsLong(string_to_convert): | |
329 """Encodes an ASCII string as a python long. | |
330 | |
331 This is used for modeling S3 version_id's as apitools generation. Because | |
332 python longs can be arbitrarily large, this works. | |
333 | |
334 Args: | |
335 string_to_convert: ASCII string to convert to a long. | |
336 | |
337 Returns: | |
338 Long that represents the input string. | |
339 """ | |
340 return long(string_to_convert.encode('hex'), 16) | |
341 | |
342 | |
343 def _DecodeLongAsString(long_to_convert): | |
344 """Decodes an encoded python long into an ASCII string. | |
345 | |
346 This is used for modeling S3 version_id's as apitools generation. | |
347 | |
348 Args: | |
349 long_to_convert: long to convert to ASCII string. If this is already a | |
350 string, it is simply returned. | |
351 | |
352 Returns: | |
353 String decoded from the input long. | |
354 """ | |
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') | |
359 | |
360 | |
361 def GenerationFromUrlAndString(url, generation): | |
362 """Decodes a generation from a StorageURL and a generation string. | |
363 | |
364 This is used to represent gs and s3 versioning. | |
365 | |
366 Args: | |
367 url: StorageUrl representing the object. | |
368 generation: Long or string representing the object's generation or | |
369 version. | |
370 | |
371 Returns: | |
372 Valid generation string for use in URLs. | |
373 """ | |
374 if url.scheme == 's3' and generation: | |
375 return _DecodeLongAsString(generation) | |
376 return generation | |
377 | |
378 | |
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)) | |
394 | |
395 | |
396 class LifecycleTranslation(object): | |
397 """Functions for converting between various lifecycle formats. | |
398 | |
399 This class handles conversation to and from Boto Cors objects, JSON text, | |
400 and apitools Message objects. | |
401 """ | |
402 | |
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 | |
428 | |
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 | |
459 | |
460 @classmethod | |
461 def JsonLifecycleFromMessage(cls, lifecycle_message): | |
462 """Translates an apitools message to lifecycle JSON.""" | |
463 return str(encoding.MessageToJson(lifecycle_message)) + '\n' | |
464 | |
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) | |
481 | |
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() | |
486 | |
487 | |
488 class CorsTranslation(object): | |
489 """Functions for converting between various CORS formats. | |
490 | |
491 This class handles conversation to and from Boto Cors objects, JSON text, | |
492 and apitools Message objects. | |
493 """ | |
494 | |
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 | |
522 | |
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 | |
545 | |
546 @classmethod | |
547 def JsonCorsToMessageEntries(cls, json_cors): | |
548 """Translates CORS JSON to an apitools message. | |
549 | |
550 Args: | |
551 json_cors: JSON string representing CORS configuration. | |
552 | |
553 Returns: | |
554 List of apitools Bucket.CorsValueListEntry. An empty list represents | |
555 no CORS configuration. | |
556 """ | |
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) | |
566 | |
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 | |
582 | |
583 | |
584 def S3MarkerAclFromObjectMetadata(object_metadata): | |
585 """Retrieves GUID-marked S3 ACL from object metadata, if present. | |
586 | |
587 Args: | |
588 object_metadata: Object metadata to check. | |
589 | |
590 Returns: | |
591 S3 ACL text, if present, None otherwise. | |
592 """ | |
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 | |
598 | |
599 | |
600 def AddS3MarkerAclToObjectMetadata(object_metadata, acl_text): | |
601 """Adds a GUID-marked S3 ACL to the object metadata. | |
602 | |
603 Args: | |
604 object_metadata: Object metadata to add the acl to. | |
605 acl_text: S3 ACL text to add. | |
606 """ | |
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 = [] | |
611 | |
612 object_metadata.metadata.additionalProperties.append( | |
613 apitools_messages.Object.MetadataValue.AdditionalProperty( | |
614 key=S3_ACL_MARKER_GUID, value=acl_text)) | |
615 | |
616 | |
617 class AclTranslation(object): | |
618 """Functions for converting between various ACL formats. | |
619 | |
620 This class handles conversion to and from Boto ACL objects, JSON text, | |
621 and apitools Message objects. | |
622 """ | |
623 | |
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'} | |
628 | |
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 | |
635 | |
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) | |
643 | |
644 @classmethod | |
645 def BotoAclToJson(cls, acl): | |
646 if hasattr(acl, 'entries'): | |
647 return cls.BotoEntriesToJson(acl.entries) | |
648 return [] | |
649 | |
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 | |
657 | |
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 | |
665 | |
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 | |
673 | |
674 @classmethod | |
675 def BotoEntriesToJson(cls, entries): | |
676 return [cls.BotoEntryToJson(entry) for entry in entries.entry_list] | |
677 | |
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.') | |
710 | |
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) | |
739 | |
740 acl_entry_json['role'] = cls.XML_TO_JSON_ROLES[entry.permission] | |
741 return acl_entry_json | |
742 | |
743 @classmethod | |
744 def JsonToMessage(cls, json_data, message_type): | |
745 """Converts the input JSON data into list of Object/BucketAccessControls. | |
746 | |
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. | |
751 | |
752 Raises: | |
753 ArgumentException on invalid JSON data. | |
754 | |
755 Returns: | |
756 List of ObjectAccessControl or BucketAccessControl elements. | |
757 """ | |
758 try: | |
759 deserialized_acl = json.loads(json_data) | |
760 | |
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) | |
767 | |
768 @classmethod | |
769 def JsonFromMessage(cls, acl): | |
770 """Strips unnecessary fields from an ACL message and returns valid JSON. | |
771 | |
772 Args: | |
773 acl: iterable ObjectAccessControl or BucketAccessControl | |
774 | |
775 Returns: | |
776 ACL JSON string. | |
777 """ | |
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=(',', ': ')) | |
OLD | NEW |