diff --git a/pkg/bucket/lifecycle/and.go b/pkg/bucket/lifecycle/and.go
index c1ef17ca2..36f7cd833 100644
--- a/pkg/bucket/lifecycle/and.go
+++ b/pkg/bucket/lifecycle/and.go
@@ -20,22 +20,33 @@ import (
"encoding/xml"
)
+var errDuplicateTagKey = Errorf("Duplicate Tag Keys are not allowed")
+
// And - a tag to combine a prefix and multiple tags for lifecycle configuration rule.
type And struct {
XMLName xml.Name `xml:"And"`
- Prefix string `xml:"Prefix,omitempty"`
+ Prefix Prefix `xml:"Prefix,omitempty"`
Tags []Tag `xml:"Tag,omitempty"`
}
-var errDuplicateTagKey = Errorf("Duplicate Tag Keys are not allowed")
-
// isEmpty returns true if Tags field is null
func (a And) isEmpty() bool {
- return len(a.Tags) == 0 && a.Prefix == ""
+ return len(a.Tags) == 0 && !a.Prefix.set
}
// Validate - validates the And field
func (a And) Validate() error {
+ emptyPrefix := !a.Prefix.set
+ emptyTags := len(a.Tags) == 0
+
+ if emptyPrefix && emptyTags {
+ return nil
+ }
+
+ if emptyPrefix && !emptyTags || !emptyPrefix && emptyTags {
+ return errXMLNotWellFormed
+ }
+
if a.ContainsDuplicateTag() {
return errDuplicateTagKey
}
diff --git a/pkg/bucket/lifecycle/filter.go b/pkg/bucket/lifecycle/filter.go
index e93458723..fe1d1f661 100644
--- a/pkg/bucket/lifecycle/filter.go
+++ b/pkg/bucket/lifecycle/filter.go
@@ -18,6 +18,7 @@ package lifecycle
import (
"encoding/xml"
+ "io"
)
var (
@@ -27,10 +28,14 @@ var (
// Filter - a filter for a lifecycle configuration Rule.
type Filter struct {
XMLName xml.Name `xml:"Filter"`
- Prefix string
- And And
- Tag Tag
+ Prefix Prefix
+
+ And And
+ andSet bool
+
+ Tag Tag
+ tagSet bool
// Caching tags, only once
cachedTags []string
}
@@ -61,11 +66,62 @@ func (f Filter) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
return e.EncodeToken(xml.EndElement{Name: start.Name})
}
+// UnmarshalXML - decodes XML data.
+func (f *Filter) UnmarshalXML(d *xml.Decoder, start xml.StartElement) (err error) {
+ for {
+ // Read tokens from the XML document in a stream.
+ t, err := d.Token()
+ if err != nil {
+ if err == io.EOF {
+ break
+ }
+ return err
+ }
+
+ switch se := t.(type) {
+ case xml.StartElement:
+ switch se.Name.Local {
+ case "Prefix":
+ var p Prefix
+ if err = d.DecodeElement(&p, &se); err != nil {
+ return err
+ }
+ f.Prefix = p
+ case "And":
+ var and And
+ if err = d.DecodeElement(&and, &se); err != nil {
+ return err
+ }
+ f.And = and
+ f.andSet = true
+ case "Tag":
+ var tag Tag
+ if err = d.DecodeElement(&tag, &se); err != nil {
+ return err
+ }
+ f.Tag = tag
+ f.tagSet = true
+ default:
+ return errUnknownXMLTag
+ }
+ }
+ }
+ return nil
+}
+
+// IsEmpty returns true if Filter is not specified in the XML
+func (f Filter) IsEmpty() bool {
+ return !f.Prefix.set && !f.andSet && !f.tagSet
+}
+
// Validate - validates the filter element
func (f Filter) Validate() error {
+ if !f.Prefix.set && !f.andSet && !f.tagSet {
+ return errXMLNotWellFormed
+ }
// A Filter must have exactly one of Prefix, Tag, or And specified.
if !f.And.isEmpty() {
- if f.Prefix != "" {
+ if f.Prefix.set {
return errInvalidFilter
}
if !f.Tag.IsEmpty() {
@@ -75,12 +131,15 @@ func (f Filter) Validate() error {
return err
}
}
- if f.Prefix != "" {
+ if f.Prefix.set {
if !f.Tag.IsEmpty() {
return errInvalidFilter
}
}
if !f.Tag.IsEmpty() {
+ if f.Prefix.set {
+ return errInvalidFilter
+ }
if err := f.Tag.Validate(); err != nil {
return err
}
diff --git a/pkg/bucket/lifecycle/filter_test.go b/pkg/bucket/lifecycle/filter_test.go
index a398fce3d..d9cd5959e 100644
--- a/pkg/bucket/lifecycle/filter_test.go
+++ b/pkg/bucket/lifecycle/filter_test.go
@@ -35,7 +35,7 @@ func TestUnsupportedFilters(t *testing.T) {
key-prefix
`,
- expectedErr: nil,
+ expectedErr: errXMLNotWellFormed,
},
{ // Filter with Tag tags
inputXML: `
@@ -85,6 +85,7 @@ func TestUnsupportedFilters(t *testing.T) {
{ // Filter with And and multiple Tag tags
inputXML: `
+
key1
value1
diff --git a/pkg/bucket/lifecycle/lifecycle.go b/pkg/bucket/lifecycle/lifecycle.go
index c9fbad14f..eef08bd21 100644
--- a/pkg/bucket/lifecycle/lifecycle.go
+++ b/pkg/bucket/lifecycle/lifecycle.go
@@ -79,16 +79,16 @@ func (lc Lifecycle) HasActiveRules(prefix string, recursive bool) bool {
continue
}
- if len(prefix) > 0 && len(rule.Filter.Prefix) > 0 {
+ if len(prefix) > 0 && len(rule.GetPrefix()) > 0 {
if !recursive {
// If not recursive, incoming prefix must be in rule prefix
- if !strings.HasPrefix(prefix, rule.Filter.Prefix) {
+ if !strings.HasPrefix(prefix, rule.GetPrefix()) {
continue
}
}
if recursive {
// If recursive, we can skip this rule if it doesn't match the tested prefix.
- if !strings.HasPrefix(prefix, rule.Filter.Prefix) && !strings.HasPrefix(rule.Filter.Prefix, prefix) {
+ if !strings.HasPrefix(prefix, rule.GetPrefix()) && !strings.HasPrefix(rule.GetPrefix(), prefix) {
continue
}
}
@@ -167,7 +167,7 @@ func (lc Lifecycle) FilterActionableRules(obj ObjectOpts) []Rule {
if rule.Status == Disabled {
continue
}
- if !strings.HasPrefix(obj.Name, rule.Prefix()) {
+ if !strings.HasPrefix(obj.Name, rule.GetPrefix()) {
continue
}
// Indicates whether MinIO will remove a delete marker with no
diff --git a/pkg/bucket/lifecycle/lifecycle_test.go b/pkg/bucket/lifecycle/lifecycle_test.go
index e4ad92dd5..101336d98 100644
--- a/pkg/bucket/lifecycle/lifecycle_test.go
+++ b/pkg/bucket/lifecycle/lifecycle_test.go
@@ -25,72 +25,6 @@ import (
)
func TestParseAndValidateLifecycleConfig(t *testing.T) {
- // Test for lifecycle config with more than 1000 rules
- var manyRules []Rule
- for i := 0; i < 1001; i++ {
- rule := Rule{
- ID: fmt.Sprintf("toManyRule%d", i),
- Status: "Enabled",
- Expiration: Expiration{Days: ExpirationDays(i)},
- }
- manyRules = append(manyRules, rule)
- }
-
- manyRuleLcConfig, err := xml.Marshal(Lifecycle{Rules: manyRules})
- if err != nil {
- t.Fatal("Failed to marshal lifecycle config with more than 1000 rules")
- }
-
- // Test for lifecycle config with rules containing overlapping prefixes
- rule1 := Rule{
- ID: "rule1",
- Status: "Enabled",
- Expiration: Expiration{Days: ExpirationDays(3)},
- Filter: Filter{
- Prefix: "/a/b",
- },
- }
- rule2 := Rule{
- ID: "rule2",
- Status: "Enabled",
- Expiration: Expiration{Days: ExpirationDays(3)},
- Filter: Filter{
- And: And{
- Prefix: "/a/b/c",
- },
- },
- }
- overlappingRules := []Rule{rule1, rule2}
- overlappingLcConfig, err := xml.Marshal(Lifecycle{Rules: overlappingRules})
- if err != nil {
- t.Fatal("Failed to marshal lifecycle config with rules having overlapping prefix")
- }
-
- // Test for lifecycle rules with duplicate IDs
- rule3 := Rule{
- ID: "duplicateID",
- Status: "Enabled",
- Expiration: Expiration{Days: ExpirationDays(3)},
- Filter: Filter{
- Prefix: "/a/b",
- },
- }
- rule4 := Rule{
- ID: "duplicateID",
- Status: "Enabled",
- Expiration: Expiration{Days: ExpirationDays(4)},
- Filter: Filter{
- And: And{
- Prefix: "/x/z",
- },
- },
- }
- duplicateIDRules := []Rule{rule3, rule4}
- duplicateIDLcConfig, err := xml.Marshal(Lifecycle{Rules: duplicateIDRules})
- if err != nil {
- t.Fatal("Failed to marshal lifecycle config of rules with duplicate ID.")
- }
-
testCases := []struct {
inputConfig string
expectedParsingErr error
@@ -136,21 +70,28 @@ func TestParseAndValidateLifecycleConfig(t *testing.T) {
expectedParsingErr: nil,
expectedValidationErr: errLifecycleNoRule,
},
- { // lifecycle config with more than 1000 rules
- inputConfig: string(manyRuleLcConfig),
- expectedParsingErr: nil,
- expectedValidationErr: errLifecycleTooManyRules,
- },
{ // lifecycle config with rules having overlapping prefix
- inputConfig: string(overlappingLcConfig),
+ inputConfig: `rule1Enabled/a/b3rule2Enabled/a/b/ckey1val13 `,
expectedParsingErr: nil,
expectedValidationErr: nil,
},
{ // lifecycle config with rules having duplicate ID
- inputConfig: string(duplicateIDLcConfig),
+ inputConfig: `duplicateIDEnabled/a/b3duplicateIDEnabled/x/zkey1val14`,
expectedParsingErr: nil,
expectedValidationErr: errLifecycleDuplicateID,
},
+ // Missing in
+ {
+ inputConfig: `sample-rule-2/a/b/cEnabled1`,
+ expectedParsingErr: nil,
+ expectedValidationErr: errXMLNotWellFormed,
+ },
+ // Legitimate lifecycle
+ {
+ inputConfig: `ruleEnabled1`,
+ expectedParsingErr: nil,
+ expectedValidationErr: nil,
+ },
}
for i, tc := range testCases {
@@ -181,17 +122,17 @@ func TestMarshalLifecycleConfig(t *testing.T) {
Rules: []Rule{
{
Status: "Enabled",
- Filter: Filter{Prefix: "prefix-1"},
+ Filter: Filter{Prefix: Prefix{string: "prefix-1", set: true}},
Expiration: Expiration{Days: ExpirationDays(3)},
},
{
Status: "Enabled",
- Filter: Filter{Prefix: "prefix-1"},
+ Filter: Filter{Prefix: Prefix{string: "prefix-1", set: true}},
Expiration: Expiration{Date: ExpirationDate(midnightTS)},
},
{
Status: "Enabled",
- Filter: Filter{Prefix: "prefix-1"},
+ Filter: Filter{Prefix: Prefix{string: "prefix-1", set: true}},
Expiration: Expiration{Date: ExpirationDate(midnightTS)},
NoncurrentVersionTransition: NoncurrentVersionTransition{NoncurrentDays: 2, StorageClass: "TEST"},
},
diff --git a/pkg/bucket/lifecycle/noncurrentversion.go b/pkg/bucket/lifecycle/noncurrentversion.go
index d1912cdfc..acbaf24ac 100644
--- a/pkg/bucket/lifecycle/noncurrentversion.go
+++ b/pkg/bucket/lifecycle/noncurrentversion.go
@@ -1,5 +1,5 @@
/*
- * MinIO Cloud Storage, (C) 2019 MinIO, Inc.
+ * MinIO Cloud Storage, (C) 2019-2020 MinIO, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -24,12 +24,7 @@ import (
type NoncurrentVersionExpiration struct {
XMLName xml.Name `xml:"NoncurrentVersionExpiration"`
NoncurrentDays ExpirationDays `xml:"NoncurrentDays,omitempty"`
-}
-
-// NoncurrentVersionTransition - an action for lifecycle configuration rule.
-type NoncurrentVersionTransition struct {
- NoncurrentDays ExpirationDays `xml:"NoncurrentDays"`
- StorageClass string `xml:"StorageClass"`
+ set bool
}
// MarshalXML if non-current days not set to non zero value
@@ -41,11 +36,43 @@ func (n NoncurrentVersionExpiration) MarshalXML(e *xml.Encoder, start xml.StartE
return e.EncodeElement(noncurrentVersionExpirationWrapper(n), start)
}
+// UnmarshalXML decodes NoncurrentVersionExpiration
+func (n *NoncurrentVersionExpiration) UnmarshalXML(d *xml.Decoder, startElement xml.StartElement) error {
+ type noncurrentVersionExpirationWrapper NoncurrentVersionExpiration
+ var val noncurrentVersionExpirationWrapper
+ err := d.DecodeElement(&val, &startElement)
+ if err != nil {
+ return err
+ }
+ *n = NoncurrentVersionExpiration(val)
+ n.set = true
+ return nil
+}
+
// IsDaysNull returns true if days field is null
func (n NoncurrentVersionExpiration) IsDaysNull() bool {
return n.NoncurrentDays == ExpirationDays(0)
}
+// Validate returns an error with wrong value
+func (n NoncurrentVersionExpiration) Validate() error {
+ if !n.set {
+ return nil
+ }
+ val := int(n.NoncurrentDays)
+ if val <= 0 {
+ return errXMLNotWellFormed
+ }
+ return nil
+}
+
+// NoncurrentVersionTransition - an action for lifecycle configuration rule.
+type NoncurrentVersionTransition struct {
+ NoncurrentDays ExpirationDays `xml:"NoncurrentDays"`
+ StorageClass string `xml:"StorageClass"`
+ set bool
+}
+
// MarshalXML is extended to leave out
// tags
func (n NoncurrentVersionTransition) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
@@ -60,3 +87,27 @@ func (n NoncurrentVersionTransition) MarshalXML(e *xml.Encoder, start xml.StartE
func (n NoncurrentVersionTransition) IsDaysNull() bool {
return n.NoncurrentDays == ExpirationDays(0)
}
+
+// UnmarshalXML decodes NoncurrentVersionExpiration
+func (n *NoncurrentVersionTransition) UnmarshalXML(d *xml.Decoder, startElement xml.StartElement) error {
+ type noncurrentVersionTransitionWrapper NoncurrentVersionTransition
+ var val noncurrentVersionTransitionWrapper
+ err := d.DecodeElement(&val, &startElement)
+ if err != nil {
+ return err
+ }
+ *n = NoncurrentVersionTransition(val)
+ n.set = true
+ return nil
+}
+
+// Validate returns an error with wrong value
+func (n NoncurrentVersionTransition) Validate() error {
+ if !n.set {
+ return nil
+ }
+ if int(n.NoncurrentDays) <= 0 || n.StorageClass == "" {
+ return errXMLNotWellFormed
+ }
+ return nil
+}
diff --git a/pkg/bucket/lifecycle/prefix.go b/pkg/bucket/lifecycle/prefix.go
new file mode 100644
index 000000000..4a7de225b
--- /dev/null
+++ b/pkg/bucket/lifecycle/prefix.go
@@ -0,0 +1,50 @@
+/*
+ * MinIO Cloud Storage, (C) 2020 MinIO, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package lifecycle
+
+import (
+ "encoding/xml"
+)
+
+// Prefix holds the prefix xml tag in and
+type Prefix struct {
+ string
+ set bool
+}
+
+// UnmarshalXML - decodes XML data.
+func (p *Prefix) UnmarshalXML(d *xml.Decoder, start xml.StartElement) (err error) {
+ var s string
+ if err = d.DecodeElement(&s, &start); err != nil {
+ return err
+ }
+ *p = Prefix{string: s, set: true}
+ return nil
+}
+
+// MarshalXML - decodes XML data.
+func (p Prefix) MarshalXML(e *xml.Encoder, startElement xml.StartElement) error {
+ if !p.set {
+ return nil
+ }
+ return e.EncodeElement(p.string, startElement)
+}
+
+// String returns the prefix string
+func (p Prefix) String() string {
+ return p.string
+}
diff --git a/pkg/bucket/lifecycle/rule.go b/pkg/bucket/lifecycle/rule.go
index 66c90e490..7676b1161 100644
--- a/pkg/bucket/lifecycle/rule.go
+++ b/pkg/bucket/lifecycle/rule.go
@@ -38,6 +38,7 @@ type Rule struct {
ID string `xml:"ID,omitempty"`
Status Status `xml:"Status"`
Filter Filter `xml:"Filter,omitempty"`
+ Prefix Prefix `xml:"Prefix,omitempty"`
Expiration Expiration `xml:"Expiration,omitempty"`
Transition Transition `xml:"Transition,omitempty"`
// FIXME: add a type to catch unsupported AbortIncompleteMultipartUpload AbortIncompleteMultipartUpload `xml:"AbortIncompleteMultipartUpload,omitempty"`
@@ -92,27 +93,44 @@ func (r Rule) validateStatus() error {
return nil
}
-func (r Rule) validateAction() error {
+func (r Rule) validateExpiration() error {
return r.Expiration.Validate()
}
-func (r Rule) validateFilter() error {
- return r.Filter.Validate()
+func (r Rule) validateNoncurrentExpiration() error {
+ return r.NoncurrentVersionExpiration.Validate()
+}
+
+func (r Rule) validatePrefixAndFilter() error {
+ if !r.Prefix.set && r.Filter.IsEmpty() || r.Prefix.set && !r.Filter.IsEmpty() {
+ return errXMLNotWellFormed
+ }
+ if !r.Prefix.set {
+ return r.Filter.Validate()
+ }
+ return nil
}
func (r Rule) validateTransition() error {
return r.Transition.Validate()
}
-// Prefix - a rule can either have prefix under or under
-// . This method returns the prefix from the
-// location where it is available
-func (r Rule) Prefix() string {
- if r.Filter.Prefix != "" {
- return r.Filter.Prefix
+func (r Rule) validateNoncurrentTransition() error {
+ return r.NoncurrentVersionTransition.Validate()
+}
+
+// GetPrefix - a rule can either have prefix under ,
+// or under . This method returns the prefix from the
+// location where it is available.
+func (r Rule) GetPrefix() string {
+ if p := r.Prefix.String(); p != "" {
+ return p
}
- if r.Filter.And.Prefix != "" {
- return r.Filter.And.Prefix
+ if p := r.Filter.Prefix.String(); p != "" {
+ return p
+ }
+ if p := r.Filter.And.Prefix.String(); p != "" {
+ return p
}
return ""
}
@@ -145,14 +163,23 @@ func (r Rule) Validate() error {
if err := r.validateStatus(); err != nil {
return err
}
- if err := r.validateAction(); err != nil {
+ if err := r.validateExpiration(); err != nil {
return err
}
- if err := r.validateFilter(); err != nil {
+ if err := r.validateNoncurrentExpiration(); err != nil {
+ return err
+ }
+ if err := r.validatePrefixAndFilter(); err != nil {
return err
}
if err := r.validateTransition(); err != nil {
return err
}
+ if err := r.validateNoncurrentTransition(); err != nil {
+ return err
+ }
+ if !r.Expiration.set && !r.Transition.set && !r.NoncurrentVersionExpiration.set && !r.NoncurrentVersionTransition.set {
+ return errXMLNotWellFormed
+ }
return nil
}
diff --git a/pkg/bucket/lifecycle/rule_test.go b/pkg/bucket/lifecycle/rule_test.go
index c3e7a1e48..bb805b3cb 100644
--- a/pkg/bucket/lifecycle/rule_test.go
+++ b/pkg/bucket/lifecycle/rule_test.go
@@ -38,6 +38,7 @@ func TestInvalidRules(t *testing.T) {
{ // Rule with empty ID
inputXML: `
+
365