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> {
|
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"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue