diff --git a/core/src/main/java/hudson/FilePath.java b/core/src/main/java/hudson/FilePath.java index 2f3c08fcbad6592e2889ec2bcb1154ca969a3711..fb6a09a2d51a53e2e13dbdb98596f3183a131b7f 100644 --- a/core/src/main/java/hudson/FilePath.java +++ b/core/src/main/java/hudson/FilePath.java @@ -1271,7 +1271,7 @@ public final class FilePath implements Serializable { private static final long serialVersionUID = 1L; @Override public Void invoke(File f, VirtualChannel channel) throws IOException { - Util.deleteRecursive(deleting(f)); + Util.deleteRecursive(fileToPath(f), path -> deleting(path.toFile()).delete()); return null; } } @@ -1286,7 +1286,7 @@ public final class FilePath implements Serializable { private static final long serialVersionUID = 1L; @Override public Void invoke(File f, VirtualChannel channel) throws IOException { - Util.deleteContentsRecursive(deleting(f)); + Util.deleteContentsRecursive(fileToPath(f), path -> deleting(path.toFile()).delete()); return null; } } diff --git a/core/src/main/java/hudson/Util.java b/core/src/main/java/hudson/Util.java index 5c258d94699fd7c41448152a43346808e4fe0412..3e857f481c67fc709ea642dc20b63f680b17785a 100644 --- a/core/src/main/java/hudson/Util.java +++ b/core/src/main/java/hudson/Util.java @@ -75,6 +75,7 @@ import java.time.temporal.ChronoUnit; import java.util.*; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Predicate; import java.util.logging.Level; import java.util.logging.LogRecord; import java.util.logging.Logger; @@ -241,7 +242,18 @@ public class Util { * if the operation fails. */ public static void deleteContentsRecursive(@Nonnull File file) throws IOException { - newPathRemover().forceRemoveDirectoryContents(fileToPath(file)); + deleteContentsRecursive(fileToPath(file), path -> true); + } + + /** + * Deletes the given directory contents (but not the directory itself) recursively using a filter. + * @param path a directory to delete + * @param pathFilter a predicate that when evaluated to true will delete that path; when false, will ignore that path + * @throws IOException if the operation fails + */ + @Restricted(NoExternalUse.class) + public static void deleteContentsRecursive(@Nonnull Path path, @Nonnull Predicate pathFilter) throws IOException { + newPathRemover(pathFilter).forceRemoveDirectoryContents(path); } /** @@ -252,7 +264,7 @@ public class Util { * @throws IOException if it exists but could not be successfully deleted */ public static void deleteFile(@Nonnull File f) throws IOException { - newPathRemover().forceRemoveFile(fileToPath(f)); + newPathRemover(path -> true).forceRemoveFile(fileToPath(f)); } /** @@ -264,7 +276,18 @@ public class Util { * if the operation fails. */ public static void deleteRecursive(@Nonnull File dir) throws IOException { - newPathRemover().forceRemoveRecursive(fileToPath(dir)); + deleteRecursive(fileToPath(dir), path -> true); + } + + /** + * Deletes the given directory and contents recursively using a filter. + * @param dir a directory to delete + * @param pathFilter a predicate that when evaluated to true will delete that path; when false, will ignore that path + * @throws IOException if the operation fails + */ + @Restricted(NoExternalUse.class) + public static void deleteRecursive(@Nonnull Path dir, @Nonnull Predicate pathFilter) throws IOException { + newPathRemover(pathFilter).forceRemoveRecursive(dir); } /* @@ -1560,8 +1583,8 @@ public class Util { @Restricted(value = NoExternalUse.class) static boolean GC_AFTER_FAILED_DELETE = SystemProperties.getBoolean(Util.class.getName() + ".performGCOnFailedDelete"); - private static PathRemover newPathRemover() { - return PathRemover.newRobustRemover(DELETION_MAX - 1, GC_AFTER_FAILED_DELETE, WAIT_BETWEEN_DELETION_RETRIES); + private static PathRemover newPathRemover(@Nonnull Predicate pathFilter) { + return PathRemover.newFilteredRobustRemover(pathFilter, DELETION_MAX - 1, GC_AFTER_FAILED_DELETE, WAIT_BETWEEN_DELETION_RETRIES); } /** diff --git a/core/src/main/java/jenkins/util/io/PathRemover.java b/core/src/main/java/jenkins/util/io/PathRemover.java index 747439927768022f8a102e1db7d456d11179f454..789c9b4d795d4a8377a51e915f9edfee0c83c17a 100644 --- a/core/src/main/java/jenkins/util/io/PathRemover.java +++ b/core/src/main/java/jenkins/util/io/PathRemover.java @@ -42,6 +42,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.Set; +import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -49,21 +50,23 @@ import java.util.stream.Stream; public class PathRemover { public static PathRemover newSimpleRemover() { - return new PathRemover(ignored -> false); + return new PathRemover(ignored -> false, ignored -> true); } public static PathRemover newRemoverWithStrategy(@Nonnull RetryStrategy retryStrategy) { - return new PathRemover(retryStrategy); + return new PathRemover(retryStrategy, ignored -> true); } - public static PathRemover newRobustRemover(int maxRetries, boolean gcAfterFailedRemove, long waitBetweenRetries) { - return new PathRemover(new PausingGCRetryStrategy(maxRetries < 1 ? 1 : maxRetries, gcAfterFailedRemove, waitBetweenRetries)); + public static PathRemover newFilteredRobustRemover(@Nonnull Predicate pathFilter, int maxRetries, boolean gcAfterFailedRemove, long waitBetweenRetries) { + return new PathRemover(new PausingGCRetryStrategy(maxRetries < 1 ? 1 : maxRetries, gcAfterFailedRemove, waitBetweenRetries), pathFilter); } private final RetryStrategy retryStrategy; + private final Predicate pathFilter; - private PathRemover(@Nonnull RetryStrategy retryStrategy) { + private PathRemover(@Nonnull RetryStrategy retryStrategy, @Nonnull Predicate pathFilter) { this.retryStrategy = retryStrategy; + this.pathFilter = pathFilter; } public void forceRemoveFile(@Nonnull Path path) throws IOException { @@ -186,7 +189,7 @@ public class PathRemover { } } - private static Optional tryRemoveFile(@Nonnull Path path) { + private Optional tryRemoveFile(@Nonnull Path path) { try { removeOrMakeRemovableThenRemove(path.normalize()); return Optional.empty(); @@ -195,7 +198,7 @@ public class PathRemover { } } - private static List tryRemoveRecursive(@Nonnull Path path) { + private List tryRemoveRecursive(@Nonnull Path path) { Path normalized = path.normalize(); List accumulatedErrors = Util.isSymlink(normalized) ? new ArrayList<>() : tryRemoveDirectoryContents(normalized); @@ -203,7 +206,7 @@ public class PathRemover { return accumulatedErrors; } - private static List tryRemoveDirectoryContents(@Nonnull Path path) { + private List tryRemoveDirectoryContents(@Nonnull Path path) { Path normalized = path.normalize(); List accumulatedErrors = new ArrayList<>(); if (!Files.isDirectory(normalized)) return accumulatedErrors; @@ -217,7 +220,8 @@ public class PathRemover { return accumulatedErrors; } - private static void removeOrMakeRemovableThenRemove(@Nonnull Path path) throws IOException { + private void removeOrMakeRemovableThenRemove(@Nonnull Path path) throws IOException { + if (!pathFilter.test(path)) return; try { Files.deleteIfExists(path); } catch (IOException e) { @@ -256,9 +260,9 @@ public class PathRemover { $ rm x rm: x not removed: Permission denied */ - Path parent = path.getParent().normalize(); - if (parent != null && !Files.isWritable(parent)) { - makeWritable(parent); + Optional maybeParent = Optional.ofNullable(path.getParent()).map(Path::normalize).filter(p -> !Files.isWritable(p)); + if (maybeParent.isPresent()) { + makeWritable(maybeParent.get()); } } diff --git a/core/src/test/java/jenkins/util/io/PathRemoverTest.java b/core/src/test/java/jenkins/util/io/PathRemoverTest.java index 747775531dae900509365ff01ca04078fc1573f8..fbfdd45a56aae7e046e0ea2851352e475b5dff16 100644 --- a/core/src/test/java/jenkins/util/io/PathRemoverTest.java +++ b/core/src/test/java/jenkins/util/io/PathRemoverTest.java @@ -91,6 +91,7 @@ public class PathRemoverTest { given(path.toString()).willReturn(filename); given(path.toFile()).willReturn(file); given(path.getFileSystem()).willReturn(fs); + given(path.normalize()).willReturn(path); given(fs.provider()).willReturn(fsProvider); given(fsProvider.deleteIfExists(path)).willThrow(new FileSystemException(filename)); given(fsProvider.readAttributes(path, BasicFileAttributes.class)).willReturn(attributes);