Files
Chris LuandGitHub b7928637a0 refactor(s3api): move Lifecycle XML structs to leaf package lifecycle_xml (#9360)
* refactor(s3api): move Lifecycle XML structs to leaf package lifecycle_xml

The structs S3 PutBucketLifecycleConfiguration parses and the canonical
conversion to s3lifecycle.Rule lived in package s3api, which transitively
imports weed/server (which imports weed/shell). Any caller outside
weed/s3api — the shell, the future lifecycle worker — that wanted to
parse a bucket's lifecycle XML hit an import cycle.

Moves:
  weed/s3api/s3api_policy.go              -> lifecycle_xml/types.go
  weed/s3api/s3api_lifecycle_canonical.go -> lifecycle_xml/canonical.go
  s3api_lifecycle_canonical_test.go       -> lifecycle_xml/canonical_test.go
  s3api_policy_test.go                    -> lifecycle_xml/round_trip_test.go

Renames the public RuleStatus type (was unexported ruleStatus) and adds
small accessor methods (Set/Val/AndSet/TagSet) for fields the s3api
handler needs to read across the package boundary. Adds NewPrefix and
NewExpirationDays constructors so the GET handler can build response
rules without poking at unexported fields. Adds a Tag struct local to
the package so it has zero internal seaweed deps. Adds a one-shot
ParseCanonical(xml []byte) helper for non-server callers.

s3api_policy.go was misnamed — its content is lifecycle XML, not S3
bucket policy. The new package name reflects the actual scope.

* test(s3api/lifecycle_xml): exercise public API in tests

- canonical_test.go's parseLifecycle helper went through xml.Unmarshal
  directly; route it through the package's exported Parse so tests
  validate the public entrypoint.
- round_trip_test.go asserted internal flags (rule.Filter.tagSet,
  rule.Filter.andSet, Transition.set, NoncurrentVersionTransition.set);
  switch to TagSet(), AndSet(), Set() — exercises the public contract
  that downstream callers (s3api handler, future shell command) rely on.
2026-05-07 18:54:06 -07:00

111 lines
2.7 KiB
Go

package lifecycle_xml
import (
"bytes"
"encoding/xml"
"github.com/seaweedfs/seaweedfs/weed/s3api/s3lifecycle"
)
// Parse decodes a BucketLifecycleConfiguration XML body into the wire-form
// Lifecycle struct.
func Parse(xmlBytes []byte) (*Lifecycle, error) {
var lc Lifecycle
if err := xml.NewDecoder(bytes.NewReader(xmlBytes)).Decode(&lc); err != nil {
return nil, err
}
return &lc, nil
}
// ParseCanonical is the one-shot path most non-server callers want:
// raw XML in, []*s3lifecycle.Rule out.
func ParseCanonical(xmlBytes []byte) ([]*s3lifecycle.Rule, error) {
lc, err := Parse(xmlBytes)
if err != nil {
return nil, err
}
return LifecycleToCanonical(lc), nil
}
// LifecycleToCanonical flattens the XML-deserialized Lifecycle into the
// engine's flat Rule shape. The optional <Filter> element may contain
// <Prefix> | <Tag> | <And>, or be absent (in which case the older top-level
// <Prefix> applies).
func LifecycleToCanonical(lc *Lifecycle) []*s3lifecycle.Rule {
if lc == nil {
return nil
}
out := make([]*s3lifecycle.Rule, 0, len(lc.Rules))
for i := range lc.Rules {
out = append(out, ruleToCanonical(&lc.Rules[i]))
}
return out
}
func ruleToCanonical(r *Rule) *s3lifecycle.Rule {
out := &s3lifecycle.Rule{
ID: r.ID,
Status: string(r.Status),
}
prefix, tags, sizeGT, sizeLT := flattenFilter(&r.Filter)
if prefix == "" && r.Prefix.set {
prefix = r.Prefix.val
}
out.Prefix = prefix
if len(tags) > 0 {
out.FilterTags = tags
}
out.FilterSizeGreaterThan = sizeGT
out.FilterSizeLessThan = sizeLT
if r.Expiration.set {
out.ExpirationDays = r.Expiration.Days
if !r.Expiration.Date.Time.IsZero() {
out.ExpirationDate = r.Expiration.Date.Time
}
if r.Expiration.DeleteMarker.set {
out.ExpiredObjectDeleteMarker = r.Expiration.DeleteMarker.val
}
}
if r.NoncurrentVersionExpiration.set {
out.NoncurrentVersionExpirationDays = r.NoncurrentVersionExpiration.NoncurrentDays
out.NewerNoncurrentVersions = r.NoncurrentVersionExpiration.NewerNoncurrentVersions
}
if r.AbortIncompleteMultipartUpload.set {
out.AbortMPUDaysAfterInitiation = r.AbortIncompleteMultipartUpload.DaysAfterInitiation
}
return out
}
func flattenFilter(f *Filter) (prefix string, tags map[string]string, sizeGT, sizeLT int64) {
if !f.set {
return
}
if f.andSet {
if f.And.Prefix.set {
prefix = f.And.Prefix.val
}
if len(f.And.Tags) > 0 {
tags = make(map[string]string, len(f.And.Tags))
for _, t := range f.And.Tags {
tags[t.Key] = t.Value
}
}
sizeGT = f.And.ObjectSizeGreaterThan
sizeLT = f.And.ObjectSizeLessThan
return
}
if f.tagSet {
tags = map[string]string{f.Tag.Key: f.Tag.Value}
} else if f.Prefix.set {
prefix = f.Prefix.val
}
sizeGT = f.ObjectSizeGreaterThan
sizeLT = f.ObjectSizeLessThan
return
}