Skip to content

Make mv command fallback to copy only if the src and dst are on different device #6040

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 17, 2025

Conversation

hamflx
Copy link
Contributor

@hamflx hamflx commented Mar 2, 2024

fixes #6029

@hamflx hamflx force-pushed the main branch 4 times, most recently from c06329e to c3c4cef Compare March 2, 2024 04:01
@tertsdiepraam
Copy link
Member

Thanks for putting this up! I have two questions reading this that I think we need to answer before merging this.

  1. Can we think of any other cases where the rename call fails that we need to take into account?

  2. What does GNU do? We can't look at the source code, but we can use strace on GNU mv to see what syscalls it performs. I'd like to know whether they also give up after a failed rename.

@@ -25,6 +25,12 @@ uucore = { workspace = true, features = [
"update-control",
] }

[target.'cfg(target_os = "windows")'.dependencies]
winapi = { version="0.3" }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we do it with winapi_util?
I am a bit reluctant to add a new dep

Copy link
Contributor Author

@hamflx hamflx Mar 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

winapi_util seems like no ERROR_NOT_SAME_DEVICE. maybe i can use windows-sys?

Copy link

github-actions bot commented Mar 2, 2024

GNU testsuite comparison:

GNU test failed: tests/mv/backup-dir. tests/mv/backup-dir is passing on 'main'. Maybe you have to rebase?
Skip an intermittent issue tests/tail/inotify-dir-recreate (fails in this run but passes in the 'main' branch)

@hamflx
Copy link
Contributor Author

hamflx commented Mar 2, 2024

@tertsdiepraam

  1. maybe EIO should be considered (such as the file system does not support move operation i gussed)?
  2. yes, the gnu mv give up copying files after EACCES fired:
    renameat2(AT_FDCWD, "docs", AT_FDCWD, "../some-docs", RENAME_NOREPLACE) = -1 EACCES (Permission denied)
    newfstatat(AT_FDCWD, "../some-docs", 0x7ffd6d9a2cd0, 0) = -1 ENOENT (No such file or directory)
    newfstatat(AT_FDCWD, "docs", {st_mode=S_IFDIR|0755, st_size=132, ...}, AT_SYMLINK_NOFOLLOW) = 0
    newfstatat(AT_FDCWD, "../some-docs", 0x7ffd6d9a2830, AT_SYMLINK_NOFOLLOW) = -1 ENOENT (No such file or directory)
    ....
    write(2, ": Permission denied", 19: Permission denied)
    
    it fallback to copy if we copy files across device (mv docs /tmp/some-docs):
    renameat2(AT_FDCWD, "docs", AT_FDCWD, "/tmp/some-docs", RENAME_NOREPLACE) = -1 EXDEV (Invalid cross-device link)
    newfstatat(AT_FDCWD, "/tmp/some-docs", 0x7ffc6b18c1b0, 0) = -1 ENOENT (No such file or directory)
    newfstatat(AT_FDCWD, "docs", {st_mode=S_IFDIR|0755, st_size=132, ...}, AT_SYMLINK_NOFOLLOW) = 0
    newfstatat(AT_FDCWD, "/tmp/some-docs", 0x7ffc6b18bd10, AT_SYMLINK_NOFOLLOW) = -1 ENOENT (No such file or directory)
    rmdir("/tmp/some-docs")                 = -1 ENOENT (No such file or directory)
    lgetxattr("docs", "security.selinux", "unconfined_u:object_r:user_tmp_t"..., 255) = 36
    access("/var/run/setrans/.setrans-unix", F_OK) = -1 ENOENT (No such file or directory)
    futex(0x7fd465c55520, FUTEX_WAKE_PRIVATE, 2147483647) = 0
    futex(0x7fd465c56644, FUTEX_WAKE_PRIVATE, 2147483647) = 0
    openat(AT_FDCWD, "/proc/thread-self/attr/fscreate", O_RDWR|O_CLOEXEC) = 3
    write(3, "unconfined_u:object_r:user_tmp_t"..., 36) = 36
    close(3)                                = 0
    mkdir("/tmp/some-docs", 0700)           = 0
    newfstatat(AT_FDCWD, "/tmp/some-docs", {st_mode=S_IFDIR|0700, st_size=6, ...}, AT_SYMLINK_NOFOLLOW) = 0
    openat(AT_FDCWD, "docs", O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_DIRECTORY) = 3
    fstat(3, {st_mode=S_IFDIR|0755, st_size=132, ...}) = 0
    getdents64(3, 0x561b1e9f5fc0 /* 9 entries */, 32768) = 280
    getdents64(3, 0x561b1e9f5fc0 /* 0 entries */, 32768) = 0
    close(3)                                = 0
    newfstatat(AT_FDCWD, "docs/src", {st_mode=S_IFDIR|0755, st_size=4096, ...}, AT_SYMLINK_NOFOLLOW) = 0
    newfstatat(AT_FDCWD, "/tmp/some-docs/src", 0x7ffc6b18b7c0, AT_SYMLINK_NOFOLLOW) = -1 ENOENT (No such file or directory)
    rmdir("/tmp/some-docs/src")             = -1 ENOENT (No such file or directory)
    lgetxattr("docs/src", "security.selinux", "unconfined_u:object_r:user_tmp_t"..., 255) = 36
    mkdir("/tmp/some-docs/src", 0700)       = 0
    newfstatat(AT_FDCWD, "/tmp/some-docs/src", {st_mode=S_IFDIR|0700, st_size=6, ...}, AT_SYMLINK_NOFOLLOW) = 0
    openat(AT_FDCWD, "docs/src", O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_DIRECTORY) = 3
    fstat(3, {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0
    getdents64(3, 0x561b1e9fe1d0 /* 15 entries */, 32768) = 512
    getdents64(3, 0x561b1e9fe1d0 /* 0 entries */, 32768) = 0
    close(3)                                = 0
    ...
    

Copy link

GNU testsuite comparison:

GNU test failed: tests/mv/backup-dir. tests/mv/backup-dir is passing on 'main'. Maybe you have to rebase?
Congrats! The gnu test tests/chown/preserve-root is no longer failing!

@sylvestre
Copy link
Contributor

Two jobs are failing. Minor stuff :)

@hamflx hamflx force-pushed the main branch 2 times, most recently from a861732 to 5b4c5ee Compare March 14, 2024 07:34
Copy link

GNU testsuite comparison:

GNU test failed: tests/mv/backup-dir. tests/mv/backup-dir is passing on 'main'. Maybe you have to rebase?

@hamflx
Copy link
Contributor Author

hamflx commented Mar 14, 2024

@sylvestre i have fixed cSpell and Cargo.toml formatting issues.

GNU testsuite comparison:

GNU test failed: tests/mv/backup-dir. tests/mv/backup-dir is passing on 'main'. Maybe you have to rebase?

but it does not seem to be caused my changes.

Copy link

GNU testsuite comparison:

GNU test failed: tests/mv/backup-dir. tests/mv/backup-dir is passing on 'main'. Maybe you have to rebase?

@sylvestre
Copy link
Contributor

GNU testsuite comparison:

GNU test failed: tests/mv/backup-dir. tests/mv/backup-dir is passing on 'main'. Maybe you have to rebase?

sorry but it is very very likely your change here

@hamflx
Copy link
Contributor Author

hamflx commented Mar 24, 2024

@sylvestre Thanks for your patience. I have found the problem, the call to std::fs::rename("E/", "E/.~1~") reported an error: EINVAL. This is not an EXDEV error, so I made it return immediately.

But I don't think it's my fault, because the GNU tests running mv -T --backup=numbered C E/ || fail=1 will move E to E.~1~, but uu_mv will move E to E/.~1~. This is clearly incorrect.

Before my changes, the GNU tests passed, but the result is incorrect. see the outputs below:

hamflx@hamflx-workstation:~/coreutils/test$ git fetch origin
hamflx@hamflx-workstation:~/coreutils/test$ git reset --hard origin/main
HEAD is now at dcb53b6c9 head: add missing features
hamflx@hamflx-workstation:~/coreutils/test$ cargo build -p uu_mv
    Finished dev [unoptimized + debuginfo] target(s) in 0.04s
hamflx@hamflx-workstation:~/coreutils/test$ find . -delete && mkdir C D E && touch C/c D/d E/e
hamflx@hamflx-workstation:~/coreutils/test$ mv -T --backup=numbered C E/
hamflx@hamflx-workstation:~/coreutils/test$ tree -a
.
├── D
│   └── d
├── E
│   └── c
└── E.~1~
    └── e

3 directories, 3 files
hamflx@hamflx-workstation:~/coreutils/test$ find . -delete && mkdir C D E && touch C/c D/d E/e
hamflx@hamflx-workstation:~/coreutils/test$ ../target/debug/mv -T --backup=numbered C E/
hamflx@hamflx-workstation:~/coreutils/test$ tree -a
.
├── D
│   └── d
└── E
    └── c

2 directories, 2 files

I have created a new branch to try to fix this problem: #6119. The outputs after this change:

➜  test git:(fix/invalid-backup-numbered-path) find . -delete && mkdir C D E && touch C/c D/d E/e
➜  test git:(fix/invalid-backup-numbered-path) ../target/debug/mv -T --backup=numbered C E/
➜  test git:(fix/invalid-backup-numbered-path) tree -a
.
├── D
│   └── d
├── E
│   └── c
└── E.~1~
    └── e

3 directories, 3 files

Copy link

GNU testsuite comparison:

GNU test failed: tests/mv/backup-dir. tests/mv/backup-dir is passing on 'main'. Maybe you have to rebase?

Copy link

github-actions bot commented Dec 2, 2024

GNU testsuite comparison:

GNU test failed: tests/mv/backup-dir. tests/mv/backup-dir is passing on 'main'. Maybe you have to rebase?
Skipping an intermittent issue tests/tail/inotify-dir-recreate (passes in this run but fails in the 'main' branch)

@hamflx
Copy link
Contributor Author

hamflx commented Feb 2, 2025

What needs to be done to get this PR merged?

@sylvestre

@sylvestre sylvestre requested a review from jfinkels February 2, 2025 15:47
@sylvestre
Copy link
Contributor

I think @jfinkels has been upgrading your PR :)
it should land soon. sorry about the latency

@jfinkels
Copy link
Collaborator

jfinkels commented Feb 2, 2025

@hamflx the best thing to do is resolve the merge conflicts and build failures in this merge request. If it helps, you can look at my attempt to update your branch #7152. (I don't have Windows so it's very hard for me to debug the Windows CI failures.)

@hamflx
Copy link
Contributor Author

hamflx commented Feb 6, 2025

This change adds a Windows-specific check that explicitly removes the destination file before attempting to rename/move the source file. This is needed because on Windows, if the source file is opened (even with FILE_SHARE_DELETE) and the destination file exists, the MoveFileExW operation will fail with "Access Denied". By removing the destination file first, we ensure the move operation can succeed.

fn rename_with_fallback(
    from: &Path,
    to: &Path,
    multi_progress: Option<&MultiProgress>,
) -> io::Result<()> {
    #[cfg(windows)]
    if to.is_file() {
        // On Windows, if the source file is opened (even with FILE_SHARE_DELETE) and
        // the destination file exists, MoveFileExW will fail with "Access Denied".
        // We need to explicitly remove the destination file first to make the move operation succeed.
        fs::remove_file(to)?;
    }
    // ...
}

Copy link

github-actions bot commented Feb 6, 2025

GNU testsuite comparison:

Skip an intermittent issue tests/misc/stdbuf (fails in this run but passes in the 'main' branch)

@hamflx hamflx force-pushed the main branch 5 times, most recently from 09b9fe0 to b1a7b67 Compare February 6, 2025 06:41
Copy link

github-actions bot commented Feb 6, 2025

GNU testsuite comparison:

Skip an intermittent issue tests/misc/usage_vs_getopt (fails in this run but passes in the 'main' branch)

@hamflx
Copy link
Contributor Author

hamflx commented Feb 6, 2025

I adjusted the solution by removing the deletion logic before rename (which could lead to scenarios where the file move fails but the target file is deleted).

Instead, it now directly falls back to the copy logic. However, to avoid situations where copy operations might fail due to lacking delete permissions on the source file, we added the can_delete_file function to check whether the source file can be deleted.

@hamflx
Copy link
Contributor Author

hamflx commented Feb 6, 2025

@sylvestre ready for review

@sylvestre sylvestre self-requested a review February 16, 2025 23:22
@sylvestre sylvestre merged commit 58c336d into uutils:main Feb 17, 2025
65 checks passed
@sylvestre
Copy link
Contributor

Thanks and well done :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

the mv command takes a long time even if the src and dest on same device.
4 participants