storj.io/minio@v0.0.0-20230509071714-0cbc90f649b1/cmd/bucket-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 cmd 18 19 import ( 20 "context" 21 "encoding/xml" 22 "fmt" 23 "io" 24 "net/http" 25 "runtime" 26 "strings" 27 "sync" 28 "time" 29 30 miniogo "github.com/minio/minio-go/v7" 31 "github.com/minio/minio-go/v7/pkg/tags" 32 33 xhttp "storj.io/minio/cmd/http" 34 "storj.io/minio/cmd/logger" 35 sse "storj.io/minio/pkg/bucket/encryption" 36 "storj.io/minio/pkg/bucket/lifecycle" 37 "storj.io/minio/pkg/event" 38 "storj.io/minio/pkg/hash" 39 "storj.io/minio/pkg/madmin" 40 "storj.io/minio/pkg/s3select" 41 ) 42 43 const ( 44 // Disabled means the lifecycle rule is inactive 45 Disabled = "Disabled" 46 ) 47 48 // LifecycleSys - Bucket lifecycle subsystem. 49 type LifecycleSys struct{} 50 51 // Get - gets lifecycle config associated to a given bucket name. 52 func (sys *LifecycleSys) Get(bucketName string) (lc *lifecycle.Lifecycle, err error) { 53 if GlobalIsGateway { 54 objAPI := newObjectLayerFn() 55 if objAPI == nil { 56 return nil, errServerNotInitialized 57 } 58 59 return nil, BucketLifecycleNotFound{Bucket: bucketName} 60 } 61 62 return globalBucketMetadataSys.GetLifecycleConfig(bucketName) 63 } 64 65 // NewLifecycleSys - creates new lifecycle system. 66 func NewLifecycleSys() *LifecycleSys { 67 return &LifecycleSys{} 68 } 69 70 type expiryTask struct { 71 objInfo ObjectInfo 72 versionExpiry bool 73 } 74 75 type expiryState struct { 76 once sync.Once 77 expiryCh chan expiryTask 78 } 79 80 func (es *expiryState) queueExpiryTask(oi ObjectInfo, rmVersion bool) { 81 select { 82 case <-GlobalContext.Done(): 83 es.once.Do(func() { 84 close(es.expiryCh) 85 }) 86 case es.expiryCh <- expiryTask{objInfo: oi, versionExpiry: rmVersion}: 87 default: 88 } 89 } 90 91 var ( 92 globalExpiryState *expiryState 93 ) 94 95 func newExpiryState() *expiryState { 96 return &expiryState{ 97 expiryCh: make(chan expiryTask, 10000), 98 } 99 } 100 101 func initBackgroundExpiry(ctx context.Context, objectAPI ObjectLayer) { 102 globalExpiryState = newExpiryState() 103 go func() { 104 for t := range globalExpiryState.expiryCh { 105 applyExpiryRule(ctx, objectAPI, t.objInfo, false, t.versionExpiry) 106 } 107 }() 108 } 109 110 type transitionState struct { 111 once sync.Once 112 // add future metrics here 113 transitionCh chan ObjectInfo 114 } 115 116 func (t *transitionState) queueTransitionTask(oi ObjectInfo) { 117 select { 118 case <-GlobalContext.Done(): 119 t.once.Do(func() { 120 close(t.transitionCh) 121 }) 122 case t.transitionCh <- oi: 123 default: 124 } 125 } 126 127 var ( 128 globalTransitionState *transitionState 129 globalTransitionConcurrent = runtime.GOMAXPROCS(0) / 2 130 ) 131 132 func newTransitionState() *transitionState { 133 // fix minimum concurrent transition to 1 for single CPU setup 134 if globalTransitionConcurrent == 0 { 135 globalTransitionConcurrent = 1 136 } 137 return &transitionState{ 138 transitionCh: make(chan ObjectInfo, 10000), 139 } 140 } 141 142 // addWorker creates a new worker to process tasks 143 func (t *transitionState) addWorker(ctx context.Context, objectAPI ObjectLayer) { 144 // Add a new worker. 145 go func() { 146 for { 147 select { 148 case <-ctx.Done(): 149 return 150 case oi, ok := <-t.transitionCh: 151 if !ok { 152 return 153 } 154 if err := transitionObject(ctx, objectAPI, oi); err != nil { 155 logger.LogIf(ctx, err) 156 } 157 } 158 } 159 }() 160 } 161 162 func initBackgroundTransition(ctx context.Context, objectAPI ObjectLayer) { 163 if globalTransitionState == nil { 164 return 165 } 166 167 // Start with globalTransitionConcurrent. 168 for i := 0; i < globalTransitionConcurrent; i++ { 169 globalTransitionState.addWorker(ctx, objectAPI) 170 } 171 } 172 173 func validateLifecycleTransition(ctx context.Context, bucket string, lfc *lifecycle.Lifecycle) error { 174 for _, rule := range lfc.Rules { 175 if rule.Transition.StorageClass != "" { 176 sameTarget, destbucket, err := validateTransitionDestination(ctx, bucket, rule.Transition.StorageClass) 177 if err != nil { 178 return err 179 } 180 if sameTarget && destbucket == bucket { 181 return fmt.Errorf("Transition destination cannot be the same as the source bucket") 182 } 183 } 184 } 185 return nil 186 } 187 188 // validateTransitionDestination returns error if transition destination bucket missing or not configured 189 // It also returns true if transition destination is same as this server. 190 func validateTransitionDestination(ctx context.Context, bucket string, targetLabel string) (bool, string, error) { 191 tgt := globalBucketTargetSys.GetRemoteTargetWithLabel(ctx, bucket, targetLabel) 192 if tgt == nil { 193 return false, "", BucketRemoteTargetNotFound{Bucket: bucket} 194 } 195 arn, err := madmin.ParseARN(tgt.Arn) 196 if err != nil { 197 return false, "", BucketRemoteTargetNotFound{Bucket: bucket} 198 } 199 if arn.Type != madmin.ILMService { 200 return false, "", BucketRemoteArnTypeInvalid{} 201 } 202 clnt := globalBucketTargetSys.GetRemoteTargetClient(ctx, tgt.Arn) 203 if clnt == nil { 204 return false, "", BucketRemoteTargetNotFound{Bucket: bucket} 205 } 206 if found, _ := clnt.BucketExists(ctx, arn.Bucket); !found { 207 return false, "", BucketRemoteDestinationNotFound{Bucket: arn.Bucket} 208 } 209 sameTarget, _ := isLocalHost(clnt.EndpointURL().Hostname(), clnt.EndpointURL().Port(), globalMinioPort) 210 return sameTarget, arn.Bucket, nil 211 } 212 213 // transitionSC returns storage class label for this bucket 214 func transitionSC(ctx context.Context, bucket string) string { 215 cfg, err := globalBucketMetadataSys.GetLifecycleConfig(bucket) 216 if err != nil { 217 return "" 218 } 219 for _, rule := range cfg.Rules { 220 if rule.Status == Disabled { 221 continue 222 } 223 if rule.Transition.StorageClass != "" { 224 return rule.Transition.StorageClass 225 } 226 } 227 return "" 228 } 229 230 // return true if ARN representing transition storage class is present in a active rule 231 // for the lifecycle configured on this bucket 232 func transitionSCInUse(ctx context.Context, lfc *lifecycle.Lifecycle, bucket, arnStr string) bool { 233 tgtLabel := globalBucketTargetSys.GetRemoteLabelWithArn(ctx, bucket, arnStr) 234 if tgtLabel == "" { 235 return false 236 } 237 for _, rule := range lfc.Rules { 238 if rule.Status == Disabled { 239 continue 240 } 241 if rule.Transition.StorageClass != "" && rule.Transition.StorageClass == tgtLabel { 242 return true 243 } 244 } 245 return false 246 } 247 248 // set PutObjectOptions for PUT operation to transition data to target cluster 249 func putTransitionOpts(objInfo ObjectInfo) (putOpts miniogo.PutObjectOptions, err error) { 250 meta := make(map[string]string) 251 252 putOpts = miniogo.PutObjectOptions{ 253 UserMetadata: meta, 254 ContentType: objInfo.ContentType, 255 ContentEncoding: objInfo.ContentEncoding, 256 StorageClass: objInfo.StorageClass, 257 Internal: miniogo.AdvancedPutOptions{ 258 SourceVersionID: objInfo.VersionID, 259 SourceMTime: objInfo.ModTime, 260 SourceETag: objInfo.ETag, 261 }, 262 } 263 264 if objInfo.UserTags != "" { 265 tag, _ := tags.ParseObjectTags(objInfo.UserTags) 266 if tag != nil { 267 putOpts.UserTags = tag.ToMap() 268 } 269 } 270 271 lkMap := caseInsensitiveMap(objInfo.UserDefined) 272 if lang, ok := lkMap.Lookup(xhttp.ContentLanguage); ok { 273 putOpts.ContentLanguage = lang 274 } 275 if disp, ok := lkMap.Lookup(xhttp.ContentDisposition); ok { 276 putOpts.ContentDisposition = disp 277 } 278 if cc, ok := lkMap.Lookup(xhttp.CacheControl); ok { 279 putOpts.CacheControl = cc 280 } 281 if mode, ok := lkMap.Lookup(xhttp.AmzObjectLockMode); ok { 282 rmode := miniogo.RetentionMode(mode) 283 putOpts.Mode = rmode 284 } 285 if retainDateStr, ok := lkMap.Lookup(xhttp.AmzObjectLockRetainUntilDate); ok { 286 rdate, err := time.Parse(time.RFC3339, retainDateStr) 287 if err != nil { 288 return putOpts, err 289 } 290 putOpts.RetainUntilDate = rdate 291 } 292 if lhold, ok := lkMap.Lookup(xhttp.AmzObjectLockLegalHold); ok { 293 putOpts.LegalHold = miniogo.LegalHoldStatus(lhold) 294 } 295 296 return putOpts, nil 297 } 298 299 // handle deletes of transitioned objects or object versions when one of the following is true: 300 // 1. temporarily restored copies of objects (restored with the PostRestoreObject API) expired. 301 // 2. life cycle expiry date is met on the object. 302 // 3. Object is removed through DELETE api call 303 func deleteTransitionedObject(ctx context.Context, objectAPI ObjectLayer, bucket, object string, lcOpts lifecycle.ObjectOpts, restoredObject, isDeleteTierOnly bool) error { 304 if lcOpts.TransitionStatus == "" && !isDeleteTierOnly { 305 return nil 306 } 307 lc, err := globalLifecycleSys.Get(bucket) 308 if err != nil { 309 return err 310 } 311 arn := getLifecycleTransitionTargetArn(ctx, lc, bucket, lcOpts) 312 if arn == nil { 313 return fmt.Errorf("remote target not configured") 314 } 315 tgt := globalBucketTargetSys.GetRemoteTargetClient(ctx, arn.String()) 316 if tgt == nil { 317 return fmt.Errorf("remote target not configured") 318 } 319 320 var opts ObjectOptions 321 opts.Versioned = globalBucketVersioningSys.Enabled(bucket) 322 opts.VersionID = lcOpts.VersionID 323 if restoredObject { 324 // delete locally restored copy of object or object version 325 // from the source, while leaving metadata behind. The data on 326 // transitioned tier lies untouched and still accessible 327 opts.TransitionStatus = lcOpts.TransitionStatus 328 _, err = objectAPI.DeleteObject(ctx, bucket, object, opts) 329 return err 330 } 331 332 // When an object is past expiry, delete the data from transitioned tier and 333 // metadata from source 334 if err := tgt.RemoveObject(context.Background(), arn.Bucket, object, miniogo.RemoveObjectOptions{VersionID: lcOpts.VersionID}); err != nil { 335 logger.LogIf(ctx, err) 336 } 337 338 if isDeleteTierOnly { 339 return nil 340 } 341 342 objInfo, err := objectAPI.DeleteObject(ctx, bucket, object, opts) 343 if err != nil { 344 return err 345 } 346 347 // Send audit for the lifecycle delete operation 348 auditLogLifecycle(ctx, bucket, object) 349 350 eventName := event.ObjectRemovedDelete 351 if lcOpts.DeleteMarker { 352 eventName = event.ObjectRemovedDeleteMarkerCreated 353 } 354 // Notify object deleted event. 355 sendEvent(eventArgs{ 356 EventName: eventName, 357 BucketName: bucket, 358 Object: objInfo, 359 Host: "Internal: [ILM-EXPIRY]", 360 }) 361 362 // should never reach here 363 return nil 364 } 365 366 // transition object to target specified by the transition ARN. When an object is transitioned to another 367 // storage specified by the transition ARN, the metadata is left behind on source cluster and original content 368 // is moved to the transition tier. Note that in the case of encrypted objects, entire encrypted stream is moved 369 // to the transition tier without decrypting or re-encrypting. 370 func transitionObject(ctx context.Context, objectAPI ObjectLayer, objInfo ObjectInfo) error { 371 lc, err := globalLifecycleSys.Get(objInfo.Bucket) 372 if err != nil { 373 return err 374 } 375 lcOpts := lifecycle.ObjectOpts{ 376 Name: objInfo.Name, 377 UserTags: objInfo.UserTags, 378 } 379 arn := getLifecycleTransitionTargetArn(ctx, lc, objInfo.Bucket, lcOpts) 380 if arn == nil { 381 return fmt.Errorf("remote target not configured") 382 } 383 tgt := globalBucketTargetSys.GetRemoteTargetClient(ctx, arn.String()) 384 if tgt == nil { 385 return fmt.Errorf("remote target not configured") 386 } 387 388 gr, err := objectAPI.GetObjectNInfo(ctx, objInfo.Bucket, objInfo.Name, nil, http.Header{}, readLock, ObjectOptions{ 389 VersionID: objInfo.VersionID, 390 TransitionStatus: lifecycle.TransitionPending, 391 }) 392 if err != nil { 393 return err 394 } 395 oi := gr.ObjInfo 396 if oi.TransitionStatus == lifecycle.TransitionComplete { 397 gr.Close() 398 return nil 399 } 400 401 putOpts, err := putTransitionOpts(oi) 402 if err != nil { 403 gr.Close() 404 return err 405 406 } 407 if _, err = tgt.PutObject(ctx, arn.Bucket, oi.Name, gr, oi.Size, putOpts); err != nil { 408 gr.Close() 409 return err 410 } 411 gr.Close() 412 413 var opts ObjectOptions 414 opts.Versioned = globalBucketVersioningSys.Enabled(oi.Bucket) 415 opts.VersionID = oi.VersionID 416 opts.TransitionStatus = lifecycle.TransitionComplete 417 eventName := event.ObjectTransitionComplete 418 419 objInfo, err = objectAPI.DeleteObject(ctx, oi.Bucket, oi.Name, opts) 420 if err != nil { 421 eventName = event.ObjectTransitionFailed 422 } 423 424 // Notify object deleted event. 425 sendEvent(eventArgs{ 426 EventName: eventName, 427 BucketName: objInfo.Bucket, 428 Object: objInfo, 429 Host: "Internal: [ILM-Transition]", 430 }) 431 432 return err 433 } 434 435 // getLifecycleTransitionTargetArn returns transition ARN for storage class specified in the config. 436 func getLifecycleTransitionTargetArn(ctx context.Context, lc *lifecycle.Lifecycle, bucket string, obj lifecycle.ObjectOpts) *madmin.ARN { 437 for _, rule := range lc.FilterActionableRules(obj) { 438 if rule.Transition.StorageClass != "" { 439 return globalBucketTargetSys.GetRemoteArnWithLabel(ctx, bucket, rule.Transition.StorageClass) 440 } 441 } 442 return nil 443 } 444 445 // getTransitionedObjectReader returns a reader from the transitioned tier. 446 func getTransitionedObjectReader(ctx context.Context, bucket, object string, rs *HTTPRangeSpec, h http.Header, oi ObjectInfo, opts ObjectOptions) (gr *GetObjectReader, err error) { 447 var lc *lifecycle.Lifecycle 448 lc, err = globalLifecycleSys.Get(bucket) 449 if err != nil { 450 return nil, err 451 } 452 453 arn := getLifecycleTransitionTargetArn(ctx, lc, bucket, lifecycle.ObjectOpts{ 454 Name: object, 455 UserTags: oi.UserTags, 456 ModTime: oi.ModTime, 457 VersionID: oi.VersionID, 458 DeleteMarker: oi.DeleteMarker, 459 IsLatest: oi.IsLatest, 460 }) 461 if arn == nil { 462 return nil, fmt.Errorf("remote target not configured") 463 } 464 tgt := globalBucketTargetSys.GetRemoteTargetClient(ctx, arn.String()) 465 if tgt == nil { 466 return nil, fmt.Errorf("remote target not configured") 467 } 468 fn, off, length, err := NewGetObjectReader(rs, oi, opts) 469 if err != nil { 470 return nil, ErrorRespToObjectError(err, bucket, object) 471 } 472 gopts := miniogo.GetObjectOptions{VersionID: opts.VersionID} 473 474 // get correct offsets for encrypted object 475 if off >= 0 && length >= 0 { 476 if err := gopts.SetRange(off, off+length-1); err != nil { 477 return nil, ErrorRespToObjectError(err, bucket, object) 478 } 479 } 480 481 reader, err := tgt.GetObject(ctx, arn.Bucket, object, gopts) 482 if err != nil { 483 return nil, err 484 } 485 closeReader := func() { reader.Close() } 486 487 return fn(reader, h, opts.CheckPrecondFn, closeReader) 488 } 489 490 // RestoreRequestType represents type of restore. 491 type RestoreRequestType string 492 493 const ( 494 // SelectRestoreRequest specifies select request. This is the only valid value 495 SelectRestoreRequest RestoreRequestType = "SELECT" 496 ) 497 498 // Encryption specifies encryption setting on restored bucket 499 type Encryption struct { 500 EncryptionType sse.SSEAlgorithm `xml:"EncryptionType"` 501 KMSContext string `xml:"KMSContext,omitempty"` 502 KMSKeyID string `xml:"KMSKeyId,omitempty"` 503 } 504 505 // MetadataEntry denotes name and value. 506 type MetadataEntry struct { 507 Name string `xml:"Name"` 508 Value string `xml:"Value"` 509 } 510 511 // S3Location specifies s3 location that receives result of a restore object request 512 type S3Location struct { 513 BucketName string `xml:"BucketName,omitempty"` 514 Encryption Encryption `xml:"Encryption,omitempty"` 515 Prefix string `xml:"Prefix,omitempty"` 516 StorageClass string `xml:"StorageClass,omitempty"` 517 Tagging *tags.Tags `xml:"Tagging,omitempty"` 518 UserMetadata []MetadataEntry `xml:"UserMetadata"` 519 } 520 521 // OutputLocation specifies bucket where object needs to be restored 522 type OutputLocation struct { 523 S3 S3Location `xml:"S3,omitempty"` 524 } 525 526 // IsEmpty returns true if output location not specified. 527 func (o *OutputLocation) IsEmpty() bool { 528 return o.S3.BucketName == "" 529 } 530 531 // SelectParameters specifies sql select parameters 532 type SelectParameters struct { 533 s3select.S3Select 534 } 535 536 // IsEmpty returns true if no select parameters set 537 func (sp *SelectParameters) IsEmpty() bool { 538 return sp == nil 539 } 540 541 var ( 542 selectParamsXMLName = "SelectParameters" 543 ) 544 545 // UnmarshalXML - decodes XML data. 546 func (sp *SelectParameters) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { 547 // Essentially the same as S3Select barring the xml name. 548 if start.Name.Local == selectParamsXMLName { 549 start.Name = xml.Name{Space: "", Local: "SelectRequest"} 550 } 551 return sp.S3Select.UnmarshalXML(d, start) 552 } 553 554 // RestoreObjectRequest - xml to restore a transitioned object 555 type RestoreObjectRequest struct { 556 XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ RestoreRequest" json:"-"` 557 Days int `xml:"Days,omitempty"` 558 Type RestoreRequestType `xml:"Type,omitempty"` 559 Tier string `xml:"Tier,-"` 560 Description string `xml:"Description,omitempty"` 561 SelectParameters *SelectParameters `xml:"SelectParameters,omitempty"` 562 OutputLocation OutputLocation `xml:"OutputLocation,omitempty"` 563 } 564 565 // Maximum 2MiB size per restore object request. 566 const maxRestoreObjectRequestSize = 2 << 20 567 568 // parseRestoreRequest parses RestoreObjectRequest from xml 569 func parseRestoreRequest(reader io.Reader) (*RestoreObjectRequest, error) { 570 req := RestoreObjectRequest{} 571 if err := xml.NewDecoder(io.LimitReader(reader, maxRestoreObjectRequestSize)).Decode(&req); err != nil { 572 return nil, err 573 } 574 return &req, nil 575 } 576 577 // validate a RestoreObjectRequest as per AWS S3 spec https://docs.aws.amazon.com/AmazonS3/latest/API/API_RestoreObject.html 578 func (r *RestoreObjectRequest) validate(ctx context.Context, objAPI ObjectLayer) error { 579 if r.Type != SelectRestoreRequest && !r.SelectParameters.IsEmpty() { 580 return fmt.Errorf("Select parameters can only be specified with SELECT request type") 581 } 582 if r.Type == SelectRestoreRequest && r.SelectParameters.IsEmpty() { 583 return fmt.Errorf("SELECT restore request requires select parameters to be specified") 584 } 585 586 if r.Type != SelectRestoreRequest && !r.OutputLocation.IsEmpty() { 587 return fmt.Errorf("OutputLocation required only for SELECT request type") 588 } 589 if r.Type == SelectRestoreRequest && r.OutputLocation.IsEmpty() { 590 return fmt.Errorf("OutputLocation required for SELECT requests") 591 } 592 593 if r.Days != 0 && r.Type == SelectRestoreRequest { 594 return fmt.Errorf("Days cannot be specified with SELECT restore request") 595 } 596 if r.Days == 0 && r.Type != SelectRestoreRequest { 597 return fmt.Errorf("restoration days should be at least 1") 598 } 599 // Check if bucket exists. 600 if !r.OutputLocation.IsEmpty() { 601 if _, err := objAPI.GetBucketInfo(ctx, r.OutputLocation.S3.BucketName); err != nil { 602 return err 603 } 604 if r.OutputLocation.S3.Prefix == "" { 605 return fmt.Errorf("Prefix is a required parameter in OutputLocation") 606 } 607 if r.OutputLocation.S3.Encryption.EncryptionType != xhttp.AmzEncryptionAES { 608 return NotImplemented{} 609 } 610 } 611 return nil 612 } 613 614 // set ObjectOptions for PUT call to restore temporary copy of transitioned data 615 func putRestoreOpts(bucket, object string, rreq *RestoreObjectRequest, objInfo ObjectInfo) (putOpts ObjectOptions) { 616 meta := make(map[string]string) 617 sc := rreq.OutputLocation.S3.StorageClass 618 if sc == "" { 619 sc = objInfo.StorageClass 620 } 621 meta[strings.ToLower(xhttp.AmzStorageClass)] = sc 622 623 if rreq.Type == SelectRestoreRequest { 624 for _, v := range rreq.OutputLocation.S3.UserMetadata { 625 if !strings.HasPrefix("x-amz-meta", strings.ToLower(v.Name)) { 626 meta["x-amz-meta-"+v.Name] = v.Value 627 continue 628 } 629 meta[v.Name] = v.Value 630 } 631 if tags := rreq.OutputLocation.S3.Tagging.String(); tags != "" { 632 meta[xhttp.AmzObjectTagging] = tags 633 } 634 if rreq.OutputLocation.S3.Encryption.EncryptionType != "" { 635 meta[xhttp.AmzServerSideEncryption] = xhttp.AmzEncryptionAES 636 } 637 return ObjectOptions{ 638 Versioned: globalBucketVersioningSys.Enabled(bucket), 639 VersionSuspended: globalBucketVersioningSys.Suspended(bucket), 640 UserDefined: meta, 641 } 642 } 643 for k, v := range objInfo.UserDefined { 644 meta[k] = v 645 } 646 if len(objInfo.UserTags) != 0 { 647 meta[xhttp.AmzObjectTagging] = objInfo.UserTags 648 } 649 650 return ObjectOptions{ 651 Versioned: globalBucketVersioningSys.Enabled(bucket), 652 VersionSuspended: globalBucketVersioningSys.Suspended(bucket), 653 UserDefined: meta, 654 VersionID: objInfo.VersionID, 655 MTime: objInfo.ModTime, 656 Expires: objInfo.Expires, 657 } 658 } 659 660 var ( 661 errRestoreHDRMissing = fmt.Errorf("x-amz-restore header not found") 662 errRestoreHDRMalformed = fmt.Errorf("x-amz-restore header malformed") 663 ) 664 665 // parse x-amz-restore header from user metadata to get the status of ongoing request and expiry of restoration 666 // if any. This header value is of format: ongoing-request=true|false, expires=time 667 func parseRestoreHeaderFromMeta(meta map[string]string) (ongoing bool, expiry time.Time, err error) { 668 restoreHdr, ok := meta[xhttp.AmzRestore] 669 if !ok { 670 return ongoing, expiry, errRestoreHDRMissing 671 } 672 rslc := strings.SplitN(restoreHdr, ",", 2) 673 if len(rslc) != 2 { 674 return ongoing, expiry, errRestoreHDRMalformed 675 } 676 rstatusSlc := strings.SplitN(rslc[0], "=", 2) 677 if len(rstatusSlc) != 2 { 678 return ongoing, expiry, errRestoreHDRMalformed 679 } 680 rExpSlc := strings.SplitN(rslc[1], "=", 2) 681 if len(rExpSlc) != 2 { 682 return ongoing, expiry, errRestoreHDRMalformed 683 } 684 685 expiry, err = time.Parse(http.TimeFormat, rExpSlc[1]) 686 if err != nil { 687 return 688 } 689 return rstatusSlc[1] == "true", expiry, nil 690 } 691 692 // restoreTransitionedObject is similar to PostObjectRestore from AWS GLACIER 693 // storage class. When PostObjectRestore API is called, a temporary copy of the object 694 // is restored locally to the bucket on source cluster until the restore expiry date. 695 // The copy that was transitioned continues to reside in the transitioned tier. 696 func restoreTransitionedObject(ctx context.Context, bucket, object string, objAPI ObjectLayer, objInfo ObjectInfo, rreq *RestoreObjectRequest, restoreExpiry time.Time) error { 697 var rs *HTTPRangeSpec 698 gr, err := getTransitionedObjectReader(ctx, bucket, object, rs, http.Header{}, objInfo, ObjectOptions{ 699 VersionID: objInfo.VersionID}) 700 if err != nil { 701 return err 702 } 703 defer gr.Close() 704 hashReader, err := hash.NewReader(gr, objInfo.Size, "", "", objInfo.Size) 705 if err != nil { 706 return err 707 } 708 pReader := NewPutObjReader(hashReader) 709 opts := putRestoreOpts(bucket, object, rreq, objInfo) 710 opts.UserDefined[xhttp.AmzRestore] = fmt.Sprintf("ongoing-request=%t, expiry-date=%s", false, restoreExpiry.Format(http.TimeFormat)) 711 if _, err := objAPI.PutObject(ctx, bucket, object, pReader, opts); err != nil { 712 return err 713 } 714 715 return nil 716 }