| OLD | NEW |
| (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)) |
| OLD | NEW |