You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

199 lines
6.6 KiB

package lifecycle
import (
"bytes"
"context"
"encoding/xml"
"errors"
"fmt"
"time"
"github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
"github.com/seaweedfs/seaweedfs/weed/s3api/s3lifecycle"
)
// lifecycleConfig mirrors the XML structure just enough to parse rules.
// We define a minimal local struct to avoid importing the s3api package
// (which would create a circular dependency if s3api ever imports the worker).
type lifecycleConfig struct {
XMLName xml.Name `xml:"LifecycleConfiguration"`
Rules []lifecycleConfigRule `xml:"Rule"`
}
type lifecycleConfigRule struct {
ID string `xml:"ID"`
Status string `xml:"Status"`
Filter lifecycleFilter `xml:"Filter"`
Prefix string `xml:"Prefix"`
Expiration lifecycleExpiration `xml:"Expiration"`
NoncurrentVersionExpiration noncurrentVersionExpiration `xml:"NoncurrentVersionExpiration"`
AbortIncompleteMultipartUpload abortMPU `xml:"AbortIncompleteMultipartUpload"`
}
type lifecycleFilter struct {
Prefix string `xml:"Prefix"`
Tag lifecycleTag `xml:"Tag"`
And lifecycleAnd `xml:"And"`
ObjectSizeGreaterThan int64 `xml:"ObjectSizeGreaterThan"`
ObjectSizeLessThan int64 `xml:"ObjectSizeLessThan"`
}
type lifecycleAnd struct {
Prefix string `xml:"Prefix"`
Tags []lifecycleTag `xml:"Tag"`
ObjectSizeGreaterThan int64 `xml:"ObjectSizeGreaterThan"`
ObjectSizeLessThan int64 `xml:"ObjectSizeLessThan"`
}
type lifecycleTag struct {
Key string `xml:"Key"`
Value string `xml:"Value"`
}
type lifecycleExpiration struct {
Days int `xml:"Days"`
Date string `xml:"Date"`
ExpiredObjectDeleteMarker bool `xml:"ExpiredObjectDeleteMarker"`
}
type noncurrentVersionExpiration struct {
NoncurrentDays int `xml:"NoncurrentDays"`
NewerNoncurrentVersions int `xml:"NewerNoncurrentVersions"`
}
type abortMPU struct {
DaysAfterInitiation int `xml:"DaysAfterInitiation"`
}
// errMalformedLifecycleXML indicates the lifecycle XML exists but could not be parsed.
// Callers should fail closed (not fall back to TTL) to avoid broader deletions.
var errMalformedLifecycleXML = errors.New("malformed lifecycle XML")
// loadLifecycleRulesFromBucket reads the lifecycle XML from a bucket's
// metadata and converts it to evaluator-friendly rules.
//
// Returns:
// - (rules, nil) when lifecycle XML is configured and parseable
// - (nil, nil) when no lifecycle XML is configured (caller should use TTL fallback)
// - (nil, errMalformedLifecycleXML) when XML exists but is malformed (fail closed)
// - (nil, err) for transient filer errors (caller should use TTL fallback with warning)
func loadLifecycleRulesFromBucket(
ctx context.Context,
client filer_pb.SeaweedFilerClient,
bucketsPath, bucket string,
) ([]s3lifecycle.Rule, error) {
bucketDir := bucketsPath
resp, err := filer_pb.LookupEntry(ctx, client, &filer_pb.LookupDirectoryEntryRequest{
Directory: bucketDir,
Name: bucket,
})
if err != nil {
// Transient filer error — not the same as malformed XML.
return nil, fmt.Errorf("lookup bucket %s: %w", bucket, err)
}
if resp.Entry == nil || resp.Entry.Extended == nil {
return nil, nil
}
xmlData := resp.Entry.Extended[lifecycleXMLKey]
if len(xmlData) == 0 {
return nil, nil
}
rules, parseErr := parseLifecycleXML(xmlData)
if parseErr != nil {
return nil, fmt.Errorf("%w: bucket %s: %v", errMalformedLifecycleXML, bucket, parseErr)
}
// Return non-nil empty slice when XML was present but yielded no rules
// (e.g., all rules disabled). This lets callers distinguish "no XML" (nil)
// from "XML present, no effective rules" (empty slice).
if rules == nil {
rules = []s3lifecycle.Rule{}
}
return rules, nil
}
// parseLifecycleXML parses lifecycle configuration XML and converts it
// to evaluator-friendly rules.
func parseLifecycleXML(data []byte) ([]s3lifecycle.Rule, error) {
var config lifecycleConfig
if err := xml.NewDecoder(bytes.NewReader(data)).Decode(&config); err != nil {
return nil, fmt.Errorf("decode lifecycle XML: %w", err)
}
var rules []s3lifecycle.Rule
for _, r := range config.Rules {
rule := s3lifecycle.Rule{
ID: r.ID,
Status: r.Status,
}
// Resolve prefix: Filter.And.Prefix > Filter.Prefix > Rule.Prefix
switch {
case r.Filter.And.Prefix != "" || len(r.Filter.And.Tags) > 0 ||
r.Filter.And.ObjectSizeGreaterThan > 0 || r.Filter.And.ObjectSizeLessThan > 0:
rule.Prefix = r.Filter.And.Prefix
rule.FilterTags = tagsToMap(r.Filter.And.Tags)
rule.FilterSizeGreaterThan = r.Filter.And.ObjectSizeGreaterThan
rule.FilterSizeLessThan = r.Filter.And.ObjectSizeLessThan
case r.Filter.Tag.Key != "":
rule.Prefix = r.Filter.Prefix
rule.FilterTags = map[string]string{r.Filter.Tag.Key: r.Filter.Tag.Value}
rule.FilterSizeGreaterThan = r.Filter.ObjectSizeGreaterThan
rule.FilterSizeLessThan = r.Filter.ObjectSizeLessThan
default:
if r.Filter.Prefix != "" {
rule.Prefix = r.Filter.Prefix
} else {
rule.Prefix = r.Prefix
}
rule.FilterSizeGreaterThan = r.Filter.ObjectSizeGreaterThan
rule.FilterSizeLessThan = r.Filter.ObjectSizeLessThan
}
rule.ExpirationDays = r.Expiration.Days
rule.ExpiredObjectDeleteMarker = r.Expiration.ExpiredObjectDeleteMarker
rule.NoncurrentVersionExpirationDays = r.NoncurrentVersionExpiration.NoncurrentDays
rule.NewerNoncurrentVersions = r.NoncurrentVersionExpiration.NewerNoncurrentVersions
rule.AbortMPUDaysAfterInitiation = r.AbortIncompleteMultipartUpload.DaysAfterInitiation
// Parse Date if present.
if r.Expiration.Date != "" {
// Date may be RFC3339 or ISO 8601 date-only.
parsed, parseErr := parseExpirationDate(r.Expiration.Date)
if parseErr != nil {
glog.V(1).Infof("s3_lifecycle: skipping rule %s: invalid expiration date %q: %v", r.ID, r.Expiration.Date, parseErr)
continue
}
rule.ExpirationDate = parsed
}
rules = append(rules, rule)
}
return rules, nil
}
func tagsToMap(tags []lifecycleTag) map[string]string {
if len(tags) == 0 {
return nil
}
m := make(map[string]string, len(tags))
for _, t := range tags {
m[t.Key] = t.Value
}
return m
}
func parseExpirationDate(s string) (time.Time, error) {
// Try RFC3339 first, then ISO 8601 date-only.
formats := []string{
"2006-01-02T15:04:05Z07:00",
"2006-01-02",
}
for _, f := range formats {
t, err := time.Parse(f, s)
if err == nil {
return t, nil
}
}
return time.Time{}, fmt.Errorf("unrecognized date format: %s", s)
}