Skip to content

Commit 72afe97

Browse files
tjgqcopybara-github
authored andcommitted
Minimize syscalls in FileSystemUtils.moveFile/copyFile.
By making use of an already available `FileStatus` to determine the source permissions and modified time without additional `stat` calls, and setting the permissions with a single `chmod` call when possible. Also fix a couple of bugs, found by adding comprehensive tests: - the `copyFile` fast path should refuse to copy a directory, matching the slow path; - the error handling for the `moveFile` slow path was incorrect (it must catch exceptions, not check the `delete()` return value). PiperOrigin-RevId: 827587130 Change-Id: If6b15b11e76e0e2d5af41d38fe9c3b57ade6ef3b
1 parent 97c9220 commit 72afe97

File tree

3 files changed

+339
-120
lines changed

3 files changed

+339
-120
lines changed

src/main/java/com/google/devtools/build/lib/vfs/FileSystemUtils.java

Lines changed: 57 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -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
}

src/test/java/com/google/devtools/build/lib/vfs/BUILD

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,13 @@ java_library(
6262
"//src/main/java/com/google/devtools/build/lib/skyframe/serialization/autocodec",
6363
"//src/main/java/com/google/devtools/build/lib/skyframe/serialization/testutils",
6464
"//src/main/java/com/google/devtools/build/lib/skyframe/serialization/testutils:round-tripping",
65+
"//src/main/java/com/google/devtools/build/lib/unix",
66+
"//src/main/java/com/google/devtools/build/lib/util:os",
6567
"//src/main/java/com/google/devtools/build/lib/util:string_encoding",
6668
"//src/main/java/com/google/devtools/build/lib/vfs",
6769
"//src/main/java/com/google/devtools/build/lib/vfs:pathfragment",
6870
"//src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs",
71+
"//src/main/java/com/google/devtools/build/lib/windows",
6972
"//src/test/java/com/google/devtools/build/lib/testutil",
7073
"//src/test/java/com/google/devtools/build/lib/testutil:TestThread",
7174
"//src/test/java/com/google/devtools/build/lib/testutil:TestUtils",

0 commit comments

Comments
 (0)