refactor(storage): extract tagging helpers from ecfs (#1881)

This commit is contained in:
安正超
2026-02-22 11:20:41 +08:00
committed by GitHub
parent 6972a7b4b2
commit 094e6a7319
3 changed files with 185 additions and 49 deletions

View File

@@ -34,6 +34,10 @@ use crate::storage::s3_api::multipart::{
use crate::storage::s3_api::response::{
access_denied_error, map_abort_multipart_upload_error, not_initialized_error, s3_response,
};
use crate::storage::s3_api::tagging::{
build_delete_object_tagging_output, build_get_bucket_tagging_output, build_get_object_tagging_output,
build_put_object_tagging_output, validate_object_tag_set,
};
use crate::storage::sse::{
DecryptionRequest, EncryptionRequest, PrepareEncryptionRequest, check_encryption_metadata, sse_decryption, sse_encryption,
sse_prepare_encryption, strip_managed_encryption_metadata,
@@ -1627,7 +1631,7 @@ impl S3 for FS {
let version_id_resp = version_id.clone().unwrap_or_default();
helper = helper.version_id(version_id_resp);
let result = Ok(s3_response(DeleteObjectTaggingOutput { version_id }));
let result = Ok(s3_response(build_delete_object_tagging_output(version_id)));
let _ = helper.complete(&result);
let duration = start_time.elapsed();
histogram!("rustfs.object_tagging.operation.duration.seconds", "operation" => "delete").record(duration.as_secs_f64());
@@ -2291,7 +2295,7 @@ impl S3 for FS {
}
};
Ok(s3_response(GetBucketTaggingOutput { tag_set }))
Ok(s3_response(build_get_bucket_tagging_output(tag_set)))
}
#[instrument(level = "debug", skip(self))]
@@ -3080,7 +3084,12 @@ impl S3 for FS {
#[instrument(level = "debug", skip(self))]
async fn get_object_tagging(&self, req: S3Request<GetObjectTaggingInput>) -> S3Result<S3Response<GetObjectTaggingOutput>> {
let start_time = std::time::Instant::now();
let GetObjectTaggingInput { bucket, key: object, .. } = req.input;
let GetObjectTaggingInput {
bucket,
key: object,
version_id,
..
} = req.input;
info!("Starting get_object_tagging for bucket: {}, object: {}", bucket, object);
@@ -3090,9 +3099,8 @@ impl S3 for FS {
};
// Support versioned objects
let version_id = req.input.version_id.clone();
let opts = ObjectOptions {
version_id: self.parse_version_id(version_id)?.map(Into::into),
version_id: self.parse_version_id(version_id.clone())?.map(Into::into),
..Default::default()
};
@@ -3112,10 +3120,7 @@ impl S3 for FS {
counter!("rustfs.get_object_tagging.success").increment(1);
let duration = start_time.elapsed();
histogram!("rustfs.object_tagging.operation.duration.seconds", "operation" => "put").record(duration.as_secs_f64());
Ok(s3_response(GetObjectTaggingOutput {
tag_set,
version_id: req.input.version_id.clone(),
}))
Ok(s3_response(build_get_object_tagging_output(tag_set, version_id)))
}
#[instrument(level = "debug", skip(self, _req))]
@@ -4277,49 +4282,15 @@ impl S3 for FS {
..
} = req.input.clone();
if tagging.tag_set.len() > 10 {
// TOTO: Note that Amazon S3 limits the maximum number of tags to 10 tags per object.
// Reference: https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-tagging.html
// Reference: https://docs.aws.amazon.com/zh_cn/AmazonS3/latest/API/API_PutObjectTagging.html
// https://github.com/minio/mint/blob/master/run/core/aws-sdk-go-v2/main.go#L1647
error!("Tag set exceeds maximum of 10 tags: {}", tagging.tag_set.len());
return Err(s3_error!(InvalidTag, "Cannot have more than 10 tags per object"));
if let Err(err) = validate_object_tag_set(&tagging.tag_set) {
error!("Invalid object tags for bucket: {}, object: {}: {}", bucket, object, err);
return Err(err);
}
let Some(store) = new_object_layer_fn() else {
return Err(not_initialized_error());
};
let mut tag_keys = std::collections::HashSet::with_capacity(tagging.tag_set.len());
for tag in &tagging.tag_set {
let key = tag.key.as_ref().filter(|k| !k.is_empty()).ok_or_else(|| {
error!("Empty tag key");
s3_error!(InvalidTag, "Tag key cannot be empty")
})?;
if key.len() > 128 {
error!("Tag key too long: {} bytes", key.len());
return Err(s3_error!(InvalidTag, "Tag key is too long, maximum allowed length is 128 characters"));
}
// allow to set the value of a tag to an empty string, but cannot set it to a null value.
// Referencehttps://docs.aws.amazon.com/zh_cn/AWSEC2/latest/UserGuide/Using_Tags.html#tag-restrictions
let value = tag.value.as_ref().ok_or_else(|| {
error!("Null tag value");
s3_error!(InvalidTag, "Tag value cannot be null")
})?;
if value.len() > 256 {
error!("Tag value too long: {} bytes", value.len());
return Err(s3_error!(InvalidTag, "Tag value is too long, maximum allowed length is 256 characters"));
}
if !tag_keys.insert(key) {
error!("Duplicate tag key: {}", key);
return Err(s3_error!(InvalidTag, "Cannot provide multiple Tags with the same key"));
}
}
let tags = encode_tags(tagging.tag_set);
debug!("Encoded tags: {}", tags);
@@ -4354,9 +4325,7 @@ impl S3 for FS {
let version_id_resp = req.input.version_id.clone().unwrap_or_default();
helper = helper.version_id(version_id_resp);
let result = Ok(s3_response(PutObjectTaggingOutput {
version_id: req.input.version_id.clone(),
}));
let result = Ok(s3_response(build_put_object_tagging_output(req.input.version_id.clone())));
let _ = helper.complete(&result);
let duration = start_time.elapsed();
histogram!("rustfs.object_tagging.operation.duration.seconds", "operation" => "put").record(duration.as_secs_f64());

View File

@@ -32,4 +32,5 @@ pub(crate) mod replication {}
pub(crate) mod response;
pub(crate) mod restore {}
pub(crate) mod select {}
pub(crate) mod tagging;
pub(crate) mod validation {}

View File

@@ -0,0 +1,166 @@
// 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::{DeleteObjectTaggingOutput, GetBucketTaggingOutput, GetObjectTaggingOutput, PutObjectTaggingOutput, Tag};
use s3s::{S3Error, S3ErrorCode, S3Result};
use std::collections::HashSet;
fn invalid_tag_error(message: &str) -> S3Error {
S3Error::with_message(S3ErrorCode::InvalidTag, message.to_string())
}
pub(crate) fn validate_object_tag_set(tag_set: &[Tag]) -> S3Result<()> {
if tag_set.len() > 10 {
return Err(invalid_tag_error("Cannot have more than 10 tags per object"));
}
let mut tag_keys = HashSet::with_capacity(tag_set.len());
for tag in tag_set {
let key = tag
.key
.as_deref()
.filter(|key| !key.is_empty())
.ok_or_else(|| invalid_tag_error("Tag key cannot be empty"))?;
if key.len() > 128 {
return Err(invalid_tag_error("Tag key is too long, maximum allowed length is 128 characters"));
}
let value = tag
.value
.as_deref()
.ok_or_else(|| invalid_tag_error("Tag value cannot be null"))?;
if value.len() > 256 {
return Err(invalid_tag_error("Tag value is too long, maximum allowed length is 256 characters"));
}
if !tag_keys.insert(key) {
return Err(invalid_tag_error("Cannot provide multiple Tags with the same key"));
}
}
Ok(())
}
pub(crate) fn build_get_bucket_tagging_output(tag_set: Vec<Tag>) -> GetBucketTaggingOutput {
GetBucketTaggingOutput { tag_set }
}
pub(crate) fn build_get_object_tagging_output(tag_set: Vec<Tag>, version_id: Option<String>) -> GetObjectTaggingOutput {
GetObjectTaggingOutput { tag_set, version_id }
}
pub(crate) fn build_put_object_tagging_output(version_id: Option<String>) -> PutObjectTaggingOutput {
PutObjectTaggingOutput { version_id }
}
pub(crate) fn build_delete_object_tagging_output(version_id: Option<String>) -> DeleteObjectTaggingOutput {
DeleteObjectTaggingOutput { version_id }
}
#[cfg(test)]
mod tests {
use super::{
build_delete_object_tagging_output, build_get_bucket_tagging_output, build_get_object_tagging_output,
build_put_object_tagging_output, validate_object_tag_set,
};
use s3s::S3ErrorCode;
use s3s::dto::Tag;
fn tag(key: Option<&str>, value: Option<&str>) -> Tag {
Tag {
key: key.map(ToOwned::to_owned),
value: value.map(ToOwned::to_owned),
}
}
#[test]
fn test_validate_object_tag_set_accepts_valid_tag_set() {
let tag_set = vec![tag(Some("k1"), Some("v1")), tag(Some("k2"), Some(""))];
let result = validate_object_tag_set(&tag_set);
assert!(result.is_ok());
}
#[test]
fn test_validate_object_tag_set_rejects_more_than_ten_tags() {
let tag_set: Vec<Tag> = (0..11).map(|i| tag(Some(&format!("k{i}")), Some(&format!("v{i}")))).collect();
let err = validate_object_tag_set(&tag_set).expect_err("tag set with more than ten tags must be rejected");
assert_eq!(*err.code(), S3ErrorCode::InvalidTag);
assert!(err.to_string().contains("Cannot have more than 10 tags per object"));
}
#[test]
fn test_validate_object_tag_set_rejects_empty_tag_key() {
let err = validate_object_tag_set(&[tag(Some(""), Some("v1"))]).expect_err("empty tag key must be rejected");
assert_eq!(*err.code(), S3ErrorCode::InvalidTag);
assert!(err.to_string().contains("Tag key cannot be empty"));
}
#[test]
fn test_validate_object_tag_set_rejects_too_long_tag_key() {
let long_key = "k".repeat(129);
let err = validate_object_tag_set(&[tag(Some(&long_key), Some("v1"))]).expect_err("too long tag key must be rejected");
assert_eq!(*err.code(), S3ErrorCode::InvalidTag);
assert!(
err.to_string()
.contains("Tag key is too long, maximum allowed length is 128 characters")
);
}
#[test]
fn test_validate_object_tag_set_rejects_null_tag_value() {
let err = validate_object_tag_set(&[tag(Some("k1"), None)]).expect_err("null tag value must be rejected");
assert_eq!(*err.code(), S3ErrorCode::InvalidTag);
assert!(err.to_string().contains("Tag value cannot be null"));
}
#[test]
fn test_validate_object_tag_set_rejects_too_long_tag_value() {
let long_value = "v".repeat(257);
let err =
validate_object_tag_set(&[tag(Some("k1"), Some(&long_value))]).expect_err("too long tag value must be rejected");
assert_eq!(*err.code(), S3ErrorCode::InvalidTag);
assert!(
err.to_string()
.contains("Tag value is too long, maximum allowed length is 256 characters")
);
}
#[test]
fn test_validate_object_tag_set_rejects_duplicate_tag_key() {
let err = validate_object_tag_set(&[tag(Some("k1"), Some("v1")), tag(Some("k1"), Some("v2"))])
.expect_err("duplicate tag key must be rejected");
assert_eq!(*err.code(), S3ErrorCode::InvalidTag);
assert!(err.to_string().contains("Cannot provide multiple Tags with the same key"));
}
#[test]
fn test_build_tagging_outputs_preserve_fields() {
let tag_set = vec![tag(Some("k1"), Some("v1"))];
let version_id = Some("vid-1".to_string());
let bucket_output = build_get_bucket_tagging_output(tag_set.clone());
let get_object_output = build_get_object_tagging_output(tag_set.clone(), version_id.clone());
let put_object_output = build_put_object_tagging_output(version_id.clone());
let delete_object_output = build_delete_object_tagging_output(version_id.clone());
assert_eq!(bucket_output.tag_set, tag_set);
assert_eq!(get_object_output.tag_set, vec![tag(Some("k1"), Some("v1"))]);
assert_eq!(get_object_output.version_id, version_id);
assert_eq!(put_object_output.version_id, Some("vid-1".to_string()));
assert_eq!(delete_object_output.version_id, Some("vid-1".to_string()));
}
}