fs: Fix copy_recursive (#25317)

Closes #24746

This PR modifies the implementation of `copy_recursive`. Previously, we
were copying and pasting simultaneously, which caused an issue when a
user copied a folder into one of its subfolders. This resulted in new
content being created in the folder while copying, and subsequent
recursive calls to `copy_recursive` would continue this process, leading
to an infinite loop.

In this PR, the approach has been changed: we now first collect the
paths of the files to be copied, and only then perform the copy
operation.

Additionally, I have added corresponding tests. On the main branch, this
test would previously run indefinitely.

Release Notes:

- Fixed `copy_recursive` runs infinitely when copying a folder into its
subfolder.
This commit is contained in:
张小白 2025-02-24 17:02:14 +08:00 committed by GitHub
parent f517050548
commit 1f257f4704
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -1379,7 +1379,10 @@ impl FakeFs {
pub fn files(&self) -> Vec<PathBuf> { pub fn files(&self) -> Vec<PathBuf> {
let mut result = Vec::new(); let mut result = Vec::new();
let mut queue = collections::VecDeque::new(); let mut queue = collections::VecDeque::new();
queue.push_back((PathBuf::from("/"), self.state.lock().root.clone())); queue.push_back((
PathBuf::from(util::path!("/")),
self.state.lock().root.clone(),
));
while let Some((path, entry)) = queue.pop_front() { while let Some((path, entry)) = queue.pop_front() {
let e = entry.lock(); let e = entry.lock();
match &*e { match &*e {
@ -2007,11 +2010,52 @@ pub fn normalize_path(path: &Path) -> PathBuf {
ret ret
} }
pub fn copy_recursive<'a>( pub async fn copy_recursive<'a>(
fs: &'a dyn Fs, fs: &'a dyn Fs,
source: &'a Path, source: &'a Path,
target: &'a Path, target: &'a Path,
options: CopyOptions, options: CopyOptions,
) -> Result<()> {
for (is_dir, item) in read_dir_items(fs, source).await? {
let Ok(item_relative_path) = item.strip_prefix(source) else {
continue;
};
let target_item = target.join(item_relative_path);
if is_dir {
if !options.overwrite && fs.metadata(&target_item).await.is_ok_and(|m| m.is_some()) {
if options.ignore_if_exists {
continue;
} else {
return Err(anyhow!("{target_item:?} already exists"));
}
}
let _ = fs
.remove_dir(
&target_item,
RemoveOptions {
recursive: true,
ignore_if_not_exists: true,
},
)
.await;
fs.create_dir(&target_item).await?;
} else {
fs.copy_file(&item, &target_item, options).await?;
}
}
Ok(())
}
async fn read_dir_items<'a>(fs: &'a dyn Fs, source: &'a Path) -> Result<Vec<(bool, PathBuf)>> {
let mut items = Vec::new();
read_recursive(fs, source, &mut items).await?;
Ok(items)
}
fn read_recursive<'a>(
fs: &'a dyn Fs,
source: &'a Path,
output: &'a mut Vec<(bool, PathBuf)>,
) -> BoxFuture<'a, Result<()>> { ) -> BoxFuture<'a, Result<()>> {
use futures::future::FutureExt; use futures::future::FutureExt;
@ -2020,39 +2064,19 @@ pub fn copy_recursive<'a>(
.metadata(source) .metadata(source)
.await? .await?
.ok_or_else(|| anyhow!("path does not exist: {}", source.display()))?; .ok_or_else(|| anyhow!("path does not exist: {}", source.display()))?;
if metadata.is_dir {
if !options.overwrite && fs.metadata(target).await.is_ok_and(|m| m.is_some()) {
if options.ignore_if_exists {
return Ok(());
} else {
return Err(anyhow!("{target:?} already exists"));
}
}
let _ = fs if metadata.is_dir {
.remove_dir( output.push((true, source.to_path_buf()));
target,
RemoveOptions {
recursive: true,
ignore_if_not_exists: true,
},
)
.await;
fs.create_dir(target).await?;
let mut children = fs.read_dir(source).await?; let mut children = fs.read_dir(source).await?;
while let Some(child_path) = children.next().await { while let Some(child_path) = children.next().await {
if let Ok(child_path) = child_path { if let Ok(child_path) = child_path {
if let Some(file_name) = child_path.file_name() { read_recursive(fs, &child_path, output).await?;
let child_target_path = target.join(file_name);
copy_recursive(fs, &child_path, &child_target_path, options).await?;
}
} }
} }
Ok(())
} else { } else {
fs.copy_file(source, target, options).await output.push((false, source.to_path_buf()));
} }
Ok(())
} }
.boxed() .boxed()
} }
@ -2094,12 +2118,13 @@ mod tests {
use super::*; use super::*;
use gpui::BackgroundExecutor; use gpui::BackgroundExecutor;
use serde_json::json; use serde_json::json;
use util::path;
#[gpui::test] #[gpui::test]
async fn test_fake_fs(executor: BackgroundExecutor) { async fn test_fake_fs(executor: BackgroundExecutor) {
let fs = FakeFs::new(executor.clone()); let fs = FakeFs::new(executor.clone());
fs.insert_tree( fs.insert_tree(
"/root", path!("/root"),
json!({ json!({
"dir1": { "dir1": {
"a": "A", "a": "A",
@ -2118,32 +2143,229 @@ mod tests {
assert_eq!( assert_eq!(
fs.files(), fs.files(),
vec![ vec![
PathBuf::from("/root/dir1/a"), PathBuf::from(path!("/root/dir1/a")),
PathBuf::from("/root/dir1/b"), PathBuf::from(path!("/root/dir1/b")),
PathBuf::from("/root/dir2/c"), PathBuf::from(path!("/root/dir2/c")),
PathBuf::from("/root/dir2/dir3/d"), PathBuf::from(path!("/root/dir2/dir3/d")),
] ]
); );
fs.create_symlink("/root/dir2/link-to-dir3".as_ref(), "./dir3".into()) fs.create_symlink(path!("/root/dir2/link-to-dir3").as_ref(), "./dir3".into())
.await .await
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
fs.canonicalize("/root/dir2/link-to-dir3".as_ref()) fs.canonicalize(path!("/root/dir2/link-to-dir3").as_ref())
.await .await
.unwrap(), .unwrap(),
PathBuf::from("/root/dir2/dir3"), PathBuf::from(path!("/root/dir2/dir3")),
); );
assert_eq!( assert_eq!(
fs.canonicalize("/root/dir2/link-to-dir3/d".as_ref()) fs.canonicalize(path!("/root/dir2/link-to-dir3/d").as_ref())
.await .await
.unwrap(), .unwrap(),
PathBuf::from("/root/dir2/dir3/d"), PathBuf::from(path!("/root/dir2/dir3/d")),
); );
assert_eq!( assert_eq!(
fs.load("/root/dir2/link-to-dir3/d".as_ref()).await.unwrap(), fs.load(path!("/root/dir2/link-to-dir3/d").as_ref())
.await
.unwrap(),
"D", "D",
); );
} }
#[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",
}
},
"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")),
]
);
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")),
]
);
}
#[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"
);
}
} }