@@ -368,16 +368,29 @@ public static ByteSink asByteSink(final Path path) {
368368 }
369369
370370 /**
371- * Copies the file from location "from" to location "to", while overwriting a
372- * potentially existing "to". File's last modified time, executable and
373- * writable bits are also preserved.
371+ * Copies a file, potentially overwriting the destination. Preserves the modification time and
372+ * permissions.
374373 *
375- * <p>If no error occurs, the method returns normally. If a parent directory does
376- * not exist, a FileNotFoundException is thrown. An IOException is thrown when
377- * other erroneous situations occur. (e.g. read errors)
374+ * <p>If the source is a symbolic link, it will be followed. If the destination is a symbolic
375+ * link, it will be replaced.
376+ *
377+ * <p>Copying directories is not supported.
378+ *
379+ * @param from the source path
380+ * @param to the destination path
381+ * @throws FileNotFoundException if the source does not exist, or the parent directory of the
382+ * destination does not exist
383+ * @throws IOException if the copy fails for any other reason
378384 */
379- @ ThreadSafe // but not atomic
385+ @ ThreadSafe // but not atomic
380386 public static void copyFile (Path from , Path to ) throws IOException {
387+ copyFile (from , to , from .stat ());
388+ }
389+
390+ private static void copyFile (Path from , Path to , FileStatus stat ) throws IOException {
391+ if (!stat .isFile ()) {
392+ throw new IOException ("don't know how to copy " + from );
393+ }
381394 var fromNio = from .getFileSystem ().getNioPath (from .asFragment ());
382395 var toNio = to .getFileSystem ().getNioPath (to .asFragment ());
383396 if (fromNio != null && toNio != null ) {
@@ -391,25 +404,23 @@ public static void copyFile(Path from, Path to) throws IOException {
391404 }
392405 return ;
393406 }
394- try {
395- // Target may be a symlink, in which case opening a stream below would not actually replace
396- // it.
397- to .delete ();
398- } catch (IOException e ) {
399- throw new IOException ("error copying file: "
400- + "couldn't delete destination: " + e .getMessage ());
401- }
407+ // Target may be a symlink, in which case we should not follow it.
408+ to .delete ();
402409 try (InputStream in = from .getInputStream ();
403410 OutputStream out = to .getOutputStream ()) {
404411 // This may use a faster copy method (such as via an in-kernel buffer) if both streams are
405412 // backed by files.
406413 in .transferTo (out );
407414 }
408- to .setLastModifiedTime (from .getLastModifiedTime ()); // Preserve mtime.
409- if (!from .isWritable ()) {
410- to .setWritable (false ); // Make file read-only if original was read-only.
415+ to .setLastModifiedTime (stat .getLastModifiedTime ());
416+ int perms = stat .getPermissions ();
417+ if (perms != -1 ) {
418+ to .chmod (perms );
419+ } else {
420+ to .setReadable (from .isReadable ());
421+ to .setWritable (from .isWritable ());
422+ to .setExecutable (from .isExecutable ());
411423 }
412- to .setExecutable (from .isExecutable ()); // Copy executable bit.
413424 }
414425
415426 /** Describes the behavior of a {@link #moveFile(Path, Path)} operation. */
@@ -422,52 +433,57 @@ public enum MoveResult {
422433 }
423434
424435 /**
425- * Moves the file from location "from" to location "to", while overwriting a potentially existing
426- * "to". If "from" is a regular file, its last modified time, executable and writable bits are
427- * also preserved. Symlinks are also supported but not directories or special files.
436+ * Moves a file or symbolic link, potentially overwriting the destination. Does not follow
437+ * symbolic links.
428438 *
429439 * <p>This method is not guaranteed to be atomic. Use {@link Path#renameTo(Path)} instead.
430440 *
431- * <p>If the move fails (usually because the "from" and "to" live in different file systems), this
432- * falls back to copying the file. Note that these two operations have very different performance
433- * characteristics and is why this operation reports back to the caller what actually happened.
441+ * <p>If the move fails (usually because the source and destination are in different filesystems),
442+ * falls back to copying the file, preserving its permissions and modification time. Note that the
443+ * fallback has very different performance characteristics, which is why this method reports what
444+ * actually happened back to the caller.
434445 *
435- * @param from location of the file to move
436- * @param to destination to where to move the file
446+ * @param from the source path
447+ * @param to the destination path
437448 * @return a description of how the move was performed
438- * @throws FileNotFoundException if the source file does not exist, or the parent directory of the
439- * destination file does not exit
449+ * @throws FileNotFoundException if the source does not exist, or the parent directory of the
450+ * destination does not exit
440451 * @throws IOException if the move fails for any other reason
441452 */
442453 @ ThreadSafe // but not atomic
443454 public static MoveResult moveFile (Path from , Path to ) throws IOException {
444- // We don't try-catch here for better performance.
445455 try {
446456 from .renameTo (to );
447457 return MoveResult .FILE_MOVED ;
448- } catch (IOException unused ) {
458+ } catch (IOException ignored ) {
449459 // Fallback to a copy.
450460 FileStatus stat = from .stat (Symlinks .NOFOLLOW );
451461 if (stat .isFile ()) {
452- copyFile (from , to );
462+ copyFile (from , to , stat );
453463 } else if (stat .isSymbolicLink ()) {
454- PathFragment fromTarget = from .readSymbolicLink ();
464+ PathFragment targetPath = from .readSymbolicLink ();
455465 try {
456- to .createSymbolicLink (fromTarget );
457- } catch (IOException unused2 ) {
466+ to .createSymbolicLink (targetPath );
467+ } catch (IOException ignored2 ) {
458468 // May have failed due the target file existing, but not being a symlink.
459469 // TODO: Only catch FileAlreadyExistsException once we throw that.
460470 to .delete ();
461- to .createSymbolicLink (fromTarget );
471+ to .createSymbolicLink (targetPath );
462472 }
463473 } else {
464- throw new IOException ("Don't know how to copy " + from );
474+ // TODO(tjgq): The move/copy cases should have a consistent result for a directory.
475+ throw new IOException ("Don't know how to move " + from );
465476 }
466- if (!from .delete ()) {
467- if (!to .delete ()) {
468- throw new IOException ("Unable to delete " + to );
477+ try {
478+ from .delete ();
479+ } catch (IOException e ) {
480+ // If we fail to delete the source, then delete the destination.
481+ try {
482+ to .delete ();
483+ } catch (IOException e2 ) {
484+ e .addSuppressed (e2 );
469485 }
470- throw new IOException ( "Unable to delete " + from ) ;
486+ throw e ;
471487 }
472488 return MoveResult .FILE_COPIED ;
473489 }
0 commit comments