Skip to content
Open
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
14 changes: 12 additions & 2 deletions src/main/java/org/apache/commons/io/file/CountingPathVisitor.java
Original file line number Diff line number Diff line change
Expand Up @@ -304,14 +304,24 @@ protected void updateDirCounter(final Path dir, final IOException exc) {
}

/**
* Updates the counters for visiting the given file.
* Updates the counters for visiting the given file, ignoring symbolic links.
* <p>
* According to the JavaDoc, {@link BasicFileAttributes#size} is only well-defined for regular files.
* For symbolic links on Linux for example, it counts the # (charset dependent?) bytes in the inode name,
* which is <em>not</em> what we want to count here.
* Intuitively, the appropriate check would be {@link Files#isRegularFile}.
* However, for symbolic links, {@code isRegularFile} returns {@code true} under a "follow links" regime.
* That would still not give us what we want, so instead we settle for a {@code !Files.isSymbolicLink} check.
* </p>
*
* @param file the visited file.
* @param attributes the visited file attributes.
*/
protected void updateFileCounters(final Path file, final BasicFileAttributes attributes) {
pathCounters.getFileCounter().increment();
pathCounters.getByteCounter().add(attributes.size());
if (!Files.isSymbolicLink(file)) {
pathCounters.getByteCounter().add(attributes.size());
}
}

@Override
Expand Down
47 changes: 46 additions & 1 deletion src/main/java/org/apache/commons/io/file/PathUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
import org.apache.commons.io.file.Counters.PathCounters;
import org.apache.commons.io.file.attribute.FileTimes;
import org.apache.commons.io.filefilter.IOFileFilter;
import org.apache.commons.io.filefilter.TrueFileFilter;
import org.apache.commons.io.function.IOFunction;
import org.apache.commons.io.function.IOSupplier;

Expand Down Expand Up @@ -227,6 +228,8 @@ private RelativeSortedPaths(final Path dir1, final Path dir2, final int maxDepth
*/
public static final FileVisitOption[] EMPTY_FILE_VISIT_OPTION_ARRAY = {};

private static final Set<FileVisitOption> FOLLOW_LINKS_FILE_VISIT_OPTIONS = EnumSet.of(FileVisitOption.FOLLOW_LINKS);

/**
* Empty {@link LinkOption} array.
*/
Expand Down Expand Up @@ -368,6 +371,33 @@ public static long copy(final IOSupplier<InputStream> in, final Path target, fin

/**
* Copies a directory to another directory.
* <p>
* Symbolic links are either followed or copied, depending on the {@link LinkOption#NOFOLLOW_LINKS} option.
* Non-symbolic links, aka. hard links, are always copied, given that they appear as regular files to Java.
* {@code LinkOption} does not apply to hard links.
* Symbolic links can link to files or directories, while non-symbolic links can only link to files.
* Symbolic links can be (ab)used to create endlessly recursive directories.
* </p>
*
* <strong>Without {@link LinkOption#NOFOLLOW_LINKS} option (the default)</strong>
* <p>
* Given that Java defines {@link LinkOption#NOFOLLOW_LINKS} as an explicit option, the default is the absence of that option, which is to follow links.
* Symbolic links in the source directory are followed, resulting in a target directory that has no symbolic links.
* Cyclic symbolic links cause the copy operation to abort and throw a {@link java.nio.file.FileSystemLoopException}.
* Broken symbolic links are ignored; they are not copied.
* </p>
*
* <strong>With {@link LinkOption#NOFOLLOW_LINKS} option</strong>
* <p>
* Symbolic links in the source directory are copied to the target directory as symbolic links.
* Symbolic links linking inside the source directory are copied as relative links, meaning that the target symbolic
* link will link inside the target directory to a copied file or directory.
* Symbolic links linking outside the source directory are copied as absolute links, meaning that the target symbolic
* link will link outside the target directory to the same file or directory the link is linking to in the source directory.
* Cyclic symbolic links are preserved as regular symbolic links.
* Their cyclic nature is irrelevant to the copy operation.
* Broken symbolic links are ignored; they are not copied.
* </p>
*
* @param sourceDirectory The source directory.
* @param targetDirectory The target directory.
Expand All @@ -377,7 +407,22 @@ public static long copy(final IOSupplier<InputStream> in, final Path target, fin
*/
public static PathCounters copyDirectory(final Path sourceDirectory, final Path targetDirectory, final CopyOption... copyOptions) throws IOException {
final Path absoluteSource = sourceDirectory.toAbsolutePath();
return visitFileTree(new CopyDirectoryVisitor(Counters.longPathCounters(), absoluteSource, targetDirectory, copyOptions), absoluteSource)
// LinkOption.NOFOLLOW_LINKS is the explicit option, "follow symlinks" the implicit default.
// For FileVisitOption it's the other way around: FileVisitOption.FOLLOW_LINKS is the explicit option, and "do not follow symlinks" is the implicit
// default.
// If they're not in sync, the behavior is inconsistent, so we have to make sure they're in sync.
// CopyOption is given by the caller, FileVisitOption is under our control here, so we sync the latter to the former.
Set<FileVisitOption> fileVisitOptions = FOLLOW_LINKS_FILE_VISIT_OPTIONS;
if (copyOptions != null) {
for (CopyOption copyOption : copyOptions) {
if (copyOption == LinkOption.NOFOLLOW_LINKS) {
fileVisitOptions = Collections.emptySet();
break;
}
}
}
return visitFileTree(new CopyDirectoryVisitor(Counters.longPathCounters(), TrueFileFilter.INSTANCE, TrueFileFilter.INSTANCE, absoluteSource,
targetDirectory, copyOptions), absoluteSource, fileVisitOptions, Integer.MAX_VALUE)
.getPathCounters();
}

Expand Down
Loading
Loading