github.com/uber/kraken@v0.1.4/lib/backend/gcsbackend/client.go (about)

     1  // Copyright (c) 2016-2019 Uber Technologies, Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //	   http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  package gcsbackend
    15  
    16  import (
    17  	"context"
    18  	"errors"
    19  	"fmt"
    20  	"io"
    21  	"path"
    22  
    23  	"github.com/uber/kraken/core"
    24  	"github.com/uber/kraken/lib/backend"
    25  	"github.com/uber/kraken/lib/backend/backenderrors"
    26  	"github.com/uber/kraken/lib/backend/namepath"
    27  	"github.com/uber/kraken/utils/log"
    28  
    29  	"cloud.google.com/go/storage"
    30  	"google.golang.org/api/iterator"
    31  	"google.golang.org/api/option"
    32  	"gopkg.in/yaml.v2"
    33  )
    34  
    35  const _gcs = "gcs"
    36  
    37  func init() {
    38  	backend.Register(_gcs, &factory{})
    39  }
    40  
    41  type factory struct{}
    42  
    43  func (f *factory) Create(
    44  	confRaw interface{}, authConfRaw interface{}) (backend.Client, error) {
    45  
    46  	confBytes, err := yaml.Marshal(confRaw)
    47  	if err != nil {
    48  		return nil, errors.New("marshal gcs config")
    49  	}
    50  	authConfBytes, err := yaml.Marshal(authConfRaw)
    51  	if err != nil {
    52  		return nil, errors.New("marshal gcs auth config")
    53  	}
    54  
    55  	var config Config
    56  	if err := yaml.Unmarshal(confBytes, &config); err != nil {
    57  		return nil, errors.New("unmarshal gcs config")
    58  	}
    59  	var userAuth UserAuthConfig
    60  	if err := yaml.Unmarshal(authConfBytes, &userAuth); err != nil {
    61  		return nil, errors.New("unmarshal gcs auth config")
    62  	}
    63  
    64  	return NewClient(config, userAuth)
    65  }
    66  
    67  // Client implements a backend.Client for GCS.
    68  type Client struct {
    69  	config Config
    70  	pather namepath.Pather
    71  	gcs    GCS
    72  }
    73  
    74  // Option allows setting optional Client parameters.
    75  type Option func(*Client)
    76  
    77  // WithGCS configures a Client with a custom GCS implementation.
    78  func WithGCS(gcs GCS) Option {
    79  	return func(c *Client) { c.gcs = gcs }
    80  }
    81  
    82  // NewClient creates a new Client for GCS.
    83  func NewClient(
    84  	config Config, userAuth UserAuthConfig, opts ...Option) (*Client, error) {
    85  
    86  	config.applyDefaults()
    87  	if config.Username == "" {
    88  		return nil, errors.New("invalid config: username required")
    89  	}
    90  	if config.Bucket == "" {
    91  		return nil, errors.New("invalid config: bucket required")
    92  	}
    93  	if !path.IsAbs(config.RootDirectory) {
    94  		return nil, errors.New("invalid config: root_directory must be absolute path")
    95  	}
    96  
    97  	pather, err := namepath.New(config.RootDirectory, config.NamePath)
    98  	if err != nil {
    99  		return nil, fmt.Errorf("namepath: %s", err)
   100  	}
   101  
   102  	auth, ok := userAuth[config.Username]
   103  	if !ok {
   104  		return nil, errors.New("auth not configured for username")
   105  	}
   106  
   107  	if len(opts) > 0 {
   108  		// For mock.
   109  		client := &Client{config, pather, nil}
   110  		for _, opt := range opts {
   111  			opt(client)
   112  		}
   113  		return client, nil
   114  	}
   115  
   116  	ctx := context.Background()
   117  	sClient, err := storage.NewClient(ctx,
   118  		option.WithCredentialsJSON([]byte(auth.GCS.AccessBlob)))
   119  	if err != nil {
   120  		return nil, fmt.Errorf("invalid gcs credentials: %s", err)
   121  	}
   122  
   123  	client := &Client{config, pather,
   124  		NewGCS(ctx, sClient.Bucket(config.Bucket), &config)}
   125  
   126  	log.Infof("Initalized GCS backend with config: %s", config)
   127  	return client, nil
   128  }
   129  
   130  // Stat returns blob info for name.
   131  func (c *Client) Stat(namespace, name string) (*core.BlobInfo, error) {
   132  	path, err := c.pather.BlobPath(name)
   133  	if err != nil {
   134  		return nil, fmt.Errorf("blob path: %s", err)
   135  	}
   136  
   137  	objectAttrs, err := c.gcs.ObjectAttrs(path)
   138  	if err != nil {
   139  		if isObjectNotFound(err) {
   140  			return nil, backenderrors.ErrBlobNotFound
   141  		}
   142  		return nil, err
   143  	}
   144  
   145  	return core.NewBlobInfo(objectAttrs.Size), nil
   146  }
   147  
   148  // Download downloads the content from a configured bucket and writes the
   149  // data to dst.
   150  func (c *Client) Download(namespace, name string, dst io.Writer) error {
   151  	path, err := c.pather.BlobPath(name)
   152  	if err != nil {
   153  		return fmt.Errorf("blob path: %s", err)
   154  	}
   155  
   156  	_, err = c.gcs.Download(path, dst)
   157  	return err
   158  }
   159  
   160  // Upload uploads src to a configured bucket.
   161  func (c *Client) Upload(namespace, name string, src io.Reader) error {
   162  	path, err := c.pather.BlobPath(name)
   163  	if err != nil {
   164  		return fmt.Errorf("blob path: %s", err)
   165  	}
   166  
   167  	_, err = c.gcs.Upload(path, src)
   168  	return err
   169  }
   170  
   171  // List lists names that start with prefix.
   172  func (c *Client) List(prefix string, opts ...backend.ListOption) (*backend.ListResult, error) {
   173  	options := backend.DefaultListOptions()
   174  	for _, opt := range opts {
   175  		opt(options)
   176  	}
   177  
   178  	absPrefix := path.Join(c.pather.BasePath(), prefix)
   179  	pageIterator := c.gcs.GetObjectIterator(absPrefix)
   180  
   181  	maxKeys := c.config.ListMaxKeys
   182  	paginationToken := ""
   183  	if options.Paginated {
   184  		maxKeys = options.MaxKeys
   185  		paginationToken = options.ContinuationToken
   186  	}
   187  
   188  	pager := iterator.NewPager(pageIterator, maxKeys, paginationToken)
   189  	blobs, continuationToken, err := c.gcs.NextPage(pager)
   190  	if err != nil {
   191  		return nil, err
   192  	}
   193  
   194  	var names []string
   195  	for _, b := range blobs {
   196  		name, err := c.pather.NameFromBlobPath(b)
   197  		if err != nil {
   198  			log.With("blob", b).Errorf("Error converting blob path into name: %s", err)
   199  			continue
   200  		}
   201  		names = append(names, name)
   202  	}
   203  	result := &backend.ListResult{
   204  		Names:             names,
   205  		ContinuationToken: continuationToken,
   206  	}
   207  
   208  	if !options.Paginated {
   209  		result.ContinuationToken = ""
   210  	}
   211  	return result, nil
   212  }
   213  
   214  // isObjectNotFound is helper function for identify non-existing object error.
   215  func isObjectNotFound(err error) bool {
   216  	return err == storage.ErrObjectNotExist || err == storage.ErrBucketNotExist
   217  }
   218  
   219  // GCSImpl implements GCS interaface.
   220  type GCSImpl struct {
   221  	ctx    context.Context
   222  	bucket *storage.BucketHandle
   223  	config *Config
   224  }
   225  
   226  func NewGCS(ctx context.Context, bucket *storage.BucketHandle,
   227  	config *Config) *GCSImpl {
   228  
   229  	return &GCSImpl{ctx, bucket, config}
   230  }
   231  
   232  func (g *GCSImpl) ObjectAttrs(objectName string) (*storage.ObjectAttrs, error) {
   233  	handle := g.bucket.Object(objectName)
   234  	return handle.Attrs(g.ctx)
   235  }
   236  
   237  func (g *GCSImpl) Download(objectName string, w io.Writer) (int64, error) {
   238  	rc, err := g.bucket.Object(objectName).NewReader(g.ctx)
   239  	if err != nil {
   240  		if isObjectNotFound(err) {
   241  			return 0, backenderrors.ErrBlobNotFound
   242  		}
   243  		return 0, err
   244  	}
   245  	defer rc.Close()
   246  
   247  	r, err := io.CopyN(w, rc, int64(g.config.BufferGuard))
   248  	if err != nil && err != io.EOF {
   249  		return 0, err
   250  	}
   251  
   252  	return r, nil
   253  }
   254  
   255  func (g *GCSImpl) Upload(objectName string, r io.Reader) (int64, error) {
   256  	wc := g.bucket.Object(objectName).NewWriter(g.ctx)
   257  	wc.ChunkSize = int(g.config.UploadChunkSize)
   258  
   259  	w, err := io.CopyN(wc, r, int64(g.config.UploadChunkSize))
   260  	if err != nil && err != io.EOF {
   261  		return 0, err
   262  	}
   263  
   264  	if err := wc.Close(); err != nil {
   265  		return 0, err
   266  	}
   267  
   268  	return w, nil
   269  }
   270  
   271  func (g *GCSImpl) GetObjectIterator(prefix string) iterator.Pageable {
   272  	var query storage.Query
   273  
   274  	query.Prefix = prefix
   275  	return g.bucket.Objects(g.ctx, &query)
   276  }
   277  
   278  func (g *GCSImpl) NextPage(pager *iterator.Pager) ([]string, string,
   279  	error) {
   280  
   281  	var objectAttrs []*storage.ObjectAttrs
   282  	continuationToken, err := pager.NextPage(&objectAttrs)
   283  	if err != nil {
   284  		return nil, "", err
   285  	}
   286  
   287  	names := make([]string, len(objectAttrs))
   288  	for idx, objectAttr := range objectAttrs {
   289  		names[idx] = objectAttr.Name
   290  	}
   291  	return names, continuationToken, nil
   292  }