| OLD | NEW |
| 1 # -*- coding: utf-8 -*- | 1 # -*- coding: utf-8 -*- |
| 2 # Copyright 2011 Google Inc. All Rights Reserved. | 2 # Copyright 2011 Google Inc. All Rights Reserved. |
| 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 Unix-like rm command for cloud storage providers.""" | 15 """Implementation of Unix-like rm command for cloud storage providers.""" |
| 16 | 16 |
| 17 from __future__ import absolute_import | 17 from __future__ import absolute_import |
| 18 | 18 |
| 19 from gslib.cloud_api import BucketNotFoundException |
| 19 from gslib.cloud_api import NotEmptyException | 20 from gslib.cloud_api import NotEmptyException |
| 21 from gslib.cloud_api import NotFoundException |
| 20 from gslib.cloud_api import ServiceException | 22 from gslib.cloud_api import ServiceException |
| 21 from gslib.command import Command | 23 from gslib.command import Command |
| 22 from gslib.command import GetFailureCount | 24 from gslib.command import GetFailureCount |
| 23 from gslib.command import ResetFailureCount | 25 from gslib.command import ResetFailureCount |
| 24 from gslib.command_argument import CommandArgument | 26 from gslib.command_argument import CommandArgument |
| 25 from gslib.cs_api_map import ApiSelector | 27 from gslib.cs_api_map import ApiSelector |
| 26 from gslib.exception import CommandException | 28 from gslib.exception import CommandException |
| 27 from gslib.name_expansion import NameExpansionIterator | 29 from gslib.name_expansion import NameExpansionIterator |
| 28 from gslib.storage_url import StorageUrlFromString | 30 from gslib.storage_url import StorageUrlFromString |
| 29 from gslib.translation_helper import PreconditionsFromHeaders | 31 from gslib.translation_helper import PreconditionsFromHeaders |
| (...skipping 102 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 132 flag implies the -a flag and will delete all object versions. | 134 flag implies the -a flag and will delete all object versions. |
| 133 | 135 |
| 134 -a Delete all versions of an object. | 136 -a Delete all versions of an object. |
| 135 """) | 137 """) |
| 136 | 138 |
| 137 | 139 |
| 138 def _RemoveExceptionHandler(cls, e): | 140 def _RemoveExceptionHandler(cls, e): |
| 139 """Simple exception handler to allow post-completion status.""" | 141 """Simple exception handler to allow post-completion status.""" |
| 140 if not cls.continue_on_error: | 142 if not cls.continue_on_error: |
| 141 cls.logger.error(str(e)) | 143 cls.logger.error(str(e)) |
| 142 cls.everything_removed_okay = False | 144 # TODO: Use shared state to track missing bucket names when we get a |
| 145 # BucketNotFoundException. Then improve bucket removal logic and exception |
| 146 # messages. |
| 147 if isinstance(e, BucketNotFoundException): |
| 148 cls.bucket_not_found_count += 1 |
| 149 cls.logger.error(str(e)) |
| 150 else: |
| 151 cls.op_failure_count += 1 |
| 143 | 152 |
| 144 | 153 |
| 145 # pylint: disable=unused-argument | 154 # pylint: disable=unused-argument |
| 146 def _RemoveFoldersExceptionHandler(cls, e): | 155 def _RemoveFoldersExceptionHandler(cls, e): |
| 147 """When removing folders, we don't mind if none exist.""" | 156 """When removing folders, we don't mind if none exist.""" |
| 148 if (isinstance(e, CommandException.__class__) and | 157 if (isinstance(e, CommandException.__class__) and |
| 149 'No URLs matched' in e.message): | 158 'No URLs matched' in e.message) or isinstance(e, NotFoundException): |
| 150 pass | 159 pass |
| 151 else: | 160 else: |
| 152 raise e | 161 raise e |
| 153 | 162 |
| 154 | 163 |
| 155 def _RemoveFuncWrapper(cls, name_expansion_result, thread_state=None): | 164 def _RemoveFuncWrapper(cls, name_expansion_result, thread_state=None): |
| 156 cls.RemoveFunc(name_expansion_result, thread_state=thread_state) | 165 cls.RemoveFunc(name_expansion_result, thread_state=thread_state) |
| 157 | 166 |
| 158 | 167 |
| 159 class RmCommand(Command): | 168 class RmCommand(Command): |
| (...skipping 23 matching lines...) Expand all Loading... |
| 183 help_type='command_help', | 192 help_type='command_help', |
| 184 help_one_line_summary='Remove objects', | 193 help_one_line_summary='Remove objects', |
| 185 help_text=_DETAILED_HELP_TEXT, | 194 help_text=_DETAILED_HELP_TEXT, |
| 186 subcommand_help_text={}, | 195 subcommand_help_text={}, |
| 187 ) | 196 ) |
| 188 | 197 |
| 189 def RunCommand(self): | 198 def RunCommand(self): |
| 190 """Command entry point for the rm command.""" | 199 """Command entry point for the rm command.""" |
| 191 # self.recursion_requested is initialized in command.py (so it can be | 200 # self.recursion_requested is initialized in command.py (so it can be |
| 192 # checked in parent class for all commands). | 201 # checked in parent class for all commands). |
| 193 self.continue_on_error = False | 202 self.continue_on_error = self.parallel_operations |
| 194 self.read_args_from_stdin = False | 203 self.read_args_from_stdin = False |
| 195 self.all_versions = False | 204 self.all_versions = False |
| 196 if self.sub_opts: | 205 if self.sub_opts: |
| 197 for o, unused_a in self.sub_opts: | 206 for o, unused_a in self.sub_opts: |
| 198 if o == '-a': | 207 if o == '-a': |
| 199 self.all_versions = True | 208 self.all_versions = True |
| 200 elif o == '-f': | 209 elif o == '-f': |
| 201 self.continue_on_error = True | 210 self.continue_on_error = True |
| 202 elif o == '-I': | 211 elif o == '-I': |
| 203 self.read_args_from_stdin = True | 212 self.read_args_from_stdin = True |
| 204 elif o == '-r' or o == '-R': | 213 elif o == '-r' or o == '-R': |
| 205 self.recursion_requested = True | 214 self.recursion_requested = True |
| 206 self.all_versions = True | 215 self.all_versions = True |
| 207 | 216 |
| 208 if self.read_args_from_stdin: | 217 if self.read_args_from_stdin: |
| 209 if self.args: | 218 if self.args: |
| 210 raise CommandException('No arguments allowed with the -I flag.') | 219 raise CommandException('No arguments allowed with the -I flag.') |
| 211 url_strs = StdinIterator() | 220 url_strs = StdinIterator() |
| 212 else: | 221 else: |
| 213 if not self.args: | 222 if not self.args: |
| 214 raise CommandException('The rm command (without -I) expects at ' | 223 raise CommandException('The rm command (without -I) expects at ' |
| 215 'least one URL.') | 224 'least one URL.') |
| 216 url_strs = self.args | 225 url_strs = self.args |
| 217 | 226 |
| 227 # Tracks if any deletes failed. |
| 228 self.op_failure_count = 0 |
| 229 |
| 230 # Tracks if any buckets were missing. |
| 231 self.bucket_not_found_count = 0 |
| 232 |
| 218 bucket_urls_to_delete = [] | 233 bucket_urls_to_delete = [] |
| 219 bucket_strings_to_delete = [] | 234 bucket_strings_to_delete = [] |
| 220 if self.recursion_requested: | 235 if self.recursion_requested: |
| 221 bucket_fields = ['id'] | 236 bucket_fields = ['id'] |
| 222 for url_str in url_strs: | 237 for url_str in url_strs: |
| 223 url = StorageUrlFromString(url_str) | 238 url = StorageUrlFromString(url_str) |
| 224 if url.IsBucket() or url.IsProvider(): | 239 if url.IsBucket() or url.IsProvider(): |
| 225 for blr in self.WildcardIterator(url_str).IterBuckets( | 240 for blr in self.WildcardIterator(url_str).IterBuckets( |
| 226 bucket_fields=bucket_fields): | 241 bucket_fields=bucket_fields): |
| 227 bucket_urls_to_delete.append(blr.storage_url) | 242 bucket_urls_to_delete.append(blr.storage_url) |
| 228 bucket_strings_to_delete.append(url_str) | 243 bucket_strings_to_delete.append(url_str) |
| 229 | 244 |
| 230 self.preconditions = PreconditionsFromHeaders(self.headers or {}) | 245 self.preconditions = PreconditionsFromHeaders(self.headers or {}) |
| 231 | 246 |
| 232 # Used to track if any files failed to be removed. | |
| 233 self.everything_removed_okay = True | |
| 234 | |
| 235 try: | 247 try: |
| 236 # Expand wildcards, dirs, buckets, and bucket subdirs in URLs. | 248 # Expand wildcards, dirs, buckets, and bucket subdirs in URLs. |
| 237 name_expansion_iterator = NameExpansionIterator( | 249 name_expansion_iterator = NameExpansionIterator( |
| 238 self.command_name, self.debug, self.logger, self.gsutil_api, | 250 self.command_name, self.debug, self.logger, self.gsutil_api, |
| 239 url_strs, self.recursion_requested, project_id=self.project_id, | 251 url_strs, self.recursion_requested, project_id=self.project_id, |
| 240 all_versions=self.all_versions, | 252 all_versions=self.all_versions, |
| 241 continue_on_error=self.continue_on_error or self.parallel_operations) | 253 continue_on_error=self.continue_on_error or self.parallel_operations) |
| 242 | 254 |
| 243 # Perform remove requests in parallel (-m) mode, if requested, using | 255 # Perform remove requests in parallel (-m) mode, if requested, using |
| 244 # configured number of parallel processes and threads. Otherwise, | 256 # configured number of parallel processes and threads. Otherwise, |
| 245 # perform requests with sequential function calls in current process. | 257 # perform requests with sequential function calls in current process. |
| 246 self.Apply(_RemoveFuncWrapper, name_expansion_iterator, | 258 self.Apply(_RemoveFuncWrapper, name_expansion_iterator, |
| 247 _RemoveExceptionHandler, | 259 _RemoveExceptionHandler, |
| 248 fail_on_error=(not self.continue_on_error)) | 260 fail_on_error=(not self.continue_on_error), |
| 261 shared_attrs=['op_failure_count', 'bucket_not_found_count']) |
| 249 | 262 |
| 250 # Assuming the bucket has versioning enabled, url's that don't map to | 263 # Assuming the bucket has versioning enabled, url's that don't map to |
| 251 # objects should throw an error even with all_versions, since the prior | 264 # objects should throw an error even with all_versions, since the prior |
| 252 # round of deletes only sends objects to a history table. | 265 # round of deletes only sends objects to a history table. |
| 253 # This assumption that rm -a is only called for versioned buckets should be | 266 # This assumption that rm -a is only called for versioned buckets should be |
| 254 # corrected, but the fix is non-trivial. | 267 # corrected, but the fix is non-trivial. |
| 255 except CommandException as e: | 268 except CommandException as e: |
| 256 # Don't raise if there are buckets to delete -- it's valid to say: | 269 # Don't raise if there are buckets to delete -- it's valid to say: |
| 257 # gsutil rm -r gs://some_bucket | 270 # gsutil rm -r gs://some_bucket |
| 258 # if the bucket is empty. | 271 # if the bucket is empty. |
| 259 if not bucket_urls_to_delete and not self.continue_on_error: | 272 if not bucket_urls_to_delete and not self.continue_on_error: |
| 260 raise | 273 raise |
| 261 # Reset the failure count if we failed due to an empty bucket that we're | 274 # Reset the failure count if we failed due to an empty bucket that we're |
| 262 # going to delete. | 275 # going to delete. |
| 263 msg = 'No URLs matched: ' | 276 msg = 'No URLs matched: ' |
| 264 if msg in str(e): | 277 if msg in str(e): |
| 265 parts = str(e).split(msg) | 278 parts = str(e).split(msg) |
| 266 if len(parts) == 2 and parts[1] in bucket_strings_to_delete: | 279 if len(parts) == 2 and parts[1] in bucket_strings_to_delete: |
| 267 ResetFailureCount() | 280 ResetFailureCount() |
| 281 else: |
| 282 raise |
| 268 except ServiceException, e: | 283 except ServiceException, e: |
| 269 if not self.continue_on_error: | 284 if not self.continue_on_error: |
| 270 raise | 285 raise |
| 271 | 286 |
| 272 if not self.everything_removed_okay and not self.continue_on_error: | 287 if self.bucket_not_found_count: |
| 288 raise CommandException('Encountered non-existent bucket during listing') |
| 289 |
| 290 if self.op_failure_count and not self.continue_on_error: |
| 273 raise CommandException('Some files could not be removed.') | 291 raise CommandException('Some files could not be removed.') |
| 274 | 292 |
| 275 # If this was a gsutil rm -r command covering any bucket subdirs, | 293 # If this was a gsutil rm -r command covering any bucket subdirs, |
| 276 # remove any dir_$folder$ objects (which are created by various web UI | 294 # remove any dir_$folder$ objects (which are created by various web UI |
| 277 # tools to simulate folders). | 295 # tools to simulate folders). |
| 278 if self.recursion_requested: | 296 if self.recursion_requested: |
| 279 had_previous_failures = GetFailureCount() > 0 | 297 had_previous_failures = GetFailureCount() > 0 |
| 280 folder_object_wildcards = [] | 298 folder_object_wildcards = [] |
| 281 for url_str in url_strs: | 299 for url_str in url_strs: |
| 282 url = StorageUrlFromString(url_str) | 300 url = StorageUrlFromString(url_str) |
| (...skipping 22 matching lines...) Expand all Loading... |
| 305 # Now that all data has been deleted, delete any bucket URLs. | 323 # Now that all data has been deleted, delete any bucket URLs. |
| 306 for url in bucket_urls_to_delete: | 324 for url in bucket_urls_to_delete: |
| 307 self.logger.info('Removing %s...', url) | 325 self.logger.info('Removing %s...', url) |
| 308 | 326 |
| 309 @Retry(NotEmptyException, tries=3, timeout_secs=1) | 327 @Retry(NotEmptyException, tries=3, timeout_secs=1) |
| 310 def BucketDeleteWithRetry(): | 328 def BucketDeleteWithRetry(): |
| 311 self.gsutil_api.DeleteBucket(url.bucket_name, provider=url.scheme) | 329 self.gsutil_api.DeleteBucket(url.bucket_name, provider=url.scheme) |
| 312 | 330 |
| 313 BucketDeleteWithRetry() | 331 BucketDeleteWithRetry() |
| 314 | 332 |
| 333 if self.op_failure_count: |
| 334 plural_str = 's' if self.op_failure_count else '' |
| 335 raise CommandException('%d file%s/object%s could not be removed.' % ( |
| 336 self.op_failure_count, plural_str, plural_str)) |
| 337 |
| 315 return 0 | 338 return 0 |
| 316 | 339 |
| 317 def RemoveFunc(self, name_expansion_result, thread_state=None): | 340 def RemoveFunc(self, name_expansion_result, thread_state=None): |
| 318 gsutil_api = GetCloudApiInstance(self, thread_state=thread_state) | 341 gsutil_api = GetCloudApiInstance(self, thread_state=thread_state) |
| 319 | 342 |
| 320 exp_src_url = name_expansion_result.expanded_storage_url | 343 exp_src_url = name_expansion_result.expanded_storage_url |
| 321 self.logger.info('Removing %s...', exp_src_url) | 344 self.logger.info('Removing %s...', exp_src_url) |
| 322 gsutil_api.DeleteObject( | 345 gsutil_api.DeleteObject( |
| 323 exp_src_url.bucket_name, exp_src_url.object_name, | 346 exp_src_url.bucket_name, exp_src_url.object_name, |
| 324 preconditions=self.preconditions, generation=exp_src_url.generation, | 347 preconditions=self.preconditions, generation=exp_src_url.generation, |
| 325 provider=exp_src_url.scheme) | 348 provider=exp_src_url.scheme) |
| 326 | 349 |
| OLD | NEW |