Skip to content

Commit 22f6525

Browse files
tresfgarydgregory
andauthored
[IO-769] FileUtils copyDirectory() should NOT use COPY_ATTRIBUTES (#377)
* Initial commit: Don't copy all attributes, only timestamp information * Fix docs, formatting, add unit tests * Format: "catch(" -> "catch (" --------- Co-authored-by: Gary Gregory <garydgregory@users.noreply.github.com>
1 parent a6de42b commit 22f6525

2 files changed

Lines changed: 92 additions & 89 deletions

File tree

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

Lines changed: 78 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@
4343
import java.nio.file.NotDirectoryException;
4444
import java.nio.file.Path;
4545
import java.nio.file.StandardCopyOption;
46+
import java.nio.file.attribute.BasicFileAttributeView;
47+
import java.nio.file.attribute.BasicFileAttributes;
4648
import java.nio.file.attribute.FileTime;
4749
import java.time.Duration;
4850
import java.time.Instant;
@@ -54,7 +56,6 @@
5456
import java.time.chrono.ChronoLocalDateTime;
5557
import java.time.chrono.ChronoZonedDateTime;
5658
import java.util.ArrayList;
57-
import java.util.Arrays;
5859
import java.util.Collection;
5960
import java.util.Collections;
6061
import java.util.Date;
@@ -199,23 +200,6 @@ public class FileUtils {
199200
*/
200201
public static final File[] EMPTY_FILE_ARRAY = {};
201202

202-
/**
203-
* Copies the given array and adds StandardCopyOption.COPY_ATTRIBUTES.
204-
*
205-
* @param copyOptions sorted copy options.
206-
* @return a new array.
207-
*/
208-
private static CopyOption[] addCopyAttributes(final CopyOption... copyOptions) {
209-
// Make a copy first since we don't want to sort the call site's version.
210-
final CopyOption[] actual = Arrays.copyOf(copyOptions, copyOptions.length + 1);
211-
Arrays.sort(actual, 0, copyOptions.length);
212-
if (Arrays.binarySearch(copyOptions, 0, copyOptions.length, StandardCopyOption.COPY_ATTRIBUTES) >= 0) {
213-
return copyOptions;
214-
}
215-
actual[actual.length - 1] = StandardCopyOption.COPY_ATTRIBUTES;
216-
return actual;
217-
}
218-
219203
/**
220204
* Returns a human-readable version of the file size, where the input represents a specific number of bytes.
221205
* <p>
@@ -489,9 +473,10 @@ public static File[] convertFileCollectionToFileArray(final Collection<File> fil
489473
* method merges the source with the destination, with the source taking precedence.
490474
* </p>
491475
* <p>
492-
* <strong>Note:</strong> This method tries to preserve the files' last modified date/times using
493-
* {@link File#setLastModified(long)}, however it is not guaranteed that those operations will succeed. If the
494-
* modification operation fails, the methods throws IOException.
476+
* <strong>Note:</strong> Setting {@code preserveFileDate} to {@code true} tries to preserve the file's last
477+
* modified date/times using {@link BasicFileAttributeView#setTimes(FileTime, FileTime, FileTime)}, however it is
478+
* not guaranteed that the operation will succeed. If the modification operation fails it will fallback to
479+
* {@link File#setLastModified(long)} and if that fails, the methods throws IOException.
495480
* </p>
496481
*
497482
* @param srcDir an existing directory to copy, must not be {@code null}.
@@ -594,9 +579,10 @@ public static void copyDirectory(final File srcDir, final File destDir, final Fi
594579
* method merges the source with the destination, with the source taking precedence.
595580
* </p>
596581
* <p>
597-
* <strong>Note:</strong> Setting {@code preserveFileDate} to {@code true} tries to preserve the files' last
598-
* modified date/times using {@link File#setLastModified(long)}, however it is not guaranteed that those operations
599-
* will succeed. If the modification operation fails, the methods throws IOException.
582+
* <strong>Note:</strong> Setting {@code preserveFileDate} to {@code true} tries to preserve the file's last
583+
* modified date/times using {@link BasicFileAttributeView#setTimes(FileTime, FileTime, FileTime)}, however it is
584+
* not guaranteed that the operation will succeed. If the modification operation fails it will fallback to
585+
* {@link File#setLastModified(long)} and if that fails, the methods throws IOException.
600586
* </p>
601587
* <b>Example: Copy directories only</b>
602588
*
@@ -643,9 +629,10 @@ public static void copyDirectory(final File srcDir, final File destDir, final Fi
643629
* method merges the source with the destination, with the source taking precedence.
644630
* </p>
645631
* <p>
646-
* <strong>Note:</strong> Setting {@code preserveFileDate} to {@code true} tries to preserve the files' last
647-
* modified date/times using {@link File#setLastModified(long)}, however it is not guaranteed that those operations
648-
* will succeed. If the modification operation fails, the methods throws IOException.
632+
* <strong>Note:</strong> Setting {@code preserveFileDate} to {@code true} tries to preserve the file's last
633+
* modified date/times using {@link BasicFileAttributeView#setTimes(FileTime, FileTime, FileTime)}, however it is
634+
* not guaranteed that the operation will succeed. If the modification operation fails it will fallback to
635+
* {@link File#setLastModified(long)} and if that fails, the methods throws IOException.
649636
* </p>
650637
* <b>Example: Copy directories only</b>
651638
*
@@ -698,7 +685,7 @@ public static void copyDirectory(final File srcDir, final File destDir, final Fi
698685
}
699686
}
700687
}
701-
doCopyDirectory(srcDir, destDir, fileFilter, exclusionList, preserveFileDate, preserveFileDate ? addCopyAttributes(copyOptions) : copyOptions);
688+
doCopyDirectory(srcDir, destDir, fileFilter, exclusionList, preserveFileDate, copyOptions);
702689
}
703690

704691
/**
@@ -712,9 +699,10 @@ public static void copyDirectory(final File srcDir, final File destDir, final Fi
712699
* method merges the source with the destination, with the source taking precedence.
713700
* </p>
714701
* <p>
715-
* <strong>Note:</strong> This method tries to preserve the files' last modified date/times using
716-
* {@link File#setLastModified(long)}, however it is not guaranteed that those operations will succeed. If the
717-
* modification operation fails, the methods throws IOException.
702+
* <strong>Note:</strong> Setting {@code preserveFileDate} to {@code true} tries to preserve the file's last
703+
* modified date/times using {@link BasicFileAttributeView#setTimes(FileTime, FileTime, FileTime)}, however it is
704+
* not guaranteed that the operation will succeed. If the modification operation fails it will fallback to
705+
* {@link File#setLastModified(long)} and if that fails, the methods throws IOException.
718706
* </p>
719707
*
720708
* @param sourceDir an existing directory to copy, must not be {@code null}.
@@ -740,8 +728,9 @@ public static void copyDirectoryToDirectory(final File sourceDir, final File des
740728
* </p>
741729
* <p>
742730
* <strong>Note:</strong> This method tries to preserve the file's last modified date/times using
743-
* {@link StandardCopyOption#COPY_ATTRIBUTES}, however it is not guaranteed that the operation will succeed. If the
744-
* modification operation fails, the methods throws IOException.
731+
* {@link BasicFileAttributeView#setTimes(FileTime, FileTime, FileTime)}, however it is not guaranteed that the
732+
* operation will succeed. If the modification operation fails it will fallback to
733+
* {@link File#setLastModified(long)} and if that fails, the methods throws IOException.
745734
* </p>
746735
*
747736
* @param srcFile an existing file to copy, must not be {@code null}.
@@ -754,7 +743,7 @@ public static void copyDirectoryToDirectory(final File sourceDir, final File des
754743
* @see #copyFile(File, File, boolean)
755744
*/
756745
public static void copyFile(final File srcFile, final File destFile) throws IOException {
757-
copyFile(srcFile, destFile, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING);
746+
copyFile(srcFile, destFile, StandardCopyOption.REPLACE_EXISTING);
758747
}
759748

760749
/**
@@ -766,8 +755,9 @@ public static void copyFile(final File srcFile, final File destFile) throws IOEx
766755
* </p>
767756
* <p>
768757
* <strong>Note:</strong> Setting {@code preserveFileDate} to {@code true} tries to preserve the file's last
769-
* modified date/times using {@link StandardCopyOption#COPY_ATTRIBUTES}, however it is not guaranteed that the operation
770-
* will succeed. If the modification operation fails, the methods throws IOException.
758+
* modified date/times using {@link BasicFileAttributeView#setTimes(FileTime, FileTime, FileTime)}, however it is
759+
* not guaranteed that the operation will succeed. If the modification operation fails it will fallback to
760+
* {@link File#setLastModified(long)} and if that fails, the methods throws IOException.
771761
* </p>
772762
*
773763
* @param srcFile an existing file to copy, must not be {@code null}.
@@ -781,9 +771,7 @@ public static void copyFile(final File srcFile, final File destFile) throws IOEx
781771
*/
782772
public static void copyFile(final File srcFile, final File destFile, final boolean preserveFileDate) throws IOException {
783773
// @formatter:off
784-
copyFile(srcFile, destFile, preserveFileDate
785-
? new CopyOption[] {StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING}
786-
: new CopyOption[] {StandardCopyOption.REPLACE_EXISTING});
774+
copyFile(srcFile, destFile, preserveFileDate, new CopyOption[] {StandardCopyOption.REPLACE_EXISTING});
787775
// @formatter:on
788776
}
789777

@@ -796,8 +784,9 @@ public static void copyFile(final File srcFile, final File destFile, final boole
796784
* </p>
797785
* <p>
798786
* <strong>Note:</strong> Setting {@code preserveFileDate} to {@code true} tries to preserve the file's last
799-
* modified date/times using {@link StandardCopyOption#COPY_ATTRIBUTES}, however it is not guaranteed that the operation
800-
* will succeed. If the modification operation fails, the methods throws IOException.
787+
* modified date/times using {@link BasicFileAttributeView#setTimes(FileTime, FileTime, FileTime)}, however it is
788+
* not guaranteed that the operation will succeed. If the modification operation fails it will fallback to
789+
* {@link File#setLastModified(long)} and if that fails, the methods throws IOException.
801790
* </p>
802791
*
803792
* @param srcFile an existing file to copy, must not be {@code null}.
@@ -813,7 +802,20 @@ public static void copyFile(final File srcFile, final File destFile, final boole
813802
* @since 2.8.0
814803
*/
815804
public static void copyFile(final File srcFile, final File destFile, final boolean preserveFileDate, final CopyOption... copyOptions) throws IOException {
816-
copyFile(srcFile, destFile, preserveFileDate ? addCopyAttributes(copyOptions) : copyOptions);
805+
requireFileCopy(srcFile, destFile);
806+
requireFile(srcFile, "srcFile");
807+
requireCanonicalPathsNotEquals(srcFile, destFile);
808+
createParentDirectories(destFile);
809+
requireFileIfExists(destFile, "destFile");
810+
if (destFile.exists()) {
811+
requireCanWrite(destFile, "destFile");
812+
}
813+
Files.copy(srcFile.toPath(), destFile.toPath(), copyOptions);
814+
815+
// On Windows, the last modified time is copied by default.
816+
if(preserveFileDate) {
817+
setTimes(srcFile, destFile);
818+
}
817819
}
818820

819821
/**
@@ -835,16 +837,7 @@ public static void copyFile(final File srcFile, final File destFile, final boole
835837
* @since 2.9.0
836838
*/
837839
public static void copyFile(final File srcFile, final File destFile, final CopyOption... copyOptions) throws IOException {
838-
requireFileCopy(srcFile, destFile);
839-
requireFile(srcFile, "srcFile");
840-
requireCanonicalPathsNotEquals(srcFile, destFile);
841-
createParentDirectories(destFile);
842-
requireFileIfExists(destFile, "destFile");
843-
if (destFile.exists()) {
844-
requireCanWrite(destFile, "destFile");
845-
}
846-
// On Windows, the last modified time is copied by default.
847-
Files.copy(srcFile.toPath(), destFile.toPath(), copyOptions);
840+
copyFile(srcFile, destFile, true, copyOptions);
848841
}
849842

850843
/**
@@ -876,8 +869,9 @@ public static long copyFile(final File input, final OutputStream output) throws
876869
* </p>
877870
* <p>
878871
* <strong>Note:</strong> This method tries to preserve the file's last modified date/times using
879-
* {@link StandardCopyOption#COPY_ATTRIBUTES}, however it is not guaranteed that the operation will succeed. If the
880-
* modification operation fails, the methods throws IOException.
872+
* {@link BasicFileAttributeView#setTimes(FileTime, FileTime, FileTime)}, however it is not guaranteed that the
873+
* operation will succeed. If the modification operation fails it will fallback to
874+
* {@link File#setLastModified(long)} and if that fails, the methods throws IOException.
881875
* </p>
882876
*
883877
* @param srcFile an existing file to copy, must not be {@code null}.
@@ -900,8 +894,9 @@ public static void copyFileToDirectory(final File srcFile, final File destDir) t
900894
* </p>
901895
* <p>
902896
* <strong>Note:</strong> Setting {@code preserveFileDate} to {@code true} tries to preserve the file's last
903-
* modified date/times using {@link StandardCopyOption#COPY_ATTRIBUTES}, however it is not guaranteed that the operation
904-
* will succeed. If the modification operation fails, the methods throws IOException.
897+
* modified date/times using {@link BasicFileAttributeView#setTimes(FileTime, FileTime, FileTime)}, however it is
898+
* not guaranteed that the operation will succeed. If the modification operation fails it will fallback to
899+
* {@link File#setLastModified(long)} and if that fails, the methods throws IOException.
905900
* </p>
906901
*
907902
* @param sourceFile an existing file to copy, must not be {@code null}.
@@ -957,10 +952,10 @@ public static void copyInputStreamToFile(final InputStream source, final File de
957952
* merges the source with the destination, with the source taking precedence.
958953
* </p>
959954
* <p>
960-
* <strong>Note:</strong> This method tries to preserve the files' last modified date/times using
961-
* {@link StandardCopyOption#COPY_ATTRIBUTES} or {@link File#setLastModified(long)} depending on the input, however it
962-
* is not guaranteed that those operations will succeed. If the modification operation fails, the methods throws
963-
* IOException.
955+
* <strong>Note:</strong> Setting {@code preserveFileDate} to {@code true} tries to preserve the file's last
956+
* modified date/times using {@link BasicFileAttributeView#setTimes(FileTime, FileTime, FileTime)}, however it is
957+
* not guaranteed that the operation will succeed. If the modification operation fails it will fallback to
958+
* {@link File#setLastModified(long)} and if that fails, the methods throws IOException.
964959
* </p>
965960
*
966961
* @param sourceFile an existing file or directory to copy, must not be {@code null}.
@@ -993,10 +988,10 @@ public static void copyToDirectory(final File sourceFile, final File destination
993988
* If the destination file exists, then this method will overwrite it.
994989
* </p>
995990
* <p>
996-
* <strong>Note:</strong> This method tries to preserve the file's last
997-
* modified date/times using {@link File#setLastModified(long)}, however
998-
* it is not guaranteed that the operation will succeed.
999-
* If the modification operation fails, the methods throws IOException.
991+
* <strong>Note:</strong> Setting {@code preserveFileDate} to {@code true} tries to preserve the file's last
992+
* modified date/times using {@link BasicFileAttributeView#setTimes(FileTime, FileTime, FileTime)}, however it is
993+
* not guaranteed that the operation will succeed. If the modification operation fails it will fallback to
994+
* {@link File#setLastModified(long)} and if that fails, the methods throws IOException.
1000995
* </p>
1001996
*
1002997
* @param sourceIterable existing files to copy, must not be {@code null}.
@@ -1310,13 +1305,13 @@ private static void doCopyDirectory(final File srcDir, final File destDir, final
13101305
if (srcFile.isDirectory()) {
13111306
doCopyDirectory(srcFile, dstFile, fileFilter, exclusionList, preserveDirDate, copyOptions);
13121307
} else {
1313-
copyFile(srcFile, dstFile, copyOptions);
1308+
copyFile(srcFile, dstFile, preserveDirDate, copyOptions);
13141309
}
13151310
}
13161311
}
13171312
// Do this last, as the above has probably affected directory metadata
13181313
if (preserveDirDate) {
1319-
setLastModified(srcDir, destDir);
1314+
setTimes(srcDir, destDir);
13201315
}
13211316
}
13221317

@@ -2386,7 +2381,8 @@ public static void moveFile(final File srcFile, final File destFile, final CopyO
23862381
requireAbsent(destFile, "destFile");
23872382
final boolean rename = srcFile.renameTo(destFile);
23882383
if (!rename) {
2389-
copyFile(srcFile, destFile, copyOptions);
2384+
// Don't interfere with file date on move, handled by StandardCopyOption.COPY_ATTRIBUTES
2385+
copyFile(srcFile, destFile, false, copyOptions);
23902386
if (!srcFile.delete()) {
23912387
FileUtils.deleteQuietly(destFile);
23922388
throw new IOException("Failed to delete original file '" + srcFile + "' after copy to '" + destFile + "'");
@@ -2836,37 +2832,30 @@ private static File requireFileIfExists(final File file, final String name) {
28362832
}
28372833

28382834
/**
2839-
* Sets the given {@code targetFile}'s last modified date to the value from {@code sourceFile}.
2835+
* Set file lastModifiedTime, lastAccessTime and creationTime to match source file
28402836
*
28412837
* @param sourceFile The source file to query.
28422838
* @param targetFile The target file or directory to set.
28432839
* @throws NullPointerException if sourceFile is {@code null}.
28442840
* @throws NullPointerException if targetFile is {@code null}.
28452841
* @throws IOException if setting the last-modified time failed.
28462842
*/
2847-
private static void setLastModified(final File sourceFile, final File targetFile) throws IOException {
2843+
private static void setTimes(final File sourceFile, final File targetFile) throws IOException {
28482844
Objects.requireNonNull(sourceFile, "sourceFile");
28492845
Objects.requireNonNull(targetFile, "targetFile");
2850-
if (targetFile.isFile()) {
2851-
PathUtils.setLastModifiedTime(targetFile.toPath(), sourceFile.toPath());
2852-
} else {
2853-
setLastModified(targetFile, lastModified(sourceFile));
2854-
}
2855-
}
2856-
2857-
/**
2858-
* Sets the given {@code targetFile}'s last modified date to the given value.
2859-
*
2860-
* @param file The source file to query.
2861-
* @param timeMillis The new last-modified time, measured in milliseconds since the epoch 01-01-1970 GMT.
2862-
* @throws NullPointerException if file is {@code null}.
2863-
* @throws IOException if setting the last-modified time failed.
2864-
*/
2865-
private static void setLastModified(final File file, final long timeMillis) throws IOException {
2866-
Objects.requireNonNull(file, "file");
2867-
if (!file.setLastModified(timeMillis)) {
2868-
throw new IOException(String.format("Failed setLastModified(%s) on '%s'", timeMillis, file));
2869-
}
2846+
try {
2847+
// Set creation, modified, last accessed to match source file
2848+
final BasicFileAttributes srcAttr = Files.readAttributes(sourceFile.toPath(), BasicFileAttributes.class);
2849+
final BasicFileAttributeView destAttrView = Files.getFileAttributeView(targetFile.toPath(), BasicFileAttributeView.class);
2850+
// null guards are not needed; BasicFileAttributes.setTimes(...) is null safe
2851+
destAttrView.setTimes(srcAttr.lastModifiedTime(), srcAttr.lastAccessTime(), srcAttr.creationTime());
2852+
} catch (IOException unused) {
2853+
// Fallback: Only set modified time to match source file
2854+
targetFile.setLastModified(sourceFile.lastModified());
2855+
}
2856+
2857+
// TODO: (Help!) Determine historically why setLastModified(File, File) needed PathUtils.setLastModifiedTime() if
2858+
// sourceFile.isFile() was true, but needed setLastModifiedTime(File, long) if sourceFile.isFile() was false
28702859
}
28712860

28722861
/**

0 commit comments

Comments
 (0)