diff --git a/weed/s3api/s3api_object_handlers_copy.go b/weed/s3api/s3api_object_handlers_copy.go index 34f511259..4dd31c8ce 100644 --- a/weed/s3api/s3api_object_handlers_copy.go +++ b/weed/s3api/s3api_object_handlers_copy.go @@ -2334,6 +2334,8 @@ func (ctx *EncryptionHeaderContext) shouldSkipEncryptedToUnencryptedHeader() boo // cleanupVersioningMetadata removes versioning-related metadata from Extended attributes // when copying to non-versioned or suspended-versioning buckets. // This prevents objects in non-versioned buckets from carrying invalid versioning metadata. +// It also removes the source ETag to prevent metadata inconsistency, as a new ETag will be +// calculated for the destination object. func cleanupVersioningMetadata(metadata map[string][]byte) { delete(metadata, s3_constants.ExtVersionIdKey) delete(metadata, s3_constants.ExtDeleteMarkerKey) diff --git a/weed/s3api/s3api_object_handlers_copy_test.go b/weed/s3api/s3api_object_handlers_copy_test.go index d58bc1571..a4bf3c246 100644 --- a/weed/s3api/s3api_object_handlers_copy_test.go +++ b/weed/s3api/s3api_object_handlers_copy_test.go @@ -481,7 +481,8 @@ func TestShouldCreateVersionForCopy(t *testing.T) { } // TestCleanupVersioningMetadata tests the production function that removes versioning metadata. -// This ensures objects copied to non-versioned buckets don't carry invalid versioning metadata. +// This ensures objects copied to non-versioned buckets don't carry invalid versioning metadata +// or stale ETag values from the source. func TestCleanupVersioningMetadata(t *testing.T) { testCases := []struct { name string @@ -500,22 +501,33 @@ func TestCleanupVersioningMetadata(t *testing.T) { expectedKeys: []string{"X-Amz-Meta-Custom"}, removedKeys: []string{s3_constants.ExtVersionIdKey, s3_constants.ExtDeleteMarkerKey, s3_constants.ExtIsLatestKey}, }, + { + name: "RemovesSourceETag", + sourceMetadata: map[string][]byte{ + s3_constants.ExtVersionIdKey: []byte("version-123"), + s3_constants.ExtETagKey: []byte("\"abc123\""), + "X-Amz-Meta-Custom": []byte("value"), + }, + expectedKeys: []string{"X-Amz-Meta-Custom"}, + removedKeys: []string{s3_constants.ExtVersionIdKey, s3_constants.ExtETagKey}, + }, { name: "HandlesEmptyMetadata", sourceMetadata: map[string][]byte{}, expectedKeys: []string{}, - removedKeys: []string{s3_constants.ExtVersionIdKey, s3_constants.ExtDeleteMarkerKey, s3_constants.ExtIsLatestKey}, + removedKeys: []string{s3_constants.ExtVersionIdKey, s3_constants.ExtDeleteMarkerKey, s3_constants.ExtIsLatestKey, s3_constants.ExtETagKey}, }, { name: "PreservesNonVersioningMetadata", sourceMetadata: map[string][]byte{ s3_constants.ExtVersionIdKey: []byte("version-456"), + s3_constants.ExtETagKey: []byte("\"def456\""), "X-Amz-Meta-Custom": []byte("value1"), "X-Amz-Meta-Another": []byte("value2"), s3_constants.ExtIsLatestKey: []byte("true"), }, expectedKeys: []string{"X-Amz-Meta-Custom", "X-Amz-Meta-Another"}, - removedKeys: []string{s3_constants.ExtVersionIdKey, s3_constants.ExtIsLatestKey}, + removedKeys: []string{s3_constants.ExtVersionIdKey, s3_constants.ExtETagKey, s3_constants.ExtIsLatestKey}, }, }