mirror of
https://github.com/rustfs/rustfs.git
synced 2026-09-06 12:09:12 +00:00
fix: adapt object metadata to s3s DTO changes (#6981)
This commit is contained in:
@@ -1013,7 +1013,7 @@ fn build_get_object_response_headers(output: &GetObjectOutput, base_headers: &He
|
||||
insert_string_header(&mut headers, http::header::LAST_MODIFIED, format_timestamp_http_date(last_modified)?)?;
|
||||
}
|
||||
if let Some(expires) = &output.expires {
|
||||
insert_string_header(&mut headers, http::header::EXPIRES, format_timestamp_http_date(expires)?)?;
|
||||
insert_string_header(&mut headers, http::header::EXPIRES, expires.clone())?;
|
||||
}
|
||||
if let Some(version_id) = &output.version_id {
|
||||
insert_string_header(&mut headers, HeaderName::from_static("x-amz-version-id"), version_id.clone())?;
|
||||
|
||||
@@ -333,7 +333,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn metadata_operation_matches_virtual_hosted_bucket_root() {
|
||||
let host = MultiDomain::new(["example.com", "example.com:9000"]).expect("valid test host domain");
|
||||
let host = MultiDomain::new(["example.com:9000"]).expect("valid test host domain");
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(HOST, "demo-bucket.example.com:9000".parse().expect("valid host header"));
|
||||
|
||||
@@ -351,7 +351,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn metadata_operation_matches_unconfigured_host_fallbacks() {
|
||||
let host = MultiDomain::new(["s3.example.com", "s3.example.com:9000"]).expect("valid test host domain");
|
||||
let host = MultiDomain::new(["s3.example.com:9000"]).expect("valid test host domain");
|
||||
|
||||
let mut path_style_headers = HeaderMap::new();
|
||||
path_style_headers.insert(HOST, "localhost:9000".parse().expect("valid host header"));
|
||||
|
||||
@@ -265,6 +265,7 @@ impl DefaultObjectUsecase {
|
||||
));
|
||||
}
|
||||
};
|
||||
let expires_timestamp = parse_expires_header(expires.as_deref())?;
|
||||
let replacement_metadata = if replaces_metadata {
|
||||
validate_archive_content_encoding(&key, content_type.as_deref(), content_encoding.as_deref())?;
|
||||
let mut replacement_metadata = metadata.unwrap_or_default();
|
||||
@@ -276,7 +277,7 @@ impl DefaultObjectUsecase {
|
||||
content_encoding.as_deref(),
|
||||
content_language.as_deref(),
|
||||
content_type.as_deref(),
|
||||
expires.as_ref(),
|
||||
expires_timestamp.as_ref(),
|
||||
website_redirect_location.as_deref(),
|
||||
)?;
|
||||
Some(replacement_metadata)
|
||||
@@ -609,7 +610,7 @@ impl DefaultObjectUsecase {
|
||||
user_defined = replacement_metadata;
|
||||
src_info.content_type = content_type.clone();
|
||||
src_info.content_encoding = content_encoding.as_deref().and_then(normalize_content_encoding_for_storage);
|
||||
src_info.expires = expires.map(OffsetDateTime::from);
|
||||
src_info.expires = expires_timestamp.map(OffsetDateTime::from);
|
||||
} else if metadata_directive.is_some() || website_redirect_location.is_some() {
|
||||
user_defined.retain(|key, _| !key.eq_ignore_ascii_case(AMZ_WEBSITE_REDIRECT_LOCATION));
|
||||
if let Some(website_redirect_location) = website_redirect_location {
|
||||
@@ -923,6 +924,7 @@ mod tests {
|
||||
sse_algorithm: ServerSideEncryption::from(String::from("garbage")),
|
||||
kms_master_key_id: None,
|
||||
}),
|
||||
blocked_encryption_types: None,
|
||||
bucket_key_enabled: None,
|
||||
}],
|
||||
};
|
||||
|
||||
@@ -311,7 +311,11 @@ impl DefaultObjectUsecase {
|
||||
let content_disposition = metadata_map.get("content-disposition").cloned();
|
||||
let content_language = metadata_map.get("content-language").cloned();
|
||||
let website_redirect_location = metadata_map.get(AMZ_WEBSITE_REDIRECT_LOCATION).cloned();
|
||||
let expires = info.expires.map(Timestamp::from);
|
||||
let expires = info
|
||||
.expires
|
||||
.map(Timestamp::from)
|
||||
.map(|expires| format_expires_header(&expires))
|
||||
.transpose()?;
|
||||
|
||||
// Calculate tag count from user_tags already in ObjectInfo
|
||||
// This avoids an additional API call since user_tags is already populated by get_object_info
|
||||
|
||||
@@ -829,12 +829,13 @@ pub(super) fn apply_put_request_metadata(
|
||||
content_encoding: Option<ContentEncoding>,
|
||||
content_language: Option<ContentLanguage>,
|
||||
content_type: Option<ContentType>,
|
||||
expires: Option<Timestamp>,
|
||||
expires: Option<String>,
|
||||
website_redirect_location: Option<WebsiteRedirectLocation>,
|
||||
tagging: Option<TaggingHeader>,
|
||||
storage_class: Option<StorageClass>,
|
||||
) -> S3Result<()> {
|
||||
namespace_reserved_user_metadata(metadata);
|
||||
let expires = parse_expires_header(expires.as_deref())?;
|
||||
apply_standard_object_metadata(
|
||||
metadata,
|
||||
cache_control.as_deref(),
|
||||
@@ -2399,6 +2400,7 @@ mod tests {
|
||||
sse_algorithm: ServerSideEncryption::from_static(algorithm),
|
||||
kms_master_key_id: kms_key_id.map(|id| SSEKMSKeyId::from(id.to_string())),
|
||||
}),
|
||||
blocked_encryption_types: None,
|
||||
bucket_key_enabled: None,
|
||||
}],
|
||||
};
|
||||
|
||||
@@ -475,13 +475,25 @@ pub(super) fn expected_current_version_id(headers: &HeaderMap) -> S3Result<Optio
|
||||
.transpose()
|
||||
}
|
||||
|
||||
pub(super) fn parse_expires_header(expires: Option<&str>) -> S3Result<Option<Timestamp>> {
|
||||
expires
|
||||
.map(|expires| {
|
||||
Timestamp::parse(TimestampFormat::HttpDate, expires).map_err(|_| s3_error!(InvalidArgument, "Invalid Expires header"))
|
||||
})
|
||||
.transpose()
|
||||
}
|
||||
|
||||
pub(super) fn format_expires_header(expires: &Timestamp) -> S3Result<String> {
|
||||
let mut formatted = Vec::new();
|
||||
expires
|
||||
.format(TimestampFormat::HttpDate, &mut formatted)
|
||||
.map_err(|e| ApiError::from(StorageError::other(format!("Invalid expires timestamp: {e}"))))?;
|
||||
Ok(String::from_utf8_lossy(&formatted).into_owned())
|
||||
}
|
||||
|
||||
pub(super) fn insert_expires_metadata(metadata: &mut HashMap<String, String>, expires: Option<&Timestamp>) -> S3Result<()> {
|
||||
if let Some(expires) = expires {
|
||||
let mut formatted = Vec::new();
|
||||
expires
|
||||
.format(TimestampFormat::HttpDate, &mut formatted)
|
||||
.map_err(|e| ApiError::from(StorageError::other(format!("Invalid expires timestamp: {e}"))))?;
|
||||
metadata.insert("expires".to_string(), String::from_utf8_lossy(&formatted).into_owned());
|
||||
metadata.insert("expires".to_string(), format_expires_header(expires)?);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -802,6 +814,22 @@ mod tests {
|
||||
assert_eq!(throttle.claim(IO_QUEUE_CONGESTION_WARN_INTERVAL_MS + 1), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_expires_header_accepts_http_date() {
|
||||
let expires = parse_expires_header(Some("Wed, 21 Oct 2015 07:28:00 GMT"))
|
||||
.expect("valid Expires header should parse")
|
||||
.expect("header should be present");
|
||||
|
||||
assert_eq!(format_expires_header(&expires).unwrap(), "Wed, 21 Oct 2015 07:28:00 GMT");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_expires_header_rejects_invalid_http_date() {
|
||||
let err = parse_expires_header(Some("not-a-date")).expect_err("invalid Expires header should fail");
|
||||
|
||||
assert_eq!(err.code(), &S3ErrorCode::InvalidArgument);
|
||||
}
|
||||
|
||||
// classify_response_checksums is the single point that splits decrypted checksum
|
||||
// pairs into the five s3s-typed fields and the additional-algorithm `extra`
|
||||
// headers, replacing five copies of the loop. Lock its behaviour (#1252).
|
||||
@@ -938,6 +966,7 @@ mod tests {
|
||||
sse_algorithm: ServerSideEncryption::from(String::from(algorithm)),
|
||||
kms_master_key_id: kms_key_id.map(|id| SSEKMSKeyId::from(id.to_string())),
|
||||
}),
|
||||
blocked_encryption_types: None,
|
||||
bucket_key_enabled: None,
|
||||
}],
|
||||
}
|
||||
|
||||
@@ -184,6 +184,7 @@ mod tests {
|
||||
sse_algorithm: ServerSideEncryption::from_static(ServerSideEncryption::AWS_KMS),
|
||||
kms_master_key_id: key_id.map(str::to_string),
|
||||
}),
|
||||
blocked_encryption_types: None,
|
||||
bucket_key_enabled: None,
|
||||
}],
|
||||
}
|
||||
|
||||
@@ -370,6 +370,18 @@ fn put_file_server_epoch_matches(query: &PutFileQuery) -> bool {
|
||||
query.put_file_server_epoch == Some(*PUT_FILE_CAPABILITY_SERVER_EPOCH)
|
||||
}
|
||||
|
||||
fn put_file_server_epoch_accepted(query: &PutFileQuery, strict: bool) -> bool {
|
||||
if put_file_server_epoch_matches(query) {
|
||||
return true;
|
||||
}
|
||||
if strict {
|
||||
return false;
|
||||
}
|
||||
|
||||
// RUSTFS_COMPAT_TODO(put-file-auth-epoch-strict): accept signed, non-nil stale epochs because rc.2 peers can cache a server epoch before a rolling restart and cannot recover from the v1 409. Remove after the minimum supported RustFS peer version re-probes put_file capability after server-epoch conflicts and legacy put_file auth is no longer accepted.
|
||||
query.put_file_server_epoch.is_some_and(|epoch| !epoch.is_nil())
|
||||
}
|
||||
|
||||
impl<S> Service<Request<Incoming>> for InternodeRpcService<S>
|
||||
where
|
||||
S: Service<Request<Incoming>, Response = Response<Body>> + Clone + Send + 'static,
|
||||
@@ -1346,7 +1358,7 @@ async fn handle_put_file(req: Request<Incoming>, require_auth: bool) -> Response
|
||||
if require_auth && auth_nonce.is_none() {
|
||||
return response_with_status(StatusCode::FORBIDDEN, "invalid put_file auth: put_file auth required");
|
||||
}
|
||||
if require_auth && !put_file_server_epoch_matches(&query) {
|
||||
if require_auth && !put_file_server_epoch_accepted(&query, *PUT_FILE_AUTH_STRICT) {
|
||||
return response_with_status(StatusCode::CONFLICT, "put_file capability server epoch changed");
|
||||
}
|
||||
if let Some(nonce) = auth_nonce
|
||||
@@ -1690,8 +1702,8 @@ mod tests {
|
||||
READ_FILE_STREAM_PATH, WALK_DIR_BODY_SHA256_QUERY, WALK_DIR_PATH, WalkDirQuery, append_walk_dir_completion,
|
||||
internode_http_operation, internode_rpc_subsystem, is_internode_rpc_path, ns_scanner_response_body,
|
||||
ns_scanner_server_epoch_matches, put_body_size_mismatch, put_file_auth_nonce, put_file_capability_response,
|
||||
put_file_server_epoch_matches, put_file_stage_error_message, put_file_target_lock, read_file_body_stream,
|
||||
read_file_stream_buffer_size, remote_scanner_claim_rejection, response_with_disk_error,
|
||||
put_file_server_epoch_accepted, put_file_server_epoch_matches, put_file_stage_error_message, put_file_target_lock,
|
||||
read_file_body_stream, read_file_stream_buffer_size, remote_scanner_claim_rejection, response_with_disk_error,
|
||||
supports_walk_dir_stream_completion, validate_walk_dir_completion_request, verify_internode_rpc_signature,
|
||||
verify_ns_scanner_body_digest, verify_walk_dir_body_digest, walk_dir_response_body, write_authenticated_put_file,
|
||||
write_body_chunks_to_writer, write_put_file_body_chunks_to_writer,
|
||||
@@ -1832,7 +1844,7 @@ mod tests {
|
||||
|
||||
for (server_epoch, expected_status) in [
|
||||
(None, StatusCode::CONFLICT),
|
||||
(Some(uuid::Uuid::new_v4()), StatusCode::CONFLICT),
|
||||
(Some(uuid::Uuid::new_v4()), StatusCode::BAD_REQUEST),
|
||||
(Some(*super::PUT_FILE_CAPABILITY_SERVER_EPOCH), StatusCode::BAD_REQUEST),
|
||||
] {
|
||||
let nonce = uuid::Uuid::new_v4();
|
||||
@@ -2251,14 +2263,23 @@ mod tests {
|
||||
|
||||
assert_eq!(put_file_auth_nonce(&query).expect("v1 auth should parse"), Some(nonce));
|
||||
assert!(put_file_server_epoch_matches(&query));
|
||||
assert!(put_file_server_epoch_accepted(&query, false));
|
||||
assert!(put_file_server_epoch_accepted(&query, true));
|
||||
|
||||
let mut stale_epoch = query.clone();
|
||||
stale_epoch.put_file_server_epoch = Some(uuid::Uuid::new_v4());
|
||||
assert!(!put_file_server_epoch_matches(&stale_epoch));
|
||||
assert!(put_file_server_epoch_accepted(&stale_epoch, false));
|
||||
assert!(!put_file_server_epoch_accepted(&stale_epoch, true));
|
||||
|
||||
let mut missing_epoch = query.clone();
|
||||
missing_epoch.put_file_server_epoch = None;
|
||||
assert!(!put_file_server_epoch_matches(&missing_epoch));
|
||||
assert!(!put_file_server_epoch_accepted(&missing_epoch, false));
|
||||
|
||||
let mut nil_epoch = query.clone();
|
||||
nil_epoch.put_file_server_epoch = Some(uuid::Uuid::nil());
|
||||
assert!(!put_file_server_epoch_accepted(&nil_epoch, false));
|
||||
|
||||
let mut append = query.clone();
|
||||
append.append = true;
|
||||
|
||||
Reference in New Issue
Block a user