refactor(storage): extract ListParts response assembly helper (#1812)

This commit is contained in:
安正超
2026-02-14 13:58:18 +08:00
committed by GitHub
parent 257e31a4b4
commit 6d6a2b7ed6
4 changed files with 169 additions and 37 deletions
+5 -36
View File
@@ -24,6 +24,8 @@ use crate::storage::helper::OperationHelper;
use crate::storage::options::{filter_object_metadata, get_content_sha256};
use crate::storage::readers::InMemoryAsyncReader;
use crate::storage::s3_api::bucket::{build_list_objects_output, build_list_objects_v2_output};
use crate::storage::s3_api::common::rustfs_owner;
use crate::storage::s3_api::multipart::build_list_parts_output;
use crate::storage::sse::{
DecryptionRequest, EncryptionRequest, PrepareEncryptionRequest, check_encryption_metadata, sse_decryption, sse_encryption,
sse_prepare_encryption, strip_managed_encryption_metadata,
@@ -164,10 +166,8 @@ macro_rules! try_ {
};
}
pub(crate) static RUSTFS_OWNER: LazyLock<Owner> = LazyLock::new(|| Owner {
display_name: Some("rustfs".to_owned()),
id: Some("c19050dbcee97fda828689dda99097a6321af2248fa760517237346e5d9c8a66".to_owned()),
});
// Shared owner metadata source for S3 response compatibility.
pub(crate) static RUSTFS_OWNER: LazyLock<Owner> = LazyLock::new(rustfs_owner);
#[derive(Debug, Clone)]
pub struct FS {
@@ -3846,38 +3846,7 @@ impl S3 for FS {
.await
.map_err(ApiError::from)?;
let output = ListPartsOutput {
bucket: Some(res.bucket),
key: Some(res.object),
upload_id: Some(res.upload_id),
parts: Some(
res.parts
.into_iter()
.map(|p| Part {
e_tag: p.etag.map(|etag| to_s3s_etag(&etag)),
last_modified: p.last_mod.map(Timestamp::from),
part_number: Some(p.part_num as i32),
size: Some(p.size as i64),
..Default::default()
})
.collect(),
),
owner: Some(RUSTFS_OWNER.to_owned()),
initiator: Some(Initiator {
id: RUSTFS_OWNER.id.clone(),
display_name: RUSTFS_OWNER.display_name.clone(),
}),
is_truncated: Some(res.is_truncated),
next_part_number_marker: res.next_part_number_marker.try_into().ok(),
max_parts: res.max_parts.try_into().ok(),
part_number_marker: res.part_number_marker.try_into().ok(),
storage_class: if res.storage_class.is_empty() {
None
} else {
Some(res.storage_class.into())
},
..Default::default()
};
let output = build_list_parts_output(res);
Ok(S3Response::new(output))
}
+36
View File
@@ -0,0 +1,36 @@
// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use s3s::dto::{Initiator, Owner};
// Stable owner identity used in S3 response payloads (for example ACL, ListBuckets, and multipart listings).
// RustFS currently does not maintain a persistent per-tenant canonical owner ID, so we keep a deterministic
// placeholder to preserve response compatibility and test stability.
// This value is metadata-only and must not be used for authentication/authorization decisions.
pub(crate) const RUSTFS_OWNER_DISPLAY_NAME: &str = "rustfs";
pub(crate) const RUSTFS_OWNER_ID: &str = "c19050dbcee97fda828689dda99097a6321af2248fa760517237346e5d9c8a66";
pub(crate) fn rustfs_owner() -> Owner {
Owner {
display_name: Some(RUSTFS_OWNER_DISPLAY_NAME.to_owned()),
id: Some(RUSTFS_OWNER_ID.to_owned()),
}
}
pub(crate) fn rustfs_initiator() -> Initiator {
Initiator {
id: Some(RUSTFS_OWNER_ID.to_owned()),
display_name: Some(RUSTFS_OWNER_DISPLAY_NAME.to_owned()),
}
}
+2 -1
View File
@@ -19,8 +19,9 @@
pub(crate) mod acl {}
pub(crate) mod bucket;
pub(crate) mod common;
pub(crate) mod encryption {}
pub(crate) mod multipart {}
pub(crate) mod multipart;
/// Object helper facade placeholder.
///
/// Read-path helpers shared across storage components should live in neutral
+126
View File
@@ -0,0 +1,126 @@
// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use crate::storage::s3_api::common::{rustfs_initiator, rustfs_owner};
use rustfs_ecstore::client::object_api_utils::to_s3s_etag;
use rustfs_ecstore::store_api::ListPartsInfo;
use s3s::dto::{ListPartsOutput, Part, Timestamp};
pub(crate) fn build_list_parts_output(res: ListPartsInfo) -> ListPartsOutput {
let owner = rustfs_owner();
let initiator = rustfs_initiator();
ListPartsOutput {
bucket: Some(res.bucket),
key: Some(res.object),
upload_id: Some(res.upload_id),
parts: Some(
res.parts
.into_iter()
.map(|p| Part {
e_tag: p.etag.map(|etag| to_s3s_etag(&etag)),
last_modified: p.last_mod.map(Timestamp::from),
part_number: p.part_num.try_into().ok(),
size: p.size.try_into().ok(),
..Default::default()
})
.collect(),
),
owner: Some(owner),
initiator: Some(initiator),
is_truncated: Some(res.is_truncated),
next_part_number_marker: res.next_part_number_marker.try_into().ok(),
max_parts: res.max_parts.try_into().ok(),
part_number_marker: res.part_number_marker.try_into().ok(),
storage_class: if res.storage_class.is_empty() {
None
} else {
Some(res.storage_class.into())
},
..Default::default()
}
}
#[cfg(test)]
mod tests {
use super::build_list_parts_output;
use rustfs_ecstore::client::object_api_utils::to_s3s_etag;
use rustfs_ecstore::store_api::{ListPartsInfo, PartInfo};
#[test]
fn test_list_parts_output_maps_parts_and_owner() {
let input = ListPartsInfo {
bucket: "bucket-a".to_string(),
object: "obj-a".to_string(),
upload_id: "upload-a".to_string(),
storage_class: "STANDARD".to_string(),
part_number_marker: 1,
next_part_number_marker: 2,
max_parts: 1000,
is_truncated: true,
parts: vec![PartInfo {
part_num: 1,
size: 11,
etag: Some("etag-1".to_string()),
..Default::default()
}],
..Default::default()
};
let output = build_list_parts_output(input);
let parts = output.parts.as_ref().expect("parts should be present");
assert_eq!(output.bucket.as_deref(), Some("bucket-a"));
assert_eq!(output.key.as_deref(), Some("obj-a"));
assert_eq!(output.upload_id.as_deref(), Some("upload-a"));
assert_eq!(output.storage_class.as_ref().map(|v| v.as_str()), Some("STANDARD"));
assert_eq!(output.part_number_marker, Some(1));
assert_eq!(output.next_part_number_marker, Some(2));
assert_eq!(output.max_parts, Some(1000));
assert_eq!(output.is_truncated, Some(true));
assert_eq!(parts.len(), 1);
assert_eq!(parts[0].part_number, Some(1));
assert_eq!(parts[0].size, Some(11));
assert_eq!(parts[0].e_tag, Some(to_s3s_etag("etag-1")));
assert!(output.owner.is_some());
assert!(output.initiator.is_some());
}
#[test]
fn test_list_parts_output_handles_empty_storage_class_and_overflow_markers() {
let input = ListPartsInfo {
storage_class: String::new(),
part_number_marker: usize::MAX,
next_part_number_marker: usize::MAX,
max_parts: usize::MAX,
parts: vec![PartInfo {
part_num: usize::MAX,
size: usize::MAX,
..Default::default()
}],
..Default::default()
};
let output = build_list_parts_output(input);
let parts = output.parts.as_ref().expect("parts should be present");
assert_eq!(output.storage_class, None);
assert_eq!(output.part_number_marker, None);
assert_eq!(output.next_part_number_marker, None);
assert_eq!(output.max_parts, None);
assert_eq!(parts.len(), 1);
assert_eq!(parts[0].part_number, None);
assert_eq!(parts[0].size, None);
}
}