OLD | NEW |
| (Empty) |
1 # -*- coding: utf-8 -*- | |
2 # Copyright 2013 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 """XML/boto gsutil Cloud API implementation for GCS and Amazon S3.""" | |
16 | |
17 from __future__ import absolute_import | |
18 | |
19 import base64 | |
20 import binascii | |
21 import datetime | |
22 import errno | |
23 import httplib | |
24 import json | |
25 import multiprocessing | |
26 import os | |
27 import pickle | |
28 import random | |
29 import re | |
30 import socket | |
31 import tempfile | |
32 import textwrap | |
33 import time | |
34 import xml | |
35 from xml.dom.minidom import parseString as XmlParseString | |
36 from xml.sax import _exceptions as SaxExceptions | |
37 | |
38 import boto | |
39 from boto import handler | |
40 from boto.exception import ResumableDownloadException as BotoResumableDownloadEx
ception | |
41 from boto.exception import ResumableTransferDisposition | |
42 from boto.gs.cors import Cors | |
43 from boto.gs.lifecycle import LifecycleConfig | |
44 from boto.s3.cors import CORSConfiguration as S3Cors | |
45 from boto.s3.deletemarker import DeleteMarker | |
46 from boto.s3.lifecycle import Lifecycle as S3Lifecycle | |
47 from boto.s3.prefix import Prefix | |
48 | |
49 from gslib.boto_resumable_upload import BotoResumableUpload | |
50 from gslib.cloud_api import AccessDeniedException | |
51 from gslib.cloud_api import ArgumentException | |
52 from gslib.cloud_api import BadRequestException | |
53 from gslib.cloud_api import CloudApi | |
54 from gslib.cloud_api import NotEmptyException | |
55 from gslib.cloud_api import NotFoundException | |
56 from gslib.cloud_api import PreconditionException | |
57 from gslib.cloud_api import ResumableDownloadException | |
58 from gslib.cloud_api import ResumableUploadAbortException | |
59 from gslib.cloud_api import ResumableUploadException | |
60 from gslib.cloud_api import ResumableUploadStartOverException | |
61 from gslib.cloud_api import ServiceException | |
62 from gslib.cloud_api_helper import ValidateDstObjectMetadata | |
63 # Imported for boto AuthHandler purposes. | |
64 import gslib.devshell_auth_plugin # pylint: disable=unused-import | |
65 from gslib.exception import CommandException | |
66 from gslib.exception import InvalidUrlError | |
67 from gslib.hashing_helper import Base64EncodeHash | |
68 from gslib.hashing_helper import Base64ToHexHash | |
69 from gslib.project_id import GOOG_PROJ_ID_HDR | |
70 from gslib.project_id import PopulateProjectId | |
71 from gslib.storage_url import StorageUrlFromString | |
72 from gslib.third_party.storage_apitools import storage_v1_messages as apitools_m
essages | |
73 from gslib.translation_helper import AclTranslation | |
74 from gslib.translation_helper import AddS3MarkerAclToObjectMetadata | |
75 from gslib.translation_helper import CorsTranslation | |
76 from gslib.translation_helper import CreateBucketNotFoundException | |
77 from gslib.translation_helper import CreateObjectNotFoundException | |
78 from gslib.translation_helper import DEFAULT_CONTENT_TYPE | |
79 from gslib.translation_helper import EncodeStringAsLong | |
80 from gslib.translation_helper import GenerationFromUrlAndString | |
81 from gslib.translation_helper import HeadersFromObjectMetadata | |
82 from gslib.translation_helper import LifecycleTranslation | |
83 from gslib.translation_helper import REMOVE_CORS_CONFIG | |
84 from gslib.translation_helper import S3MarkerAclFromObjectMetadata | |
85 from gslib.util import ConfigureNoOpAuthIfNeeded | |
86 from gslib.util import DEFAULT_FILE_BUFFER_SIZE | |
87 from gslib.util import GetFileSize | |
88 from gslib.util import GetMaxRetryDelay | |
89 from gslib.util import GetNumRetries | |
90 from gslib.util import MultiprocessingIsAvailable | |
91 from gslib.util import S3_DELETE_MARKER_GUID | |
92 from gslib.util import TWO_MIB | |
93 from gslib.util import UnaryDictToXml | |
94 from gslib.util import UTF8 | |
95 from gslib.util import XML_PROGRESS_CALLBACKS | |
96 | |
97 TRANSLATABLE_BOTO_EXCEPTIONS = (boto.exception.BotoServerError, | |
98 boto.exception.InvalidUriError, | |
99 boto.exception.ResumableDownloadException, | |
100 boto.exception.ResumableUploadException, | |
101 boto.exception.StorageCreateError, | |
102 boto.exception.StorageResponseError) | |
103 | |
104 # If multiprocessing is available, this will be overridden to a (thread-safe) | |
105 # multiprocessing.Value in a call to InitializeMultiprocessingVariables. | |
106 boto_auth_initialized = False | |
107 | |
108 NON_EXISTENT_OBJECT_REGEX = re.compile(r'.*non-\s*existent\s*object', | |
109 flags=re.DOTALL) | |
110 # Determines whether an etag is a valid MD5. | |
111 MD5_REGEX = re.compile(r'^"*[a-fA-F0-9]{32}"*$') | |
112 | |
113 | |
114 def InitializeMultiprocessingVariables(): | |
115 """Perform necessary initialization for multiprocessing. | |
116 | |
117 See gslib.command.InitializeMultiprocessingVariables for an explanation | |
118 of why this is necessary. | |
119 """ | |
120 global boto_auth_initialized # pylint: disable=global-variable-undefined | |
121 boto_auth_initialized = multiprocessing.Value('i', 0) | |
122 | |
123 | |
124 class BotoTranslation(CloudApi): | |
125 """Boto-based XML translation implementation of gsutil Cloud API. | |
126 | |
127 This class takes gsutil Cloud API objects, translates them to XML service | |
128 calls, and translates the results back into gsutil Cloud API objects for | |
129 use by the caller. | |
130 """ | |
131 | |
132 def __init__(self, bucket_storage_uri_class, logger, provider=None, | |
133 credentials=None, debug=0): | |
134 """Performs necessary setup for interacting with the cloud storage provider. | |
135 | |
136 Args: | |
137 bucket_storage_uri_class: boto storage_uri class, used by APIs that | |
138 provide boto translation or mocking. | |
139 logger: logging.logger for outputting log messages. | |
140 provider: Provider prefix describing cloud storage provider to connect to. | |
141 'gs' and 's3' are supported. Function implementations ignore | |
142 the provider argument and use this one instead. | |
143 credentials: Unused. | |
144 debug: Debug level for the API implementation (0..3). | |
145 """ | |
146 super(BotoTranslation, self).__init__(bucket_storage_uri_class, logger, | |
147 provider=provider, debug=debug) | |
148 _ = credentials | |
149 global boto_auth_initialized # pylint: disable=global-variable-undefined | |
150 if MultiprocessingIsAvailable()[0] and not boto_auth_initialized.value: | |
151 ConfigureNoOpAuthIfNeeded() | |
152 boto_auth_initialized.value = 1 | |
153 elif not boto_auth_initialized: | |
154 ConfigureNoOpAuthIfNeeded() | |
155 boto_auth_initialized = True | |
156 self.api_version = boto.config.get_value( | |
157 'GSUtil', 'default_api_version', '1') | |
158 | |
159 def GetBucket(self, bucket_name, provider=None, fields=None): | |
160 """See CloudApi class for function doc strings.""" | |
161 _ = provider | |
162 bucket_uri = self._StorageUriForBucket(bucket_name) | |
163 headers = {} | |
164 self._AddApiVersionToHeaders(headers) | |
165 try: | |
166 return self._BotoBucketToBucket(bucket_uri.get_bucket(validate=True, | |
167 headers=headers), | |
168 fields=fields) | |
169 except TRANSLATABLE_BOTO_EXCEPTIONS, e: | |
170 self._TranslateExceptionAndRaise(e, bucket_name=bucket_name) | |
171 | |
172 def ListBuckets(self, project_id=None, provider=None, fields=None): | |
173 """See CloudApi class for function doc strings.""" | |
174 _ = provider | |
175 get_fields = self._ListToGetFields(list_fields=fields) | |
176 headers = {} | |
177 self._AddApiVersionToHeaders(headers) | |
178 if self.provider == 'gs': | |
179 headers[GOOG_PROJ_ID_HDR] = PopulateProjectId(project_id) | |
180 try: | |
181 provider_uri = boto.storage_uri( | |
182 '%s://' % self.provider, | |
183 suppress_consec_slashes=False, | |
184 bucket_storage_uri_class=self.bucket_storage_uri_class, | |
185 debug=self.debug) | |
186 | |
187 buckets_iter = provider_uri.get_all_buckets(headers=headers) | |
188 for bucket in buckets_iter: | |
189 if self.provider == 's3' and bucket.name.lower() != bucket.name: | |
190 # S3 listings can return buckets with upper-case names, but boto | |
191 # can't successfully call them. | |
192 continue | |
193 yield self._BotoBucketToBucket(bucket, fields=get_fields) | |
194 except TRANSLATABLE_BOTO_EXCEPTIONS, e: | |
195 self._TranslateExceptionAndRaise(e) | |
196 | |
197 def PatchBucket(self, bucket_name, metadata, canned_acl=None, | |
198 canned_def_acl=None, preconditions=None, provider=None, | |
199 fields=None): | |
200 """See CloudApi class for function doc strings.""" | |
201 _ = provider | |
202 bucket_uri = self._StorageUriForBucket(bucket_name) | |
203 headers = {} | |
204 self._AddApiVersionToHeaders(headers) | |
205 try: | |
206 self._AddPreconditionsToHeaders(preconditions, headers) | |
207 if metadata.acl: | |
208 boto_acl = AclTranslation.BotoAclFromMessage(metadata.acl) | |
209 bucket_uri.set_xml_acl(boto_acl.to_xml(), headers=headers) | |
210 if canned_acl: | |
211 canned_acls = bucket_uri.canned_acls() | |
212 if canned_acl not in canned_acls: | |
213 raise CommandException('Invalid canned ACL "%s".' % canned_acl) | |
214 bucket_uri.set_acl(canned_acl, bucket_uri.object_name) | |
215 if canned_def_acl: | |
216 canned_acls = bucket_uri.canned_acls() | |
217 if canned_def_acl not in canned_acls: | |
218 raise CommandException('Invalid canned ACL "%s".' % canned_def_acl) | |
219 bucket_uri.set_def_acl(canned_def_acl, bucket_uri.object_name) | |
220 if metadata.cors: | |
221 if metadata.cors == REMOVE_CORS_CONFIG: | |
222 metadata.cors = [] | |
223 boto_cors = CorsTranslation.BotoCorsFromMessage(metadata.cors) | |
224 bucket_uri.set_cors(boto_cors, False) | |
225 if metadata.defaultObjectAcl: | |
226 boto_acl = AclTranslation.BotoAclFromMessage( | |
227 metadata.defaultObjectAcl) | |
228 bucket_uri.set_def_xml_acl(boto_acl.to_xml(), headers=headers) | |
229 if metadata.lifecycle: | |
230 boto_lifecycle = LifecycleTranslation.BotoLifecycleFromMessage( | |
231 metadata.lifecycle) | |
232 bucket_uri.configure_lifecycle(boto_lifecycle, False) | |
233 if metadata.logging: | |
234 if self.provider == 'gs': | |
235 headers[GOOG_PROJ_ID_HDR] = PopulateProjectId(None) | |
236 if metadata.logging.logBucket and metadata.logging.logObjectPrefix: | |
237 bucket_uri.enable_logging(metadata.logging.logBucket, | |
238 metadata.logging.logObjectPrefix, | |
239 False, headers) | |
240 else: # Logging field is present and empty. Disable logging. | |
241 bucket_uri.disable_logging(False, headers) | |
242 if metadata.versioning: | |
243 bucket_uri.configure_versioning(metadata.versioning.enabled, | |
244 headers=headers) | |
245 if metadata.website: | |
246 main_page_suffix = metadata.website.mainPageSuffix | |
247 error_page = metadata.website.notFoundPage | |
248 bucket_uri.set_website_config(main_page_suffix, error_page) | |
249 return self.GetBucket(bucket_name, fields=fields) | |
250 except TRANSLATABLE_BOTO_EXCEPTIONS, e: | |
251 self._TranslateExceptionAndRaise(e, bucket_name=bucket_name) | |
252 | |
253 def CreateBucket(self, bucket_name, project_id=None, metadata=None, | |
254 provider=None, fields=None): | |
255 """See CloudApi class for function doc strings.""" | |
256 _ = provider | |
257 bucket_uri = self._StorageUriForBucket(bucket_name) | |
258 location = '' | |
259 if metadata and metadata.location: | |
260 location = metadata.location | |
261 # Pass storage_class param only if this is a GCS bucket. (In S3 the | |
262 # storage class is specified on the key object.) | |
263 headers = {} | |
264 if bucket_uri.scheme == 'gs': | |
265 self._AddApiVersionToHeaders(headers) | |
266 headers[GOOG_PROJ_ID_HDR] = PopulateProjectId(project_id) | |
267 storage_class = '' | |
268 if metadata and metadata.storageClass: | |
269 storage_class = metadata.storageClass | |
270 try: | |
271 bucket_uri.create_bucket(headers=headers, location=location, | |
272 storage_class=storage_class) | |
273 except TRANSLATABLE_BOTO_EXCEPTIONS, e: | |
274 self._TranslateExceptionAndRaise(e, bucket_name=bucket_name) | |
275 else: | |
276 try: | |
277 bucket_uri.create_bucket(headers=headers, location=location) | |
278 except TRANSLATABLE_BOTO_EXCEPTIONS, e: | |
279 self._TranslateExceptionAndRaise(e, bucket_name=bucket_name) | |
280 return self.GetBucket(bucket_name, fields=fields) | |
281 | |
282 def DeleteBucket(self, bucket_name, preconditions=None, provider=None): | |
283 """See CloudApi class for function doc strings.""" | |
284 _ = provider, preconditions | |
285 bucket_uri = self._StorageUriForBucket(bucket_name) | |
286 headers = {} | |
287 self._AddApiVersionToHeaders(headers) | |
288 try: | |
289 bucket_uri.delete_bucket(headers=headers) | |
290 except TRANSLATABLE_BOTO_EXCEPTIONS, e: | |
291 translated_exception = self._TranslateBotoException( | |
292 e, bucket_name=bucket_name) | |
293 if (translated_exception and | |
294 'BucketNotEmpty' in translated_exception.reason): | |
295 try: | |
296 if bucket_uri.get_versioning_config(): | |
297 if self.provider == 's3': | |
298 raise NotEmptyException( | |
299 'VersionedBucketNotEmpty (%s). Currently, gsutil does not ' | |
300 'support listing or removing S3 DeleteMarkers, so you may ' | |
301 'need to delete these using another tool to successfully ' | |
302 'delete this bucket.' % bucket_name, status=e.status) | |
303 raise NotEmptyException( | |
304 'VersionedBucketNotEmpty (%s)' % bucket_name, status=e.status) | |
305 else: | |
306 raise NotEmptyException('BucketNotEmpty (%s)' % bucket_name, | |
307 status=e.status) | |
308 except TRANSLATABLE_BOTO_EXCEPTIONS, e2: | |
309 self._TranslateExceptionAndRaise(e2, bucket_name=bucket_name) | |
310 elif translated_exception and translated_exception.status == 404: | |
311 raise NotFoundException('Bucket %s does not exist.' % bucket_name) | |
312 else: | |
313 self._TranslateExceptionAndRaise(e, bucket_name=bucket_name) | |
314 | |
315 def ListObjects(self, bucket_name, prefix=None, delimiter=None, | |
316 all_versions=None, provider=None, fields=None): | |
317 """See CloudApi class for function doc strings.""" | |
318 _ = provider | |
319 get_fields = self._ListToGetFields(list_fields=fields) | |
320 bucket_uri = self._StorageUriForBucket(bucket_name) | |
321 prefix_list = [] | |
322 headers = {} | |
323 self._AddApiVersionToHeaders(headers) | |
324 try: | |
325 objects_iter = bucket_uri.list_bucket(prefix=prefix or '', | |
326 delimiter=delimiter or '', | |
327 all_versions=all_versions, | |
328 headers=headers) | |
329 except TRANSLATABLE_BOTO_EXCEPTIONS, e: | |
330 self._TranslateExceptionAndRaise(e, bucket_name=bucket_name) | |
331 | |
332 try: | |
333 for key in objects_iter: | |
334 if isinstance(key, Prefix): | |
335 prefix_list.append(key.name) | |
336 yield CloudApi.CsObjectOrPrefix(key.name, | |
337 CloudApi.CsObjectOrPrefixType.PREFIX) | |
338 else: | |
339 key_to_convert = key | |
340 | |
341 # Listed keys are populated with these fields during bucket listing. | |
342 key_http_fields = set(['bucket', 'etag', 'name', 'updated', | |
343 'generation', 'metageneration', 'size']) | |
344 | |
345 # When fields == None, the caller is requesting all possible fields. | |
346 # If the caller requested any fields that are not populated by bucket | |
347 # listing, we'll need to make a separate HTTP call for each object to | |
348 # get its metadata and populate the remaining fields with the result. | |
349 if not get_fields or (get_fields and not | |
350 get_fields.issubset(key_http_fields)): | |
351 | |
352 generation = None | |
353 if getattr(key, 'generation', None): | |
354 generation = key.generation | |
355 if getattr(key, 'version_id', None): | |
356 generation = key.version_id | |
357 key_to_convert = self._GetBotoKey(bucket_name, key.name, | |
358 generation=generation) | |
359 return_object = self._BotoKeyToObject(key_to_convert, | |
360 fields=get_fields) | |
361 | |
362 yield CloudApi.CsObjectOrPrefix(return_object, | |
363 CloudApi.CsObjectOrPrefixType.OBJECT) | |
364 except TRANSLATABLE_BOTO_EXCEPTIONS, e: | |
365 self._TranslateExceptionAndRaise(e, bucket_name=bucket_name) | |
366 | |
367 def GetObjectMetadata(self, bucket_name, object_name, generation=None, | |
368 provider=None, fields=None): | |
369 """See CloudApi class for function doc strings.""" | |
370 _ = provider | |
371 try: | |
372 return self._BotoKeyToObject(self._GetBotoKey(bucket_name, object_name, | |
373 generation=generation), | |
374 fields=fields) | |
375 except TRANSLATABLE_BOTO_EXCEPTIONS, e: | |
376 self._TranslateExceptionAndRaise(e, bucket_name=bucket_name, | |
377 object_name=object_name, | |
378 generation=generation) | |
379 | |
380 def _CurryDigester(self, digester_object): | |
381 """Curries a digester object into a form consumable by boto. | |
382 | |
383 Key instantiates its own digesters by calling hash_algs[alg]() [note there | |
384 are no arguments to this function]. So in order to pass in our caught-up | |
385 digesters during a resumable download, we need to pass the digester | |
386 object but don't get to look it up based on the algorithm name. Here we | |
387 use a lambda to make lookup implicit. | |
388 | |
389 Args: | |
390 digester_object: Input object to be returned by the created function. | |
391 | |
392 Returns: | |
393 A function which when called will return the input object. | |
394 """ | |
395 return lambda: digester_object | |
396 | |
397 def GetObjectMedia( | |
398 self, bucket_name, object_name, download_stream, provider=None, | |
399 generation=None, object_size=None, | |
400 download_strategy=CloudApi.DownloadStrategy.ONE_SHOT, | |
401 start_byte=0, end_byte=None, progress_callback=None, | |
402 serialization_data=None, digesters=None): | |
403 """See CloudApi class for function doc strings.""" | |
404 # This implementation will get the object metadata first if we don't pass it | |
405 # in via serialization_data. | |
406 headers = {} | |
407 self._AddApiVersionToHeaders(headers) | |
408 if 'accept-encoding' not in headers: | |
409 headers['accept-encoding'] = 'gzip' | |
410 if end_byte: | |
411 headers['range'] = 'bytes=%s-%s' % (start_byte, end_byte) | |
412 elif start_byte > 0: | |
413 headers['range'] = 'bytes=%s-' % (start_byte) | |
414 else: | |
415 headers['range'] = 'bytes=%s' % (start_byte) | |
416 | |
417 # Since in most cases we already made a call to get the object metadata, | |
418 # here we avoid an extra HTTP call by unpickling the key. This is coupled | |
419 # with the implementation in _BotoKeyToObject. | |
420 if serialization_data: | |
421 serialization_dict = json.loads(serialization_data) | |
422 key = pickle.loads(binascii.a2b_base64(serialization_dict['url'])) | |
423 else: | |
424 key = self._GetBotoKey(bucket_name, object_name, generation=generation) | |
425 | |
426 if digesters and self.provider == 'gs': | |
427 hash_algs = {} | |
428 for alg in digesters: | |
429 hash_algs[alg] = self._CurryDigester(digesters[alg]) | |
430 else: | |
431 hash_algs = {} | |
432 | |
433 total_size = object_size or 0 | |
434 if serialization_data: | |
435 total_size = json.loads(serialization_data)['total_size'] | |
436 | |
437 if total_size: | |
438 num_progress_callbacks = max(int(total_size) / TWO_MIB, | |
439 XML_PROGRESS_CALLBACKS) | |
440 else: | |
441 num_progress_callbacks = XML_PROGRESS_CALLBACKS | |
442 | |
443 try: | |
444 if download_strategy is CloudApi.DownloadStrategy.RESUMABLE: | |
445 self._PerformResumableDownload( | |
446 download_stream, key, headers=headers, callback=progress_callback, | |
447 num_callbacks=num_progress_callbacks, hash_algs=hash_algs) | |
448 elif download_strategy is CloudApi.DownloadStrategy.ONE_SHOT: | |
449 self._PerformSimpleDownload( | |
450 download_stream, key, progress_callback=progress_callback, | |
451 num_progress_callbacks=num_progress_callbacks, headers=headers, | |
452 hash_algs=hash_algs) | |
453 else: | |
454 raise ArgumentException('Unsupported DownloadStrategy: %s' % | |
455 download_strategy) | |
456 except TRANSLATABLE_BOTO_EXCEPTIONS, e: | |
457 self._TranslateExceptionAndRaise(e, bucket_name=bucket_name, | |
458 object_name=object_name, | |
459 generation=generation) | |
460 | |
461 if self.provider == 's3': | |
462 if digesters: | |
463 | |
464 class HashToDigester(object): | |
465 """Wrapper class to expose hash digests. | |
466 | |
467 boto creates its own digesters in s3's get_file, returning on-the-fly | |
468 hashes only by way of key.local_hashes. To propagate the digest back | |
469 to the caller, this stub class implements the digest() function. | |
470 """ | |
471 | |
472 def __init__(self, hash_val): | |
473 self.hash_val = hash_val | |
474 | |
475 def digest(self): # pylint: disable=invalid-name | |
476 return self.hash_val | |
477 | |
478 for alg_name in digesters: | |
479 if ((download_strategy == CloudApi.DownloadStrategy.RESUMABLE and | |
480 start_byte != 0) or | |
481 not ((getattr(key, 'local_hashes', None) and | |
482 alg_name in key.local_hashes))): | |
483 # For resumable downloads, boto does not provide a mechanism to | |
484 # catch up the hash in the case of a partially complete download. | |
485 # In this case or in the case where no digest was successfully | |
486 # calculated, set the digester to None, which indicates that we'll | |
487 # need to manually calculate the hash from the local file once it | |
488 # is complete. | |
489 digesters[alg_name] = None | |
490 else: | |
491 # Use the on-the-fly hash. | |
492 digesters[alg_name] = HashToDigester(key.local_hashes[alg_name]) | |
493 | |
494 def _PerformSimpleDownload(self, download_stream, key, progress_callback=None, | |
495 num_progress_callbacks=XML_PROGRESS_CALLBACKS, | |
496 headers=None, hash_algs=None): | |
497 if not headers: | |
498 headers = {} | |
499 self._AddApiVersionToHeaders(headers) | |
500 try: | |
501 key.get_contents_to_file(download_stream, cb=progress_callback, | |
502 num_cb=num_progress_callbacks, headers=headers, | |
503 hash_algs=hash_algs) | |
504 except TypeError: # s3 and mocks do not support hash_algs | |
505 key.get_contents_to_file(download_stream, cb=progress_callback, | |
506 num_cb=num_progress_callbacks, headers=headers) | |
507 | |
508 def _PerformResumableDownload(self, fp, key, headers=None, callback=None, | |
509 num_callbacks=XML_PROGRESS_CALLBACKS, | |
510 hash_algs=None): | |
511 """Downloads bytes from key to fp, resuming as needed. | |
512 | |
513 Args: | |
514 fp: File pointer into which data should be downloaded | |
515 key: Key object from which data is to be downloaded | |
516 headers: Headers to send when retrieving the file | |
517 callback: (optional) a callback function that will be called to report | |
518 progress on the download. The callback should accept two integer | |
519 parameters. The first integer represents the number of | |
520 bytes that have been successfully transmitted from the service. The | |
521 second represents the total number of bytes that need to be | |
522 transmitted. | |
523 num_callbacks: (optional) If a callback is specified with the callback | |
524 parameter, this determines the granularity of the callback | |
525 by defining the maximum number of times the callback will be | |
526 called during the file transfer. | |
527 hash_algs: Dict of hash algorithms to apply to downloaded bytes. | |
528 | |
529 Raises: | |
530 ResumableDownloadException on error. | |
531 """ | |
532 if not headers: | |
533 headers = {} | |
534 self._AddApiVersionToHeaders(headers) | |
535 | |
536 retryable_exceptions = (httplib.HTTPException, IOError, socket.error, | |
537 socket.gaierror) | |
538 | |
539 debug = key.bucket.connection.debug | |
540 | |
541 num_retries = GetNumRetries() | |
542 progress_less_iterations = 0 | |
543 | |
544 while True: # Retry as long as we're making progress. | |
545 had_file_bytes_before_attempt = GetFileSize(fp) | |
546 try: | |
547 cur_file_size = GetFileSize(fp, position_to_eof=True) | |
548 | |
549 def DownloadProxyCallback(total_bytes_downloaded, total_size): | |
550 """Translates a boto callback into a gsutil Cloud API callback. | |
551 | |
552 Callbacks are originally made by boto.s3.Key.get_file(); here we take | |
553 into account that we're resuming a download. | |
554 | |
555 Args: | |
556 total_bytes_downloaded: Actual bytes downloaded so far, not | |
557 including the point we resumed from. | |
558 total_size: Total size of the download. | |
559 """ | |
560 if callback: | |
561 callback(cur_file_size + total_bytes_downloaded, total_size) | |
562 | |
563 headers = headers.copy() | |
564 headers['Range'] = 'bytes=%d-%d' % (cur_file_size, key.size - 1) | |
565 cb = DownloadProxyCallback | |
566 | |
567 # Disable AWSAuthConnection-level retry behavior, since that would | |
568 # cause downloads to restart from scratch. | |
569 try: | |
570 key.get_file(fp, headers, cb, num_callbacks, override_num_retries=0, | |
571 hash_algs=hash_algs) | |
572 except TypeError: | |
573 key.get_file(fp, headers, cb, num_callbacks, override_num_retries=0) | |
574 fp.flush() | |
575 # Download succeeded. | |
576 return | |
577 except retryable_exceptions, e: | |
578 if debug >= 1: | |
579 self.logger.info('Caught exception (%s)', repr(e)) | |
580 if isinstance(e, IOError) and e.errno == errno.EPIPE: | |
581 # Broken pipe error causes httplib to immediately | |
582 # close the socket (http://bugs.python.org/issue5542), | |
583 # so we need to close and reopen the key before resuming | |
584 # the download. | |
585 if self.provider == 's3': | |
586 key.get_file(fp, headers, cb, num_callbacks, override_num_retries=0) | |
587 else: # self.provider == 'gs' | |
588 key.get_file(fp, headers, cb, num_callbacks, | |
589 override_num_retries=0, hash_algs=hash_algs) | |
590 except BotoResumableDownloadException, e: | |
591 if (e.disposition == | |
592 ResumableTransferDisposition.ABORT_CUR_PROCESS): | |
593 raise ResumableDownloadException(e.message) | |
594 else: | |
595 if debug >= 1: | |
596 self.logger.info('Caught ResumableDownloadException (%s) - will ' | |
597 'retry', e.message) | |
598 | |
599 # At this point we had a re-tryable failure; see if made progress. | |
600 if GetFileSize(fp) > had_file_bytes_before_attempt: | |
601 progress_less_iterations = 0 | |
602 else: | |
603 progress_less_iterations += 1 | |
604 | |
605 if progress_less_iterations > num_retries: | |
606 # Don't retry any longer in the current process. | |
607 raise ResumableDownloadException( | |
608 'Too many resumable download attempts failed without ' | |
609 'progress. You might try this download again later') | |
610 | |
611 # Close the key, in case a previous download died partway | |
612 # through and left data in the underlying key HTTP buffer. | |
613 # Do this within a try/except block in case the connection is | |
614 # closed (since key.close() attempts to do a final read, in which | |
615 # case this read attempt would get an IncompleteRead exception, | |
616 # which we can safely ignore). | |
617 try: | |
618 key.close() | |
619 except httplib.IncompleteRead: | |
620 pass | |
621 | |
622 sleep_time_secs = min(random.random() * (2 ** progress_less_iterations), | |
623 GetMaxRetryDelay()) | |
624 if debug >= 1: | |
625 self.logger.info( | |
626 'Got retryable failure (%d progress-less in a row).\nSleeping %d ' | |
627 'seconds before re-trying', progress_less_iterations, | |
628 sleep_time_secs) | |
629 time.sleep(sleep_time_secs) | |
630 | |
631 def PatchObjectMetadata(self, bucket_name, object_name, metadata, | |
632 canned_acl=None, generation=None, preconditions=None, | |
633 provider=None, fields=None): | |
634 """See CloudApi class for function doc strings.""" | |
635 _ = provider | |
636 object_uri = self._StorageUriForObject(bucket_name, object_name, | |
637 generation=generation) | |
638 | |
639 headers = {} | |
640 self._AddApiVersionToHeaders(headers) | |
641 meta_headers = HeadersFromObjectMetadata(metadata, self.provider) | |
642 | |
643 metadata_plus = {} | |
644 metadata_minus = set() | |
645 metadata_changed = False | |
646 for k, v in meta_headers.iteritems(): | |
647 metadata_changed = True | |
648 if v is None: | |
649 metadata_minus.add(k) | |
650 else: | |
651 metadata_plus[k] = v | |
652 | |
653 self._AddPreconditionsToHeaders(preconditions, headers) | |
654 | |
655 if metadata_changed: | |
656 try: | |
657 object_uri.set_metadata(metadata_plus, metadata_minus, False, | |
658 headers=headers) | |
659 except TRANSLATABLE_BOTO_EXCEPTIONS, e: | |
660 self._TranslateExceptionAndRaise(e, bucket_name=bucket_name, | |
661 object_name=object_name, | |
662 generation=generation) | |
663 | |
664 if metadata.acl: | |
665 boto_acl = AclTranslation.BotoAclFromMessage(metadata.acl) | |
666 try: | |
667 object_uri.set_xml_acl(boto_acl.to_xml(), key_name=object_name) | |
668 except TRANSLATABLE_BOTO_EXCEPTIONS, e: | |
669 self._TranslateExceptionAndRaise(e, bucket_name=bucket_name, | |
670 object_name=object_name, | |
671 generation=generation) | |
672 if canned_acl: | |
673 canned_acls = object_uri.canned_acls() | |
674 if canned_acl not in canned_acls: | |
675 raise CommandException('Invalid canned ACL "%s".' % canned_acl) | |
676 object_uri.set_acl(canned_acl, object_uri.object_name) | |
677 | |
678 return self.GetObjectMetadata(bucket_name, object_name, | |
679 generation=generation, fields=fields) | |
680 | |
681 def _PerformSimpleUpload(self, dst_uri, upload_stream, md5=None, | |
682 canned_acl=None, progress_callback=None, | |
683 headers=None): | |
684 dst_uri.set_contents_from_file(upload_stream, md5=md5, policy=canned_acl, | |
685 cb=progress_callback, headers=headers) | |
686 | |
687 def _PerformStreamingUpload(self, dst_uri, upload_stream, canned_acl=None, | |
688 progress_callback=None, headers=None): | |
689 if dst_uri.get_provider().supports_chunked_transfer(): | |
690 dst_uri.set_contents_from_stream(upload_stream, policy=canned_acl, | |
691 cb=progress_callback, headers=headers) | |
692 else: | |
693 # Provider doesn't support chunked transfer, so copy to a temporary | |
694 # file. | |
695 (temp_fh, temp_path) = tempfile.mkstemp() | |
696 try: | |
697 with open(temp_path, 'wb') as out_fp: | |
698 stream_bytes = upload_stream.read(DEFAULT_FILE_BUFFER_SIZE) | |
699 while stream_bytes: | |
700 out_fp.write(stream_bytes) | |
701 stream_bytes = upload_stream.read(DEFAULT_FILE_BUFFER_SIZE) | |
702 with open(temp_path, 'rb') as in_fp: | |
703 dst_uri.set_contents_from_file(in_fp, policy=canned_acl, | |
704 headers=headers) | |
705 finally: | |
706 os.close(temp_fh) | |
707 os.unlink(temp_path) | |
708 | |
709 def _PerformResumableUpload(self, key, upload_stream, upload_size, | |
710 tracker_callback, canned_acl=None, | |
711 serialization_data=None, progress_callback=None, | |
712 headers=None): | |
713 resumable_upload = BotoResumableUpload( | |
714 tracker_callback, self.logger, resume_url=serialization_data) | |
715 resumable_upload.SendFile(key, upload_stream, upload_size, | |
716 canned_acl=canned_acl, cb=progress_callback, | |
717 headers=headers) | |
718 | |
719 def _UploadSetup(self, object_metadata, preconditions=None): | |
720 """Shared upload implementation. | |
721 | |
722 Args: | |
723 object_metadata: Object metadata describing destination object. | |
724 preconditions: Optional gsutil Cloud API preconditions. | |
725 | |
726 Returns: | |
727 Headers dictionary, StorageUri for upload (based on inputs) | |
728 """ | |
729 ValidateDstObjectMetadata(object_metadata) | |
730 | |
731 headers = HeadersFromObjectMetadata(object_metadata, self.provider) | |
732 self._AddApiVersionToHeaders(headers) | |
733 | |
734 if object_metadata.crc32c: | |
735 if 'x-goog-hash' in headers: | |
736 headers['x-goog-hash'] += ( | |
737 ',crc32c=%s' % object_metadata.crc32c.rstrip('\n')) | |
738 else: | |
739 headers['x-goog-hash'] = ( | |
740 'crc32c=%s' % object_metadata.crc32c.rstrip('\n')) | |
741 if object_metadata.md5Hash: | |
742 if 'x-goog-hash' in headers: | |
743 headers['x-goog-hash'] += ( | |
744 ',md5=%s' % object_metadata.md5Hash.rstrip('\n')) | |
745 else: | |
746 headers['x-goog-hash'] = ( | |
747 'md5=%s' % object_metadata.md5Hash.rstrip('\n')) | |
748 | |
749 if 'content-type' in headers and not headers['content-type']: | |
750 headers['content-type'] = 'application/octet-stream' | |
751 | |
752 self._AddPreconditionsToHeaders(preconditions, headers) | |
753 | |
754 dst_uri = self._StorageUriForObject(object_metadata.bucket, | |
755 object_metadata.name) | |
756 return headers, dst_uri | |
757 | |
758 def _HandleSuccessfulUpload(self, dst_uri, object_metadata, fields=None): | |
759 """Set ACLs on an uploaded object and return its metadata. | |
760 | |
761 Args: | |
762 dst_uri: Generation-specific StorageUri describing the object. | |
763 object_metadata: Metadata for the object, including an ACL if applicable. | |
764 fields: If present, return only these Object metadata fields. | |
765 | |
766 Returns: | |
767 gsutil Cloud API Object metadata. | |
768 | |
769 Raises: | |
770 CommandException if the object was overwritten / deleted concurrently. | |
771 """ | |
772 try: | |
773 # The XML API does not support if-generation-match for GET requests. | |
774 # Therefore, if the object gets overwritten before the ACL and get_key | |
775 # operations, the best we can do is warn that it happened. | |
776 self._SetObjectAcl(object_metadata, dst_uri) | |
777 return self._BotoKeyToObject(dst_uri.get_key(), fields=fields) | |
778 except boto.exception.InvalidUriError as e: | |
779 if e.message and NON_EXISTENT_OBJECT_REGEX.match(e.message.encode(UTF8)): | |
780 raise CommandException('\n'.join(textwrap.wrap( | |
781 'Uploaded object (%s) was deleted or overwritten immediately ' | |
782 'after it was uploaded. This can happen if you attempt to upload ' | |
783 'to the same object multiple times concurrently.' % dst_uri.uri))) | |
784 else: | |
785 raise | |
786 | |
787 def _SetObjectAcl(self, object_metadata, dst_uri): | |
788 """Sets the ACL (if present in object_metadata) on an uploaded object.""" | |
789 if object_metadata.acl: | |
790 boto_acl = AclTranslation.BotoAclFromMessage(object_metadata.acl) | |
791 dst_uri.set_xml_acl(boto_acl.to_xml()) | |
792 elif self.provider == 's3': | |
793 s3_acl = S3MarkerAclFromObjectMetadata(object_metadata) | |
794 if s3_acl: | |
795 dst_uri.set_xml_acl(s3_acl) | |
796 | |
797 def UploadObjectResumable( | |
798 self, upload_stream, object_metadata, canned_acl=None, preconditions=None, | |
799 provider=None, fields=None, size=None, serialization_data=None, | |
800 tracker_callback=None, progress_callback=None): | |
801 """See CloudApi class for function doc strings.""" | |
802 if self.provider == 's3': | |
803 # Resumable uploads are not supported for s3. | |
804 return self.UploadObject( | |
805 upload_stream, object_metadata, canned_acl=canned_acl, | |
806 preconditions=preconditions, fields=fields, size=size) | |
807 headers, dst_uri = self._UploadSetup(object_metadata, | |
808 preconditions=preconditions) | |
809 if not tracker_callback: | |
810 raise ArgumentException('No tracker callback function set for ' | |
811 'resumable upload of %s' % dst_uri) | |
812 try: | |
813 self._PerformResumableUpload(dst_uri.new_key(headers=headers), | |
814 upload_stream, size, tracker_callback, | |
815 canned_acl=canned_acl, | |
816 serialization_data=serialization_data, | |
817 progress_callback=progress_callback, | |
818 headers=headers) | |
819 return self._HandleSuccessfulUpload(dst_uri, object_metadata, | |
820 fields=fields) | |
821 except TRANSLATABLE_BOTO_EXCEPTIONS, e: | |
822 self._TranslateExceptionAndRaise(e, bucket_name=object_metadata.bucket, | |
823 object_name=object_metadata.name) | |
824 | |
825 def UploadObjectStreaming(self, upload_stream, object_metadata, | |
826 canned_acl=None, progress_callback=None, | |
827 preconditions=None, provider=None, fields=None): | |
828 """See CloudApi class for function doc strings.""" | |
829 headers, dst_uri = self._UploadSetup(object_metadata, | |
830 preconditions=preconditions) | |
831 | |
832 try: | |
833 self._PerformStreamingUpload( | |
834 dst_uri, upload_stream, canned_acl=canned_acl, | |
835 progress_callback=progress_callback, headers=headers) | |
836 return self._HandleSuccessfulUpload(dst_uri, object_metadata, | |
837 fields=fields) | |
838 except TRANSLATABLE_BOTO_EXCEPTIONS, e: | |
839 self._TranslateExceptionAndRaise(e, bucket_name=object_metadata.bucket, | |
840 object_name=object_metadata.name) | |
841 | |
842 def UploadObject(self, upload_stream, object_metadata, canned_acl=None, | |
843 preconditions=None, size=None, progress_callback=None, | |
844 provider=None, fields=None): | |
845 """See CloudApi class for function doc strings.""" | |
846 headers, dst_uri = self._UploadSetup(object_metadata, | |
847 preconditions=preconditions) | |
848 | |
849 try: | |
850 md5 = None | |
851 if object_metadata.md5Hash: | |
852 md5 = [] | |
853 # boto expects hex at index 0, base64 at index 1 | |
854 md5.append(Base64ToHexHash(object_metadata.md5Hash)) | |
855 md5.append(object_metadata.md5Hash.strip('\n"\'')) | |
856 self._PerformSimpleUpload(dst_uri, upload_stream, md5=md5, | |
857 canned_acl=canned_acl, | |
858 progress_callback=progress_callback, | |
859 headers=headers) | |
860 return self._HandleSuccessfulUpload(dst_uri, object_metadata, | |
861 fields=fields) | |
862 except TRANSLATABLE_BOTO_EXCEPTIONS, e: | |
863 self._TranslateExceptionAndRaise(e, bucket_name=object_metadata.bucket, | |
864 object_name=object_metadata.name) | |
865 | |
866 def DeleteObject(self, bucket_name, object_name, preconditions=None, | |
867 generation=None, provider=None): | |
868 """See CloudApi class for function doc strings.""" | |
869 _ = provider | |
870 headers = {} | |
871 self._AddApiVersionToHeaders(headers) | |
872 self._AddPreconditionsToHeaders(preconditions, headers) | |
873 | |
874 uri = self._StorageUriForObject(bucket_name, object_name, | |
875 generation=generation) | |
876 try: | |
877 uri.delete_key(validate=False, headers=headers) | |
878 except TRANSLATABLE_BOTO_EXCEPTIONS, e: | |
879 self._TranslateExceptionAndRaise(e, bucket_name=bucket_name, | |
880 object_name=object_name, | |
881 generation=generation) | |
882 | |
883 def CopyObject(self, src_obj_metadata, dst_obj_metadata, src_generation=None, | |
884 canned_acl=None, preconditions=None, progress_callback=None, | |
885 max_bytes_per_call=None, provider=None, fields=None): | |
886 """See CloudApi class for function doc strings.""" | |
887 _ = provider | |
888 | |
889 if max_bytes_per_call is not None: | |
890 raise NotImplementedError('XML API does not suport max_bytes_per_call') | |
891 dst_uri = self._StorageUriForObject(dst_obj_metadata.bucket, | |
892 dst_obj_metadata.name) | |
893 | |
894 # Usually it's okay to treat version_id and generation as | |
895 # the same, but in this case the underlying boto call determines the | |
896 # provider based on the presence of one or the other. | |
897 src_version_id = None | |
898 if self.provider == 's3': | |
899 src_version_id = src_generation | |
900 src_generation = None | |
901 | |
902 headers = HeadersFromObjectMetadata(dst_obj_metadata, self.provider) | |
903 self._AddApiVersionToHeaders(headers) | |
904 self._AddPreconditionsToHeaders(preconditions, headers) | |
905 | |
906 if canned_acl: | |
907 headers[dst_uri.get_provider().acl_header] = canned_acl | |
908 | |
909 preserve_acl = True if dst_obj_metadata.acl else False | |
910 if self.provider == 's3': | |
911 s3_acl = S3MarkerAclFromObjectMetadata(dst_obj_metadata) | |
912 if s3_acl: | |
913 preserve_acl = True | |
914 | |
915 try: | |
916 new_key = dst_uri.copy_key( | |
917 src_obj_metadata.bucket, src_obj_metadata.name, | |
918 preserve_acl=preserve_acl, headers=headers, | |
919 src_version_id=src_version_id, src_generation=src_generation) | |
920 | |
921 return self._BotoKeyToObject(new_key, fields=fields) | |
922 except TRANSLATABLE_BOTO_EXCEPTIONS, e: | |
923 self._TranslateExceptionAndRaise(e, dst_obj_metadata.bucket, | |
924 dst_obj_metadata.name) | |
925 | |
926 def ComposeObject(self, src_objs_metadata, dst_obj_metadata, | |
927 preconditions=None, provider=None, fields=None): | |
928 """See CloudApi class for function doc strings.""" | |
929 _ = provider | |
930 ValidateDstObjectMetadata(dst_obj_metadata) | |
931 | |
932 dst_obj_name = dst_obj_metadata.name | |
933 dst_obj_metadata.name = None | |
934 dst_bucket_name = dst_obj_metadata.bucket | |
935 dst_obj_metadata.bucket = None | |
936 headers = HeadersFromObjectMetadata(dst_obj_metadata, self.provider) | |
937 if not dst_obj_metadata.contentType: | |
938 dst_obj_metadata.contentType = DEFAULT_CONTENT_TYPE | |
939 headers['content-type'] = dst_obj_metadata.contentType | |
940 self._AddApiVersionToHeaders(headers) | |
941 self._AddPreconditionsToHeaders(preconditions, headers) | |
942 | |
943 dst_uri = self._StorageUriForObject(dst_bucket_name, dst_obj_name) | |
944 | |
945 src_components = [] | |
946 for src_obj in src_objs_metadata: | |
947 src_uri = self._StorageUriForObject(dst_bucket_name, src_obj.name, | |
948 generation=src_obj.generation) | |
949 src_components.append(src_uri) | |
950 | |
951 try: | |
952 dst_uri.compose(src_components, headers=headers) | |
953 | |
954 return self.GetObjectMetadata(dst_bucket_name, dst_obj_name, | |
955 fields=fields) | |
956 except TRANSLATABLE_BOTO_EXCEPTIONS, e: | |
957 self._TranslateExceptionAndRaise(e, dst_obj_metadata.bucket, | |
958 dst_obj_metadata.name) | |
959 | |
960 def _AddPreconditionsToHeaders(self, preconditions, headers): | |
961 """Adds preconditions (if any) to headers.""" | |
962 if preconditions and self.provider == 'gs': | |
963 if preconditions.gen_match is not None: | |
964 headers['x-goog-if-generation-match'] = str(preconditions.gen_match) | |
965 if preconditions.meta_gen_match is not None: | |
966 headers['x-goog-if-metageneration-match'] = str( | |
967 preconditions.meta_gen_match) | |
968 | |
969 def _AddApiVersionToHeaders(self, headers): | |
970 if self.provider == 'gs': | |
971 headers['x-goog-api-version'] = self.api_version | |
972 | |
973 def _GetMD5FromETag(self, src_etag): | |
974 """Returns an MD5 from the etag iff the etag is a valid MD5 hash. | |
975 | |
976 Args: | |
977 src_etag: Object etag for which to return the MD5. | |
978 | |
979 Returns: | |
980 MD5 in hex string format, or None. | |
981 """ | |
982 if src_etag and MD5_REGEX.search(src_etag): | |
983 return src_etag.strip('"\'').lower() | |
984 | |
985 def _StorageUriForBucket(self, bucket): | |
986 """Returns a boto storage_uri for the given bucket name. | |
987 | |
988 Args: | |
989 bucket: Bucket name (string). | |
990 | |
991 Returns: | |
992 Boto storage_uri for the bucket. | |
993 """ | |
994 return boto.storage_uri( | |
995 '%s://%s' % (self.provider, bucket), | |
996 suppress_consec_slashes=False, | |
997 bucket_storage_uri_class=self.bucket_storage_uri_class, | |
998 debug=self.debug) | |
999 | |
1000 def _StorageUriForObject(self, bucket, object_name, generation=None): | |
1001 """Returns a boto storage_uri for the given object. | |
1002 | |
1003 Args: | |
1004 bucket: Bucket name (string). | |
1005 object_name: Object name (string). | |
1006 generation: Generation or version_id of object. If None, live version | |
1007 of the object is used. | |
1008 | |
1009 Returns: | |
1010 Boto storage_uri for the object. | |
1011 """ | |
1012 uri_string = '%s://%s/%s' % (self.provider, bucket, object_name) | |
1013 if generation: | |
1014 uri_string += '#%s' % generation | |
1015 return boto.storage_uri( | |
1016 uri_string, suppress_consec_slashes=False, | |
1017 bucket_storage_uri_class=self.bucket_storage_uri_class, | |
1018 debug=self.debug) | |
1019 | |
1020 def _GetBotoKey(self, bucket_name, object_name, generation=None): | |
1021 """Gets the boto key for an object. | |
1022 | |
1023 Args: | |
1024 bucket_name: Bucket containing the object. | |
1025 object_name: Object name. | |
1026 generation: Generation or version of the object to retrieve. | |
1027 | |
1028 Returns: | |
1029 Boto key for the object. | |
1030 """ | |
1031 object_uri = self._StorageUriForObject(bucket_name, object_name, | |
1032 generation=generation) | |
1033 try: | |
1034 key = object_uri.get_key() | |
1035 if not key: | |
1036 raise CreateObjectNotFoundException('404', self.provider, | |
1037 bucket_name, object_name, | |
1038 generation=generation) | |
1039 return key | |
1040 except TRANSLATABLE_BOTO_EXCEPTIONS, e: | |
1041 self._TranslateExceptionAndRaise(e, bucket_name=bucket_name, | |
1042 object_name=object_name, | |
1043 generation=generation) | |
1044 | |
1045 def _ListToGetFields(self, list_fields=None): | |
1046 """Removes 'items/' from the input fields and converts it to a set. | |
1047 | |
1048 This way field sets requested for ListBucket/ListObject can be used in | |
1049 _BotoBucketToBucket and _BotoKeyToObject calls. | |
1050 | |
1051 Args: | |
1052 list_fields: Iterable fields usable in ListBucket/ListObject calls. | |
1053 | |
1054 Returns: | |
1055 Set of fields usable in GetBucket/GetObject or | |
1056 _BotoBucketToBucket/_BotoKeyToObject calls. | |
1057 """ | |
1058 if list_fields: | |
1059 get_fields = set() | |
1060 for field in list_fields: | |
1061 if field in ['kind', 'nextPageToken', 'prefixes']: | |
1062 # These are not actually object / bucket metadata fields. | |
1063 # They are fields specific to listing, so we don't consider them. | |
1064 continue | |
1065 get_fields.add(re.sub(r'items/', '', field)) | |
1066 return get_fields | |
1067 | |
1068 # pylint: disable=too-many-statements | |
1069 def _BotoBucketToBucket(self, bucket, fields=None): | |
1070 """Constructs an apitools Bucket from a boto bucket. | |
1071 | |
1072 Args: | |
1073 bucket: Boto bucket. | |
1074 fields: If present, construct the apitools Bucket with only this set of | |
1075 metadata fields. | |
1076 | |
1077 Returns: | |
1078 apitools Bucket. | |
1079 """ | |
1080 bucket_uri = self._StorageUriForBucket(bucket.name) | |
1081 | |
1082 cloud_api_bucket = apitools_messages.Bucket(name=bucket.name, | |
1083 id=bucket.name) | |
1084 headers = {} | |
1085 self._AddApiVersionToHeaders(headers) | |
1086 if self.provider == 'gs': | |
1087 if not fields or 'storageClass' in fields: | |
1088 if hasattr(bucket, 'get_storage_class'): | |
1089 cloud_api_bucket.storageClass = bucket.get_storage_class() | |
1090 if not fields or 'acl' in fields: | |
1091 for acl in AclTranslation.BotoBucketAclToMessage( | |
1092 bucket.get_acl(headers=headers)): | |
1093 try: | |
1094 cloud_api_bucket.acl.append(acl) | |
1095 except TRANSLATABLE_BOTO_EXCEPTIONS, e: | |
1096 translated_exception = self._TranslateBotoException( | |
1097 e, bucket_name=bucket.name) | |
1098 if (translated_exception and | |
1099 isinstance(translated_exception, | |
1100 AccessDeniedException)): | |
1101 # JSON API doesn't differentiate between a blank ACL list | |
1102 # and an access denied, so this is intentionally left blank. | |
1103 pass | |
1104 else: | |
1105 self._TranslateExceptionAndRaise(e, bucket_name=bucket.name) | |
1106 if not fields or 'cors' in fields: | |
1107 try: | |
1108 boto_cors = bucket_uri.get_cors() | |
1109 cloud_api_bucket.cors = CorsTranslation.BotoCorsToMessage(boto_cors) | |
1110 except TRANSLATABLE_BOTO_EXCEPTIONS, e: | |
1111 self._TranslateExceptionAndRaise(e, bucket_name=bucket.name) | |
1112 if not fields or 'defaultObjectAcl' in fields: | |
1113 for acl in AclTranslation.BotoObjectAclToMessage( | |
1114 bucket.get_def_acl(headers=headers)): | |
1115 try: | |
1116 cloud_api_bucket.defaultObjectAcl.append(acl) | |
1117 except TRANSLATABLE_BOTO_EXCEPTIONS, e: | |
1118 translated_exception = self._TranslateBotoException( | |
1119 e, bucket_name=bucket.name) | |
1120 if (translated_exception and | |
1121 isinstance(translated_exception, | |
1122 AccessDeniedException)): | |
1123 # JSON API doesn't differentiate between a blank ACL list | |
1124 # and an access denied, so this is intentionally left blank. | |
1125 pass | |
1126 else: | |
1127 self._TranslateExceptionAndRaise(e, bucket_name=bucket.name) | |
1128 if not fields or 'lifecycle' in fields: | |
1129 try: | |
1130 boto_lifecycle = bucket_uri.get_lifecycle_config() | |
1131 cloud_api_bucket.lifecycle = ( | |
1132 LifecycleTranslation.BotoLifecycleToMessage(boto_lifecycle)) | |
1133 except TRANSLATABLE_BOTO_EXCEPTIONS, e: | |
1134 self._TranslateExceptionAndRaise(e, bucket_name=bucket.name) | |
1135 if not fields or 'logging' in fields: | |
1136 try: | |
1137 boto_logging = bucket_uri.get_logging_config() | |
1138 if boto_logging and 'Logging' in boto_logging: | |
1139 logging_config = boto_logging['Logging'] | |
1140 log_object_prefix_present = 'LogObjectPrefix' in logging_config | |
1141 log_bucket_present = 'LogBucket' in logging_config | |
1142 if log_object_prefix_present or log_bucket_present: | |
1143 cloud_api_bucket.logging = apitools_messages.Bucket.LoggingValue() | |
1144 if log_object_prefix_present: | |
1145 cloud_api_bucket.logging.logObjectPrefix = ( | |
1146 logging_config['LogObjectPrefix']) | |
1147 if log_bucket_present: | |
1148 cloud_api_bucket.logging.logBucket = logging_config['LogBucket'] | |
1149 except TRANSLATABLE_BOTO_EXCEPTIONS, e: | |
1150 self._TranslateExceptionAndRaise(e, bucket_name=bucket.name) | |
1151 if not fields or 'website' in fields: | |
1152 try: | |
1153 boto_website = bucket_uri.get_website_config() | |
1154 if boto_website and 'WebsiteConfiguration' in boto_website: | |
1155 website_config = boto_website['WebsiteConfiguration'] | |
1156 main_page_suffix_present = 'MainPageSuffix' in website_config | |
1157 not_found_page_present = 'NotFoundPage' in website_config | |
1158 if main_page_suffix_present or not_found_page_present: | |
1159 cloud_api_bucket.website = apitools_messages.Bucket.WebsiteValue() | |
1160 if main_page_suffix_present: | |
1161 cloud_api_bucket.website.mainPageSuffix = ( | |
1162 website_config['MainPageSuffix']) | |
1163 if not_found_page_present: | |
1164 cloud_api_bucket.website.notFoundPage = ( | |
1165 website_config['NotFoundPage']) | |
1166 except TRANSLATABLE_BOTO_EXCEPTIONS, e: | |
1167 self._TranslateExceptionAndRaise(e, bucket_name=bucket.name) | |
1168 if not fields or 'location' in fields: | |
1169 cloud_api_bucket.location = bucket_uri.get_location() | |
1170 if not fields or 'versioning' in fields: | |
1171 versioning = bucket_uri.get_versioning_config(headers=headers) | |
1172 if versioning: | |
1173 if (self.provider == 's3' and 'Versioning' in versioning and | |
1174 versioning['Versioning'] == 'Enabled'): | |
1175 cloud_api_bucket.versioning = ( | |
1176 apitools_messages.Bucket.VersioningValue(enabled=True)) | |
1177 elif self.provider == 'gs': | |
1178 cloud_api_bucket.versioning = ( | |
1179 apitools_messages.Bucket.VersioningValue(enabled=True)) | |
1180 | |
1181 # For S3 long bucket listing we do not support CORS, lifecycle, website, and | |
1182 # logging translation. The individual commands can be used to get | |
1183 # the XML equivalents for S3. | |
1184 return cloud_api_bucket | |
1185 | |
1186 def _BotoKeyToObject(self, key, fields=None): | |
1187 """Constructs an apitools Object from a boto key. | |
1188 | |
1189 Args: | |
1190 key: Boto key to construct Object from. | |
1191 fields: If present, construct the apitools Object with only this set of | |
1192 metadata fields. | |
1193 | |
1194 Returns: | |
1195 apitools Object corresponding to key. | |
1196 """ | |
1197 custom_metadata = None | |
1198 if not fields or 'metadata' in fields: | |
1199 custom_metadata = self._TranslateBotoKeyCustomMetadata(key) | |
1200 cache_control = None | |
1201 if not fields or 'cacheControl' in fields: | |
1202 cache_control = getattr(key, 'cache_control', None) | |
1203 component_count = None | |
1204 if not fields or 'componentCount' in fields: | |
1205 component_count = getattr(key, 'component_count', None) | |
1206 content_disposition = None | |
1207 if not fields or 'contentDisposition' in fields: | |
1208 content_disposition = getattr(key, 'content_disposition', None) | |
1209 # Other fields like updated and ACL depend on the generation | |
1210 # of the object, so populate that regardless of whether it was requested. | |
1211 generation = self._TranslateBotoKeyGeneration(key) | |
1212 metageneration = None | |
1213 if not fields or 'metageneration' in fields: | |
1214 metageneration = self._TranslateBotoKeyMetageneration(key) | |
1215 updated = None | |
1216 # Translation code to avoid a dependency on dateutil. | |
1217 if not fields or 'updated' in fields: | |
1218 updated = self._TranslateBotoKeyTimestamp(key) | |
1219 etag = None | |
1220 if not fields or 'etag' in fields: | |
1221 etag = getattr(key, 'etag', None) | |
1222 if etag: | |
1223 etag = etag.strip('"\'') | |
1224 crc32c = None | |
1225 if not fields or 'crc32c' in fields: | |
1226 if hasattr(key, 'cloud_hashes') and 'crc32c' in key.cloud_hashes: | |
1227 crc32c = base64.encodestring(key.cloud_hashes['crc32c']).rstrip('\n') | |
1228 md5_hash = None | |
1229 if not fields or 'md5Hash' in fields: | |
1230 if hasattr(key, 'cloud_hashes') and 'md5' in key.cloud_hashes: | |
1231 md5_hash = base64.encodestring(key.cloud_hashes['md5']).rstrip('\n') | |
1232 elif self._GetMD5FromETag(getattr(key, 'etag', None)): | |
1233 md5_hash = Base64EncodeHash(self._GetMD5FromETag(key.etag)) | |
1234 elif self.provider == 's3': | |
1235 # S3 etags are MD5s for non-multi-part objects, but multi-part objects | |
1236 # (which include all objects >= 5 GB) have a custom checksum | |
1237 # implementation that is not currently supported by gsutil. | |
1238 self.logger.warn( | |
1239 'Non-MD5 etag (%s) present for key %s, data integrity checks are ' | |
1240 'not possible.', key.etag, key) | |
1241 | |
1242 # Serialize the boto key in the media link if it is requested. This | |
1243 # way we can later access the key without adding an HTTP call. | |
1244 media_link = None | |
1245 if not fields or 'mediaLink' in fields: | |
1246 media_link = binascii.b2a_base64( | |
1247 pickle.dumps(key, pickle.HIGHEST_PROTOCOL)) | |
1248 size = None | |
1249 if not fields or 'size' in fields: | |
1250 size = key.size or 0 | |
1251 storage_class = None | |
1252 if not fields or 'storageClass' in fields: | |
1253 storage_class = getattr(key, 'storage_class', None) | |
1254 | |
1255 cloud_api_object = apitools_messages.Object( | |
1256 bucket=key.bucket.name, | |
1257 name=key.name, | |
1258 size=size, | |
1259 contentEncoding=key.content_encoding, | |
1260 contentLanguage=key.content_language, | |
1261 contentType=key.content_type, | |
1262 cacheControl=cache_control, | |
1263 contentDisposition=content_disposition, | |
1264 etag=etag, | |
1265 crc32c=crc32c, | |
1266 md5Hash=md5_hash, | |
1267 generation=generation, | |
1268 metageneration=metageneration, | |
1269 componentCount=component_count, | |
1270 updated=updated, | |
1271 metadata=custom_metadata, | |
1272 mediaLink=media_link, | |
1273 storageClass=storage_class) | |
1274 | |
1275 # Remaining functions amend cloud_api_object. | |
1276 self._TranslateDeleteMarker(key, cloud_api_object) | |
1277 if not fields or 'acl' in fields: | |
1278 generation_str = GenerationFromUrlAndString( | |
1279 StorageUrlFromString(self.provider), generation) | |
1280 self._TranslateBotoKeyAcl(key, cloud_api_object, | |
1281 generation=generation_str) | |
1282 | |
1283 return cloud_api_object | |
1284 | |
1285 def _TranslateBotoKeyCustomMetadata(self, key): | |
1286 """Populates an apitools message from custom metadata in the boto key.""" | |
1287 custom_metadata = None | |
1288 if getattr(key, 'metadata', None): | |
1289 custom_metadata = apitools_messages.Object.MetadataValue( | |
1290 additionalProperties=[]) | |
1291 for k, v in key.metadata.iteritems(): | |
1292 if k.lower() == 'content-language': | |
1293 # Work around content-language being inserted into custom metadata. | |
1294 continue | |
1295 custom_metadata.additionalProperties.append( | |
1296 apitools_messages.Object.MetadataValue.AdditionalProperty( | |
1297 key=k, value=v)) | |
1298 return custom_metadata | |
1299 | |
1300 def _TranslateBotoKeyGeneration(self, key): | |
1301 """Returns the generation/version_id number from the boto key if present.""" | |
1302 generation = None | |
1303 if self.provider == 'gs': | |
1304 if getattr(key, 'generation', None): | |
1305 generation = long(key.generation) | |
1306 elif self.provider == 's3': | |
1307 if getattr(key, 'version_id', None): | |
1308 generation = EncodeStringAsLong(key.version_id) | |
1309 return generation | |
1310 | |
1311 def _TranslateBotoKeyMetageneration(self, key): | |
1312 """Returns the metageneration number from the boto key if present.""" | |
1313 metageneration = None | |
1314 if self.provider == 'gs': | |
1315 if getattr(key, 'metageneration', None): | |
1316 metageneration = long(key.metageneration) | |
1317 return metageneration | |
1318 | |
1319 def _TranslateBotoKeyTimestamp(self, key): | |
1320 """Parses the timestamp from the boto key into an datetime object. | |
1321 | |
1322 This avoids a dependency on dateutil. | |
1323 | |
1324 Args: | |
1325 key: Boto key to get timestamp from. | |
1326 | |
1327 Returns: | |
1328 datetime object if string is parsed successfully, None otherwise. | |
1329 """ | |
1330 if key.last_modified: | |
1331 if '.' in key.last_modified: | |
1332 key_us_timestamp = key.last_modified.rstrip('Z') + '000Z' | |
1333 else: | |
1334 key_us_timestamp = key.last_modified.rstrip('Z') + '.000000Z' | |
1335 fmt = '%Y-%m-%dT%H:%M:%S.%fZ' | |
1336 try: | |
1337 return datetime.datetime.strptime(key_us_timestamp, fmt) | |
1338 except ValueError: | |
1339 try: | |
1340 # Try alternate format | |
1341 fmt = '%a, %d %b %Y %H:%M:%S %Z' | |
1342 return datetime.datetime.strptime(key.last_modified, fmt) | |
1343 except ValueError: | |
1344 # Could not parse the time; leave updated as None. | |
1345 return None | |
1346 | |
1347 def _TranslateDeleteMarker(self, key, cloud_api_object): | |
1348 """Marks deleted objects with a metadata value (for S3 compatibility).""" | |
1349 if isinstance(key, DeleteMarker): | |
1350 if not cloud_api_object.metadata: | |
1351 cloud_api_object.metadata = apitools_messages.Object.MetadataValue() | |
1352 cloud_api_object.metadata.additionalProperties = [] | |
1353 cloud_api_object.metadata.additionalProperties.append( | |
1354 apitools_messages.Object.MetadataValue.AdditionalProperty( | |
1355 key=S3_DELETE_MARKER_GUID, value=True)) | |
1356 | |
1357 def _TranslateBotoKeyAcl(self, key, cloud_api_object, generation=None): | |
1358 """Updates cloud_api_object with the ACL from the boto key.""" | |
1359 storage_uri_for_key = self._StorageUriForObject(key.bucket.name, key.name, | |
1360 generation=generation) | |
1361 headers = {} | |
1362 self._AddApiVersionToHeaders(headers) | |
1363 try: | |
1364 if self.provider == 'gs': | |
1365 key_acl = storage_uri_for_key.get_acl(headers=headers) | |
1366 # key.get_acl() does not support versioning so we need to use | |
1367 # storage_uri to ensure we're getting the versioned ACL. | |
1368 for acl in AclTranslation.BotoObjectAclToMessage(key_acl): | |
1369 cloud_api_object.acl.append(acl) | |
1370 if self.provider == 's3': | |
1371 key_acl = key.get_xml_acl(headers=headers) | |
1372 # ACLs for s3 are different and we use special markers to represent | |
1373 # them in the gsutil Cloud API. | |
1374 AddS3MarkerAclToObjectMetadata(cloud_api_object, key_acl) | |
1375 except boto.exception.GSResponseError, e: | |
1376 if e.status == 403: | |
1377 # Consume access denied exceptions to mimic JSON behavior of simply | |
1378 # returning None if sufficient permission is not present. The caller | |
1379 # needs to handle the case where the ACL is not populated. | |
1380 pass | |
1381 else: | |
1382 raise | |
1383 | |
1384 def _TranslateExceptionAndRaise(self, e, bucket_name=None, object_name=None, | |
1385 generation=None): | |
1386 """Translates a Boto exception and raises the translated or original value. | |
1387 | |
1388 Args: | |
1389 e: Any Exception. | |
1390 bucket_name: Optional bucket name in request that caused the exception. | |
1391 object_name: Optional object name in request that caused the exception. | |
1392 generation: Optional generation in request that caused the exception. | |
1393 | |
1394 Raises: | |
1395 Translated CloudApi exception, or the original exception if it was not | |
1396 translatable. | |
1397 """ | |
1398 translated_exception = self._TranslateBotoException( | |
1399 e, bucket_name=bucket_name, object_name=object_name, | |
1400 generation=generation) | |
1401 if translated_exception: | |
1402 raise translated_exception | |
1403 else: | |
1404 raise | |
1405 | |
1406 def _TranslateBotoException(self, e, bucket_name=None, object_name=None, | |
1407 generation=None): | |
1408 """Translates boto exceptions into their gsutil Cloud API equivalents. | |
1409 | |
1410 Args: | |
1411 e: Any exception in TRANSLATABLE_BOTO_EXCEPTIONS. | |
1412 bucket_name: Optional bucket name in request that caused the exception. | |
1413 object_name: Optional object name in request that caused the exception. | |
1414 generation: Optional generation in request that caused the exception. | |
1415 | |
1416 Returns: | |
1417 CloudStorageApiServiceException for translatable exceptions, None | |
1418 otherwise. | |
1419 | |
1420 Because we're using isinstance, check for subtypes first. | |
1421 """ | |
1422 if isinstance(e, boto.exception.StorageResponseError): | |
1423 if e.status == 400: | |
1424 return BadRequestException(e.code, status=e.status, body=e.body) | |
1425 elif e.status == 401 or e.status == 403: | |
1426 return AccessDeniedException(e.code, status=e.status, body=e.body) | |
1427 elif e.status == 404: | |
1428 if bucket_name: | |
1429 if object_name: | |
1430 return CreateObjectNotFoundException(e.status, self.provider, | |
1431 bucket_name, object_name, | |
1432 generation=generation) | |
1433 return CreateBucketNotFoundException(e.status, self.provider, | |
1434 bucket_name) | |
1435 return NotFoundException(e.code, status=e.status, body=e.body) | |
1436 elif e.status == 409 and e.code and 'BucketNotEmpty' in e.code: | |
1437 return NotEmptyException('BucketNotEmpty (%s)' % bucket_name, | |
1438 status=e.status, body=e.body) | |
1439 elif e.status == 410: | |
1440 # 410 errors should always cause us to start over - either the UploadID | |
1441 # has expired or there was a server-side problem that requires starting | |
1442 # the upload over from scratch. | |
1443 return ResumableUploadStartOverException(e.message) | |
1444 elif e.status == 412: | |
1445 return PreconditionException(e.code, status=e.status, body=e.body) | |
1446 if isinstance(e, boto.exception.StorageCreateError): | |
1447 return ServiceException('Bucket already exists.', status=e.status, | |
1448 body=e.body) | |
1449 | |
1450 if isinstance(e, boto.exception.BotoServerError): | |
1451 return ServiceException(e.message, status=e.status, body=e.body) | |
1452 | |
1453 if isinstance(e, boto.exception.InvalidUriError): | |
1454 # Work around textwrap when searching for this string. | |
1455 if e.message and NON_EXISTENT_OBJECT_REGEX.match(e.message.encode(UTF8)): | |
1456 return NotFoundException(e.message, status=404) | |
1457 return InvalidUrlError(e.message) | |
1458 | |
1459 if isinstance(e, boto.exception.ResumableUploadException): | |
1460 if e.disposition == boto.exception.ResumableTransferDisposition.ABORT: | |
1461 return ResumableUploadAbortException(e.message) | |
1462 elif (e.disposition == | |
1463 boto.exception.ResumableTransferDisposition.START_OVER): | |
1464 return ResumableUploadStartOverException(e.message) | |
1465 else: | |
1466 return ResumableUploadException(e.message) | |
1467 | |
1468 if isinstance(e, boto.exception.ResumableDownloadException): | |
1469 return ResumableDownloadException(e.message) | |
1470 | |
1471 return None | |
1472 | |
1473 # For function docstrings, see CloudApiDelegator class. | |
1474 def XmlPassThroughGetAcl(self, storage_url, def_obj_acl=False): | |
1475 """See CloudApiDelegator class for function doc strings.""" | |
1476 try: | |
1477 uri = boto.storage_uri( | |
1478 storage_url.url_string, suppress_consec_slashes=False, | |
1479 bucket_storage_uri_class=self.bucket_storage_uri_class, | |
1480 debug=self.debug) | |
1481 if def_obj_acl: | |
1482 return uri.get_def_acl() | |
1483 else: | |
1484 return uri.get_acl() | |
1485 except TRANSLATABLE_BOTO_EXCEPTIONS, e: | |
1486 self._TranslateExceptionAndRaise(e) | |
1487 | |
1488 def XmlPassThroughSetAcl(self, acl_text, storage_url, canned=True, | |
1489 def_obj_acl=False): | |
1490 """See CloudApiDelegator class for function doc strings.""" | |
1491 try: | |
1492 uri = boto.storage_uri( | |
1493 storage_url.url_string, suppress_consec_slashes=False, | |
1494 bucket_storage_uri_class=self.bucket_storage_uri_class, | |
1495 debug=self.debug) | |
1496 if canned: | |
1497 if def_obj_acl: | |
1498 canned_acls = uri.canned_acls() | |
1499 if acl_text not in canned_acls: | |
1500 raise CommandException('Invalid canned ACL "%s".' % acl_text) | |
1501 uri.set_def_acl(acl_text, uri.object_name) | |
1502 else: | |
1503 canned_acls = uri.canned_acls() | |
1504 if acl_text not in canned_acls: | |
1505 raise CommandException('Invalid canned ACL "%s".' % acl_text) | |
1506 uri.set_acl(acl_text, uri.object_name) | |
1507 else: | |
1508 if def_obj_acl: | |
1509 uri.set_def_xml_acl(acl_text, uri.object_name) | |
1510 else: | |
1511 uri.set_xml_acl(acl_text, uri.object_name) | |
1512 except TRANSLATABLE_BOTO_EXCEPTIONS, e: | |
1513 self._TranslateExceptionAndRaise(e) | |
1514 | |
1515 # pylint: disable=catching-non-exception | |
1516 def XmlPassThroughSetCors(self, cors_text, storage_url): | |
1517 """See CloudApiDelegator class for function doc strings.""" | |
1518 # Parse XML document and convert into Cors object. | |
1519 if storage_url.scheme == 's3': | |
1520 cors_obj = S3Cors() | |
1521 else: | |
1522 cors_obj = Cors() | |
1523 h = handler.XmlHandler(cors_obj, None) | |
1524 try: | |
1525 xml.sax.parseString(cors_text, h) | |
1526 except SaxExceptions.SAXParseException, e: | |
1527 raise CommandException('Requested CORS is invalid: %s at line %s, ' | |
1528 'column %s' % (e.getMessage(), e.getLineNumber(), | |
1529 e.getColumnNumber())) | |
1530 | |
1531 try: | |
1532 uri = boto.storage_uri( | |
1533 storage_url.url_string, suppress_consec_slashes=False, | |
1534 bucket_storage_uri_class=self.bucket_storage_uri_class, | |
1535 debug=self.debug) | |
1536 uri.set_cors(cors_obj, False) | |
1537 except TRANSLATABLE_BOTO_EXCEPTIONS, e: | |
1538 self._TranslateExceptionAndRaise(e) | |
1539 | |
1540 def XmlPassThroughGetCors(self, storage_url): | |
1541 """See CloudApiDelegator class for function doc strings.""" | |
1542 uri = boto.storage_uri( | |
1543 storage_url.url_string, suppress_consec_slashes=False, | |
1544 bucket_storage_uri_class=self.bucket_storage_uri_class, | |
1545 debug=self.debug) | |
1546 try: | |
1547 cors = uri.get_cors(False) | |
1548 except TRANSLATABLE_BOTO_EXCEPTIONS, e: | |
1549 self._TranslateExceptionAndRaise(e) | |
1550 | |
1551 parsed_xml = xml.dom.minidom.parseString(cors.to_xml().encode(UTF8)) | |
1552 # Pretty-print the XML to make it more easily human editable. | |
1553 return parsed_xml.toprettyxml(indent=' ') | |
1554 | |
1555 def XmlPassThroughGetLifecycle(self, storage_url): | |
1556 """See CloudApiDelegator class for function doc strings.""" | |
1557 try: | |
1558 uri = boto.storage_uri( | |
1559 storage_url.url_string, suppress_consec_slashes=False, | |
1560 bucket_storage_uri_class=self.bucket_storage_uri_class, | |
1561 debug=self.debug) | |
1562 lifecycle = uri.get_lifecycle_config(False) | |
1563 except TRANSLATABLE_BOTO_EXCEPTIONS, e: | |
1564 self._TranslateExceptionAndRaise(e) | |
1565 | |
1566 parsed_xml = xml.dom.minidom.parseString(lifecycle.to_xml().encode(UTF8)) | |
1567 # Pretty-print the XML to make it more easily human editable. | |
1568 return parsed_xml.toprettyxml(indent=' ') | |
1569 | |
1570 def XmlPassThroughSetLifecycle(self, lifecycle_text, storage_url): | |
1571 """See CloudApiDelegator class for function doc strings.""" | |
1572 # Parse XML document and convert into lifecycle object. | |
1573 if storage_url.scheme == 's3': | |
1574 lifecycle_obj = S3Lifecycle() | |
1575 else: | |
1576 lifecycle_obj = LifecycleConfig() | |
1577 h = handler.XmlHandler(lifecycle_obj, None) | |
1578 try: | |
1579 xml.sax.parseString(lifecycle_text, h) | |
1580 except SaxExceptions.SAXParseException, e: | |
1581 raise CommandException( | |
1582 'Requested lifecycle config is invalid: %s at line %s, column %s' % | |
1583 (e.getMessage(), e.getLineNumber(), e.getColumnNumber())) | |
1584 | |
1585 try: | |
1586 uri = boto.storage_uri( | |
1587 storage_url.url_string, suppress_consec_slashes=False, | |
1588 bucket_storage_uri_class=self.bucket_storage_uri_class, | |
1589 debug=self.debug) | |
1590 uri.configure_lifecycle(lifecycle_obj, False) | |
1591 except TRANSLATABLE_BOTO_EXCEPTIONS, e: | |
1592 self._TranslateExceptionAndRaise(e) | |
1593 | |
1594 def XmlPassThroughGetLogging(self, storage_url): | |
1595 """See CloudApiDelegator class for function doc strings.""" | |
1596 try: | |
1597 uri = boto.storage_uri( | |
1598 storage_url.url_string, suppress_consec_slashes=False, | |
1599 bucket_storage_uri_class=self.bucket_storage_uri_class, | |
1600 debug=self.debug) | |
1601 logging_config_xml = UnaryDictToXml(uri.get_logging_config()) | |
1602 except TRANSLATABLE_BOTO_EXCEPTIONS, e: | |
1603 self._TranslateExceptionAndRaise(e) | |
1604 | |
1605 return XmlParseString(logging_config_xml).toprettyxml() | |
1606 | |
1607 def XmlPassThroughGetWebsite(self, storage_url): | |
1608 """See CloudApiDelegator class for function doc strings.""" | |
1609 try: | |
1610 uri = boto.storage_uri( | |
1611 storage_url.url_string, suppress_consec_slashes=False, | |
1612 bucket_storage_uri_class=self.bucket_storage_uri_class, | |
1613 debug=self.debug) | |
1614 web_config_xml = UnaryDictToXml(uri.get_website_config()) | |
1615 except TRANSLATABLE_BOTO_EXCEPTIONS, e: | |
1616 self._TranslateExceptionAndRaise(e) | |
1617 | |
1618 return XmlParseString(web_config_xml).toprettyxml() | |
OLD | NEW |