You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@commons.apache.org by gg...@apache.org on 2020/12/06 19:09:50 UTC

[commons-io] branch master updated (a75b412 -> 2bc7e31)

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

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


    from a75b412  In-line local var.
     new 8e39c7d  Refactor internals link options, no functional change.
     new bf47606  Better in-line comments.
     new 2bc7e31  Reimplement some FileUtils internals in terms of refactored PathUtils methods to provide better behavioral compatibility with older versions like 2.6 in the area of deleting read-only file system elements.

The 3 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 src/main/java/org/apache/commons/io/FileUtils.java | 211 ++++++++++-----------
 .../commons/io/file/CleaningPathVisitor.java       |   2 +-
 .../commons/io/file/DeletingPathVisitor.java       |  24 ++-
 .../java/org/apache/commons/io/file/PathUtils.java | 118 +++++++++---
 .../commons/io/FileUtilsCleanSymlinksTestCase.java |  16 +-
 5 files changed, 227 insertions(+), 144 deletions(-)


[commons-io] 02/03: Better in-line comments.

Posted by gg...@apache.org.
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 bf47606dccc78acf42754c1d58544e9ec0c1705d
Author: Gary Gregory <ga...@gmail.com>
AuthorDate: Sun Dec 6 12:52:01 2020 -0500

    Better in-line comments.
---
 .../commons/io/FileUtilsCleanSymlinksTestCase.java       | 16 ++++++++--------
 1 file changed, 8 insertions(+), 8 deletions(-)

diff --git a/src/test/java/org/apache/commons/io/FileUtilsCleanSymlinksTestCase.java b/src/test/java/org/apache/commons/io/FileUtilsCleanSymlinksTestCase.java
index e874bc3..27d61c5 100644
--- a/src/test/java/org/apache/commons/io/FileUtilsCleanSymlinksTestCase.java
+++ b/src/test/java/org/apache/commons/io/FileUtilsCleanSymlinksTestCase.java
@@ -39,7 +39,7 @@ public class FileUtilsCleanSymlinksTestCase {
     @Test
     public void testCleanDirWithSymlinkFile() throws Exception {
         if (System.getProperty("os.name").startsWith("Win")) {
-            // cant create symlinks in windows.
+            // Can't use "ln" for symlinks on the command line in Windows.
             return;
         }
 
@@ -74,7 +74,7 @@ public class FileUtilsCleanSymlinksTestCase {
     @Test
     public void testCleanDirWithASymlinkDir() throws Exception {
         if (System.getProperty("os.name").startsWith("Win")) {
-            // cant create symlinks in windows.
+            // Can't use "ln" for symlinks on the command line in Windows.
             return;
         }
 
@@ -109,7 +109,7 @@ public class FileUtilsCleanSymlinksTestCase {
     @Test
     public void testCleanDirWithParentSymlinks() throws Exception {
         if (System.getProperty("os.name").startsWith("Win")) {
-            // cant create symlinks in windows.
+            // Can't use "ln" for symlinks on the command line in Windows.
             return;
         }
 
@@ -148,7 +148,7 @@ public class FileUtilsCleanSymlinksTestCase {
     @Test
     public void testStillClearsIfGivenDirectoryIsASymlink() throws Exception {
         if (System.getProperty("os.name").startsWith("Win")) {
-            // cant create symlinks in windows.
+            // Can't use "ln" for symlinks on the command line in Windows.
             return;
         }
 
@@ -170,7 +170,7 @@ public class FileUtilsCleanSymlinksTestCase {
     @Test
     public void testIdentifiesSymlinkDir() throws Exception {
         if (System.getProperty("os.name").startsWith("Win")) {
-            // cant create symlinks in windows.
+            // Can't use "ln" for symlinks on the command line in Windows.
             return;
         }
 
@@ -187,7 +187,7 @@ public class FileUtilsCleanSymlinksTestCase {
     @Test
     public void testIdentifiesSymlinkFile() throws Exception {
         if (System.getProperty("os.name").startsWith("Win")) {
-            // cant create symlinks in windows.
+            // Can't use "ln" for symlinks on the command line in Windows.
             return;
         }
 
@@ -204,7 +204,7 @@ public class FileUtilsCleanSymlinksTestCase {
     @Test
     public void testIdentifiesBrokenSymlinkFile() throws Exception {
         if (System.getProperty("os.name").startsWith("Win")) {
-            // cant create symlinks in windows.
+            // Can't use "ln" for symlinks on the command line in Windows.
             return;
         }
 
@@ -224,7 +224,7 @@ public class FileUtilsCleanSymlinksTestCase {
     @Test
     public void testCorrectlyIdentifySymlinkWithParentSymLink() throws Exception {
         if (System.getProperty("os.name").startsWith("Win")) {
-            // cant create symlinks in windows.
+            // Can't use "ln" for symlinks on the command line in Windows.
             return;
         }
 


[commons-io] 01/03: Refactor internals link options, no functional change.

Posted by gg...@apache.org.
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 8e39c7dbba317da4fb5606ead507fa8bcfd6372f
Author: Gary Gregory <ga...@gmail.com>
AuthorDate: Sun Dec 6 12:51:35 2020 -0500

    Refactor internals link options, no functional change.
---
 src/main/java/org/apache/commons/io/file/DeletingPathVisitor.java | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)

diff --git a/src/main/java/org/apache/commons/io/file/DeletingPathVisitor.java b/src/main/java/org/apache/commons/io/file/DeletingPathVisitor.java
index 371a465..6235b56 100644
--- a/src/main/java/org/apache/commons/io/file/DeletingPathVisitor.java
+++ b/src/main/java/org/apache/commons/io/file/DeletingPathVisitor.java
@@ -55,6 +55,7 @@ public class DeletingPathVisitor extends CountingPathVisitor {
 
     private final String[] skip;
     private final boolean overrideReadOnly;
+    private final LinkOption[] linkOptions;
 
     /**
      * Constructs a new visitor that deletes files except for the files and directories explicitly given.
@@ -70,6 +71,8 @@ public class DeletingPathVisitor extends CountingPathVisitor {
         Arrays.sort(temp);
         this.skip = temp;
         this.overrideReadOnly = StandardDeleteOption.overrideReadOnly(deleteOption);
+        // TODO Files.deleteIfExists() never follows links, so use LinkOption.NOFOLLOW_LINKS in other calls to Files.
+        this.linkOptions = PathUtils.NOFOLLOW_LINK_OPTION_ARRAY.clone();
     }
 
     /**
@@ -133,10 +136,9 @@ public class DeletingPathVisitor extends CountingPathVisitor {
 
     @Override
     public FileVisitResult visitFile(final Path file, final BasicFileAttributes attrs) throws IOException {
-        // Files.deleteIfExists() never follows links, so use LinkOption.NOFOLLOW_LINKS in other calls to Files.
-        if (accept(file) && Files.exists(file, LinkOption.NOFOLLOW_LINKS)) {
+        if (accept(file) && Files.exists(file, linkOptions)) {
             if (overrideReadOnly) {
-                PathUtils.setReadOnly(file, false, LinkOption.NOFOLLOW_LINKS);
+                PathUtils.setReadOnly(file, false, linkOptions);
             }
             Files.deleteIfExists(file);
         }


[commons-io] 03/03: Reimplement some FileUtils internals in terms of refactored PathUtils methods to provide better behavioral compatibility with older versions like 2.6 in the area of deleting read-only file system elements.

Posted by gg...@apache.org.
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 2bc7e31f1dee0cb6ff4f3c57a63ac09cd4c2d1aa
Author: Gary Gregory <ga...@gmail.com>
AuthorDate: Sun Dec 6 14:09:44 2020 -0500

    Reimplement some FileUtils internals in terms of refactored PathUtils
    methods to provide better behavioral compatibility with older versions
    like 2.6 in the area of deleting read-only file system elements.
    
    Also clean up some Javadocs.
---
 src/main/java/org/apache/commons/io/FileUtils.java | 211 ++++++++++-----------
 .../commons/io/file/CleaningPathVisitor.java       |   2 +-
 .../commons/io/file/DeletingPathVisitor.java       |  18 +-
 .../java/org/apache/commons/io/file/PathUtils.java | 118 +++++++++---
 4 files changed, 215 insertions(+), 134 deletions(-)

diff --git a/src/main/java/org/apache/commons/io/FileUtils.java b/src/main/java/org/apache/commons/io/FileUtils.java
index 153666d..99b3386 100644
--- a/src/main/java/org/apache/commons/io/FileUtils.java
+++ b/src/main/java/org/apache/commons/io/FileUtils.java
@@ -238,55 +238,6 @@ public class FileUtils {
     }
 
     /**
-     * Checks that the given {@code File} exists and is a directory.
-     *
-     * @param directory The {@code File} to check.
-     * @return the given directory.
-     * @throws IllegalArgumentException if the given {@code File} does not exist or is not a directory.
-     */
-    private static File checkDirectory(final File directory) {
-        if (!directory.exists()) {
-            throw new IllegalArgumentException(directory + " does not exist");
-        }
-        if (!directory.isDirectory()) {
-            throw new IllegalArgumentException(directory + " is not a directory");
-        }
-        return directory;
-    }
-
-    /**
-     * Checks that two file lengths are equal.
-     *
-     * @param srcFile Source file.
-     * @param destFile Destination file.
-     * @param srcLen Source file length.
-     * @param dstLen Destination file length
-     * @throws IOException Thrown when the given sizes are not equal.
-     */
-    private static void checkEqualSizes(final File srcFile, final File destFile, final long srcLen, final long dstLen)
-            throws IOException {
-        if (srcLen != dstLen) {
-            throw new IOException("Failed to copy full contents from '" + srcFile + "' to '" + destFile
-                    + "' Expected length: " + srcLen + " Actual: " + dstLen);
-        }
-    }
-
-    /**
-     * Checks requirements for file copy.
-     *
-     * @param source the source file
-     * @param destination the destination
-     * @throws FileNotFoundException if the destination does not exist
-     */
-    private static void checkFileRequirements(final File source, final File destination) throws FileNotFoundException {
-        Objects.requireNonNull(source, "source");
-        Objects.requireNonNull(destination, "target");
-        if (!source.exists()) {
-            throw new FileNotFoundException("Source '" + source + "' does not exist");
-        }
-    }
-
-    /**
      * Computes the checksum of a file using the specified checksum object.
      * Multiple files may be checked using one <code>Checksum</code> instance
      * if desired simply by reusing the same checksum object.
@@ -304,9 +255,7 @@ public class FileUtils {
      * @since 1.3
      */
     public static Checksum checksum(final File file, final Checksum checksum) throws IOException {
-        if (file.isDirectory()) {
-            throw new IllegalArgumentException("Checksums can't be computed on directories");
-        }
+        requireFile(file, "file");
         try (InputStream in = new CheckedInputStream(new FileInputStream(file), checksum)) {
             IOUtils.consume(in);
         }
@@ -717,7 +666,7 @@ public class FileUtils {
      */
     public static void copyDirectory(final File srcDir, final File destDir, final FileFilter filter,
         final boolean preserveFileDate, final CopyOption... copyOptions) throws IOException {
-        checkFileRequirements(srcDir, destDir);
+        requireFileRequirements(srcDir, destDir);
         if (!srcDir.isDirectory()) {
             throw new IOException("Source '" + srcDir + "' exists but is not a directory");
         }
@@ -875,7 +824,7 @@ public class FileUtils {
      */
     public static void copyFile(final File srcFile, final File destFile, final boolean preserveFileDate, final CopyOption... copyOptions)
         throws IOException {
-        checkFileRequirements(srcFile, destFile);
+        requireFileRequirements(srcFile, destFile);
         if (srcFile.isDirectory()) {
             throw new IOException("Source '" + srcFile + "' exists but is a directory");
         }
@@ -1041,7 +990,6 @@ public class FileUtils {
         }
     }
 
-
     /**
      * Copies a files to a directory preserving each file's date.
      * <p>
@@ -1122,6 +1070,7 @@ public class FileUtils {
         }
     }
 
+
     /**
      * Copies bytes from the URL <code>source</code> to a file
      * <code>destination</code>. The directories up to <code>destination</code>
@@ -1291,20 +1240,12 @@ public class FileUtils {
      * @param child     the file to consider as the child.
      * @return true is the candidate leaf is under by the specified composite. False otherwise.
      * @throws IOException              if an IO error occurs while checking the files.
-     * @throws IllegalArgumentException if {@code directory} is null or not a directory.
+     * @throws IllegalArgumentException if {@code directory} is not a directory.
      * @see FilenameUtils#directoryContains(String, String)
      * @since 2.2
      */
     public static boolean directoryContains(final File directory, final File child) throws IOException {
-
-        // Fail fast against NullPointerException
-        if (directory == null) {
-            throw new IllegalArgumentException("Directory must not be null");
-        }
-
-        if (!directory.isDirectory()) {
-            throw new IllegalArgumentException("Not a directory: " + directory);
-        }
+        requireDirectory(directory, "directory");
 
         if (child == null) {
             return false;
@@ -1399,9 +1340,9 @@ public class FileUtils {
         Files.copy(srcPath, destPath, copyOptions);
 
         // TODO IO-386: Do we still need this check?
-        checkEqualSizes(srcFile, destFile, Files.size(srcPath), Files.size(destPath));
+        requireEqualSizes(srcFile, destFile, Files.size(srcPath), Files.size(destPath));
         // TODO IO-386: Do we still need this check?
-        checkEqualSizes(srcFile, destFile, srcFile.length(), destFile.length());
+        requireEqualSizes(srcFile, destFile, srcFile.length(), destFile.length());
 
         if (preserveFileDate) {
             setLastModified(srcFile, destFile);
@@ -1426,7 +1367,8 @@ public class FileUtils {
     public static void forceDelete(final File file) throws IOException {
         final Counters.PathCounters deleteCounters;
         try {
-            deleteCounters = PathUtils.delete(file.toPath(), StandardDeleteOption.OVERRIDE_READ_ONLY);
+            deleteCounters = PathUtils.delete(file.toPath(), PathUtils.EMPTY_LINK_OPTION_ARRAY,
+                StandardDeleteOption.OVERRIDE_READ_ONLY);
         } catch (final IOException e) {
             throw new IOException("Unable to delete file: " + file, e);
         }
@@ -1713,11 +1655,7 @@ public class FileUtils {
      * @throws IllegalArgumentException if the reference file doesn't exist
      */
     public static boolean isFileNewer(final File file, final File reference) {
-        Objects.requireNonNull(reference, "reference");
-        if (!reference.exists()) {
-            throw new IllegalArgumentException("The reference file '"
-                    + reference + "' doesn't exist");
-        }
+        requireExists(reference, "reference");
         return isFileNewer(file, reference.lastModified());
     }
 
@@ -1882,10 +1820,7 @@ public class FileUtils {
      * @throws IllegalArgumentException if the reference file doesn't exist
      */
     public static boolean isFileOlder(final File file, final File reference) {
-        if (!Objects.requireNonNull(reference, "reference").exists()) {
-            throw new IllegalArgumentException("The reference file '"
-                    + reference + "' doesn't exist");
-        }
+        requireExists(reference, "reference");
         return isFileOlder(file, reference.lastModified());
     }
 
@@ -1956,7 +1891,6 @@ public class FileUtils {
      * The resulting iterator MUST be consumed in its entirety in order to close its underlying stream.
      * </p>
      * <p>
-     * <p>
      * All files found are filtered by an IOFileFilter.
      * </p>
      *
@@ -2152,6 +2086,7 @@ public class FileUtils {
             throw new IllegalArgumentException(e);
         }
     }
+
     /**
      * Finds files within a given directory (and optionally its
      * subdirectories). All files found are filtered by an IOFileFilter.
@@ -2249,7 +2184,6 @@ public class FileUtils {
         }
         moveDirectory(src, new File(destDir, src.getName()));
     }
-
     /**
      * Moves a file.
      * <p>
@@ -2478,7 +2412,6 @@ public class FileUtils {
         return readFileToString(file, Charset.defaultCharset());
     }
 
-
     /**
      * Reads the contents of a file into a String.
      * The file is always closed.
@@ -2525,6 +2458,7 @@ public class FileUtils {
         return readLines(file, Charset.defaultCharset());
     }
 
+
     /**
      * Reads the contents of a file line by line to a List of Strings.
      * The file is always closed.
@@ -2557,6 +2491,86 @@ public class FileUtils {
     }
 
     /**
+     * Requires that the given {@code File} exists and is a directory.
+     *
+     * @param directory The {@code File} to check.
+     * @param param The param name to use in the exception message in case of null input.
+     * @return the given directory.
+     * @throws IllegalArgumentException if the given {@code File} does not exist or is not a directory.
+     */
+    private static File requireDirectory(final File directory, String param) {
+        requireExists(directory, param);
+        if (!directory.isDirectory()) {
+            throw new IllegalArgumentException(directory + " is not a directory");
+        }
+        return directory;
+    }
+
+    /**
+     * Requires that two file lengths are equal.
+     *
+     * @param srcFile Source file.
+     * @param destFile Destination file.
+     * @param srcLen Source file length.
+     * @param dstLen Destination file length
+     * @throws IOException Thrown when the given sizes are not equal.
+     */
+    private static void requireEqualSizes(final File srcFile, final File destFile, final long srcLen, final long dstLen)
+            throws IOException {
+        if (srcLen != dstLen) {
+            throw new IOException("Failed to copy full contents from '" + srcFile + "' to '" + destFile
+                    + "' Expected length: " + srcLen + " Actual: " + dstLen);
+        }
+    }
+
+    /**
+     * Requires that the given {@code File} exists.
+     *
+     * @param file The {@code File} to check.
+     * @param param The param name to use in the exception message in case of null input.
+     * @return the given file.
+     * @throws IllegalArgumentException if the given {@code File} does not exist or is not a directory.
+     */
+    private static File requireExists(final File file, String param) {
+        Objects.requireNonNull(file, param);
+        if (!file.exists()) {
+            throw new IllegalArgumentException(file + " does not exist");
+        }
+        return file;
+    }
+
+    /**
+     * Requires that the given {@code File} exists and is a file.
+     *
+     * @param file The {@code File} to check.
+     * @param param The param name to use in the exception message in case of null input.
+     * @return the given file.
+     * @throws IllegalArgumentException if the given {@code File} does not exist or is not a directory.
+     */
+    private static File requireFile(final File file, String param) {
+        requireExists(file, param);
+        if (!file.isFile()) {
+            throw new IllegalArgumentException(file + " is not a file");
+        }
+        return file;
+    }
+
+    /**
+     * Requires requirements for file copy.
+     *
+     * @param source the source file
+     * @param destination the destination
+     * @throws FileNotFoundException if the destination does not exist
+     */
+    private static void requireFileRequirements(final File source, final File destination) throws FileNotFoundException {
+        Objects.requireNonNull(source, "source");
+        Objects.requireNonNull(destination, "target");
+        if (!source.exists()) {
+            throw new FileNotFoundException("Source '" + source + "' does not exist");
+        }
+    }
+
+    /**
      * Sets the given {@code targetFile}'s last modified date to the value from {@code sourceFile}.
      *
      * @param sourceFile The source file to query.
@@ -2587,16 +2601,13 @@ public class FileUtils {
      * @return the length of the file, or recursive size of the directory,
      * provided (in bytes).
      *
-     * @throws NullPointerException     if the file is {@code null}
+     * @throws NullPointerException     if the file is {@code null}.
      * @throws IllegalArgumentException if the file does not exist.
      *
      * @since 2.0
      */
     public static long sizeOf(final File file) {
-        if (!file.exists()) {
-            final String message = file + " does not exist";
-            throw new IllegalArgumentException(message);
-        }
+        requireExists(file, "file");
         if (file.isDirectory()) {
             return sizeOfDirectory0(file); // private method; expects directory
         }
@@ -2628,16 +2639,13 @@ public class FileUtils {
      * @return the length of the file, or recursive size of the directory,
      * provided (in bytes).
      *
-     * @throws NullPointerException     if the file is {@code null}
+     * @throws NullPointerException     if the file is {@code null}.
      * @throws IllegalArgumentException if the file does not exist.
      *
      * @since 2.4
      */
     public static BigInteger sizeOfAsBigInteger(final File file) {
-        if (!file.exists()) {
-            final String message = file + " does not exist";
-            throw new IllegalArgumentException(message);
-        }
+        requireExists(file, "file");
         if (file.isDirectory()) {
             return sizeOfDirectoryBig0(file); // internal method
         }
@@ -2664,13 +2672,13 @@ public class FileUtils {
      * method that does not overflow.
      * </p>
      *
-     * @param directory directory to inspect, must not be {@code null}
+     * @param directory directory to inspect, must not be {@code null}.
      * @return size of directory in bytes, 0 if directory is security restricted, a negative number when the real total
      * is greater than {@link Long#MAX_VALUE}.
-     * @throws NullPointerException if the directory is {@code null}
+     * @throws NullPointerException if the directory is {@code null}.
      */
     public static long sizeOfDirectory(final File directory) {
-        return sizeOfDirectory0(checkDirectory(directory));
+        return sizeOfDirectory0(requireDirectory(directory, "directory"));
     }
 
     /**
@@ -2700,13 +2708,13 @@ public class FileUtils {
     /**
      * Counts the size of a directory recursively (sum of the length of all files).
      *
-     * @param directory directory to inspect, must not be {@code null}
+     * @param directory directory to inspect, must not be {@code null}.
      * @return size of directory in bytes, 0 if directory is security restricted.
-     * @throws NullPointerException if the directory is {@code null}
+     * @throws NullPointerException if the directory is {@code null}.
      * @since 2.4
      */
     public static BigInteger sizeOfDirectoryAsBigInteger(final File directory) {
-        return sizeOfDirectoryBig0(checkDirectory(directory));
+        return sizeOfDirectoryBig0(requireDirectory(directory, "directory"));
     }
 
     /**
@@ -2919,16 +2927,7 @@ public class FileUtils {
      * @throws IOException if an I/O error occurs
      */
     private static File[] verifiedListFiles(final File directory) throws IOException {
-        if (!directory.exists()) {
-            final String message = directory + " does not exist";
-            throw new IllegalArgumentException(message);
-        }
-
-        if (!directory.isDirectory()) {
-            final String message = directory + " is not a directory";
-            throw new IllegalArgumentException(message);
-        }
-
+        requireDirectory(directory, "directory");
         final File[] files = directory.listFiles();
         if (files == null) {  // null if security restricted
             throw new IOException("Failed to list contents of " + directory);
diff --git a/src/main/java/org/apache/commons/io/file/CleaningPathVisitor.java b/src/main/java/org/apache/commons/io/file/CleaningPathVisitor.java
index 9df929b..162ab8f 100644
--- a/src/main/java/org/apache/commons/io/file/CleaningPathVisitor.java
+++ b/src/main/java/org/apache/commons/io/file/CleaningPathVisitor.java
@@ -60,7 +60,7 @@ public class CleaningPathVisitor extends CountingPathVisitor {
      * Constructs a new visitor that deletes files except for the files and directories explicitly given.
      *
      * @param pathCounter How to count visits.
-     * @param deleteOption options indicating how deletion is handled.
+     * @param deleteOption How deletion is handled.
      * @param skip The files to skip deleting.
      * @since 2.8.0
      */
diff --git a/src/main/java/org/apache/commons/io/file/DeletingPathVisitor.java b/src/main/java/org/apache/commons/io/file/DeletingPathVisitor.java
index 6235b56..b6c85c5 100644
--- a/src/main/java/org/apache/commons/io/file/DeletingPathVisitor.java
+++ b/src/main/java/org/apache/commons/io/file/DeletingPathVisitor.java
@@ -61,18 +61,32 @@ public class DeletingPathVisitor extends CountingPathVisitor {
      * Constructs a new visitor that deletes files except for the files and directories explicitly given.
      *
      * @param pathCounter How to count visits.
-     * @param deleteOption options indicating how deletion is handled.
+     * @param deleteOption How deletion is handled.
      * @param skip The files to skip deleting.
      * @since 2.8.0
      */
     public DeletingPathVisitor(final PathCounters pathCounter, final DeleteOption[] deleteOption, final String... skip) {
+        this(pathCounter, PathUtils.NOFOLLOW_LINK_OPTION_ARRAY, deleteOption, skip);
+    }
+
+    /**
+     * Constructs a new visitor that deletes files except for the files and directories explicitly given.
+     *
+     * @param pathCounter How to count visits.
+     * @param linkOptions How symbolic links are handled.
+     * @param deleteOption How deletion is handled.
+     * @param skip The files to skip deleting.
+     * @since 2.9.0
+     */
+    public DeletingPathVisitor(final PathCounters pathCounter, final LinkOption[] linkOptions,
+        final DeleteOption[] deleteOption, final String... skip) {
         super(pathCounter);
         final String[] temp = skip != null ? skip.clone() : EMPTY_STRING_ARRAY;
         Arrays.sort(temp);
         this.skip = temp;
         this.overrideReadOnly = StandardDeleteOption.overrideReadOnly(deleteOption);
         // TODO Files.deleteIfExists() never follows links, so use LinkOption.NOFOLLOW_LINKS in other calls to Files.
-        this.linkOptions = PathUtils.NOFOLLOW_LINK_OPTION_ARRAY.clone();
+        this.linkOptions = linkOptions == null ? PathUtils.NOFOLLOW_LINK_OPTION_ARRAY : linkOptions.clone();
     }
 
     /**
diff --git a/src/main/java/org/apache/commons/io/file/PathUtils.java b/src/main/java/org/apache/commons/io/file/PathUtils.java
index 0ce9e6c..2ccd609 100644
--- a/src/main/java/org/apache/commons/io/file/PathUtils.java
+++ b/src/main/java/org/apache/commons/io/file/PathUtils.java
@@ -152,6 +152,13 @@ public final class PathUtils {
     public static final LinkOption[] EMPTY_LINK_OPTION_ARRAY = new LinkOption[0];
 
     /**
+     * {@link LinkOption} array for {@link LinkOption#NOFOLLOW_LINKS}.
+     * 
+     * @since 2.9.0
+     */
+    public static final LinkOption[] NOFOLLOW_LINK_OPTION_ARRAY = new LinkOption[] {LinkOption.NOFOLLOW_LINKS};
+
+    /**
      * Empty {@link OpenOption} array.
      */
     public static final OpenOption[] EMPTY_OPEN_OPTION_ARRAY = new OpenOption[0];
@@ -193,13 +200,14 @@ public final class PathUtils {
      * Cleans a directory including sub-directories without deleting directories.
      *
      * @param directory directory to clean.
-     * @param options options indicating how deletion is handled.
+     * @param deleteOptions How deletion is handled.
      * @return The visitation path counters.
      * @throws IOException if an I/O error is thrown by a visitor method.
      * @since 2.8.0
      */
-    public static PathCounters cleanDirectory(final Path directory, final DeleteOption... options) throws IOException {
-        return visitFileTree(new CleaningPathVisitor(Counters.longPathCounters(), options), directory)
+    public static PathCounters cleanDirectory(final Path directory, final DeleteOption... deleteOptions)
+        throws IOException {
+        return visitFileTree(new CleaningPathVisitor(Counters.longPathCounters(), deleteOptions), directory)
             .getPathCounters();
     }
 
@@ -347,16 +355,42 @@ public final class PathUtils {
      * </ul>
      *
      * @param path file or directory to delete, must not be {@code null}
-     * @param options options indicating how deletion is handled.
+     * @param deleteOptions How deletion is handled.
      * @return The visitor used to delete the given directory.
      * @throws NullPointerException if the directory is {@code null}
      * @throws IOException if an I/O error is thrown by a visitor method or if an I/O error occurs.
      * @since 2.8.0
      */
-    public static PathCounters delete(final Path path, final DeleteOption... options) throws IOException {
+    public static PathCounters delete(final Path path, final DeleteOption... deleteOptions) throws IOException {
+        // File deletion through Files deletes links, not targets, so use LinkOption.NOFOLLOW_LINKS.
+        return Files.isDirectory(path, LinkOption.NOFOLLOW_LINKS) ? deleteDirectory(path, deleteOptions)
+            : deleteFile(path, deleteOptions);
+    }
+
+    /**
+     * Deletes a file or directory. If the path is a directory, delete it and all sub-directories.
+     * <p>
+     * The difference between File.delete() and this method are:
+     * </p>
+     * <ul>
+     * <li>A directory to delete does not have to be empty.</li>
+     * <li>You get exceptions when a file or directory cannot be deleted; {@link java.io.File#delete()} returns a
+     * boolean.
+     * </ul>
+     *
+     * @param path file or directory to delete, must not be {@code null}
+     * @param linkOptions configures how symbolic links are handled.
+     * @param deleteOptions How deletion is handled.
+     * @return The visitor used to delete the given directory.
+     * @throws NullPointerException if the directory is {@code null}
+     * @throws IOException if an I/O error is thrown by a visitor method or if an I/O error occurs.
+     * @since 2.9.0
+     */
+    public static PathCounters delete(final Path path, final LinkOption[] linkOptions,
+        final DeleteOption... deleteOptions) throws IOException {
         // File deletion through Files deletes links, not targets, so use LinkOption.NOFOLLOW_LINKS.
-        return Files.isDirectory(path, LinkOption.NOFOLLOW_LINKS) ? deleteDirectory(path, options)
-            : deleteFile(path, options);
+        return Files.isDirectory(path, linkOptions) ? deleteDirectory(path, linkOptions, deleteOptions)
+            : deleteFile(path, linkOptions, deleteOptions);
     }
 
     /**
@@ -374,14 +408,32 @@ public final class PathUtils {
      * Deletes a directory including sub-directories.
      *
      * @param directory directory to delete.
-     * @param options options indicating how deletion is handled.
+     * @param deleteOptions How deletion is handled.
      * @return The visitor used to delete the given directory.
      * @throws IOException if an I/O error is thrown by a visitor method.
      * @since 2.8.0
      */
-    public static PathCounters deleteDirectory(final Path directory, final DeleteOption... options) throws IOException {
-        return visitFileTree(new DeletingPathVisitor(Counters.longPathCounters(), options), directory)
-            .getPathCounters();
+    public static PathCounters deleteDirectory(final Path directory, final DeleteOption... deleteOptions)
+        throws IOException {
+        return visitFileTree(
+            new DeletingPathVisitor(Counters.longPathCounters(), PathUtils.NOFOLLOW_LINK_OPTION_ARRAY, deleteOptions),
+            directory).getPathCounters();
+    }
+
+    /**
+     * Deletes a directory including sub-directories.
+     *
+     * @param directory directory to delete.
+     * @param linkOptions configures how symbolic links are handled.
+     * @param deleteOptions How deletion is handled.
+     * @return The visitor used to delete the given directory.
+     * @throws IOException if an I/O error is thrown by a visitor method.
+     * @since 2.9.0
+     */
+    public static PathCounters deleteDirectory(final Path directory, final LinkOption[] linkOptions,
+        final DeleteOption... deleteOptions) throws IOException {
+        return visitFileTree(new DeletingPathVisitor(Counters.longPathCounters(), linkOptions, deleteOptions),
+            directory).getPathCounters();
     }
 
     /**
@@ -400,22 +452,38 @@ public final class PathUtils {
      * Deletes the given file.
      *
      * @param file The file to delete.
-     * @param options options indicating how deletion is handled.
+     * @param deleteOptions How deletion is handled.
      * @return A visitor with path counts set to 1 file, 0 directories, and the size of the deleted file.
      * @throws IOException if an I/O error occurs.
      * @throws NoSuchFileException if the file is a directory.
      * @since 2.8.0
      */
-    public static PathCounters deleteFile(final Path file, final DeleteOption... options) throws IOException {
+    public static PathCounters deleteFile(final Path file, final DeleteOption... deleteOptions) throws IOException {
         // Files.deleteIfExists() never follows links, so use LinkOption.NOFOLLOW_LINKS in other calls to Files.
-        if (Files.isDirectory(file, LinkOption.NOFOLLOW_LINKS)) {
+        return deleteFile(file, NOFOLLOW_LINK_OPTION_ARRAY, deleteOptions);
+    }
+
+    /**
+     * Deletes the given file.
+     *
+     * @param file The file to delete.
+     * @param linkOptions configures how symbolic links are handled.
+     * @param deleteOptions How deletion is handled.
+     * @return A visitor with path counts set to 1 file, 0 directories, and the size of the deleted file.
+     * @throws IOException if an I/O error occurs.
+     * @throws NoSuchFileException if the file is a directory.
+     * @since 2.9.0
+     */
+    public static PathCounters deleteFile(final Path file, final LinkOption[] linkOptions,
+        final DeleteOption... deleteOptions) throws NoSuchFileException, IOException {
+        if (Files.isDirectory(file, linkOptions)) {
             throw new NoSuchFileException(file.toString());
         }
         final PathCounters pathCounts = Counters.longPathCounters();
-        final boolean exists = Files.exists(file, LinkOption.NOFOLLOW_LINKS);
+        final boolean exists = Files.exists(file, linkOptions);
         final long size = exists ? Files.size(file) : 0;
-        if (overrideReadOnly(options) && exists) {
-            setReadOnly(file, false, LinkOption.NOFOLLOW_LINKS);
+        if (overrideReadOnly(deleteOptions) && exists) {
+            setReadOnly(file, false, linkOptions);
         }
         if (Files.deleteIfExists(file)) {
             pathCounts.getFileCounter().increment();
@@ -727,14 +795,14 @@ public final class PathUtils {
     /**
      * Returns true if the given options contain {@link StandardDeleteOption#OVERRIDE_READ_ONLY}.
      *
-     * @param options the array to test
+     * @param deleteOptions the array to test
      * @return true if the given options contain {@link StandardDeleteOption#OVERRIDE_READ_ONLY}.
      */
-    private static boolean overrideReadOnly(final DeleteOption[] options) {
-        if (options == null) {
+    private static boolean overrideReadOnly(final DeleteOption... deleteOptions) {
+        if (deleteOptions == null) {
             return false;
         }
-        for (final DeleteOption deleteOption : options) {
+        for (final DeleteOption deleteOption : deleteOptions) {
             if (deleteOption == StandardDeleteOption.OVERRIDE_READ_ONLY) {
                 return true;
             }
@@ -797,21 +865,21 @@ public final class PathUtils {
      *
      * @param path The path to set.
      * @param readOnly true for read-only, false for not read-only.
-     * @param options options indicating how symbolic links are handled.
+     * @param linkOptions options indicating how symbolic links are handled.
      * @return The given path.
      * @throws IOException if an I/O error occurs.
      * @since 2.8.0
      */
-    public static Path setReadOnly(final Path path, final boolean readOnly, final LinkOption... options)
+    public static Path setReadOnly(final Path path, final boolean readOnly, final LinkOption... linkOptions)
         throws IOException {
         final DosFileAttributeView fileAttributeView = Files.getFileAttributeView(path, DosFileAttributeView.class,
-            options);
+            linkOptions);
         if (fileAttributeView != null) {
             fileAttributeView.setReadOnly(readOnly);
             return path;
         }
         final PosixFileAttributeView posixFileAttributeView = Files.getFileAttributeView(path,
-            PosixFileAttributeView.class, options);
+            PosixFileAttributeView.class, linkOptions);
         if (posixFileAttributeView != null) {
             // Works on Windows but not on Ubuntu:
             // Files.setAttribute(path, "unix:readonly", readOnly, options);