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
The following commit(s) were added to refs/heads/master by this push:
new 8fe59e4 [IO-684] Add force version of PathUtils.delete() APIs and
friends.
8fe59e4 is described below
commit 8fe59e41e8f092b33923bcb9a281fbc228a2bf8f
Author: Gary Gregory <[email protected]>
AuthorDate: Fri Aug 14 21:40:59 2020 -0400
[IO-684] Add force version of PathUtils.delete() APIs and friends.
- Add PathUtils.delete(Path, DeleteOption...).
- Add PathUtils.deleteDirectory(Path, DeleteOption...).
- Add PathUtils.deleteFile(Path, DeleteOption...).
- Add PathUtils.setReadOnly(Path, boolean, LinkOption...).
- Add CleaningPathVisitor.CleaningPathVisitor(PathCounters,
DeleteOption[], String...).
- Add DeletingPathVisitor.DeletingPathVisitor(PathCounters,
DeleteOption[], String...).
Since the NIO APIs in Files class are parameterized with interfaces
implemented by enums (e.g. StandardCopyOption implements CopyOption),
the new PathUtils APIs here similarly work with a new enum called
StandardDeleteOption that implements a new DeleteOption interface. This
allow the API signature to remain stable while additional features can
be provided by new enums or adding values to StandardDeleteOption.
---
src/changes/changes.xml | 8 +
src/main/java/org/apache/commons/io/FileUtils.java | 5 +-
.../commons/io/file/CleaningPathVisitor.java | 22 ++-
.../org/apache/commons/io/file/DeleteOption.java | 32 ++++
.../commons/io/file/DeletingPathVisitor.java | 21 ++-
.../java/org/apache/commons/io/file/PathUtils.java | 199 ++++++++++++++++-----
.../commons/io/file/StandardDeleteOption.java | 52 ++++++
.../io/FileUtilsCleanDirectoryTestCase.java | 12 +-
.../io/file/PathUtilsDeleteDirectoryTest.java | 22 +++
.../commons/io/file/PathUtilsDeleteFileTest.java | 50 +++++-
.../commons/io/file/PathUtilsDeleteTest.java | 120 ++++++++++++-
11 files changed, 474 insertions(+), 69 deletions(-)
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index fda50bf..1a6c1c5 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -95,6 +95,14 @@ The <action> type attribute can be add,update,fix,remove.
<action dev="ggregory" type="add" due-to="Gary Gregory">
Add ReversedLinesFileReader.readLines(int).
</action>
+ <action issue="IO-684" dev="ggregory" type="add" due-to="Gary Gregory,
Robin Jansohn">
+ Add PathUtils.delete(Path, DeleteOption...).
+ Add PathUtils.deleteDirectory(Path, DeleteOption...).
+ Add PathUtils.deleteFile(Path, DeleteOption...).
+ Add PathUtils.setReadOnly(Path, boolean, LinkOption...).
+ Add CleaningPathVisitor.CleaningPathVisitor(PathCounters,
DeleteOption[], String...).
+ Add DeletingPathVisitor.DeletingPathVisitor(PathCounters,
DeleteOption[], String...).
+ </action>
<action dev="ggregory" type="fix" due-to="Gary Gregory">
Replace FindBugs with SpotBugs.
</action>
diff --git a/src/main/java/org/apache/commons/io/FileUtils.java
b/src/main/java/org/apache/commons/io/FileUtils.java
index 3bcd996..8cf6607 100644
--- a/src/main/java/org/apache/commons/io/FileUtils.java
+++ b/src/main/java/org/apache/commons/io/FileUtils.java
@@ -49,6 +49,7 @@ import java.util.zip.Checksum;
import org.apache.commons.io.file.Counters;
import org.apache.commons.io.file.PathUtils;
+import org.apache.commons.io.file.StandardDeleteOption;
import org.apache.commons.io.filefilter.DirectoryFileFilter;
import org.apache.commons.io.filefilter.FalseFileFilter;
import org.apache.commons.io.filefilter.FileFilterUtils;
@@ -325,6 +326,7 @@ public class FileUtils {
* @param directory directory to clean
* @throws IOException in case cleaning is unsuccessful
* @throws IllegalArgumentException if {@code directory} does not exist or
is not a directory
+ * @see #forceDelete(File)
*/
public static void cleanDirectory(final File directory) throws IOException
{
final File[] files = verifiedListFiles(directory);
@@ -349,6 +351,7 @@ public class FileUtils {
* @param directory directory to clean, must not be {@code null}
* @throws NullPointerException if the directory is {@code null}
* @throws IOException in case cleaning is unsuccessful
+ * @see #forceDeleteOnExit(File)
*/
private static void cleanDirectoryOnExit(final File directory) throws
IOException {
final File[] files = verifiedListFiles(directory);
@@ -1437,7 +1440,7 @@ public class FileUtils {
public static void forceDelete(final File file) throws IOException {
final Counters.PathCounters deleteCounters;
try {
- deleteCounters = PathUtils.delete(file.toPath());
+ deleteCounters = PathUtils.delete(file.toPath(),
StandardDeleteOption.OVERRIDE_READ_ONLY);
} catch (IOException e) {
throw new IOException("Unable to delete file: " + file, e);
}
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 f56ee7d..80546c0 100644
--- a/src/main/java/org/apache/commons/io/file/CleaningPathVisitor.java
+++ b/src/main/java/org/apache/commons/io/file/CleaningPathVisitor.java
@@ -20,6 +20,7 @@ package org.apache.commons.io.file;
import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
+import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Arrays;
@@ -53,6 +54,7 @@ public class CleaningPathVisitor extends CountingPathVisitor {
}
private final String[] skip;
+ private final boolean overrideReadOnly;
/**
* Constructs a new visitor that deletes files except for the files and
directories explicitly given.
@@ -61,10 +63,24 @@ public class CleaningPathVisitor extends
CountingPathVisitor {
* @param skip The files to skip deleting.
*/
public CleaningPathVisitor(final PathCounters pathCounter, final String...
skip) {
+ this(pathCounter, PathUtils.EMPTY_DELETE_OPTION_ARRAY, skip);
+ }
+
+ /**
+ * 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 skip The files to skip deleting.
+ * @since 2.8.0
+ */
+ public CleaningPathVisitor(final PathCounters pathCounter, 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);
}
/**
@@ -85,7 +101,11 @@ public class CleaningPathVisitor extends
CountingPathVisitor {
@Override
public FileVisitResult visitFile(final Path file, final
BasicFileAttributes attributes) throws IOException {
- if (accept(file) && Files.exists(file)) {
+ // 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 (overrideReadOnly) {
+ PathUtils.setReadOnly(file, false, LinkOption.NOFOLLOW_LINKS);
+ }
Files.deleteIfExists(file);
}
updateFileCounters(file, attributes);
diff --git a/src/main/java/org/apache/commons/io/file/DeleteOption.java
b/src/main/java/org/apache/commons/io/file/DeleteOption.java
new file mode 100644
index 0000000..3fe21ad
--- /dev/null
+++ b/src/main/java/org/apache/commons/io/file/DeleteOption.java
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.io.file;
+
+/**
+ * An object that configures how to delete a file.
+ *
+ * <p>
+ * The {@link StandardDeleteOption} enumeration type defines our standard
options.
+ * </p>
+ *
+ * @see StandardDeleteOption
+ * @since 2.8.0
+ */
+public interface DeleteOption {
+ // empty
+}
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 2d6405d..7437650 100644
--- a/src/main/java/org/apache/commons/io/file/DeletingPathVisitor.java
+++ b/src/main/java/org/apache/commons/io/file/DeletingPathVisitor.java
@@ -20,6 +20,7 @@ package org.apache.commons.io.file;
import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
+import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Arrays;
@@ -54,6 +55,7 @@ public class DeletingPathVisitor extends CountingPathVisitor {
}
private final String[] skip;
+ private final boolean overrideReadOnly;
/**
* Constructs a new visitor that deletes files except for the files and
directories explicitly given.
@@ -63,10 +65,23 @@ public class DeletingPathVisitor extends
CountingPathVisitor {
* @param skip The files to skip deleting.
*/
public DeletingPathVisitor(final PathCounters pathCounter, final String...
skip) {
+ this(pathCounter, PathUtils.EMPTY_DELETE_OPTION_ARRAY, skip);
+ }
+
+ /**
+ * 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 skip The files to skip deleting.
+ * @since 2.8.0
+ */
+ public DeletingPathVisitor(final PathCounters pathCounter, 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);
}
/**
@@ -95,7 +110,11 @@ public class DeletingPathVisitor extends
CountingPathVisitor {
@Override
public FileVisitResult visitFile(final Path file, final
BasicFileAttributes attrs) throws IOException {
- if (accept(file) && Files.exists(file)) {
+ // 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 (overrideReadOnly) {
+ PathUtils.setReadOnly(file, false, LinkOption.NOFOLLOW_LINKS);
+ }
Files.deleteIfExists(file);
}
updateFileCounters(file, attrs);
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 6f5a8c3..d370ef0 100644
--- a/src/main/java/org/apache/commons/io/file/PathUtils.java
+++ b/src/main/java/org/apache/commons/io/file/PathUtils.java
@@ -27,12 +27,16 @@ import java.nio.file.FileVisitOption;
import java.nio.file.FileVisitor;
import java.nio.file.Files;
import java.nio.file.LinkOption;
-import java.nio.file.NotDirectoryException;
+import java.nio.file.NoSuchFileException;
import java.nio.file.OpenOption;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.AclEntry;
import java.nio.file.attribute.AclFileAttributeView;
+import java.nio.file.attribute.DosFileAttributeView;
+import java.nio.file.attribute.PosixFileAttributeView;
+import java.nio.file.attribute.PosixFileAttributes;
+import java.nio.file.attribute.PosixFilePermission;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
@@ -76,7 +80,7 @@ public final class PathUtils {
* @throws IOException if an I/O error is thrown by a visitor method.
*/
private RelativeSortedPaths(final Path dir1, final Path dir2, final
int maxDepth,
- final LinkOption[] linkOptions, final FileVisitOption[]
fileVisitOptions) throws IOException {
+ final LinkOption[] linkOptions, final FileVisitOption[]
fileVisitOptions) throws IOException {
List<Path> tmpRelativeDirList1 = null;
List<Path> tmpRelativeDirList2 = null;
List<Path> tmpRelativeFileList1 = null;
@@ -91,10 +95,10 @@ public final class PathUtils {
if (!parentDirExists1 || !parentDirExists2) {
equals = !parentDirExists1 && !parentDirExists2;
} else {
- AccumulatorPathVisitor visitor1 = accumulate(dir1,
maxDepth, fileVisitOptions);
- AccumulatorPathVisitor visitor2 = accumulate(dir2,
maxDepth, fileVisitOptions);
+ final AccumulatorPathVisitor visitor1 = accumulate(dir1,
maxDepth, fileVisitOptions);
+ final AccumulatorPathVisitor visitor2 = accumulate(dir2,
maxDepth, fileVisitOptions);
if (visitor1.getDirList().size() !=
visitor2.getDirList().size()
- || visitor1.getFileList().size() !=
visitor2.getFileList().size()) {
+ || visitor1.getFileList().size() !=
visitor2.getFileList().size()) {
equals = false;
} else {
tmpRelativeDirList1 =
visitor1.relativizeDirectories(dir1, true, null);
@@ -117,6 +121,13 @@ public final class PathUtils {
}
/**
+ * Empty {@link LinkOption} array.
+ *
+ * @since 2.8.0
+ */
+ public static final DeleteOption[] EMPTY_DELETE_OPTION_ARRAY = new
DeleteOption[0];
+
+ /**
* Empty {@link FileVisitOption} array.
*/
public static final FileVisitOption[] EMPTY_FILE_VISIT_OPTION_ARRAY = new
FileVisitOption[0];
@@ -141,9 +152,9 @@ public final class PathUtils {
* @return file tree information.
*/
private static AccumulatorPathVisitor accumulate(final Path directory,
final int maxDepth,
- final FileVisitOption[] fileVisitOptions) throws IOException {
+ final FileVisitOption[] fileVisitOptions) throws IOException {
return visitFileTree(AccumulatorPathVisitor.withLongCounters(),
directory,
- toFileVisitOptionSet(fileVisitOptions), maxDepth);
+ toFileVisitOptionSet(fileVisitOptions), maxDepth);
}
/**
@@ -154,22 +165,21 @@ public final class PathUtils {
* @throws IOException if an I/O error is thrown by a visitor method.
*/
public static PathCounters cleanDirectory(final Path directory) throws
IOException {
- return cleanDirectory(directory, FileVisitOption.FOLLOW_LINKS);
+ return cleanDirectory(directory, EMPTY_DELETE_OPTION_ARRAY);
}
/**
* Cleans a directory including sub-directories without deleting
directories.
*
* @param directory directory to clean.
- * @param fileVisitOptions See {@link
Files#walkFileTree(Path,Set,int,FileVisitor)}.
+ * @param options options indicating 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
FileVisitOption... fileVisitOptions)
- throws IOException {
- return visitFileTree(CleaningPathVisitor.withLongCounters(),
directory, toFileVisitOptionSet(fileVisitOptions),
- Integer.MAX_VALUE).getPathCounters();
+ public static PathCounters cleanDirectory(final Path directory, final
DeleteOption... options) throws IOException {
+ return visitFileTree(new
CleaningPathVisitor(Counters.longPathCounters(), options), directory)
+ .getPathCounters();
}
/**
@@ -182,10 +192,10 @@ public final class PathUtils {
* @throws IOException if an I/O error is thrown by a visitor method.
*/
public static PathCounters copyDirectory(final Path sourceDirectory, final
Path targetDirectory,
- final CopyOption... copyOptions) throws IOException {
+ final CopyOption... copyOptions) throws IOException {
return visitFileTree(
- new CopyDirectoryVisitor(Counters.longPathCounters(),
sourceDirectory, targetDirectory, copyOptions),
- sourceDirectory).getPathCounters();
+ new CopyDirectoryVisitor(Counters.longPathCounters(),
sourceDirectory, targetDirectory, copyOptions),
+ sourceDirectory).getPathCounters();
}
/**
@@ -199,7 +209,7 @@ public final class PathUtils {
* @see Files#copy(InputStream, Path, CopyOption...)
*/
public static Path copyFile(final URL sourceFile, final Path targetFile,
final CopyOption... copyOptions)
- throws IOException {
+ throws IOException {
try (final InputStream inputStream = sourceFile.openStream()) {
Files.copy(inputStream, targetFile, copyOptions);
return targetFile;
@@ -217,7 +227,7 @@ public final class PathUtils {
* @see Files#copy(Path, Path, CopyOption...)
*/
public static Path copyFileToDirectory(final Path sourceFile, final Path
targetDirectory,
- final CopyOption... copyOptions) throws IOException {
+ final CopyOption... copyOptions) throws IOException {
return Files.copy(sourceFile,
targetDirectory.resolve(sourceFile.getFileName()), copyOptions);
}
@@ -232,7 +242,7 @@ public final class PathUtils {
* @see Files#copy(InputStream, Path, CopyOption...)
*/
public static Path copyFileToDirectory(final URL sourceFile, final Path
targetDirectory,
- final CopyOption... copyOptions) throws IOException {
+ final CopyOption... copyOptions) throws IOException {
try (final InputStream inputStream = sourceFile.openStream()) {
Files.copy(inputStream,
targetDirectory.resolve(sourceFile.getFile()), copyOptions);
return targetDirectory;
@@ -267,7 +277,31 @@ public final class PathUtils {
* @throws IOException if an I/O error is thrown by a visitor method or if
an I/O error occurs.
*/
public static PathCounters delete(final Path path) throws IOException {
- return Files.isDirectory(path) ? deleteDirectory(path) :
deleteFile(path);
+ return delete(path, EMPTY_DELETE_OPTION_ARRAY);
+ }
+
+ /**
+ * 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 options options indicating 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 {
+ // 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);
}
/**
@@ -278,20 +312,21 @@ public final class PathUtils {
* @throws IOException if an I/O error is thrown by a visitor method.
*/
public static PathCounters deleteDirectory(final Path directory) throws
IOException {
- return deleteDirectory(directory, FileVisitOption.FOLLOW_LINKS);
+ return deleteDirectory(directory, EMPTY_DELETE_OPTION_ARRAY);
}
/**
* Deletes a directory including sub-directories.
*
* @param directory directory to delete.
- * @param fileVisitOptions See {@link
Files#walkFileTree(Path,Set,int,FileVisitor)}.
+ * @param options options indicating 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
FileVisitOption... fileVisitOptions) throws IOException {
- return visitFileTree(DeletingPathVisitor.withLongCounters(),
directory).getPathCounters();
+ public static PathCounters deleteDirectory(final Path directory, final
DeleteOption... options) throws IOException {
+ return visitFileTree(new
DeletingPathVisitor(Counters.longPathCounters(), options), directory)
+ .getPathCounters();
}
/**
@@ -300,14 +335,33 @@ public final class PathUtils {
* @param file The file to delete.
* @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 NotDirectoryException if the file is a directory.
+ * @throws NoSuchFileException if the file is a directory.
*/
public static PathCounters deleteFile(final Path file) throws IOException {
- if (Files.isDirectory(file)) {
- throw new NotDirectoryException(file.toString());
+ return deleteFile(file, EMPTY_DELETE_OPTION_ARRAY);
+ }
+
+ /**
+ * Deletes the given file.
+ *
+ * @param file The file to delete.
+ * @param options options indicating 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 {
+ // Files.deleteIfExists() never follows links, so use
LinkOption.NOFOLLOW_LINKS in other calls to Files.
+ if (Files.isDirectory(file, LinkOption.NOFOLLOW_LINKS)) {
+ throw new NoSuchFileException(file.toString());
}
final PathCounters pathCounts = Counters.longPathCounters();
- final long size = Files.exists(file) ? Files.size(file) : 0;
+ final boolean exists = Files.exists(file, LinkOption.NOFOLLOW_LINKS);
+ final long size = exists ? Files.size(file) : 0;
+ if (overrideReadOnly(options) && exists) {
+ setReadOnly(file, false, LinkOption.NOFOLLOW_LINKS);
+ }
if (Files.deleteIfExists(file)) {
pathCounts.getFileCounter().increment();
pathCounts.getByteCounter().add(size);
@@ -316,6 +370,24 @@ public final class PathUtils {
}
/**
+ * Returns true if the given options contain {@link
StandardDeleteOption#OVERRIDE_READ_ONLY}.
+ *
+ * @param options 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) {
+ return false;
+ }
+ for (final DeleteOption deleteOption : options) {
+ if (deleteOption == StandardDeleteOption.OVERRIDE_READ_ONLY) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
* Compares the file sets of two Paths to determine if they are equal or
not while considering file contents. The
* comparison includes all files in all sub-directories.
*
@@ -326,7 +398,7 @@ public final class PathUtils {
*/
public static boolean directoryAndFileContentEquals(final Path path1,
final Path path2) throws IOException {
return directoryAndFileContentEquals(path1, path2,
EMPTY_LINK_OPTION_ARRAY, EMPTY_OPEN_OPTION_ARRAY,
- EMPTY_FILE_VISIT_OPTION_ARRAY);
+ EMPTY_FILE_VISIT_OPTION_ARRAY);
}
/**
@@ -342,8 +414,8 @@ public final class PathUtils {
* @throws IOException if an I/O error is thrown by a visitor method
*/
public static boolean directoryAndFileContentEquals(final Path path1,
final Path path2,
- final LinkOption[] linkOptions, final OpenOption[] openOptions,
final FileVisitOption[] fileVisitOption)
- throws IOException {
+ final LinkOption[] linkOptions, final OpenOption[] openOptions, final
FileVisitOption[] fileVisitOption)
+ throws IOException {
// First walk both file trees and gather normalized paths.
if (path1 == null && path2 == null) {
return true;
@@ -355,7 +427,7 @@ public final class PathUtils {
return true;
}
final RelativeSortedPaths relativeSortedPaths = new
RelativeSortedPaths(path1, path2, Integer.MAX_VALUE,
- linkOptions, fileVisitOption);
+ linkOptions, fileVisitOption);
// If the normalized path names and counts are not the same, no need
to compare contents.
if (!relativeSortedPaths.equals) {
return false;
@@ -363,7 +435,7 @@ public final class PathUtils {
// Both visitors contain the same normalized paths, we can compare
file contents.
final List<Path> fileList1 = relativeSortedPaths.relativeFileList1;
final List<Path> fileList2 = relativeSortedPaths.relativeFileList2;
- for (Path path : fileList1) {
+ for (final Path path : fileList1) {
final int binarySearch = Collections.binarySearch(fileList2, path);
if (binarySearch > -1) {
if (!fileContentEquals(path1.resolve(path),
path2.resolve(path), linkOptions, openOptions)) {
@@ -387,7 +459,7 @@ public final class PathUtils {
*/
public static boolean directoryContentEquals(final Path path1, final Path
path2) throws IOException {
return directoryContentEquals(path1, path2, Integer.MAX_VALUE,
EMPTY_LINK_OPTION_ARRAY,
- EMPTY_FILE_VISIT_OPTION_ARRAY);
+ EMPTY_FILE_VISIT_OPTION_ARRAY);
}
/**
@@ -403,7 +475,7 @@ public final class PathUtils {
* @throws IOException if an I/O error is thrown by a visitor method
*/
public static boolean directoryContentEquals(final Path path1, final Path
path2, final int maxDepth,
- LinkOption[] linkOptions, FileVisitOption[] fileVisitOptions)
throws IOException {
+ final LinkOption[] linkOptions, final FileVisitOption[]
fileVisitOptions) throws IOException {
return new RelativeSortedPaths(path1, path2, maxDepth, linkOptions,
fileVisitOptions).equals;
}
@@ -440,7 +512,7 @@ public final class PathUtils {
* @see org.apache.commons.io.FileUtils#contentEquals(java.io.File,
java.io.File)
*/
public static boolean fileContentEquals(final Path path1, final Path
path2, final LinkOption[] linkOptions,
- final OpenOption[] openOptions) throws IOException {
+ final OpenOption[] openOptions) throws IOException {
if (path1 == null && path2 == null) {
return true;
}
@@ -475,7 +547,7 @@ public final class PathUtils {
return true;
}
try (final InputStream inputStream1 = Files.newInputStream(nPath1,
openOptions);
- final InputStream inputStream2 = Files.newInputStream(nPath2,
openOptions)) {
+ final InputStream inputStream2 = Files.newInputStream(nPath2,
openOptions)) {
return IOUtils.contentEquals(inputStream1, inputStream2);
}
}
@@ -541,8 +613,8 @@ public final class PathUtils {
* @param comparator How to sort.
* @return A collection of relativized paths, optionally sorted.
*/
- static List<Path> relativize(Collection<Path> collection, Path parent,
boolean sort,
- Comparator<? super Path> comparator) {
+ static List<Path> relativize(final Collection<Path> collection, final Path
parent, final boolean sort,
+ final Comparator<? super Path> comparator) {
Stream<Path> stream = collection.stream().map(parent::relativize);
if (sort) {
stream = comparator == null ? stream.sorted() :
stream.sorted(comparator);
@@ -551,6 +623,43 @@ public final class PathUtils {
}
/**
+ * Sets the given Path to the {@code readOnly} value.
+ * <p>
+ * This behavior is OS dependent.
+ * </p>
+ *
+ * @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.
+ * @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)
+ throws IOException {
+ final DosFileAttributeView fileAttributeView =
Files.getFileAttributeView(path, DosFileAttributeView.class,
+ options);
+ if (fileAttributeView != null) {
+ fileAttributeView.setReadOnly(readOnly);
+ return path;
+ }
+ final PosixFileAttributeView posixFileAttributeView =
Files.getFileAttributeView(path,
+ PosixFileAttributeView.class, options);
+ if (posixFileAttributeView != null) {
+ // Works on Windows but not on Ubuntu:
+ // Files.setAttribute(path, "unix:readonly", readOnly, options);
+ // java.lang.IllegalArgumentException: 'unix:readonly' not
recognized
+ final PosixFileAttributes readAttributes =
posixFileAttributeView.readAttributes();
+ final Set<PosixFilePermission> permissions =
readAttributes.permissions();
+ permissions.remove(PosixFilePermission.OWNER_WRITE);
+ permissions.remove(PosixFilePermission.GROUP_WRITE);
+ permissions.remove(PosixFilePermission.OTHERS_WRITE);
+ return Files.setPosixFilePermissions(path, permissions);
+ }
+ throw new IOException("No DosFileAttributeView or
PosixFileAttributeView for " + path);
+ }
+
+ /**
* Converts an array of {@link FileVisitOption} to a {@link Set}.
*
* @param fileVisitOptions input array.
@@ -558,7 +667,7 @@ public final class PathUtils {
*/
static Set<FileVisitOption> toFileVisitOptionSet(final FileVisitOption...
fileVisitOptions) {
return fileVisitOptions == null ? EnumSet.noneOf(FileVisitOption.class)
- : Arrays.stream(fileVisitOptions).collect(Collectors.toSet());
+ : Arrays.stream(fileVisitOptions).collect(Collectors.toSet());
}
/**
@@ -574,7 +683,7 @@ public final class PathUtils {
* @throws IOException if an I/O error is thrown by a visitor method
*/
public static <T extends FileVisitor<? super Path>> T visitFileTree(final
T visitor, final Path directory)
- throws IOException {
+ throws IOException {
Files.walkFileTree(directory, visitor);
return visitor;
}
@@ -593,8 +702,8 @@ public final class PathUtils {
*
* @throws IOException if an I/O error is thrown by a visitor method
*/
- public static <T extends FileVisitor<? super Path>> T visitFileTree(T
visitor, Path start,
- Set<FileVisitOption> options, int maxDepth) throws IOException {
+ public static <T extends FileVisitor<? super Path>> T visitFileTree(final
T visitor, final Path start,
+ final Set<FileVisitOption> options, final int maxDepth) throws
IOException {
Files.walkFileTree(start, options, maxDepth, visitor);
return visitor;
}
@@ -613,7 +722,7 @@ public final class PathUtils {
* @throws IOException if an I/O error is thrown by a visitor method
*/
public static <T extends FileVisitor<? super Path>> T visitFileTree(final
T visitor, final String first,
- final String... more) throws IOException {
+ final String... more) throws IOException {
return visitFileTree(visitor, Paths.get(first, more));
}
@@ -630,7 +739,7 @@ public final class PathUtils {
* @throws IOException if an I/O error is thrown by a visitor method
*/
public static <T extends FileVisitor<? super Path>> T visitFileTree(final
T visitor, final URI uri)
- throws IOException {
+ throws IOException {
return visitFileTree(visitor, Paths.get(uri));
}
diff --git a/src/main/java/org/apache/commons/io/file/StandardDeleteOption.java
b/src/main/java/org/apache/commons/io/file/StandardDeleteOption.java
new file mode 100644
index 0000000..e7343cb
--- /dev/null
+++ b/src/main/java/org/apache/commons/io/file/StandardDeleteOption.java
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.io.file;
+
+/**
+ * Defines the standard delete options.
+ *
+ * @since 2.8.0
+ */
+public enum StandardDeleteOption implements DeleteOption {
+
+ /**
+ * Overrides the read-only attribute to allow deletion.
+ */
+ OVERRIDE_READ_ONLY;
+
+ /**
+ * Returns true if the given options contain {@link
StandardDeleteOption#OVERRIDE_READ_ONLY}.
+ *
+ * For now, assume the array is not sorted.
+ *
+ * @param options the array to test
+ * @return true if the given options contain {@link
StandardDeleteOption#OVERRIDE_READ_ONLY}.
+ */
+ public static boolean overrideReadOnly(final DeleteOption[] options) {
+ if (options == null || options.length == 0) {
+ return false;
+ }
+ for (final DeleteOption deleteOption : options) {
+ if (deleteOption == StandardDeleteOption.OVERRIDE_READ_ONLY) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+}
diff --git
a/src/test/java/org/apache/commons/io/FileUtilsCleanDirectoryTestCase.java
b/src/test/java/org/apache/commons/io/FileUtilsCleanDirectoryTestCase.java
index 2586da3..6abee2d 100644
--- a/src/test/java/org/apache/commons/io/FileUtilsCleanDirectoryTestCase.java
+++ b/src/test/java/org/apache/commons/io/FileUtilsCleanDirectoryTestCase.java
@@ -85,11 +85,8 @@ public class FileUtilsCleanDirectoryTestCase {
assumeTrue(chmod(top, 0, false));
try {
+ // cleanDirectory calls forceDelete
FileUtils.cleanDirectory(top);
- fail("expected IOException");
- } catch (final IOException e) {
- assertEquals("Failed to list contents of " +
- top.getAbsolutePath(), e.getMessage());
} finally {
chmod(top, 755, false);
}
@@ -104,19 +101,16 @@ public class FileUtilsCleanDirectoryTestCase {
assumeTrue(chmod(top, 500, false));
try {
+ // cleanDirectory calls forceDelete
FileUtils.cleanDirectory(top);
- fail("expected IOException");
- } catch (final IOException e) {
- assertEquals("Unable to delete file: " +
- file.getAbsolutePath(), e.getMessage());
} finally {
chmod(top, 755, false);
}
}
+ /** Only runs on Linux. */
private boolean chmod(final File file, final int mode, final boolean
recurse)
throws InterruptedException {
- // TODO: Refactor this to FileSystemUtils
final List<String> args = new ArrayList<>();
args.add("chmod");
diff --git
a/src/test/java/org/apache/commons/io/file/PathUtilsDeleteDirectoryTest.java
b/src/test/java/org/apache/commons/io/file/PathUtilsDeleteDirectoryTest.java
index 8caf5ec..f126f0a 100644
--- a/src/test/java/org/apache/commons/io/file/PathUtilsDeleteDirectoryTest.java
+++ b/src/test/java/org/apache/commons/io/file/PathUtilsDeleteDirectoryTest.java
@@ -20,6 +20,7 @@ package org.apache.commons.io.file;
import static org.apache.commons.io.file.CounterAssertions.assertCounts;
import java.io.IOException;
+import java.nio.file.FileVisitOption;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@@ -60,6 +61,27 @@ public class PathUtilsDeleteDirectoryTest {
}
/**
+ * Tests a directory with one file of size 0.
+ */
+ private void testDeleteDirectory1FileSize0(final DeleteOption... options)
throws IOException {
+ // TODO Setup the test to use FileVisitOption.
+
PathUtils.copyDirectory(Paths.get("src/test/resources/org/apache/commons/io/dirs-1-file-size-0"),
tempDir);
+ assertCounts(1, 1, 0, PathUtils.deleteDirectory(tempDir, options));
+ // This will throw if not empty.
+ Files.deleteIfExists(tempDir);
+ }
+
+ @Test
+ public void testDeleteDirectory1FileSize0NoOptions() throws IOException {
+ testDeleteDirectory1FileSize0(PathUtils.EMPTY_DELETE_OPTION_ARRAY);
+ }
+
+ @Test
+ public void testDeleteDirectory1FileSize0OverrideReadOnly() throws
IOException {
+ testDeleteDirectory1FileSize0(StandardDeleteOption.OVERRIDE_READ_ONLY);
+ }
+
+ /**
* Tests a directory with one file of size 1.
*/
@Test
diff --git
a/src/test/java/org/apache/commons/io/file/PathUtilsDeleteFileTest.java
b/src/test/java/org/apache/commons/io/file/PathUtilsDeleteFileTest.java
index 32710f9..fa2375d 100644
--- a/src/test/java/org/apache/commons/io/file/PathUtilsDeleteFileTest.java
+++ b/src/test/java/org/apache/commons/io/file/PathUtilsDeleteFileTest.java
@@ -18,14 +18,15 @@
package org.apache.commons.io.file;
import static org.apache.commons.io.file.CounterAssertions.assertCounts;
-
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
import java.io.IOException;
import java.nio.file.Files;
-import java.nio.file.NotDirectoryException;
+import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.Paths;
-
import org.apache.commons.io.file.Counters.PathCounters;
+import org.apache.commons.lang3.SystemUtils;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
@@ -96,7 +97,48 @@ public class PathUtilsDeleteFileTest {
*/
@Test
public void testDeleteFileEmptyDirectory() throws IOException {
- Assertions.assertThrows(NotDirectoryException.class, () ->
testDeleteFileEmpty(PathUtils.deleteFile(tempDir)));
+ Assertions.assertThrows(NoSuchFileException.class, () ->
testDeleteFileEmpty(PathUtils.deleteFile(tempDir)));
+ // This will throw if not empty.
+ Files.deleteIfExists(tempDir);
+ }
+
+ /**
+ * Tests a directory with one file of size 1.
+ */
+ @Test
+ public void testDeleteReadOnlyFileDirectory1FileSize1() throws IOException
{
+ final String fileName = "file-size-1.bin";
+ PathUtils.copyFileToDirectory(
+
Paths.get("src/test/resources/org/apache/commons/io/dirs-1-file-size-1/" +
fileName), tempDir);
+ final Path resolved = tempDir.resolve(fileName);
+ PathUtils.setReadOnly(resolved, true);
+ if (SystemUtils.IS_OS_WINDOWS) {
+ // Fails on Windows's Ubuntu subsystem.
+ assertFalse(Files.isWritable(resolved));
+ assertThrows(IOException.class, () ->
PathUtils.deleteFile(resolved));
+ }
+ assertCounts(0, 1, 1, PathUtils.deleteFile(resolved,
StandardDeleteOption.OVERRIDE_READ_ONLY));
+ // This will throw if not empty.
+ Files.deleteIfExists(tempDir);
+ }
+
+ /**
+ * Tests a directory with one file of size 1.
+ */
+ @Test
+ public void testSetReadOnlyFileDirectory1FileSize1() throws IOException {
+ final String fileName = "file-size-1.bin";
+ PathUtils.copyFileToDirectory(
+
Paths.get("src/test/resources/org/apache/commons/io/dirs-1-file-size-1/" +
fileName), tempDir);
+ final Path resolved = tempDir.resolve(fileName);
+ PathUtils.setReadOnly(resolved, true);
+ if (SystemUtils.IS_OS_WINDOWS) {
+ // Fails on Windows's Ubuntu subsystem.
+ assertFalse(Files.isWritable(resolved));
+ assertThrows(IOException.class, () ->
PathUtils.deleteFile(resolved));
+ }
+ PathUtils.setReadOnly(resolved, false);
+ PathUtils.deleteFile(resolved);
// This will throw if not empty.
Files.deleteIfExists(tempDir);
}
diff --git a/src/test/java/org/apache/commons/io/file/PathUtilsDeleteTest.java
b/src/test/java/org/apache/commons/io/file/PathUtilsDeleteTest.java
index ef56b72..b509a49 100644
--- a/src/test/java/org/apache/commons/io/file/PathUtilsDeleteTest.java
+++ b/src/test/java/org/apache/commons/io/file/PathUtilsDeleteTest.java
@@ -19,8 +19,10 @@ package org.apache.commons.io.file;
import static org.apache.commons.io.file.CounterAssertions.assertCounts;
+import java.awt.geom.PathIterator;
import java.io.IOException;
import java.nio.file.Files;
+import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.Paths;
@@ -50,34 +52,101 @@ public class PathUtilsDeleteTest {
tempDir = Files.createTempDirectory(getClass().getCanonicalName());
}
- /**
- * Tests a directory with one file of size 0.
- */
@Test
public void testDeleteDirectory1FileSize0() throws IOException {
final String fileName = "file-size-0.bin";
FileUtils.copyFileToDirectory(
-
Paths.get("src/test/resources/org/apache/commons/io/dirs-1-file-size-0/" +
fileName).toFile(),
- tempDir.toFile());
+
Paths.get("src/test/resources/org/apache/commons/io/dirs-1-file-size-0/" +
fileName).toFile(),
+ tempDir.toFile());
assertCounts(0, 1, 0, PathUtils.delete(tempDir.resolve(fileName)));
// This will throw if not empty.
Files.deleteIfExists(tempDir);
}
+ private void testDeleteDirectory1FileSize0(final DeleteOption... options)
throws IOException {
+ final String fileName = "file-size-0.bin";
+ FileUtils.copyFileToDirectory(
+
Paths.get("src/test/resources/org/apache/commons/io/dirs-1-file-size-0/" +
fileName).toFile(),
+ tempDir.toFile());
+ assertCounts(0, 1, 0, PathUtils.delete(tempDir.resolve(fileName),
options));
+ // This will throw if not empty.
+ Files.deleteIfExists(tempDir);
+ }
+
/**
- * Tests a directory with one file of size 1.
+ * Tests a directory with one file of size 0.
*/
@Test
+ public void testDeleteDirectory1FileSize0ForceOff() throws IOException {
+ testDeleteDirectory1FileSize0();
+ }
+
+ /**
+ * Tests a directory with one file of size 0.
+ */
+ @Test
+ public void testDeleteDirectory1FileSize0ForceOn() throws IOException {
+ testDeleteDirectory1FileSize0();
+ }
+
+ @Test
+ public void testDeleteDirectory1FileSize0NoOption() throws IOException {
+ testDeleteDirectory1FileSize0(PathUtils.EMPTY_DELETE_OPTION_ARRAY);
+ }
+
+ @Test
+ public void testDeleteDirectory1FileSize0OverrideReadonly() throws
IOException {
+ testDeleteDirectory1FileSize0(StandardDeleteOption.OVERRIDE_READ_ONLY);
+ }
+
+ @Test
public void testDeleteDirectory1FileSize1() throws IOException {
final String fileName = "file-size-1.bin";
FileUtils.copyFileToDirectory(
-
Paths.get("src/test/resources/org/apache/commons/io/dirs-1-file-size-1/" +
fileName).toFile(),
- tempDir.toFile());
+
Paths.get("src/test/resources/org/apache/commons/io/dirs-1-file-size-1/" +
fileName).toFile(),
+ tempDir.toFile());
assertCounts(0, 1, 1, PathUtils.delete(tempDir.resolve(fileName)));
// This will throw if not empty.
Files.deleteIfExists(tempDir);
}
+ private void testDeleteDirectory1FileSize1(final DeleteOption... options)
throws IOException {
+ // TODO Setup the test to use LinkOption.
+ final String fileName = "file-size-1.bin";
+ FileUtils.copyFileToDirectory(
+
Paths.get("src/test/resources/org/apache/commons/io/dirs-1-file-size-1/" +
fileName).toFile(),
+ tempDir.toFile());
+ assertCounts(0, 1, 1, PathUtils.delete(tempDir.resolve(fileName),
options));
+ // This will throw if not empty.
+ Files.deleteIfExists(tempDir);
+ }
+
+ /**
+ * Tests a directory with one file of size 1.
+ */
+ @Test
+ public void testDeleteDirectory1FileSize1ForceOff() throws IOException {
+ testDeleteDirectory1FileSize1();
+ }
+
+ /**
+ * Tests a directory with one file of size 1.
+ */
+ @Test
+ public void testDeleteDirectory1FileSize1ForceOn() throws IOException {
+ testDeleteDirectory1FileSize1();
+ }
+
+ @Test
+ public void testDeleteDirectory1FileSize1NoOption() throws IOException {
+ testDeleteDirectory1FileSize1(PathUtils.EMPTY_DELETE_OPTION_ARRAY);
+ }
+
+ @Test
+ public void testDeleteDirectory1FileSize1OverrideReadOnly() throws
IOException {
+ testDeleteDirectory1FileSize1(StandardDeleteOption.OVERRIDE_READ_ONLY);
+ }
+
/**
* Tests an empty folder.
*/
@@ -88,11 +157,46 @@ public class PathUtilsDeleteTest {
Files.deleteIfExists(tempDir);
}
+ /**
+ * Tests an empty folder.
+ */
+ private void testDeleteEmptyDirectory(final DeleteOption... options)
throws IOException {
+ testDeleteEmptyDirectory(PathUtils.delete(tempDir, options));
+ // This will throw if not empty.
+ Files.deleteIfExists(tempDir);
+ }
+
private void testDeleteEmptyDirectory(final PathCounters pathCounts) {
assertCounts(1, 0, 0, pathCounts);
}
/**
+ * Tests an empty folder.
+ */
+ @Test
+ public void testDeleteEmptyDirectoryForceOff() throws IOException {
+ testDeleteEmptyDirectory();
+ }
+
+ /**
+ * Tests an empty folder.
+ */
+ @Test
+ public void testDeleteEmptyDirectoryForceOn() throws IOException {
+ testDeleteEmptyDirectory();
+ }
+
+ @Test
+ public void testDeleteEmptyDirectoryNoOption() throws IOException {
+ testDeleteEmptyDirectory(PathUtils.EMPTY_DELETE_OPTION_ARRAY);
+ }
+
+ @Test
+ public void testDeleteEmptyDirectoryOverrideReadOnly() throws IOException {
+ testDeleteEmptyDirectory(StandardDeleteOption.OVERRIDE_READ_ONLY);
+ }
+
+ /**
* Tests a file that does not exist.
*/
@Test