Skip to content

Commit 4a514d3

Browse files
author
Gary Gregory
committed
[IO-597] FileUtils.iterateFiles runs out of memory when executed for a
directory with large number of files. Re-implement FileUtils' iterateFiles(), iterateFilesAndDirs(), listFiles(), listFilesAndDirs() to use NIO file tree walking instead of IO file listings to avoid memory consumption issues on large file trees.
1 parent 42ed368 commit 4a514d3

39 files changed

Lines changed: 2741 additions & 2435 deletions

src/changes/changes.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,11 @@ The <action> type attribute can be add,update,fix,remove.
6666
<action dev="ggregory" type="fix" due-to="Gary Gregory">
6767
AccumulatorPathVisitor does not track directories properly.
6868
</action>
69+
<action issue="IO-597" dev="ggregory" type="fix" due-to="Gary Gregory, Arvind, Rob Spoor">
70+
FileUtils.iterateFiles runs out of memory when executed for a directory with large number of files.
71+
Re-implement FileUtils' iterateFiles(), iterateFilesAndDirs(), listFiles(), listFilesAndDirs() to use NIO
72+
file tree walking instead of IO file listings to avoid memory consumption issues on large file trees.
73+
</action>
6974
<!-- ADD -->
7075
<action dev="ggregory" type="add" due-to="Gary Gregory">
7176
Add FileSystemProviders class.

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

Lines changed: 105 additions & 132 deletions
Large diffs are not rendered by default.
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package org.apache.commons.io;
19+
20+
import java.io.Closeable;
21+
import java.util.Iterator;
22+
import java.util.Objects;
23+
import java.util.stream.Stream;
24+
25+
/**
26+
* Wraps and presents a stream as a closable iterator resource that automatically closes itself when reaching the end
27+
* of stream.
28+
*
29+
* @param <E> The stream and iterator type.
30+
* @since 2.9.0
31+
*/
32+
class StreamIterator<E> implements Iterator<E>, Closeable {
33+
34+
/**
35+
* Wraps and presents a stream as a closable resource that automatically closes itself when reaching the end of
36+
* stream.
37+
* <h2>Warning</h2>
38+
* <p>
39+
* In order to close the stream, the call site MUST either close the stream it allocated OR call the iterator until
40+
* the end.
41+
* </p>
42+
*
43+
* @param <T> The stream and iterator type.
44+
* @param stream The stream iterate.
45+
* @return A new iterator.
46+
*/
47+
@SuppressWarnings("resource") // Caller MUST close or iterate to the end.
48+
public static <T> Iterator<T> iterator(final Stream<T> stream) {
49+
return new StreamIterator<>(stream).iterator;
50+
}
51+
52+
private StreamIterator(final Stream<E> stream) {
53+
super();
54+
this.stream = Objects.requireNonNull(stream, "stream");
55+
this.iterator = stream.iterator();
56+
}
57+
58+
private final Iterator<E> iterator;
59+
private final Stream<E> stream;
60+
61+
@Override
62+
public boolean hasNext() {
63+
final boolean hasNext = iterator.hasNext();
64+
if (!hasNext) {
65+
close();
66+
}
67+
return hasNext;
68+
}
69+
70+
@Override
71+
public E next() {
72+
final E next = iterator.next();
73+
if (next == null) {
74+
close();
75+
}
76+
return next;
77+
}
78+
79+
/**
80+
* Closes the underlying stream.
81+
*/
82+
@Override
83+
public void close() {
84+
stream.close();
85+
86+
}
87+
88+
}

src/main/java/org/apache/commons/io/file/AccumulatorPathVisitor.java

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
* <h2>Example</h2>
3636
*
3737
* <pre>
38-
* Path dir = Paths.get(".");
38+
* Path dir = Paths.get("");
3939
* // We are interested in files older than one day
4040
* long cutoff = System.currentTimeMillis() - (24 * 60 * 60 * 1000);
4141
* AccumulatorPathVisitor visitor = AccumulatorPathVisitor.withLongCounters(new AgeFileFilter(cutoff));
@@ -70,12 +70,14 @@ public static AccumulatorPathVisitor withBigIntegerCounters() {
7070
/**
7171
* Creates a new instance configured with a BigInteger {@link PathCounters}.
7272
*
73-
* @param pathFilter Filters files to accumulate and count.
73+
* @param fileFilter Filters files to accumulate and count.
74+
* @param dirFilter Filters directories to accumulate and count.
7475
* @return a new instance configured with a long {@link PathCounters}.
7576
* @since 2.9.0
7677
*/
77-
public static AccumulatorPathVisitor withBigIntegerCounters(final PathFilter pathFilter) {
78-
return new AccumulatorPathVisitor(Counters.bigIntegerPathCounters(), pathFilter);
78+
public static AccumulatorPathVisitor withBigIntegerCounters(final PathFilter fileFilter,
79+
final PathFilter dirFilter) {
80+
return new AccumulatorPathVisitor(Counters.bigIntegerPathCounters(), fileFilter, dirFilter);
7981
}
8082

8183
/**
@@ -90,12 +92,13 @@ public static AccumulatorPathVisitor withLongCounters() {
9092
/**
9193
* Creates a new instance configured with a long {@link PathCounters}.
9294
*
93-
* @param pathFilter Filters files to accumulate and count.
95+
* @param fileFilter Filters files to accumulate and count.
96+
* @param dirFilter Filters directories to accumulate and count.
9497
* @return a new instance configured with a long {@link PathCounters}.
9598
* @since 2.9.0
9699
*/
97-
public static AccumulatorPathVisitor withLongCounters(final PathFilter pathFilter) {
98-
return new AccumulatorPathVisitor(Counters.longPathCounters(), pathFilter);
100+
public static AccumulatorPathVisitor withLongCounters(final PathFilter fileFilter, final PathFilter dirFilter) {
101+
return new AccumulatorPathVisitor(Counters.longPathCounters(), fileFilter, dirFilter);
99102
}
100103

101104
private final List<Path> dirList = new ArrayList<>();
@@ -124,11 +127,13 @@ public AccumulatorPathVisitor(final PathCounters pathCounter) {
124127
* Constructs a new instance.
125128
*
126129
* @param pathCounter How to count path visits.
127-
* @param pathFilter Filters which paths to count.
130+
* @param pathFilter Filters which files to count.
131+
* @param dirFilter Filters which directories to count.
128132
* @since 2.9.0
129133
*/
130-
public AccumulatorPathVisitor(final PathCounters pathCounter, final PathFilter pathFilter) {
131-
super(pathCounter, pathFilter);
134+
public AccumulatorPathVisitor(final PathCounters pathCounter, final PathFilter pathFilter,
135+
final PathFilter dirFilter) {
136+
super(pathCounter, pathFilter, dirFilter);
132137
}
133138

134139
private void add(final List<Path> list, final Path dir) {

src/main/java/org/apache/commons/io/file/CopyDirectoryVisitor.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,15 +59,16 @@ public CopyDirectoryVisitor(final PathCounters pathCounter, final Path sourceDir
5959
* Constructs a new visitor that deletes files except for the files and directories explicitly given.
6060
*
6161
* @param pathCounter How to count visits.
62-
* @param pathFilter How to filter paths.
62+
* @param fileFilter How to filter file paths.
63+
* @param dirFilter How to filter directory paths.
6364
* @param sourceDirectory The source directory
6465
* @param targetDirectory The target directory
6566
* @param copyOptions Specifies how the copying should be done.
6667
* @since 2.9.0
6768
*/
68-
public CopyDirectoryVisitor(final PathCounters pathCounter, final PathFilter pathFilter, final Path sourceDirectory, final Path targetDirectory,
69-
final CopyOption... copyOptions) {
70-
super(pathCounter, pathFilter);
69+
public CopyDirectoryVisitor(final PathCounters pathCounter, final PathFilter fileFilter, final PathFilter dirFilter,
70+
final Path sourceDirectory, final Path targetDirectory, final CopyOption... copyOptions) {
71+
super(pathCounter, fileFilter, dirFilter);
7172
this.sourceDirectory = sourceDirectory;
7273
this.targetDirectory = targetDirectory;
7374
this.copyOptions = copyOptions == null ? PathUtils.EMPTY_COPY_OPTIONS : copyOptions.clone();

src/main/java/org/apache/commons/io/file/CountingPathVisitor.java

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,28 +55,31 @@ public static CountingPathVisitor withLongCounters() {
5555
}
5656

5757
private final PathCounters pathCounters;
58-
private final PathFilter pathFilter;
58+
private final PathFilter fileFilter;
59+
private final PathFilter dirFilter;
5960

6061
/**
6162
* Constructs a new instance.
6263
*
6364
* @param pathCounter How to count path visits.
6465
*/
6566
public CountingPathVisitor(final PathCounters pathCounter) {
66-
this(pathCounter, TrueFileFilter.INSTANCE);
67+
this(pathCounter, TrueFileFilter.INSTANCE, TrueFileFilter.INSTANCE);
6768
}
6869

6970
/**
7071
* Constructs a new instance.
7172
*
7273
* @param pathCounter How to count path visits.
73-
* @param pathFilter Filters which paths to count.
74+
* @param fileFilter Filters which files to count.
75+
* @param dirFilter Filters which directories to count.
7476
* @since 2.9.0
7577
*/
76-
public CountingPathVisitor(final PathCounters pathCounter, final PathFilter pathFilter) {
78+
public CountingPathVisitor(final PathCounters pathCounter, final PathFilter fileFilter, PathFilter dirFilter) {
7779
super();
7880
this.pathCounters = Objects.requireNonNull(pathCounter, "pathCounter");
79-
this.pathFilter = Objects.requireNonNull(pathFilter, "pathFilter");
81+
this.fileFilter = Objects.requireNonNull(fileFilter, "fileFilter");
82+
this.dirFilter = Objects.requireNonNull(dirFilter, "dirFilter");
8083
}
8184

8285
@Override
@@ -110,6 +113,12 @@ public FileVisitResult postVisitDirectory(final Path dir, final IOException exc)
110113
updateDirCounter(dir, exc);
111114
return FileVisitResult.CONTINUE;
112115
}
116+
117+
@Override
118+
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attributes) throws IOException {
119+
final FileVisitResult accept = dirFilter.accept(dir, attributes);
120+
return accept != FileVisitResult.CONTINUE ? FileVisitResult.SKIP_SUBTREE : FileVisitResult.CONTINUE;
121+
}
113122

114123
@Override
115124
public String toString() {
@@ -140,7 +149,7 @@ protected void updateFileCounters(final Path file, final BasicFileAttributes att
140149

141150
@Override
142151
public FileVisitResult visitFile(final Path file, final BasicFileAttributes attributes) throws IOException {
143-
if (Files.exists(file) && pathFilter.accept(file, attributes) == FileVisitResult.CONTINUE) {
152+
if (Files.exists(file) && fileFilter.accept(file, attributes) == FileVisitResult.CONTINUE) {
144153
updateFileCounters(file, attributes);
145154
}
146155
return FileVisitResult.CONTINUE;

src/main/java/org/apache/commons/io/file/PathFilter.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public interface PathFilter {
3333
* Tests whether or not to include the specified Path in a result.
3434
*
3535
* @param path The Path to test.
36-
* @param attributes the file's basic attributes.
36+
* @param attributes the file's basic attributes (TODO may be null).
3737
* @return a FileVisitResult
3838
*/
3939
FileVisitResult accept(Path path, BasicFileAttributes attributes);

src/main/java/org/apache/commons/io/file/PathUtils.java

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,17 @@ public static PathCounters countDirectory(final Path directory) throws IOExcepti
280280
return visitFileTree(new CountingPathVisitor(Counters.longPathCounters()), directory).getPathCounters();
281281
}
282282

283+
/**
284+
* Gets the current directory.
285+
*
286+
* @return the current directory.
287+
*
288+
* @since 2.9.0
289+
*/
290+
public static Path current() {
291+
return Paths.get("");
292+
}
293+
283294
/**
284295
* Deletes a file or directory. If the path is a directory, delete it and all sub-directories.
285296
* <p>
@@ -684,7 +695,8 @@ public static boolean isNewer(final Path file, final long timeMillis, final Link
684695
* @return a new instance.
685696
* @throws IOException if an I/O error occurs.
686697
*/
687-
public static DirectoryStream<Path> newDirectoryStream(final Path dir, final PathFilter pathFilter) throws IOException {
698+
public static DirectoryStream<Path> newDirectoryStream(final Path dir, final PathFilter pathFilter)
699+
throws IOException {
688700
return Files.newDirectoryStream(dir, new DirectoryStreamFilter(pathFilter));
689701
}
690702

@@ -874,6 +886,24 @@ public static <T extends FileVisitor<? super Path>> T visitFileTree(final T visi
874886
return visitFileTree(visitor, Paths.get(uri));
875887
}
876888

889+
/**
890+
* Returns a stream of filtered paths.
891+
*
892+
* @param start the start path
893+
* @param pathFilter the path filter
894+
* @param maxDepth the maximum depth of directories to walk.
895+
* @param readAttributes whether to call the filters with file attributes (false passes null).
896+
* @param options the options to configure the walk.
897+
* @return a filtered stream of paths.
898+
* @throws IOException if an I/O error is thrown when accessing the starting file.
899+
* @since 2.9.0
900+
*/
901+
public static Stream<Path> walk(final Path start, final PathFilter pathFilter, final int maxDepth,
902+
boolean readAttributes, final FileVisitOption... options) throws IOException {
903+
return Files.walk(start, maxDepth, options).filter(path -> pathFilter.accept(path,
904+
readAttributes ? readBasicFileAttributesQuietly(path) : null) == FileVisitResult.CONTINUE);
905+
}
906+
877907
/**
878908
* Does allow to instantiate.
879909
*/

src/main/java/org/apache/commons/io/filefilter/AbstractFileFilter.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
*/
4040
public abstract class AbstractFileFilter implements IOFileFilter, PathVisitor {
4141

42-
protected static FileVisitResult toFileVisitResult(final boolean accept) {
42+
static FileVisitResult toFileVisitResult(final boolean accept, final Path path) {
4343
return accept ? FileVisitResult.CONTINUE : FileVisitResult.TERMINATE;
4444
}
4545

src/main/java/org/apache/commons/io/filefilter/AgeFileFilter.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
* </p>
3535
* <h2>Using Classic IO</h2>
3636
* <pre>
37-
* Path dir = Paths.get(".");
37+
* Path dir = Paths.get("");
3838
* // We are interested in files older than one day
3939
* long cutoff = System.currentTimeMillis() - (24 * 60 * 60 * 1000);
4040
* String[] files = dir.list(new AgeFileFilter(cutoff));
@@ -45,7 +45,7 @@
4545
*
4646
* <h2>Using NIO</h2>
4747
* <pre>
48-
* Path dir = Paths.get(".");
48+
* Path dir = Paths.get("");
4949
* // We are interested in files older than one day
5050
* long cutoff = System.currentTimeMillis() - (24 * 60 * 60 * 1000);
5151
* AccumulatorPathVisitor visitor = AccumulatorPathVisitor.withLongCounters(new AgeFileFilter(cutoff));
@@ -182,7 +182,7 @@ public FileVisitResult accept(final Path file, final BasicFileAttributes attribu
182182
} catch (final IOException e) {
183183
return handle(e);
184184
}
185-
return toFileVisitResult(acceptOlder != newer);
185+
return toFileVisitResult(acceptOlder != newer, file);
186186
}
187187

188188
/**

0 commit comments

Comments
 (0)