This is an automated email from the ASF dual-hosted git repository. xuanwo pushed a commit to branch xuanwo/fs-recursive-delete in repository https://gitbox.apache.org/repos/asf/opendal.git
commit a26cc9e327f1fc5bb9f15a275e287c0b352c5a5c Author: Xuanwo <[email protected]> AuthorDate: Thu Nov 27 19:56:39 2025 +0800 feat: Add delete with recursive support for fs Signed-off-by: Xuanwo <[email protected]> --- core/src/layers/simulate.rs | 18 +++++++++++++----- core/src/services/fs/backend.rs | 1 + core/src/services/fs/deleter.rs | 12 ++++++++++-- core/tests/behavior/async_delete.rs | 35 +++++++++++++++++++++++++++++++++++ 4 files changed, 59 insertions(+), 7 deletions(-) diff --git a/core/src/layers/simulate.rs b/core/src/layers/simulate.rs index 3f8139eb8..1d64d6408 100644 --- a/core/src/layers/simulate.rs +++ b/core/src/layers/simulate.rs @@ -319,11 +319,19 @@ impl<A: Access, D> SimulateDeleter<A, D> { impl<A: Access, D: oio::Delete> oio::Delete for SimulateDeleter<A, D> { async fn delete(&mut self, path: &str, args: OpDelete) -> Result<()> { - if args.recursive() && self.accessor.config.delete_recursive { - return self - .accessor - .simulate_delete_with_recursive(&mut self.deleter, path, args) - .await; + if args.recursive() { + let cap = self.accessor.info.native_capability(); + + if cap.delete_with_recursive { + return self.deleter.delete(path, args).await; + } + + if self.accessor.config.delete_recursive { + return self + .accessor + .simulate_delete_with_recursive(&mut self.deleter, path, args) + .await; + } } self.deleter.delete(path, args).await diff --git a/core/src/services/fs/backend.rs b/core/src/services/fs/backend.rs index 8f6ddb417..923ce2cda 100644 --- a/core/src/services/fs/backend.rs +++ b/core/src/services/fs/backend.rs @@ -153,6 +153,7 @@ impl Builder for FsBuilder { create_dir: true, delete: true, + delete_with_recursive: true, list: true, diff --git a/core/src/services/fs/deleter.rs b/core/src/services/fs/deleter.rs index cd896aae9..bf8eeecd6 100644 --- a/core/src/services/fs/deleter.rs +++ b/core/src/services/fs/deleter.rs @@ -32,15 +32,23 @@ impl FsDeleter { } impl oio::OneShotDelete for FsDeleter { - async fn delete_once(&self, path: String, _: OpDelete) -> Result<()> { + async fn delete_once(&self, path: String, args: OpDelete) -> Result<()> { let p = self.core.root.join(path.trim_end_matches('/')); + let recursive = args.recursive(); + let meta = tokio::fs::metadata(&p).await; match meta { Ok(meta) => { if meta.is_dir() { - tokio::fs::remove_dir(&p).await.map_err(new_std_io_error)?; + if recursive { + tokio::fs::remove_dir_all(&p) + .await + .map_err(new_std_io_error)?; + } else { + tokio::fs::remove_dir(&p).await.map_err(new_std_io_error)?; + } } else { tokio::fs::remove_file(&p).await.map_err(new_std_io_error)?; } diff --git a/core/tests/behavior/async_delete.rs b/core/tests/behavior/async_delete.rs index e2b141727..625496075 100644 --- a/core/tests/behavior/async_delete.rs +++ b/core/tests/behavior/async_delete.rs @@ -39,6 +39,9 @@ pub fn tests(op: &Operator, tests: &mut Vec<Trial>) { test_batch_delete, test_batch_delete_with_version )); + if cap.delete_with_recursive { + tests.extend(async_trials!(op, test_delete_with_recursive_basic)); + } if cap.list_with_recursive { tests.extend(async_trials!(op, test_remove_all_basic)); if !cap.create_dir { @@ -276,6 +279,38 @@ pub async fn test_delete_with_not_existing_version(op: Operator) -> Result<()> { Ok(()) } +pub async fn test_delete_with_recursive_basic(op: Operator) -> Result<()> { + if !op.info().full_capability().delete_with_recursive { + return Ok(()); + } + + let base = format!("delete_recursive_{}/", uuid::Uuid::new_v4()); + + let files = [ + format!("{base}file.txt"), + format!("{base}dir1/file1.txt"), + format!("{base}dir1/dir2/file2.txt"), + ]; + + for path in &files { + op.write(path, "delete recursive").await?; + } + + op.delete_with(&base).recursive(true).await?; + + let mut l = op.lister_with(&base).recursive(true).await?; + assert!( + l.try_next().await?.is_none(), + "all entries should be removed" + ); + + for path in &files { + assert!(!op.exists(path).await?, "{path} should be removed"); + } + + Ok(()) +} + pub async fn test_batch_delete(op: Operator) -> Result<()> { let mut cap = op.info().full_capability(); if cap.delete_max_size.unwrap_or(1) <= 1 {
