From af7514acdbff34722b47938fcea41cbe9a5cf0ec Mon Sep 17 00:00:00 2001 From: cgdecker Date: Mon, 13 Nov 2017 13:52:13 -0800 Subject: [PATCH] Roll forward. *** Original change description *** Add MoreFiles.fileTraverser(). ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=175582408 --- .../guava/src/com/google/common/io/Files.java | 3 + .../common/io/MoreFilesFileTraverserTest.java | 119 ++++++++++++++++++ .../com/google/common/io/MoreFilesTest.java | 2 + guava/src/com/google/common/io/Files.java | 3 + guava/src/com/google/common/io/MoreFiles.java | 52 ++++++-- 5 files changed, 172 insertions(+), 7 deletions(-) create mode 100644 guava-tests/test/com/google/common/io/MoreFilesFileTraverserTest.java diff --git a/android/guava/src/com/google/common/io/Files.java b/android/guava/src/com/google/common/io/Files.java index ee43f85b8a3e..3f7d9f574dc6 100644 --- a/android/guava/src/com/google/common/io/Files.java +++ b/android/guava/src/com/google/common/io/Files.java @@ -852,6 +852,9 @@ public String toString() { * this case, iterables created by this traverser could contain files that are outside of the * given directory or even be infinite if there is a symbolic link loop. * + *

If available, consider using {@link MoreFiles#fileTraverser()} instead. It behaves the same + * except that it doesn't follow symbolic links and returns {@code Path} instances. + * *

If the {@link File} passed to one of the {@link Traverser} methods does not exist or is not * a directory, no exception will be thrown and the returned {@link Iterable} will contain a * single element: that file. diff --git a/guava-tests/test/com/google/common/io/MoreFilesFileTraverserTest.java b/guava-tests/test/com/google/common/io/MoreFilesFileTraverserTest.java new file mode 100644 index 000000000000..157a4ce88086 --- /dev/null +++ b/guava-tests/test/com/google/common/io/MoreFilesFileTraverserTest.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2017 The Guava Authors + * + * Licensed 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 com.google.common.io; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.common.collect.Iterables; +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import java.io.IOException; +import java.nio.file.Path; +import junit.framework.TestCase; + +/** + * Tests for {@link MoreFiles#fileTraverser()}. + * + * @author Jens Nyman + */ + +public class MoreFilesFileTraverserTest extends TestCase { + + private Path rootDir; + + @Override + public void setUp() throws IOException { + rootDir = Files.createTempDir().toPath(); + } + + @Override + public void tearDown() throws IOException { + MoreFiles.deleteRecursively(rootDir); + } + + public void testFileTraverser_emptyDirectory() throws Exception { + assertThat(MoreFiles.fileTraverser().breadthFirst(rootDir)).containsExactly(rootDir); + } + + public void testFileTraverser_nonExistingFile() throws Exception { + Path file = rootDir.resolve("file-that-doesnt-exist"); + + assertThat(MoreFiles.fileTraverser().breadthFirst(file)).containsExactly(file); + } + + public void testFileTraverser_file() throws Exception { + Path file = newFile("some-file"); + + assertThat(MoreFiles.fileTraverser().breadthFirst(file)).containsExactly(file); + } + + public void testFileTraverser_singleFile() throws Exception { + Path file = newFile("some-file"); + + assertThat(MoreFiles.fileTraverser().breadthFirst(rootDir)).containsExactly(rootDir, file); + } + + public void testFileTraverser_singleDirectory() throws Exception { + Path file = newDir("some-dir"); + + assertThat(MoreFiles.fileTraverser().breadthFirst(rootDir)).containsExactly(rootDir, file); + } + + public void testFileTraverser_multipleFilesAndDirectories() throws Exception { + Path fileA = newFile("file-a"); + Path fileB = newFile("file-b"); + Path dir1 = newDir("dir-1"); + Path dir2 = newDir("dir-2"); + + assertThat(MoreFiles.fileTraverser().breadthFirst(rootDir)) + .containsExactly(rootDir, fileA, fileB, dir1, dir2); + } + + public void testFileTraverser_multipleDirectoryLayers_breadthFirstStartsWithTopLayer() + throws Exception { + Path fileA = newFile("file-a"); + Path dir1 = newDir("dir-1"); + newFile("dir-1/file-b"); + newFile("dir-1/dir-2"); + + assertThat(Iterables.limit(MoreFiles.fileTraverser().breadthFirst(rootDir), 3)) + .containsExactly(rootDir, fileA, dir1); + } + + public void testFileTraverser_multipleDirectoryLayers_traversalReturnsAll() throws Exception { + Path fileA = newFile("file-a"); + Path dir1 = newDir("dir-1"); + Path fileB = newFile("dir-1/file-b"); + Path dir2 = newFile("dir-1/dir-2"); + + assertThat(MoreFiles.fileTraverser().breadthFirst(rootDir)) + .containsExactly(rootDir, fileA, fileB, dir1, dir2); + } + + @CanIgnoreReturnValue + private Path newDir(String name) { + Path dir = rootDir.resolve(name); + dir.toFile().mkdir(); + return dir; + } + + @CanIgnoreReturnValue + private Path newFile(String name) throws IOException { + Path file = rootDir.resolve(name); + file.toFile().createNewFile(); + return file; + } +} diff --git a/guava-tests/test/com/google/common/io/MoreFilesTest.java b/guava-tests/test/com/google/common/io/MoreFilesTest.java index 3b33f5b58920..8702ceb81aed 100644 --- a/guava-tests/test/com/google/common/io/MoreFilesTest.java +++ b/guava-tests/test/com/google/common/io/MoreFilesTest.java @@ -48,6 +48,8 @@ /** * Tests for {@link MoreFiles}. * + *

Note: {@link MoreFiles#fileTraverser()} is tested in {@link MoreFilesFileTraverserTest}. + * * @author Colin Decker */ diff --git a/guava/src/com/google/common/io/Files.java b/guava/src/com/google/common/io/Files.java index ee43f85b8a3e..3f7d9f574dc6 100644 --- a/guava/src/com/google/common/io/Files.java +++ b/guava/src/com/google/common/io/Files.java @@ -852,6 +852,9 @@ public String toString() { * this case, iterables created by this traverser could contain files that are outside of the * given directory or even be infinite if there is a symbolic link loop. * + *

If available, consider using {@link MoreFiles#fileTraverser()} instead. It behaves the same + * except that it doesn't follow symbolic links and returns {@code Path} instances. + * *

If the {@link File} passed to one of the {@link Traverser} methods does not exist or is not * a directory, no exception will be thrown and the returned {@link Iterable} will contain a * single element: that file. diff --git a/guava/src/com/google/common/io/MoreFiles.java b/guava/src/com/google/common/io/MoreFiles.java index 866464ff24f9..5b1b3193b1e8 100644 --- a/guava/src/com/google/common/io/MoreFiles.java +++ b/guava/src/com/google/common/io/MoreFiles.java @@ -25,6 +25,8 @@ import com.google.common.base.Predicate; import com.google.common.collect.ImmutableList; import com.google.common.collect.TreeTraverser; +import com.google.common.graph.SuccessorsFunction; +import com.google.common.graph.Traverser; import com.google.common.io.ByteSource.AsCharSource; import com.google.j2objc.annotations.J2ObjCIncompatible; import java.io.IOException; @@ -291,16 +293,52 @@ private static final class DirectoryTreeTraverser extends TreeTraverser { @Override public Iterable children(Path dir) { - if (Files.isDirectory(dir, NOFOLLOW_LINKS)) { - try { - return listFiles(dir); - } catch (IOException e) { - // the exception thrown when iterating a DirectoryStream if an I/O exception occurs - throw new DirectoryIteratorException(e); + return fileTreeChildren(dir); + } + } + + /** + * Returns a {@link Traverser} instance for the file and directory tree. The returned traverser + * starts from a {@link Path} and will return all files and directories it encounters. + * + *

The returned traverser attempts to avoid following symbolic links to directories. However, + * the traverser cannot guarantee that it will not follow symbolic links to directories as it is + * possible for a directory to be replaced with a symbolic link between checking if the file is a + * directory and actually reading the contents of that directory. + * + *

If the {@link Path} passed to one of the traversal methods does not exist or is not a + * directory, no exception will be thrown and the returned {@link Iterable} will contain a single + * element: that path. + * + *

{@link DirectoryIteratorException} may be thrown when iterating {@link Iterable} instances + * created by this traverser if an {@link IOException} is thrown by a call to {@link + * #listFiles(Path)}. + * + *

Example: {@code MoreFiles.fileTraverser().breadthFirst("/")} may return files with the + * following paths: {@code ["/", "/etc", "/home", "/usr", "/etc/config.txt", "/etc/fonts", ...]} + */ + public static Traverser fileTraverser() { + return Traverser.forTree(FILE_TREE); + } + + private static final SuccessorsFunction FILE_TREE = + new SuccessorsFunction() { + @Override + public Iterable successors(Path path) { + return fileTreeChildren(path); } + }; + + private static Iterable fileTreeChildren(Path dir) { + if (Files.isDirectory(dir, NOFOLLOW_LINKS)) { + try { + return listFiles(dir); + } catch (IOException e) { + // the exception thrown when iterating a DirectoryStream if an I/O exception occurs + throw new DirectoryIteratorException(e); } - return ImmutableList.of(); } + return ImmutableList.of(); } /**