Closes #46661 This PR changes `fs.rename` to use the platform’s atomic no-overwrite rename on all platforms when `overwrite` is `false`. This fixes a case where concurrent renames to the same target could race past a separate metadata check and end up overwriting each other. In Project Panel, we can still rename entries in parallel without worrying about OS internals not handling it correctly or making these renames sequential. Release Notes: - Fixed an issue in the Project Panel where conflicting file moves could overwrite each other instead of leaving the losing file in place.
624 lines
18 KiB
Rust
624 lines
18 KiB
Rust
use std::{
|
|
io::Write,
|
|
path::{Path, PathBuf},
|
|
};
|
|
|
|
use fs::*;
|
|
use gpui::BackgroundExecutor;
|
|
use serde_json::json;
|
|
use tempfile::TempDir;
|
|
use util::path;
|
|
|
|
#[gpui::test]
|
|
async fn test_fake_fs(executor: BackgroundExecutor) {
|
|
let fs = FakeFs::new(executor.clone());
|
|
fs.insert_tree(
|
|
path!("/root"),
|
|
json!({
|
|
"dir1": {
|
|
"a": "A",
|
|
"b": "B"
|
|
},
|
|
"dir2": {
|
|
"c": "C",
|
|
"dir3": {
|
|
"d": "D"
|
|
}
|
|
}
|
|
}),
|
|
)
|
|
.await;
|
|
|
|
assert_eq!(
|
|
fs.files(),
|
|
vec![
|
|
PathBuf::from(path!("/root/dir1/a")),
|
|
PathBuf::from(path!("/root/dir1/b")),
|
|
PathBuf::from(path!("/root/dir2/c")),
|
|
PathBuf::from(path!("/root/dir2/dir3/d")),
|
|
]
|
|
);
|
|
|
|
fs.create_symlink(path!("/root/dir2/link-to-dir3").as_ref(), "./dir3".into())
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_eq!(
|
|
fs.canonicalize(path!("/root/dir2/link-to-dir3").as_ref())
|
|
.await
|
|
.unwrap(),
|
|
PathBuf::from(path!("/root/dir2/dir3")),
|
|
);
|
|
assert_eq!(
|
|
fs.canonicalize(path!("/root/dir2/link-to-dir3/d").as_ref())
|
|
.await
|
|
.unwrap(),
|
|
PathBuf::from(path!("/root/dir2/dir3/d")),
|
|
);
|
|
assert_eq!(
|
|
fs.load(path!("/root/dir2/link-to-dir3/d").as_ref())
|
|
.await
|
|
.unwrap(),
|
|
"D",
|
|
);
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_copy_recursive_with_single_file(executor: BackgroundExecutor) {
|
|
let fs = FakeFs::new(executor.clone());
|
|
fs.insert_tree(
|
|
path!("/outer"),
|
|
json!({
|
|
"a": "A",
|
|
"b": "B",
|
|
"inner": {}
|
|
}),
|
|
)
|
|
.await;
|
|
|
|
assert_eq!(
|
|
fs.files(),
|
|
vec![
|
|
PathBuf::from(path!("/outer/a")),
|
|
PathBuf::from(path!("/outer/b")),
|
|
]
|
|
);
|
|
|
|
let source = Path::new(path!("/outer/a"));
|
|
let target = Path::new(path!("/outer/a copy"));
|
|
copy_recursive(fs.as_ref(), source, target, Default::default())
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_eq!(
|
|
fs.files(),
|
|
vec![
|
|
PathBuf::from(path!("/outer/a")),
|
|
PathBuf::from(path!("/outer/a copy")),
|
|
PathBuf::from(path!("/outer/b")),
|
|
]
|
|
);
|
|
|
|
let source = Path::new(path!("/outer/a"));
|
|
let target = Path::new(path!("/outer/inner/a copy"));
|
|
copy_recursive(fs.as_ref(), source, target, Default::default())
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_eq!(
|
|
fs.files(),
|
|
vec![
|
|
PathBuf::from(path!("/outer/a")),
|
|
PathBuf::from(path!("/outer/a copy")),
|
|
PathBuf::from(path!("/outer/b")),
|
|
PathBuf::from(path!("/outer/inner/a copy")),
|
|
]
|
|
);
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_copy_recursive_with_single_dir(executor: BackgroundExecutor) {
|
|
let fs = FakeFs::new(executor.clone());
|
|
fs.insert_tree(
|
|
path!("/outer"),
|
|
json!({
|
|
"a": "A",
|
|
"empty": {},
|
|
"non-empty": {
|
|
"b": "B",
|
|
}
|
|
}),
|
|
)
|
|
.await;
|
|
|
|
assert_eq!(
|
|
fs.files(),
|
|
vec![
|
|
PathBuf::from(path!("/outer/a")),
|
|
PathBuf::from(path!("/outer/non-empty/b")),
|
|
]
|
|
);
|
|
assert_eq!(
|
|
fs.directories(false),
|
|
vec![
|
|
PathBuf::from(path!("/")),
|
|
PathBuf::from(path!("/outer")),
|
|
PathBuf::from(path!("/outer/empty")),
|
|
PathBuf::from(path!("/outer/non-empty")),
|
|
]
|
|
);
|
|
|
|
let source = Path::new(path!("/outer/empty"));
|
|
let target = Path::new(path!("/outer/empty copy"));
|
|
copy_recursive(fs.as_ref(), source, target, Default::default())
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_eq!(
|
|
fs.files(),
|
|
vec![
|
|
PathBuf::from(path!("/outer/a")),
|
|
PathBuf::from(path!("/outer/non-empty/b")),
|
|
]
|
|
);
|
|
assert_eq!(
|
|
fs.directories(false),
|
|
vec![
|
|
PathBuf::from(path!("/")),
|
|
PathBuf::from(path!("/outer")),
|
|
PathBuf::from(path!("/outer/empty")),
|
|
PathBuf::from(path!("/outer/empty copy")),
|
|
PathBuf::from(path!("/outer/non-empty")),
|
|
]
|
|
);
|
|
|
|
let source = Path::new(path!("/outer/non-empty"));
|
|
let target = Path::new(path!("/outer/non-empty copy"));
|
|
copy_recursive(fs.as_ref(), source, target, Default::default())
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_eq!(
|
|
fs.files(),
|
|
vec![
|
|
PathBuf::from(path!("/outer/a")),
|
|
PathBuf::from(path!("/outer/non-empty/b")),
|
|
PathBuf::from(path!("/outer/non-empty copy/b")),
|
|
]
|
|
);
|
|
assert_eq!(
|
|
fs.directories(false),
|
|
vec![
|
|
PathBuf::from(path!("/")),
|
|
PathBuf::from(path!("/outer")),
|
|
PathBuf::from(path!("/outer/empty")),
|
|
PathBuf::from(path!("/outer/empty copy")),
|
|
PathBuf::from(path!("/outer/non-empty")),
|
|
PathBuf::from(path!("/outer/non-empty copy")),
|
|
]
|
|
);
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_copy_recursive(executor: BackgroundExecutor) {
|
|
let fs = FakeFs::new(executor.clone());
|
|
fs.insert_tree(
|
|
path!("/outer"),
|
|
json!({
|
|
"inner1": {
|
|
"a": "A",
|
|
"b": "B",
|
|
"inner3": {
|
|
"d": "D",
|
|
},
|
|
"inner4": {}
|
|
},
|
|
"inner2": {
|
|
"c": "C",
|
|
}
|
|
}),
|
|
)
|
|
.await;
|
|
|
|
assert_eq!(
|
|
fs.files(),
|
|
vec![
|
|
PathBuf::from(path!("/outer/inner1/a")),
|
|
PathBuf::from(path!("/outer/inner1/b")),
|
|
PathBuf::from(path!("/outer/inner2/c")),
|
|
PathBuf::from(path!("/outer/inner1/inner3/d")),
|
|
]
|
|
);
|
|
assert_eq!(
|
|
fs.directories(false),
|
|
vec![
|
|
PathBuf::from(path!("/")),
|
|
PathBuf::from(path!("/outer")),
|
|
PathBuf::from(path!("/outer/inner1")),
|
|
PathBuf::from(path!("/outer/inner2")),
|
|
PathBuf::from(path!("/outer/inner1/inner3")),
|
|
PathBuf::from(path!("/outer/inner1/inner4")),
|
|
]
|
|
);
|
|
|
|
let source = Path::new(path!("/outer"));
|
|
let target = Path::new(path!("/outer/inner1/outer"));
|
|
copy_recursive(fs.as_ref(), source, target, Default::default())
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_eq!(
|
|
fs.files(),
|
|
vec![
|
|
PathBuf::from(path!("/outer/inner1/a")),
|
|
PathBuf::from(path!("/outer/inner1/b")),
|
|
PathBuf::from(path!("/outer/inner2/c")),
|
|
PathBuf::from(path!("/outer/inner1/inner3/d")),
|
|
PathBuf::from(path!("/outer/inner1/outer/inner1/a")),
|
|
PathBuf::from(path!("/outer/inner1/outer/inner1/b")),
|
|
PathBuf::from(path!("/outer/inner1/outer/inner2/c")),
|
|
PathBuf::from(path!("/outer/inner1/outer/inner1/inner3/d")),
|
|
]
|
|
);
|
|
assert_eq!(
|
|
fs.directories(false),
|
|
vec![
|
|
PathBuf::from(path!("/")),
|
|
PathBuf::from(path!("/outer")),
|
|
PathBuf::from(path!("/outer/inner1")),
|
|
PathBuf::from(path!("/outer/inner2")),
|
|
PathBuf::from(path!("/outer/inner1/inner3")),
|
|
PathBuf::from(path!("/outer/inner1/inner4")),
|
|
PathBuf::from(path!("/outer/inner1/outer")),
|
|
PathBuf::from(path!("/outer/inner1/outer/inner1")),
|
|
PathBuf::from(path!("/outer/inner1/outer/inner2")),
|
|
PathBuf::from(path!("/outer/inner1/outer/inner1/inner3")),
|
|
PathBuf::from(path!("/outer/inner1/outer/inner1/inner4")),
|
|
]
|
|
);
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_copy_recursive_with_overwriting(executor: BackgroundExecutor) {
|
|
let fs = FakeFs::new(executor.clone());
|
|
fs.insert_tree(
|
|
path!("/outer"),
|
|
json!({
|
|
"inner1": {
|
|
"a": "A",
|
|
"b": "B",
|
|
"outer": {
|
|
"inner1": {
|
|
"a": "B"
|
|
}
|
|
}
|
|
},
|
|
"inner2": {
|
|
"c": "C",
|
|
}
|
|
}),
|
|
)
|
|
.await;
|
|
|
|
assert_eq!(
|
|
fs.files(),
|
|
vec![
|
|
PathBuf::from(path!("/outer/inner1/a")),
|
|
PathBuf::from(path!("/outer/inner1/b")),
|
|
PathBuf::from(path!("/outer/inner2/c")),
|
|
PathBuf::from(path!("/outer/inner1/outer/inner1/a")),
|
|
]
|
|
);
|
|
assert_eq!(
|
|
fs.load(path!("/outer/inner1/outer/inner1/a").as_ref())
|
|
.await
|
|
.unwrap(),
|
|
"B",
|
|
);
|
|
|
|
let source = Path::new(path!("/outer"));
|
|
let target = Path::new(path!("/outer/inner1/outer"));
|
|
copy_recursive(
|
|
fs.as_ref(),
|
|
source,
|
|
target,
|
|
CopyOptions {
|
|
overwrite: true,
|
|
..Default::default()
|
|
},
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_eq!(
|
|
fs.files(),
|
|
vec![
|
|
PathBuf::from(path!("/outer/inner1/a")),
|
|
PathBuf::from(path!("/outer/inner1/b")),
|
|
PathBuf::from(path!("/outer/inner2/c")),
|
|
PathBuf::from(path!("/outer/inner1/outer/inner1/a")),
|
|
PathBuf::from(path!("/outer/inner1/outer/inner1/b")),
|
|
PathBuf::from(path!("/outer/inner1/outer/inner2/c")),
|
|
PathBuf::from(path!("/outer/inner1/outer/inner1/outer/inner1/a")),
|
|
]
|
|
);
|
|
assert_eq!(
|
|
fs.load(path!("/outer/inner1/outer/inner1/a").as_ref())
|
|
.await
|
|
.unwrap(),
|
|
"A"
|
|
);
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_copy_recursive_with_ignoring(executor: BackgroundExecutor) {
|
|
let fs = FakeFs::new(executor.clone());
|
|
fs.insert_tree(
|
|
path!("/outer"),
|
|
json!({
|
|
"inner1": {
|
|
"a": "A",
|
|
"b": "B",
|
|
"outer": {
|
|
"inner1": {
|
|
"a": "B"
|
|
}
|
|
}
|
|
},
|
|
"inner2": {
|
|
"c": "C",
|
|
}
|
|
}),
|
|
)
|
|
.await;
|
|
|
|
assert_eq!(
|
|
fs.files(),
|
|
vec![
|
|
PathBuf::from(path!("/outer/inner1/a")),
|
|
PathBuf::from(path!("/outer/inner1/b")),
|
|
PathBuf::from(path!("/outer/inner2/c")),
|
|
PathBuf::from(path!("/outer/inner1/outer/inner1/a")),
|
|
]
|
|
);
|
|
assert_eq!(
|
|
fs.load(path!("/outer/inner1/outer/inner1/a").as_ref())
|
|
.await
|
|
.unwrap(),
|
|
"B",
|
|
);
|
|
|
|
let source = Path::new(path!("/outer"));
|
|
let target = Path::new(path!("/outer/inner1/outer"));
|
|
copy_recursive(
|
|
fs.as_ref(),
|
|
source,
|
|
target,
|
|
CopyOptions {
|
|
ignore_if_exists: true,
|
|
..Default::default()
|
|
},
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_eq!(
|
|
fs.files(),
|
|
vec![
|
|
PathBuf::from(path!("/outer/inner1/a")),
|
|
PathBuf::from(path!("/outer/inner1/b")),
|
|
PathBuf::from(path!("/outer/inner2/c")),
|
|
PathBuf::from(path!("/outer/inner1/outer/inner1/a")),
|
|
PathBuf::from(path!("/outer/inner1/outer/inner1/b")),
|
|
PathBuf::from(path!("/outer/inner1/outer/inner2/c")),
|
|
PathBuf::from(path!("/outer/inner1/outer/inner1/outer/inner1/a")),
|
|
]
|
|
);
|
|
assert_eq!(
|
|
fs.load(path!("/outer/inner1/outer/inner1/a").as_ref())
|
|
.await
|
|
.unwrap(),
|
|
"B"
|
|
);
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_realfs_atomic_write(executor: BackgroundExecutor) {
|
|
// With the file handle still open, the file should be replaced
|
|
// https://github.com/zed-industries/zed/issues/30054
|
|
let fs = RealFs::new(None, executor);
|
|
let temp_dir = TempDir::new().unwrap();
|
|
let file_to_be_replaced = temp_dir.path().join("file.txt");
|
|
let mut file = std::fs::File::create_new(&file_to_be_replaced).unwrap();
|
|
file.write_all(b"Hello").unwrap();
|
|
// drop(file); // We still hold the file handle here
|
|
let content = std::fs::read_to_string(&file_to_be_replaced).unwrap();
|
|
assert_eq!(content, "Hello");
|
|
smol::block_on(fs.atomic_write(file_to_be_replaced.clone(), "World".into())).unwrap();
|
|
let content = std::fs::read_to_string(&file_to_be_replaced).unwrap();
|
|
assert_eq!(content, "World");
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_realfs_atomic_write_non_existing_file(executor: BackgroundExecutor) {
|
|
let fs = RealFs::new(None, executor);
|
|
let temp_dir = TempDir::new().unwrap();
|
|
let file_to_be_replaced = temp_dir.path().join("file.txt");
|
|
smol::block_on(fs.atomic_write(file_to_be_replaced.clone(), "Hello".into())).unwrap();
|
|
let content = std::fs::read_to_string(&file_to_be_replaced).unwrap();
|
|
assert_eq!(content, "Hello");
|
|
}
|
|
|
|
#[gpui::test]
|
|
#[cfg(target_os = "windows")]
|
|
async fn test_realfs_canonicalize(executor: BackgroundExecutor) {
|
|
use util::paths::SanitizedPath;
|
|
|
|
let fs = RealFs::new(None, executor);
|
|
let temp_dir = TempDir::new().unwrap();
|
|
let file = temp_dir.path().join("test (1).txt");
|
|
let file = SanitizedPath::new(&file);
|
|
std::fs::write(&file, "test").unwrap();
|
|
|
|
let canonicalized = fs.canonicalize(file.as_path()).await;
|
|
assert!(canonicalized.is_ok());
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_rename(executor: BackgroundExecutor) {
|
|
let fs = FakeFs::new(executor.clone());
|
|
fs.insert_tree(
|
|
path!("/root"),
|
|
json!({
|
|
"src": {
|
|
"file_a.txt": "content a",
|
|
"file_b.txt": "content b"
|
|
}
|
|
}),
|
|
)
|
|
.await;
|
|
|
|
fs.rename(
|
|
Path::new(path!("/root/src/file_a.txt")),
|
|
Path::new(path!("/root/src/new/renamed_a.txt")),
|
|
RenameOptions {
|
|
create_parents: true,
|
|
..Default::default()
|
|
},
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
// Assert that the `file_a.txt` file was being renamed and moved to a
|
|
// different directory that did not exist before.
|
|
assert_eq!(
|
|
fs.files(),
|
|
vec![
|
|
PathBuf::from(path!("/root/src/file_b.txt")),
|
|
PathBuf::from(path!("/root/src/new/renamed_a.txt")),
|
|
]
|
|
);
|
|
|
|
let result = fs
|
|
.rename(
|
|
Path::new(path!("/root/src/file_b.txt")),
|
|
Path::new(path!("/root/src/old/renamed_b.txt")),
|
|
RenameOptions {
|
|
create_parents: false,
|
|
..Default::default()
|
|
},
|
|
)
|
|
.await;
|
|
|
|
// Assert that the `file_b.txt` file was not renamed nor moved, as
|
|
// `create_parents` was set to `false`.
|
|
// different directory that did not exist before.
|
|
assert!(result.is_err());
|
|
assert_eq!(
|
|
fs.files(),
|
|
vec![
|
|
PathBuf::from(path!("/root/src/file_b.txt")),
|
|
PathBuf::from(path!("/root/src/new/renamed_a.txt")),
|
|
]
|
|
);
|
|
}
|
|
|
|
#[gpui::test]
|
|
#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))]
|
|
async fn test_realfs_parallel_rename_without_overwrite_preserves_losing_source(
|
|
executor: BackgroundExecutor,
|
|
) {
|
|
let temp_dir = TempDir::new().unwrap();
|
|
let root = temp_dir.path();
|
|
let source_a = root.join("dir_a/shared.txt");
|
|
let source_b = root.join("dir_b/shared.txt");
|
|
let target = root.join("shared.txt");
|
|
|
|
std::fs::create_dir_all(source_a.parent().unwrap()).unwrap();
|
|
std::fs::create_dir_all(source_b.parent().unwrap()).unwrap();
|
|
std::fs::write(&source_a, "from a").unwrap();
|
|
std::fs::write(&source_b, "from b").unwrap();
|
|
|
|
let fs = RealFs::new(None, executor);
|
|
let (first_result, second_result) = futures::future::join(
|
|
fs.rename(&source_a, &target, RenameOptions::default()),
|
|
fs.rename(&source_b, &target, RenameOptions::default()),
|
|
)
|
|
.await;
|
|
|
|
assert_ne!(first_result.is_ok(), second_result.is_ok());
|
|
assert!(target.exists());
|
|
assert_eq!(source_a.exists() as u8 + source_b.exists() as u8, 1);
|
|
}
|
|
|
|
#[gpui::test]
|
|
#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))]
|
|
async fn test_realfs_rename_ignore_if_exists_leaves_source_and_target_unchanged(
|
|
executor: BackgroundExecutor,
|
|
) {
|
|
let temp_dir = TempDir::new().unwrap();
|
|
let root = temp_dir.path();
|
|
let source = root.join("source.txt");
|
|
let target = root.join("target.txt");
|
|
|
|
std::fs::write(&source, "from source").unwrap();
|
|
std::fs::write(&target, "from target").unwrap();
|
|
|
|
let fs = RealFs::new(None, executor);
|
|
let result = fs
|
|
.rename(
|
|
&source,
|
|
&target,
|
|
RenameOptions {
|
|
ignore_if_exists: true,
|
|
..Default::default()
|
|
},
|
|
)
|
|
.await;
|
|
|
|
assert!(result.is_ok());
|
|
|
|
assert_eq!(std::fs::read_to_string(&source).unwrap(), "from source");
|
|
assert_eq!(std::fs::read_to_string(&target).unwrap(), "from target");
|
|
}
|
|
|
|
#[gpui::test]
|
|
#[cfg(unix)]
|
|
async fn test_realfs_broken_symlink_metadata(executor: BackgroundExecutor) {
|
|
let tempdir = TempDir::new().unwrap();
|
|
let path = tempdir.path();
|
|
let fs = RealFs::new(None, executor);
|
|
let symlink_path = path.join("symlink");
|
|
smol::block_on(fs.create_symlink(&symlink_path, PathBuf::from("file_a.txt"))).unwrap();
|
|
let metadata = fs
|
|
.metadata(&symlink_path)
|
|
.await
|
|
.expect("metadata call succeeds")
|
|
.expect("metadata returned");
|
|
assert!(metadata.is_symlink);
|
|
assert!(!metadata.is_dir);
|
|
assert!(!metadata.is_fifo);
|
|
assert!(!metadata.is_executable);
|
|
// don't care about len or mtime on symlinks?
|
|
}
|
|
|
|
#[gpui::test]
|
|
#[cfg(unix)]
|
|
async fn test_realfs_symlink_loop_metadata(executor: BackgroundExecutor) {
|
|
let tempdir = TempDir::new().unwrap();
|
|
let path = tempdir.path();
|
|
let fs = RealFs::new(None, executor);
|
|
let symlink_path = path.join("symlink");
|
|
smol::block_on(fs.create_symlink(&symlink_path, PathBuf::from("symlink"))).unwrap();
|
|
let metadata = fs
|
|
.metadata(&symlink_path)
|
|
.await
|
|
.expect("metadata call succeeds")
|
|
.expect("metadata returned");
|
|
assert!(metadata.is_symlink);
|
|
assert!(!metadata.is_dir);
|
|
assert!(!metadata.is_fifo);
|
|
assert!(!metadata.is_executable);
|
|
// don't care about len or mtime on symlinks?
|
|
}
|