Skip to content
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

mv: gnu test case part-hardlink fix #6632

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from

Conversation

matrixhead
Copy link
Contributor

fix for #6631

behaviours changed

  • mv will now preserve links during an inter-device move.
  • Previously, mv would silently fail to create a backup when no-target-directory was specified and the destination ended with a / This patch resolves that issue.
  • The progress bar will now work for simple inter-device file moves as well.

src/uu/mv/src/mv.rs Outdated Show resolved Hide resolved
Copy link

GNU testsuite comparison:

Congrats! The gnu test tests/mv/hard-link-1 is no longer failing!
Congrats! The gnu test tests/mv/part-hardlink is no longer failing!
Skipping an intermittent issue tests/tail/inotify-dir-recreate (passes in this run but fails in the 'main' branch)

src/uu/mv/src/mv.rs Outdated Show resolved Hide resolved
@sylvestre
Copy link
Sponsor Contributor

Bravo

Congrats! The gnu test tests/mv/hard-link-1 is no longer failing!
Congrats! The gnu test tests/mv/part-hardlink is no longer failing!


Copy link

GNU testsuite comparison:

Congrats! The gnu test tests/mv/hard-link-1 is no longer failing!
Congrats! The gnu test tests/mv/part-hardlink is no longer failing!

1 similar comment
Copy link

GNU testsuite comparison:

Congrats! The gnu test tests/mv/hard-link-1 is no longer failing!
Congrats! The gnu test tests/mv/part-hardlink is no longer failing!

Copy link

GNU testsuite comparison:

Congrats! The gnu test tests/mv/hard-link-1 is no longer failing!
Congrats! The gnu test tests/mv/part-hardlink is no longer failing!

src/uu/mv/src/mv.rs Outdated Show resolved Hide resolved
Copy link

GNU testsuite comparison:

Skip an intermittent issue tests/tail/inotify-dir-recreate (fails in this run but passes in the 'main' branch)
Congrats! The gnu test tests/mv/hard-link-1 is no longer failing!
Congrats! The gnu test tests/mv/part-hardlink is no longer failing!

Copy link
Collaborator

@BenWiederhake BenWiederhake left a comment

Choose a reason for hiding this comment

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

Thank you for tackling these issues! However, combining these three behavioral changes seems like a bad idea, given that each of them have delicate consequences.

It seems that get_dir_content is the entirely wrong tool to approach mv, and it causes incorrect behavior.

Finally, it seems you fell into a common trap / bad pattern: Guessing a potential error condition (and not detecting others), then doing an action (and badly handling most error conditions.) This causes unhelpful error messages and unnecessary syscalls.

Comment on lines 651 to 654
}
// We fall back to copying and then removing.
else {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Please move the comment into the else clause. Currently, it looks misleading, as if the if is now complete.

Comment on lines 763 to 779
if !from.exists() {
if let Some(msg) = from.to_str() {
let msg = format!("Path \"{}\" does not exist or you don't have access!", msg);
return Err(FsXError::new(FsXErrorKind::NotFound, &msg));
}
return Err(FsXError::new(
FsXErrorKind::NotFound,
"Path does not exist or you don't have access!",
));
}

let dir_content = get_dir_content(from)?;
Copy link
Collaborator

Choose a reason for hiding this comment

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

This misses some error conditions, which leads to the following confusing behavior, both on main and on this PR:

$ rm -rf /tmp/foobar/ && cargo run -q --features mv mv -v /root/ /tmp/foobar
mv: Permission denied
[$? = 1]

Which permission was denied? When accessing which file? Is the permission problem with the source or with the destination? I hope you can see how this error message might be confusing when there's many files being moved/copied.

GNU offers a much more user-friendly error message:

$ rm -rf /tmp/foobar/ && mv -v /root/ /tmp/foobar
created directory '/tmp/foobar'
mv: cannot access '/root/': Permission denied
[$? = 1]

The same holds with the error message on line 770: It doesn't even mention the filename.

Please instead handle errors on the actions that cause them. In this case, handling the error when it happens (instead of trying to guess) completely eliminates the need for the from.exists() call.

Comment on lines 843 to 850
if let Some(file_name) = from.file_name().and_then(|file_name| file_name.to_str()) {
file_name.to_string()
} else {
return Err(FsXError::new(
FsXErrorKind::InvalidFileName,
"Invalid file name",
));
};
Copy link
Collaborator

Choose a reason for hiding this comment

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

File names are not necessarily valid UTF-8. This code would raise an error if such a file name is encountered. Does that mean non-UTF8-filenames cause an error when enabling progress bars?

We already don't have good handling for non-UTF-8 filenames, but let's try to avoid making the situation worse.


// Ensure that the folder structure is preserved, files are copied, and their contents remain intact.
assert_eq!(
read_to_string(other_fs_tempdir.path().join("a/b/d"),).expect("Unable to read other_fs_file"),
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why is there a spurious comma?

Suggested change
read_to_string(other_fs_tempdir.path().join("a/b/d"),).expect("Unable to read other_fs_file"),
read_to_string(other_fs_tempdir.path().join("a/b/d")).expect("Unable to read other_fs_file"),

"d"
);
assert_eq!(
read_to_string(other_fs_tempdir.path().join("a/b/c/e"),).expect("Unable to read other_fs_file"),
Copy link
Collaborator

Choose a reason for hiding this comment

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

Same here

let result_file_move = if let Some(new_source) = moved_files.get(file_info) {
fs::hard_link(new_source, to)?;
Ok(file_info.file_size())
} else if let Some(progress_bar) = progress_bar {
Copy link
Collaborator

Choose a reason for hiding this comment

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

If you write code for the progress-bar case, then please add a test that exercises the progress-bar case.

));
}

let dir_content = get_dir_content(from)?;
Copy link
Collaborator

Choose a reason for hiding this comment

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

get_dir_content does not return sufficient information. GNU mv copies/moves as many files/directories as it can, even if it first encounter some files where it has insufficient permissions:

$ mv -v source/ /tmp
created directory '/tmp/source'
created directory '/tmp/source/d1'
mv: cannot access 'source/d1': Permission denied
created directory '/tmp/source/d3'
mv: cannot access 'source/d3': Permission denied
copied 'source/f2' -> '/tmp/source/f2'
[$? = 1]
$ ls source/  # source has NOT been deleted!
d1  d3  f2
$ ls /tmp/source/
d1  d3  f2

However, get_dir_content bails completely if there are any permission issues:

$ cargo run -q --features mv mv -v source/ /tmp
mv: cannot move 'source/' to '/tmp/source': Permission denied
[$? = 1]
$ ls /tmp/source/
ls: cannot access '/tmp/source/': No such file or directory
[$? = 2]

And the final nail in the coffin is that get_dir_content insists to first read the entire filesystem structure, which uses up potentially vast amounts of memory, and gives ample opportunity for all kinds of TOCTOU races: https://docs.rs/fs_extra/latest/fs_extra/dir/struct.DirContent.html

Therefore, we should avoid using get_dir_content entirely. I'm not even sure it can be used correctly.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, you're right. Instead of using get_dir_content, we could follow the approach used in uu_cp, which involves walking through the directory, copying files, and creating folders as we go.

Copy link
Collaborator

@BenWiederhake BenWiederhake left a comment

Choose a reason for hiding this comment

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

Whoops, I meant to set the "Request changes" flag. See above for my reasoning. In particular, get_dir_content must go.

May I suggest to split the tree changes into two or three PRs?

Copy link

github-actions bot commented Sep 8, 2024

GNU testsuite comparison:

GNU test failed: tests/timeout/timeout. tests/timeout/timeout is passing on 'main'. Maybe you have to rebase?
Congrats! The gnu test tests/mv/hard-link-1 is no longer failing!
Congrats! The gnu test tests/mv/part-hardlink is no longer failing!

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?
Skip an intermittent issue tests/tail/inotify-dir-recreate (fails in this run but passes in the 'main' branch)

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?

@matrixhead
Copy link
Contributor Author

@BenWiederhake I cleaned up this PR. Now, it focuses solely on the directory copying part. I'll work on verbose output and error reporting next. For now, could you let me know if this approach looks okay?

Copy link

GNU testsuite comparison:

Skipping an intermittent issue tests/tail/inotify-dir-recreate (passes in this run but fails in the 'main' branch)

src/uu/mv/src/mv.rs Outdated Show resolved Hide resolved
Copy link

GNU testsuite comparison:

Skipping an intermittent issue tests/tail/inotify-dir-recreate (passes in this run but fails in the 'main' branch)

Copy link

GNU testsuite comparison:

Skip an intermittent issue tests/rm/rm1 (fails in this run but passes in the 'main' branch)
GNU test failed: tests/timeout/timeout. tests/timeout/timeout is passing on 'main'. Maybe you have to rebase?

Copy link

GNU testsuite comparison:

GNU test failed: tests/timeout/timeout. tests/timeout/timeout is passing on 'main'. Maybe you have to rebase?

Copy link

GNU testsuite comparison:

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

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.

3 participants