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

Side by Side Diff: third_party/gsutil/gslib/wildcard_iterator.py

Issue 10199002: Upgrade gsutil to 3.4 (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: Addressed comments Created 8 years, 8 months 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 | « third_party/gsutil/gslib/util.py ('k') | third_party/gsutil/gsutil » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 # Copyright 2010 Google Inc.
2 #
3 # Permission is hereby granted, free of charge, to any person obtaining a
4 # copy of this software and associated documentation files (the
5 # "Software"), to deal in the Software without restriction, including
6 # without limitation the rights to use, copy, modify, merge, publish, dis-
7 # tribute, sublicense, and/or sell copies of the Software, and to permit
8 # persons to whom the Software is furnished to do so, subject to the fol-
9 # lowing conditions:
10 #
11 # The above copyright notice and this permission notice shall be included
12 # in all copies or substantial portions of the Software.
13 #
14 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
15 # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL-
16 # ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
17 # SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
18 # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
20 # IN THE SOFTWARE.
21
22 """Implementation of wildcarding over StorageUris.
23
24 StorageUri is an abstraction that Google introduced in the boto library,
25 for representing storage provider-independent bucket and object names with
26 a shorthand URI-like syntax (see boto/boto/storage_uri.py) The current
27 class provides wildcarding support for StorageUri objects (including both
28 bucket and file system objects), allowing one to express collections of
29 objects with syntax like the following:
30 gs://mybucket/images/*.png
31 file:///tmp/???abc???
32
33 We provide wildcarding support as part of gsutil rather than as part
34 of boto because wildcarding is really part of shell command-like
35 functionality.
36
37 A comment about wildcard semantics: We support both single path component
38 wildcards (e.g., using '*') and recursive wildcards (using '**'), for both
39 file and cloud URIs. For example,
40 gs://bucket/doc/*/*.html
41 would enumerate HTML files one directory down from gs://bucket/doc, while
42 gs://bucket/**/*.html
43 would enumerate HTML files in all objects contained in the bucket.
44
45 Note also that if you use file system wildcards it's likely your shell
46 interprets the wildcarding before passing the command to gsutil. For example:
47 % gsutil cp /opt/eclipse/*/*.html gs://bucket/eclipse
48 would likely be expanded by the shell into the following before running gsutil:
49 % gsutil cp /opt/eclipse/RUNNING.html gs://bucket/eclipse
50
51 Note also that most shells don't support '**' wildcarding (I think only
52 zsh does). If you want to use '**' wildcarding with such a shell you can
53 single quote each wildcarded string, so it gets passed uninterpreted by the
54 shell to gsutil (at which point gsutil will perform the wildcarding expansion):
55 % gsutil cp '/opt/eclipse/**/*.html' gs://bucket/eclipse
56 """
57
58 import boto
59 import fnmatch
60 import glob
61 import os
62 import re
63 import sys
64 import urllib
65
66 from boto.s3.prefix import Prefix
67 from boto.storage_uri import BucketStorageUri
68 from bucket_listing_ref import BucketListingRef
69
70 # Regex to determine if a string contains any wildcards.
71 WILDCARD_REGEX = re.compile('[*?\[\]]')
72
73 WILDCARD_OBJECT_ITERATOR = 'wildcard_object_iterator'
74 WILDCARD_BUCKET_ITERATOR = 'wildcard_bucket_iterator'
75
76
77 class WildcardIterator(object):
78 """Base class for wildcarding over StorageUris.
79
80 This class implements support for iterating over StorageUris that
81 contain wildcards.
82
83 The base class is abstract; you should instantiate using the
84 wildcard_iterator() static factory method, which chooses the right
85 implementation depending on the StorageUri.
86 """
87
88 def __repr__(self):
89 """Returns string representation of WildcardIterator."""
90 return 'WildcardIterator(%s)' % self.wildcard_uri
91
92
93 class CloudWildcardIterator(WildcardIterator):
94 """WildcardIterator subclass for buckets and objects.
95
96 Iterates over BucketListingRef matching the StorageUri wildcard. It's
97 much more efficient to request the Key from the BucketListingRef (via
98 GetKey()) than to request the StorageUri and then call uri.get_key()
99 to retrieve the key, for cases where you want to get metadata that's
100 available in the Bucket (for example to get the name and size of
101 each object), because that information is available in the bucket GET
102 results. If you were to iterate over URIs for such cases and then get
103 the name and size info from each resulting StorageUri, it would cause
104 an additional object GET request for each of the result URIs.
105 """
106
107 def __init__(self, wildcard_uri, proj_id_handler,
108 bucket_storage_uri_class=BucketStorageUri,
109 headers=None, debug=0):
110 """
111 Instantiates an iterator over BucketListingRef matching given wildcard URI.
112
113 Args:
114 wildcard_uri: StorageUri that contains the wildcard to iterate.
115 proj_id_handler: ProjectIdHandler to use for current command.
116 bucket_storage_uri_class: BucketStorageUri interface.
117 Settable for testing/mocking.
118 headers: Dictionary containing optional HTTP headers to pass to boto.
119 debug: Debug level to pass in to boto connection (range 0..3).
120 """
121 self.wildcard_uri = wildcard_uri
122 # Make a copy of the headers so any updates we make during wildcard
123 # expansion aren't left in the input params (specifically, so we don't
124 # include the x-goog-project-id header needed by a subset of cases, in
125 # the data returned to caller, which could then be used in other cases
126 # where that header must not be passed).
127 if headers is None:
128 self.headers = {}
129 else:
130 self.headers = headers.copy()
131 self.proj_id_handler = proj_id_handler
132 self.bucket_storage_uri_class = bucket_storage_uri_class
133 self.debug = debug
134
135 def __iter__(self):
136 """Python iterator that gets called when iterating over cloud wildcard.
137
138 Yields:
139 BucketListingRef, or empty iterator if no matches.
140 """
141 # First handle bucket wildcarding, if any.
142 if ContainsWildcard(self.wildcard_uri.bucket_name):
143 regex = fnmatch.translate(self.wildcard_uri.bucket_name)
144 bucket_uris = []
145 prog = re.compile(regex)
146 self.proj_id_handler.FillInProjectHeaderIfNeeded(WILDCARD_BUCKET_ITERATOR,
147 self.wildcard_uri,
148 self.headers)
149 for b in self.wildcard_uri.get_all_buckets(headers=self.headers):
150 if prog.match(b.name):
151 # Use str(b.name) because get_all_buckets() returns Unicode
152 # string, which when used to construct x-goog-copy-src metadata
153 # requests for object-to-object copies causes pathname '/' chars
154 # to be entity-encoded (bucket%2Fdir instead of bucket/dir),
155 # which causes the request to fail.
156 uri_str = '%s://%s' % (self.wildcard_uri.scheme,
157 urllib.quote_plus(str(b.name)))
158 bucket_uris.append(
159 boto.storage_uri(
160 uri_str, debug=self.debug,
161 bucket_storage_uri_class=self.bucket_storage_uri_class,
162 suppress_consec_slashes=False))
163 else:
164 bucket_uris = [self.wildcard_uri.clone_replace_name('')]
165
166 # Now iterate over bucket(s), and handle object wildcarding, if any.
167 self.proj_id_handler.FillInProjectHeaderIfNeeded(WILDCARD_OBJECT_ITERATOR,
168 self.wildcard_uri,
169 self.headers)
170 for bucket_uri in bucket_uris:
171 if self.wildcard_uri.names_bucket():
172 # Bucket-only URI.
173 yield BucketListingRef(bucket_uri, key=None, prefix=None,
174 headers=self.headers)
175 else:
176 # URI contains an object name. If there's no wildcard just yield
177 # the needed URI.
178 if not ContainsWildcard(self.wildcard_uri.object_name):
179 uri_to_yield = bucket_uri.clone_replace_name(
180 self.wildcard_uri.object_name)
181 yield BucketListingRef(uri_to_yield, key=None, prefix=None,
182 headers=self.headers)
183 else:
184 # URI contains a wildcard. Expand iteratively by making a prefix
185 # query of the string preceding the first wildcard char, setting
186 # delimiter=/ (unless the wildcard is **), then filtering the results
187 # by the wildcard at that level. For example given the wildcard:
188 # gs://bucket/abc/d*e/f*.txt
189 # we would:
190 # - get a bucket listing with prefix=abc/d, delimiter=/
191 # - filter each result for those that start with the result + *e
192 # Assuming gs://bucket/abc/dxyze is a result from this iteration, the
193 # next iteration would:
194 # - get a bucket listing with prefix= abc/dxyze, delimiter=/
195 # - filter each result for those that start with the result + f.txt
196 #
197 # Initialize the iteration with bucket name from bucket_uri but
198 # object name from self.wildcard_uri. This is needed to handle cases
199 # where both the bucket and object names contain wildcards.
200 uris_needing_expansion = [
201 bucket_uri.clone_replace_name(self.wildcard_uri.object_name)]
202 while len(uris_needing_expansion) > 0:
203 uri = uris_needing_expansion.pop(0)
204 (prefix, delimiter, prefix_wildcard, suffix) = (
205 self._BuildBucketFilterStrings(uri.object_name))
206 prog = re.compile(fnmatch.translate(prefix_wildcard))
207 # List bucket for objects matching prefix up to delimiter.
208 for key in bucket_uri.get_bucket(
209 validate=False, headers=self.headers).list(
210 prefix=prefix, delimiter=delimiter, headers=self.headers):
211 # Check that the prefix regex matches.
212 # Match rstripped key.name, to correspond with the rstripped
213 # prefix_wildcard from _BuildBucketFilterStrings.
214 if prog.match(key.name.rstrip('/')):
215 if suffix and WILDCARD_REGEX.search(suffix):
216 # There's more wildcard left to expand.
217 uris_needing_expansion.append(
218 uri.clone_replace_name(key.name + suffix))
219 else:
220 # Done expanding.
221 if suffix:
222 expanded_uri = uri.clone_replace_name(key.name + suffix)
223 else:
224 expanded_uri = uri.clone_replace_name(key.name)
225 if isinstance(key, Prefix):
226 yield BucketListingRef(expanded_uri, key=None, prefix=key,
227 headers=self.headers)
228 else:
229 yield BucketListingRef(expanded_uri, key=key, prefix=None,
230 headers=self.headers)
231
232 def _BuildBucketFilterStrings(self, wildcard):
233 """
234 Builds strings needed for querying a bucket and filtering results to
235 implement wildcard object name matching.
236
237 Args:
238 wildcard: The wildcard string to match to objects.
239
240 Returns:
241 (prefix, delimiter, prefix_wildcard, suffix)
242 where:
243 prefix is the prefix to be sent in bucket GET request.
244 delimiter is the delimiter to be sent in bucket GET request.
245 prefix_wildcard is the wildcard to be used to filter bucket GET results.
246 suffix is string to be appended to filtered bucket GET results for next
247 wildcard expansion iteration.
248
249 Raises:
250 AssertionError if wildcard doesn't contain any wildcard chars.
251 """
252 # Generate a request prefix if the object name part of the wildcard starts
253 # with a non-regex string (e.g., that's true for 'gs://bucket/abc*xyz').
254 match = WILDCARD_REGEX.search(wildcard)
255 assert match
256 if match.start() > 0:
257 # Wildcard does not occur at beginning of object name, so construct a
258 # prefix string to send to server.
259 prefix = wildcard[:match.start()]
260 else:
261 prefix = None
262 # Construct a sub-wildcard for the current path component. For
263 # example, while iterating the first prefix match result (with
264 # prefix abc/d), prefix_wildcard will be d*e and suffix would be
265 # f*.txt.
266 wildcard_part = wildcard[match.start():]
267 end = wildcard_part.find('/')
268 if end != -1:
269 wildcard_part = wildcard_part[:end+1]
270 # Remove trailing '/' so we will match gs://bucket/abc* as well as
271 # gs://bucket/abc*/ with the same wildcard regex.
272 prefix_wildcard = ((prefix or '') + wildcard_part).rstrip('/')
273 suffix = wildcard[match.end():]
274 end = suffix.find('/')
275 if end == -1:
276 suffix = ''
277 else:
278 suffix = suffix[end+1:]
279 # To implement recursive wildcarding, if prefix_wildcard suffix starts with
280 # '**' don't send a delimiter, and combine suffix at end of prefix_wildcard.
281 if prefix_wildcard.find('**') != -1:
282 delimiter = None
283 prefix_wildcard = prefix_wildcard + suffix
284 suffix = ''
285 else:
286 delimiter = '/'
287 # The following debug output is useful for tracing how the algorithm
288 # walks through a multi-part wildcard like gs://bucket/abc/d*e/f*.txt
289 if self.debug > 1:
290 sys.stderr.write(
291 'DEBUG: wildcard=%s, prefix=%s, delimiter=%s, '
292 'prefix_wildcard=%s, suffix=%s\n' %
293 (wildcard, prefix, delimiter, prefix_wildcard, suffix))
294 return (prefix, delimiter, prefix_wildcard, suffix)
295
296 def IterKeys(self):
297 """
298 Convenience iterator that runs underlying iterator and returns Key for each
299 iteration.
300
301 Yields:
302 Subclass of boto.s3.key.Key, or empty iterator if no matches.
303
304 Raises:
305 WildcardException: for bucket-only uri.
306 """
307 for bucket_listing_ref in self. __iter__():
308 if bucket_listing_ref.HasKey():
309 yield bucket_listing_ref.GetKey()
310
311 def IterUris(self):
312 """
313 Convenience iterator that runs underlying iterator and returns StorageUri
314 for each iteration.
315
316 Yields:
317 StorageUri, or empty iterator if no matches.
318 """
319 for bucket_listing_ref in self. __iter__():
320 yield bucket_listing_ref.GetUri()
321
322 def IterUrisForKeys(self):
323 """
324 Convenience iterator that runs underlying iterator and returns the
325 StorageUri for each iterated BucketListingRef that has a Key.
326
327 Yields:
328 StorageUri, or empty iterator if no matches.
329 """
330 for bucket_listing_ref in self. __iter__():
331 if bucket_listing_ref.HasKey():
332 yield bucket_listing_ref.GetUri()
333
334
335 class FileWildcardIterator(WildcardIterator):
336 """WildcardIterator subclass for files and directories.
337
338 If you use recursive wildcards ('**') only a single such wildcard is
339 supported. For example you could use the wildcard '**/*.txt' to list all .txt
340 files in any subdirectory of the current directory, but you couldn't use a
341 wildcard like '**/abc/**/*.txt' (which would, if supported, let you find .txt
342 files in any subdirectory named 'abc').
343 """
344
345 def __init__(self, wildcard_uri, headers=None, debug=0):
346 """
347 Instantiate an iterator over BucketListingRefs matching given wildcard URI.
348
349 Args:
350 wildcard_uri: StorageUri that contains the wildcard to iterate.
351 headers: Dictionary containing optional HTTP headers to pass to boto.
352 debug: Debug level to pass in to boto connection (range 0..3).
353 """
354 self.wildcard_uri = wildcard_uri
355 self.headers = headers
356 self.debug = debug
357
358 def __iter__(self):
359 wildcard = self.wildcard_uri.object_name
360 match = re.search('\*\*', wildcard)
361 if match:
362 # Recursive wildcarding request ('.../**/...').
363 # Example input: wildcard = '/tmp/tmp2pQJAX/**/*'
364 base_dir = wildcard[:match.start()-1]
365 remaining_wildcard = wildcard[match.start()+2:]
366 # At this point for the above example base_dir = '/tmp/tmp2pQJAX' and
367 # remaining_wildcard = '/*'
368 if remaining_wildcard.startswith('*'):
369 raise WildcardException('Invalid wildcard with more than 2 consecutive '
370 '*s (%s)' % wildcard)
371 # If there was no remaining wildcard past the recursive wildcard,
372 # treat it as if it were a '*'. For example, file://tmp/** is equivalent
373 # to file://tmp/**/*
374 if not remaining_wildcard:
375 remaining_wildcard = '*'
376 # Skip slash(es).
377 remaining_wildcard = remaining_wildcard.lstrip('/')
378 filepaths = []
379 for dirpath, unused_dirnames, filenames in os.walk(base_dir):
380 filepaths.extend(
381 os.path.join(dirpath, f) for f in fnmatch.filter(filenames,
382 remaining_wildcard)
383 )
384 else:
385 # Not a recursive wildcarding request.
386 filepaths = glob.glob(wildcard)
387 for filepath in filepaths:
388 expanded_uri = self.wildcard_uri.clone_replace_name(filepath)
389 yield BucketListingRef(expanded_uri)
390
391 def IterKeys(self):
392 """
393 Placeholder to allow polymorphic use of WildcardIterator.
394
395 Raises:
396 WildcardException: in all cases.
397 """
398 raise WildcardException(
399 'Iterating over Keys not possible for file wildcards')
400
401 def IterUris(self):
402 """
403 Convenience iterator that runs underlying iterator and returns StorageUri
404 for each iteration.
405
406 Yields:
407 StorageUri, or empty iterator if no matches.
408 """
409 for bucket_listing_ref in self. __iter__():
410 yield bucket_listing_ref.GetUri()
411
412
413 class WildcardException(StandardError):
414 """Exception thrown for invalid wildcard URIs."""
415
416 def __init__(self, reason):
417 StandardError.__init__(self)
418 self.reason = reason
419
420 def __repr__(self):
421 return 'WildcardException: %s' % self.reason
422
423 def __str__(self):
424 return 'WildcardException: %s' % self.reason
425
426
427 def wildcard_iterator(uri_or_str, proj_id_handler,
428 bucket_storage_uri_class=BucketStorageUri,
429 headers=None, debug=0):
430 """Instantiate a WildCardIterator for the given StorageUri.
431
432 Args:
433 uri_or_str: StorageUri or URI string naming wildcard objects to iterate.
434 proj_id_handler: ProjectIdHandler to use for current command.
435 bucket_storage_uri_class: BucketStorageUri interface.
436 Settable for testing/mocking.
437 headers: Dictionary containing optional HTTP headers to pass to boto.
438 debug: Debug level to pass in to boto connection (range 0..3).
439
440 Returns:
441 A WildcardIterator that handles the requested iteration.
442 """
443
444 if isinstance(uri_or_str, basestring):
445 # Disable enforce_bucket_naming, to allow bucket names containing
446 # wildcard chars.
447 uri = boto.storage_uri(
448 uri_or_str, debug=debug, validate=False,
449 bucket_storage_uri_class=bucket_storage_uri_class,
450 suppress_consec_slashes=False)
451 else:
452 uri = uri_or_str
453
454 if uri.is_cloud_uri():
455 return CloudWildcardIterator(
456 uri, proj_id_handler,
457 bucket_storage_uri_class=bucket_storage_uri_class, headers=headers,
458 debug=debug)
459 elif uri.is_file_uri():
460 return FileWildcardIterator(uri, headers=headers, debug=debug)
461 else:
462 raise WildcardException('Unexpected type of StorageUri (%s)' % uri)
463
464
465 def ContainsWildcard(uri_or_str):
466 """Checks whether uri_or_str contains a wildcard.
467
468 Args:
469 uri_or_str: StorageUri or URI string to check.
470
471 Returns:
472 bool indicator.
473 """
474 if isinstance(uri_or_str, basestring):
475 return bool(WILDCARD_REGEX.search(uri_or_str))
476 else:
477 return bool(WILDCARD_REGEX.search(uri_or_str.uri))
OLDNEW
« no previous file with comments | « third_party/gsutil/gslib/util.py ('k') | third_party/gsutil/gsutil » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698