[
https://issues.apache.org/jira/browse/IO-885?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18053975#comment-18053975
]
Peter De Maeyer edited comment on IO-885 at 1/23/26 6:25 PM:
-------------------------------------------------------------
{quote}
As IO-845 investigates, "an exact 1:1 copy of a self-contained directory tree
including symlinks" is not well defined. There are different reasonable
interpretations of what an exact 1:1 copy is.
{quote}
I think it _is_ quite well defined in a number of common use cases. Please
consider:
* The {{LinkOption.NOFOLLOW_LINKS}} which allows differentiation behavior.
* I deliberately wrote the adjective "self-contained" meaning "not containing
links outside of the directory tree". In that case, it _is_ well defined.
Also consider default JDK behavior, which we can use as inspiration:
{code:java}
@Test
void copySymbolicLinkToFile(@TempDir Path tempDir) throws Exception {
final Path file = Files.createFile(tempDir.resolve("file"));
final Path symbolicLink =
Files.createSymbolicLink(tempDir.resolve("symbolic-link"), file);
final Path copy = tempDir.resolve("copy");
Files.copy(symbolicLink, copy);
assertFalse(Files.isSymbolicLink(copy));
}
@Test
void copySymbolicLinkToFileWithNoFollowLinks(@TempDir Path tempDir) throws
Exception {
final Path file = Files.createFile(tempDir.resolve("file"));
final Path symbolicLink =
Files.createSymbolicLink(tempDir.resolve("symbolic-link"), file);
final Path copy = tempDir.resolve("copy");
Files.copy(symbolicLink, copy, NOFOLLOW_LINKS);
assertTrue(Files.isSymbolicLink(copy));
}
@Test
void copySymbolicLinkToDirectory(@TempDir Path tempDir) throws Exception {
final Path dir = Files.createDirectory(tempDir.resolve("dir"));
final Path symbolicLink =
Files.createSymbolicLink(tempDir.resolve("symbolic-link"), dir);
Files.createFile(dir.resolve("file"));
final Path copy = tempDir.resolve("copy");
Files.copy(symbolicLink, copy);
assertFalse(Files.isSymbolicLink(copy));
assertFalse(Files.exists(copy.resolve("file")));
}
@Test
void copySymbolicLinkToDirectoryWithNoFollowSymlinks(@TempDir Path tempDir)
throws Exception {
final Path dir = Files.createDirectory(tempDir.resolve("dir"));
final Path symbolicLink =
Files.createSymbolicLink(tempDir.resolve("symbolic-link"), dir);
Files.createFile(dir.resolve("file"));
final Path copy = tempDir.resolve("copy");
Files.copy(symbolicLink, copy, NOFOLLOW_LINKS);
assertTrue(Files.isSymbolicLink(copy));
assertTrue(Files.exists(copy.resolve("file")));
}
{code}
was (Author: peterdm):
{quote}
As IO-845 investigates, "an exact 1:1 copy of a self-contained directory tree
including symlinks" is not well defined. There are different reasonable
interpretations of what an exact 1:1 copy is.
{quote}
I think it _is_ quite well defined in a number of common use cases. Please
consider:
* The {{LinkOption.NOFOLLOW_LINKS}} which allows differentiation behavior.
* I deliberately wrote the adjective "self-contained" meaning "not containing
links outside of the directory tree". In that case, it _is_ well defined.
Also consider default JDK behavior, which we can use as inspiration:
{code:java}
@Test
void copySymbolicLinkToFile(@TempDir Path tempDir) throws IOException {
final Path file = Files.createFile(tempDir.resolve("file"));
final Path symbolicLink =
Files.createSymbolicLink(tempDir.resolve("symbolic-link"), file);
final Path copy = tempDir.resolve("copy");
Files.copy(symbolicLink, copy);
assertFalse(Files.isSymbolicLink(copy));
}
@Test
void copySymbolicLinkToFileWithNoFollowLinks(@TempDir Path tempDir) throws
IOException {
final Path file = Files.createFile(tempDir.resolve("file"));
final Path symbolicLink =
Files.createSymbolicLink(tempDir.resolve("symbolic-link"), file);
final Path copy = tempDir.resolve("copy");
Files.copy(symbolicLink, copy, NOFOLLOW_LINKS);
assertTrue(Files.isSymbolicLink(copy));
}
@Test
void copySymbolicLinkToDirectory(@TempDir Path tempDir) throws IOException {
final Path dir = Files.createDirectory(tempDir.resolve("dir"));
final Path symbolicLink =
Files.createSymbolicLink(tempDir.resolve("symbolic-link"), dir);
Files.createFile(dir.resolve("file"));
final Path copy = tempDir.resolve("copy");
Files.copy(symbolicLink, copy);
assertFalse(Files.isSymbolicLink(copy));
assertFalse(Files.exists(copy.resolve("file")));
}
@Test
void copySymbolicLinkToDirectoryWithNoFollowSymlinks(@TempDir Path tempDir)
throws IOException {
final Path dir = Files.createDirectory(tempDir.resolve("dir"));
final Path symbolicLink =
Files.createSymbolicLink(tempDir.resolve("symbolic-link"), dir);
Files.createFile(dir.resolve("file"));
final Path copy = tempDir.resolve("copy");
Files.copy(symbolicLink, copy, NOFOLLOW_LINKS);
assertTrue(Files.isSymbolicLink(copy));
assertTrue(Files.exists(copy.resolve("file")));
}
{code}
> PathUtils.copyDirectory with NOFOLLOW_LINKS ignores symlinks
> ------------------------------------------------------------
>
> Key: IO-885
> URL: https://issues.apache.org/jira/browse/IO-885
> Project: Commons IO
> Issue Type: Bug
> Components: Utilities
> Affects Versions: 2.20.0, 2.21.0
> Reporter: Peter De Maeyer
> Priority: Major
>
> Before 2.20.0, {{PathUtils.copyDirectory}} with {{NOFOLLOW_LINKS}} preserved
> symlinks.{^}(1)^ This supported the common use case of making an exact 1:1
> copy of a self-contained directory tree including symlinks, such as for
> example a {{java/}} installation of OpenJDK 17. This is illustrated by the
> following test:
> {code:java}
> import static java.nio.file.Files.createDirectory;
> import static java.nio.file.Files.createFile;
> import static java.nio.file.Files.createSymbolicLink;
> import static java.nio.file.Files.exists;
> import static java.nio.file.Files.isSymbolicLink;
> import static java.nio.file.LinkOption.NOFOLLOW_LINKS;
> import static org.apache.commons.io.file.PathUtils.copyDirectory;
> import static org.junit.jupiter.api.Assertions.assertTrue;
>
> import java.nio.file.Path;
>
> import org.junit.jupiter.api.Test;
> import org.junit.jupiter.api.io.TempDir;
>
> class PathUtilsTest {
> @Test
> void copyDirectoryPreservesSymlinks(@TempDir Path tempDir) throws
> Exception {
> Path sourceDir = createDirectory(tempDir.resolve("source"));
> Path dir = createDirectory(sourceDir.resolve("dir"));
> Path dirLink = createSymbolicLink(sourceDir.resolve("link-to-dir"),
> dir);
> Path file = createFile(dir.resolve("file"));
> Path fileLink = createSymbolicLink(dir.resolve("link-to-file"), file);
> Path targetDir = tempDir.resolve("target");
> copyDirectory(sourceDir, targetDir, NOFOLLOW_LINKS);
> Path copyOfDir = targetDir.resolve("dir");
> assertTrue(exists(copyOfDir));
> Path copyOfDirLink = targetDir.resolve("link-to-dir");
> assertTrue(exists(copyOfDirLink));
> assertTrue(isSymbolicLink(copyOfDirLink));
> Path copyOfFileLink = copyOfDir.resolve("link-to-file");
> assertTrue(exists(copyOfFileLink));
> assertTrue(isSymbolicLink(copyOfFileLink));
> }
> }
> {code}
> This behavior changed in 2.20.0, so that it is now _impossible_ to make a 1:1
> copy whilst preserving symlinks, no matter what copy or link options you try.
> The above test thus fails.
> Explaining it in words, given a {{source/}} directory tree:
> {noformat}
> source/
> dir/
> file
> symlink-to-file
> symlink-to-dir
> {noformat}
> it was possible to copy it using {{NOFOLLOW_SYMLINKS}} to:
> {noformat}
> target/
> dir/
> file
> symlink-to-file
> symlink-to-dir
> {noformat}
> That is expected and intuitive behavior.
> Since 2.20.0 that behavior broke and now results in the following:
> {noformat}
> target/
> dir/
> file
> {noformat}
> Notice the missing symlinks.
> I didn't try, but I suspect the same applies to non-symbolic (hard) links.
> I consider this a regression that needs to be fixed.
> This issue is related to IO-845, which is not settled yet thus inconclusive.
> The behavior is undefined. That is a pity, I hereby request to _define_ that
> behavior so that there is at least a way to copy preserving symlinks. It
> essentially boils down to Eliotte's analysis in
> [this|https://issues.apache.org/jira/browse/IO-845?focusedCommentId=17813679&page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel#comment-17813679]
> comment, with the additional remark that it should be dependent on the
> {{NOFOLLOW_LINKS}} option:
> * When {{NOFOLLOW_LINKS}}: then behave as Eliotte's option 2. In this case,
> circular (sym)links are handled gracefully, because they're just preserved as
> circular symlinks on the target.
> * When no option, then the behavior implicitly means "follow symlinks". Then
> behave as "going down the rabbit hole", flattenig/resolving symlinks into
> their actual content on the target. That (sort of) addresses [Gary's
> comment|https://issues.apache.org/jira/browse/IO-845?focusedCommentId=17830158&page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel#comment-17830158]
> I think. This option has the drawback that in case of circular symlinks, you
> may end up in an endless recursive loop, but hey, that's a consequence what
> you asked for so acceptable IMO.{^}(2)^
> ----
> ^(1)^ _Not_ specifying {{NOFOLLOW_LINKS}} resulted in "going down the rabbit
> hole" and copying the linked content instead of the link, which is usually
> _not_ what you want, but anyway, that's the behavior that the JDK chose as
> default when they made the implicit "follow symlinks" the default and
> {{NOFOLLOW_LINKS}} the explicit option. Not a choice I particularly agree
> with, but I can understand it was made for portability reasons where the
> target directory may be on a file system that does not support symlinks.
> ^(2)^ Alternatively, we could define additional custom {{CopyOptions}} or
> {{LinkOptions}} to allow more fine-grained control over the behavior, but it
> would complicate matters too much, also in terms of having to define the
> semantic of all combinations of options. It would become too difficult to
> understand, so I don't advise it.
--
This message was sent by Atlassian Jira
(v8.20.10#820010)