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 := `