fix(s3): persist multipart upload storage class (#5170)

Fixes rustfs/backlog#1464.
This commit is contained in:
cxymds
2026-07-24 13:47:25 +08:00
committed by GitHub
parent cb344a3c77
commit 6f6d8a4d3e
5 changed files with 430 additions and 13 deletions
+60 -11
View File
@@ -41,7 +41,7 @@ use super::storage_api::multipart_usecase::io::{HashReader, WriteEncryption, Wri
use super::storage_api::multipart_usecase::object_utils::to_s3s_etag;
use super::storage_api::multipart_usecase::options::{
copy_src_opts, extract_metadata_from_mime, get_complete_multipart_upload_opts, get_content_sha256_with_query, get_opts,
parse_copy_source_range, put_opts, validate_archive_content_encoding,
namespace_reserved_user_metadata, parse_copy_source_range, put_opts, validate_archive_content_encoding,
};
use super::storage_api::multipart_usecase::s3_api::multipart::{
ListMultipartUploadsParams, build_list_multipart_uploads_output, build_list_parts_output,
@@ -74,7 +74,7 @@ use rustfs_targets::EventName;
use rustfs_utils::CompressionAlgorithm;
use rustfs_utils::http::{
SUFFIX_REPLICATION_STATUS, SUFFIX_REPLICATION_TIMESTAMP, get_source_scheme,
headers::{AMZ_DECODED_CONTENT_LENGTH, AMZ_OBJECT_TAGGING},
headers::{AMZ_DECODED_CONTENT_LENGTH, AMZ_OBJECT_TAGGING, AMZ_STORAGE_CLASS},
insert_str,
};
use s3s::dto::{
@@ -85,9 +85,7 @@ use s3s::dto::{
};
use s3s::header::{X_AMZ_OBJECT_LOCK_LEGAL_HOLD, X_AMZ_OBJECT_LOCK_MODE, X_AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE};
use s3s::{S3Error, S3ErrorCode, S3Request, S3Response, S3Result, s3_error};
#[cfg(test)]
use std::collections::HashMap;
use std::collections::HashSet;
use std::collections::{HashMap, HashSet};
use std::str::FromStr;
use std::sync::Arc;
use tokio::sync::RwLock;
@@ -178,6 +176,26 @@ fn complete_part_from_s3(value: CompletedPart) -> CompletePart {
}
}
fn create_multipart_upload_metadata(
input_metadata: Option<HashMap<String, String>>,
headers: &HeaderMap,
tagging: Option<String>,
storage_class: Option<&s3s::dto::StorageClass>,
) -> HashMap<String, String> {
let mut metadata = input_metadata.unwrap_or_default();
namespace_reserved_user_metadata(&mut metadata);
extract_metadata_from_mime(headers, &mut metadata);
if let Some(tags) = tagging {
metadata.insert(AMZ_OBJECT_TAGGING.to_owned(), tags);
}
if let Some(storage_class) = storage_class {
metadata.insert(AMZ_STORAGE_CLASS.to_owned(), storage_class.as_str().to_owned());
}
metadata
}
async fn validate_table_catalog_object_mutation(bucket: &str, key: &str) -> S3Result<()> {
table_catalog::validate_bucket_object_mutation(bucket, key)
.await
@@ -643,12 +661,7 @@ impl DefaultMultipartUsecase {
req.headers.get("content-encoding").and_then(|value| value.to_str().ok()),
)?;
let mut metadata = input_metadata.unwrap_or_default();
extract_metadata_from_mime(&req.headers, &mut metadata);
if let Some(tags) = tagging {
metadata.insert(AMZ_OBJECT_TAGGING.to_owned(), tags);
}
let mut metadata = create_multipart_upload_metadata(input_metadata, &req.headers, tagging, storage_class.as_ref());
let has_explicit_object_lock_retention = object_lock_mode.is_some() || object_lock_retain_until_date.is_some();
if let Some(object_lock_metadata) = build_put_like_object_lock_metadata(
@@ -1695,6 +1708,42 @@ mod tests {
assert_eq!(err.code(), &S3ErrorCode::InvalidStorageClass);
}
#[test]
fn create_multipart_upload_metadata_persists_dto_storage_class_without_raw_header() {
let metadata = create_multipart_upload_metadata(
None,
&HeaderMap::new(),
None,
Some(&StorageClass::from_static("REDUCED_REDUNDANCY")),
);
assert_eq!(metadata.get(AMZ_STORAGE_CLASS), Some(&"REDUCED_REDUNDANCY".to_string()));
}
#[test]
fn create_multipart_upload_metadata_keeps_user_and_system_namespaces_separate() {
let mut headers = HeaderMap::new();
headers.insert("content-type", HeaderValue::from_static("application/octet-stream"));
headers.insert(AMZ_STORAGE_CLASS, HeaderValue::from_static("STANDARD"));
let input_metadata = HashMap::from([
("content-type".to_string(), "user-content-type".to_string()),
(AMZ_STORAGE_CLASS.to_string(), "user-storage-class".to_string()),
]);
let metadata = create_multipart_upload_metadata(
Some(input_metadata),
&headers,
Some("project=rustfs".to_string()),
Some(&StorageClass::from_static("REDUCED_REDUNDANCY")),
);
assert_eq!(metadata.get("content-type"), Some(&"application/octet-stream".to_string()));
assert_eq!(metadata.get("x-amz-meta-content-type"), Some(&"user-content-type".to_string()));
assert_eq!(metadata.get(AMZ_STORAGE_CLASS), Some(&"REDUCED_REDUNDANCY".to_string()));
assert_eq!(metadata.get("x-amz-meta-x-amz-storage-class"), Some(&"user-storage-class".to_string()));
assert_eq!(metadata.get(AMZ_OBJECT_TAGGING), Some(&"project=rustfs".to_string()));
}
#[tokio::test]
async fn execute_complete_multipart_upload_rejects_missing_parts_payload() {
let input = CompleteMultipartUploadInput::builder()