Skip to main content

post3_server/s3/
responses.rs

1use 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    // Echo back input markers
216    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    // Include NextKeyMarker/NextVersionIdMarker when truncated for pagination
257    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// --- DeleteObjects ---
277
278#[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
344// --- Multipart upload responses ---
345
346pub 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// --- XML request parsing for CompleteMultipartUpload ---
505
506#[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('&', "&amp;")
534        .replace('<', "&lt;")
535        .replace('>', "&gt;")
536        .replace('"', "&quot;")
537        .replace('\'', "&apos;")
538}