mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-24 06:50:40 +02:00
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.
This commit is contained in:
1 parent
c567da7164
commit
b7928637a0
5 files changed
+112
-42
No files matched your search
@@ -0,0 +1,110 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
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))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
package lifecycle_xml
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLifecycleXMLRoundTrip_NoncurrentVersionExpiration(t *testing.T) {
|
||||
input := `<LifecycleConfiguration>
|
||||
<Rule>
|
||||
<ID>expire-noncurrent</ID>
|
||||
<Status>Enabled</Status>
|
||||
<Filter><Prefix></Prefix></Filter>
|
||||
<NoncurrentVersionExpiration>
|
||||
<NoncurrentDays>30</NoncurrentDays>
|
||||
<NewerNoncurrentVersions>2</NewerNoncurrentVersions>
|
||||
</NoncurrentVersionExpiration>
|
||||
</Rule>
|
||||
</LifecycleConfiguration>`
|
||||
|
||||
var lc Lifecycle
|
||||
if err := xml.Unmarshal([]byte(input), &lc); err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
|
||||
if len(lc.Rules) != 1 {
|
||||
t.Fatalf("expected 1 rule, got %d", len(lc.Rules))
|
||||
}
|
||||
rule := lc.Rules[0]
|
||||
if rule.ID != "expire-noncurrent" {
|
||||
t.Errorf("expected ID 'expire-noncurrent', got %q", rule.ID)
|
||||
}
|
||||
if rule.NoncurrentVersionExpiration.NoncurrentDays != 30 {
|
||||
t.Errorf("expected NoncurrentDays=30, got %d", rule.NoncurrentVersionExpiration.NoncurrentDays)
|
||||
}
|
||||
if rule.NoncurrentVersionExpiration.NewerNoncurrentVersions != 2 {
|
||||
t.Errorf("expected NewerNoncurrentVersions=2, got %d", rule.NoncurrentVersionExpiration.NewerNoncurrentVersions)
|
||||
}
|
||||
|
||||
// Re-marshal and verify it round-trips.
|
||||
out, err := xml.Marshal(lc)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal: %v", err)
|
||||
}
|
||||
s := string(out)
|
||||
if !strings.Contains(s, "<NoncurrentDays>30</NoncurrentDays>") {
|
||||
t.Errorf("marshaled XML missing NoncurrentDays: %s", s)
|
||||
}
|
||||
if !strings.Contains(s, "<NewerNoncurrentVersions>2</NewerNoncurrentVersions>") {
|
||||
t.Errorf("marshaled XML missing NewerNoncurrentVersions: %s", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLifecycleXMLRoundTrip_AbortIncompleteMultipartUpload(t *testing.T) {
|
||||
input := `<LifecycleConfiguration>
|
||||
<Rule>
|
||||
<ID>abort-mpu</ID>
|
||||
<Status>Enabled</Status>
|
||||
<Filter><Prefix></Prefix></Filter>
|
||||
<AbortIncompleteMultipartUpload>
|
||||
<DaysAfterInitiation>7</DaysAfterInitiation>
|
||||
</AbortIncompleteMultipartUpload>
|
||||
</Rule>
|
||||
</LifecycleConfiguration>`
|
||||
|
||||
var lc Lifecycle
|
||||
if err := xml.Unmarshal([]byte(input), &lc); err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
|
||||
rule := lc.Rules[0]
|
||||
if rule.AbortIncompleteMultipartUpload.DaysAfterInitiation != 7 {
|
||||
t.Errorf("expected DaysAfterInitiation=7, got %d", rule.AbortIncompleteMultipartUpload.DaysAfterInitiation)
|
||||
}
|
||||
|
||||
out, err := xml.Marshal(lc)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal: %v", err)
|
||||
}
|
||||
if !strings.Contains(string(out), "<DaysAfterInitiation>7</DaysAfterInitiation>") {
|
||||
t.Errorf("marshaled XML missing DaysAfterInitiation: %s", string(out))
|
||||
}
|
||||
}
|
||||
|
||||
func TestLifecycleXMLRoundTrip_FilterWithTag(t *testing.T) {
|
||||
input := `<LifecycleConfiguration>
|
||||
<Rule>
|
||||
<ID>tag-filter</ID>
|
||||
<Status>Enabled</Status>
|
||||
<Filter>
|
||||
<Tag><Key>env</Key><Value>dev</Value></Tag>
|
||||
</Filter>
|
||||
<Expiration><Days>7</Days></Expiration>
|
||||
</Rule>
|
||||
</LifecycleConfiguration>`
|
||||
|
||||
var lc Lifecycle
|
||||
if err := xml.Unmarshal([]byte(input), &lc); err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
|
||||
rule := lc.Rules[0]
|
||||
if !rule.Filter.TagSet() {
|
||||
t.Error("expected Filter.TagSet() to be true")
|
||||
}
|
||||
if rule.Filter.Tag.Key != "env" || rule.Filter.Tag.Value != "dev" {
|
||||
t.Errorf("expected Tag{env:dev}, got Tag{%s:%s}", rule.Filter.Tag.Key, rule.Filter.Tag.Value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLifecycleXMLRoundTrip_FilterWithAnd(t *testing.T) {
|
||||
input := `<LifecycleConfiguration>
|
||||
<Rule>
|
||||
<ID>and-filter</ID>
|
||||
<Status>Enabled</Status>
|
||||
<Filter>
|
||||
<And>
|
||||
<Prefix>logs/</Prefix>
|
||||
<Tag><Key>env</Key><Value>dev</Value></Tag>
|
||||
<Tag><Key>tier</Key><Value>hot</Value></Tag>
|
||||
<ObjectSizeGreaterThan>1024</ObjectSizeGreaterThan>
|
||||
<ObjectSizeLessThan>1048576</ObjectSizeLessThan>
|
||||
</And>
|
||||
</Filter>
|
||||
<Expiration><Days>7</Days></Expiration>
|
||||
</Rule>
|
||||
</LifecycleConfiguration>`
|
||||
|
||||
var lc Lifecycle
|
||||
if err := xml.Unmarshal([]byte(input), &lc); err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
|
||||
rule := lc.Rules[0]
|
||||
if !rule.Filter.AndSet() {
|
||||
t.Error("expected Filter.AndSet() to be true")
|
||||
}
|
||||
if rule.Filter.And.Prefix.String() != "logs/" {
|
||||
t.Errorf("expected And.Prefix='logs/', got %q", rule.Filter.And.Prefix.String())
|
||||
}
|
||||
if len(rule.Filter.And.Tags) != 2 {
|
||||
t.Fatalf("expected 2 And tags, got %d", len(rule.Filter.And.Tags))
|
||||
}
|
||||
if rule.Filter.And.ObjectSizeGreaterThan != 1024 {
|
||||
t.Errorf("expected ObjectSizeGreaterThan=1024, got %d", rule.Filter.And.ObjectSizeGreaterThan)
|
||||
}
|
||||
if rule.Filter.And.ObjectSizeLessThan != 1048576 {
|
||||
t.Errorf("expected ObjectSizeLessThan=1048576, got %d", rule.Filter.And.ObjectSizeLessThan)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLifecycleXMLRoundTrip_FilterWithSizeOnly(t *testing.T) {
|
||||
input := `<LifecycleConfiguration>
|
||||
<Rule>
|
||||
<ID>size-filter</ID>
|
||||
<Status>Enabled</Status>
|
||||
<Filter>
|
||||
<ObjectSizeGreaterThan>512</ObjectSizeGreaterThan>
|
||||
<ObjectSizeLessThan>10485760</ObjectSizeLessThan>
|
||||
</Filter>
|
||||
<Expiration><Days>30</Days></Expiration>
|
||||
</Rule>
|
||||
</LifecycleConfiguration>`
|
||||
|
||||
var lc Lifecycle
|
||||
if err := xml.Unmarshal([]byte(input), &lc); err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
|
||||
rule := lc.Rules[0]
|
||||
if rule.Filter.ObjectSizeGreaterThan != 512 {
|
||||
t.Errorf("expected ObjectSizeGreaterThan=512, got %d", rule.Filter.ObjectSizeGreaterThan)
|
||||
}
|
||||
if rule.Filter.ObjectSizeLessThan != 10485760 {
|
||||
t.Errorf("expected ObjectSizeLessThan=10485760, got %d", rule.Filter.ObjectSizeLessThan)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLifecycleXML_TransitionSetFlag(t *testing.T) {
|
||||
// Verify that Transition.Set() is true after unmarshaling.
|
||||
input := `<LifecycleConfiguration>
|
||||
<Rule>
|
||||
<ID>transition</ID>
|
||||
<Status>Enabled</Status>
|
||||
<Filter><Prefix></Prefix></Filter>
|
||||
<Transition>
|
||||
<Days>30</Days>
|
||||
<StorageClass>GLACIER</StorageClass>
|
||||
</Transition>
|
||||
</Rule>
|
||||
</LifecycleConfiguration>`
|
||||
|
||||
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 := `<LifecycleConfiguration>
|
||||
<Rule>
|
||||
<ID>nv-transition</ID>
|
||||
<Status>Enabled</Status>
|
||||
<Filter><Prefix></Prefix></Filter>
|
||||
<NoncurrentVersionTransition>
|
||||
<NoncurrentDays>60</NoncurrentDays>
|
||||
<StorageClass>GLACIER</StorageClass>
|
||||
</NoncurrentVersionTransition>
|
||||
</Rule>
|
||||
</LifecycleConfiguration>`
|
||||
|
||||
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 := `<LifecycleConfiguration>
|
||||
<Rule>
|
||||
<ID>rotation</ID>
|
||||
<Filter><Prefix></Prefix></Filter>
|
||||
<Status>Enabled</Status>
|
||||
<Expiration><Days>30</Days></Expiration>
|
||||
<NoncurrentVersionExpiration>
|
||||
<NoncurrentDays>1</NoncurrentDays>
|
||||
</NoncurrentVersionExpiration>
|
||||
<AbortIncompleteMultipartUpload>
|
||||
<DaysAfterInitiation>1</DaysAfterInitiation>
|
||||
</AbortIncompleteMultipartUpload>
|
||||
</Rule>
|
||||
</LifecycleConfiguration>`
|
||||
|
||||
var lc Lifecycle
|
||||
if err := xml.Unmarshal([]byte(input), &lc); err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
|
||||
rule := lc.Rules[0]
|
||||
if rule.ID != "rotation" {
|
||||
t.Errorf("expected ID 'rotation', got %q", rule.ID)
|
||||
}
|
||||
if rule.Expiration.Days != 30 {
|
||||
t.Errorf("expected Expiration.Days=30, got %d", rule.Expiration.Days)
|
||||
}
|
||||
if rule.NoncurrentVersionExpiration.NoncurrentDays != 1 {
|
||||
t.Errorf("expected NoncurrentDays=1, got %d", rule.NoncurrentVersionExpiration.NoncurrentDays)
|
||||
}
|
||||
if rule.AbortIncompleteMultipartUpload.DaysAfterInitiation != 1 {
|
||||
t.Errorf("expected DaysAfterInitiation=1, got %d", rule.AbortIncompleteMultipartUpload.DaysAfterInitiation)
|
||||
}
|
||||
|
||||
// Re-marshal and verify all fields survive.
|
||||
out, err := xml.Marshal(lc)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal: %v", err)
|
||||
}
|
||||
s := string(out)
|
||||
for _, expected := range []string{
|
||||
"<Days>30</Days>",
|
||||
"<NoncurrentDays>1</NoncurrentDays>",
|
||||
"<DaysAfterInitiation>1</DaysAfterInitiation>",
|
||||
} {
|
||||
if !strings.Contains(s, expected) {
|
||||
t.Errorf("marshaled XML missing %q: %s", expected, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,400 @@
|
||||
// Package lifecycle_xml is the XML wire-form for S3 BucketLifecycleConfiguration:
|
||||
// the structs S3 PutBucketLifecycleConfiguration accepts and
|
||||
// GetBucketLifecycleConfiguration returns. It lives outside weed/s3api so
|
||||
// callers outside that package (shell, lifecycle worker) can parse and
|
||||
// emit the same shape without pulling weed/s3api's full dependency graph
|
||||
// — weed/s3api transitively imports weed/server, which imports weed/shell.
|
||||
//
|
||||
// Conversion to the engine-friendly canonical form
|
||||
// (weed/s3api/s3lifecycle.Rule) lives next to the types here as
|
||||
// LifecycleToCanonical.
|
||||
package lifecycle_xml
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"time"
|
||||
)
|
||||
|
||||
// RuleStatus represents lifecycle rule status.
|
||||
type RuleStatus string
|
||||
|
||||
// Supported status types.
|
||||
const (
|
||||
Enabled RuleStatus = "Enabled"
|
||||
Disabled RuleStatus = "Disabled"
|
||||
)
|
||||
|
||||
// Tag is the lifecycle filter tag pair. Defined locally so this package
|
||||
// has no dependency on weed/s3api.
|
||||
type Tag struct {
|
||||
Key string `xml:"Key"`
|
||||
Value string `xml:"Value"`
|
||||
}
|
||||
|
||||
// Lifecycle - Configuration for bucket lifecycle.
|
||||
type Lifecycle struct {
|
||||
XMLName xml.Name `xml:"LifecycleConfiguration"`
|
||||
Rules []Rule `xml:"Rule"`
|
||||
}
|
||||
|
||||
// Rule - a rule for lifecycle configuration.
|
||||
type Rule struct {
|
||||
XMLName xml.Name `xml:"Rule"`
|
||||
ID string `xml:"ID,omitempty"`
|
||||
Status RuleStatus `xml:"Status"`
|
||||
Filter Filter `xml:"Filter,omitempty"`
|
||||
Prefix Prefix `xml:"Prefix,omitempty"`
|
||||
Expiration Expiration `xml:"Expiration,omitempty"`
|
||||
Transition Transition `xml:"Transition,omitempty"`
|
||||
NoncurrentVersionExpiration NoncurrentVersionExpiration `xml:"NoncurrentVersionExpiration,omitempty"`
|
||||
NoncurrentVersionTransition NoncurrentVersionTransition `xml:"NoncurrentVersionTransition,omitempty"`
|
||||
AbortIncompleteMultipartUpload AbortIncompleteMultipartUpload `xml:"AbortIncompleteMultipartUpload,omitempty"`
|
||||
}
|
||||
|
||||
// Filter - a filter for a lifecycle configuration Rule.
|
||||
type Filter struct {
|
||||
XMLName xml.Name `xml:"Filter"`
|
||||
set bool
|
||||
|
||||
Prefix Prefix
|
||||
|
||||
And And
|
||||
andSet bool
|
||||
|
||||
Tag Tag
|
||||
tagSet bool
|
||||
|
||||
ObjectSizeGreaterThan int64
|
||||
ObjectSizeLessThan int64
|
||||
}
|
||||
|
||||
// Prefix holds the prefix xml tag in <Rule> and <Filter>
|
||||
type Prefix struct {
|
||||
XMLName xml.Name `xml:"Prefix"`
|
||||
set bool
|
||||
|
||||
val string
|
||||
}
|
||||
|
||||
func (p Prefix) String() string {
|
||||
return p.val
|
||||
}
|
||||
|
||||
// Set returns whether the Prefix carries a value (vs being absent).
|
||||
func (p Prefix) Set() bool { return p.set }
|
||||
|
||||
// Val returns the prefix string. Empty when !Set.
|
||||
func (p Prefix) Val() string { return p.val }
|
||||
|
||||
// NewPrefix builds a Prefix marked as set.
|
||||
func NewPrefix(val string) Prefix { return Prefix{val: val, set: true} }
|
||||
|
||||
// NewExpirationDays builds an Expiration with Days set.
|
||||
func NewExpirationDays(days int) Expiration { return Expiration{Days: days, set: true} }
|
||||
|
||||
// Set reports whether this Filter is present in the XML.
|
||||
func (f Filter) Set() bool { return f.set }
|
||||
|
||||
// AndSet reports whether the Filter contains an <And> branch.
|
||||
func (f Filter) AndSet() bool { return f.andSet }
|
||||
|
||||
// TagSet reports whether the Filter contains a single <Tag> branch.
|
||||
func (f Filter) TagSet() bool { return f.tagSet }
|
||||
|
||||
// Set reports whether the Transition element was present.
|
||||
func (t Transition) Set() bool { return t.set }
|
||||
|
||||
// Set reports whether the NoncurrentVersionTransition element was present.
|
||||
func (n NoncurrentVersionTransition) Set() bool { return n.set }
|
||||
|
||||
// Set reports whether the Expiration element was present.
|
||||
func (e Expiration) Set() bool { return e.set }
|
||||
|
||||
// MarshalXML encodes Prefix field into an XML form.
|
||||
func (p Prefix) MarshalXML(e *xml.Encoder, startElement xml.StartElement) error {
|
||||
if !p.set {
|
||||
return nil
|
||||
}
|
||||
return e.EncodeElement(p.val, startElement)
|
||||
}
|
||||
|
||||
func (p *Prefix) UnmarshalXML(d *xml.Decoder, startElement xml.StartElement) error {
|
||||
prefix := ""
|
||||
_ = d.DecodeElement(&prefix, &startElement)
|
||||
*p = Prefix{set: true, val: prefix}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalXML encodes Filter field into an XML form.
|
||||
func (f Filter) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||
if !f.set {
|
||||
return nil
|
||||
}
|
||||
if err := e.EncodeToken(start); err != nil {
|
||||
return err
|
||||
}
|
||||
if f.andSet {
|
||||
if err := e.EncodeElement(f.And, xml.StartElement{Name: xml.Name{Local: "And"}}); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if f.tagSet {
|
||||
if err := e.EncodeElement(f.Tag, xml.StartElement{Name: xml.Name{Local: "Tag"}}); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := e.EncodeElement(f.Prefix, xml.StartElement{Name: xml.Name{Local: "Prefix"}}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if f.ObjectSizeGreaterThan > 0 {
|
||||
if err := e.EncodeElement(f.ObjectSizeGreaterThan, xml.StartElement{Name: xml.Name{Local: "ObjectSizeGreaterThan"}}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if f.ObjectSizeLessThan > 0 {
|
||||
if err := e.EncodeElement(f.ObjectSizeLessThan, xml.StartElement{Name: xml.Name{Local: "ObjectSizeLessThan"}}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return e.EncodeToken(xml.EndElement{Name: start.Name})
|
||||
}
|
||||
|
||||
// UnmarshalXML decodes Filter from XML, handling all child elements.
|
||||
func (f *Filter) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
f.set = true
|
||||
for {
|
||||
tok, err := d.Token()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch t := tok.(type) {
|
||||
case xml.StartElement:
|
||||
switch t.Name.Local {
|
||||
case "Prefix":
|
||||
if err := d.DecodeElement(&f.Prefix, &t); err != nil {
|
||||
return err
|
||||
}
|
||||
case "Tag":
|
||||
f.tagSet = true
|
||||
if err := d.DecodeElement(&f.Tag, &t); err != nil {
|
||||
return err
|
||||
}
|
||||
case "And":
|
||||
f.andSet = true
|
||||
if err := d.DecodeElement(&f.And, &t); err != nil {
|
||||
return err
|
||||
}
|
||||
case "ObjectSizeGreaterThan":
|
||||
if err := d.DecodeElement(&f.ObjectSizeGreaterThan, &t); err != nil {
|
||||
return err
|
||||
}
|
||||
case "ObjectSizeLessThan":
|
||||
if err := d.DecodeElement(&f.ObjectSizeLessThan, &t); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
if err := d.Skip(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case xml.EndElement:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// And - a tag to combine a prefix and multiple tags for lifecycle configuration rule.
|
||||
type And struct {
|
||||
XMLName xml.Name `xml:"And"`
|
||||
Prefix Prefix `xml:"Prefix,omitempty"`
|
||||
Tags []Tag `xml:"Tag,omitempty"`
|
||||
ObjectSizeGreaterThan int64 `xml:"ObjectSizeGreaterThan,omitempty"`
|
||||
ObjectSizeLessThan int64 `xml:"ObjectSizeLessThan,omitempty"`
|
||||
}
|
||||
|
||||
// Expiration - expiration actions for a rule in lifecycle configuration.
|
||||
type Expiration struct {
|
||||
XMLName xml.Name `xml:"Expiration"`
|
||||
Days int `xml:"Days,omitempty"`
|
||||
Date ExpirationDate `xml:"Date,omitempty"`
|
||||
DeleteMarker ExpireDeleteMarker `xml:"ExpiredObjectDeleteMarker"`
|
||||
|
||||
set bool
|
||||
}
|
||||
|
||||
// MarshalXML encodes expiration field into an XML form.
|
||||
func (e Expiration) MarshalXML(enc *xml.Encoder, startElement xml.StartElement) error {
|
||||
if !e.set {
|
||||
return nil
|
||||
}
|
||||
type expirationWrapper Expiration
|
||||
return enc.EncodeElement(expirationWrapper(e), startElement)
|
||||
}
|
||||
|
||||
func (e *Expiration) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
type wrapper Expiration
|
||||
var w wrapper
|
||||
if err := d.DecodeElement(&w, &start); err != nil {
|
||||
return err
|
||||
}
|
||||
*e = Expiration(w)
|
||||
e.set = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// ExpireDeleteMarker represents value of ExpiredObjectDeleteMarker field in Expiration XML element.
|
||||
type ExpireDeleteMarker struct {
|
||||
val bool
|
||||
set bool
|
||||
}
|
||||
|
||||
// MarshalXML encodes delete marker boolean into an XML form.
|
||||
func (b ExpireDeleteMarker) MarshalXML(e *xml.Encoder, startElement xml.StartElement) error {
|
||||
if !b.set {
|
||||
return nil
|
||||
}
|
||||
return e.EncodeElement(b.val, startElement)
|
||||
}
|
||||
|
||||
func (b *ExpireDeleteMarker) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
var v bool
|
||||
if err := d.DecodeElement(&v, &start); err != nil {
|
||||
return err
|
||||
}
|
||||
*b = ExpireDeleteMarker{val: v, set: true}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ExpirationDate is a embedded type containing time.Time to unmarshal
|
||||
// Date in Expiration
|
||||
type ExpirationDate struct {
|
||||
time.Time
|
||||
}
|
||||
|
||||
// MarshalXML encodes expiration date if it is non-zero and encodes
|
||||
// empty string otherwise
|
||||
func (eDate ExpirationDate) MarshalXML(e *xml.Encoder, startElement xml.StartElement) error {
|
||||
if eDate.Time.IsZero() {
|
||||
return nil
|
||||
}
|
||||
return e.EncodeElement(eDate.Format(time.RFC3339), startElement)
|
||||
}
|
||||
|
||||
// Transition - transition actions for a rule in lifecycle configuration.
|
||||
type Transition struct {
|
||||
XMLName xml.Name `xml:"Transition"`
|
||||
Days int `xml:"Days,omitempty"`
|
||||
Date time.Time `xml:"Date,omitempty"`
|
||||
StorageClass string `xml:"StorageClass,omitempty"`
|
||||
|
||||
set bool
|
||||
}
|
||||
|
||||
// MarshalXML encodes transition field into an XML form.
|
||||
func (t Transition) MarshalXML(enc *xml.Encoder, start xml.StartElement) error {
|
||||
if !t.set {
|
||||
return nil
|
||||
}
|
||||
type transitionWrapper Transition
|
||||
return enc.EncodeElement(transitionWrapper(t), start)
|
||||
}
|
||||
|
||||
func (t *Transition) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
type wrapper Transition
|
||||
var w wrapper
|
||||
if err := d.DecodeElement(&w, &start); err != nil {
|
||||
return err
|
||||
}
|
||||
*t = Transition(w)
|
||||
t.set = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// TransitionDays is a type alias to unmarshal Days in Transition
|
||||
type TransitionDays int
|
||||
|
||||
// NoncurrentVersionExpiration - expiration actions for non-current object versions.
|
||||
type NoncurrentVersionExpiration struct {
|
||||
XMLName xml.Name `xml:"NoncurrentVersionExpiration"`
|
||||
NoncurrentDays int `xml:"NoncurrentDays,omitempty"`
|
||||
NewerNoncurrentVersions int `xml:"NewerNoncurrentVersions,omitempty"`
|
||||
|
||||
set bool
|
||||
}
|
||||
|
||||
// MarshalXML encodes NoncurrentVersionExpiration field into an XML form.
|
||||
func (n NoncurrentVersionExpiration) MarshalXML(enc *xml.Encoder, start xml.StartElement) error {
|
||||
if !n.set {
|
||||
return nil
|
||||
}
|
||||
type wrapper NoncurrentVersionExpiration
|
||||
return enc.EncodeElement(wrapper(n), start)
|
||||
}
|
||||
|
||||
func (n *NoncurrentVersionExpiration) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
type wrapper NoncurrentVersionExpiration
|
||||
var w wrapper
|
||||
if err := d.DecodeElement(&w, &start); err != nil {
|
||||
return err
|
||||
}
|
||||
*n = NoncurrentVersionExpiration(w)
|
||||
n.set = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// NoncurrentVersionTransition - transition actions for non-current object versions.
|
||||
type NoncurrentVersionTransition struct {
|
||||
XMLName xml.Name `xml:"NoncurrentVersionTransition"`
|
||||
NoncurrentDays int `xml:"NoncurrentDays,omitempty"`
|
||||
StorageClass string `xml:"StorageClass,omitempty"`
|
||||
|
||||
set bool
|
||||
}
|
||||
|
||||
// MarshalXML encodes NoncurrentVersionTransition field into an XML form.
|
||||
func (n NoncurrentVersionTransition) MarshalXML(enc *xml.Encoder, start xml.StartElement) error {
|
||||
if !n.set {
|
||||
return nil
|
||||
}
|
||||
type wrapper NoncurrentVersionTransition
|
||||
return enc.EncodeElement(wrapper(n), start)
|
||||
}
|
||||
|
||||
func (n *NoncurrentVersionTransition) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
type wrapper NoncurrentVersionTransition
|
||||
var w wrapper
|
||||
if err := d.DecodeElement(&w, &start); err != nil {
|
||||
return err
|
||||
}
|
||||
*n = NoncurrentVersionTransition(w)
|
||||
n.set = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// AbortIncompleteMultipartUpload - abort action for incomplete multipart uploads.
|
||||
type AbortIncompleteMultipartUpload struct {
|
||||
XMLName xml.Name `xml:"AbortIncompleteMultipartUpload"`
|
||||
DaysAfterInitiation int `xml:"DaysAfterInitiation,omitempty"`
|
||||
|
||||
set bool
|
||||
}
|
||||
|
||||
// MarshalXML encodes AbortIncompleteMultipartUpload field into an XML form.
|
||||
func (a AbortIncompleteMultipartUpload) MarshalXML(enc *xml.Encoder, start xml.StartElement) error {
|
||||
if !a.set {
|
||||
return nil
|
||||
}
|
||||
type wrapper AbortIncompleteMultipartUpload
|
||||
return enc.EncodeElement(wrapper(a), start)
|
||||
}
|
||||
|
||||
func (a *AbortIncompleteMultipartUpload) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
type wrapper AbortIncompleteMultipartUpload
|
||||
var w wrapper
|
||||
if err := d.DecodeElement(&w, &start); err != nil {
|
||||
return err
|
||||
}
|
||||
*a = AbortIncompleteMultipartUpload(w)
|
||||
a.set = true
|
||||
return nil
|
||||
}
|
||||
Reference in new issue
Block a user