This is an automated email from the ASF dual-hosted git repository. xuanwo pushed a commit to branch xuanwo/delete-recursive in repository https://gitbox.apache.org/repos/asf/opendal.git
commit 83ede25b9c1349d17c5ec827351e114d5099c84a Author: Xuanwo <[email protected]> AuthorDate: Thu Nov 27 18:33:21 2025 +0800 feat: Add simulation for delete with recursive Signed-off-by: Xuanwo <[email protected]> --- core/src/layers/correctness_check.rs | 8 +++ core/src/layers/simulate.rs | 96 ++++++++++++++++++++++++++++- core/src/raw/ops.rs | 13 ++++ core/src/types/capability.rs | 2 + core/src/types/delete/deleter.rs | 3 + core/src/types/delete/input.rs | 6 ++ core/src/types/operator/operator_futures.rs | 6 ++ core/src/types/options.rs | 6 ++ 8 files changed, 137 insertions(+), 3 deletions(-) diff --git a/core/src/layers/correctness_check.rs b/core/src/layers/correctness_check.rs index ca24fe11c..57b8dada4 100644 --- a/core/src/layers/correctness_check.rs +++ b/core/src/layers/correctness_check.rs @@ -234,6 +234,14 @@ impl<T> CheckWrapper<T> { )); } + if args.recursive() && !self.info.full_capability().delete_with_recursive { + return Err(new_unsupported_error( + &self.info, + Operation::Delete, + "recursive", + )); + } + Ok(()) } } diff --git a/core/src/layers/simulate.rs b/core/src/layers/simulate.rs index ee340fc5c..862c84993 100644 --- a/core/src/layers/simulate.rs +++ b/core/src/layers/simulate.rs @@ -20,6 +20,7 @@ use std::fmt::Formatter; use std::sync::Arc; use crate::raw::oio::FlatLister; +use crate::raw::oio::List; use crate::raw::oio::PrefixLister; use crate::raw::*; use crate::*; @@ -30,6 +31,7 @@ pub struct SimulateLayer { list_recursive: bool, stat_dir: bool, create_dir: bool, + delete_recursive: bool, } impl Default for SimulateLayer { @@ -38,6 +40,7 @@ impl Default for SimulateLayer { list_recursive: true, stat_dir: true, create_dir: true, + delete_recursive: true, } } } @@ -60,6 +63,12 @@ impl SimulateLayer { self.create_dir = enabled; self } + + /// Enable or disable recursive delete simulation. Default: true. + pub fn with_delete_recursive(mut self, enabled: bool) -> Self { + self.delete_recursive = enabled; + self + } } impl<A: Access> Layer<A> for SimulateLayer { @@ -71,6 +80,9 @@ impl<A: Access> Layer<A> for SimulateLayer { if self.create_dir && cap.list && cap.write_can_empty { cap.create_dir = true; } + if self.delete_recursive && cap.list && cap.delete { + cap.delete_with_recursive = true; + } cap }); @@ -153,7 +165,7 @@ impl<A: Access> SimulateAccessor<A> { self.inner.stat(path, args).await } - async fn simulate_list( + pub(crate) async fn simulate_list( &self, path: &str, args: OpList, @@ -206,6 +218,48 @@ impl<A: Access> SimulateAccessor<A> { Ok((rp, lister)) } + + pub(crate) async fn simulate_delete_with_recursive<D: oio::Delete>( + &self, + deleter: &mut D, + path: &str, + args: OpDelete, + ) -> Result<()> { + if !self.info.full_capability().delete_with_recursive { + return Err(Error::new( + ErrorKind::Unsupported, + "recursive delete is not supported", + )); + } + + let non_recursive = args.clone().with_recursive(false); + + match self.inner().stat(path, OpStat::default()).await { + Ok(meta) => { + let meta = meta.into_metadata(); + if !meta.mode().is_dir() { + deleter.delete(path, non_recursive.clone()).await?; + } + } + Err(err) if err.kind() == ErrorKind::NotFound => {} + Err(err) => return Err(err), + } + + let (_rp, mut lister) = self + .simulate_list(path, OpList::new().with_recursive(true)) + .await?; + + while let Some(entry) = lister.next().await? { + let entry = entry.into_entry(); + let mut entry_args = non_recursive.clone(); + if let Some(version) = entry.metadata().version() { + entry_args = entry_args.with_version(version); + } + deleter.delete(entry.path(), entry_args).await?; + } + + Ok(()) + } } impl<A: Access> LayeredAccess for SimulateAccessor<A> { @@ -213,7 +267,7 @@ impl<A: Access> LayeredAccess for SimulateAccessor<A> { type Reader = A::Reader; type Writer = A::Writer; type Lister = SimulateLister<A, A::Lister>; - type Deleter = A::Deleter; + type Deleter = SimulateDeleter<A, A::Deleter>; fn inner(&self) -> &Self::Inner { &self.inner @@ -240,7 +294,14 @@ impl<A: Access> LayeredAccess for SimulateAccessor<A> { } async fn delete(&self) -> Result<(RpDelete, Self::Deleter)> { - self.inner().delete().await + let (rp, deleter) = self.inner().delete().await?; + let accessor = SimulateAccessor { + config: self.config.clone(), + info: self.info.clone(), + inner: self.inner.clone(), + }; + + Ok((rp, SimulateDeleter::new(accessor, deleter))) } async fn list(&self, path: &str, args: OpList) -> Result<(RpList, Self::Lister)> { @@ -254,3 +315,32 @@ impl<A: Access> LayeredAccess for SimulateAccessor<A> { pub type SimulateLister<A, P> = FourWays<P, FlatLister<Arc<A>, P>, PrefixLister<P>, PrefixLister<FlatLister<Arc<A>, P>>>; + +/// Deleter wrapper that simulates recursive deletion. +pub struct SimulateDeleter<A: Access, D> { + accessor: SimulateAccessor<A>, + deleter: D, +} + +impl<A: Access, D> SimulateDeleter<A, D> { + pub fn new(accessor: SimulateAccessor<A>, deleter: D) -> Self { + Self { accessor, deleter } + } +} + +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; + } + + self.deleter.delete(path, args).await + } + + fn close(&mut self) -> impl Future<Output = Result<()>> + MaybeSend { + self.deleter.close() + } +} diff --git a/core/src/raw/ops.rs b/core/src/raw/ops.rs index abd89be9e..117cbd586 100644 --- a/core/src/raw/ops.rs +++ b/core/src/raw/ops.rs @@ -43,6 +43,7 @@ impl OpCreateDir { #[derive(Debug, Clone, Default, Eq, Hash, PartialEq)] pub struct OpDelete { version: Option<String>, + recursive: bool, } impl OpDelete { @@ -59,16 +60,28 @@ impl OpDelete { self } + /// Change the recursive flag of this delete operation. + pub fn with_recursive(mut self, recursive: bool) -> Self { + self.recursive = recursive; + self + } + /// Get the version of this delete operation. pub fn version(&self) -> Option<&str> { self.version.as_deref() } + + /// Whether this delete should remove objects recursively. + pub fn recursive(&self) -> bool { + self.recursive + } } impl From<options::DeleteOptions> for OpDelete { fn from(value: options::DeleteOptions) -> Self { Self { version: value.version, + recursive: value.recursive, } } } diff --git a/core/src/types/capability.rs b/core/src/types/capability.rs index 89be64107..7c49eee18 100644 --- a/core/src/types/capability.rs +++ b/core/src/types/capability.rs @@ -143,6 +143,8 @@ pub struct Capability { pub delete: bool, /// Indicates if versions delete operations are supported. pub delete_with_version: bool, + /// Indicates if recursive delete operations are supported. + pub delete_with_recursive: bool, /// Maximum size supported for single delete operations. pub delete_max_size: Option<usize>, diff --git a/core/src/types/delete/deleter.rs b/core/src/types/delete/deleter.rs index 05621f0f7..cee20eed3 100644 --- a/core/src/types/delete/deleter.rs +++ b/core/src/types/delete/deleter.rs @@ -99,6 +99,9 @@ impl Deleter { if let Some(version) = &input.version { op = op.with_version(version); } + if input.recursive { + op = op.with_recursive(true); + } self.deleter.delete_dyn(&input.path, op).await?; Ok(()) diff --git a/core/src/types/delete/input.rs b/core/src/types/delete/input.rs index 3530b29af..62982921a 100644 --- a/core/src/types/delete/input.rs +++ b/core/src/types/delete/input.rs @@ -26,6 +26,8 @@ pub struct DeleteInput { pub path: String, /// The version of the path to delete. pub version: Option<String>, + /// Whether to perform recursive deletion. + pub recursive: bool, } /// IntoDeleteInput is a helper trait that makes it easier for users to play with `Deleter`. @@ -46,6 +48,7 @@ impl IntoDeleteInput for &str { fn into_delete_input(self) -> DeleteInput { DeleteInput { path: self.to_string(), + recursive: false, ..Default::default() } } @@ -56,6 +59,7 @@ impl IntoDeleteInput for String { fn into_delete_input(self) -> DeleteInput { DeleteInput { path: self, + recursive: false, ..Default::default() } } @@ -69,6 +73,7 @@ impl IntoDeleteInput for (String, OpDelete) { let mut input = DeleteInput { path, + recursive: args.recursive(), ..Default::default() }; @@ -86,6 +91,7 @@ impl IntoDeleteInput for Entry { let mut input = DeleteInput { path, + recursive: false, ..Default::default() }; diff --git a/core/src/types/operator/operator_futures.rs b/core/src/types/operator/operator_futures.rs index 1a1276bb1..4c67b6a5f 100644 --- a/core/src/types/operator/operator_futures.rs +++ b/core/src/types/operator/operator_futures.rs @@ -1258,6 +1258,12 @@ impl<F: Future<Output = Result<()>>> FutureDelete<F> { self.args.version = Some(v.to_string()); self } + + /// Enable recursive deletion. + pub fn recursive(mut self, recursive: bool) -> Self { + self.args.recursive = recursive; + self + } } /// Future that generated by [`Operator::deleter_with`]. diff --git a/core/src/types/options.rs b/core/src/types/options.rs index d6d81a5a8..f373dd5da 100644 --- a/core/src/types/options.rs +++ b/core/src/types/options.rs @@ -25,6 +25,12 @@ use std::collections::HashMap; pub struct DeleteOptions { /// The version of the file to delete. pub version: Option<String>, + /// Whether to delete the target recursively. + /// + /// - If `false`, behaves like the traditional single-object delete. + /// - If `true`, all entries under the path (or sharing the prefix for file-like paths) + /// will be removed. + pub recursive: bool, } /// Options for list operations.
