diff --git a/rustfs/src/storage/ecfs.rs b/rustfs/src/storage/ecfs.rs index 3e4d0bb22..affcc64b7 100644 --- a/rustfs/src/storage/ecfs.rs +++ b/rustfs/src/storage/ecfs.rs @@ -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 = 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 = 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)) } diff --git a/rustfs/src/storage/s3_api/common.rs b/rustfs/src/storage/s3_api/common.rs new file mode 100644 index 000000000..bcc26a397 --- /dev/null +++ b/rustfs/src/storage/s3_api/common.rs @@ -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()), + } +} diff --git a/rustfs/src/storage/s3_api/mod.rs b/rustfs/src/storage/s3_api/mod.rs index 5fac73943..eded25e1c 100644 --- a/rustfs/src/storage/s3_api/mod.rs +++ b/rustfs/src/storage/s3_api/mod.rs @@ -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 diff --git a/rustfs/src/storage/s3_api/multipart.rs b/rustfs/src/storage/s3_api/multipart.rs new file mode 100644 index 000000000..e3dbc7113 --- /dev/null +++ b/rustfs/src/storage/s3_api/multipart.rs @@ -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); + } +}