Skip to content

Commit 0be5698

Browse files
committed
[IO-862] FileUtils.deleteDirectory fails for a directory containing a
broken symbolic link
1 parent 3a61904 commit 0be5698

3 files changed

Lines changed: 67 additions & 17 deletions

File tree

src/changes/changes.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ The <action> type attribute can be add,update,fix,remove.
5757
<action dev="ggregory" type="fix" issue="IO-859" due-to="JD Dean, Gary Gregory">FileUtils.forceDelete on non-existent file on Windows throws IOException rather than FileNotFoundException.</action>
5858
<action dev="ggregory" type="fix" due-to="Éamonn McManus">Use Unicode escapes for superscript characters. #701.</action>
5959
<action dev="ggregory" type="fix" issue="IO-863" due-to="Éamonn McManus, Gary Gregory">Recent incompatible change to FileUtils.listFiles re extensions, see IO-856.</action>
60+
<action dev="ggregory" type="fix" issue="IO-862" due-to="Éamonn McManus, Gary Gregory">FileUtils.deleteDirectory fails for a directory containing a broken symbolic link.</action>
6061
<!-- ADD -->
6162
<action dev="ggregory" type="add" due-to="Gary Gregory">Add @FunctionalInterface to ClassNameMatcher.</action>
6263
<action dev="ggregory" type="add" due-to="Gary Gregory">Add ValidatingObjectInputStream.Builder and ValidatingObjectInputStream.builder().</action>

src/main/java/org/apache/commons/io/FileUtils.java

Lines changed: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -285,19 +285,28 @@ public static String byteCountToDisplaySize(final Number size) {
285285
}
286286

287287
/**
288-
* Requires that the given {@link File} exists, and throws a {@link FileNotFoundException} if it doesn't.
288+
* Requires that the given {@link File} is non-null and exists (if strict is true).
289289
*
290290
* @param file The {@link File} to check.
291-
* @throws FileNotFoundException if the file does not exist
291+
* @param strict whether to check that the {@code file} exists.
292+
* @throws FileNotFoundException if the file does not exist.
292293
* @throws NullPointerException if the given {@link File} is {@code null}.
293294
*/
294-
private static void checkExists(final File file) throws FileNotFoundException {
295-
Objects.requireNonNull(file, "file");
296-
if (!file.exists()) {
295+
private static void checkExists(final File file, final boolean strict) throws FileNotFoundException {
296+
Objects.requireNonNull(file, PROTOCOL_FILE);
297+
if (strict && !file.exists()) {
297298
throw new FileNotFoundException(file.toString());
298299
}
299300
}
300301

302+
/**
303+
* Requires that the given {@link File} exists, and throws a {@link FileNotFoundException} if it doesn't.
304+
*
305+
* @param file The {@link File} to check.
306+
* @param name The NullPointerException message.
307+
* @throws FileNotFoundException if the file does not exist.
308+
* @throws NullPointerException if the given {@link File} is {@code null}.
309+
*/
301310
private static void checkFileExists(final File file, final String name) throws FileNotFoundException {
302311
Objects.requireNonNull(file, name);
303312
if (!file.isFile()) {
@@ -350,8 +359,8 @@ public static Checksum checksum(final File file, final Checksum checksum) throws
350359
*
351360
* @param file the file to checksum, must not be {@code null}
352361
* @return the checksum value
353-
* @throws NullPointerException if the given {@link File} is {@code null}.
354-
* @throws IllegalArgumentException if the given {@link File} does not exist or is not a file.
362+
* @throws NullPointerException if the {@code file} is {@code null}.
363+
* @throws IllegalArgumentException if the {@code file} does not exist or is not a file.
355364
* @throws IOException if an IO error occurs reading the file.
356365
* @since 1.3
357366
*/
@@ -364,20 +373,20 @@ public static long checksumCRC32(final File file) throws IOException {
364373
*
365374
* @param directory directory to clean
366375
* @throws NullPointerException if the given {@link File} is {@code null}.
367-
* @throws IllegalArgumentException if directory does not exist or is not a directory.
376+
* @throws IllegalArgumentException if the {@code directory} does not exist or is not a directory.
368377
* @throws IOException if an I/O error occurs.
369378
* @see #forceDelete(File)
370379
*/
371380
public static void cleanDirectory(final File directory) throws IOException {
372-
IOConsumer.forAll(FileUtils::forceDelete, listFiles(directory, null));
381+
IOConsumer.forAll(f -> forceDelete(f, false), listFiles(directory, null));
373382
}
374383

375384
/**
376385
* Cleans a directory without deleting it.
377386
*
378387
* @param directory directory to clean, must not be {@code null}
379388
* @throws NullPointerException if the given {@link File} is {@code null}.
380-
* @throws IllegalArgumentException if directory does not exist or is not a directory.
389+
* @throws IllegalArgumentException if the {@code directory} does not exist or is not a directory.
381390
* @throws IOException if an I/O error occurs.
382391
* @see #forceDeleteOnExit(File)
383392
*/
@@ -1386,8 +1395,28 @@ private static void doCopyDirectory(final File srcDir, final File destDir, final
13861395
* @throws IOException in case deletion is unsuccessful.
13871396
*/
13881397
public static void forceDelete(final File file) throws IOException {
1398+
forceDelete(file, true);
1399+
}
1400+
1401+
/**
1402+
* Deletes a file or directory. For a directory, delete it and all subdirectories.
1403+
* <p>
1404+
* The difference between File.delete() and this method are:
1405+
* </p>
1406+
* <ul>
1407+
* <li>The directory does not have to be empty.</li>
1408+
* <li>You get an exception when a file or directory cannot be deleted.</li>
1409+
* </ul>
1410+
*
1411+
* @param file file or directory to delete, must not be {@code null}.
1412+
* @param strict whether to throw a FileNotFoundException.
1413+
* @throws NullPointerException if the file is {@code null}.
1414+
* @throws FileNotFoundException if the file was not found.
1415+
* @throws IOException in case deletion is unsuccessful.
1416+
*/
1417+
private static void forceDelete(final File file, final boolean strict) throws IOException {
13891418
Objects.requireNonNull(file, PROTOCOL_FILE);
1390-
checkExists(file); // fail-fast
1419+
checkExists(file, strict); // fail-fast
13911420
final Counters.PathCounters deleteCounters;
13921421
try {
13931422
deleteCounters = PathUtils.delete(file.toPath(), PathUtils.EMPTY_LINK_OPTION_ARRAY, StandardDeleteOption.OVERRIDE_READ_ONLY);
@@ -2265,11 +2294,11 @@ private static AccumulatorPathVisitor listAccumulate(final File directory, final
22652294
/**
22662295
* Lists files in a directory, asserting that the supplied directory exists and is a directory.
22672296
*
2268-
* @param directory The directory to list
2297+
* @param directory The directory to list.
22692298
* @param fileFilter Optional file filter, may be null.
22702299
* @return The files in the directory, never {@code null}.
22712300
* @throws NullPointerException if directory is {@code null}.
2272-
* @throws IllegalArgumentException if {@link directory} exists but is not a directory
2301+
* @throws IllegalArgumentException if {@link directory} exists but is not a directory.
22732302
* @throws IOException if an I/O error occurs.
22742303
*/
22752304
private static File[] listFiles(final File directory, final FileFilter fileFilter) throws IOException {

src/test/java/org/apache/commons/io/FileUtilsTest.java

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -248,10 +248,10 @@ private void createFilesForTestCopyDirectory(final File grandParentDir, final Fi
248248

249249
private ImmutablePair<Path, Path> createTempSymbolicLinkedRelativeDir() throws IOException {
250250
final Path targetDir = tempDirPath.resolve("subdir");
251-
final Path symlinkDir = tempDirPath.resolve("symlinked-dir");
251+
final Path symLinkedDir = tempDirPath.resolve("symlinked-dir");
252252
Files.createDirectory(targetDir);
253-
Files.createSymbolicLink(symlinkDir, targetDir);
254-
return ImmutablePair.of(symlinkDir, targetDir);
253+
Files.createSymbolicLink(symLinkedDir, targetDir);
254+
return ImmutablePair.of(symLinkedDir, targetDir);
255255
}
256256

257257
private Set<String> getFilePathSet(final List<File> files) {
@@ -318,7 +318,6 @@ private boolean setLastModifiedMillis(final File testFile, final long millis) {
318318
public void setUp() throws Exception {
319319
testFile1 = new File(tempDirFile, "file1-test.txt");
320320
testFile2 = new File(tempDirFile, "file1a-test.txt");
321-
322321
testFile1Size = testFile1.length();
323322
testFile2Size = testFile2.length();
324323
if (!testFile1.getParentFile().exists()) {
@@ -1586,6 +1585,7 @@ public void testDeleteDirectorySymbolicLinkAbsentTarget() throws IOException {
15861585
final Path symlinkedDir = pair.getLeft();
15871586
final Path targetDir = pair.getRight();
15881587
assertTrue(Files.exists(symlinkedDir), symlinkedDir::toString);
1588+
// remove target directory, keeping symbolic link
15891589
Files.delete(targetDir);
15901590
assertFalse(Files.exists(targetDir), targetDir::toString);
15911591
assertFalse(Files.exists(symlinkedDir), symlinkedDir::toString);
@@ -1594,6 +1594,26 @@ public void testDeleteDirectorySymbolicLinkAbsentTarget() throws IOException {
15941594
assertFalse(Files.exists(symlinkedDir), symlinkedDir::toString);
15951595
}
15961596

1597+
@Test
1598+
public void testDeleteDirectorySymbolicLinkAbsentDeepTarget() throws IOException {
1599+
final ImmutablePair<Path, Path> pair = createTempSymbolicLinkedRelativeDir();
1600+
final Path symLinkedDir = pair.getLeft();
1601+
final Path targetDir = pair.getRight();
1602+
// more setup
1603+
final Path targetDir2 = targetDir.resolve("subdir2");
1604+
final Path symLinkedDir2 = targetDir.resolve("symlinked-dir2");
1605+
Files.createDirectory(targetDir2);
1606+
Files.createSymbolicLink(symLinkedDir2, targetDir2);
1607+
assertTrue(Files.exists(symLinkedDir2), symLinkedDir2::toString);
1608+
// remove target directory, keeping symbolic link
1609+
Files.delete(targetDir2);
1610+
assertFalse(Files.exists(targetDir2), targetDir2::toString);
1611+
assertFalse(Files.exists(symLinkedDir2), symLinkedDir2::toString);
1612+
// actual test
1613+
FileUtils.deleteDirectory(targetDir.toFile());
1614+
assertFalse(Files.exists(targetDir), targetDir::toString);
1615+
}
1616+
15971617
@Test
15981618
public void testDeleteQuietlyDir() throws IOException {
15991619
final File testDirectory = new File(tempDirFile, "testDeleteQuietlyDir");

0 commit comments

Comments
 (0)