@@ -90,8 +90,9 @@ use crate::error::S3Error;
9090use crate :: post_policy:: PresignedPost ;
9191use crate :: serde_types:: {
9292 BucketLifecycleConfiguration , BucketLocationResult , CompleteMultipartUploadData ,
93- CorsConfiguration , GetObjectAttributesOutput , HeadObjectResult ,
94- InitiateMultipartUploadResponse , ListBucketResult , ListMultipartUploadsResult , Part ,
93+ CorsConfiguration , DeleteObjectsRequest , DeleteObjectsResult , GetObjectAttributesOutput ,
94+ HeadObjectResult , InitiateMultipartUploadResponse , ListBucketResult ,
95+ ListMultipartUploadsResult , ObjectIdentifier , Part ,
9596} ;
9697#[ allow( unused_imports) ]
9798use crate :: utils:: { PutStreamResponse , error_from_response_data} ;
@@ -2116,6 +2117,92 @@ impl Bucket {
21162117 request. response_data ( false ) . await
21172118 }
21182119
2120+ /// Delete multiple objects from S3 using the Multi-Object Delete API.
2121+ ///
2122+ /// If more than 1000 objects are provided, they are automatically batched
2123+ /// into multiple requests (S3 allows at most 1000 keys per request).
2124+ /// Results from all batches are combined into a single response.
2125+ ///
2126+ /// # Example:
2127+ ///
2128+ /// ```no_run
2129+ /// use s3::bucket::Bucket;
2130+ /// use s3::creds::Credentials;
2131+ /// use s3::serde_types::ObjectIdentifier;
2132+ /// use anyhow::Result;
2133+ ///
2134+ /// # #[tokio::main]
2135+ /// # async fn main() -> Result<()> {
2136+ ///
2137+ /// let bucket_name = "rust-s3-test";
2138+ /// let region = "us-east-1".parse()?;
2139+ /// let credentials = Credentials::default()?;
2140+ /// let bucket = Bucket::new(bucket_name, region, credentials)?;
2141+ ///
2142+ /// let objects = vec![
2143+ /// ObjectIdentifier::new("file1.txt"),
2144+ /// ObjectIdentifier::new("file2.txt"),
2145+ /// ObjectIdentifier::new("file3.txt"),
2146+ /// ];
2147+ ///
2148+ /// // Async variant with `tokio` or `async-std` features
2149+ /// let response = bucket.delete_objects(objects).await?;
2150+ ///
2151+ /// // `sync` feature will produce an identical method
2152+ /// #[cfg(feature = "sync")]
2153+ /// let response = bucket.delete_objects(objects)?;
2154+ ///
2155+ /// // Blocking variant, generated with `blocking` feature in combination
2156+ /// // with `tokio` or `async-std` features.
2157+ /// #[cfg(feature = "blocking")]
2158+ /// let response = bucket.delete_objects_blocking(objects)?;
2159+ /// #
2160+ /// # Ok(())
2161+ /// # }
2162+ /// ```
2163+ #[ maybe_async:: maybe_async]
2164+ pub async fn delete_objects < I : Into < Vec < ObjectIdentifier > > > (
2165+ & self ,
2166+ objects : I ,
2167+ ) -> Result < DeleteObjectsResult , S3Error > {
2168+ let objects = objects. into ( ) ;
2169+ let mut result = DeleteObjectsResult {
2170+ deleted : Vec :: new ( ) ,
2171+ errors : Vec :: new ( ) ,
2172+ } ;
2173+
2174+ // Strip leading '/' from keys to match library convention.
2175+ // Other methods (put_object, delete_object, etc.) strip the leading
2176+ // slash when building the URL; we do the same for the XML body.
2177+ let objects: Vec < ObjectIdentifier > = objects
2178+ . into_iter ( )
2179+ . map ( |mut obj| {
2180+ if let Some ( stripped) = obj. key . strip_prefix ( '/' ) {
2181+ obj. key = stripped. to_string ( ) ;
2182+ }
2183+ obj
2184+ } )
2185+ . collect ( ) ;
2186+
2187+ for chunk in objects. chunks ( 1000 ) {
2188+ let data = DeleteObjectsRequest {
2189+ objects : chunk. to_vec ( ) ,
2190+ quiet : false ,
2191+ } ;
2192+ let command = Command :: DeleteObjects { data } ;
2193+ let request = RequestImpl :: new ( self , "/" , command) . await ?;
2194+ let response_data = request. response_data ( false ) . await ?;
2195+ if response_data. status_code ( ) >= 300 {
2196+ return Err ( error_from_response_data ( response_data) ?) ;
2197+ }
2198+ let msg: DeleteObjectsResult = quick_xml:: de:: from_str ( response_data. as_str ( ) ?) ?;
2199+ result. deleted . extend ( msg. deleted ) ;
2200+ result. errors . extend ( msg. errors ) ;
2201+ }
2202+
2203+ Ok ( result)
2204+ }
2205+
21192206 /// Head object from S3.
21202207 ///
21212208 /// # Example:
@@ -3784,6 +3871,64 @@ mod test {
37843871 put_head_delete_object_with_headers ( * test_r2_bucket ( ) ) . await ;
37853872 }
37863873
3874+ #[ maybe_async:: maybe_async]
3875+ async fn put_delete_objects ( bucket : Bucket ) {
3876+ use crate :: serde_types:: ObjectIdentifier ;
3877+
3878+ let paths = [
3879+ "/+bulk_delete_1.file" ,
3880+ "/+bulk_delete_2.file" ,
3881+ "/+bulk_delete_3.file" ,
3882+ ] ;
3883+ let test: Vec < u8 > = object ( 128 ) ;
3884+
3885+ // Put test objects
3886+ for path in & paths {
3887+ let response_data = bucket. put_object ( * path, & test) . await . unwrap ( ) ;
3888+ assert_eq ! ( response_data. status_code( ) , 200 ) ;
3889+ }
3890+
3891+ // Bulk delete them
3892+ let objects: Vec < ObjectIdentifier > =
3893+ paths. iter ( ) . map ( |p| ObjectIdentifier :: new ( * p) ) . collect ( ) ;
3894+ let result = bucket. delete_objects ( objects) . await . unwrap ( ) ;
3895+
3896+ assert_eq ! ( result. deleted. len( ) , 3 ) ;
3897+ assert ! ( result. errors. is_empty( ) ) ;
3898+
3899+ // Verify they are gone
3900+ for path in & paths {
3901+ let exists = bucket. object_exists ( * path) . await . unwrap ( ) ;
3902+ assert ! ( !exists) ;
3903+ }
3904+ }
3905+
3906+ #[ ignore]
3907+ #[ maybe_async:: test(
3908+ feature = "sync" ,
3909+ async ( all( not( feature = "sync" ) , feature = "with-tokio" ) , tokio:: test) ,
3910+ async (
3911+ all( not( feature = "sync" ) , feature = "with-async-std" ) ,
3912+ async_std:: test
3913+ )
3914+ ) ]
3915+ async fn aws_test_delete_objects ( ) {
3916+ put_delete_objects ( * test_aws_bucket ( ) ) . await ;
3917+ }
3918+
3919+ #[ ignore]
3920+ #[ maybe_async:: test(
3921+ feature = "sync" ,
3922+ async ( all( not( feature = "sync" ) , feature = "with-tokio" ) , tokio:: test) ,
3923+ async (
3924+ all( not( feature = "sync" ) , feature = "with-async-std" ) ,
3925+ async_std:: test
3926+ )
3927+ ) ]
3928+ async fn minio_test_delete_objects ( ) {
3929+ put_delete_objects ( * test_minio_bucket ( ) ) . await ;
3930+ }
3931+
37873932 #[ maybe_async:: test(
37883933 feature = "sync" ,
37893934 async ( all( not( feature = "sync" ) , feature = "with-tokio" ) , tokio:: test) ,
0 commit comments