post3_server/s3/
responses.rs1use post3::models::{BucketInfo, ListMultipartUploadsResult, ListObjectsResult, ListPartsResult};
2use serde::Deserialize;
3
4pub fn list_buckets_xml(buckets: &[BucketInfo]) -> String {
5 let mut xml = String::from(
6 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
7 <ListAllMyBucketsResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
8 <Owner><ID>post3</ID><DisplayName>post3</DisplayName></Owner>\
9 <Buckets>",
10 );
11
12 for b in buckets {
13 xml.push_str("<Bucket><Name>");
14 xml.push_str(&xml_escape(&b.name));
15 xml.push_str("</Name><CreationDate>");
16 xml.push_str(&b.created_at.format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string());
17 xml.push_str("</CreationDate></Bucket>");
18 }
19
20 xml.push_str("</Buckets></ListAllMyBucketsResult>");
21 xml
22}
23
24pub fn list_objects_v2_xml(
25 bucket_name: &str,
26 result: &ListObjectsResult,
27 max_keys: i64,
28 continuation_token: Option<&str>,
29 start_after: Option<&str>,
30) -> String {
31 let mut xml = String::from(
32 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
33 <ListBucketResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">",
34 );
35
36 xml.push_str("<Name>");
37 xml.push_str(&xml_escape(bucket_name));
38 xml.push_str("</Name>");
39
40 xml.push_str("<Prefix>");
41 if let Some(ref pfx) = result.prefix {
42 xml.push_str(&xml_escape(pfx));
43 }
44 xml.push_str("</Prefix>");
45
46 if let Some(sa) = start_after {
47 xml.push_str("<StartAfter>");
48 xml.push_str(&xml_escape(sa));
49 xml.push_str("</StartAfter>");
50 }
51
52 xml.push_str("<KeyCount>");
53 xml.push_str(&result.key_count.to_string());
54 xml.push_str("</KeyCount>");
55
56 xml.push_str("<MaxKeys>");
57 xml.push_str(&max_keys.to_string());
58 xml.push_str("</MaxKeys>");
59
60 xml.push_str("<IsTruncated>");
61 xml.push_str(if result.is_truncated { "true" } else { "false" });
62 xml.push_str("</IsTruncated>");
63
64 if let Some(ref delim) = result.delimiter {
65 xml.push_str("<Delimiter>");
66 xml.push_str(&xml_escape(delim));
67 xml.push_str("</Delimiter>");
68 }
69
70 if let Some(token) = continuation_token {
71 xml.push_str("<ContinuationToken>");
72 xml.push_str(&xml_escape(token));
73 xml.push_str("</ContinuationToken>");
74 }
75
76 if let Some(ref token) = result.next_continuation_token {
77 xml.push_str("<NextContinuationToken>");
78 xml.push_str(&xml_escape(token));
79 xml.push_str("</NextContinuationToken>");
80 }
81
82 for obj in &result.objects {
83 xml.push_str("<Contents>");
84 xml.push_str("<Key>");
85 xml.push_str(&xml_escape(&obj.key));
86 xml.push_str("</Key>");
87 xml.push_str("<LastModified>");
88 xml.push_str(
89 &obj.last_modified
90 .format("%Y-%m-%dT%H:%M:%S%.3fZ")
91 .to_string(),
92 );
93 xml.push_str("</LastModified>");
94 xml.push_str("<ETag>");
95 xml.push_str(&xml_escape(&obj.etag));
96 xml.push_str("</ETag>");
97 xml.push_str("<Size>");
98 xml.push_str(&obj.size.to_string());
99 xml.push_str("</Size>");
100 xml.push_str("<StorageClass>STANDARD</StorageClass>");
101 xml.push_str("</Contents>");
102 }
103
104 for cp in &result.common_prefixes {
105 xml.push_str("<CommonPrefixes><Prefix>");
106 xml.push_str(&xml_escape(cp));
107 xml.push_str("</Prefix></CommonPrefixes>");
108 }
109
110 xml.push_str("</ListBucketResult>");
111 xml
112}
113
114pub fn list_objects_v1_xml(
115 bucket_name: &str,
116 result: &ListObjectsResult,
117 max_keys: i64,
118 marker: Option<&str>,
119) -> String {
120 let mut xml = String::from(
121 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
122 <ListBucketResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">",
123 );
124
125 xml.push_str("<Name>");
126 xml.push_str(&xml_escape(bucket_name));
127 xml.push_str("</Name>");
128
129 xml.push_str("<Prefix>");
130 if let Some(ref pfx) = result.prefix {
131 xml.push_str(&xml_escape(pfx));
132 }
133 xml.push_str("</Prefix>");
134
135 xml.push_str("<Marker>");
136 if let Some(m) = marker {
137 xml.push_str(&xml_escape(m));
138 }
139 xml.push_str("</Marker>");
140
141 xml.push_str("<MaxKeys>");
142 xml.push_str(&max_keys.to_string());
143 xml.push_str("</MaxKeys>");
144
145 xml.push_str("<IsTruncated>");
146 xml.push_str(if result.is_truncated { "true" } else { "false" });
147 xml.push_str("</IsTruncated>");
148
149 if let Some(ref token) = result.next_continuation_token {
150 xml.push_str("<NextMarker>");
151 xml.push_str(&xml_escape(token));
152 xml.push_str("</NextMarker>");
153 }
154
155 if let Some(ref delim) = result.delimiter {
156 xml.push_str("<Delimiter>");
157 xml.push_str(&xml_escape(delim));
158 xml.push_str("</Delimiter>");
159 }
160
161 for obj in &result.objects {
162 xml.push_str("<Contents>");
163 xml.push_str("<Key>");
164 xml.push_str(&xml_escape(&obj.key));
165 xml.push_str("</Key>");
166 xml.push_str("<LastModified>");
167 xml.push_str(
168 &obj.last_modified
169 .format("%Y-%m-%dT%H:%M:%S%.3fZ")
170 .to_string(),
171 );
172 xml.push_str("</LastModified>");
173 xml.push_str("<ETag>");
174 xml.push_str(&xml_escape(&obj.etag));
175 xml.push_str("</ETag>");
176 xml.push_str("<Size>");
177 xml.push_str(&obj.size.to_string());
178 xml.push_str("</Size>");
179 xml.push_str("<Owner><ID>post3</ID><DisplayName>post3</DisplayName></Owner>");
180 xml.push_str("<StorageClass>STANDARD</StorageClass>");
181 xml.push_str("</Contents>");
182 }
183
184 for cp in &result.common_prefixes {
185 xml.push_str("<CommonPrefixes><Prefix>");
186 xml.push_str(&xml_escape(cp));
187 xml.push_str("</Prefix></CommonPrefixes>");
188 }
189
190 xml.push_str("</ListBucketResult>");
191 xml
192}
193
194pub fn list_object_versions_xml(
195 bucket_name: &str,
196 result: &ListObjectsResult,
197 max_keys: i64,
198 key_marker: Option<&str>,
199) -> String {
200 let mut xml = String::from(
201 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
202 <ListVersionsResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">",
203 );
204
205 xml.push_str("<Name>");
206 xml.push_str(&xml_escape(bucket_name));
207 xml.push_str("</Name>");
208
209 xml.push_str("<Prefix>");
210 if let Some(ref pfx) = result.prefix {
211 xml.push_str(&xml_escape(pfx));
212 }
213 xml.push_str("</Prefix>");
214
215 xml.push_str("<KeyMarker>");
217 if let Some(km) = key_marker {
218 xml.push_str(&xml_escape(km));
219 }
220 xml.push_str("</KeyMarker>");
221 xml.push_str("<VersionIdMarker/>");
222
223 xml.push_str("<MaxKeys>");
224 xml.push_str(&max_keys.to_string());
225 xml.push_str("</MaxKeys>");
226
227 xml.push_str("<IsTruncated>");
228 xml.push_str(if result.is_truncated { "true" } else { "false" });
229 xml.push_str("</IsTruncated>");
230
231 for obj in &result.objects {
232 xml.push_str("<Version>");
233 xml.push_str("<Key>");
234 xml.push_str(&xml_escape(&obj.key));
235 xml.push_str("</Key>");
236 xml.push_str("<VersionId>null</VersionId>");
237 xml.push_str("<IsLatest>true</IsLatest>");
238 xml.push_str("<LastModified>");
239 xml.push_str(
240 &obj.last_modified
241 .format("%Y-%m-%dT%H:%M:%S%.3fZ")
242 .to_string(),
243 );
244 xml.push_str("</LastModified>");
245 xml.push_str("<ETag>");
246 xml.push_str(&xml_escape(&obj.etag));
247 xml.push_str("</ETag>");
248 xml.push_str("<Size>");
249 xml.push_str(&obj.size.to_string());
250 xml.push_str("</Size>");
251 xml.push_str("<StorageClass>STANDARD</StorageClass>");
252 xml.push_str("<Owner><ID>post3</ID><DisplayName>post3</DisplayName></Owner>");
253 xml.push_str("</Version>");
254 }
255
256 if result.is_truncated {
258 if let Some(last_obj) = result.objects.last() {
259 xml.push_str("<NextKeyMarker>");
260 xml.push_str(&xml_escape(&last_obj.key));
261 xml.push_str("</NextKeyMarker>");
262 xml.push_str("<NextVersionIdMarker>null</NextVersionIdMarker>");
263 }
264 }
265
266 xml.push_str("</ListVersionsResult>");
267 xml
268}
269
270pub fn get_bucket_location_xml() -> String {
271 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
272 <LocationConstraint xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\"/>"
273 .to_string()
274}
275
276#[derive(Debug, Deserialize)]
279#[serde(rename = "Delete")]
280struct DeleteObjectsRequest {
281 #[serde(rename = "Object")]
282 objects: Vec<DeleteObjectEntry>,
283 #[serde(rename = "Quiet", default)]
284 quiet: Option<bool>,
285}
286
287#[derive(Debug, Deserialize)]
288struct DeleteObjectEntry {
289 #[serde(rename = "Key")]
290 key: String,
291}
292
293pub fn parse_delete_objects_xml(body: &[u8]) -> Result<(Vec<String>, bool), String> {
294 let request: DeleteObjectsRequest =
295 quick_xml::de::from_reader(body).map_err(|e| format!("invalid XML: {e}"))?;
296 let quiet = request.quiet.unwrap_or(false);
297 let keys = request.objects.into_iter().map(|o| o.key).collect();
298 Ok((keys, quiet))
299}
300
301pub fn delete_objects_result_xml(deleted: &[String], errors: &[(String, String, String)]) -> String {
302 let mut xml = String::from(
303 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
304 <DeleteResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">",
305 );
306
307 for key in deleted {
308 xml.push_str("<Deleted><Key>");
309 xml.push_str(&xml_escape(key));
310 xml.push_str("</Key></Deleted>");
311 }
312
313 for (key, code, message) in errors {
314 xml.push_str("<Error><Key>");
315 xml.push_str(&xml_escape(key));
316 xml.push_str("</Key><Code>");
317 xml.push_str(&xml_escape(code));
318 xml.push_str("</Code><Message>");
319 xml.push_str(&xml_escape(message));
320 xml.push_str("</Message></Error>");
321 }
322
323 xml.push_str("</DeleteResult>");
324 xml
325}
326
327pub fn error_xml(code: &str, message: &str, resource: &str) -> String {
328 let request_id = uuid::Uuid::new_v4().to_string();
329 format!(
330 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
331 <Error>\
332 <Code>{code}</Code>\
333 <Message>{message}</Message>\
334 <Resource>{resource}</Resource>\
335 <RequestId>{request_id}</RequestId>\
336 </Error>",
337 code = xml_escape(code),
338 message = xml_escape(message),
339 resource = xml_escape(resource),
340 request_id = request_id,
341 )
342}
343
344pub fn initiate_multipart_upload_xml(bucket: &str, key: &str, upload_id: &str) -> String {
347 format!(
348 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
349 <InitiateMultipartUploadResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
350 <Bucket>{bucket}</Bucket>\
351 <Key>{key}</Key>\
352 <UploadId>{upload_id}</UploadId>\
353 </InitiateMultipartUploadResult>",
354 bucket = xml_escape(bucket),
355 key = xml_escape(key),
356 upload_id = xml_escape(upload_id),
357 )
358}
359
360pub fn complete_multipart_upload_xml(
361 location: &str,
362 bucket: &str,
363 key: &str,
364 etag: &str,
365) -> String {
366 format!(
367 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
368 <CompleteMultipartUploadResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
369 <Location>{location}</Location>\
370 <Bucket>{bucket}</Bucket>\
371 <Key>{key}</Key>\
372 <ETag>{etag}</ETag>\
373 </CompleteMultipartUploadResult>",
374 location = xml_escape(location),
375 bucket = xml_escape(bucket),
376 key = xml_escape(key),
377 etag = xml_escape(etag),
378 )
379}
380
381pub fn list_parts_xml(result: &ListPartsResult, max_parts: i32) -> String {
382 let mut xml = String::from(
383 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
384 <ListPartsResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">",
385 );
386
387 xml.push_str("<Bucket>");
388 xml.push_str(&xml_escape(&result.bucket));
389 xml.push_str("</Bucket>");
390
391 xml.push_str("<Key>");
392 xml.push_str(&xml_escape(&result.key));
393 xml.push_str("</Key>");
394
395 xml.push_str("<UploadId>");
396 xml.push_str(&xml_escape(&result.upload_id));
397 xml.push_str("</UploadId>");
398
399 xml.push_str("<MaxParts>");
400 xml.push_str(&max_parts.to_string());
401 xml.push_str("</MaxParts>");
402
403 xml.push_str("<IsTruncated>");
404 xml.push_str(if result.is_truncated { "true" } else { "false" });
405 xml.push_str("</IsTruncated>");
406
407 if let Some(marker) = result.next_part_number_marker {
408 xml.push_str("<NextPartNumberMarker>");
409 xml.push_str(&marker.to_string());
410 xml.push_str("</NextPartNumberMarker>");
411 }
412
413 for part in &result.parts {
414 xml.push_str("<Part>");
415 xml.push_str("<PartNumber>");
416 xml.push_str(&part.part_number.to_string());
417 xml.push_str("</PartNumber>");
418 xml.push_str("<LastModified>");
419 xml.push_str(
420 &part
421 .created_at
422 .format("%Y-%m-%dT%H:%M:%S%.3fZ")
423 .to_string(),
424 );
425 xml.push_str("</LastModified>");
426 xml.push_str("<ETag>");
427 xml.push_str(&xml_escape(&part.etag));
428 xml.push_str("</ETag>");
429 xml.push_str("<Size>");
430 xml.push_str(&part.size.to_string());
431 xml.push_str("</Size>");
432 xml.push_str("</Part>");
433 }
434
435 xml.push_str("</ListPartsResult>");
436 xml
437}
438
439pub fn list_multipart_uploads_xml(
440 result: &ListMultipartUploadsResult,
441 max_uploads: i32,
442) -> String {
443 let mut xml = String::from(
444 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
445 <ListMultipartUploadsResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">",
446 );
447
448 xml.push_str("<Bucket>");
449 xml.push_str(&xml_escape(&result.bucket));
450 xml.push_str("</Bucket>");
451
452 xml.push_str("<Prefix>");
453 if let Some(ref pfx) = result.prefix {
454 xml.push_str(&xml_escape(pfx));
455 }
456 xml.push_str("</Prefix>");
457
458 xml.push_str("<MaxUploads>");
459 xml.push_str(&max_uploads.to_string());
460 xml.push_str("</MaxUploads>");
461
462 xml.push_str("<IsTruncated>");
463 xml.push_str(if result.is_truncated {
464 "true"
465 } else {
466 "false"
467 });
468 xml.push_str("</IsTruncated>");
469
470 if let Some(ref marker) = result.next_key_marker {
471 xml.push_str("<NextKeyMarker>");
472 xml.push_str(&xml_escape(marker));
473 xml.push_str("</NextKeyMarker>");
474 }
475 if let Some(ref marker) = result.next_upload_id_marker {
476 xml.push_str("<NextUploadIdMarker>");
477 xml.push_str(&xml_escape(marker));
478 xml.push_str("</NextUploadIdMarker>");
479 }
480
481 for upload in &result.uploads {
482 xml.push_str("<Upload>");
483 xml.push_str("<Key>");
484 xml.push_str(&xml_escape(&upload.key));
485 xml.push_str("</Key>");
486 xml.push_str("<UploadId>");
487 xml.push_str(&xml_escape(&upload.upload_id));
488 xml.push_str("</UploadId>");
489 xml.push_str("<Initiated>");
490 xml.push_str(
491 &upload
492 .initiated
493 .format("%Y-%m-%dT%H:%M:%S%.3fZ")
494 .to_string(),
495 );
496 xml.push_str("</Initiated>");
497 xml.push_str("</Upload>");
498 }
499
500 xml.push_str("</ListMultipartUploadsResult>");
501 xml
502}
503
504#[derive(Debug, Deserialize)]
507#[serde(rename = "CompleteMultipartUpload")]
508struct CompleteMultipartUploadRequest {
509 #[serde(rename = "Part")]
510 parts: Vec<CompletePart>,
511}
512
513#[derive(Debug, Deserialize)]
514struct CompletePart {
515 #[serde(rename = "PartNumber")]
516 part_number: i32,
517 #[serde(rename = "ETag")]
518 etag: String,
519}
520
521pub fn parse_complete_multipart_xml(body: &[u8]) -> Result<Vec<(i32, String)>, String> {
522 let request: CompleteMultipartUploadRequest =
523 quick_xml::de::from_reader(body).map_err(|e| format!("invalid XML: {e}"))?;
524
525 Ok(request
526 .parts
527 .into_iter()
528 .map(|p| (p.part_number, p.etag))
529 .collect())
530}
531
532fn xml_escape(s: &str) -> String {
533 s.replace('&', "&")
534 .replace('<', "<")
535 .replace('>', ">")
536 .replace('"', """)
537 .replace('\'', "'")
538}