mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-24 13:16:28 +00:00
feat(s3): support metadata extensions for bucket listings (#2344)
This commit is contained in:
@@ -75,6 +75,14 @@ mod delete_objects_versioning_test;
|
||||
#[cfg(test)]
|
||||
mod list_object_versions_regression_test;
|
||||
|
||||
// versions&metadata=true extension regression test
|
||||
#[cfg(test)]
|
||||
mod list_object_versions_metadata_extension_test;
|
||||
|
||||
// list-type=2&metadata=true extension regression test
|
||||
#[cfg(test)]
|
||||
mod list_objects_v2_metadata_extension_test;
|
||||
|
||||
#[cfg(test)]
|
||||
mod protocols;
|
||||
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
// 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.
|
||||
|
||||
//! End-to-end regression test for the versions metadata extension:
|
||||
//! `GET /{bucket}?versions&metadata=true`
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::common::{RustFSTestEnvironment, init_logging, local_http_client};
|
||||
use aws_sdk_s3::types::{BucketVersioningStatus, VersioningConfiguration};
|
||||
use http::header::HOST;
|
||||
use reqwest::StatusCode;
|
||||
use rustfs_signer::constants::UNSIGNED_PAYLOAD;
|
||||
use rustfs_signer::sign_v4;
|
||||
use s3s::Body;
|
||||
use serial_test::serial;
|
||||
use std::error::Error;
|
||||
use tracing::info;
|
||||
|
||||
async fn signed_get(
|
||||
url: &str,
|
||||
access_key: &str,
|
||||
secret_key: &str,
|
||||
) -> Result<reqwest::Response, Box<dyn Error + Send + Sync>> {
|
||||
let uri = url.parse::<http::Uri>()?;
|
||||
let authority = uri.authority().ok_or("request URL missing authority")?.to_string();
|
||||
let request = http::Request::builder()
|
||||
.method(http::Method::GET)
|
||||
.uri(uri)
|
||||
.header(HOST, authority)
|
||||
.header("x-amz-content-sha256", UNSIGNED_PAYLOAD)
|
||||
.body(Body::empty())?;
|
||||
|
||||
let signed = sign_v4(request, 0, access_key, secret_key, "", "us-east-1");
|
||||
|
||||
let client = local_http_client();
|
||||
let mut request_builder = client.get(url);
|
||||
for (name, value) in signed.headers() {
|
||||
request_builder = request_builder.header(name, value);
|
||||
}
|
||||
|
||||
Ok(request_builder.send().await?)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn test_list_object_versions_metadata_extension_returns_metadata_tags_and_internal()
|
||||
-> Result<(), Box<dyn Error + Send + Sync>> {
|
||||
init_logging();
|
||||
info!("🧪 TEST: versions&metadata=true returns versions metadata extensions");
|
||||
|
||||
let mut env = RustFSTestEnvironment::new().await?;
|
||||
env.start_rustfs_server(vec![]).await?;
|
||||
|
||||
let client = env.create_s3_client();
|
||||
let bucket = "test-list-versions-metadata-ext";
|
||||
let key = "versions/metadata-object.txt";
|
||||
|
||||
client.create_bucket().bucket(bucket).send().await?;
|
||||
client
|
||||
.put_bucket_versioning()
|
||||
.bucket(bucket)
|
||||
.versioning_configuration(
|
||||
VersioningConfiguration::builder()
|
||||
.status(BucketVersioningStatus::Enabled)
|
||||
.build(),
|
||||
)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
client
|
||||
.put_object()
|
||||
.bucket(bucket)
|
||||
.key(key)
|
||||
.metadata("project", "alpha")
|
||||
.metadata("owner", "ops")
|
||||
.tagging("env=test&project=alpha")
|
||||
.body(aws_sdk_s3::primitives::ByteStream::from_static(b"metadata extension body"))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let url = format!("{}/{}?versions&metadata=true&prefix={}", env.url, bucket, urlencoding::encode(key));
|
||||
let response = signed_get(&url, &env.access_key, &env.secret_key).await?;
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK, "versions&metadata=true should succeed");
|
||||
|
||||
let body = response.text().await?;
|
||||
info!("versions&metadata=true response body: {}", body);
|
||||
|
||||
assert!(body.contains("<ListVersionsResult"), "expected ListVersionsResult XML, got: {body}");
|
||||
assert!(body.contains("<Version>"), "expected at least one Version entry, got: {body}");
|
||||
assert!(body.contains("<UserMetadata>"), "expected UserMetadata extension, got: {body}");
|
||||
assert!(
|
||||
body.contains("<project>alpha</project>"),
|
||||
"expected stripped user metadata key in response, got: {body}"
|
||||
);
|
||||
assert!(
|
||||
body.contains("<owner>ops</owner>"),
|
||||
"expected second metadata key in response, got: {body}"
|
||||
);
|
||||
assert!(
|
||||
body.contains("<UserTags>env=test&project=alpha</UserTags>"),
|
||||
"expected UserTags extension in response, got: {body}"
|
||||
);
|
||||
assert!(body.contains("<Internal>"), "expected Internal extension, got: {body}");
|
||||
assert!(body.contains("<K>1</K>"), "expected Internal/K in response, got: {body}");
|
||||
assert!(body.contains("<M>0</M>"), "expected Internal/M in response, got: {body}");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
// 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.
|
||||
|
||||
//! End-to-end regression test for the objects metadata extension:
|
||||
//! `GET /{bucket}?list-type=2&metadata=true`
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::common::{RustFSTestEnvironment, init_logging, local_http_client};
|
||||
use http::header::HOST;
|
||||
use reqwest::StatusCode;
|
||||
use rustfs_signer::constants::UNSIGNED_PAYLOAD;
|
||||
use rustfs_signer::sign_v4;
|
||||
use s3s::Body;
|
||||
use serial_test::serial;
|
||||
use std::error::Error;
|
||||
use tracing::info;
|
||||
|
||||
async fn signed_get(
|
||||
url: &str,
|
||||
access_key: &str,
|
||||
secret_key: &str,
|
||||
) -> Result<reqwest::Response, Box<dyn Error + Send + Sync>> {
|
||||
let uri = url.parse::<http::Uri>()?;
|
||||
let authority = uri.authority().ok_or("request URL missing authority")?.to_string();
|
||||
let request = http::Request::builder()
|
||||
.method(http::Method::GET)
|
||||
.uri(uri)
|
||||
.header(HOST, authority)
|
||||
.header("x-amz-content-sha256", UNSIGNED_PAYLOAD)
|
||||
.body(Body::empty())?;
|
||||
|
||||
let signed = sign_v4(request, 0, access_key, secret_key, "", "us-east-1");
|
||||
|
||||
let client = local_http_client();
|
||||
let mut request_builder = client.get(url);
|
||||
for (name, value) in signed.headers() {
|
||||
request_builder = request_builder.header(name, value);
|
||||
}
|
||||
|
||||
Ok(request_builder.send().await?)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn test_list_objects_v2_metadata_extension_returns_metadata_tags_and_internal()
|
||||
-> Result<(), Box<dyn Error + Send + Sync>> {
|
||||
init_logging();
|
||||
info!("🧪 TEST: list-type=2&metadata=true returns object metadata extensions");
|
||||
|
||||
let mut env = RustFSTestEnvironment::new().await?;
|
||||
env.start_rustfs_server(vec![]).await?;
|
||||
|
||||
let client = env.create_s3_client();
|
||||
let bucket = "test-list-objects-v2-metadata-ext";
|
||||
let key = "objects/metadata-object.txt";
|
||||
|
||||
client.create_bucket().bucket(bucket).send().await?;
|
||||
client
|
||||
.put_object()
|
||||
.bucket(bucket)
|
||||
.key(key)
|
||||
.metadata("project", "alpha")
|
||||
.metadata("owner", "ops")
|
||||
.tagging("env=test&project=alpha")
|
||||
.body(aws_sdk_s3::primitives::ByteStream::from_static(b"metadata extension body"))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let url = format!("{}/{}?list-type=2&metadata=true&prefix={}", env.url, bucket, urlencoding::encode(key));
|
||||
let response = signed_get(&url, &env.access_key, &env.secret_key).await?;
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK, "list-type=2&metadata=true should succeed");
|
||||
|
||||
let body = response.text().await?;
|
||||
info!("list-type=2&metadata=true response body: {}", body);
|
||||
|
||||
assert!(body.contains("<ListBucketResult"), "expected ListBucketResult XML, got: {body}");
|
||||
assert!(body.contains("<Contents>"), "expected at least one Contents entry, got: {body}");
|
||||
assert!(body.contains("<UserMetadata>"), "expected UserMetadata extension, got: {body}");
|
||||
assert!(
|
||||
body.contains("<project>alpha</project>"),
|
||||
"expected stripped user metadata key in response, got: {body}"
|
||||
);
|
||||
assert!(
|
||||
body.contains("<owner>ops</owner>"),
|
||||
"expected second metadata key in response, got: {body}"
|
||||
);
|
||||
assert!(
|
||||
body.contains("<UserTags>env=test&project=alpha</UserTags>"),
|
||||
"expected UserTags extension in response, got: {body}"
|
||||
);
|
||||
assert!(body.contains("<Internal>"), "expected Internal extension, got: {body}");
|
||||
assert!(body.contains("<K>1</K>"), "expected Internal/K in response, got: {body}");
|
||||
assert!(body.contains("<M>0</M>"), "expected Internal/M in response, got: {body}");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user