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
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)
|
|
}
|