diff --git a/codegen/src/v1/ops.rs b/codegen/src/v1/ops.rs index 4883885b..ca26d532 100644 --- a/codegen/src/v1/ops.rs +++ b/codegen/src/v1/ops.rs @@ -607,6 +607,20 @@ fn codegen_op_http_de_multipart(op: &Operation, rust_types: &RustTypes) { "let bucket = http::unwrap_bucket(req);", "let key = http::parse_field_value(&m, \"key\")?.ok_or_else(|| invalid_request!(\"missing key\"))?;", "", + "// Extract success_action_status and success_action_redirect for POST object", + "if let Some(status_str) = m.find_field_value(\"success_action_status\") {", + " let status = status_str.parse::()", + " .map_err(|_| invalid_request!(\"invalid success_action_status\"))?;", + " // AWS only accepts 200, 201, or 204", + " if status != 200 && status != 201 && status != 204 {", + " return Err(invalid_request!(\"success_action_status must be 200, 201, or 204\"));", + " }", + " req.s3ext.success_action_status = Some(status);", + "}", + "if let Some(redirect) = m.find_field_value(\"success_action_redirect\") {", + " req.s3ext.success_action_redirect = Some(redirect.to_owned());", + "}", + "", "let vec_stream = req.s3ext.vec_stream.take().expect(\"missing vec stream\");", "", "let content_length = i64::try_from(vec_stream.exact_remaining_length())", @@ -707,7 +721,25 @@ fn codegen_op_http_call(op: &Operation) { let method = op.name.to_snake_case(); g!("let input = Self::deserialize_http(req)?;"); + + if op.name == "PutObject" { + g!(); + g!("// Extract success_action fields before building s3_req (which may move some values from req)"); + g!("let success_action_redirect = req.s3ext.success_action_redirect.take();"); + g!("let success_action_status = req.s3ext.success_action_status.take();"); + g!(); + } + g!("let mut s3_req = super::build_s3_request(input, req);"); + + if op.name == "PutObject" { + g!(); + g!("// Extract bucket and key for success_action response before s3_req is consumed"); + g!("let bucket_for_response = s3_req.input.bucket.clone();"); + g!("let key_for_response = s3_req.input.key.clone();"); + g!(); + } + g!("let s3 = ccx.s3;"); g!("if let Some(access) = ccx.access {{"); @@ -738,6 +770,20 @@ fn codegen_op_http_call(op: &Operation) { g!("resp.extensions.extend(s3_resp.extensions);"); + if op.name == "PutObject" { + g!(); + g!("// Handle POST object success_action_redirect and success_action_status"); + g!("if let Some(redirect_url) = success_action_redirect {{"); + g!( + " resp = super::post_object::handle_success_action_redirect(resp, &bucket_for_response, &key_for_response, &redirect_url)?;" + ); + g!("}} else if let Some(status) = success_action_status {{"); + g!( + " resp = super::post_object::handle_success_action_status(resp, &bucket_for_response, &key_for_response, status)?;" + ); + g!("}}"); + } + g!("Ok(resp)"); g!("}}"); diff --git a/crates/s3s/src/dto/mod.rs b/crates/s3s/src/dto/mod.rs index 43853662..5179a4b0 100644 --- a/crates/s3s/src/dto/mod.rs +++ b/crates/s3s/src/dto/mod.rs @@ -35,6 +35,9 @@ pub use self::event_stream::*; mod etag; pub use self::etag::*; +mod post_response; +pub use self::post_response::*; + pub type List = Vec; pub type Map = std::collections::HashMap; diff --git a/crates/s3s/src/dto/post_response.rs b/crates/s3s/src/dto/post_response.rs new file mode 100644 index 00000000..7ffca038 --- /dev/null +++ b/crates/s3s/src/dto/post_response.rs @@ -0,0 +1,32 @@ +//! `PostResponse` structure for S3 POST object uploads +//! +//! See: + +use crate::dto::ETag; +use crate::xml::{SerResult, Serialize, Serializer}; + +/// Response returned for POST object uploads when `success_action_status` is 200 or 201 +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PostResponse { + /// Location of the uploaded object + pub location: String, + /// Bucket name + pub bucket: String, + /// Object key + pub key: String, + /// `ETag` of the uploaded object + pub e_tag: ETag, +} + +impl Serialize for PostResponse { + fn serialize(&self, s: &mut Serializer) -> SerResult { + s.element("PostResponse", |s| { + s.content("Location", &self.location)?; + s.content("Bucket", &self.bucket)?; + s.content("Key", &self.key)?; + s.content("ETag", &self.e_tag)?; + Ok(()) + })?; + Ok(()) + } +} diff --git a/crates/s3s/src/http/request.rs b/crates/s3s/src/http/request.rs index 22669651..9d508ef2 100644 --- a/crates/s3s/src/http/request.rs +++ b/crates/s3s/src/http/request.rs @@ -35,6 +35,10 @@ pub(crate) struct S3Extensions { pub region: Option, pub service: Option, pub trailing_headers: Option, + + // POST object success action fields + pub success_action_status: Option, + pub success_action_redirect: Option, } impl From for Request { diff --git a/crates/s3s/src/ops/generated.rs b/crates/s3s/src/ops/generated.rs index 8334af1b..06fb44c1 100644 --- a/crates/s3s/src/ops/generated.rs +++ b/crates/s3s/src/ops/generated.rs @@ -5443,6 +5443,21 @@ impl PutObject { let bucket = http::unwrap_bucket(req); let key = http::parse_field_value(&m, "key")?.ok_or_else(|| invalid_request!("missing key"))?; + // Extract success_action_status and success_action_redirect for POST object + if let Some(status_str) = m.find_field_value("success_action_status") { + let status = status_str + .parse::() + .map_err(|_| invalid_request!("invalid success_action_status"))?; + // AWS only accepts 200, 201, or 204 + if status != 200 && status != 201 && status != 204 { + return Err(invalid_request!("success_action_status must be 200, 201, or 204")); + } + req.s3ext.success_action_status = Some(status); + } + if let Some(redirect) = m.find_field_value("success_action_redirect") { + req.s3ext.success_action_redirect = Some(redirect.to_owned()); + } + let vec_stream = req.s3ext.vec_stream.take().expect("missing vec stream"); let content_length = i64::try_from(vec_stream.exact_remaining_length()) @@ -5619,7 +5634,17 @@ impl super::Operation for PutObject { async fn call(&self, ccx: &CallContext<'_>, req: &mut http::Request) -> S3Result { let input = Self::deserialize_http(req)?; + + // Extract success_action fields before building s3_req (which may move some values from req) + let success_action_redirect = req.s3ext.success_action_redirect.take(); + let success_action_status = req.s3ext.success_action_status.take(); + let mut s3_req = super::build_s3_request(input, req); + + // Extract bucket and key for success_action response before s3_req is consumed + let bucket_for_response = s3_req.input.bucket.clone(); + let key_for_response = s3_req.input.key.clone(); + let s3 = ccx.s3; if let Some(access) = ccx.access { access.put_object(&mut s3_req).await?; @@ -5632,6 +5657,14 @@ impl super::Operation for PutObject { let mut resp = Self::serialize_http(s3_resp.output)?; resp.headers.extend(s3_resp.headers); resp.extensions.extend(s3_resp.extensions); + + // Handle POST object success_action_redirect and success_action_status + if let Some(redirect_url) = success_action_redirect { + resp = + super::post_object::handle_success_action_redirect(resp, &bucket_for_response, &key_for_response, &redirect_url)?; + } else if let Some(status) = success_action_status { + resp = super::post_object::handle_success_action_status(resp, &bucket_for_response, &key_for_response, status)?; + } Ok(resp) } } diff --git a/crates/s3s/src/ops/generated_minio.rs b/crates/s3s/src/ops/generated_minio.rs index ad736995..ff1318d5 100644 --- a/crates/s3s/src/ops/generated_minio.rs +++ b/crates/s3s/src/ops/generated_minio.rs @@ -5455,6 +5455,21 @@ impl PutObject { let bucket = http::unwrap_bucket(req); let key = http::parse_field_value(&m, "key")?.ok_or_else(|| invalid_request!("missing key"))?; + // Extract success_action_status and success_action_redirect for POST object + if let Some(status_str) = m.find_field_value("success_action_status") { + let status = status_str + .parse::() + .map_err(|_| invalid_request!("invalid success_action_status"))?; + // AWS only accepts 200, 201, or 204 + if status != 200 && status != 201 && status != 204 { + return Err(invalid_request!("success_action_status must be 200, 201, or 204")); + } + req.s3ext.success_action_status = Some(status); + } + if let Some(redirect) = m.find_field_value("success_action_redirect") { + req.s3ext.success_action_redirect = Some(redirect.to_owned()); + } + let vec_stream = req.s3ext.vec_stream.take().expect("missing vec stream"); let content_length = i64::try_from(vec_stream.exact_remaining_length()) @@ -5634,7 +5649,17 @@ impl super::Operation for PutObject { async fn call(&self, ccx: &CallContext<'_>, req: &mut http::Request) -> S3Result { let input = Self::deserialize_http(req)?; + + // Extract success_action fields before building s3_req (which may move some values from req) + let success_action_redirect = req.s3ext.success_action_redirect.take(); + let success_action_status = req.s3ext.success_action_status.take(); + let mut s3_req = super::build_s3_request(input, req); + + // Extract bucket and key for success_action response before s3_req is consumed + let bucket_for_response = s3_req.input.bucket.clone(); + let key_for_response = s3_req.input.key.clone(); + let s3 = ccx.s3; if let Some(access) = ccx.access { access.put_object(&mut s3_req).await?; @@ -5647,6 +5672,14 @@ impl super::Operation for PutObject { let mut resp = Self::serialize_http(s3_resp.output)?; resp.headers.extend(s3_resp.headers); resp.extensions.extend(s3_resp.extensions); + + // Handle POST object success_action_redirect and success_action_status + if let Some(redirect_url) = success_action_redirect { + resp = + super::post_object::handle_success_action_redirect(resp, &bucket_for_response, &key_for_response, &redirect_url)?; + } else if let Some(status) = success_action_status { + resp = super::post_object::handle_success_action_status(resp, &bucket_for_response, &key_for_response, status)?; + } Ok(resp) } } diff --git a/crates/s3s/src/ops/mod.rs b/crates/s3s/src/ops/mod.rs index c0c31c64..5f46b056 100644 --- a/crates/s3s/src/ops/mod.rs +++ b/crates/s3s/src/ops/mod.rs @@ -14,6 +14,7 @@ use self::signature::SignatureContext; mod get_object; mod multipart; +mod post_object; #[cfg(test)] mod tests; diff --git a/crates/s3s/src/ops/post_object.rs b/crates/s3s/src/ops/post_object.rs new file mode 100644 index 00000000..17c89a11 --- /dev/null +++ b/crates/s3s/src/ops/post_object.rs @@ -0,0 +1,86 @@ +use crate::S3Result; +use crate::dto::{ETag, PostResponse}; +use crate::http::Response; + +use hyper::StatusCode; +use hyper::header::CONTENT_TYPE; + +/// Handle `success_action_redirect` for POST object +pub fn handle_success_action_redirect(mut resp: Response, bucket: &str, key: &str, redirect_url: &str) -> S3Result { + // Extract ETag from response headers + let etag_header = resp + .headers + .get(hyper::header::ETAG) + .and_then(|v| v.to_str().ok()) + .unwrap_or(""); + + // Build redirect URL with query parameters + let redirect_with_params = if redirect_url.contains('?') { + format!( + "{}&bucket={}&key={}&etag={}", + redirect_url, + urlencoding::encode(bucket), + urlencoding::encode(key), + urlencoding::encode(etag_header) + ) + } else { + format!( + "{}?bucket={}&key={}&etag={}", + redirect_url, + urlencoding::encode(bucket), + urlencoding::encode(key), + urlencoding::encode(etag_header) + ) + }; + + resp.status = StatusCode::SEE_OTHER; // 303 + resp.headers.insert( + hyper::header::LOCATION, + redirect_with_params + .parse() + .map_err(|e| invalid_request!(e, "invalid redirect URL"))?, + ); + resp.body = crate::http::Body::empty(); + + Ok(resp) +} + +/// Handle `success_action_status` for POST object +pub fn handle_success_action_status(mut resp: Response, bucket: &str, key: &str, status: u16) -> S3Result { + let status_code = match status { + 200 => StatusCode::OK, + 201 => StatusCode::CREATED, + _ => StatusCode::NO_CONTENT, // 204 or any other value (should not happen due to validation) + }; + resp.status = status_code; + + // For 200 and 201, return XML body with POST response information + if status == 200 || status == 201 { + let etag_header = resp + .headers + .get(hyper::header::ETAG) + .and_then(|v| v.to_str().ok()) + .unwrap_or(""); + + // Build location URL + let location = format!("/{bucket}/{key}"); + + // Create PostResponse structure + // Parse ETag from header format (already includes quotes) + let parsed_etag = + ETag::parse_http_header(etag_header.as_bytes()).unwrap_or_else(|_| ETag::Strong(etag_header.to_owned())); + + let post_response = PostResponse { + location: location.clone(), + bucket: bucket.to_owned(), + key: key.to_owned(), + e_tag: parsed_etag, + }; + + // Serialize to XML + crate::http::set_xml_body(&mut resp, &post_response)?; + resp.headers.insert(CONTENT_TYPE, "application/xml".parse().unwrap()); + } + + Ok(resp) +}