use crate::error::ProxyError;
use crate::types::S3Operation;
use http::Method;
pub fn parse_s3_request(
method: &Method,
uri_path: &str,
query: Option<&str>,
_headers: &http::HeaderMap,
host_style: HostStyle,
) -> Result<S3Operation, ProxyError> {
if matches!(host_style, HostStyle::Path) && uri_path.trim_start_matches('/').is_empty() {
if *method == Method::GET {
return Ok(S3Operation::ListBuckets);
}
return Err(ProxyError::InvalidRequest(
"unsupported operation on /".into(),
));
}
let (bucket, key) = match host_style {
HostStyle::Path => parse_path_style(uri_path)?,
HostStyle::VirtualHosted { bucket } => {
(bucket, uri_path.trim_start_matches('/').to_string())
}
};
build_s3_operation(method, bucket, key, query)
}
pub fn build_s3_operation(
method: &Method,
bucket: String,
key: String,
query: Option<&str>,
) -> Result<S3Operation, ProxyError> {
let query_params = parse_query_params(query);
let upload_id = query_params
.iter()
.find(|(k, _)| k == "uploadId")
.map(|(_, v)| v.clone());
let has_uploads = query_params.iter().any(|(k, _)| k == "uploads");
match *method {
Method::GET => {
if key.is_empty() {
Ok(S3Operation::ListBucket {
bucket,
raw_query: query.map(|q| q.to_string()),
})
} else {
Ok(S3Operation::GetObject { bucket, key })
}
}
Method::HEAD => Ok(S3Operation::HeadObject { bucket, key }),
Method::PUT => {
if let Some(upload_id) = upload_id {
let part_number = query_params
.iter()
.find(|(k, _)| k == "partNumber")
.and_then(|(_, v)| v.parse().ok())
.ok_or_else(|| ProxyError::InvalidRequest("missing partNumber".into()))?;
Ok(S3Operation::UploadPart {
bucket,
key,
upload_id,
part_number,
})
} else {
Ok(S3Operation::PutObject { bucket, key })
}
}
Method::POST => {
if has_uploads {
Ok(S3Operation::CreateMultipartUpload { bucket, key })
} else if let Some(upload_id) = upload_id {
Ok(S3Operation::CompleteMultipartUpload {
bucket,
key,
upload_id,
})
} else {
Err(ProxyError::InvalidRequest(
"unsupported POST operation".into(),
))
}
}
Method::DELETE => {
if let Some(upload_id) = upload_id {
Ok(S3Operation::AbortMultipartUpload {
bucket,
key,
upload_id,
})
} else if !key.is_empty() {
Ok(S3Operation::DeleteObject { bucket, key })
} else {
Err(ProxyError::InvalidRequest(
"unsupported DELETE operation".into(),
))
}
}
_ => Err(ProxyError::InvalidRequest(format!(
"unsupported method: {}",
method
))),
}
}
#[derive(Debug, Clone)]
pub enum HostStyle {
Path,
VirtualHosted { bucket: String },
}
fn parse_path_style(path: &str) -> Result<(String, String), ProxyError> {
let trimmed = path.trim_start_matches('/');
if trimmed.is_empty() {
return Err(ProxyError::InvalidRequest("empty path".into()));
}
match trimmed.split_once('/') {
Some((bucket, key)) => Ok((bucket.to_string(), key.to_string())),
None => Ok((trimmed.to_string(), String::new())),
}
}
fn parse_query_params(query: Option<&str>) -> Vec<(String, String)> {
query
.map(|q| {
url::form_urlencoded::parse(q.as_bytes())
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect()
})
.unwrap_or_default()
}