From 5f2e594480e30180b611cc5622273ce3bc17782f Mon Sep 17 00:00:00 2001 From: LeonWang0735 Date: Mon, 12 Jan 2026 22:02:09 +0800 Subject: [PATCH] fix:handle null version ID in delete and return version_id in get_object (#1479) Signed-off-by: houseme Co-authored-by: houseme --- rustfs/src/storage/ecfs.rs | 18 ++++++++++++++++++ rustfs/src/storage/options.rs | 34 +++++++++++++++++++++++++++------- 2 files changed, 45 insertions(+), 7 deletions(-) diff --git a/rustfs/src/storage/ecfs.rs b/rustfs/src/storage/ecfs.rs index 5df5821d..35146ff3 100644 --- a/rustfs/src/storage/ecfs.rs +++ b/rustfs/src/storage/ecfs.rs @@ -2491,6 +2491,23 @@ impl S3 for FS { } } + let versioned = BucketVersioningSys::prefix_enabled(&bucket, &key).await; + + // Get version_id from object info + // If versioning is enabled and version_id exists in object info, return it + // If version_id is Uuid::nil(), return "null" string (AWS S3 convention) + let output_version_id = if versioned { + info.version_id.map(|vid| { + if vid == Uuid::nil() { + "null".to_string() + } else { + vid.to_string() + } + }) + } else { + None + }; + let output = GetObjectOutput { body, content_length: Some(response_content_length), @@ -2511,6 +2528,7 @@ impl S3 for FS { checksum_sha256, checksum_crc64nvme, checksum_type, + version_id: output_version_id, ..Default::default() }; diff --git a/rustfs/src/storage/options.rs b/rustfs/src/storage/options.rs index e0247373..49592850 100644 --- a/rustfs/src/storage/options.rs +++ b/rustfs/src/storage/options.rs @@ -65,13 +65,23 @@ pub async fn del_opts( let vid = vid.map(|v| v.as_str().trim().to_owned()); - if let Some(ref id) = vid - && *id != Uuid::nil().to_string() - && let Err(err) = Uuid::parse_str(id.as_str()) - { - error!("del_opts: invalid version id: {} error: {}", id, err); - return Err(StorageError::InvalidVersionID(bucket.to_owned(), object.to_owned(), id.clone())); - } + // Handle AWS S3 special case: "null" string represents null version ID + // When VersionId='null' is specified, it means delete the object with null version ID + let vid = if let Some(ref id) = vid { + if id.eq_ignore_ascii_case("null") { + // Convert "null" to Uuid::nil() string representation + Some(Uuid::nil().to_string()) + } else { + // Validate UUID format for other version IDs + if *id != Uuid::nil().to_string() && Uuid::parse_str(id.as_str()).is_err() { + error!("del_opts: invalid version id: {} error: invalid UUID format", id); + return Err(StorageError::InvalidVersionID(bucket.to_owned(), object.to_owned(), id.clone())); + } + Some(id.clone()) + } + } else { + None + }; let mut opts = put_opts_from_headers(headers, metadata.clone()).map_err(|err| { error!("del_opts: invalid argument: {} error: {}", object, err); @@ -704,6 +714,16 @@ mod tests { assert!(!opts.delete_prefix); } + #[tokio::test] + async fn test_del_opts_with_null_version_id() { + let headers = create_test_headers(); + let metadata = create_test_metadata(); + let result = del_opts("test-bucket", "test-object", Some("null".to_string()), &headers, metadata.clone()).await; + assert!(result.is_ok()); + let result = del_opts("test-bucket", "test-object", Some("NULL".to_string()), &headers, metadata.clone()).await; + assert!(result.is_ok()); + } + #[tokio::test] async fn test_get_opts_basic() { let headers = create_test_headers();