Skip to content

Commit ec4144b

Browse files
authored
[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
1 parent e8a0739 commit ec4144b

2 files changed

Lines changed: 59 additions & 10 deletions

File tree

src/main/java/org/apache/commons/io/FileUtils.java

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
import java.time.chrono.ChronoLocalDateTime;
5858
import java.time.chrono.ChronoZonedDateTime;
5959
import java.util.ArrayList;
60+
import java.util.Arrays;
6061
import java.util.Collection;
6162
import java.util.Collections;
6263
import java.util.Date;
@@ -297,7 +298,9 @@ private static void checkFileExists(final File file, final String name) throws F
297298
if (file.exists()) {
298299
throw new IllegalArgumentException("Parameter '" + name + "' is not a file: " + file);
299300
}
300-
throw new FileNotFoundException("Source '" + file + "' does not exist");
301+
if (!Files.isSymbolicLink(file.toPath())) {
302+
throw new FileNotFoundException("Source '" + file + "' does not exist");
303+
}
301304
}
302305
}
303306

@@ -504,6 +507,13 @@ public static File[] convertFileCollectionToFileArray(final Collection<File> fil
504507
* not guaranteed that the operation will succeed. If the modification operation fails, it falls back to
505508
* {@link File#setLastModified(long)}. If that fails, the method throws IOException.
506509
* </p>
510+
* <p>
511+
* Symbolic links in the source directory are copied to new symbolic links in the destination
512+
* directory that point to the original target. The target of the link is not copied unless
513+
* it is also under the source directory. Even if it is under the source directory, the new symbolic
514+
* link in the destination points to the original target in the source directory, not to the
515+
* newly created copy of the target.
516+
* </p>
507517
*
508518
* @param srcDir an existing directory to copy, must not be {@code null}.
509519
* @param destDir the new directory, must not be {@code null}.
@@ -816,7 +826,7 @@ public static void copyFile(final File srcFile, final File destFile, final boole
816826
* @param srcFile an existing file to copy, must not be {@code null}.
817827
* @param destFile the new file, must not be {@code null}.
818828
* @param preserveFileDate true if the file date of the copy should be the same as the original.
819-
* @param copyOptions options specifying how the copy should be done, for example {@link StandardCopyOption}..
829+
* @param copyOptions options specifying how the copy should be done, for example {@link StandardCopyOption}.
820830
* @throws NullPointerException if any of the given {@link File}s are {@code null}.
821831
* @throws FileNotFoundException if the source does not exist.
822832
* @throws IllegalArgumentException if source is not a file.
@@ -825,7 +835,7 @@ public static void copyFile(final File srcFile, final File destFile, final boole
825835
* @see #copyFileToDirectory(File, File, boolean)
826836
* @since 2.8.0
827837
*/
828-
public static void copyFile(final File srcFile, final File destFile, final boolean preserveFileDate, final CopyOption... copyOptions) throws IOException {
838+
public static void copyFile(final File srcFile, final File destFile, final boolean preserveFileDate, CopyOption... copyOptions) throws IOException {
829839
Objects.requireNonNull(destFile, "destination");
830840
checkFileExists(srcFile, "srcFile");
831841
requireCanonicalPathsNotEquals(srcFile, destFile);
@@ -834,10 +844,18 @@ public static void copyFile(final File srcFile, final File destFile, final boole
834844
checkFileExists(destFile, "destFile");
835845
requireCanWrite(destFile, "destFile");
836846
}
847+
848+
final boolean isSymLink = Files.isSymbolicLink(srcFile.toPath());
849+
if (isSymLink && !Arrays.asList(copyOptions).contains(LinkOption.NOFOLLOW_LINKS)) {
850+
final List<CopyOption> list = new ArrayList<CopyOption>(Arrays.asList(copyOptions));
851+
list.add(LinkOption.NOFOLLOW_LINKS);
852+
copyOptions = list.toArray(new CopyOption[0]);
853+
}
854+
837855
Files.copy(srcFile.toPath(), destFile.toPath(), copyOptions);
838856

839857
// On Windows, the last modified time is copied by default.
840-
if (preserveFileDate && !setTimes(srcFile, destFile)) {
858+
if (preserveFileDate && !isSymLink && !setTimes(srcFile, destFile)) {
841859
throw new IOException("Cannot set the file time.");
842860
}
843861
}

src/test/java/org/apache/commons/io/FileUtilsTest.java

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -757,6 +757,33 @@ public void testContentEqualsIgnoreEOL() throws Exception {
757757
assertTrue(FileUtils.contentEqualsIgnoreEOL(file1, file2, null));
758758
}
759759

760+
@Test
761+
public void testCopyDirectory_symLink() throws IOException {
762+
// Make a file
763+
final File sourceDirectory = new File(tempDirFile, "source_directory");
764+
sourceDirectory.mkdir();
765+
final File targetFile = new File(sourceDirectory, "hello.txt");
766+
FileUtils.writeStringToFile(targetFile, "HELLO WORLD", "UTF8");
767+
768+
// Make a symlink to the file
769+
final Path targetPath = targetFile.toPath();
770+
final Path linkPath = sourceDirectory.toPath().resolve("linkfile");
771+
Files.createSymbolicLink(linkPath, targetPath);
772+
assumeTrue(Files.isSymbolicLink(linkPath), () -> "Expected a symlink here: " + linkPath);
773+
assumeTrue(Files.exists(linkPath));
774+
assumeTrue(Files.exists(linkPath, LinkOption.NOFOLLOW_LINKS));
775+
776+
// Now copy sourceDirectory, including the broken link, to another directory
777+
final File destination = new File(tempDirFile, "destination");
778+
FileUtils.copyDirectory(sourceDirectory, destination);
779+
assertTrue(destination.exists());
780+
final Path copiedSymlink = new File(destination, "linkfile").toPath();
781+
782+
// test for the existence of the copied symbolic link as a link
783+
assertTrue(Files.isSymbolicLink(copiedSymlink));
784+
assertTrue(Files.exists(copiedSymlink));
785+
}
786+
760787
/**
761788
* Tests IO-807.
762789
*/
@@ -783,12 +810,16 @@ public void testCopyDirectory_brokenSymLink() throws IOException {
783810

784811
// Now copy sourceDirectory, including the broken link, to another directory
785812
final File destination = new File(tempDirFile, "destination");
786-
final FileNotFoundException thrown = assertThrows(
787-
FileNotFoundException.class,
788-
() -> FileUtils.copyDirectory(sourceDirectory, destination),
789-
"ignored broken link"
790-
);
791-
assertTrue(thrown.getMessage().contains("linkfile' does not exist"));
813+
FileUtils.copyDirectory(sourceDirectory, destination);
814+
assertTrue(destination.exists());
815+
final Path copiedBrokenSymlink = new File(destination, "linkfile").toPath();
816+
817+
// test for the existence of the copied symbolic link as a link
818+
assertTrue(Files.isSymbolicLink(copiedBrokenSymlink));
819+
820+
// shouldn't be able to read through to the source of the link.
821+
// If we can, then the link points somewhere other than the deleted file
822+
assertFalse(Files.exists(copiedBrokenSymlink));
792823
}
793824

794825
@Test

0 commit comments

Comments
 (0)