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
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import java.util.Objects;

import org.apache.commons.io.file.Counters.PathCounters;
import org.apache.commons.io.filefilter.TrueFileFilter;

/**
* Copies a source directory to a target directory.
Expand All @@ -53,7 +54,7 @@ private static CopyOption[] toCopyOption(final CopyOption... copyOptions) {
* @param copyOptions Specifies how the copying should be done.
*/
public CopyDirectoryVisitor(final PathCounters pathCounter, final Path sourceDirectory, final Path targetDirectory, final CopyOption... copyOptions) {
super(pathCounter);
super(pathCounter, TrueFileFilter.INSTANCE, TrueFileFilter.INSTANCE);
this.sourceDirectory = sourceDirectory;
this.targetDirectory = targetDirectory;
this.copyOptions = toCopyOption(copyOptions);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,14 @@ protected void updateDirCounter(final Path dir, final IOException exc) {
*/
protected void updateFileCounters(final Path file, final BasicFileAttributes attributes) {
pathCounters.getFileCounter().increment();
pathCounters.getByteCounter().add(attributes.size());
// According to the JavaDoc, 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 NOT what we want to count here.
// Intuitively, the appropriate check would be Files.isRegularFile.
// However, for symbolic links, isRegularFile returns true under a "follow links" regime.
// That would still not give us what we want, so instead we settle for a !Files.isSymbolicLink check.
if (!Files.isSymbolicLink(file)) {
pathCounters.getByteCounter().add(attributes.size());
}
}

@Override
Expand Down
40 changes: 39 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 @@ -227,6 +227,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 +370,27 @@ public static long copy(final IOSupplier<InputStream> in, final Path target, fin

/**
* Copies a directory to another directory.
* 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.
*
* <h4>Without {@link LinkOption#NOFOLLOW_LINKS} option (the default)</h4>
* 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.
*
* <h4>With {@link LinkOption#NOFOLLOW_LINKS} option</h4>
* 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.
*
* @param sourceDirectory The source directory.
* @param targetDirectory The target directory.
Expand All @@ -377,7 +400,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)
// CopyOption.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 (LinkOption.NOFOLLOW_LINKS.equals(copyOption)) {
fileVisitOptions = Collections.emptySet();
break;
}
}
}
return visitFileTree(new CopyDirectoryVisitor(Counters.longPathCounters(), absoluteSource, targetDirectory, copyOptions), absoluteSource,
fileVisitOptions, Integer.MAX_VALUE)
.getPathCounters();
}

Expand Down
Loading