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  }