This is an automated email from the ASF dual-hosted git repository.

ggregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-io.git

commit 0be56985e1e7d42f9ad4396e94a735530e21a305
Author: Gary Gregory <garydgreg...@gmail.com>
AuthorDate: Thu Nov 14 10:53:17 2024 -0500

    [IO-862] FileUtils.deleteDirectory fails for a directory containing a
    broken symbolic link
---
 src/changes/changes.xml                            |  1 +
 src/main/java/org/apache/commons/io/FileUtils.java | 55 +++++++++++++++++-----
 .../java/org/apache/commons/io/FileUtilsTest.java  | 28 +++++++++--
 3 files changed, 67 insertions(+), 17 deletions(-)

diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 84e0b4bd5..7468e3ee7 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -57,6 +57,7 @@ The <action> type attribute can be add,update,fix,remove.
       <action dev="ggregory" type="fix" issue="IO-859" due-to="JD Dean, Gary 
Gregory">FileUtils.forceDelete on non-existent file on Windows throws 
IOException rather than FileNotFoundException.</action>
       <action dev="ggregory" type="fix"                due-to="Éamonn 
McManus">Use Unicode escapes for superscript characters. #701.</action>
       <action dev="ggregory" type="fix" issue="IO-863" due-to="Éamonn McManus, 
Gary Gregory">Recent incompatible change to FileUtils.listFiles re extensions, 
see IO-856.</action>
+      <action dev="ggregory" type="fix" issue="IO-862" due-to="Éamonn McManus, 
Gary Gregory">FileUtils.deleteDirectory fails for a directory containing a 
broken symbolic link.</action>
       <!-- ADD -->
       <action dev="ggregory" type="add"                due-to="Gary 
Gregory">Add @FunctionalInterface to ClassNameMatcher.</action>
       <action dev="ggregory" type="add"                due-to="Gary 
Gregory">Add ValidatingObjectInputStream.Builder and 
ValidatingObjectInputStream.builder().</action>
diff --git a/src/main/java/org/apache/commons/io/FileUtils.java 
b/src/main/java/org/apache/commons/io/FileUtils.java
index 55a717c6b..63345f113 100644
--- a/src/main/java/org/apache/commons/io/FileUtils.java
+++ b/src/main/java/org/apache/commons/io/FileUtils.java
@@ -285,19 +285,28 @@ public class FileUtils {
     }
 
     /**
-     * Requires that the given {@link File} exists, and throws a {@link 
FileNotFoundException} if it doesn't.
+     * Requires that the given {@link File} is non-null and exists (if strict 
is true).
      *
      * @param file The {@link File} to check.
-     * @throws FileNotFoundException if the file does not exist
+     * @param strict whether to check that the {@code file} exists.
+     * @throws FileNotFoundException if the file does not exist.
      * @throws NullPointerException  if the given {@link File} is {@code null}.
      */
-    private static void checkExists(final File file) throws 
FileNotFoundException {
-        Objects.requireNonNull(file, "file");
-        if (!file.exists()) {
+    private static void checkExists(final File file, final boolean strict) 
throws FileNotFoundException {
+        Objects.requireNonNull(file, PROTOCOL_FILE);
+        if (strict && !file.exists()) {
             throw new FileNotFoundException(file.toString());
         }
     }
 
+    /**
+     * Requires that the given {@link File} exists, and throws a {@link 
FileNotFoundException} if it doesn't.
+     *
+     * @param file The {@link File} to check.
+     * @param name The NullPointerException message.
+     * @throws FileNotFoundException if the file does not exist.
+     * @throws NullPointerException  if the given {@link File} is {@code null}.
+     */
     private static void checkFileExists(final File file, final String name) 
throws FileNotFoundException {
         Objects.requireNonNull(file, name);
         if (!file.isFile()) {
@@ -350,8 +359,8 @@ public class FileUtils {
      *
      * @param file the file to checksum, must not be {@code null}
      * @return the checksum value
-     * @throws NullPointerException if the given {@link File} is {@code null}.
-     * @throws IllegalArgumentException if the given {@link File} does not 
exist or is not a file.
+     * @throws NullPointerException if the {@code file} is {@code null}.
+     * @throws IllegalArgumentException if the {@code file} does not exist or 
is not a file.
      * @throws IOException              if an IO error occurs reading the file.
      * @since 1.3
      */
@@ -364,12 +373,12 @@ public class FileUtils {
      *
      * @param directory directory to clean
      * @throws NullPointerException if the given {@link File} is {@code null}.
-     * @throws IllegalArgumentException if directory does not exist or is not 
a directory.
+     * @throws IllegalArgumentException if the {@code directory} does not 
exist or is not a directory.
      * @throws IOException if an I/O error occurs.
      * @see #forceDelete(File)
      */
     public static void cleanDirectory(final File directory) throws IOException 
{
-        IOConsumer.forAll(FileUtils::forceDelete, listFiles(directory, null));
+        IOConsumer.forAll(f -> forceDelete(f, false), listFiles(directory, 
null));
     }
 
     /**
@@ -377,7 +386,7 @@ public class FileUtils {
      *
      * @param directory directory to clean, must not be {@code null}
      * @throws NullPointerException if the given {@link File} is {@code null}.
-     * @throws IllegalArgumentException if directory does not exist or is not 
a directory.
+     * @throws IllegalArgumentException if the {@code directory} does not 
exist or is not a directory.
      * @throws IOException if an I/O error occurs.
      * @see #forceDeleteOnExit(File)
      */
@@ -1386,8 +1395,28 @@ public class FileUtils {
      * @throws IOException           in case deletion is unsuccessful.
      */
     public static void forceDelete(final File file) throws IOException {
+        forceDelete(file, true);
+    }
+
+    /**
+     * Deletes a file or directory. For a directory, delete it and all 
subdirectories.
+     * <p>
+     * The difference between File.delete() and this method are:
+     * </p>
+     * <ul>
+     * <li>The directory does not have to be empty.</li>
+     * <li>You get an exception when a file or directory cannot be 
deleted.</li>
+     * </ul>
+     *
+     * @param file file or directory to delete, must not be {@code null}.
+     * @param strict whether to throw a FileNotFoundException.
+     * @throws NullPointerException  if the file is {@code null}.
+     * @throws FileNotFoundException if the file was not found.
+     * @throws IOException           in case deletion is unsuccessful.
+     */
+    private static void forceDelete(final File file, final boolean strict) 
throws IOException {
         Objects.requireNonNull(file, PROTOCOL_FILE);
-        checkExists(file); // fail-fast
+        checkExists(file, strict); // fail-fast
         final Counters.PathCounters deleteCounters;
         try {
             deleteCounters = PathUtils.delete(file.toPath(), 
PathUtils.EMPTY_LINK_OPTION_ARRAY, StandardDeleteOption.OVERRIDE_READ_ONLY);
@@ -2265,11 +2294,11 @@ public class FileUtils {
     /**
      * Lists files in a directory, asserting that the supplied directory 
exists and is a directory.
      *
-     * @param directory The directory to list
+     * @param directory The directory to list.
      * @param fileFilter Optional file filter, may be null.
      * @return The files in the directory, never {@code null}.
      * @throws NullPointerException if directory is {@code null}.
-     * @throws IllegalArgumentException if {@link directory} exists but is not 
a directory
+     * @throws IllegalArgumentException if {@link directory} exists but is not 
a directory.
      * @throws IOException if an I/O error occurs.
      */
     private static File[] listFiles(final File directory, final FileFilter 
fileFilter) throws IOException {
diff --git a/src/test/java/org/apache/commons/io/FileUtilsTest.java 
b/src/test/java/org/apache/commons/io/FileUtilsTest.java
index f2f8e422f..4e1ef0b8e 100644
--- a/src/test/java/org/apache/commons/io/FileUtilsTest.java
+++ b/src/test/java/org/apache/commons/io/FileUtilsTest.java
@@ -248,10 +248,10 @@ public class FileUtilsTest extends AbstractTempDirTest {
 
     private ImmutablePair<Path, Path> createTempSymbolicLinkedRelativeDir() 
throws IOException {
         final Path targetDir = tempDirPath.resolve("subdir");
-        final Path symlinkDir = tempDirPath.resolve("symlinked-dir");
+        final Path symLinkedDir = tempDirPath.resolve("symlinked-dir");
         Files.createDirectory(targetDir);
-        Files.createSymbolicLink(symlinkDir, targetDir);
-        return ImmutablePair.of(symlinkDir, targetDir);
+        Files.createSymbolicLink(symLinkedDir, targetDir);
+        return ImmutablePair.of(symLinkedDir, targetDir);
     }
 
     private Set<String> getFilePathSet(final List<File> files) {
@@ -318,7 +318,6 @@ public class FileUtilsTest extends AbstractTempDirTest {
     public void setUp() throws Exception {
         testFile1 = new File(tempDirFile, "file1-test.txt");
         testFile2 = new File(tempDirFile, "file1a-test.txt");
-
         testFile1Size = testFile1.length();
         testFile2Size = testFile2.length();
         if (!testFile1.getParentFile().exists()) {
@@ -1586,6 +1585,7 @@ public class FileUtilsTest extends AbstractTempDirTest {
         final Path symlinkedDir = pair.getLeft();
         final Path targetDir = pair.getRight();
         assertTrue(Files.exists(symlinkedDir), symlinkedDir::toString);
+        // remove target directory, keeping symbolic link
         Files.delete(targetDir);
         assertFalse(Files.exists(targetDir), targetDir::toString);
         assertFalse(Files.exists(symlinkedDir), symlinkedDir::toString);
@@ -1594,6 +1594,26 @@ public class FileUtilsTest extends AbstractTempDirTest {
         assertFalse(Files.exists(symlinkedDir), symlinkedDir::toString);
     }
 
+    @Test
+    public void testDeleteDirectorySymbolicLinkAbsentDeepTarget() throws 
IOException {
+        final ImmutablePair<Path, Path> pair = 
createTempSymbolicLinkedRelativeDir();
+        final Path symLinkedDir = pair.getLeft();
+        final Path targetDir = pair.getRight();
+        // more setup
+        final Path targetDir2 = targetDir.resolve("subdir2");
+        final Path symLinkedDir2 = targetDir.resolve("symlinked-dir2");
+        Files.createDirectory(targetDir2);
+        Files.createSymbolicLink(symLinkedDir2, targetDir2);
+        assertTrue(Files.exists(symLinkedDir2), symLinkedDir2::toString);
+        // remove target directory, keeping symbolic link
+        Files.delete(targetDir2);
+        assertFalse(Files.exists(targetDir2), targetDir2::toString);
+        assertFalse(Files.exists(symLinkedDir2), symLinkedDir2::toString);
+        // actual test
+        FileUtils.deleteDirectory(targetDir.toFile());
+        assertFalse(Files.exists(targetDir), targetDir::toString);
+    }
+
     @Test
     public void testDeleteQuietlyDir() throws IOException {
         final File testDirectory = new File(tempDirFile, 
"testDeleteQuietlyDir");

Reply via email to