fix:handle null version ID in delete and return version_id in get_object (#1479)

Signed-off-by: houseme <housemecn@gmail.com>
Co-authored-by: houseme <housemecn@gmail.com>
This commit is contained in:
LeonWang0735
2026-01-12 22:02:09 +08:00
committed by GitHub
parent bec51bb783
commit 5f2e594480
2 changed files with 45 additions and 7 deletions

View File

@@ -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()
};

View File

@@ -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();