1use std::iter;
2use std::time::Duration;
3
4use instant_xml::{FromXml, ToXml};
5use jiff::Timestamp;
6use md5::{Digest as _, Md5};
7use url::Url;
8
9use crate::actions::{Method, S3_XML_NS, S3Action};
10use crate::signing::sign;
11use crate::sorting_iter::SortingIterator;
12use crate::{Bucket, Credentials, Map};
13
14#[derive(Debug, Clone)]
20pub struct DeleteObjects<'a, I> {
21 bucket: &'a Bucket,
22 credentials: Option<&'a Credentials>,
23 objects: I,
24 quiet: bool,
25
26 query: Map<'a>,
27 headers: Map<'a>,
28}
29
30impl<'a, I> DeleteObjects<'a, I> {
31 #[inline]
32 pub const fn new(bucket: &'a Bucket, credentials: Option<&'a Credentials>, objects: I) -> Self {
33 Self {
34 bucket,
35 credentials,
36 objects,
37 quiet: false,
38 query: Map::new(),
39 headers: Map::new(),
40 }
41 }
42
43 pub const fn quiet(&self) -> bool {
44 self.quiet
45 }
46
47 pub const fn set_quiet(&mut self, quiet: bool) {
48 self.quiet = quiet;
49 }
50}
51
52#[derive(Debug, Clone, Default)]
53pub struct ObjectIdentifier {
54 pub key: String,
55 pub version_id: Option<String>,
56}
57
58impl ObjectIdentifier {
59 #[must_use]
60 pub fn new(key: String) -> Self {
61 Self {
62 key,
63 ..Default::default()
64 }
65 }
66}
67
68#[derive(Debug, Clone, FromXml)]
69#[xml(rename = "DeleteResult", ns(S3_XML_NS))]
70pub struct DeleteObjectsResponse {
71 pub deleted: Vec<DeletedObject>,
72 #[xml(rename = "Error")]
73 pub errors: Vec<ErrorObject>,
74}
75
76#[derive(Debug, Clone, FromXml)]
77#[xml(rename = "Deleted", ns(S3_XML_NS))]
78pub struct DeletedObject {
79 #[xml(rename = "Key")]
80 pub key: String,
81 #[xml(rename = "VersionId")]
82 pub version_id: Option<String>,
83 #[xml(rename = "DeleteMarker")]
84 pub delete_marker: Option<bool>,
85 #[xml(rename = "DeleteMarkerVersionId")]
86 pub delete_marker_version_id: Option<String>,
87}
88
89#[derive(Debug, Clone, FromXml)]
90#[xml(rename = "Error", ns(S3_XML_NS))]
91pub struct ErrorObject {
92 #[xml(rename = "Key")]
93 pub key: String,
94 #[xml(rename = "VersionId")]
95 pub version_id: Option<String>,
96 #[xml(rename = "Code")]
97 pub code: String,
98 #[xml(rename = "Message")]
99 pub message: String,
100}
101
102impl DeleteObjectsResponse {
103 pub fn parse(s: &str) -> Result<Self, instant_xml::Error> {
109 instant_xml::from_str(s)
110 }
111}
112
113impl<'a, I> DeleteObjects<'a, I>
114where
115 I: Iterator<Item = &'a ObjectIdentifier>,
116{
117 pub fn body_with_md5(self) -> (String, String) {
123 #[derive(ToXml)]
124 #[xml(rename = "Delete")]
125 struct DeleteBody<'a> {
126 objects: Vec<Object<'a>>,
127 #[xml(rename = "Quiet")]
128 quiet: Option<bool>,
129 }
130 #[derive(ToXml)]
131 #[xml(rename = "Object")]
132 struct Object<'a> {
133 #[xml(rename = "Key")]
134 key: &'a str,
135 #[xml(rename = "VersionId")]
136 version_id: Option<&'a str>,
137 }
138
139 let objects: Vec<Object<'a>> = self
140 .objects
141 .map(|o| Object {
142 key: o.key.as_str(),
143 version_id: o.version_id.as_deref(),
144 })
145 .collect();
146
147 let req = DeleteBody {
148 objects,
149 quiet: self.quiet.then_some(true),
150 };
151
152 let body = instant_xml::to_string(&req).unwrap();
153
154 let content_md5 = crate::base64::encode(Md5::digest(body.as_bytes()));
155 (body, content_md5)
156 }
157}
158
159impl<'a, I> S3Action<'a> for DeleteObjects<'a, I>
160where
161 I: Iterator<Item = &'a ObjectIdentifier>,
162{
163 const METHOD: Method = Method::Post;
164
165 fn query_mut(&mut self) -> &mut Map<'a> {
166 &mut self.query
167 }
168
169 fn headers_mut(&mut self) -> &mut Map<'a> {
170 &mut self.headers
171 }
172
173 fn sign_with_time(&self, expires_in: Duration, time: &Timestamp) -> Url {
174 let url = self.bucket.base_url().clone();
175 let query = SortingIterator::new(iter::once(("delete", "1")), self.query.iter());
176
177 match self.credentials {
178 Some(credentials) => sign(
179 time,
180 Self::METHOD,
181 url,
182 credentials.key(),
183 credentials.secret(),
184 credentials.token(),
185 self.bucket.region(),
186 expires_in.as_secs(),
187 query,
188 self.headers.iter(),
189 ),
190 None => crate::signing::util::add_query_params(url, query),
191 }
192 }
193}
194
195#[cfg(test)]
196mod tests {
197 use pretty_assertions::assert_eq;
198
199 use crate::{Bucket, Credentials, UrlStyle};
200
201 use super::*;
202
203 #[test]
204 fn aws_example() {
205 let date = Timestamp::from_second(1369353600).unwrap();
207 let expires_in = Duration::from_secs(86400);
208
209 let endpoint = "https://s3.amazonaws.com".parse().unwrap();
210 let bucket = Bucket::new(
211 endpoint,
212 UrlStyle::VirtualHost,
213 "examplebucket",
214 "us-east-1",
215 )
216 .unwrap();
217 let credentials = Credentials::new(
218 "AKIAIOSFODNN7EXAMPLE",
219 "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
220 );
221
222 let objects = [
223 ObjectIdentifier {
224 key: "123".to_owned(),
225 ..Default::default()
226 },
227 ObjectIdentifier {
228 key: "456".to_owned(),
229 version_id: Some("ver1234".to_owned()),
230 },
231 ];
232 let action = DeleteObjects::new(&bucket, Some(&credentials), objects.iter());
233
234 let url = action.sign_with_time(expires_in, &date);
235 let expected = "https://examplebucket.s3.amazonaws.com/?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20130524%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20130524T000000Z&X-Amz-Expires=86400&X-Amz-SignedHeaders=host&delete=1&X-Amz-Signature=0e6170ba8cb7873da76b7fb63638658607f484265935099b3d8cea5195af843c";
236
237 assert_eq!(expected, url.as_str());
238 }
239
240 #[test]
241 fn anonymous_custom_query() {
242 let expires_in = Duration::from_secs(86400);
243
244 let endpoint = "https://s3.amazonaws.com".parse().unwrap();
245 let bucket = Bucket::new(
246 endpoint,
247 UrlStyle::VirtualHost,
248 "examplebucket",
249 "us-east-1",
250 )
251 .unwrap();
252
253 let objects = [
254 ObjectIdentifier {
255 key: "123".to_owned(),
256 ..Default::default()
257 },
258 ObjectIdentifier {
259 key: "456".to_owned(),
260 version_id: Some("ver1234".to_owned()),
261 },
262 ];
263 let action = DeleteObjects::new(&bucket, None, objects.iter());
264 let url = action.sign(expires_in);
265 let expected = "https://examplebucket.s3.amazonaws.com/?delete=1";
266
267 assert_eq!(expected, url.as_str());
268 }
269
270 #[test]
271 fn parse_response_success() {
272 let input = r#"<?xml version="1.0" encoding="UTF-8"?>
273 <DeleteResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
274 <Deleted>
275 <Key>duck.jpg</Key>
276 <VersionId>ver1234</VersionId>
277 <DeleteMarker>true</DeleteMarker>
278 <DeleteMarkerVersionId>del1234</DeleteMarkerVersionId>
279 </Deleted>
280 <Deleted>
281 <Key>duck2.jpg</Key>
282 </Deleted>
283 </DeleteResult>
284 "#;
285
286 let parsed = DeleteObjectsResponse::parse(input).unwrap();
287 assert_eq!(parsed.deleted.len(), 2);
288 assert_eq!(parsed.errors.len(), 0);
289
290 let deleted = &parsed.deleted[0];
291 assert_eq!(deleted.key, "duck.jpg");
292 assert_eq!(deleted.version_id, Some("ver1234".to_string()));
293 assert_eq!(deleted.delete_marker, Some(true));
294 assert_eq!(
295 deleted.delete_marker_version_id,
296 Some("del1234".to_string())
297 );
298
299 let deleted = &parsed.deleted[1];
300 assert_eq!(deleted.key, "duck2.jpg");
301 assert!(deleted.version_id.is_none());
302 assert!(deleted.delete_marker.is_none());
303 assert!(deleted.delete_marker_version_id.is_none());
304 }
305
306 #[test]
307 fn parse_response_errors() {
308 let input = r#"<?xml version="1.0" encoding="UTF-8"?>
309 <DeleteResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
310 <Error>
311 <Key>idk.txt</Key>
312 <Code>ErrorCode</Code>
313 <Message>Error message</Message>
314 </Error>
315 <Error>
316 <Key>idk2.txt</Key>
317 <VersionId>ver2</VersionId>
318 <Code>ErrorCode2</Code>
319 <Message>Error message 2</Message>
320 </Error>
321 </DeleteResult>
322 "#;
323
324 let parsed = DeleteObjectsResponse::parse(input).unwrap();
325 assert_eq!(parsed.deleted.len(), 0);
326 assert_eq!(parsed.errors.len(), 2);
327
328 let error = &parsed.errors[0];
329 assert_eq!(error.key, "idk.txt");
330 assert!(error.version_id.is_none());
331 assert_eq!(error.code, "ErrorCode");
332 assert_eq!(error.message, "Error message");
333
334 let error = &parsed.errors[1];
335 assert_eq!(error.key, "idk2.txt");
336 assert_eq!(error.version_id, Some("ver2".to_string()));
337 assert_eq!(error.code, "ErrorCode2");
338 assert_eq!(error.message, "Error message 2");
339 }
340}