storj.io/minio@v0.0.0-20230509071714-0cbc90f649b1/pkg/bucket/lifecycle/lifecycle.go (about) 1 /* 2 * MinIO Cloud Storage, (C) 2019 MinIO, Inc. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package lifecycle 18 19 import ( 20 "encoding/xml" 21 "fmt" 22 "io" 23 "strings" 24 "time" 25 ) 26 27 var ( 28 errLifecycleTooManyRules = Errorf("Lifecycle configuration allows a maximum of 1000 rules") 29 errLifecycleNoRule = Errorf("Lifecycle configuration should have at least one rule") 30 errLifecycleDuplicateID = Errorf("Lifecycle configuration has rule with the same ID. Rule ID must be unique.") 31 errXMLNotWellFormed = Errorf("The XML you provided was not well-formed or did not validate against our published schema") 32 ) 33 34 const ( 35 // TransitionComplete marks completed transition 36 TransitionComplete = "complete" 37 // TransitionPending - transition is yet to be attempted 38 TransitionPending = "pending" 39 ) 40 41 // Action represents a delete action or other transition 42 // actions that will be implemented later. 43 type Action int 44 45 //go:generate stringer -type Action $GOFILE 46 47 const ( 48 // NoneAction means no action required after evaluating lifecycle rules 49 NoneAction Action = iota 50 // DeleteAction means the object needs to be removed after evaluating lifecycle rules 51 DeleteAction 52 // DeleteVersionAction deletes a particular version 53 DeleteVersionAction 54 // TransitionAction transitions a particular object after evaluating lifecycle transition rules 55 TransitionAction 56 //TransitionVersionAction transitions a particular object version after evaluating lifecycle transition rules 57 TransitionVersionAction 58 // DeleteRestoredAction means the temporarily restored object needs to be removed after evaluating lifecycle rules 59 DeleteRestoredAction 60 // DeleteRestoredVersionAction deletes a particular version that was temporarily restored 61 DeleteRestoredVersionAction 62 ) 63 64 // Lifecycle - Configuration for bucket lifecycle. 65 type Lifecycle struct { 66 XMLName xml.Name `xml:"LifecycleConfiguration"` 67 Rules []Rule `xml:"Rule"` 68 } 69 70 // UnmarshalXML - decodes XML data. 71 func (lc *Lifecycle) UnmarshalXML(d *xml.Decoder, start xml.StartElement) (err error) { 72 switch start.Name.Local { 73 case "LifecycleConfiguration", "BucketLifecycleConfiguration": 74 default: 75 return xml.UnmarshalError(fmt.Sprintf("expected element type <LifecycleConfiguration>/<BucketLifecycleConfiguration> but have <%s>", 76 start.Name.Local)) 77 } 78 for { 79 // Read tokens from the XML document in a stream. 80 t, err := d.Token() 81 if err != nil { 82 if err == io.EOF { 83 break 84 } 85 return err 86 } 87 88 switch se := t.(type) { 89 case xml.StartElement: 90 switch se.Name.Local { 91 case "Rule": 92 var r Rule 93 if err = d.DecodeElement(&r, &se); err != nil { 94 return err 95 } 96 lc.Rules = append(lc.Rules, r) 97 default: 98 return xml.UnmarshalError(fmt.Sprintf("expected element type <Rule> but have <%s>", se.Name.Local)) 99 } 100 } 101 } 102 return nil 103 } 104 105 // HasActiveRules - returns whether policy has active rules for. 106 // Optionally a prefix can be supplied. 107 // If recursive is specified the function will also return true if any level below the 108 // prefix has active rules. If no prefix is specified recursive is effectively true. 109 func (lc Lifecycle) HasActiveRules(prefix string, recursive bool) bool { 110 if len(lc.Rules) == 0 { 111 return false 112 } 113 for _, rule := range lc.Rules { 114 if rule.Status == Disabled { 115 continue 116 } 117 118 if len(prefix) > 0 && len(rule.GetPrefix()) > 0 { 119 if !recursive { 120 // If not recursive, incoming prefix must be in rule prefix 121 if !strings.HasPrefix(prefix, rule.GetPrefix()) { 122 continue 123 } 124 } 125 if recursive { 126 // If recursive, we can skip this rule if it doesn't match the tested prefix. 127 if !strings.HasPrefix(prefix, rule.GetPrefix()) && !strings.HasPrefix(rule.GetPrefix(), prefix) { 128 continue 129 } 130 } 131 } 132 133 if rule.NoncurrentVersionExpiration.NoncurrentDays > 0 { 134 return true 135 } 136 if rule.NoncurrentVersionTransition.NoncurrentDays > 0 { 137 return true 138 } 139 if rule.Expiration.IsNull() && rule.Transition.IsNull() { 140 continue 141 } 142 if !rule.Expiration.IsDateNull() && rule.Expiration.Date.Before(time.Now()) { 143 return true 144 } 145 if !rule.Transition.IsDateNull() && rule.Transition.Date.Before(time.Now()) { 146 return true 147 } 148 if !rule.Expiration.IsDaysNull() || !rule.Transition.IsDaysNull() { 149 return true 150 } 151 } 152 return false 153 } 154 155 // ParseLifecycleConfig - parses data in given reader to Lifecycle. 156 func ParseLifecycleConfig(reader io.Reader) (*Lifecycle, error) { 157 var lc Lifecycle 158 if err := xml.NewDecoder(reader).Decode(&lc); err != nil { 159 return nil, err 160 } 161 return &lc, nil 162 } 163 164 // Validate - validates the lifecycle configuration 165 func (lc Lifecycle) Validate() error { 166 // Lifecycle config can't have more than 1000 rules 167 if len(lc.Rules) > 1000 { 168 return errLifecycleTooManyRules 169 } 170 // Lifecycle config should have at least one rule 171 if len(lc.Rules) == 0 { 172 return errLifecycleNoRule 173 } 174 // Validate all the rules in the lifecycle config 175 for _, r := range lc.Rules { 176 if err := r.Validate(); err != nil { 177 return err 178 } 179 } 180 // Make sure Rule ID is unique 181 for i := range lc.Rules { 182 if i == len(lc.Rules)-1 { 183 break 184 } 185 otherRules := lc.Rules[i+1:] 186 for _, otherRule := range otherRules { 187 if lc.Rules[i].ID == otherRule.ID { 188 return errLifecycleDuplicateID 189 } 190 } 191 } 192 return nil 193 } 194 195 // FilterActionableRules returns the rules actions that need to be executed 196 // after evaluating prefix/tag filtering 197 func (lc Lifecycle) FilterActionableRules(obj ObjectOpts) []Rule { 198 if obj.Name == "" { 199 return nil 200 } 201 var rules []Rule 202 for _, rule := range lc.Rules { 203 if rule.Status == Disabled { 204 continue 205 } 206 if !strings.HasPrefix(obj.Name, rule.GetPrefix()) { 207 continue 208 } 209 // Indicates whether MinIO will remove a delete marker with no 210 // noncurrent versions. If set to true, the delete marker will 211 // be expired; if set to false the policy takes no action. This 212 // cannot be specified with Days or Date in a Lifecycle 213 // Expiration Policy. 214 if rule.Expiration.DeleteMarker.val { 215 rules = append(rules, rule) 216 continue 217 } 218 // The NoncurrentVersionExpiration action requests MinIO to expire 219 // noncurrent versions of objects x days after the objects become 220 // noncurrent. 221 if !rule.NoncurrentVersionExpiration.IsDaysNull() { 222 rules = append(rules, rule) 223 continue 224 } 225 // The NoncurrentVersionTransition action requests MinIO to transition 226 // noncurrent versions of objects x days after the objects become 227 // noncurrent. 228 if !rule.NoncurrentVersionTransition.IsDaysNull() { 229 rules = append(rules, rule) 230 continue 231 } 232 233 if rule.Filter.TestTags(strings.Split(obj.UserTags, "&")) { 234 rules = append(rules, rule) 235 } 236 if !rule.Transition.IsNull() { 237 rules = append(rules, rule) 238 } 239 } 240 return rules 241 } 242 243 // ObjectOpts provides information to deduce the lifecycle actions 244 // which can be triggered on the resultant object. 245 type ObjectOpts struct { 246 Name string 247 UserTags string 248 ModTime time.Time 249 VersionID string 250 IsLatest bool 251 DeleteMarker bool 252 NumVersions int 253 SuccessorModTime time.Time 254 TransitionStatus string 255 RestoreOngoing bool 256 RestoreExpires time.Time 257 } 258 259 // ExpiredObjectDeleteMarker returns true if an object version referred to by o 260 // is the only version remaining and is a delete marker. It returns false 261 // otherwise. 262 func (o ObjectOpts) ExpiredObjectDeleteMarker() bool { 263 return o.DeleteMarker && o.NumVersions == 1 264 } 265 266 // ComputeAction returns the action to perform by evaluating all lifecycle rules 267 // against the object name and its modification time. 268 func (lc Lifecycle) ComputeAction(obj ObjectOpts) Action { 269 var action = NoneAction 270 if obj.ModTime.IsZero() { 271 return action 272 } 273 274 for _, rule := range lc.FilterActionableRules(obj) { 275 if obj.ExpiredObjectDeleteMarker() && rule.Expiration.DeleteMarker.val { 276 // Indicates whether MinIO will remove a delete marker with no noncurrent versions. 277 // Only latest marker is removed. If set to true, the delete marker will be expired; 278 // if set to false the policy takes no action. This cannot be specified with Days or 279 // Date in a Lifecycle Expiration Policy. 280 return DeleteVersionAction 281 } 282 283 if !rule.NoncurrentVersionExpiration.IsDaysNull() { 284 if obj.VersionID != "" && !obj.IsLatest && !obj.SuccessorModTime.IsZero() { 285 // Non current versions should be deleted if their age exceeds non current days configuration 286 // https://docs.aws.amazon.com/AmazonS3/latest/dev/intro-lifecycle-rules.html#intro-lifecycle-rules-actions 287 if time.Now().After(ExpectedExpiryTime(obj.SuccessorModTime, int(rule.NoncurrentVersionExpiration.NoncurrentDays))) { 288 return DeleteVersionAction 289 } 290 } 291 292 if obj.VersionID != "" && obj.ExpiredObjectDeleteMarker() { 293 // From https: //docs.aws.amazon.com/AmazonS3/latest/dev/lifecycle-configuration-examples.html : 294 // The NoncurrentVersionExpiration action in the same Lifecycle configuration removes noncurrent objects X days 295 // after they become noncurrent. Thus, in this example, all object versions are permanently removed X days after 296 // object creation. You will have expired object delete markers, but Amazon S3 detects and removes the expired 297 // object delete markers for you. 298 if time.Now().After(ExpectedExpiryTime(obj.ModTime, int(rule.NoncurrentVersionExpiration.NoncurrentDays))) { 299 return DeleteVersionAction 300 } 301 } 302 } 303 304 if !rule.NoncurrentVersionTransition.IsDaysNull() { 305 if obj.VersionID != "" && !obj.IsLatest && !obj.SuccessorModTime.IsZero() && !obj.DeleteMarker && obj.TransitionStatus != TransitionComplete { 306 // Non current versions should be deleted if their age exceeds non current days configuration 307 // https://docs.aws.amazon.com/AmazonS3/latest/dev/intro-lifecycle-rules.html#intro-lifecycle-rules-actions 308 if time.Now().After(ExpectedExpiryTime(obj.SuccessorModTime, int(rule.NoncurrentVersionTransition.NoncurrentDays))) { 309 return TransitionVersionAction 310 } 311 } 312 } 313 314 // Remove the object or simply add a delete marker (once) in a versioned bucket 315 if obj.VersionID == "" || obj.IsLatest && !obj.DeleteMarker { 316 switch { 317 case !rule.Expiration.IsDateNull(): 318 if time.Now().UTC().After(rule.Expiration.Date.Time) { 319 return DeleteAction 320 } 321 case !rule.Expiration.IsDaysNull(): 322 if time.Now().UTC().After(ExpectedExpiryTime(obj.ModTime, int(rule.Expiration.Days))) { 323 return DeleteAction 324 } 325 } 326 327 if obj.TransitionStatus != TransitionComplete { 328 switch { 329 case !rule.Transition.IsDateNull(): 330 if time.Now().UTC().After(rule.Transition.Date.Time) { 331 action = TransitionAction 332 } 333 case !rule.Transition.IsDaysNull(): 334 if time.Now().UTC().After(ExpectedExpiryTime(obj.ModTime, int(rule.Transition.Days))) { 335 action = TransitionAction 336 } 337 } 338 } 339 if !obj.RestoreExpires.IsZero() && time.Now().After(obj.RestoreExpires) { 340 if obj.VersionID != "" { 341 action = DeleteRestoredVersionAction 342 } else { 343 action = DeleteRestoredAction 344 } 345 } 346 347 } 348 } 349 return action 350 } 351 352 // ExpectedExpiryTime calculates the expiry, transition or restore date/time based on a object modtime. 353 // The expected transition or restore time is always a midnight time following the the object 354 // modification time plus the number of transition/restore days. 355 // e.g. If the object modtime is `Thu May 21 13:42:50 GMT 2020` and the object should 356 // transition in 1 day, then the expected transition time is `Fri, 23 May 2020 00:00:00 GMT` 357 func ExpectedExpiryTime(modTime time.Time, days int) time.Time { 358 t := modTime.UTC().Add(time.Duration(days+1) * 24 * time.Hour) 359 return t.Truncate(24 * time.Hour) 360 } 361 362 // PredictExpiryTime returns the expiry date/time of a given object 363 // after evaluting the current lifecycle document. 364 func (lc Lifecycle) PredictExpiryTime(obj ObjectOpts) (string, time.Time) { 365 if obj.DeleteMarker { 366 // We don't need to send any x-amz-expiration for delete marker. 367 return "", time.Time{} 368 } 369 370 var finalExpiryDate time.Time 371 var finalExpiryRuleID string 372 373 // Iterate over all actionable rules and find the earliest 374 // expiration date and its associated rule ID. 375 for _, rule := range lc.FilterActionableRules(obj) { 376 if !rule.NoncurrentVersionExpiration.IsDaysNull() && !obj.IsLatest && obj.VersionID != "" { 377 return rule.ID, ExpectedExpiryTime(obj.SuccessorModTime, int(rule.NoncurrentVersionExpiration.NoncurrentDays)) 378 } 379 380 if !rule.Expiration.IsDateNull() { 381 if finalExpiryDate.IsZero() || finalExpiryDate.After(rule.Expiration.Date.Time) { 382 finalExpiryRuleID = rule.ID 383 finalExpiryDate = rule.Expiration.Date.Time 384 } 385 } 386 if !rule.Expiration.IsDaysNull() { 387 expectedExpiry := ExpectedExpiryTime(obj.ModTime, int(rule.Expiration.Days)) 388 if finalExpiryDate.IsZero() || finalExpiryDate.After(expectedExpiry) { 389 finalExpiryRuleID = rule.ID 390 finalExpiryDate = expectedExpiry 391 } 392 } 393 } 394 return finalExpiryRuleID, finalExpiryDate 395 }