Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(265)

Side by Side Diff: gslib/commands/setmeta.py

Issue 698893003: Update checked in version of gsutil to version 4.6 (Closed) Base URL: http://dart.googlecode.com/svn/third_party/gsutil/
Patch Set: Created 6 years, 1 month ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « gslib/commands/rsync.py ('k') | gslib/commands/signurl.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
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
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
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-')
OLDNEW
« no previous file with comments | « gslib/commands/rsync.py ('k') | gslib/commands/signurl.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698