use super::*;
mod object_dispatch_helpers;
use object_dispatch_helpers::*;
impl BucketWarden {
pub(crate) fn dispatch_object_s3_http(
&mut self,
request: S3HttpRequest,
method: &str,
bucket: &str,
key: &str,
) -> Result<S3HttpResponse, RuntimeError> {
match (method, Some(bucket), Some(key)) {
("POST", Some(bucket), Some(key)) if request.query.contains_key("uploads") => {
validate_acl_write(&request.headers, &[])?;
let mut metadata = metadata_from_headers(&request.headers);
metadata.encryption = Some(self.encryption_from_headers(bucket, &request.headers)?);
let lock = self.object_lock_from_headers(bucket, &request.headers)?;
let result = self.create_multipart_upload(
&request.principal,
CreateMultipartUploadRequest {
bucket: bucket.to_string(),
key: key.to_string(),
metadata,
},
lock,
)?;
Ok(xml_response(200, create_multipart_upload_xml(&result)))
}
("PUT", Some(bucket), Some(key))
if request.query.contains_key("partNumber")
&& request.query.contains_key("uploadId") =>
{
let part_number = request
.query
.get("partNumber")
.and_then(|value| value.parse::<u16>().ok())
.ok_or_else(|| RuntimeError::InvalidPartNumber("partNumber".to_string()))?;
let upload_id = request
.query
.get("uploadId")
.expect("uploadId checked above")
.clone();
let checksums = validate_write_checksums(&request.headers, &request.body)?;
let result =
if let Some(copy_source) = header(&request.headers, "x-amz-copy-source") {
let (source_bucket, source_key, source_version_id) =
parse_copy_source(copy_source)?;
let range =
header(&request.headers, "x-amz-copy-source-range").map(str::to_string);
self.upload_part_copy(
&request.principal,
&source_bucket,
&source_key,
source_version_id.as_deref(),
UploadPartRequest {
bucket: bucket.to_string(),
key: key.to_string(),
upload_id,
part_number,
body: Vec::new(),
},
range.as_deref(),
)?
} else {
self.upload_part(
&request.principal,
UploadPartRequest {
bucket: bucket.to_string(),
key: key.to_string(),
upload_id,
part_number,
body: request.body,
},
)?
};
let mut response =
S3HttpResponse::new(200).with_header("ETag", result.etag.clone());
if header(&request.headers, "x-amz-copy-source").is_some() {
response = response.with_body(
upload_part_copy_xml(&CopyPartResult {
etag: result.etag.clone(),
})
.into_bytes(),
);
response
.headers
.insert("content-type".to_string(), "application/xml".to_string());
}
Ok(apply_checksum_response_headers(response, checksums))
}
("GET", Some(bucket), Some(key)) if request.query.contains_key("uploadId") => {
let upload_id = request
.query
.get("uploadId")
.expect("uploadId checked above");
let result = self.list_parts(
&request.principal,
bucket,
key,
upload_id,
request
.query
.get("part-number-marker")
.map(|value| {
value
.parse::<u16>()
.map_err(|_| RuntimeError::InvalidListParameter {
name: "part-number-marker".to_string(),
value: value.clone(),
})
})
.transpose()?,
request
.query
.get("max-parts")
.map(|value| parse_max_keys(value))
.transpose()?,
)?;
Ok(xml_response(200, list_parts_xml(&result)))
}
("POST", Some(bucket), Some(key)) if request.query.contains_key("uploadId") => {
let upload_id = request
.query
.get("uploadId")
.expect("uploadId checked above")
.clone();
let parts = parse_completed_multipart_upload(&request.body);
let result = self.complete_multipart_upload(
&request.principal,
CompleteMultipartUploadRequest {
bucket: bucket.to_string(),
key: key.to_string(),
upload_id,
parts: parts.parts,
},
)?;
Ok(xml_response(200, complete_multipart_upload_xml(&result)))
}
("POST", Some(bucket), Some(key)) if request.query.contains_key("restore") => {
let result = self.restore_object(&request.principal, bucket, key)?;
Ok(S3HttpResponse::new(202)
.with_header("x-amz-restore", result)
.with_header("content-type", "application/xml")
.with_body(
b"<RestoreObjectResult><Status>Restored</Status></RestoreObjectResult>"
.to_vec(),
))
}
("POST", Some(bucket), Some(key)) if request.query.contains_key("select") => {
if let Some(feature_id) = select_object_content_request_error_feature(&request.body)
{
return Ok(self
.s3_service_specific_error_response(feature_id)
.expect("tracked select service-specific error"));
}
let result =
self.select_object_content(&request.principal, bucket, key, &request.body)?;
Ok(S3HttpResponse::new(200)
.with_header("content-type", result.content_type)
.with_header("x-amz-select-output-format", result.output_format)
.with_header("x-amz-version-id", result.version_id)
.with_body(result.body))
}
("PUT", Some(bucket), Some(key)) if request.query.contains_key("renameObject") => {
let source_key = parse_rename_source(bucket, &request.headers)?;
if let Some(destination) = self
.buckets
.get(bucket)
.and_then(|bucket_state| bucket_state.objects.get(key))
.and_then(ObjectState::current_version)
{
if let Some(response) = conditional_response(
&request.headers,
&destination.etag,
destination.last_modified_epoch_seconds,
)? {
return Ok(response);
}
} else if header(&request.headers, "if-match").is_some() {
return Ok(precondition_failed());
}
let result = self.rename_object(
&request.principal,
RenameObjectRequest {
bucket: bucket.to_string(),
source_key,
destination_key: key.to_string(),
},
)?;
Ok(S3HttpResponse::new(200)
.with_header("etag", quote_etag(&result.etag))
.with_header("x-amz-version-id", result.version_id))
}
("PUT", Some(bucket), Some(key)) if request.query.contains_key("encryption") => {
let result = self.update_object_encryption(
&request.principal,
parse_update_object_encryption_request(
bucket,
key,
&request.query,
&request.body,
)?,
)?;
Ok(apply_encryption_response_headers(
S3HttpResponse::new(200).with_header("x-amz-version-id", result.version_id),
Some(&result.encryption),
))
}
("GET", Some(bucket), Some(key)) if request.query.contains_key("attributes") => {
let requested = parse_requested_object_attributes(&request.headers)?;
let result = if let Some(version_id) = request.query.get("versionId") {
self.get_object_version_attributes(&request.principal, bucket, key, version_id)?
} else {
self.get_object_attributes(&request.principal, bucket, key)?
};
Ok(xml_response(
200,
get_object_attributes_xml(&result, &requested),
))
}
("GET", Some(bucket), Some(key)) if request.query.contains_key("torrent") => {
let result = if let Some(version_id) = request.query.get("versionId") {
self.get_object_version_torrent(&request.principal, bucket, key, version_id)?
} else {
self.get_object_torrent(&request.principal, bucket, key)?
};
get_object_torrent_response(&result, header(&request.headers, "host"))
}
("GET", Some(bucket), Some(key)) if request.query.contains_key("legal-hold") => {
let result = self.get_object_legal_hold(
&request.principal,
bucket,
key,
request.query.get("versionId").map(String::as_str),
)?;
Ok(xml_response(200, object_legal_hold_xml(&result)))
}
("PUT", Some(bucket), Some(key)) if request.query.contains_key("legal-hold") => {
let enabled = parse_object_legal_hold(&request.body)?;
let result = self.put_object_legal_hold(
&request.principal,
ObjectLegalHoldRequest {
bucket: bucket.to_string(),
key: key.to_string(),
version_id: request.query.get("versionId").cloned(),
enabled,
},
)?;
Ok(xml_response(200, object_legal_hold_xml(&result)))
}
("GET", Some(bucket), Some(key)) if request.query.contains_key("retention") => {
let result = self.get_object_retention(
&request.principal,
bucket,
key,
request.query.get("versionId").map(String::as_str),
)?;
Ok(xml_response(200, object_retention_xml(&result)))
}
("PUT", Some(bucket), Some(key)) if request.query.contains_key("retention") => {
let (mode, retain_until_epoch_seconds) = parse_object_retention(&request.body)?;
let bypass_governance =
header(&request.headers, "x-amz-bypass-governance-retention")
.is_some_and(|value| value.eq_ignore_ascii_case("true"));
let result = self.put_object_retention(
&request.principal,
ObjectRetentionRequest {
bucket: bucket.to_string(),
key: key.to_string(),
version_id: request.query.get("versionId").cloned(),
mode,
retain_until_epoch_seconds,
bypass_governance,
},
)?;
Ok(xml_response(200, object_retention_xml(&result)))
}
("GET", Some(bucket), Some(key)) if request.query.contains_key("tagging") => {
let result = self.get_object_tagging(
&request.principal,
bucket,
key,
request.query.get("versionId").map(String::as_str),
)?;
Ok(xml_response(200, object_tagging_xml(&result)))
}
("PUT", Some(bucket), Some(key)) if request.query.contains_key("tagging") => {
let tags = parse_object_tags(&request.body)?;
let result = self.put_object_tagging(
&request.principal,
ObjectTaggingRequest {
bucket: bucket.to_string(),
key: key.to_string(),
version_id: request.query.get("versionId").cloned(),
tags,
},
)?;
Ok(xml_response(200, object_tagging_xml(&result)))
}
("DELETE", Some(bucket), Some(key)) if request.query.contains_key("tagging") => {
let result = self.delete_object_tagging(
&request.principal,
bucket,
key,
request.query.get("versionId").map(String::as_str),
)?;
Ok(xml_response(200, object_tagging_xml(&result)))
}
("GET", Some(bucket), Some(key)) if request.query.contains_key("acl") => {
let result = self.get_object_acl(
&request.principal,
bucket,
key,
request.query.get("versionId").map(String::as_str),
)?;
Ok(xml_response(200, acl_xml(&result)))
}
("PUT", Some(bucket), Some(key)) if request.query.contains_key("acl") => {
validate_acl_write(&request.headers, &request.body)?;
let result = self.put_object_acl(
&request.principal,
bucket,
key,
request.query.get("versionId").map(String::as_str),
)?;
Ok(xml_response(200, acl_xml(&result)))
}
("DELETE", Some(bucket), Some(key)) if request.query.contains_key("uploadId") => {
let upload_id = request
.query
.get("uploadId")
.expect("uploadId checked above")
.clone();
self.abort_multipart_upload(
&request.principal,
AbortMultipartUploadRequest {
bucket: bucket.to_string(),
key: key.to_string(),
upload_id,
},
)?;
Ok(S3HttpResponse::new(204))
}
("PUT", Some(bucket), Some(key)) => {
dispatch_put_object_s3_http(self, request, bucket, key)
}
("GET", Some(bucket), Some(key)) => {
let result = if let Some(version_id) = request.query.get("versionId") {
self.get_object_version(&request.principal, bucket, key, version_id)?
} else {
self.get_object(&request.principal, bucket, key)?
};
if let Some(response) = conditional_response(
&request.headers,
&result.etag,
result.last_modified_epoch_seconds,
)? {
return Ok(response);
}
Ok(object_response(
result,
header(&request.headers, "range"),
&request.query,
)?)
}
("HEAD", Some(bucket), Some(key)) => {
let result = if let Some(version_id) = request.query.get("versionId") {
self.head_object_version(&request.principal, bucket, key, version_id)?
} else {
self.head_object(&request.principal, bucket, key)?
};
if let Some(response) = conditional_response(
&request.headers,
&result.etag,
result.last_modified_epoch_seconds,
)? {
return Ok(response);
}
Ok(head_response(
result,
header(&request.headers, "range"),
&request.query,
)?)
}
("DELETE", Some(bucket), Some(key)) => {
let bypass_governance =
header(&request.headers, "x-amz-bypass-governance-retention")
.is_some_and(|value| value.eq_ignore_ascii_case("true"));
if let Some(version_id) = request.query.get("versionId") {
let result = self.delete_object_version(
&request.principal,
bucket,
key,
version_id,
bypass_governance,
)?;
let mut response =
S3HttpResponse::new(204).with_header("x-amz-version-id", result.version_id);
if result.delete_marker {
response = response.with_header("x-amz-delete-marker", "true");
}
Ok(response)
} else {
let result =
self.delete_object(&request.principal, bucket, key, bypass_governance)?;
Ok(S3HttpResponse::new(204)
.with_header("x-amz-delete-marker", "true")
.with_header("x-amz-version-id", result.delete_marker_version_id))
}
}
_ => Ok(general_error_response(
"MethodNotAllowed",
405,
"The specified method is not allowed for this resource.",
)),
}
}
}