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:
parent
f517050548
commit
1f257f4704
1 changed files with 260 additions and 38 deletions
|
@ -1379,7 +1379,10 @@ impl FakeFs {
|
|||
pub fn files(&self) -> Vec<PathBuf> {
|
||||
let mut result = Vec::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() {
|
||||
let e = entry.lock();
|
||||
match &*e {
|
||||
|
@ -2007,11 +2010,52 @@ pub fn normalize_path(path: &Path) -> PathBuf {
|
|||
ret
|
||||
}
|
||||
|
||||
pub fn copy_recursive<'a>(
|
||||
pub async fn copy_recursive<'a>(
|
||||
fs: &'a dyn Fs,
|
||||
source: &'a Path,
|
||||
target: &'a Path,
|
||||
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<()>> {
|
||||
use futures::future::FutureExt;
|
||||
|
||||
|
@ -2020,39 +2064,19 @@ pub fn copy_recursive<'a>(
|
|||
.metadata(source)
|
||||
.await?
|
||||
.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
|
||||
.remove_dir(
|
||||
target,
|
||||
RemoveOptions {
|
||||
recursive: true,
|
||||
ignore_if_not_exists: true,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
fs.create_dir(target).await?;
|
||||
if metadata.is_dir {
|
||||
output.push((true, source.to_path_buf()));
|
||||
let mut children = fs.read_dir(source).await?;
|
||||
while let Some(child_path) = children.next().await {
|
||||
if let Ok(child_path) = child_path {
|
||||
if let Some(file_name) = child_path.file_name() {
|
||||
let child_target_path = target.join(file_name);
|
||||
copy_recursive(fs, &child_path, &child_target_path, options).await?;
|
||||
}
|
||||
read_recursive(fs, &child_path, output).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
fs.copy_file(source, target, options).await
|
||||
output.push((false, source.to_path_buf()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
|
@ -2094,12 +2118,13 @@ mod tests {
|
|||
use super::*;
|
||||
use gpui::BackgroundExecutor;
|
||||
use serde_json::json;
|
||||
use util::path;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_fake_fs(executor: BackgroundExecutor) {
|
||||
let fs = FakeFs::new(executor.clone());
|
||||
fs.insert_tree(
|
||||
"/root",
|
||||
path!("/root"),
|
||||
json!({
|
||||
"dir1": {
|
||||
"a": "A",
|
||||
|
@ -2118,32 +2143,229 @@ mod tests {
|
|||
assert_eq!(
|
||||
fs.files(),
|
||||
vec![
|
||||
PathBuf::from("/root/dir1/a"),
|
||||
PathBuf::from("/root/dir1/b"),
|
||||
PathBuf::from("/root/dir2/c"),
|
||||
PathBuf::from("/root/dir2/dir3/d"),
|
||||
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("/root/dir2/link-to-dir3".as_ref(), "./dir3".into())
|
||||
fs.create_symlink(path!("/root/dir2/link-to-dir3").as_ref(), "./dir3".into())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
fs.canonicalize("/root/dir2/link-to-dir3".as_ref())
|
||||
fs.canonicalize(path!("/root/dir2/link-to-dir3").as_ref())
|
||||
.await
|
||||
.unwrap(),
|
||||
PathBuf::from("/root/dir2/dir3"),
|
||||
PathBuf::from(path!("/root/dir2/dir3")),
|
||||
);
|
||||
assert_eq!(
|
||||
fs.canonicalize("/root/dir2/link-to-dir3/d".as_ref())
|
||||
fs.canonicalize(path!("/root/dir2/link-to-dir3/d").as_ref())
|
||||
.await
|
||||
.unwrap(),
|
||||
PathBuf::from("/root/dir2/dir3/d"),
|
||||
PathBuf::from(path!("/root/dir2/dir3/d")),
|
||||
);
|
||||
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",
|
||||
);
|
||||
}
|
||||
|
||||
#[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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue