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 |