Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions codegen/src/v1/ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<u16>()",
" .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())",
Expand Down Expand Up @@ -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 {{");
Expand Down Expand Up @@ -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!("}}");
Expand Down
3 changes: 3 additions & 0 deletions crates/s3s/src/dto/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> = Vec<T>;
pub type Map<K, V> = std::collections::HashMap<K, V>;

Expand Down
32 changes: 32 additions & 0 deletions crates/s3s/src/dto/post_response.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
//! `PostResponse` structure for S3 POST object uploads
//!
//! See: <https://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectPOST.html>

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<W: std::io::Write>(&self, s: &mut Serializer<W>) -> 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(())
}
}
4 changes: 4 additions & 0 deletions crates/s3s/src/http/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ pub(crate) struct S3Extensions {
pub region: Option<String>,
pub service: Option<String>,
pub trailing_headers: Option<TrailingHeaders>,

// POST object success action fields
pub success_action_status: Option<u16>,
pub success_action_redirect: Option<String>,
}

impl From<HttpRequest> for Request {
Expand Down
33 changes: 33 additions & 0 deletions crates/s3s/src/ops/generated.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<u16>()
.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())
Expand Down Expand Up @@ -5619,7 +5634,17 @@ impl super::Operation for PutObject {

async fn call(&self, ccx: &CallContext<'_>, req: &mut http::Request) -> S3Result<http::Response> {
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?;
Expand All @@ -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)
}
}
Expand Down
33 changes: 33 additions & 0 deletions crates/s3s/src/ops/generated_minio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<u16>()
.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())
Expand Down Expand Up @@ -5634,7 +5649,17 @@ impl super::Operation for PutObject {

async fn call(&self, ccx: &CallContext<'_>, req: &mut http::Request) -> S3Result<http::Response> {
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?;
Expand All @@ -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)
}
}
Expand Down
1 change: 1 addition & 0 deletions crates/s3s/src/ops/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use self::signature::SignatureContext;

mod get_object;
mod multipart;
mod post_object;

#[cfg(test)]
mod tests;
Expand Down
86 changes: 86 additions & 0 deletions crates/s3s/src/ops/post_object.rs
Original file line number Diff line number Diff line change
@@ -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<Response> {
// 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<Response> {
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)
}