Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[IO-856] FileUtils.listFiles(final File, String[], boolean) can throw NoSuchFileException #699

Merged
merged 1 commit into from
Nov 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 23 additions & 3 deletions src/main/java/org/apache/commons/io/FileUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
Expand Down Expand Up @@ -57,6 +58,7 @@
import java.time.chrono.ChronoLocalDateTime;
import java.time.chrono.ChronoZonedDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
Expand Down Expand Up @@ -2311,6 +2313,22 @@ public static Collection<File> listFiles(final File directory, final IOFileFilte
return toList(visitor.getFileList().stream().map(Path::toFile));
}

@SuppressWarnings("null")
private static void listFiles(final File directory, final List<File> files, final boolean recursive, final FilenameFilter filter) {
// Only allocate if you must.
final List<File> dirs = recursive ? new ArrayList<>() : null;
Arrays.stream(directory.listFiles()).forEach(f -> {
if (recursive && f.isDirectory()) {
dirs.add(f);
} else if (f.isFile() && filter.accept(directory, f.getName())) {
files.add(f);
}
});
if (recursive) {
dirs.forEach(d -> listFiles(d, files, true, filter));
}
}

/**
* Lists files within a given directory (and optionally its subdirectories)
* which match an array of extensions.
Expand All @@ -2322,9 +2340,11 @@ public static Collection<File> listFiles(final File directory, final IOFileFilte
* @return a collection of {@link File} with the matching files
*/
public static Collection<File> listFiles(final File directory, final String[] extensions, final boolean recursive) {
try (Stream<File> fileStream = Uncheck.get(() -> streamFiles(directory, recursive, extensions))) {
return toList(fileStream);
}
// IO-856: Don't use NIO to path walk, allocate as little as possible while traversing.
final List<File> files = new ArrayList<>();
final FilenameFilter filter = extensions != null ? new SuffixFileFilter(extensions) : TrueFileFilter.INSTANCE;
listFiles(directory, files, recursive, filter);
return files;
}

/**
Expand Down
21 changes: 14 additions & 7 deletions src/test/java/org/apache/commons/io/FileUtilsListFilesTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
Expand All @@ -44,8 +45,6 @@
import org.apache.commons.lang3.function.Consumers;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledOnOs;
import org.junit.jupiter.api.condition.OS;
import org.junit.jupiter.api.io.TempDir;

/**
Expand Down Expand Up @@ -204,12 +203,12 @@ public void testListFilesByExtension() {

files = FileUtils.listFiles(temporaryFolder, extensions, true);
fileNames = filesToFilenames(files);
assertEquals(4, fileNames.size());
assertEquals(4, fileNames.size(), fileNames::toString);
assertTrue(fileNames.contains("dummy-file.txt"));
assertFalse(fileNames.contains("dummy-index.html"));

files = FileUtils.listFiles(temporaryFolder, null, false);
assertEquals(2, files.size());
assertEquals(2, files.size(), files::toString);
fileNames = filesToFilenames(files);
assertTrue(fileNames.contains("dummy-build.xml"));
assertTrue(fileNames.contains("README"));
Expand Down Expand Up @@ -238,7 +237,6 @@ public void testListFilesWithDeletion() throws IOException {
* Tests <a href="https://issues.apache.org/jira/browse/IO-856">IO-856</a> ListFiles should not fail on vanishing files.
*/
@Test
@EnabledOnOs(value = OS.WINDOWS)
public void testListFilesWithDeletionThreaded() throws ExecutionException, InterruptedException {
// test for IO-856
// create random directory in tmp, create the directory if it does not exist
Expand All @@ -248,33 +246,42 @@ public void testListFilesWithDeletionThreaded() throws ExecutionException, Inter
fail("Could not create file path: " + tempDir.getAbsolutePath());
}
final int waitTime = 10_000;
final int maxFiles = 500;
final byte[] bytes = "TEST".getBytes(StandardCharsets.UTF_8);
final CompletableFuture<Void> c1 = CompletableFuture.runAsync(() -> {
final long endTime = System.currentTimeMillis() + waitTime;
while (System.currentTimeMillis() < endTime) {
int count = 0;
while (System.currentTimeMillis() < endTime && count < maxFiles) {
final File file = new File(tempDir.getAbsolutePath(), UUID.randomUUID() + ".deletetester");
file.deleteOnExit();
try {
Files.write(file.toPath(), bytes);
count++;
} catch (final Exception e) {
fail("Could not create test file: '" + file.getAbsolutePath() + "': " + e, e);
}
if (!file.delete()) {
fail("Could not delete test file: '" + file.getAbsolutePath() + "'");
}
}
// System.out.printf("Created %,d%n", count);
});
final CompletableFuture<Void> c2 = CompletableFuture.runAsync(() -> {
final long endTime = System.currentTimeMillis() + waitTime;
int max = 0;
try {
while (System.currentTimeMillis() < endTime) {
FileUtils.listFiles(tempDir, new String[] { "\\.deletetester" }, false);
final Collection<File> files = FileUtils.listFiles(tempDir, new String[] { "\\.deletetester" }, false);
assertNotNull(files);
max = Math.max(max, files.size());
}
} catch (final Exception e) {
System.out.printf("List size max %,d%n", max);
fail("IO-856 test failure: " + e, e);
// The exception can be hidden.
e.printStackTrace();
}
// System.out.printf("List size max %,d%n", max);
});
// wait for the threads to finish
c1.get();
Expand Down
Loading