Skip to content

feat: add bulk delete (DeleteObjects) support#448

Open
michaelludwig1 wants to merge 2 commits intodurch:masterfrom
michaelludwig1:master
Open

feat: add bulk delete (DeleteObjects) support#448
michaelludwig1 wants to merge 2 commits intodurch:masterfrom
michaelludwig1:master

Conversation

@michaelludwig1
Copy link

@michaelludwig1 michaelludwig1 commented Feb 24, 2026

Implement the S3 Multi-Object Delete API, allowing up to 1000 objects to be deleted in a single request. Requests with more than 1000 objects are automatically batched.

  • Add Command::DeleteObjects variant with XML body, Content-MD5 header
  • Add ObjectIdentifier, DeleteObjectsRequest/Result serde types
  • Add Bucket::delete_objects() with auto-chunking for >1000 keys
  • Add unit tests for serialization, escaping, and deserialization
  • Add integration tests for AWS and Minio
  • Update README with delete_objects documentation

Closes #420


This change is Reviewable

Summary by CodeRabbit

  • New Features

    • Bulk deletion of multiple S3 objects in a single operation with automatic batching and aggregated results.
    • Bounded parallelism for large transfers to improve reliability during streaming operations.
  • Documentation

    • README updated with Multi-Object Delete API reference and usage guidance.

Implement the S3 Multi-Object Delete API, allowing up to 1000 objects
to be deleted in a single request. Requests with more than 1000 objects
are automatically batched.

- Add Command::DeleteObjects variant with XML body, Content-MD5 header
- Add ObjectIdentifier, DeleteObjectsRequest/Result serde types
- Add Bucket::delete_objects() with auto-chunking for >1000 keys
- Add unit tests for serialization, escaping, and deserialization
- Add integration tests for AWS and Minio
- Update README with delete_objects documentation

Closes durch#420
@coderabbitai
Copy link

coderabbitai bot commented Feb 24, 2026

📝 Walkthrough

Walkthrough

Adds S3 bulk delete support: new serde types for DeleteObjects, a DeleteObjects command and request handling, a Bucket::delete_objects method that batches up to 1000 items, tests, and a README link. No other public signatures beyond the new types/variant.

Changes

Cohort / File(s) Summary
Serde Types & Data Models
s3/src/serde_types.rs
Add ObjectIdentifier, DeleteObjectsRequest (with Display, len, is_empty), DeletedObject, DeleteError, and DeleteObjectsResult plus serialization/deserialization tests.
Command & Request Infrastructure
s3/src/command.rs, s3/src/request/request_trait.rs
Add DeleteObjects variant to Command, set HTTP verb to POST, content-type/content-length/sha256 handling, include ?delete query and compute Content-MD5 for the request body.
Bucket API & Helpers
s3/src/bucket.rs
Add public delete_objects method that batches objects (chunks of 1000), aggregates per-batch results/errors, expose new serde types, and add calculate_max_concurrent_chunks helper; include integration test helpers and tests.
Documentation
README.md
Add entry to DELETE methods table linking to delete_objects in the Bucket API documentation.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Bucket
    participant Command
    participant S3

    Client->>Bucket: delete_objects(objects: Vec)
    activate Bucket
    Bucket->>Bucket: split into chunks (<=1000)
    loop for each chunk
      Bucket->>Command: Create DeleteObjects command with XML body
      activate Command
      Command->>Command: Serialize XML & compute hashes (sha256, content-md5)
      deactivate Command
      Command->>S3: POST /?delete with body
      activate S3
      S3-->>Command: DeleteObjectsResult (XML)
      deactivate S3
      Command-->>Bucket: parsed result
    end
    Bucket->>Client: aggregated DeleteObjectsResult
    deactivate Bucket
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 I stitched a dozen XML hops into a crate,
A thousand friends leap out the delete gate,
Chunked and bundled, a tidy little spree—
One POST, many farewell bounces, whee! 🥕✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main change: adding bulk delete (DeleteObjects) support, which is the primary objective of this pull request.
Linked Issues check ✅ Passed The PR fully implements the S3 DeleteObjects API requested in issue #420, including support for deleting up to 1,000 objects per request and automatic batching for larger requests.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing the DeleteObjects API feature: command variant, serde types for request/response, Bucket method, tests, and documentation.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@brownian-motion-v0
Copy link

Brownian Motion (Brass)

Recommendation: Proceed

Summary: PR effectively implements bulk delete for S3, aligning with stated goals.
Risk: Medium · Confidence: 80%

Highlights

  • Good test coverage
  • Clear commit history

Unknowns

  • No performance benchmarks provided

Next actions

  • Keep: thorough unit and integration tests
  • Drop: unnecessary complexity in handling edge cases
  • Add: documentation for new API usage

Reflection questions

  • What core assumption underpins this PR's approach?
  • How does this change align with the project's longer-term goals?
  • Could there be a simpler way to achieve the primary objective here?

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
s3/src/bucket.rs (1)

1600-1628: ⚠️ Potential issue | 🟡 Minor

Doc comment range doesn’t match the clamp logic.

The comment says “between 2 and 10” but the code clamps to 100. Either adjust the clamp or update the doc to match.

📝 Suggested doc alignment
-    /// Returns a value between 2 and 10, defaulting to 3 if memory detection fails.
+    /// Returns a value between 2 and 100, defaulting to 3 if memory detection fails.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@s3/src/bucket.rs` around lines 1600 - 1628, The doc comment for
calculate_max_concurrent_chunks() claims the returned value is "between 2 and
10" but the implementation clamps to 100; update either the comment or the clamp
to match intent. Locate the function calculate_max_concurrent_chunks (which uses
CHUNK_SIZE, safety_factor, memory_per_chunk, and calculated_chunks) and either
change the clamp call from .clamp(2, 100) to .clamp(2, 10) if the intended max
is 10, or modify the doc comment to state "between 2 and 100" (or a more
accurate range) and adjust the explanatory comments (safety_factor and
CHUNK_SIZE rationale) accordingly so docs and code are consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@s3/src/bucket.rs`:
- Around line 2120-2191: The delete_objects method is sending raw
ObjectIdentifier keys (including leading '/') in the DeleteObjectsRequest,
causing mismatches with other APIs that strip leading slashes; fix by
normalizing keys before building the XML: in delete_objects (and when
constructing the DeleteObjectsRequest.objects), map each ObjectIdentifier to one
with leading '/' characters removed (or replace its key string with stripped
version) so the DeleteObjectsRequest contains normalized keys consistent with
put_object and other bucket APIs.

In `@s3/src/serde_types.rs`:
- Around line 407-501: The DeleteObjectsRequest serialization and public docs
need fixes: in the fmt impl for DeleteObjectsRequest update the root element to
include xmlns="http://s3.amazonaws.com/doc/2006-03-01/" (i.e. write "<Delete
xmlns=\"...\">"), XML-escape the VersionId the same way you escape Key (use
quick_xml::escape::escape on obj.version_id when writing VersionId), change
is_empty() to return self.objects.is_empty() (leave len() if you want serialized
length), and add documentation examples for the public types ObjectIdentifier,
DeleteObjectsRequest, DeletedObject, DeleteError, and DeleteObjectsResult
showing typical construction/serialization and deserialization usage; reference
the functions/impls: ObjectIdentifier::new/with_version, impl fmt::Display for
DeleteObjectsRequest, DeleteObjectsRequest::is_empty, and the
DeletedObject/DeleteError/DeleteObjectsResult structs when adding the examples.

---

Outside diff comments:
In `@s3/src/bucket.rs`:
- Around line 1600-1628: The doc comment for calculate_max_concurrent_chunks()
claims the returned value is "between 2 and 10" but the implementation clamps to
100; update either the comment or the clamp to match intent. Locate the function
calculate_max_concurrent_chunks (which uses CHUNK_SIZE, safety_factor,
memory_per_chunk, and calculated_chunks) and either change the clamp call from
.clamp(2, 100) to .clamp(2, 10) if the intended max is 10, or modify the doc
comment to state "between 2 and 100" (or a more accurate range) and adjust the
explanatory comments (safety_factor and CHUNK_SIZE rationale) accordingly so
docs and code are consistent.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3a9314f and 1e0d0e1.

📒 Files selected for processing (5)
  • README.md
  • s3/src/bucket.rs
  • s3/src/command.rs
  • s3/src/request/request_trait.rs
  • s3/src/serde_types.rs

Comment on lines +407 to +501
/// Represents a single object identifier for the bulk delete request.
#[derive(Debug, Clone)]
pub struct ObjectIdentifier {
/// The key of the object to delete.
pub key: String,
/// The version ID of the object to delete (optional).
pub version_id: Option<String>,
}

impl ObjectIdentifier {
pub fn new(key: impl Into<String>) -> Self {
Self {
key: key.into(),
version_id: None,
}
}

pub fn with_version(key: impl Into<String>, version_id: impl Into<String>) -> Self {
Self {
key: key.into(),
version_id: Some(version_id.into()),
}
}
}

/// Request body for the DeleteObjects (bulk delete) API.
#[derive(Debug, Clone)]
pub struct DeleteObjectsRequest {
pub objects: Vec<ObjectIdentifier>,
/// If true, the response only includes errors (Quiet mode).
pub quiet: bool,
}

impl fmt::Display for DeleteObjectsRequest {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "<Delete>").expect("Can't fail");
if self.quiet {
write!(f, "<Quiet>true</Quiet>").expect("Can't fail");
}
for obj in &self.objects {
let escaped_key = quick_xml::escape::escape(&obj.key);
write!(f, "<Object><Key>{}</Key>", escaped_key).expect("Can't fail");
if let Some(ref vid) = obj.version_id {
write!(f, "<VersionId>{}</VersionId>", vid).expect("Can't fail");
}
write!(f, "</Object>").expect("Can't fail");
}
write!(f, "</Delete>")
}
}

impl DeleteObjectsRequest {
pub fn len(&self) -> usize {
self.to_string().len()
}

pub fn is_empty(&self) -> bool {
self.to_string().len() == 0
}
}

/// A single deleted object in the DeleteObjects response.
#[derive(Deserialize, Debug, Clone)]
pub struct DeletedObject {
#[serde(rename = "Key")]
pub key: String,
#[serde(rename = "VersionId")]
pub version_id: Option<String>,
#[serde(rename = "DeleteMarker")]
pub delete_marker: Option<bool>,
#[serde(rename = "DeleteMarkerVersionId")]
pub delete_marker_version_id: Option<String>,
}

/// A single error entry in the DeleteObjects response.
#[derive(Deserialize, Debug, Clone)]
pub struct DeleteError {
#[serde(rename = "Key")]
pub key: String,
#[serde(rename = "Code")]
pub code: String,
#[serde(rename = "Message")]
pub message: String,
#[serde(rename = "VersionId")]
pub version_id: Option<String>,
}

/// The parsed result of a DeleteObjects (bulk delete) response.
#[derive(Deserialize, Debug, Clone)]
pub struct DeleteObjectsResult {
#[serde(rename = "Deleted", default)]
pub deleted: Vec<DeletedObject>,
#[serde(rename = "Error", default)]
pub errors: Vec<DeleteError>,
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's check the actual code in the file to confirm the state
cd s3
head -n 510 src/serde_types.rs | tail -n 110

Repository: durch/rust-s3

Length of output: 3239


🌐 Web query:

AWS S3 DeleteObjects XML request format namespace xmlns requirement

💡 Result:

For the S3 DeleteObjects (Multi-Object Delete) REST request body, AWS expects the root element to be in the S3 XML namespace, i.e. include the default namespace on <Delete>:

<?xml version="1.0" encoding="UTF-8"?>
<Delete xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
  <Object><Key>key1</Key></Object>
  <Object><Key>key2</Key></Object>
  <Quiet>false</Quiet>
</Delete>

That xmlns="http://s3.amazonaws.com/doc/2006-03-01/" is the documented request format for DeleteObjects. [1]

Sources
[1] AWS S3 API Reference – DeleteObjects (request syntax / XML namespace) (docs.aws.amazon.com)

Citations:


🏁 Script executed:

# Check if there are any tests for DeleteObjectsRequest serialization
rg -A 10 "DeleteObjectsRequest" src/serde_types.rs | head -n 50

Repository: durch/rust-s3

Length of output: 115


🏁 Script executed:

# Check if documentation examples exist elsewhere for similar structs
rg -B 3 "/// Example" src/serde_types.rs | head -n 30

Repository: durch/rust-s3

Length of output: 115


Tighten DeleteObjectsRequest serialization and add documentation examples for public APIs.

Three functional issues and one documentation gap:

  • XML escaping inconsistency: VersionId should be XML-escaped like Key to prevent malformed XML with special characters.
  • is_empty() logic error: Currently checks serialized XML length == 0, which is always false (minimum XML is <Delete></Delete>). Should be self.objects.is_empty().
  • Missing XML namespace: AWS S3 DeleteObjects requires xmlns="http://s3.amazonaws.com/doc/2006-03-01/" on the <Delete> root element; currently absent.
  • Missing documentation examples: Per coding guidelines, all public APIs must include documentation examples. Add examples for ObjectIdentifier, DeleteObjectsRequest, DeletedObject, DeleteError, and DeleteObjectsResult.
🔧 Suggested fixes
             if let Some(ref vid) = obj.version_id {
-                write!(f, "<VersionId>{}</VersionId>", vid).expect("Can't fail");
+                let escaped_vid = quick_xml::escape::escape(vid);
+                write!(f, "<VersionId>{}</VersionId>", escaped_vid).expect("Can't fail");
             }
@@
     pub fn is_empty(&self) -> bool {
-        self.to_string().len() == 0
+        self.objects.is_empty()
     }
@@
     impl fmt::Display for DeleteObjectsRequest {
         fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-            write!(f, "<Delete>").expect("Can't fail");
+            write!(f, r#"<Delete xmlns="http://s3.amazonaws.com/doc/2006-03-01/">"#).expect("Can't fail");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@s3/src/serde_types.rs` around lines 407 - 501, The DeleteObjectsRequest
serialization and public docs need fixes: in the fmt impl for
DeleteObjectsRequest update the root element to include
xmlns="http://s3.amazonaws.com/doc/2006-03-01/" (i.e. write "<Delete
xmlns=\"...\">"), XML-escape the VersionId the same way you escape Key (use
quick_xml::escape::escape on obj.version_id when writing VersionId), change
is_empty() to return self.objects.is_empty() (leave len() if you want serialized
length), and add documentation examples for the public types ObjectIdentifier,
DeleteObjectsRequest, DeletedObject, DeleteError, and DeleteObjectsResult
showing typical construction/serialization and deserialization usage; reference
the functions/impls: ObjectIdentifier::new/with_version, impl fmt::Display for
DeleteObjectsRequest, DeleteObjectsRequest::is_empty, and the
DeletedObject/DeleteError/DeleteObjectsResult structs when adding the examples.

- Add xmlns namespace to Delete XML element
- XML-escape VersionId values (same as Key)
- Fix is_empty() to check objects.is_empty() instead of serialized length
- Strip leading '/' from object keys for consistency with other Bucket methods
- Add doc comments and examples to all public types
@brownian-motion-v0
Copy link

Brownian Motion (Brass)

Recommendation: Refactor

Summary: PR effectively implements bulk delete for S3, but risks complexity.
Risk: Medium · Confidence: 80%

Highlights

  • Good test coverage
  • Clear commit history

Unknowns

  • No performance benchmarks provided
  • Integration test stability is uncertain

Next actions

  • Keep: Bulk delete functionality and tests
  • Drop: Unused imports and excessive complexity in handling object keys
  • Add: Documentation clarifying the use of the new API

Reflection questions

  • What core assumption underpins this PR's approach?
  • How does this change align with the project's longer-term goals?
  • Could there be a simpler way to achieve the primary objective here?

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
s3/src/serde_types.rs (1)

407-506: ⚠️ Potential issue | 🟡 Minor

Add rustdoc examples for the new public delete types.
ObjectIdentifier, DeleteObjectsRequest, DeletedObject, DeleteError, and DeleteObjectsResult are public but lack documentation examples.

As per coding guidelines: “Provide documentation examples for all public APIs.”

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@s3/src/serde_types.rs` around lines 407 - 506, Public types ObjectIdentifier,
DeleteObjectsRequest, DeletedObject, DeleteError, and DeleteObjectsResult lack
rustdoc examples; add brief /// doc examples for each struct showing typical
usage: for ObjectIdentifier show constructing with new() and with_version(); for
DeleteObjectsRequest show building a request, calling to_string() (or
len/is_empty) to demonstrate the XML serialization; for
DeletedObject/DeleteError show example deserialization from representative XML
(or constructing instances) and for DeleteObjectsResult show how deleted/errors
vectors are accessed. Place these examples directly above the corresponding
struct definitions (ObjectIdentifier, DeleteObjectsRequest, DeletedObject,
DeleteError, DeleteObjectsResult) using standard rustdoc code blocks so they
compile and run in doc tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@s3/src/serde_types.rs`:
- Around line 407-506: Public types ObjectIdentifier, DeleteObjectsRequest,
DeletedObject, DeleteError, and DeleteObjectsResult lack rustdoc examples; add
brief /// doc examples for each struct showing typical usage: for
ObjectIdentifier show constructing with new() and with_version(); for
DeleteObjectsRequest show building a request, calling to_string() (or
len/is_empty) to demonstrate the XML serialization; for
DeletedObject/DeleteError show example deserialization from representative XML
(or constructing instances) and for DeleteObjectsResult show how deleted/errors
vectors are accessed. Place these examples directly above the corresponding
struct definitions (ObjectIdentifier, DeleteObjectsRequest, DeletedObject,
DeleteError, DeleteObjectsResult) using standard rustdoc code blocks so they
compile and run in doc tests.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1e0d0e1 and 4ab2c2b.

📒 Files selected for processing (2)
  • s3/src/bucket.rs
  • s3/src/serde_types.rs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support DeleteObjects API

1 participant