diff --git a/weed/s3api/s3api_bucket_handlers.go b/weed/s3api/s3api_bucket_handlers.go index a88ef563e..c5833dacd 100644 --- a/weed/s3api/s3api_bucket_handlers.go +++ b/weed/s3api/s3api_bucket_handlers.go @@ -956,20 +956,38 @@ func (s3a *S3ApiServer) PutBucketLifecycleConfigurationHandler(w http.ResponseWr if rule.Status != Enabled { continue } + // Reject Transition rules — they require storage class migration + // infrastructure that does not exist yet. + if rule.Transition.set || rule.NoncurrentVersionTransition.set { + s3err.WriteErrorResponse(w, r, s3err.ErrNotImplemented) + return + } + var rulePrefix string switch { + case rule.Filter.andSet: + rulePrefix = rule.Filter.And.Prefix.val case rule.Filter.Prefix.set: rulePrefix = rule.Filter.Prefix.val case rule.Prefix.set: rulePrefix = rule.Prefix.val - case !rule.Expiration.Date.IsZero() || rule.Transition.Days > 0 || !rule.Transition.Date.IsZero(): - s3err.WriteErrorResponse(w, r, s3err.ErrNotImplemented) - return } + // Only create filer.conf TTL entries for simple Expiration.Days rules + // with prefix-only filters (the fast path handled by RocksDB compaction + // filter). Rules with tag or size filters must be evaluated at scan time + // by the lifecycle worker, because TTL applies to all objects under the + // prefix regardless of tags or size. if rule.Expiration.Days == 0 { continue } + hasTagOrSizeFilter := rule.Filter.tagSet || + rule.Filter.ObjectSizeGreaterThan > 0 || rule.Filter.ObjectSizeLessThan > 0 || + (rule.Filter.andSet && (len(rule.Filter.And.Tags) > 0 || + rule.Filter.And.ObjectSizeGreaterThan > 0 || rule.Filter.And.ObjectSizeLessThan > 0)) + if hasTagOrSizeFilter { + continue // evaluated by lifecycle worker at scan time + } locationPrefix := fmt.Sprintf("%s/%s/%s", s3a.option.BucketsPath, bucket, rulePrefix) locConf := &filer_pb.FilerConf_PathConf{ LocationPrefix: locationPrefix, diff --git a/weed/s3api/s3api_policy_test.go b/weed/s3api/s3api_policy_test.go index b14b4f824..f6cc38f60 100644 --- a/weed/s3api/s3api_policy_test.go +++ b/weed/s3api/s3api_policy_test.go @@ -177,6 +177,51 @@ func TestLifecycleXMLRoundTrip_FilterWithSizeOnly(t *testing.T) { } } +func TestLifecycleXML_TransitionSetFlag(t *testing.T) { + // Verify that Transition.set is true after unmarshaling. + input := ` + + transition + Enabled + + + 30 + GLACIER + + +` + + var lc Lifecycle + if err := xml.Unmarshal([]byte(input), &lc); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if !lc.Rules[0].Transition.set { + t.Error("expected Transition.set=true after unmarshal") + } +} + +func TestLifecycleXML_NoncurrentVersionTransitionSetFlag(t *testing.T) { + input := ` + + nv-transition + Enabled + + + 60 + GLACIER + + +` + + var lc Lifecycle + if err := xml.Unmarshal([]byte(input), &lc); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if !lc.Rules[0].NoncurrentVersionTransition.set { + t.Error("expected NoncurrentVersionTransition.set=true after unmarshal") + } +} + func TestLifecycleXMLRoundTrip_CompleteRule(t *testing.T) { // A complete lifecycle config similar to what Terraform sends. input := `