| OLD | NEW |
| 1 # -*- coding: utf-8 -*- |
| 1 # Copyright 2012 Google Inc. All Rights Reserved. | 2 # Copyright 2012 Google Inc. All Rights Reserved. |
| 2 #coding=utf8 | |
| 3 # | 3 # |
| 4 # Licensed under the Apache License, Version 2.0 (the "License"); | 4 # Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 # you may not use this file except in compliance with 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 | 6 # You may obtain a copy of the License at |
| 7 # | 7 # |
| 8 # http://www.apache.org/licenses/LICENSE-2.0 | 8 # http://www.apache.org/licenses/LICENSE-2.0 |
| 9 # | 9 # |
| 10 # Unless required by applicable law or agreed to in writing, software | 10 # Unless required by applicable law or agreed to in writing, software |
| 11 # distributed under the License is distributed on an "AS IS" BASIS, | 11 # distributed under the License is distributed on an "AS IS" BASIS, |
| 12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 # See the License for the specific language governing permissions and | 13 # See the License for the specific language governing permissions and |
| 14 # limitations under the License. | 14 # limitations under the License. |
| 15 """Implementation of setmeta command for setting cloud object metadata.""" |
| 15 | 16 |
| 16 import boto | 17 from __future__ import absolute_import |
| 17 import csv | |
| 18 import random | |
| 19 import StringIO | |
| 20 import time | |
| 21 | 18 |
| 22 from boto.exception import GSResponseError | 19 from gslib.cloud_api import AccessDeniedException |
| 23 from boto.s3.key import Key | 20 from gslib.cloud_api import PreconditionException |
| 24 from gslib.command import COMMAND_NAME | 21 from gslib.cloud_api import Preconditions |
| 25 from gslib.command import COMMAND_NAME_ALIASES | |
| 26 from gslib.command import Command | 22 from gslib.command import Command |
| 27 from gslib.command import FILE_URIS_OK | 23 from gslib.cs_api_map import ApiSelector |
| 28 from gslib.command import MAX_ARGS | |
| 29 from gslib.command import MIN_ARGS | |
| 30 from gslib.command import PROVIDER_URIS_OK | |
| 31 from gslib.command import SUPPORTED_SUB_ARGS | |
| 32 from gslib.command import URIS_START_ARG | |
| 33 from gslib.exception import CommandException | 24 from gslib.exception import CommandException |
| 34 from gslib.help_provider import HELP_NAME | |
| 35 from gslib.help_provider import HELP_NAME_ALIASES | |
| 36 from gslib.help_provider import HELP_ONE_LINE_SUMMARY | |
| 37 from gslib.help_provider import HELP_TEXT | |
| 38 from gslib.help_provider import HELP_TYPE | |
| 39 from gslib.help_provider import HelpType | |
| 40 from gslib.name_expansion import NameExpansionIterator | 25 from gslib.name_expansion import NameExpansionIterator |
| 26 from gslib.storage_url import StorageUrlFromString |
| 27 from gslib.translation_helper import CopyObjectMetadata |
| 28 from gslib.translation_helper import ObjectMetadataFromHeaders |
| 29 from gslib.util import GetCloudApiInstance |
| 41 from gslib.util import NO_MAX | 30 from gslib.util import NO_MAX |
| 42 from gslib.util import Retry | 31 from gslib.util import Retry |
| 43 | 32 |
| 44 _detailed_help_text = (""" | 33 |
| 34 _DETAILED_HELP_TEXT = (""" |
| 45 <B>SYNOPSIS</B> | 35 <B>SYNOPSIS</B> |
| 46 gsutil setmeta [-n] -h [header:value|header] ... uri... | 36 gsutil setmeta [-n] -h [header:value|header] ... url... |
| 47 | 37 |
| 48 | 38 |
| 49 <B>DESCRIPTION</B> | 39 <B>DESCRIPTION</B> |
| 50 The gsutil setmeta command allows you to set or remove the metadata on one | 40 The gsutil setmeta command allows you to set or remove the metadata on one |
| 51 or more objects. It takes one or more header arguments followed by one or | 41 or more objects. It takes one or more header arguments followed by one or |
| 52 more URIs, where each header argument is in one of two forms: | 42 more URLs, where each header argument is in one of two forms: |
| 53 | 43 |
| 54 - if you specify header:value, it will set the given header on all | 44 - if you specify header:value, it will set the given header on all |
| 55 named objects. | 45 named objects. |
| 56 | 46 |
| 57 - if you specify header (with no value), it will remove the given header | 47 - if you specify header (with no value), it will remove the given header |
| 58 from all named objects. | 48 from all named objects. |
| 59 | 49 |
| 60 For example, the following command would set the Content-Type and | 50 For example, the following command would set the Content-Type and |
| 61 Cache-Control and remove the Content-Disposition on the specified objects: | 51 Cache-Control and remove the Content-Disposition on the specified objects: |
| 62 | 52 |
| (...skipping 17 matching lines...) Expand all Loading... |
| 80 header allowing such objects to be cached for 3600 seconds. If you need to | 70 header allowing such objects to be cached for 3600 seconds. If you need to |
| 81 ensure that updates become visible immediately, you should set a Cache-Control | 71 ensure that updates become visible immediately, you should set a Cache-Control |
| 82 header of "Cache-Control:private, max-age=0, no-transform" on such objects. | 72 header of "Cache-Control:private, max-age=0, no-transform" on such objects. |
| 83 You can do this with the command: | 73 You can do this with the command: |
| 84 | 74 |
| 85 gsutil setmeta -h "Content-Type:text/html" \\ | 75 gsutil setmeta -h "Content-Type:text/html" \\ |
| 86 -h "Cache-Control:private, max-age=0, no-transform" gs://bucket/*.html | 76 -h "Cache-Control:private, max-age=0, no-transform" gs://bucket/*.html |
| 87 | 77 |
| 88 | 78 |
| 89 <B>OPERATION COST</B> | 79 <B>OPERATION COST</B> |
| 90 This command uses four operations per URI (one to read the ACL, one to read | 80 This command uses four operations per URL (one to read the ACL, one to read |
| 91 the current metadata, one to set the new metadata, and one to set the ACL). | 81 the current metadata, one to set the new metadata, and one to set the ACL). |
| 92 | 82 |
| 93 For cases where you want all objects to have the same ACL you can avoid half | 83 For cases where you want all objects to have the same ACL you can avoid half |
| 94 these operations by setting a default ACL on the bucket(s) containing the | 84 these operations by setting a default ACL on the bucket(s) containing the |
| 95 named objects, and using the setmeta -n option. See "help gsutil defacl". | 85 named objects, and using the setmeta -n option. See "help gsutil defacl". |
| 96 | 86 |
| 97 | 87 |
| 98 <B>OPTIONS</B> | 88 <B>OPTIONS</B> |
| 99 -h Specifies a header:value to be added, or header to be removed, | 89 -h Specifies a header:value to be added, or header to be removed, |
| 100 from each named object. | 90 from each named object. |
| 101 | |
| 102 -n Causes the operations for reading and writing the ACL to be | 91 -n Causes the operations for reading and writing the ACL to be |
| 103 skipped. This halves the number of operations performed per | 92 skipped. This halves the number of operations performed per |
| 104 request, improving the speed and reducing the cost of performing | 93 request, improving the speed and reducing the cost of performing |
| 105 the operations. This option makes sense for cases where you want | 94 the operations. This option makes sense for cases where you want |
| 106 all objects to have the same ACL, for which you have set a default | 95 all objects to have the same ACL, for which you have set a default |
| 107 ACL on the bucket(s) containing the objects. See "help gsutil | 96 ACL on the bucket(s) containing the objects. See "help gsutil |
| 108 defacl". | 97 defacl". |
| 98 """) |
| 109 | 99 |
| 110 -R, -r Performs setmeta request recursively, to all objects under the | 100 # Setmeta assumes a header-like model which doesn't line up with the JSON way |
| 111 specified URI. | 101 # of doing things. This list comes from functionality that was supported by |
| 112 """) | 102 # gsutil3 at the time gsutil4 was released. |
| 103 SETTABLE_FIELDS = ['cache-control', 'content-disposition', |
| 104 'content-encoding', 'content-language', |
| 105 'content-md5', 'content-type'] |
| 106 |
| 113 | 107 |
| 114 def _SetMetadataExceptionHandler(cls, e): | 108 def _SetMetadataExceptionHandler(cls, e): |
| 115 """Exception handler that maintains state about post-completion status.""" | 109 """Exception handler that maintains state about post-completion status.""" |
| 116 cls.logger.error(e) | 110 cls.logger.error(e) |
| 117 cls.everything_set_okay = False | 111 cls.everything_set_okay = False |
| 118 | 112 |
| 119 def _SetMetadataFuncWrapper(cls, name_expansion_result): | 113 |
| 120 cls._SetMetadataFunc(name_expansion_result) | 114 def _SetMetadataFuncWrapper(cls, name_expansion_result, thread_state=None): |
| 115 cls.SetMetadataFunc(name_expansion_result, thread_state=thread_state) |
| 121 | 116 |
| 122 | 117 |
| 123 class SetMetaCommand(Command): | 118 class SetMetaCommand(Command): |
| 124 """Implementation of gsutil setmeta command.""" | 119 """Implementation of gsutil setmeta command.""" |
| 125 | 120 |
| 126 # Command specification (processed by parent class). | 121 # Command specification. See base class for documentation. |
| 127 command_spec = { | 122 command_spec = Command.CreateCommandSpec( |
| 128 # Name of command. | 123 'setmeta', |
| 129 COMMAND_NAME : 'setmeta', | 124 command_name_aliases=['setheader'], |
| 130 # List of command name aliases. | 125 min_args=1, |
| 131 COMMAND_NAME_ALIASES : ['setheader'], | 126 max_args=NO_MAX, |
| 132 # Min number of args required by this command. | 127 supported_sub_args='h:nrR', |
| 133 MIN_ARGS : 1, | 128 file_url_ok=False, |
| 134 # Max number of args required by this command, or NO_MAX. | 129 provider_url_ok=False, |
| 135 MAX_ARGS : NO_MAX, | 130 urls_start_arg=1, |
| 136 # Getopt-style string specifying acceptable sub args. | 131 gs_api_support=[ApiSelector.XML, ApiSelector.JSON], |
| 137 SUPPORTED_SUB_ARGS : 'h:nrR', | 132 gs_default_api=ApiSelector.JSON, |
| 138 # True if file URIs acceptable for this command. | 133 ) |
| 139 FILE_URIS_OK : False, | 134 # Help specification. See help_provider.py for documentation. |
| 140 # True if provider-only URIs acceptable for this command. | 135 help_spec = Command.HelpSpec( |
| 141 PROVIDER_URIS_OK : False, | 136 help_name='setmeta', |
| 142 # Index in args of first URI arg. | 137 help_name_aliases=['setheader'], |
| 143 URIS_START_ARG : 1, | 138 help_type='command_help', |
| 144 } | 139 help_one_line_summary='Set metadata on already uploaded objects', |
| 145 help_spec = { | 140 help_text=_DETAILED_HELP_TEXT, |
| 146 # Name of command or auxiliary help info for which this help applies. | 141 subcommand_help_text={}, |
| 147 HELP_NAME : 'setmeta', | 142 ) |
| 148 # List of help name aliases. | |
| 149 HELP_NAME_ALIASES : ['setheader'], | |
| 150 # Type of help: | |
| 151 HELP_TYPE : HelpType.COMMAND_HELP, | |
| 152 # One line summary of this help. | |
| 153 HELP_ONE_LINE_SUMMARY : 'Set metadata on already uploaded objects', | |
| 154 # The full help text. | |
| 155 HELP_TEXT : _detailed_help_text, | |
| 156 } | |
| 157 | 143 |
| 158 # Command entry point. | |
| 159 def RunCommand(self): | 144 def RunCommand(self): |
| 160 if (len(self.args) == 1 and not self.recursion_requested | 145 """Command entry point for the setmeta command.""" |
| 161 and not self.suri_builder.StorageUri(self.args[0]).names_object()): | 146 headers = [] |
| 162 raise CommandException('URI (%s) must name an object' % self.args[0]) | 147 if self.sub_opts: |
| 148 for o, a in self.sub_opts: |
| 149 if o == '-n': |
| 150 self.logger.warning( |
| 151 'Warning: gsutil setmeta -n is now on by default, and will be ' |
| 152 'removed in the future.\nPlease use gsutil acl set ... to set ' |
| 153 'canned ACLs.') |
| 154 elif o == '-h': |
| 155 if 'x-goog-acl' in a or 'x-amz-acl' in a: |
| 156 raise CommandException( |
| 157 'gsutil setmeta no longer allows canned ACLs. Use gsutil acl ' |
| 158 'set ... to set canned ACLs.') |
| 159 headers.append(a) |
| 160 |
| 161 (metadata_minus, metadata_plus) = self._ParseMetadataHeaders(headers) |
| 162 |
| 163 self.metadata_change = metadata_plus |
| 164 for header in metadata_minus: |
| 165 self.metadata_change[header] = '' |
| 166 |
| 167 if len(self.args) == 1 and not self.recursion_requested: |
| 168 url = StorageUrlFromString(self.args[0]) |
| 169 if not (url.IsCloudUrl() and url.IsObject()): |
| 170 raise CommandException('URL (%s) must name an object' % self.args[0]) |
| 163 | 171 |
| 164 # Used to track if any objects' metadata failed to be set. | 172 # Used to track if any objects' metadata failed to be set. |
| 165 self.everything_set_okay = True | 173 self.everything_set_okay = True |
| 166 | 174 |
| 167 name_expansion_iterator = NameExpansionIterator( | 175 name_expansion_iterator = NameExpansionIterator( |
| 168 self.command_name, self.proj_id_handler, self.headers, self.debug, | 176 self.command_name, self.debug, self.logger, self.gsutil_api, |
| 169 self.logger, self.bucket_storage_uri_class, self.args, | 177 self.args, self.recursion_requested, all_versions=self.all_versions, |
| 170 self.recursion_requested, flat=self.recursion_requested) | 178 continue_on_error=self.parallel_operations) |
| 179 |
| 171 try: | 180 try: |
| 172 # Perform requests in parallel (-m) mode, if requested, using | 181 # Perform requests in parallel (-m) mode, if requested, using |
| 173 # configured number of parallel processes and threads. Otherwise, | 182 # configured number of parallel processes and threads. Otherwise, |
| 174 # perform requests with sequential function calls in current process. | 183 # perform requests with sequential function calls in current process. |
| 175 self.Apply(_SetMetadataFuncWrapper, name_expansion_iterator, | 184 self.Apply(_SetMetadataFuncWrapper, name_expansion_iterator, |
| 176 _SetMetadataExceptionHandler, fail_on_error=True) | 185 _SetMetadataExceptionHandler, fail_on_error=True) |
| 177 except GSResponseError as e: | 186 except AccessDeniedException as e: |
| 178 if e.code == 'AccessDenied' and e.reason == 'Forbidden' \ | 187 if e.status == 403: |
| 179 and e.status == 403: | |
| 180 self._WarnServiceAccounts() | 188 self._WarnServiceAccounts() |
| 181 raise | 189 raise |
| 182 | 190 |
| 183 if not self.everything_set_okay: | 191 if not self.everything_set_okay: |
| 184 raise CommandException('Metadata for some objects could not be set.') | 192 raise CommandException('Metadata for some objects could not be set.') |
| 185 | 193 |
| 186 return 0 | 194 return 0 |
| 187 | |
| 188 @Retry(GSResponseError, tries=3, timeout_secs=1) | |
| 189 def _SetMetadataFunc(self, name_expansion_result): | |
| 190 headers = [] | |
| 191 preserve_acl = True | |
| 192 if self.sub_opts: | |
| 193 for o, a in self.sub_opts: | |
| 194 if o == '-n': | |
| 195 preserve_acl = False | |
| 196 elif o == '-h': | |
| 197 headers.append(a) | |
| 198 | 195 |
| 199 (metadata_minus, metadata_plus) = self._ParseMetadataHeaders(headers) | 196 @Retry(PreconditionException, tries=3, timeout_secs=1) |
| 197 def SetMetadataFunc(self, name_expansion_result, thread_state=None): |
| 198 """Sets metadata on an object. |
| 200 | 199 |
| 201 exp_src_uri = self.suri_builder.StorageUri( | 200 Args: |
| 202 name_expansion_result.GetExpandedUriStr()) | 201 name_expansion_result: NameExpansionResult describing target object. |
| 203 self.logger.info('Setting metadata on %s...', exp_src_uri) | 202 thread_state: gsutil Cloud API instance to use for the operation. |
| 203 """ |
| 204 gsutil_api = GetCloudApiInstance(self, thread_state=thread_state) |
| 204 | 205 |
| 205 key = exp_src_uri.get_key() | 206 exp_src_url = name_expansion_result.expanded_storage_url |
| 206 metageneration = getattr(key, 'metageneration', None) | 207 self.logger.info('Setting metadata on %s...', exp_src_url) |
| 207 generation = getattr(key, 'generation', None) | |
| 208 | 208 |
| 209 headers = {} | 209 fields = ['generation', 'metadata', 'metageneration'] |
| 210 if generation: | 210 cloud_obj_metadata = gsutil_api.GetObjectMetadata( |
| 211 headers['x-goog-if-generation-match'] = generation | 211 exp_src_url.bucket_name, exp_src_url.object_name, |
| 212 if metageneration: | 212 generation=exp_src_url.generation, provider=exp_src_url.scheme, |
| 213 headers['x-goog-if-metageneration-match'] = metageneration | 213 fields=fields) |
| 214 | 214 |
| 215 # If this fails because of a precondition, it will raise a | 215 preconditions = Preconditions( |
| 216 # GSResponseError for @Retry to handle. | 216 gen_match=cloud_obj_metadata.generation, |
| 217 exp_src_uri.set_metadata(metadata_plus, metadata_minus, preserve_acl, | 217 meta_gen_match=cloud_obj_metadata.metageneration) |
| 218 headers=headers) | 218 |
| 219 # Patch handles the patch semantics for most metadata, but we need to |
| 220 # merge the custom metadata field manually. |
| 221 patch_obj_metadata = ObjectMetadataFromHeaders(self.metadata_change) |
| 222 |
| 223 api = gsutil_api.GetApiSelector(provider=exp_src_url.scheme) |
| 224 # For XML we only want to patch through custom metadata that has |
| 225 # changed. For JSON we need to build the complete set. |
| 226 if api == ApiSelector.XML: |
| 227 pass |
| 228 elif api == ApiSelector.JSON: |
| 229 CopyObjectMetadata(patch_obj_metadata, cloud_obj_metadata, |
| 230 override=True) |
| 231 patch_obj_metadata = cloud_obj_metadata |
| 232 |
| 233 gsutil_api.PatchObjectMetadata( |
| 234 exp_src_url.bucket_name, exp_src_url.object_name, patch_obj_metadata, |
| 235 generation=exp_src_url.generation, preconditions=preconditions, |
| 236 provider=exp_src_url.scheme) |
| 219 | 237 |
| 220 def _ParseMetadataHeaders(self, headers): | 238 def _ParseMetadataHeaders(self, headers): |
| 239 """Validates and parses metadata changes from the headers argument. |
| 240 |
| 241 Args: |
| 242 headers: Header dict to validate and parse. |
| 243 |
| 244 Returns: |
| 245 (metadata_plus, metadata_minus): Tuple of header sets to add and remove. |
| 246 """ |
| 221 metadata_minus = set() | 247 metadata_minus = set() |
| 222 cust_metadata_minus = set() | 248 cust_metadata_minus = set() |
| 223 metadata_plus = {} | 249 metadata_plus = {} |
| 224 cust_metadata_plus = {} | 250 cust_metadata_plus = {} |
| 225 # Build a count of the keys encountered from each plus and minus arg so we | 251 # Build a count of the keys encountered from each plus and minus arg so we |
| 226 # can check for dupe field specs. | 252 # can check for dupe field specs. |
| 227 num_metadata_plus_elems = 0 | 253 num_metadata_plus_elems = 0 |
| 228 num_cust_metadata_plus_elems = 0 | 254 num_cust_metadata_plus_elems = 0 |
| 229 num_metadata_minus_elems = 0 | 255 num_metadata_minus_elems = 0 |
| 230 num_cust_metadata_minus_elems = 0 | 256 num_cust_metadata_minus_elems = 0 |
| (...skipping 33 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 264 metadata_minus.add(header) | 290 metadata_minus.add(header) |
| 265 num_metadata_minus_elems += 1 | 291 num_metadata_minus_elems += 1 |
| 266 | 292 |
| 267 if (num_metadata_plus_elems != len(metadata_plus) | 293 if (num_metadata_plus_elems != len(metadata_plus) |
| 268 or num_cust_metadata_plus_elems != len(cust_metadata_plus) | 294 or num_cust_metadata_plus_elems != len(cust_metadata_plus) |
| 269 or num_metadata_minus_elems != len(metadata_minus) | 295 or num_metadata_minus_elems != len(metadata_minus) |
| 270 or num_cust_metadata_minus_elems != len(cust_metadata_minus) | 296 or num_cust_metadata_minus_elems != len(cust_metadata_minus) |
| 271 or metadata_minus.intersection(set(metadata_plus.keys()))): | 297 or metadata_minus.intersection(set(metadata_plus.keys()))): |
| 272 raise CommandException('Each header must appear at most once.') | 298 raise CommandException('Each header must appear at most once.') |
| 273 other_than_base_fields = (set(metadata_plus.keys()) | 299 other_than_base_fields = (set(metadata_plus.keys()) |
| 274 .difference(Key.base_user_settable_fields)) | 300 .difference(SETTABLE_FIELDS)) |
| 275 other_than_base_fields.update( | 301 other_than_base_fields.update( |
| 276 metadata_minus.difference(Key.base_user_settable_fields)) | 302 metadata_minus.difference(SETTABLE_FIELDS)) |
| 277 for f in other_than_base_fields: | 303 for f in other_than_base_fields: |
| 278 # This check is overly simple; it would be stronger to check, for each | 304 # This check is overly simple; it would be stronger to check, for each |
| 279 # URI argument, whether f.startswith the | 305 # URL argument, whether f.startswith the |
| 280 # uri.get_provider().metadata_prefix, but here we just parse the spec | 306 # provider metadata_prefix, but here we just parse the spec |
| 281 # once, before processing any of the URIs. This means we will not | 307 # once, before processing any of the URLs. This means we will not |
| 282 # detect if the user tries to set an x-goog-meta- field on an another | 308 # detect if the user tries to set an x-goog-meta- field on an another |
| 283 # provider's object, for example. | 309 # provider's object, for example. |
| 284 if not _IsCustomMeta(f): | 310 if not _IsCustomMeta(f): |
| 285 raise CommandException('Invalid or disallowed header (%s).\n' | 311 raise CommandException( |
| 286 'Only these fields (plus x-goog-meta-* fields)' | 312 'Invalid or disallowed header (%s).\nOnly these fields (plus ' |
| 287 ' can be set or unset:\n%s' % (f, | 313 'x-goog-meta-* fields) can be set or unset:\n%s' % ( |
| 288 sorted(list(Key.base_user_settable_fields)))) | 314 f, sorted(list(SETTABLE_FIELDS)))) |
| 289 metadata_plus.update(cust_metadata_plus) | 315 metadata_plus.update(cust_metadata_plus) |
| 290 metadata_minus.update(cust_metadata_minus) | 316 metadata_minus.update(cust_metadata_minus) |
| 291 return (metadata_minus, metadata_plus) | 317 return (metadata_minus, metadata_plus) |
| 292 | 318 |
| 293 | 319 |
| 294 def _InsistAscii(string, message): | 320 def _InsistAscii(string, message): |
| 295 if not all(ord(c) < 128 for c in string): | 321 if not all(ord(c) < 128 for c in string): |
| 296 raise CommandException(message) | 322 raise CommandException(message) |
| 297 | 323 |
| 298 | 324 |
| 299 def _InsistAsciiHeader(header): | 325 def _InsistAsciiHeader(header): |
| 300 _InsistAscii(header, 'Invalid non-ASCII header (%s).' % header) | 326 _InsistAscii(header, 'Invalid non-ASCII header (%s).' % header) |
| 301 | 327 |
| 302 | 328 |
| 303 def _InsistAsciiHeaderValue(header, value): | 329 def _InsistAsciiHeaderValue(header, value): |
| 304 _InsistAscii( | 330 _InsistAscii( |
| 305 value, ('Invalid non-ASCII value (%s) was provided for header %s.' | 331 value, ('Invalid non-ASCII value (%s) was provided for header %s.' |
| 306 % (value, header))) | 332 % (value, header))) |
| 307 | 333 |
| 308 | 334 |
| 309 def _IsCustomMeta(header): | 335 def _IsCustomMeta(header): |
| 310 return header.startswith('x-goog-meta-') or header.startswith('x-amz-meta-') | 336 return header.startswith('x-goog-meta-') or header.startswith('x-amz-meta-') |
| OLD | NEW |