mirror of
https://github.com/rustfs/rustfs.git
synced 2026-03-17 14:24:08 +00:00
refactor(storage): extract tagging helpers from ecfs (#1881)
This commit is contained in:
@@ -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.
|
||||
// Reference:https://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());
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
166
rustfs/src/storage/s3_api/tagging.rs
Normal file
166
rustfs/src/storage/s3_api/tagging.rs
Normal 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()));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user