This is an automated email from the ASF dual-hosted git repository.

xuanwo pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/opendal.git


The following commit(s) were added to refs/heads/main by this push:
     new 7864bd864 feat: Add simulation for delete with recursive (#6824)
7864bd864 is described below

commit 7864bd8647da76fb673d70a366af12236f8300d7
Author: Xuanwo <[email protected]>
AuthorDate: Thu Nov 27 19:22:09 2025 +0800

    feat: Add simulation for delete with recursive (#6824)
    
    * feat: Add simulation for delete with recursive
    
    Signed-off-by: Xuanwo <[email protected]>
    
    * remove not needed pub
    
    Signed-off-by: Xuanwo <[email protected]>
    
    * Remove not needed stat
    
    Signed-off-by: Xuanwo <[email protected]>
    
    * Fix build
    
    Signed-off-by: Xuanwo <[email protected]>
    
    * Make node happy
    
    Signed-off-by: Xuanwo <[email protected]>
    
    ---------
    
    Signed-off-by: Xuanwo <[email protected]>
---
 bindings/nodejs/generated.d.ts              |  2 +
 bindings/nodejs/src/options.rs              |  3 ++
 core/src/layers/correctness_check.rs        |  8 +++
 core/src/layers/simulate.rs                 | 83 ++++++++++++++++++++++++++++-
 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 +++
 10 files changed, 130 insertions(+), 2 deletions(-)

diff --git a/bindings/nodejs/generated.d.ts b/bindings/nodejs/generated.d.ts
index 68ba74179..77ddf0705 100644
--- a/bindings/nodejs/generated.d.ts
+++ b/bindings/nodejs/generated.d.ts
@@ -1051,6 +1051,8 @@ export declare class Writer {
 
 export interface DeleteOptions {
   version?: string
+  /** Whether to delete recursively. */
+  recursive?: boolean
 }
 
 export declare const enum EntryMode {
diff --git a/bindings/nodejs/src/options.rs b/bindings/nodejs/src/options.rs
index 8331d393a..34576cf83 100644
--- a/bindings/nodejs/src/options.rs
+++ b/bindings/nodejs/src/options.rs
@@ -508,12 +508,15 @@ impl From<WriteOptions> for 
opendal::options::WriteOptions {
 #[derive(Default)]
 pub struct DeleteOptions {
     pub version: Option<String>,
+    /// Whether to delete recursively.
+    pub recursive: Option<bool>,
 }
 
 impl From<DeleteOptions> for opendal::options::DeleteOptions {
     fn from(value: DeleteOptions) -> Self {
         Self {
             version: value.version,
+            recursive: value.recursive.unwrap_or_default(),
         }
     }
 }
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..3f8139eb8 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
         });
 
@@ -206,6 +218,37 @@ 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);
+
+        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 +256,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 +283,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 +304,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.

Reply via email to