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:53 UTC

[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.

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);