mirror of
https://github.com/rustfs/rustfs.git
synced 2026-09-02 10:18:10 +00:00
fix(s3): round-trip null-version delete-marker identity (#6765)
* fix(s3): round-trip null-version delete-marker identity through listing and delete responses On a versioning-suspended bucket, a null delete marker's identity was lost on the way back to the client at three points (issue #6745): ListObjectVersions advertised the marker's VersionId as the literal nil UUID instead of null; deleting by that id succeeded but the DeleteObjects/DeleteObject response reported the identity as null with no way to correlate it to the request; and the response lacked DeleteMarker/DeleteMarkerVersionId because the marker-ness comparison mixed the client-facing identity (Some(nil)) with the storage identity (None), so the removal also mis-recorded accounting and fired DeleteMarkerCreated semantics on later paths. - Listing (bucket_usecase, s3_api/bucket, build_list_versions_next_marker) now maps the synthesized nil UUID to the literal null everywhere it reaches the wire, and VersionMarker::parse folds a nil-UUID marker from older listings into VersionMarker::Null so pagination resumes correctly. - delete_objects normalizes both sides of the marker-ness comparison via delete_file_info_version_id (matching the adjacent explicit_delete_marker admission check) and reports DeleteMarkerVersionId as null for an explicit null-marker removal. - resolve_delete_version_state reports delete_marker for an explicit-version delete whose target is a delete marker even when the bucket is versioning-suspended, fixing x-amz-delete-marker on the single-object path. - The DeleteObjects response entry echoes the version identity the request addressed for marker removals, marker-removal accounting no longer records a marker creation, and notification events fire DeleteMarkerCreated only for actual marker creation. Fixes #6745 * fix(s3): keep null-marker removal write shape undeleted and report marker semantics response-side The first cut marked the storage delete request deleted for a null-marker removal, which FileMeta::delete_version interprets as the suspended-bucket delete-mints-a-marker write and re-creates the marker just removed. Carry marker-ness to responses via explicit_delete_removed_marker (single path) and a response-only branch flag (batch path) instead, keeping every storage write shape byte-identical to the pre-fix behavior. Adds an embedded end-to-end regression test covering the full issue #6745 round trip.
This commit is contained in:
@@ -2110,9 +2110,18 @@ fn build_list_versions_next_marker(
|
||||
cache_id: Option<&str>,
|
||||
) -> (Option<String>, Option<String>) {
|
||||
if let Some(last) = objects.last() {
|
||||
// A null version carries the synthesized `Some(Uuid::nil())` identity
|
||||
// here; advertise it as the literal `null` marker so a resumed listing
|
||||
// parses it back to `VersionMarker::Null` instead of a nil UUID that
|
||||
// `find_version_index` can never match (issue #6745).
|
||||
(
|
||||
Some(append_list_cache_id_to_marker(last.name.clone(), cache_id)),
|
||||
Some(last.version_id.map(|v| v.to_string()).unwrap_or_else(|| "null".to_string())),
|
||||
Some(
|
||||
last.version_id
|
||||
.filter(|v| !v.is_nil())
|
||||
.map(|v| v.to_string())
|
||||
.unwrap_or_else(|| "null".to_string()),
|
||||
),
|
||||
)
|
||||
} else if let Some(last_prefix) = prefixes.last() {
|
||||
(Some(append_list_cache_id_to_marker(last_prefix.clone(), cache_id)), None)
|
||||
@@ -7759,6 +7768,31 @@ mod test {
|
||||
out
|
||||
}
|
||||
|
||||
// A truncated versions listing ending on a null version must advertise the
|
||||
// literal `null` continuation marker: a nil UUID parses to
|
||||
// `VersionMarker::Version(nil)`, which no stored version matches, so the
|
||||
// resumed page would replay every version (issue #6745).
|
||||
#[test]
|
||||
fn build_list_versions_next_marker_reports_null_for_nil_version_id() {
|
||||
let null_version = ObjectInfo {
|
||||
name: "obj-a".to_owned(),
|
||||
version_id: Some(uuid::Uuid::nil()),
|
||||
..Default::default()
|
||||
};
|
||||
let real_version = ObjectInfo {
|
||||
name: "obj-b".to_owned(),
|
||||
version_id: Some(uuid::Uuid::from_u128(7)),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let (next_marker, next_version_idmarker) = super::build_list_versions_next_marker(&[null_version], &[], None);
|
||||
assert_eq!(next_marker.as_deref(), Some("obj-a"));
|
||||
assert_eq!(next_version_idmarker.as_deref(), Some("null"));
|
||||
|
||||
let (_, next_version_idmarker) = super::build_list_versions_next_marker(&[real_version], &[], None);
|
||||
assert_eq!(next_version_idmarker.as_deref(), Some(uuid::Uuid::from_u128(7).to_string().as_str()));
|
||||
}
|
||||
|
||||
// ECA-03 / #944: a page whose raw keys fully collapse into fewer than max_keys
|
||||
// common prefixes must still report truncation and carry a continuation marker,
|
||||
// otherwise every key beyond the scan window is silently dropped.
|
||||
|
||||
Reference in New Issue
Block a user