diff --git a/crates/e2e_test/src/version_id_regression_test.rs b/crates/e2e_test/src/version_id_regression_test.rs index 44d9acc90..5833a84ce 100644 --- a/crates/e2e_test/src/version_id_regression_test.rs +++ b/crates/e2e_test/src/version_id_regression_test.rs @@ -67,6 +67,22 @@ mod tests { Ok(()) } + async fn suspend_versioning(client: &Client, bucket: &str) -> Result<(), Box> { + let versioning_config = VersioningConfiguration::builder() + .status(BucketVersioningStatus::Suspended) + .build(); + + client + .put_bucket_versioning() + .bucket(bucket) + .versioning_configuration(versioning_config) + .send() + .await?; + + info!("โœ… Versioning suspended for bucket {}", bucket); + Ok(()) + } + /// Test 1: PutObject should return version_id when versioning is enabled /// This directly addresses the Veeam issue from #1066 #[tokio::test] @@ -437,4 +453,49 @@ mod tests { Ok(()) } + + /// Test 7: PutObject should return "null" version_id when versioning is Suspended + #[tokio::test] + #[serial] + async fn test_put_object_returns_null_version_id_with_suspended_versioning() { + init_logging(); + info!("๐Ÿงช TEST: PutObject returns null version_id with versioning suspended"); + + let mut env = RustFSTestEnvironment::new().await.expect("Failed to create test environment"); + env.start_rustfs_server(vec![]).await.expect("Failed to start RustFS"); + + let client = create_s3_client(&env); + let bucket = "test-suspended-version-id"; + + create_bucket(&client, bucket).await.expect("Failed to create bucket"); + suspend_versioning(&client, bucket) + .await + .expect("Failed to suspend versioning"); + + let key = "test-file-suspended.txt"; + let content = b"Test content for suspended version ID test"; + + info!("๐Ÿ“ค Uploading object to suspended versioning bucket"); + let result = client + .put_object() + .bucket(bucket) + .key(key) + .body(ByteStream::from_static(content)) + .send() + .await; + + assert!(result.is_ok(), "PutObject failed: {:?}", result.err()); + let output = result.unwrap(); + + info!("๐Ÿ“ฅ PutObject response - version_id: {:?}", output.version_id); + + // When suspended, version_id must be "null" + assert_eq!( + output.version_id.as_deref(), + Some("null"), + "โŒ FAILED: version_id should be 'null' when versioning is suspended" + ); + + info!("โœ… PASSED: PutObject correctly returns 'null' version_id"); + } } diff --git a/crates/ecstore/src/bucket/versioning/mod.rs b/crates/ecstore/src/bucket/versioning/mod.rs index 05f5b22e9..29080592f 100644 --- a/crates/ecstore/src/bucket/versioning/mod.rs +++ b/crates/ecstore/src/bucket/versioning/mod.rs @@ -36,10 +36,7 @@ impl VersioningApi for VersioningConfiguration { return true; } - if let Some(exclude_folders) = self.exclude_folders - && exclude_folders - && prefix.ends_with('/') - { + if self.exclude_folders.unwrap_or(false) && prefix.ends_with('/') { return false; } diff --git a/rustfs/src/storage/ecfs.rs b/rustfs/src/storage/ecfs.rs index c6a20ff80..3e25675f2 100644 --- a/rustfs/src/storage/ecfs.rs +++ b/rustfs/src/storage/ecfs.rs @@ -4859,7 +4859,10 @@ impl S3 for FS { let manager = get_concurrency_manager(); let put_bucket = bucket.clone(); let put_key = key.clone(); - let put_version = obj_info.version_id.map(|v| v.to_string()); + let mut put_version = obj_info.version_id.map(|v| v.to_string()); + if opts.version_suspended && obj_info.version_id.is_none_or(|v| v.is_nil()) { + put_version = Some("null".to_string()); + } helper = helper.object(obj_info.clone()); if let Some(version_id) = &put_version {