mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-10 16:40:46 +02:00
* 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.
196 lines
5.8 KiB
Go
196 lines
5.8 KiB
Go
package lifecycle_xml
|
|
|
|
import (
|
|
"reflect"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// parseLifecycle is a thin helper for tests; production code reads XML via
|
|
// the regular bucket-config decoder, this shortcut goes through the
|
|
// package's public Parse so the tests exercise the exported entrypoint.
|
|
func parseLifecycle(t *testing.T, xmlSrc string) *Lifecycle {
|
|
t.Helper()
|
|
lc, err := Parse([]byte(xmlSrc))
|
|
if err != nil {
|
|
t.Fatalf("parse: %v", err)
|
|
}
|
|
return lc
|
|
}
|
|
|
|
func TestLifecycleToCanonical_TopLevelPrefix(t *testing.T) {
|
|
lc := parseLifecycle(t, `
|
|
<LifecycleConfiguration>
|
|
<Rule>
|
|
<ID>r1</ID>
|
|
<Status>Enabled</Status>
|
|
<Prefix>logs/</Prefix>
|
|
<Expiration><Days>30</Days></Expiration>
|
|
</Rule>
|
|
</LifecycleConfiguration>`)
|
|
got := LifecycleToCanonical(lc)
|
|
if len(got) != 1 {
|
|
t.Fatalf("want 1 rule, got %d", len(got))
|
|
}
|
|
r := got[0]
|
|
if r.ID != "r1" || r.Status != "Enabled" || r.Prefix != "logs/" || r.ExpirationDays != 30 {
|
|
t.Fatalf("unexpected: %+v", r)
|
|
}
|
|
}
|
|
|
|
func TestLifecycleToCanonical_FilterPrefix(t *testing.T) {
|
|
lc := parseLifecycle(t, `
|
|
<LifecycleConfiguration>
|
|
<Rule>
|
|
<Status>Enabled</Status>
|
|
<Filter><Prefix>logs/</Prefix></Filter>
|
|
<Expiration><Days>30</Days></Expiration>
|
|
</Rule>
|
|
</LifecycleConfiguration>`)
|
|
got := LifecycleToCanonical(lc)
|
|
if got[0].Prefix != "logs/" {
|
|
t.Fatalf("want logs/, got %q", got[0].Prefix)
|
|
}
|
|
}
|
|
|
|
func TestLifecycleToCanonical_FilterTagAndSize(t *testing.T) {
|
|
lc := parseLifecycle(t, `
|
|
<LifecycleConfiguration>
|
|
<Rule>
|
|
<Status>Enabled</Status>
|
|
<Filter>
|
|
<And>
|
|
<Prefix>data/</Prefix>
|
|
<Tag><Key>env</Key><Value>prod</Value></Tag>
|
|
<Tag><Key>tier</Key><Value>cold</Value></Tag>
|
|
<ObjectSizeGreaterThan>1024</ObjectSizeGreaterThan>
|
|
<ObjectSizeLessThan>10485760</ObjectSizeLessThan>
|
|
</And>
|
|
</Filter>
|
|
<Expiration><Days>90</Days></Expiration>
|
|
</Rule>
|
|
</LifecycleConfiguration>`)
|
|
got := LifecycleToCanonical(lc)[0]
|
|
if got.Prefix != "data/" {
|
|
t.Fatalf("prefix want data/, got %q", got.Prefix)
|
|
}
|
|
wantTags := map[string]string{"env": "prod", "tier": "cold"}
|
|
if !reflect.DeepEqual(got.FilterTags, wantTags) {
|
|
t.Fatalf("tags want %v, got %v", wantTags, got.FilterTags)
|
|
}
|
|
if got.FilterSizeGreaterThan != 1024 || got.FilterSizeLessThan != 10485760 {
|
|
t.Fatalf("size: gt=%d lt=%d", got.FilterSizeGreaterThan, got.FilterSizeLessThan)
|
|
}
|
|
}
|
|
|
|
func TestLifecycleToCanonical_SingleTagFilter(t *testing.T) {
|
|
lc := parseLifecycle(t, `
|
|
<LifecycleConfiguration>
|
|
<Rule>
|
|
<Status>Enabled</Status>
|
|
<Filter>
|
|
<Tag><Key>env</Key><Value>prod</Value></Tag>
|
|
</Filter>
|
|
<Expiration><Days>30</Days></Expiration>
|
|
</Rule>
|
|
</LifecycleConfiguration>`)
|
|
got := LifecycleToCanonical(lc)[0]
|
|
if !reflect.DeepEqual(got.FilterTags, map[string]string{"env": "prod"}) {
|
|
t.Fatalf("tags want {env:prod}, got %v", got.FilterTags)
|
|
}
|
|
}
|
|
|
|
func TestLifecycleToCanonical_MultipleActions(t *testing.T) {
|
|
// One XML <Rule> with three concurrent actions: Expiration.Days,
|
|
// NoncurrentVersionExpiration, AbortMultipartUpload. All must populate
|
|
// the corresponding canonical fields so RuleActionKinds expands them
|
|
// into three compiled actions downstream.
|
|
lc := parseLifecycle(t, `
|
|
<LifecycleConfiguration>
|
|
<Rule>
|
|
<ID>multi</ID>
|
|
<Status>Enabled</Status>
|
|
<Filter><Prefix>data/</Prefix></Filter>
|
|
<Expiration><Days>90</Days></Expiration>
|
|
<NoncurrentVersionExpiration>
|
|
<NoncurrentDays>30</NoncurrentDays>
|
|
<NewerNoncurrentVersions>3</NewerNoncurrentVersions>
|
|
</NoncurrentVersionExpiration>
|
|
<AbortIncompleteMultipartUpload>
|
|
<DaysAfterInitiation>7</DaysAfterInitiation>
|
|
</AbortIncompleteMultipartUpload>
|
|
</Rule>
|
|
</LifecycleConfiguration>`)
|
|
got := LifecycleToCanonical(lc)[0]
|
|
if got.ExpirationDays != 90 {
|
|
t.Fatalf("ExpirationDays want 90, got %d", got.ExpirationDays)
|
|
}
|
|
if got.NoncurrentVersionExpirationDays != 30 || got.NewerNoncurrentVersions != 3 {
|
|
t.Fatalf("NoncurrentVersion: %+v", got)
|
|
}
|
|
if got.AbortMPUDaysAfterInitiation != 7 {
|
|
t.Fatalf("AbortMPU want 7, got %d", got.AbortMPUDaysAfterInitiation)
|
|
}
|
|
}
|
|
|
|
func TestLifecycleToCanonical_ExpirationDate(t *testing.T) {
|
|
lc := parseLifecycle(t, `
|
|
<LifecycleConfiguration>
|
|
<Rule>
|
|
<Status>Enabled</Status>
|
|
<Filter><Prefix></Prefix></Filter>
|
|
<Expiration><Date>2025-06-15T00:00:00Z</Date></Expiration>
|
|
</Rule>
|
|
</LifecycleConfiguration>`)
|
|
got := LifecycleToCanonical(lc)[0]
|
|
want, _ := time.Parse(time.RFC3339, "2025-06-15T00:00:00Z")
|
|
if !got.ExpirationDate.Equal(want) {
|
|
t.Fatalf("date want %v, got %v", want, got.ExpirationDate)
|
|
}
|
|
}
|
|
|
|
func TestLifecycleToCanonical_ExpiredObjectDeleteMarker(t *testing.T) {
|
|
lc := parseLifecycle(t, `
|
|
<LifecycleConfiguration>
|
|
<Rule>
|
|
<Status>Enabled</Status>
|
|
<Filter><Prefix></Prefix></Filter>
|
|
<Expiration><ExpiredObjectDeleteMarker>true</ExpiredObjectDeleteMarker></Expiration>
|
|
</Rule>
|
|
</LifecycleConfiguration>`)
|
|
got := LifecycleToCanonical(lc)[0]
|
|
if !got.ExpiredObjectDeleteMarker {
|
|
t.Fatalf("want ExpiredObjectDeleteMarker=true")
|
|
}
|
|
}
|
|
|
|
func TestLifecycleToCanonical_DisabledRulePreserved(t *testing.T) {
|
|
// The engine's mode gate decides scheduling; conversion must preserve
|
|
// the Status verbatim regardless.
|
|
lc := parseLifecycle(t, `
|
|
<LifecycleConfiguration>
|
|
<Rule>
|
|
<Status>Disabled</Status>
|
|
<Filter><Prefix>x/</Prefix></Filter>
|
|
<Expiration><Days>30</Days></Expiration>
|
|
</Rule>
|
|
</LifecycleConfiguration>`)
|
|
got := LifecycleToCanonical(lc)[0]
|
|
if got.Status != "Disabled" {
|
|
t.Fatalf("status want Disabled, got %q", got.Status)
|
|
}
|
|
}
|
|
|
|
func TestLifecycleToCanonical_NilSafe(t *testing.T) {
|
|
if got := LifecycleToCanonical(nil); got != nil {
|
|
t.Fatalf("nil lc should return nil, got %v", got)
|
|
}
|
|
}
|
|
|
|
func TestLifecycleToCanonical_EmptyRules(t *testing.T) {
|
|
got := LifecycleToCanonical(&Lifecycle{})
|
|
if len(got) != 0 {
|
|
t.Fatalf("empty rules should be empty slice, got %d", len(got))
|
|
}
|
|
}
|