| OLD | NEW |
| (Empty) | |
| 1 # Copyright 2012 Google Inc. |
| 2 # |
| 3 # Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 # you may not use this file except in compliance with the License. |
| 5 # You may obtain a copy of the License at |
| 6 # |
| 7 # http://www.apache.org/licenses/LICENSE-2.0 |
| 8 # |
| 9 # Unless required by applicable law or agreed to in writing, software |
| 10 # distributed under the License is distributed on an "AS IS" BASIS, |
| 11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 # See the License for the specific language governing permissions and |
| 13 # limitations under the License. |
| 14 |
| 15 import gslib |
| 16 import itertools |
| 17 import wildcard_iterator |
| 18 |
| 19 from gslib.storage_uri_builder import StorageUriBuilder |
| 20 from wildcard_iterator import ContainsWildcard |
| 21 from bucket_listing_ref import BucketListingRef |
| 22 |
| 23 """ |
| 24 Name expansion support for the various ways gsutil lets users refer to |
| 25 collections of data (via explicit wildcarding as well as directory, |
| 26 bucket, and bucket subdir implicit wildcarding). This class encapsulates |
| 27 the various rules for determining how these expansions are done. |
| 28 """ |
| 29 |
| 30 |
| 31 class NameExpansionResult(object): |
| 32 """ |
| 33 Holds results of calls to NameExpansionHandler.ExpandWildcardsAndContainers(). |
| 34 """ |
| 35 # Currently we build a dict (self._expansion_map) to hold the expansion |
| 36 # results instead of using a generator to iterate incrementally |
| 37 # because the caller needs to know the count before iterating and |
| 38 # performing copy operations (in order to determine if this is a |
| 39 # multi-source copy request). That limits the scalability of wildcard |
| 40 # iteration, since the entire expansion needs to fit in memory (see |
| 41 # http://code.google.com/p/gsutil/issues/detail?id=80). |
| 42 # TODO: Rework NameExpansionResult to save |
| 43 # {StorageUri: generator of BucketListingRefs to which it expands} |
| 44 # and change the accessor functions to determine if expansion has |
| 45 # length > 1 without first |
| 46 # NameExpansionResult.IterExpandedBucketListingRefsFor() and |
| 47 # NameExpansionResult.SrcUriExpandsToMultipleSources() to work without ever |
| 48 # materializing the list. |
| 49 |
| 50 def __init__(self): |
| 51 # dict {StorageUri: [BucketListingRefs to which it expands]} |
| 52 # Note: in the future we'll change RHS to be an iterator from the |
| 53 # underlying generator supported by WildcardIterator, for scalabilty. |
| 54 self._expansion_map = {} |
| 55 # dict {StorageUri: bool indicator of whether src_uri names a container} |
| 56 # where names_container is true if URI names a directory, bucket, or |
| 57 # bucket subdir (vs how StorageUri.names_container() doesn't handle |
| 58 # latter case). |
| 59 self._names_container_map = {} |
| 60 |
| 61 def __repr__(self): |
| 62 return self._expansion_map.__repr__() |
| 63 |
| 64 def _AddExpansion(self, src_uri, names_container, |
| 65 expanded_bucket_listing_refs): |
| 66 """ |
| 67 Args: |
| 68 src_uri: StorageUri. |
| 69 names_container: bool indicator whether src_uri names a container. |
| 70 expanded_bucket_listing_refs: [BucketListingRef] to which src_uri expands. |
| 71 """ |
| 72 self._expansion_map[src_uri] = expanded_bucket_listing_refs |
| 73 self._names_container_map[src_uri] = names_container |
| 74 |
| 75 def IsEmpty(self): |
| 76 """Returns True if name expansion yielded no matches.""" |
| 77 for v in self._expansion_map.values(): |
| 78 if v: |
| 79 return False |
| 80 return True |
| 81 |
| 82 def NamesContainer(self, src_uri): |
| 83 """Returns bool indicator of whether src_uri names a directory, bucket, or |
| 84 bucket subdir. |
| 85 """ |
| 86 return self._names_container_map[src_uri] |
| 87 |
| 88 def GetSrcUris(self): |
| 89 """Returns the list of src_uri's for which name expansion was requested.""" |
| 90 return self._expansion_map.keys() |
| 91 |
| 92 # Note: We return iterators from the following functinos |
| 93 # instead of the underlying lists so we can later replace |
| 94 # this representation with a generator implementation to fix |
| 95 # http://code.google.com/p/gsutil/issues/detail?id=80. |
| 96 |
| 97 def IterExpandedBucketListingRefsFor(self, src_uri): |
| 98 """ |
| 99 Returns an iterator of BucketListingRefs to which the given src_uri |
| 100 expanded. |
| 101 """ |
| 102 return iter(self._expansion_map[src_uri]) |
| 103 |
| 104 def IterExpandedBucketListingRefs(self): |
| 105 """ |
| 106 Returns an iterator of all BucketListingRefs (across all src_uris that were |
| 107 expanded) from this NameExpansionResult. |
| 108 """ |
| 109 #result = [] |
| 110 #for exp_list in self._expansion_map.values(): |
| 111 # result.extend(exp_list) |
| 112 # return iter(result) |
| 113 list_of_iters = [] |
| 114 for src_uri in self._expansion_map: |
| 115 list_of_iters.extend(self._expansion_map[src_uri]) |
| 116 return itertools.chain(list_of_iters) |
| 117 |
| 118 def __iter__(self): |
| 119 return self.IterExpandedBucketListingRefs() |
| 120 |
| 121 def IterExpandedUris(self): |
| 122 """ |
| 123 Returns an iterator of all StorageUris (across all src_uris that were |
| 124 expanded) from this NameExpansionResult. |
| 125 """ |
| 126 result = [] |
| 127 for bucket_listing_ref in self.IterExpandedBucketListingRefs(): |
| 128 result.append(bucket_listing_ref.GetUri()) |
| 129 return iter(result) |
| 130 |
| 131 def IterExpandedUriStrings(self): |
| 132 """ |
| 133 Returns an iterator of all URI strings (across all src_uris that were |
| 134 expanded) from this NameExpansionResult. |
| 135 """ |
| 136 result = [] |
| 137 for bucket_listing_ref in self.IterExpandedBucketListingRefs(): |
| 138 result.append(bucket_listing_ref.GetUriString()) |
| 139 return iter(result) |
| 140 |
| 141 def IterExpandedKeys(self): |
| 142 """ |
| 143 Returns an iterator of all Keys (across all src_uris that were expanded) |
| 144 from this NameExpansionResult. |
| 145 """ |
| 146 result = [] |
| 147 for bucket_listing_ref in self.IterExpandedBucketListingRefs(): |
| 148 result.append(bucket_listing_ref.GetKey()) |
| 149 return iter(result) |
| 150 |
| 151 def IsMultiSrcRequest(self): |
| 152 """Returns True if this name expansion resulted in more than 1 URI.""" |
| 153 if len(self._expansion_map) == 0: |
| 154 return False |
| 155 return (len(self._expansion_map) > 1 |
| 156 or len(self._expansion_map.values()[0]) > 1) |
| 157 |
| 158 def SrcUriExpandsToMultipleSources(self, src_uri): |
| 159 """ |
| 160 Checks that src_uri names a singleton (file or object) after |
| 161 dir/wildcard expansion. The decision is more nuanced than simply |
| 162 src_uri.names_singleton()) because of the possibility that an object path |
| 163 might name a bucket "sub-directory", which in turn depends on whether |
| 164 src_uri expanded to multiple URIs. For example, when running the command: |
| 165 gsutil cp -R gs://bucket/abc ./dir |
| 166 gs://bucket/abc would be an object if nothing matches gs://bucket/abc/*; |
| 167 but would be a bucket subdir otherwise. |
| 168 |
| 169 Args: |
| 170 src_uri: StorageUri to check. |
| 171 |
| 172 Returns: |
| 173 bool indicator. |
| 174 """ |
| 175 return len(self._expansion_map[src_uri]) > 1 |
| 176 |
| 177 |
| 178 class NameExpansionHandler(object): |
| 179 |
| 180 def __init__(self, command_name, proj_id_handler, headers, debug, |
| 181 bucket_storage_uri_class): |
| 182 """ |
| 183 Args: |
| 184 command_name: name of command being run. |
| 185 proj_id_handler: ProjectIdHandler to use for current command. |
| 186 headers: Dictionary containing optional HTTP headers to pass to boto. |
| 187 debug: Debug level to pass in to boto connection (range 0..3). |
| 188 bucket_storage_uri_class: Class to instantiate for cloud StorageUris. |
| 189 Settable for testing/mocking. |
| 190 """ |
| 191 self.command_name = command_name |
| 192 self.proj_id_handler = proj_id_handler |
| 193 self.headers = headers |
| 194 self.debug = debug |
| 195 self.bucket_storage_uri_class = bucket_storage_uri_class |
| 196 self.suri_builder = StorageUriBuilder(debug, bucket_storage_uri_class) |
| 197 |
| 198 # Map holding wildcard strings to use for flat vs subdir-by-subdir listings. |
| 199 # (A flat listing means show all objects expanded all the way down.) |
| 200 self._flatness_wildcard = {True: '**', False: '*'} |
| 201 |
| 202 def WildcardIterator(self, uri_or_str): |
| 203 """ |
| 204 Helper to instantiate gslib.WildcardIterator. Args are same as |
| 205 gslib.WildcardIterator interface, but this method fills in most of the |
| 206 values from class state. |
| 207 |
| 208 Args: |
| 209 uri_or_str: StorageUri or URI string naming wildcard objects to iterate. |
| 210 """ |
| 211 return wildcard_iterator.wildcard_iterator( |
| 212 uri_or_str, self.proj_id_handler, |
| 213 bucket_storage_uri_class=self.bucket_storage_uri_class, |
| 214 headers=self.headers, debug=self.debug) |
| 215 |
| 216 def ExpandWildcardsAndContainers(self, uri_strs, recursion_requested, |
| 217 flat=True): |
| 218 """ |
| 219 Expands wildcards, object-less bucket names, subdir bucket names, and |
| 220 directory names, producing a flat listing of all the matching objects/files. |
| 221 |
| 222 Args: |
| 223 uri_strs: List of URI strings needing expansion. |
| 224 recursion_requested: True if -R specified on command-line. |
| 225 flat: Bool indicating whether bucket listings should be flattened, i.e., |
| 226 so the mapped-to results contain objects spanning subdirectories. |
| 227 |
| 228 Returns: |
| 229 gslib.name_expansion.NameExpansionResult. |
| 230 |
| 231 Raises: |
| 232 CommandException: if errors encountered. |
| 233 |
| 234 Examples with flat=True: |
| 235 - Calling with one of the uri_strs being 'gs://bucket' will enumerate all |
| 236 top-level objects, as will 'gs://bucket/' and 'gs://bucket/*'. |
| 237 - 'gs://bucket/**' will enumerate all objects in the bucket. |
| 238 - 'gs://bucket/abc' will enumerate all next-level objects under directory |
| 239 abc (i.e., not including subdirectories of abc) if gs://bucket/abc/* |
| 240 matches any objects; otherwise it will enumerate the single name |
| 241 gs://bucket/abc |
| 242 - 'gs://bucket/abc/**' will enumerate all objects under abc or any of its |
| 243 subdirectories. |
| 244 - 'file:///tmp' will enumerate all files under /tmp, as will |
| 245 'file:///tmp/*' |
| 246 - 'file:///tmp/**' will enumerate all files under /tmp or any of its |
| 247 subdirectories. |
| 248 |
| 249 Example if flat=False: calling with gs://bucket/abc/* lists matching objects |
| 250 or subdirs, but not sub-subdirs or objects beneath subdirs. |
| 251 |
| 252 Note: In step-by-step comments below we give examples assuming there's a |
| 253 gs://bucket with object paths: |
| 254 abcd/o1.txt |
| 255 abcd/o2.txt |
| 256 xyz/o1.txt |
| 257 xyz/o2.txt |
| 258 and a directory file://dir with file paths: |
| 259 dir/a.txt |
| 260 dir/b.txt |
| 261 dir/c/ |
| 262 """ |
| 263 result = NameExpansionResult() |
| 264 for uri_str in uri_strs: |
| 265 |
| 266 # Step 1: Expand any explicitly specified wildcards. |
| 267 # Starting with gs://buck*/abc* this step would expand to gs://bucket/abcd |
| 268 if ContainsWildcard(uri_str): |
| 269 post_step1_bucket_listing_refs = list(self.WildcardIterator(uri_str)) |
| 270 else: |
| 271 post_step1_bucket_listing_refs = [ |
| 272 BucketListingRef(self.suri_builder.StorageUri(uri_str))] |
| 273 |
| 274 # Step 2: Expand subdirs. |
| 275 # Starting with gs://bucket/abcd this step would expand to: |
| 276 # [abcd/o1.txt, abcd/o2.txt]. |
| 277 uri_names_container = False |
| 278 if flat: |
| 279 if recursion_requested: |
| 280 post_step2_bucket_listing_refs = [] |
| 281 for bucket_listing_ref in post_step1_bucket_listing_refs: |
| 282 (uri_names_container, bucket_listing_refs) = ( |
| 283 self._DoImplicitBucketSubdirExpansionIfApplicable( |
| 284 bucket_listing_ref.GetUri(), flat)) |
| 285 post_step2_bucket_listing_refs.extend(bucket_listing_refs) |
| 286 else: |
| 287 uri_names_container = False |
| 288 post_step2_bucket_listing_refs = post_step1_bucket_listing_refs |
| 289 else: |
| 290 uri_names_container = False |
| 291 post_step2_bucket_listing_refs = post_step1_bucket_listing_refs |
| 292 |
| 293 # Step 3. Expand directories and buckets. |
| 294 # Starting with gs://bucket this step would expand to: |
| 295 # [abcd/o1.txt, abcd/o2.txt, xyz/o1.txt, xyz/o2.txt] |
| 296 # Starting with file://dir this step would expand to: |
| 297 # [dir/a.txt, dir/b.txt, dir/c/] |
| 298 exp_src_bucket_listing_refs = [] |
| 299 wc = self._flatness_wildcard[flat] |
| 300 for bucket_listing_ref in post_step2_bucket_listing_refs: |
| 301 if (not bucket_listing_ref.GetUri().names_container() |
| 302 and (flat or not bucket_listing_ref.HasPrefix())): |
| 303 exp_src_bucket_listing_refs.append(bucket_listing_ref) |
| 304 continue |
| 305 if not recursion_requested: |
| 306 if bucket_listing_ref.GetUri().is_file_uri(): |
| 307 desc = 'directory' |
| 308 else: |
| 309 desc = 'bucket' |
| 310 print 'Omitting %s "%s". (Did you mean to do %s -R?)' % ( |
| 311 desc, bucket_listing_ref.GetUri(), self.command_name) |
| 312 continue |
| 313 uri_names_container = True |
| 314 if bucket_listing_ref.GetUri().is_file_uri(): |
| 315 # Convert dir to implicit recursive wildcard. |
| 316 uri_to_iter = '%s/%s' % (bucket_listing_ref.GetUriString(), wc) |
| 317 else: |
| 318 # Convert bucket to implicit recursive wildcard. |
| 319 uri_to_iter = bucket_listing_ref.GetUri().clone_replace_name(wc) |
| 320 wildcard_result = list(self.WildcardIterator(uri_to_iter)) |
| 321 if len(wildcard_result) > 0: |
| 322 exp_src_bucket_listing_refs.extend(wildcard_result) |
| 323 |
| 324 result._AddExpansion(self.suri_builder.StorageUri(uri_str), |
| 325 uri_names_container, |
| 326 exp_src_bucket_listing_refs) |
| 327 |
| 328 return result |
| 329 |
| 330 def _DoImplicitBucketSubdirExpansionIfApplicable(self, uri, flat): |
| 331 """ |
| 332 Checks whether uri could be an implicit bucket subdir, and expands if so; |
| 333 else returns list containing uri. For example gs://abc would be an implicit |
| 334 bucket subdir if the -R option was specified and gs://abc/* matches |
| 335 anything. |
| 336 Can only be called for -R (recursion requested). |
| 337 |
| 338 Args: |
| 339 uri: StorageUri. |
| 340 flat: bool indicating whether bucket listings should be flattened, i.e., |
| 341 so the mapped-to results contain objects spanning subdirectories. |
| 342 |
| 343 Returns: |
| 344 tuple (names_container, [BucketListingRefs to which uri expanded]) |
| 345 where names_container is true if URI names a directory, bucket, |
| 346 or bucket subdir (vs how StorageUri.names_container() doesn't |
| 347 handle latter case). |
| 348 """ |
| 349 names_container = False |
| 350 result_list = [] |
| 351 if uri.names_object(): |
| 352 # URI could be a bucket subdir. |
| 353 implicit_subdir_matches = list(self.WildcardIterator( |
| 354 self.suri_builder.StorageUri('%s/%s' % (uri.uri.rstrip('/'), |
| 355 self._flatness_wildcard[flat])))) |
| 356 if len(implicit_subdir_matches) > 0: |
| 357 names_container = True |
| 358 result_list.extend(implicit_subdir_matches) |
| 359 else: |
| 360 result_list.append(BucketListingRef(uri)) |
| 361 else: |
| 362 result_list.append(BucketListingRef(uri)) |
| 363 return (names_container, result_list) |
| 364 |
| 365 def StorageUri(self, uri_str): |
| 366 """ |
| 367 Helper to instantiate boto.StorageUri with gsutil default flag values. |
| 368 Uses self.bucket_storage_uri_class to support mocking/testing. |
| 369 (Identical to the same-named function in command.py; that and this |
| 370 copy make it convenient to call StorageUri() with a single argument, |
| 371 from the respective classes.) |
| 372 |
| 373 Args: |
| 374 uri_str: StorageUri naming bucket + optional object. |
| 375 |
| 376 Returns: |
| 377 boto.StorageUri for given uri_str. |
| 378 |
| 379 Raises: |
| 380 InvalidUriError: if uri_str not valid. |
| 381 """ |
| 382 return gslib.util.StorageUri(uri_str, self.bucket_storage_uri_class, |
| 383 self.debug) |
| OLD | NEW |