Skip to content

Commit

Permalink
fs: Fix copy_recursive (#25317)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
JunkuiZhang authored Feb 24, 2025
1 parent f517050 commit 1f257f4
Showing 1 changed file with 257 additions and 35 deletions.
292 changes: 257 additions & 35 deletions crates/fs/src/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -2007,52 +2010,73 @@ 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,
) -> BoxFuture<'a, Result<()>> {
use futures::future::FutureExt;

async move {
let metadata = fs
.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()) {
) -> 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 {
return Ok(());
continue;
} else {
return Err(anyhow!("{target:?} already exists"));
return Err(anyhow!("{target_item:?} already exists"));
}
}

let _ = fs
.remove_dir(
target,
&target_item,
RemoveOptions {
recursive: true,
ignore_if_not_exists: true,
},
)
.await;
fs.create_dir(target).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;

async move {
let metadata = fs
.metadata(source)
.await?
.ok_or_else(|| anyhow!("path does not exist: {}", source.display()))?;

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()
}
Expand Down Expand Up @@ -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",
Expand All @@ -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"
);
}
}

0 comments on commit 1f257f4

Please sign in to comment.