github.com/minio/minio@v0.0.0-20240328213742-3f72439b8a27/cmd/warm-backend-gcs.go (about)

     1  // Copyright (c) 2015-2021 MinIO, Inc.
     2  //
     3  // This file is part of MinIO Object Storage stack
     4  //
     5  // This program is free software: you can redistribute it and/or modify
     6  // it under the terms of the GNU Affero General Public License as published by
     7  // the Free Software Foundation, either version 3 of the License, or
     8  // (at your option) any later version.
     9  //
    10  // This program is distributed in the hope that it will be useful
    11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13  // GNU Affero General Public License for more details.
    14  //
    15  // You should have received a copy of the GNU Affero General Public License
    16  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17  
    18  package cmd
    19  
    20  import (
    21  	"context"
    22  	"errors"
    23  	"fmt"
    24  	"io"
    25  
    26  	"cloud.google.com/go/storage"
    27  	"github.com/minio/madmin-go/v3"
    28  	"google.golang.org/api/googleapi"
    29  	"google.golang.org/api/iterator"
    30  	"google.golang.org/api/option"
    31  
    32  	xioutil "github.com/minio/minio/internal/ioutil"
    33  )
    34  
    35  type warmBackendGCS struct {
    36  	client       *storage.Client
    37  	Bucket       string
    38  	Prefix       string
    39  	StorageClass string
    40  }
    41  
    42  func (gcs *warmBackendGCS) getDest(object string) string {
    43  	destObj := object
    44  	if gcs.Prefix != "" {
    45  		destObj = fmt.Sprintf("%s/%s", gcs.Prefix, object)
    46  	}
    47  	return destObj
    48  }
    49  
    50  // FIXME: add support for remote version ID in GCS remote tier and remove this.
    51  // Currently it's a no-op.
    52  
    53  func (gcs *warmBackendGCS) Put(ctx context.Context, key string, data io.Reader, length int64) (remoteVersionID, error) {
    54  	object := gcs.client.Bucket(gcs.Bucket).Object(gcs.getDest(key))
    55  	// TODO: set storage class
    56  	w := object.NewWriter(ctx)
    57  	if gcs.StorageClass != "" {
    58  		w.ObjectAttrs.StorageClass = gcs.StorageClass
    59  	}
    60  	if _, err := xioutil.Copy(w, data); err != nil {
    61  		return "", gcsToObjectError(err, gcs.Bucket, key)
    62  	}
    63  
    64  	return "", w.Close()
    65  }
    66  
    67  func (gcs *warmBackendGCS) Get(ctx context.Context, key string, rv remoteVersionID, opts WarmBackendGetOpts) (r io.ReadCloser, err error) {
    68  	// GCS storage decompresses a gzipped object by default and returns the data.
    69  	// Refer to https://cloud.google.com/storage/docs/transcoding#decompressive_transcoding
    70  	// Need to set `Accept-Encoding` header to `gzip` when issuing a GetObject call, to be able
    71  	// to download the object in compressed state.
    72  	// Calling ReadCompressed with true accomplishes that.
    73  	object := gcs.client.Bucket(gcs.Bucket).Object(gcs.getDest(key)).ReadCompressed(true)
    74  
    75  	r, err = object.NewRangeReader(ctx, opts.startOffset, opts.length)
    76  	if err != nil {
    77  		return nil, gcsToObjectError(err, gcs.Bucket, key)
    78  	}
    79  	return r, nil
    80  }
    81  
    82  func (gcs *warmBackendGCS) Remove(ctx context.Context, key string, rv remoteVersionID) error {
    83  	err := gcs.client.Bucket(gcs.Bucket).Object(gcs.getDest(key)).Delete(ctx)
    84  	return gcsToObjectError(err, gcs.Bucket, key)
    85  }
    86  
    87  func (gcs *warmBackendGCS) InUse(ctx context.Context) (bool, error) {
    88  	it := gcs.client.Bucket(gcs.Bucket).Objects(ctx, &storage.Query{
    89  		Delimiter: "/",
    90  		Prefix:    gcs.Prefix,
    91  		Versions:  false,
    92  	})
    93  	pager := iterator.NewPager(it, 1, "")
    94  	gcsObjects := make([]*storage.ObjectAttrs, 0)
    95  	_, err := pager.NextPage(&gcsObjects)
    96  	if err != nil {
    97  		return false, gcsToObjectError(err, gcs.Bucket, gcs.Prefix)
    98  	}
    99  	if len(gcsObjects) > 0 {
   100  		return true, nil
   101  	}
   102  	return false, nil
   103  }
   104  
   105  func newWarmBackendGCS(conf madmin.TierGCS, _ string) (*warmBackendGCS, error) {
   106  	// Validation code
   107  	if conf.Creds == "" {
   108  		return nil, errors.New("empty credentials unsupported")
   109  	}
   110  
   111  	if conf.Bucket == "" {
   112  		return nil, errors.New("no bucket name was provided")
   113  	}
   114  
   115  	credsJSON, err := conf.GetCredentialJSON()
   116  	if err != nil {
   117  		return nil, err
   118  	}
   119  
   120  	client, err := storage.NewClient(context.Background(), option.WithCredentialsJSON(credsJSON), option.WithScopes(storage.ScopeReadWrite))
   121  	if err != nil {
   122  		return nil, err
   123  	}
   124  	return &warmBackendGCS{client, conf.Bucket, conf.Prefix, conf.StorageClass}, nil
   125  }
   126  
   127  // Convert GCS errors to minio object layer errors.
   128  func gcsToObjectError(err error, params ...string) error {
   129  	if err == nil {
   130  		return nil
   131  	}
   132  
   133  	bucket := ""
   134  	object := ""
   135  	uploadID := ""
   136  	if len(params) >= 1 {
   137  		bucket = params[0]
   138  	}
   139  	if len(params) == 2 {
   140  		object = params[1]
   141  	}
   142  	if len(params) == 3 {
   143  		uploadID = params[2]
   144  	}
   145  
   146  	// in some cases just a plain error is being returned
   147  	switch err.Error() {
   148  	case "storage: bucket doesn't exist":
   149  		err = BucketNotFound{
   150  			Bucket: bucket,
   151  		}
   152  		return err
   153  	case "storage: object doesn't exist":
   154  		if uploadID != "" {
   155  			err = InvalidUploadID{
   156  				UploadID: uploadID,
   157  			}
   158  		} else {
   159  			err = ObjectNotFound{
   160  				Bucket: bucket,
   161  				Object: object,
   162  			}
   163  		}
   164  		return err
   165  	}
   166  
   167  	googleAPIErr, ok := err.(*googleapi.Error)
   168  	if !ok {
   169  		// We don't interpret non MinIO errors. As minio errors will
   170  		// have StatusCode to help to convert to object errors.
   171  		return err
   172  	}
   173  
   174  	if len(googleAPIErr.Errors) == 0 {
   175  		return err
   176  	}
   177  
   178  	reason := googleAPIErr.Errors[0].Reason
   179  	message := googleAPIErr.Errors[0].Message
   180  
   181  	switch reason {
   182  	case "required":
   183  		// Anonymous users does not have storage.xyz access to project 123.
   184  		fallthrough
   185  	case "keyInvalid":
   186  		fallthrough
   187  	case "forbidden":
   188  		err = PrefixAccessDenied{
   189  			Bucket: bucket,
   190  			Object: object,
   191  		}
   192  	case "invalid":
   193  		err = BucketNameInvalid{
   194  			Bucket: bucket,
   195  		}
   196  	case "notFound":
   197  		if object != "" {
   198  			err = ObjectNotFound{
   199  				Bucket: bucket,
   200  				Object: object,
   201  			}
   202  			break
   203  		}
   204  		err = BucketNotFound{Bucket: bucket}
   205  	case "conflict":
   206  		if message == "You already own this bucket. Please select another name." {
   207  			err = BucketAlreadyOwnedByYou{Bucket: bucket}
   208  			break
   209  		}
   210  		if message == "Sorry, that name is not available. Please try a different one." {
   211  			err = BucketAlreadyExists{Bucket: bucket}
   212  			break
   213  		}
   214  		err = BucketNotEmpty{Bucket: bucket}
   215  	}
   216  
   217  	return err
   218  }