fix(ilm): accept null version transition sources (#5069)

* fix(ilm): accept null version transition sources

Treat nil/null source version IDs as null-version transition sources when building transition transaction source identity, while preserving fail-closed validation for truly missing IDs on versioned sources.

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(ilm): preserve multipart source versions

Propagate bucket versioning into CompleteMultipartUpload, assign a concrete version ID for versioned multipart completions, and classify unversioned FileInfo sources as null-version transition sources.

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(ilm): normalize multipart completion versions

Treat nil staged version IDs as missing for versioned multipart completion and clear staged version IDs when completion publishes a null-version object under suspended or unversioned semantics.

Co-Authored-By: heihutu <heihutu@gmail.com>

---------

Co-authored-by: heihutu <heihutu@gmail.com>
This commit is contained in:
houseme
2026-07-21 13:18:45 +08:00
committed by GitHub
parent f32597bdb0
commit 75381d4ffe
4 changed files with 288 additions and 18 deletions
@@ -836,9 +836,13 @@ async fn complete_multipart_upload_transitions_immediately_via_usecase() {
.build()
.unwrap();
Box::pin(usecase.execute_complete_multipart_upload(build_request(complete_input, Method::POST)))
let complete_output = Box::pin(usecase.execute_complete_multipart_upload(build_request(complete_input, Method::POST)))
.await
.expect("Failed to complete multipart upload through usecase");
assert!(
complete_output.output.version_id.is_some(),
"versioned CompleteMultipartUpload should return the created version ID"
);
let info = wait_for_transition(&ecstore, bucket.as_str(), object, TRANSITION_WAIT_TIMEOUT)
.await
+6 -7
View File
@@ -420,6 +420,10 @@ impl DefaultMultipartUsecase {
let Some(multipart_upload) = multipart_upload else { return Err(s3_error!(InvalidPart)) };
let mut opts = get_complete_multipart_upload_opts(&req.headers).map_err(ApiError::from)?;
let versioned = BucketVersioningSys::prefix_enabled(&bucket, &key).await;
let version_suspended = BucketVersioningSys::prefix_suspended(&bucket, &key).await;
opts.versioned = versioned;
opts.version_suspended = version_suspended;
let capacity_scope_token = Uuid::new_v4();
opts.capacity_scope_token = Some(capacity_scope_token);
@@ -512,8 +516,7 @@ impl DefaultMultipartUsecase {
));
}
// Update quota tracking after successful multipart upload
let mpu_versioned = BucketVersioningSys::prefix_enabled(&bucket, &key).await;
if mpu_versioned {
if versioned {
record_bucket_object_version_write_memory(&bucket, previous_current_size, obj_info.size.max(0) as u64)
.await;
} else {
@@ -529,11 +532,7 @@ impl DefaultMultipartUsecase {
enqueue_transition_immediate(&obj_info, LcEventSrc::S3CompleteMultipartUpload).await;
let raw_mpu_version = obj_info.version_id.map(|v| v.to_string());
let mpu_version = if BucketVersioningSys::prefix_enabled(&bucket, &key).await {
raw_mpu_version.clone()
} else {
None
};
let mpu_version = if versioned { raw_mpu_version.clone() } else { None };
let mpu_version_for_event = mpu_version.clone();
// checksum: stored (decrypted) values take precedence over the request input;
// additional algorithms (XXHash3/64/128, SHA-512, MD5), which have no typed