[IO-807] Copy symlinks, not the files the symlinks point to (#558)
* update test to reflect desired behavior of copying broken symlinks
* [IO-807] copy symbolic links as links
* pmd
* typo
* add test for unbroken link
diff --git a/src/main/java/org/apache/commons/io/FileUtils.java b/src/main/java/org/apache/commons/io/FileUtils.java
index 2b3a554..5a26fc2 100644
--- a/src/main/java/org/apache/commons/io/FileUtils.java
+++ b/src/main/java/org/apache/commons/io/FileUtils.java
@@ -57,6 +57,7 @@
import java.time.chrono.ChronoLocalDateTime;
import java.time.chrono.ChronoZonedDateTime;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
@@ -297,7 +298,9 @@
if (file.exists()) {
throw new IllegalArgumentException("Parameter '" + name + "' is not a file: " + file);
}
- throw new FileNotFoundException("Source '" + file + "' does not exist");
+ if (!Files.isSymbolicLink(file.toPath())) {
+ throw new FileNotFoundException("Source '" + file + "' does not exist");
+ }
}
}
@@ -504,6 +507,13 @@
* not guaranteed that the operation will succeed. If the modification operation fails, it falls back to
* {@link File#setLastModified(long)}. If that fails, the method throws IOException.
* </p>
+ * <p>
+ * 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.
+ * </p>
*
* @param srcDir an existing directory to copy, must not be {@code null}.
* @param destDir the new directory, must not be {@code null}.
@@ -816,7 +826,7 @@
* @param srcFile an existing file to copy, must not be {@code null}.
* @param destFile the new file, must not be {@code null}.
* @param preserveFileDate true if the file date of the copy should be the same as the original.
- * @param copyOptions options specifying how the copy should be done, for example {@link StandardCopyOption}..
+ * @param copyOptions options specifying how the copy should be done, for example {@link StandardCopyOption}.
* @throws NullPointerException if any of the given {@link File}s are {@code null}.
* @throws FileNotFoundException if the source does not exist.
* @throws IllegalArgumentException if source is not a file.
@@ -825,7 +835,7 @@
* @see #copyFileToDirectory(File, File, boolean)
* @since 2.8.0
*/
- public static void copyFile(final File srcFile, final File destFile, final boolean preserveFileDate, final CopyOption... copyOptions) throws IOException {
+ public static void copyFile(final File srcFile, final File destFile, final boolean preserveFileDate, CopyOption... copyOptions) throws IOException {
Objects.requireNonNull(destFile, "destination");
checkFileExists(srcFile, "srcFile");
requireCanonicalPathsNotEquals(srcFile, destFile);
@@ -834,10 +844,18 @@
checkFileExists(destFile, "destFile");
requireCanWrite(destFile, "destFile");
}
+
+ final boolean isSymLink = Files.isSymbolicLink(srcFile.toPath());
+ if (isSymLink && !Arrays.asList(copyOptions).contains(LinkOption.NOFOLLOW_LINKS)) {
+ final List<CopyOption> list = new ArrayList<CopyOption>(Arrays.asList(copyOptions));
+ list.add(LinkOption.NOFOLLOW_LINKS);
+ copyOptions = list.toArray(new CopyOption[0]);
+ }
+
Files.copy(srcFile.toPath(), destFile.toPath(), copyOptions);
// On Windows, the last modified time is copied by default.
- if (preserveFileDate && !setTimes(srcFile, destFile)) {
+ if (preserveFileDate && !isSymLink && !setTimes(srcFile, destFile)) {
throw new IOException("Cannot set the file time.");
}
}
diff --git a/src/test/java/org/apache/commons/io/FileUtilsTest.java b/src/test/java/org/apache/commons/io/FileUtilsTest.java
index e7b3202..71811ac 100644
--- a/src/test/java/org/apache/commons/io/FileUtilsTest.java
+++ b/src/test/java/org/apache/commons/io/FileUtilsTest.java
@@ -757,6 +757,33 @@
assertTrue(FileUtils.contentEqualsIgnoreEOL(file1, file2, null));
}
+ @Test
+ 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");
+
+ // Make a symlink to the file
+ 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, including the broken link, to another directory
+ final File destination = new File(tempDirFile, "destination");
+ FileUtils.copyDirectory(sourceDirectory, destination);
+ assertTrue(destination.exists());
+ 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));
+ }
+
/**
* Tests IO-807.
*/
@@ -783,12 +810,16 @@
// Now copy sourceDirectory, including the broken link, to another directory
final File destination = new File(tempDirFile, "destination");
- final FileNotFoundException thrown = assertThrows(
- FileNotFoundException.class,
- () -> FileUtils.copyDirectory(sourceDirectory, destination),
- "ignored broken link"
- );
- assertTrue(thrown.getMessage().contains("linkfile' does not exist"));
+ FileUtils.copyDirectory(sourceDirectory, destination);
+ assertTrue(destination.exists());
+ final Path copiedBrokenSymlink = new File(destination, "linkfile").toPath();
+
+ // test for the existence of the copied symbolic link as a link
+ assertTrue(Files.isSymbolicLink(copiedBrokenSymlink));
+
+ // shouldn't be able to read through to the source of the link.
+ // If we can, then the link points somewhere other than the deleted file
+ assertFalse(Files.exists(copiedBrokenSymlink));
}
@Test