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 }