* s3api: extend lifecycle XML types with NoncurrentVersionExpiration, AbortIncompleteMultipartUpload Add missing S3 lifecycle rule types to the XML data model: - NoncurrentVersionExpiration with NoncurrentDays and NewerNoncurrentVersions - NoncurrentVersionTransition with NoncurrentDays and StorageClass - AbortIncompleteMultipartUpload with DaysAfterInitiation - Filter.ObjectSizeGreaterThan and ObjectSizeLessThan - And.ObjectSizeGreaterThan and ObjectSizeLessThan - Filter.UnmarshalXML to properly parse Tag, And, and size filter elements Each new type follows the existing set-field pattern for conditional XML marshaling. No behavior changes - these types are not yet wired into handlers or the lifecycle worker. * s3lifecycle: add lifecycle rule evaluator package New package weed/s3api/s3lifecycle/ provides a pure-function lifecycle rule evaluation engine. The evaluator accepts flattened Rule structs and ObjectInfo metadata, and returns the appropriate Action. Components: - evaluator.go: Evaluate() for per-object actions with S3 priority ordering (delete marker > noncurrent version > current expiration), ShouldExpireNoncurrentVersion() with NewerNoncurrentVersions support, EvaluateMPUAbort() for multipart upload rules - filter.go: prefix, tag, and size-based filter matching - tags.go: ExtractTags() extracts S3 tags from filer Extended metadata, HasTagRules() for scan-time optimization - version_time.go: GetVersionTimestamp() extracts timestamps from SeaweedFS version IDs (both old and new format) Comprehensive test coverage: 54 tests covering all action types, filter combinations, edge cases, and version ID formats. * s3api: add UnmarshalXML for Expiration, Transition, ExpireDeleteMarker Add UnmarshalXML methods that set the internal 'set' flag during XML parsing. Previously these flags were only set programmatically, causing XML round-trip to drop elements. This ensures lifecycle configurations stored as XML survive unmarshal/marshal cycles correctly. Add comprehensive XML round-trip tests for all lifecycle rule types including NoncurrentVersionExpiration, AbortIncompleteMultipartUpload, Filter with Tag/And/size constraints, and a complete Terraform-style lifecycle configuration. * s3lifecycle: address review feedback - Fix version_time.go overflow: guard timestampPart > MaxInt64 before the inversion subtraction to prevent uint64 wrap - Make all expiry checks inclusive (!now.Before instead of now.After) so actions trigger at the exact scheduled instant - Add NoncurrentIndex to ObjectInfo so Evaluate() can properly handle NewerNoncurrentVersions via ShouldExpireNoncurrentVersion() - Add test for high-bit overflow version ID * s3lifecycle: guard ShouldExpireNoncurrentVersion against zero SuccessorModTime Add early return when obj.IsLatest or obj.SuccessorModTime.IsZero() to prevent premature expiration of versions with uninitialized successor timestamps (zero value would compute to epoch, always expired). --------- Co-authored-by: Copilot <copilot@github.com>
128 lines
3.9 KiB
Go
128 lines
3.9 KiB
Go
package s3lifecycle
|
|
|
|
import "time"
|
|
|
|
// Evaluate checks the given lifecycle rules against an object and returns
|
|
// the highest-priority action that applies. The evaluation follows S3's
|
|
// action priority:
|
|
// 1. ExpiredObjectDeleteMarker (delete marker is sole version)
|
|
// 2. NoncurrentVersionExpiration (non-current version age/count)
|
|
// 3. Current version Expiration (Days or Date)
|
|
//
|
|
// AbortIncompleteMultipartUpload is evaluated separately since it applies
|
|
// to uploads, not objects. Use EvaluateMPUAbort for that.
|
|
func Evaluate(rules []Rule, obj ObjectInfo, now time.Time) EvalResult {
|
|
// Phase 1: ExpiredObjectDeleteMarker
|
|
if obj.IsDeleteMarker && obj.IsLatest && obj.NumVersions == 1 {
|
|
for _, rule := range rules {
|
|
if rule.Status != "Enabled" {
|
|
continue
|
|
}
|
|
if !matchesFilter(rule, obj) {
|
|
continue
|
|
}
|
|
if rule.ExpiredObjectDeleteMarker {
|
|
return EvalResult{Action: ActionExpireDeleteMarker, RuleID: rule.ID}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Phase 2: NoncurrentVersionExpiration
|
|
if !obj.IsLatest && !obj.SuccessorModTime.IsZero() {
|
|
for _, rule := range rules {
|
|
if ShouldExpireNoncurrentVersion(rule, obj, obj.NoncurrentIndex, now) {
|
|
return EvalResult{Action: ActionDeleteVersion, RuleID: rule.ID}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Phase 3: Current version Expiration
|
|
if obj.IsLatest && !obj.IsDeleteMarker {
|
|
for _, rule := range rules {
|
|
if rule.Status != "Enabled" {
|
|
continue
|
|
}
|
|
if !matchesFilter(rule, obj) {
|
|
continue
|
|
}
|
|
// Date-based expiration
|
|
if !rule.ExpirationDate.IsZero() && !now.Before(rule.ExpirationDate) {
|
|
return EvalResult{Action: ActionDeleteObject, RuleID: rule.ID}
|
|
}
|
|
// Days-based expiration
|
|
if rule.ExpirationDays > 0 {
|
|
expiryTime := expectedExpiryTime(obj.ModTime, rule.ExpirationDays)
|
|
if !now.Before(expiryTime) {
|
|
return EvalResult{Action: ActionDeleteObject, RuleID: rule.ID}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return EvalResult{Action: ActionNone}
|
|
}
|
|
|
|
// ShouldExpireNoncurrentVersion checks whether a non-current version should
|
|
// be expired considering both NoncurrentDays and NewerNoncurrentVersions.
|
|
// noncurrentIndex is the 0-based position among non-current versions sorted
|
|
// newest-first (0 = newest non-current version).
|
|
func ShouldExpireNoncurrentVersion(rule Rule, obj ObjectInfo, noncurrentIndex int, now time.Time) bool {
|
|
if rule.Status != "Enabled" {
|
|
return false
|
|
}
|
|
if rule.NoncurrentVersionExpirationDays <= 0 {
|
|
return false
|
|
}
|
|
if obj.IsLatest || obj.SuccessorModTime.IsZero() {
|
|
return false
|
|
}
|
|
if !matchesFilter(rule, obj) {
|
|
return false
|
|
}
|
|
|
|
// Check age threshold.
|
|
expiryTime := expectedExpiryTime(obj.SuccessorModTime, rule.NoncurrentVersionExpirationDays)
|
|
if now.Before(expiryTime) {
|
|
return false
|
|
}
|
|
|
|
// Check NewerNoncurrentVersions count threshold.
|
|
if rule.NewerNoncurrentVersions > 0 && noncurrentIndex < rule.NewerNoncurrentVersions {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// EvaluateMPUAbort finds the applicable AbortIncompleteMultipartUpload rule
|
|
// for a multipart upload with the given key prefix and creation time.
|
|
func EvaluateMPUAbort(rules []Rule, uploadKey string, createdAt time.Time, now time.Time) EvalResult {
|
|
for _, rule := range rules {
|
|
if rule.Status != "Enabled" {
|
|
continue
|
|
}
|
|
if rule.AbortMPUDaysAfterInitiation <= 0 {
|
|
continue
|
|
}
|
|
if !matchesPrefix(rule.Prefix, uploadKey) {
|
|
continue
|
|
}
|
|
cutoff := createdAt.Add(time.Duration(rule.AbortMPUDaysAfterInitiation) * 24 * time.Hour)
|
|
if !now.Before(cutoff) {
|
|
return EvalResult{Action: ActionAbortMultipartUpload, RuleID: rule.ID}
|
|
}
|
|
}
|
|
return EvalResult{Action: ActionNone}
|
|
}
|
|
|
|
// expectedExpiryTime computes the expiration time given a reference time and
|
|
// a number of days. Following S3 semantics, expiration happens at midnight UTC
|
|
// of the day after the specified number of days.
|
|
func expectedExpiryTime(refTime time.Time, days int) time.Time {
|
|
if days == 0 {
|
|
return refTime
|
|
}
|
|
t := refTime.UTC().Add(time.Duration(days+1) * 24 * time.Hour)
|
|
return t.Truncate(24 * time.Hour)
|
|
}
|