From 5a768d3a4459ab67eee85c1bfd43b3de187ff67c Mon Sep 17 00:00:00 2001 From: Zhengchao An Date: Tue, 28 Jul 2026 06:33:17 +0800 Subject: [PATCH] perf(s3): resolve bucket versioning once per DeleteObjects request (#5340) DeleteObjects rebuilt ObjectOptions inside its per-key phase-1 loop, and del_opts re-fetched the bucket versioning configuration on every call: a 1000-key request paid 1001 identical metadata-sys lookups, each taking the global bucket-metadata read lock and cloning a VersioningConfiguration. Split del_opts into the existing async entry point plus a synchronous del_opts_with_versioning that takes an already-resolved configuration, and reuse the request-level version_cfg the handler had already fetched. This also removes a snapshot inconsistency. The skip-stat decision (can_skip_delete_objects_pre_stat) and the post-delete accounting already derived from the request-level snapshot while del_opts re-read the config per key, so a PutBucketVersioning committing mid-request could give a key opts.versioned=true while the batch delete still ran with versioned=false: the object was deleted outright with replication state attached to a delete marker that was never created, and the advisory object-lock pre-check was skipped on a false premise. All three now derive from one snapshot. GET, HEAD and DeleteObject additionally establish bucket existence before building ObjectOptions, so a request naming a nonexistent bucket no longer performs bucket-metadata work first. GET keeps its cheap request-shape validations (key, range, partNumber) ahead of the existence check so InvalidArgument still wins over NoSuchBucket for malformed requests. Bucket validation moves to validate_bucket_exists, which takes an explicit store and shares the existing 5s-TTL cache. GET, PUT, HEAD and DeleteObject now resolve the store through the request-bound server context (backlog#1052 S6) instead of the process-global handle: get_validated_store resolves the first-published global AppContext, so in an embedded multi-instance process a request to the second server operated on the first server's store. Remaining global-handle call sites in bucket_usecase and select_object are left for a follow-up. --- .../src/app/lifecycle_transition_api_test.rs | 142 +++++++++++++++++ rustfs/src/app/object_usecase.rs | 150 ++++++++++++++---- rustfs/src/app/storage_api.rs | 16 +- rustfs/src/storage/ecfs_extend.rs | 14 +- rustfs/src/storage/options.rs | 30 +++- rustfs/src/storage/storage_api.rs | 14 +- 6 files changed, 319 insertions(+), 47 deletions(-) diff --git a/rustfs/src/app/lifecycle_transition_api_test.rs b/rustfs/src/app/lifecycle_transition_api_test.rs index 6c9fa43e2..8ca2e2a93 100644 --- a/rustfs/src/app/lifecycle_transition_api_test.rs +++ b/rustfs/src/app/lifecycle_transition_api_test.rs @@ -2388,3 +2388,145 @@ async fn object_lock_handlers_schedule_replication() { legal hold never replicates and the peer copy stays deletable" ); } + +/// Regression pin for the DeleteObjects hot path: the bucket's versioning +/// configuration must be resolved exactly once per request — not once per +/// key, which used to cost up to 1000 identical metadata lookups per call. +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +#[serial] +#[ignore = "global-state integration test: runs serialized in the CI ILM Integration (serial) lane, see ci.yml test-ilm-integration-serial"] +async fn delete_objects_resolves_bucket_versioning_once_per_request() { + use super::storage_api::test::{ReqInfo, VERSIONING_CONFIG_LOOKUPS}; + use std::sync::atomic::Ordering; + + let (_disk_paths, ecstore) = setup_test_env().await; + let usecase = DefaultObjectUsecase::from_global(); + + let bucket = format!("test-delobjs-vers-once-{}", &Uuid::new_v4().simple().to_string()[..8]); + create_test_bucket(&ecstore, bucket.as_str()).await; + + let keys = ["batch/a.txt", "batch/b.txt", "batch/c.txt"]; + for key in keys { + upload_test_object(&ecstore, bucket.as_str(), key, b"delete objects versioning payload").await; + } + + let input = DeleteObjectsInput::builder() + .bucket(bucket.clone()) + .delete(Delete { + objects: keys + .iter() + .map(|key| ObjectIdentifier { + key: key.to_string(), + version_id: None, + ..Default::default() + }) + .collect(), + quiet: None, + }) + .build() + .expect("delete objects input should build"); + + let mut req = build_request(input, Method::POST); + req.extensions.insert(ReqInfo { + cred: Some(rustfs_credentials::Credentials::default()), + is_owner: true, + ..Default::default() + }); + + let lookups_before = VERSIONING_CONFIG_LOOKUPS.load(Ordering::SeqCst); + let response = usecase + .execute_delete_objects(req) + .await + .expect("delete objects should succeed"); + + let output = response.output; + let errors = output.errors.unwrap_or_default(); + assert!(errors.is_empty(), "DeleteObjects reported per-key errors: {errors:?}"); + assert_eq!( + output.deleted.unwrap_or_default().len(), + keys.len(), + "every requested key should be deleted" + ); + + let lookups = VERSIONING_CONFIG_LOOKUPS.load(Ordering::SeqCst) - lookups_before; + assert_eq!( + lookups, + 1, + "DeleteObjects must resolve bucket versioning exactly once per request; got {lookups} lookups for {} keys", + keys.len() + ); +} + +/// Regression pin for the nonexistent-bucket fast path: GET/HEAD/DeleteObject +/// must decide NoSuchBucket from the bucket-existence check, before any bucket +/// versioning lookup runs — and cheap request-shape validation still wins over +/// bucket existence for GET. +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +#[serial] +#[ignore = "global-state integration test: runs serialized in the CI ILM Integration (serial) lane, see ci.yml test-ilm-integration-serial"] +async fn nonexistent_bucket_fails_before_versioning_lookup_on_get_head_delete() { + use super::storage_api::test::VERSIONING_CONFIG_LOOKUPS; + use std::sync::atomic::Ordering; + + let (_disk_paths, _ecstore) = setup_test_env().await; + let usecase = DefaultObjectUsecase::from_global(); + + // Never created. + let bucket = format!("test-missing-{}", &Uuid::new_v4().simple().to_string()[..8]); + + // Request-shape validation keeps precedence over bucket existence: GET with + // both range and partNumber is InvalidArgument even on a missing bucket. + let get_invalid = GetObjectInput::builder() + .bucket(bucket.clone()) + .key("k.txt".to_string()) + .range(Some(Range::Int { first: 0, last: Some(1) })) + .part_number(Some(1)) + .build() + .expect("get input should build"); + let err = Box::pin(usecase.execute_get_object(build_request(get_invalid, Method::GET))) + .await + .expect_err("range+partNumber must be rejected"); + assert_eq!(err.code(), &s3s::S3ErrorCode::InvalidArgument); + + let lookups_before = VERSIONING_CONFIG_LOOKUPS.load(Ordering::SeqCst); + + let get = GetObjectInput::builder() + .bucket(bucket.clone()) + .key("k.txt".to_string()) + .build() + .expect("get input should build"); + let err = Box::pin(usecase.execute_get_object(build_request(get, Method::GET))) + .await + .expect_err("GET on a missing bucket must fail"); + assert_eq!(err.code(), &s3s::S3ErrorCode::NoSuchBucket); + + // Bucket existence beats versionId-format validation (which lives in the + // opts builders) for HEAD and DeleteObject. + let head = HeadObjectInput::builder() + .bucket(bucket.clone()) + .key("k.txt".to_string()) + .version_id(Some("not-a-uuid".to_string())) + .build() + .expect("head input should build"); + let err = Box::pin(usecase.execute_head_object(build_request(head, Method::HEAD))) + .await + .expect_err("HEAD on a missing bucket must fail"); + assert_eq!(err.code(), &s3s::S3ErrorCode::NoSuchBucket); + + let del = DeleteObjectInput::builder() + .bucket(bucket.clone()) + .key("k.txt".to_string()) + .version_id(Some("not-a-uuid".to_string())) + .build() + .expect("delete input should build"); + let err = Box::pin(usecase.execute_delete_object(build_request(del, Method::DELETE))) + .await + .expect_err("DeleteObject on a missing bucket must fail"); + assert_eq!(err.code(), &s3s::S3ErrorCode::NoSuchBucket); + + let lookups = VERSIONING_CONFIG_LOOKUPS.load(Ordering::SeqCst) - lookups_before; + assert_eq!( + lookups, 0, + "requests naming a nonexistent bucket must fail before any bucket versioning lookup; got {lookups}" + ); +} diff --git a/rustfs/src/app/object_usecase.rs b/rustfs/src/app/object_usecase.rs index 493ae672a..e5b1d18d6 100644 --- a/rustfs/src/app/object_usecase.rs +++ b/rustfs/src/app/object_usecase.rs @@ -81,9 +81,9 @@ use super::storage_api::object_usecase::object_cache::lookup_get_object_body_cac use super::storage_api::object_usecase::object_cache::{GetObjectBodyCacheHookLookup, get_object_body_cache_plaintext_len}; use super::storage_api::object_usecase::object_utils::to_s3s_etag; use super::storage_api::object_usecase::options::{ - copy_dst_opts, copy_src_opts, del_opts, extract_metadata, extract_metadata_from_mime_with_object_name, - filter_object_metadata, get_content_sha256_with_query, get_opts, namespace_reserved_user_metadata, - normalize_content_encoding_for_storage, put_opts, validate_archive_content_encoding, + bucket_versioning_config, copy_dst_opts, copy_src_opts, del_opts, del_opts_with_versioning, extract_metadata, + extract_metadata_from_mime_with_object_name, filter_object_metadata, get_content_sha256_with_query, get_opts, + namespace_reserved_user_metadata, normalize_content_encoding_for_storage, put_opts, validate_archive_content_encoding, }; use super::storage_api::object_usecase::request_context::{self, spawn_traced}; use super::storage_api::object_usecase::s3_api::multipart::parse_list_parts_params; @@ -99,9 +99,9 @@ use super::storage_api::object_usecase::storage_class as storageclass; use super::storage_api::object_usecase::timeout_wrapper::{GetObjectTimeoutPolicy, RequestTimeoutWrapper}; use super::storage_api::object_usecase::{ECStore, OldCurrentSize}; use super::storage_api::object_usecase::{ - RFC1123, check_preconditions, get_validated_store, has_replication_rules, parse_object_lock_legal_hold, - parse_object_lock_retention, parse_part_number_i32_to_usize, remove_object_lock_metadata_for_copy, - strip_managed_encryption_metadata, validate_bucket_object_lock_enabled, validate_object_key, validate_sse_headers_for_read, + RFC1123, check_preconditions, has_replication_rules, parse_object_lock_legal_hold, parse_object_lock_retention, + parse_part_number_i32_to_usize, remove_object_lock_metadata_for_copy, strip_managed_encryption_metadata, + validate_bucket_exists, validate_bucket_object_lock_enabled, validate_object_key, validate_sse_headers_for_read, validate_sse_headers_for_write, validate_ssec_for_read, wrap_response_with_cors, }; use crate::app::runtime_sources::{ @@ -609,6 +609,16 @@ struct GetObjectRequestContext { opts: ObjectOptions, } +/// Request fields that passed the cheap GET validations, ready for the +/// bucket-metadata work in [`DefaultObjectUsecase::prepare_get_object_request_context`]. +struct GetObjectValidatedRequest { + bucket: String, + key: String, + version_id: Option, + part_number: Option, + rs: Option, +} + struct GetObjectReadSetup { info: ObjectInfo, final_stream: DynReader, @@ -3552,7 +3562,9 @@ impl DefaultObjectUsecase { } } - async fn prepare_get_object_request_context(req: &S3Request) -> S3Result { + /// Cheap request-shape validations, run before the bucket-existence store + /// lookup so invalid requests keep their InvalidArgument precedence. + fn validate_get_object_request(req: &S3Request) -> S3Result { // Clone only the fields this path needs instead of the whole input. let bucket = req.input.bucket.clone(); let key = req.input.key.clone(); @@ -3570,7 +3582,28 @@ impl DefaultObjectUsecase { return Err(s3_error!(InvalidArgument, "range and part_number invalid")); } - let opts: ObjectOptions = get_opts(&bucket, &key, version_id.clone(), part_number, &req.headers) + Ok(GetObjectValidatedRequest { + bucket, + key, + version_id, + part_number, + rs, + }) + } + + async fn prepare_get_object_request_context( + validated: GetObjectValidatedRequest, + headers: &HeaderMap, + ) -> S3Result { + let GetObjectValidatedRequest { + bucket, + key, + version_id, + part_number, + rs, + } = validated; + + let opts: ObjectOptions = get_opts(&bucket, &key, version_id.clone(), part_number, headers) .await .map_err(ApiError::from)?; @@ -3588,6 +3621,7 @@ impl DefaultObjectUsecase { &self, req: &S3Request, manager: &'static ConcurrencyManager, + store: Arc, wrapper: &RequestTimeoutWrapper, timeout_config: &GetObjectTimeoutPolicy, bucket: &str, @@ -3596,17 +3630,6 @@ impl DefaultObjectUsecase { opts: &ObjectOptions, part_number: Option, ) -> S3Result { - // SF05: Store lookup first (cached via SF01 moka cache). - let store_lookup_start = rustfs_io_metrics::get_stage_metrics_enabled().then(std::time::Instant::now); - let store = get_validated_store(bucket).await?; - if let Some(store_lookup_start) = store_lookup_start { - rustfs_io_metrics::record_get_object_stage_duration( - "s3_handler", - "store_lookup", - store_lookup_start.elapsed().as_secs_f64(), - ); - } - let read_start = std::time::Instant::now(); let read_stage_start = rustfs_io_metrics::get_stage_metrics_enabled().then_some(read_start); let cache_adapter = self.object_data_cache(); @@ -4730,7 +4753,13 @@ impl DefaultObjectUsecase { use_large_put_concurrency_tuning, ); - let store = get_validated_store(&bucket).await?; + // Resolve the store through the request-bound server context + // (backlog#1052 S6), not the process-global handle, so an embedded + // second server never writes into the first server's store. + let Some(store) = self.object_store() else { + return Err(S3Error::with_message(S3ErrorCode::InternalError, "Not init".to_string())); + }; + validate_bucket_exists(&store, &bucket).await?; let bucket_sse_config = metadata_sys::get_sse_config(&bucket).await.ok(); debug!( @@ -5513,8 +5542,40 @@ impl DefaultObjectUsecase { let helper = OperationHelper::new(&req, EventName::ObjectAccessedGet, S3Operation::GetObject).suppress_event(); // mc get 3 + // Cheap request-shape validations run first so invalid requests keep + // their InvalidArgument precedence over bucket existence. + let validated = match Self::validate_get_object_request(&req) { + Ok(validated) => validated, + Err(err) => { + lifecycle.finish_err(); + return Err(err); + } + }; + + // SF05: Store lookup next (5s-TTL bucket-validation cache). Bucket + // existence is established before any bucket-metadata work, so requests + // naming nonexistent buckets fail before the versioning lookup in + // get_opts. The store comes from the request-bound server context + // (backlog#1052 S6), not the process-global handle. + let store_lookup_start = rustfs_io_metrics::get_stage_metrics_enabled().then(std::time::Instant::now); + let Some(store) = self.object_store() else { + lifecycle.finish_err(); + return Err(S3Error::with_message(S3ErrorCode::InternalError, "Not init".to_string())); + }; + if let Err(err) = validate_bucket_exists(&store, &req.input.bucket).await { + lifecycle.finish_err(); + return Err(err); + } + if let Some(store_lookup_start) = store_lookup_start { + rustfs_io_metrics::record_get_object_stage_duration( + "s3_handler", + "store_lookup", + store_lookup_start.elapsed().as_secs_f64(), + ); + } + let request_context_start = rustfs_io_metrics::get_stage_metrics_enabled().then(std::time::Instant::now); - let request_context = match Self::prepare_get_object_request_context(&req).await { + let request_context = match Self::prepare_get_object_request_context(validated, &req.headers).await { Ok(request_context) => request_context, Err(err) => { lifecycle.finish_err(); @@ -5540,7 +5601,18 @@ impl DefaultObjectUsecase { let manager = get_concurrency_manager(); let prepared_read = match self - .prepare_get_object_read_execution(&req, manager, &wrapper, &timeout_config, &bucket, &key, rs, &opts, part_number) + .prepare_get_object_read_execution( + &req, + manager, + store, + &wrapper, + &timeout_config, + &bucket, + &key, + rs, + &opts, + part_number, + ) .await { Ok(prepared_read) => prepared_read, @@ -6474,7 +6546,7 @@ impl DefaultObjectUsecase { return Err(S3Error::with_message(S3ErrorCode::InternalError, "Not init".to_string())); }; - let version_cfg = BucketVersioningSys::get(&bucket).await.unwrap_or_default(); + let version_cfg = bucket_versioning_config(&bucket).await; let bypass_governance = has_bypass_governance_header(&req.headers); let bucket_lock_enabled = bucket_object_locking_enabled(&bucket).await; @@ -6561,14 +6633,16 @@ impl DefaultObjectUsecase { }; let metadata = extract_metadata(&req.headers); - let opts: ObjectOptions = del_opts( + // Reuse the request-level versioning config fetched above instead of + // re-resolving it per key (up to 1000 identical lookups per request). + let opts: ObjectOptions = del_opts_with_versioning( &bucket, &object.object_name, object.version_id.map(|f| f.to_string()), &req.headers, metadata, + &version_cfg, ) - .await .map_err(ApiError::from)?; // backlog#929 (HP-8): the accounting branch after the store delete @@ -6915,6 +6989,16 @@ impl DefaultObjectUsecase { authorize_request(&mut req, Action::S3Action(S3Action::ReplicateDeleteAction)).await?; } + // Establish bucket existence before any bucket-metadata work (matches + // PUT/GET): nonexistent buckets fail here instead of paying the + // versioning lookups in del_opts/get_opts first. Resolve the store + // through the request-bound server context (backlog#1052 S6), not the + // process-global handle. + let Some(store) = self.object_store() else { + return Err(S3Error::with_message(S3ErrorCode::InternalError, "Not init".to_string())); + }; + validate_bucket_exists(&store, &bucket).await?; + let metadata = extract_metadata(&req.headers); // Clone version_id before it's moved let version_id_clone = version_id.clone(); @@ -6952,9 +7036,6 @@ impl DefaultObjectUsecase { )); } validate_undo_delete_version(expected_current_version_id.as_deref(), opts.version_id.as_deref())?; - let Some(store) = self.object_store() else { - return Err(S3Error::with_message(S3ErrorCode::InternalError, "Not init".to_string())); - }; opts.expected_current_version_id = expected_current_version_id.clone(); let replicate_force_delete = force_delete @@ -7176,13 +7257,20 @@ impl DefaultObjectUsecase { return Err(s3_error!(InvalidArgument, "range and part_number invalid")); } + // Establish bucket existence before any bucket-metadata work (matches + // PUT/GET): nonexistent buckets fail here instead of paying the + // versioning lookup in get_opts first. Resolve the store through the + // request-bound server context (backlog#1052 S6), not the + // process-global handle. + let Some(store) = self.object_store() else { + return Err(S3Error::with_message(S3ErrorCode::InternalError, "Not init".to_string())); + }; + validate_bucket_exists(&store, &bucket).await?; + let opts: ObjectOptions = get_opts(&bucket, &key, version_id, part_number, &req.headers) .await .map_err(ApiError::from)?; - let Some(store) = self.object_store() else { - return Err(S3Error::with_message(S3ErrorCode::InternalError, "Not init".to_string())); - }; // Modification Points: Explicitly handles get_object_info errors, distinguishing between object absence and other errors let info = match store.get_object_info(&bucket, &key, &opts).await { Ok(info) => info, diff --git a/rustfs/src/app/storage_api.rs b/rustfs/src/app/storage_api.rs index 88f1b491c..787a630f1 100644 --- a/rustfs/src/app/storage_api.rs +++ b/rustfs/src/app/storage_api.rs @@ -865,11 +865,13 @@ pub(crate) mod io { } pub(crate) mod options { + #[cfg(test)] + pub(crate) use crate::storage::storage_api::options_consumer::VERSIONING_CONFIG_LOOKUPS; pub(crate) use crate::storage::storage_api::options_consumer::{ - copy_dst_opts, copy_src_opts, del_opts, extract_metadata, extract_metadata_from_mime, - extract_metadata_from_mime_with_object_name, filter_object_metadata, get_complete_multipart_upload_opts, - get_content_sha256_with_query, get_opts, namespace_reserved_user_metadata, normalize_content_encoding_for_storage, - parse_copy_source_range, put_opts, validate_archive_content_encoding, + bucket_versioning_config, copy_dst_opts, copy_src_opts, del_opts, del_opts_with_versioning, extract_metadata, + extract_metadata_from_mime, extract_metadata_from_mime_with_object_name, filter_object_metadata, + get_complete_multipart_upload_opts, get_content_sha256_with_query, get_opts, namespace_reserved_user_metadata, + normalize_content_encoding_for_storage, parse_copy_source_range, put_opts, validate_archive_content_encoding, }; } @@ -993,8 +995,8 @@ pub(crate) mod object_usecase { pub(crate) use crate::storage::storage_api::{ ECStore, GetObjectReader, OldCurrentSize, RFC1123, StorageDeletedObject, StorageObjectInfo, StorageObjectLockDeleteOptions, StorageObjectOptions, StorageObjectToDelete, StoragePutObjReader, check_preconditions, - get_validated_store, has_replication_rules, parse_object_lock_legal_hold, parse_object_lock_retention, - parse_part_number_i32_to_usize, remove_object_lock_metadata_for_copy, strip_managed_encryption_metadata, + has_replication_rules, parse_object_lock_legal_hold, parse_object_lock_retention, parse_part_number_i32_to_usize, + remove_object_lock_metadata_for_copy, strip_managed_encryption_metadata, validate_bucket_exists, validate_bucket_object_lock_enabled, validate_object_key, validate_sse_headers_for_read, validate_sse_headers_for_write, validate_ssec_for_read, wrap_response_with_cors, }; @@ -1074,6 +1076,8 @@ pub(crate) mod test { } } + pub(crate) use super::access::ReqInfo; + pub(crate) use super::options::VERSIONING_CONFIG_LOOKUPS; pub(crate) use super::{bucket, data_usage, ecfs, object_utils, runtime}; pub(crate) use crate::storage::storage_api::{ ECStore, Endpoint, Endpoints, PoolEndpoints, StorageObjectInfo, StorageObjectOptions, StoragePutObjReader, diff --git a/rustfs/src/storage/ecfs_extend.rs b/rustfs/src/storage/ecfs_extend.rs index f7a045636..88d348231 100644 --- a/rustfs/src/storage/ecfs_extend.rs +++ b/rustfs/src/storage/ecfs_extend.rs @@ -843,11 +843,21 @@ pub(crate) async fn get_validated_store(bucket: &str) -> S3Result S3Result<()> { // Check cache — TTL is checked manually. if let Some(inserted_at) = cache_get(bucket) && inserted_at.elapsed() < BUCKET_VALIDATION_TTL { - return Ok(store); // Cache hit, skip validation + return Ok(()); // Cache hit, skip validation } // Cache miss or expired, perform validation @@ -859,7 +869,7 @@ pub(crate) async fn get_validated_store(bucket: &str) -> S3Result VersioningConfiguration { +pub(crate) async fn bucket_versioning_config(bucket: &str) -> VersioningConfiguration { + #[cfg(test)] + VERSIONING_CONFIG_LOOKUPS.fetch_add(1, std::sync::atomic::Ordering::Relaxed); match BucketVersioningSys::get(bucket).await { Ok(cfg) => cfg, Err(err) => { @@ -97,6 +109,20 @@ pub async fn del_opts( metadata: HashMap, ) -> Result { let versioning_cfg = bucket_versioning_config(bucket).await; + del_opts_with_versioning(bucket, object, vid, headers, metadata, &versioning_cfg) +} + +/// Like [`del_opts`], but derives versioning state from an already-fetched +/// configuration so batch callers (DeleteObjects) resolve the bucket's +/// versioning once per request instead of once per key. +pub fn del_opts_with_versioning( + bucket: &str, + object: &str, + vid: Option, + headers: &HeaderMap, + metadata: HashMap, + versioning_cfg: &VersioningConfiguration, +) -> Result { let versioned = versioning_cfg.prefix_enabled(object); let version_suspended = versioning_cfg.suspended(); @@ -126,7 +152,7 @@ pub async fn del_opts( None }; - let mut opts = put_opts_from_headers(headers, metadata.clone()).map_err(|err| { + let mut opts = put_opts_from_headers(headers, metadata).map_err(|err| { error!("del_opts: invalid argument: {} error: {}", object, err); StorageError::InvalidArgument(bucket.to_owned(), object.to_owned(), err.to_string()) })?; diff --git a/rustfs/src/storage/storage_api.rs b/rustfs/src/storage/storage_api.rs index fe0be925c..1892d29bf 100644 --- a/rustfs/src/storage/storage_api.rs +++ b/rustfs/src/storage/storage_api.rs @@ -91,8 +91,8 @@ pub(crate) use super::ecfs_extend::{ RFC1123, apply_bucket_default_lock_retention, apply_cors_headers, check_preconditions, get_buffer_size_opt_in, get_validated_store, has_replication_rules, parse_object_lock_legal_hold, parse_object_lock_retention, parse_part_number_i32_to_usize, process_lambda_configurations, process_queue_configurations, process_topic_configurations, - remove_object_lock_metadata_for_copy, validate_bucket_object_lock_enabled, validate_list_object_unordered_with_delimiter, - validate_object_key, wrap_response_with_cors, + remove_object_lock_metadata_for_copy, validate_bucket_exists, validate_bucket_object_lock_enabled, + validate_list_object_unordered_with_delimiter, validate_object_key, wrap_response_with_cors, }; pub(crate) use super::sse::{ DecryptionRequest, EncryptionRequest, PrepareEncryptionRequest, extract_server_side_encryption_from_headers, sse_decryption, @@ -180,11 +180,13 @@ pub(crate) mod helper_consumer { } pub(crate) mod options_consumer { + #[cfg(test)] + pub(crate) use super::super::options::VERSIONING_CONFIG_LOOKUPS; pub(crate) use super::super::options::{ - copy_dst_opts, copy_src_opts, del_opts, extract_metadata, extract_metadata_from_mime, - extract_metadata_from_mime_with_object_name, filter_object_metadata, get_complete_multipart_upload_opts, - get_content_sha256_with_query, get_opts, namespace_reserved_user_metadata, normalize_content_encoding_for_storage, - parse_copy_source_range, put_opts, validate_archive_content_encoding, + bucket_versioning_config, copy_dst_opts, copy_src_opts, del_opts, del_opts_with_versioning, extract_metadata, + extract_metadata_from_mime, extract_metadata_from_mime_with_object_name, filter_object_metadata, + get_complete_multipart_upload_opts, get_content_sha256_with_query, get_opts, namespace_reserved_user_metadata, + normalize_content_encoding_for_storage, parse_copy_source_range, put_opts, validate_archive_content_encoding, }; pub(crate) mod contract {