* lifecycle worker: drive MPU abort from lifecycle rules Update the multipart upload abort phase to read AbortIncompleteMultipartUpload.DaysAfterInitiation from the parsed lifecycle rules. Falls back to the worker config abort_mpu_days when no lifecycle XML rule specifies the value. This means per-bucket MPU abort thresholds are now respected when set via PutBucketLifecycleConfiguration, instead of using a single global worker config value for all buckets. * lifecycle worker: only use config AbortMPUDays when no lifecycle XML exists When a bucket has lifecycle XML (useRuleEval=true) but no AbortIncompleteMultipartUpload rule, mpuAbortDays should be 0 (no abort), not the worker config default. The config fallback should only apply to buckets without lifecycle XML. * lifecycle worker: only skip .uploads at bucket root * lifecycle worker: use per-upload rule evaluation for MPU abort Replace the single bucket-wide mpuAbortDays with per-upload evaluation using s3lifecycle.EvaluateMPUAbort, which respects each rule's prefix filter and DaysAfterInitiation threshold. Previously the code took the first enabled abort rule's days value and applied it to all uploads, ignoring prefix scoping and multiple rules with different thresholds. Config fallback (abort_mpu_days) now only applies when lifecycle XML is truly absent (xmlPresent=false), not when XML exists but has no abort rules. Also fix EvaluateMPUAbort to use expectedExpiryTime for midnight-UTC semantics matching other lifecycle cutoffs. --------- 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 := expectedExpiryTime(createdAt, rule.AbortMPUDaysAfterInitiation)
|
|
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)
|
|
}
|