From 1cf92f6c2e59467d2c3119e6f774b48c9eaf298f Mon Sep 17 00:00:00 2001 From: 7y-9 Date: Tue, 9 Jun 2026 15:48:38 +0800 Subject: [PATCH] fix(s3api): clear stale object lock years (#9890) Problem: Re-storing object-lock default retention with Days left a previous Years extended attribute in place, so later loads could see both Days and stale Years. Root cause: StoreObjectLockConfigurationInExtended only wrote period fields that were set on the new configuration and did not delete old Days or Years keys before writing the replacement rule. Fix: Clear stored default-retention Days and Years keys before writing the current default retention period fields. Reproduction: go test ./weed/s3api -run TestStoreObjectLockConfigurationClearsStaleYears -count=1 failed before the fix because the stale years key remained. Validation: go test ./weed/s3api -run TestStoreObjectLockConfigurationClearsStaleYears -count=1; go test ./weed/s3api -count=1; git diff --check; git diff --cached --check Co-authored-by: Codex --- weed/s3api/object_lock_utils.go | 3 ++ weed/s3api/s3api_object_retention_test.go | 36 +++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/weed/s3api/object_lock_utils.go b/weed/s3api/object_lock_utils.go index a3eddca45..06ec7f4f2 100644 --- a/weed/s3api/object_lock_utils.go +++ b/weed/s3api/object_lock_utils.go @@ -95,6 +95,9 @@ func StoreObjectLockConfigurationInExtended(entry *filer_pb.Entry, config *Objec if config.Rule != nil && config.Rule.DefaultRetention != nil { defaultRetention := config.Rule.DefaultRetention + delete(entry.Extended, s3_constants.ExtObjectLockDefaultDaysKey) + delete(entry.Extended, s3_constants.ExtObjectLockDefaultYearsKey) + // Store mode if defaultRetention.Mode != "" { entry.Extended[s3_constants.ExtObjectLockDefaultModeKey] = []byte(defaultRetention.Mode) diff --git a/weed/s3api/s3api_object_retention_test.go b/weed/s3api/s3api_object_retention_test.go index be42df700..44089deb3 100644 --- a/weed/s3api/s3api_object_retention_test.go +++ b/weed/s3api/s3api_object_retention_test.go @@ -8,6 +8,7 @@ import ( "testing" "time" + "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants" ) @@ -817,6 +818,41 @@ func TestValidateDefaultRetention(t *testing.T) { } } +func TestStoreObjectLockConfigurationClearsStaleYears(t *testing.T) { + entry := &filer_pb.Entry{ + Extended: map[string][]byte{ + s3_constants.ExtObjectLockEnabledKey: []byte(s3_constants.ObjectLockEnabled), + s3_constants.ExtObjectLockDefaultModeKey: []byte(s3_constants.RetentionModeCompliance), + s3_constants.ExtObjectLockDefaultYearsKey: []byte("1"), + }, + } + config := CreateObjectLockConfiguration(true, s3_constants.RetentionModeGovernance, 30, 0) + + if err := StoreObjectLockConfigurationInExtended(entry, config); err != nil { + t.Fatalf("StoreObjectLockConfigurationInExtended returned error: %v", err) + } + + if _, exists := entry.Extended[s3_constants.ExtObjectLockDefaultYearsKey]; exists { + t.Fatalf("stale default retention years key was not cleared") + } + + loaded, found := LoadObjectLockConfigurationFromExtended(entry) + if !found { + t.Fatalf("expected stored object lock configuration to load") + } + if loaded.Rule == nil || loaded.Rule.DefaultRetention == nil { + t.Fatalf("expected loaded configuration to include default retention") + } + + retention := loaded.Rule.DefaultRetention + if !retention.DaysSet || retention.Days != 30 { + t.Fatalf("expected loaded retention days to be 30, got DaysSet=%v Days=%d", retention.DaysSet, retention.Days) + } + if retention.YearsSet || retention.Years != 0 { + t.Fatalf("expected loaded retention years to be cleared, got YearsSet=%v Years=%d", retention.YearsSet, retention.Years) + } +} + // Helper function to create a time pointer func timePtr(t time.Time) *time.Time { return &t