diff --git a/src/main/java/org/apache/commons/io/FileUtils.java b/src/main/java/org/apache/commons/io/FileUtils.java index 623af97a512..6053962a318 100644 --- a/src/main/java/org/apache/commons/io/FileUtils.java +++ b/src/main/java/org/apache/commons/io/FileUtils.java @@ -507,11 +507,12 @@ public static File[] convertFileCollectionToFileArray(final Collection fil * {@link File#setLastModified(long)}. If that fails, the method throws IOException. *

*

- * Symbolic links in the source directory are copied to new symbolic links in the destination - * directory that point to the original target. The target of the link is not copied unless - * it is also under the source directory. Even if it is under the source directory, the new symbolic - * link in the destination points to the original target in the source directory, not to the - * newly created copy of the target. + * Treatment of symbolic links inside {@code srcDir} depends on the target of the link. + * If the link points to a target outside of {@code srcDir} (and therefore is not copied), + * then {@code destDir} will contain link to the original, uncopied file. + * However, if the symbolic links point to a file inside {@code srcDir}, + * the new link inside {@code destDir} will point to the copy of the target inside + * {@code destDir}. *

* * @param srcDir an existing directory to copy, must not be {@code null}. @@ -536,6 +537,14 @@ public static void copyDirectory(final File srcDir, final File destDir) throws I * method merges the source with the destination, with the source taking precedence. *

*

+ * Treatment of symbolic links inside {@code srcDir} depends on the target of the link. + * If the link points to a target outside of {@code srcDir} (and therefore is not copied), + * then {@code destDir} will contain link to the original, uncopied file. + * However, if the symbolic links point to a file inside {@code srcDir}, + * the new link inside {@code destDir} will point to the copy of the target inside + * {@code destDir}. + *

+ *

* Note: Setting {@code preserveFileDate} to {@code true} tries to preserve the files' last * modified date/times using {@link File#setLastModified(long)}. However it is not guaranteed that those operations * will succeed. If the modification operation fails, the method throws IOException. @@ -565,6 +574,14 @@ public static void copyDirectory(final File srcDir, final File destDir, final bo * method merges the source with the destination, with the source taking precedence. *

*

+ * Treatment of symbolic links inside {@code srcDir} depends on the target of the link. + * If the link points to a target outside of {@code srcDir} (and therefore is not copied), + * then {@code destDir} will contain link to the original, uncopied file. + * However, if the symbolic links point to a file inside {@code srcDir}, + * the new link inside {@code destDir} will point to the copy of the target inside + * {@code destDir}. + *

+ *

* Note: This method tries to preserve the files' last modified date/times using * {@link File#setLastModified(long)}. However it is not guaranteed that those operations will succeed. If the * modification operation fails, the method throws IOException. @@ -614,6 +631,14 @@ public static void copyDirectory(final File srcDir, final File destDir, final Fi * method merges the source with the destination, with the source taking precedence. *

*

+ * Treatment of symbolic links inside {@code srcDir} depends on the target of the link. + * If the link points to a target outside of {@code srcDir} (and therefore is not copied), + * then {@code destDir} will contain link to the original, uncopied file. + * However, if the symbolic links point to a file inside {@code srcDir}, + * the new link inside {@code destDir} will point to the copy of the target inside + * {@code destDir}. + *

+ *

* Note: Setting {@code preserveFileDate} to {@code true} tries to preserve the file's last * modified date/times using {@link BasicFileAttributeView#setTimes(FileTime, FileTime, FileTime)}. However, it is * not guaranteed that the operation will succeed. If the modification operation fails it falls back to @@ -664,6 +689,14 @@ public static void copyDirectory(final File srcDir, final File destDir, final Fi * method merges the source with the destination, with the source taking precedence. *

*

+ * Treatment of symbolic links inside {@code srcDir} depends on the target of the link. + * If the link points to a target outside of {@code srcDir} (and therefore is not copied), + * then {@code destDir} will contain link to the original, uncopied file. + * However, if the symbolic links point to a file inside {@code srcDir}, + * the new link inside {@code destDir} will point to the copy of the target inside + * {@code destDir}. + *

+ *

* Note: Setting {@code preserveFileDate} to {@code true} tries to preserve the file's last * modified date/times using {@link BasicFileAttributeView#setTimes(FileTime, FileTime, FileTime)}. However, it is * not guaranteed that the operation will succeed. If the modification operation fails it falls back to @@ -720,7 +753,7 @@ public static void copyDirectory(final File srcDir, final File destDir, final Fi } } } - doCopyDirectory(srcDir, destDir, fileFilter, exclusionList, preserveFileDate, copyOptions); + doCopyDirectoryFromRoot(srcDir, destDir, fileFilter, exclusionList, preserveFileDate, copyOptions); } /** @@ -734,6 +767,14 @@ public static void copyDirectory(final File srcDir, final File destDir, final Fi * method merges the source with the destination, with the source taking precedence. *

*

+ * Treatment of symbolic links inside {@code srcDir} depends on the target of the link. + * If the link points to a target outside of {@code srcDir} (and therefore is not copied), + * then {@code destDir} will contain link to the original, uncopied file. + * However, if the symbolic links point to a file inside {@code srcDir}, + * the new link inside {@code destDir} will point to the copy of the target inside + * {@code destDir}. + *

+ *

* Note: Setting {@code preserveFileDate} to {@code true} tries to preserve the file's last * modified date/times using {@link BasicFileAttributeView#setTimes(FileTime, FileTime, FileTime)}. However, it is * not guaranteed that the operation will succeed. If the modification operation fails it falls back to @@ -1323,6 +1364,13 @@ public static boolean directoryContains(final File directory, final File child) return FilenameUtils.directoryContains(directory.getCanonicalPath(), child.getCanonicalPath()); } + // Splits src and dest directories into two variables each: one that is modified as we descend + // through the directory tree and one that isn't + private static void doCopyDirectoryFromRoot(final File srcDir, final File destDir, final FileFilter fileFilter, final List exclusionList, + final boolean preserveDirDate, final CopyOption... copyOptions) throws IOException { + doCopyDirectory(srcDir, destDir, fileFilter, exclusionList, preserveDirDate, srcDir, destDir, copyOptions); + } + /** * Internal copy directory method. Creates all destination parent directories, * including any necessary but non-existent parent directories. @@ -1332,12 +1380,14 @@ public static boolean directoryContains(final File directory, final File child) * @param fileFilter the filter to apply, null means copy all directories and files. * @param exclusionList List of files and directories to exclude from the copy, may be null. * @param preserveDirDate preserve the directories last modified dates. + * @param srcRoot the top directory being copied from, preserved during recursion + * @param destRoot the top directory being copied to, preserved during recursion * @param copyOptions options specifying how the copy should be done, see {@link StandardCopyOption}. * @throws IOException if the directory was not created along with all its parent directories. * @throws SecurityException See {@link File#mkdirs()}. */ private static void doCopyDirectory(final File srcDir, final File destDir, final FileFilter fileFilter, final List exclusionList, - final boolean preserveDirDate, final CopyOption... copyOptions) throws IOException { + final boolean preserveDirDate, final File srcRoot, final File destRoot, final CopyOption... copyOptions) throws IOException { // recurse dirs, copy files. final File[] srcFiles = listFiles(srcDir, fileFilter); requireDirectoryIfExists(destDir, "destDir"); @@ -1347,7 +1397,20 @@ private static void doCopyDirectory(final File srcDir, final File destDir, final final File dstFile = new File(destDir, srcFile.getName()); if (exclusionList == null || !exclusionList.contains(srcFile.getCanonicalPath())) { if (srcFile.isDirectory()) { - doCopyDirectory(srcFile, dstFile, fileFilter, exclusionList, preserveDirDate, copyOptions); + doCopyDirectory(srcFile, dstFile, fileFilter, exclusionList, preserveDirDate, srcRoot, destRoot, copyOptions); + } else if (Files.isSymbolicLink(srcFile.toPath())) { + final Path linkTarget = Files.readSymbolicLink(srcFile.toPath()); + if (directoryContains(srcRoot, linkTarget.toFile())) { + // make a new link that points to the copy of the target + final Path srcFileRelativePath = srcRoot.toPath().relativize(srcFile.toPath()); + final Path linkTargetRelativePath = srcRoot.toPath().relativize(linkTarget); + final Path newLink = destRoot.toPath().resolve(srcFileRelativePath); + final Path newTarget = destRoot.toPath().resolve(linkTargetRelativePath); + Files.createSymbolicLink(newLink, newTarget); + } else { + copyFile(srcFile, dstFile, preserveDirDate, copyOptions); + } + } else { copyFile(srcFile, dstFile, preserveDirDate, copyOptions); } diff --git a/src/test/java/org/apache/commons/io/FileUtilsTest.java b/src/test/java/org/apache/commons/io/FileUtilsTest.java index 2e7e48ff732..66cec142b2b 100644 --- a/src/test/java/org/apache/commons/io/FileUtilsTest.java +++ b/src/test/java/org/apache/commons/io/FileUtilsTest.java @@ -782,6 +782,79 @@ public void testCopyDirectory_symLinkExternalFile() throws Exception { assertEquals(content.toPath(), source); } + /** + * Test what happens when copyDirectory copies a directory that contains a symlink + * to a file inside the copied directory. + */ + @Test + public void testCopyDirectory_symLinkInternalFile() throws Exception { + // Make a directory + final File realDirectory = new File(tempDirFile, "real_directory"); + realDirectory.mkdir(); + + // make a file + final File content = new File(realDirectory, "hello.txt"); + FileUtils.writeStringToFile(content, "HELLO WORLD", "UTF8"); + + // Make a symlink to the file + final Path linkPath = realDirectory.toPath().resolve("link_to_file"); + Files.createSymbolicLink(linkPath, content.toPath()); + + // Now copy the directory + final File destination = new File(tempDirFile, "destination"); + FileUtils.copyDirectory(realDirectory, destination); + + // delete the original + assumeTrue(content.delete()); + + // test that the copied directory contains a link to the copied file + final File copiedLink = new File(destination, "link_to_file"); + assertTrue(Files.isSymbolicLink(copiedLink.toPath())); + final String actual = FileUtils.readFileToString(copiedLink, "UTF8"); + assertEquals("HELLO WORLD", actual); + + final Path source = Files.readSymbolicLink(copiedLink.toPath()); + assertNotEquals(content.toPath(), source); + final File copiedContent = new File(destination, "hello.txt"); + assertEquals(copiedContent.toPath(), source); + } + + @Test + public void testCopyDirectory_symLinkInternalFile_nested() throws Exception { + // Make a directory + final File originalDirectory = new File(tempDirFile, "original_directory"); + final File subDirectoryA = new File(originalDirectory, "A"); + final File subDirectoryB = new File(originalDirectory, "B"); + subDirectoryB.mkdirs(); + + // make a file + final File content = new File(subDirectoryA, "hello.txt"); + FileUtils.writeStringToFile(content, "HELLO WORLD", "UTF8"); + + // Make a symlink to the file + final Path linkPath = subDirectoryB.toPath().resolve("link_to_file"); + Files.createSymbolicLink(linkPath, content.toPath()); + + // Now copy the directory + final File copiedDirectory = new File(tempDirFile, "copied_directory"); + FileUtils.copyDirectory(originalDirectory, copiedDirectory); + + // delete the original + content.delete(); + + // test that the copied directory contains a link to the copied file + final File copiedLink = new File(new File(copiedDirectory, "B"), "link_to_file"); + assertTrue(Files.isSymbolicLink(copiedLink.toPath())); + + final Path source = Files.readSymbolicLink(copiedLink.toPath()); + assertNotEquals(content.toPath(), source); + final File copiedContent = new File(new File(copiedDirectory, "A"), "hello.txt"); + assertEquals(copiedContent.toPath(), source); + + final String actual = FileUtils.readFileToString(copiedLink, "UTF8"); + assertEquals("HELLO WORLD", actual); + } + /** * See what happens when copyDirectory copies a directory that is a symlink * to another directory containing non-symlinked files. @@ -790,10 +863,9 @@ public void testCopyDirectory_symLinkExternalFile() throws Exception { * and should not be relied on. */ @Test - public void testCopyDir_symLink() throws Exception { + public void testCopyDirectory_symLink2() throws Exception { // Make a directory final File realDirectory = new File(tempDirFile, "real_directory"); - realDirectory.mkdir(); final File content = new File(realDirectory, "hello.txt"); FileUtils.writeStringToFile(content, "HELLO WORLD", "UTF8"); @@ -818,7 +890,7 @@ public void testCopyDir_symLink() throws Exception { } @Test - public void testCopyDir_symLinkCycle() throws Exception { + public void testCopyDirectory_symLinkCycle() throws Exception { // Make a directory final File topDirectory = new File(tempDirFile, "topDirectory"); topDirectory.mkdir(); @@ -890,7 +962,6 @@ public void testCopyDirectory_brokenSymLink() throws IOException { public void testCopyDirectory_symLink() throws IOException { // Make a file final File sourceDirectory = new File(tempDirFile, "source_directory"); - sourceDirectory.mkdir(); final File targetFile = new File(sourceDirectory, "hello.txt"); FileUtils.writeStringToFile(targetFile, "HELLO WORLD", "UTF8"); @@ -898,9 +969,6 @@ public void testCopyDirectory_symLink() throws IOException { final Path targetPath = targetFile.toPath(); final Path linkPath = sourceDirectory.toPath().resolve("linkfile"); Files.createSymbolicLink(linkPath, targetPath); - assumeTrue(Files.isSymbolicLink(linkPath), () -> "Expected a symlink here: " + linkPath); - assumeTrue(Files.exists(linkPath)); - assumeTrue(Files.exists(linkPath, LinkOption.NOFOLLOW_LINKS)); // Now copy sourceDirectory to another directory final File destination = new File(tempDirFile, "destination"); @@ -909,8 +977,8 @@ public void testCopyDirectory_symLink() throws IOException { final Path copiedSymlink = new File(destination, "linkfile").toPath(); // test for the existence of the copied symbolic link as a link - assertTrue(Files.isSymbolicLink(copiedSymlink)); assertTrue(Files.exists(copiedSymlink)); + assertTrue(Files.isSymbolicLink(copiedSymlink)); } @Test