1// Copyright 2018 The Go Cloud Development Kit Authors
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//     https://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
15// Package gcsblob provides a blob implementation that uses GCS. Use OpenBucket
16// to construct a *blob.Bucket.
17//
18// URLs
19//
20// For blob.OpenBucket, gcsblob registers for the scheme "gs".
21// The default URL opener will set up a connection using default credentials
22// from the environment, as described in
23// https://cloud.google.com/docs/authentication/production.
24// Some environments, such as GCE, come without a private key. In such cases
25// the IAM Credentials API will be configured for use in Options.MakeSignBytes,
26// which will introduce latency to any and all calls to bucket.SignedURL
27// that you can avoid by installing a service account credentials file or
28// obtaining and configuring a private key:
29// https://cloud.google.com/iam/docs/creating-managing-service-account-keys
30//
31// To customize the URL opener, or for more details on the URL format,
32// see URLOpener.
33// See https://gocloud.dev/concepts/urls/ for background information.
34//
35// Escaping
36//
37// Go CDK supports all UTF-8 strings; to make this work with services lacking
38// full UTF-8 support, strings must be escaped (during writes) and unescaped
39// (during reads). The following escapes are performed for gcsblob:
40//  - Blob keys: ASCII characters 10 and 13 are escaped to "__0x<hex>__".
41//    Additionally, the "/" in "../" is escaped in the same way.
42//
43// As
44//
45// gcsblob exposes the following types for As:
46//  - Bucket: *storage.Client
47//  - Error: *googleapi.Error
48//  - ListObject: storage.ObjectAttrs
49//  - ListOptions.BeforeList: *storage.Query
50//  - Reader: *storage.Reader
51//  - ReaderOptions.BeforeRead: **storage.ObjectHandle, *storage.Reader (if accessing both, must be in that order)
52//  - Attributes: storage.ObjectAttrs
53//  - CopyOptions.BeforeCopy: *CopyObjectHandles, *storage.Copier (if accessing both, must be in that order)
54//  - WriterOptions.BeforeWrite: **storage.ObjectHandle, *storage.Writer (if accessing both, must be in that order)
55//  - SignedURLOptions.BeforeSign: *storage.SignedURLOptions
56package gcsblob // import "gocloud.dev/blob/gcsblob"
57
58import (
59	"context"
60	"encoding/json"
61	"errors"
62	"fmt"
63	"io"
64	"io/ioutil"
65	"net/http"
66	"net/url"
67	"os"
68	"sort"
69	"strings"
70	"sync"
71	"time"
72
73	"cloud.google.com/go/compute/metadata"
74	"cloud.google.com/go/storage"
75	"github.com/google/wire"
76	"golang.org/x/oauth2/google"
77	"google.golang.org/api/googleapi"
78	"google.golang.org/api/iterator"
79	"google.golang.org/api/option"
80
81	"gocloud.dev/blob"
82	"gocloud.dev/blob/driver"
83	"gocloud.dev/gcerrors"
84	"gocloud.dev/gcp"
85	"gocloud.dev/internal/escape"
86	"gocloud.dev/internal/gcerr"
87	"gocloud.dev/internal/useragent"
88)
89
90const defaultPageSize = 1000
91
92func init() {
93	blob.DefaultURLMux().RegisterBucket(Scheme, new(lazyCredsOpener))
94}
95
96// Set holds Wire providers for this package.
97var Set = wire.NewSet(
98	wire.Struct(new(URLOpener), "Client"),
99)
100
101// readDefaultCredentials gets the field values from the supplied JSON data.
102// For its possible formats please see
103// https://cloud.google.com/iam/docs/creating-managing-service-account-keys#iam-service-account-keys-create-go
104//
105// Use "golang.org/x/oauth2/google".DefaultCredentials.JSON to get
106// the contents of the preferred credential file.
107//
108// Returns null-values for fields that have not been obtained.
109func readDefaultCredentials(credFileAsJSON []byte) (AccessID string, PrivateKey []byte) {
110	// For example, a credentials file as generated for service accounts through the web console.
111	var contentVariantA struct {
112		ClientEmail string `json:"client_email"`
113		PrivateKey  string `json:"private_key"`
114	}
115	if err := json.Unmarshal(credFileAsJSON, &contentVariantA); err == nil {
116		AccessID = contentVariantA.ClientEmail
117		PrivateKey = []byte(contentVariantA.PrivateKey)
118	}
119	if AccessID != "" {
120		return
121	}
122
123	// If obtained through the REST API.
124	var contentVariantB struct {
125		Name           string `json:"name"`
126		PrivateKeyData string `json:"privateKeyData"`
127	}
128	if err := json.Unmarshal(credFileAsJSON, &contentVariantB); err == nil {
129		nextFieldIsAccessID := false
130		for _, s := range strings.Split(contentVariantB.Name, "/") {
131			if nextFieldIsAccessID {
132				AccessID = s
133				break
134			}
135			nextFieldIsAccessID = s == "serviceAccounts"
136		}
137		PrivateKey = []byte(contentVariantB.PrivateKeyData)
138	}
139
140	return
141}
142
143// lazyCredsOpener obtains Application Default Credentials on the first call
144// to OpenBucketURL.
145type lazyCredsOpener struct {
146	init   sync.Once
147	opener *URLOpener
148	err    error
149}
150
151func (o *lazyCredsOpener) OpenBucketURL(ctx context.Context, u *url.URL) (*blob.Bucket, error) {
152	o.init.Do(func() {
153		var opts Options
154		var creds *google.Credentials
155		if os.Getenv("STORAGE_EMULATOR_HOST") != "" {
156			creds, _ = google.CredentialsFromJSON(ctx, []byte(`{"type": "service_account", "project_id": "my-project-id"}`))
157		} else {
158			var err error
159			creds, err = gcp.DefaultCredentials(ctx)
160			if err != nil {
161				o.err = err
162				return
163			}
164
165			// Populate default values from credentials files, where available.
166			opts.GoogleAccessID, opts.PrivateKey = readDefaultCredentials(creds.JSON)
167
168			// … else, on GCE, at least get the instance's main service account.
169			if opts.GoogleAccessID == "" && metadata.OnGCE() {
170				mc := metadata.NewClient(nil)
171				opts.GoogleAccessID, _ = mc.Email("")
172			}
173		}
174
175		// Provide a default factory for SignBytes for environments without a private key.
176		if len(opts.PrivateKey) <= 0 && opts.GoogleAccessID != "" {
177			iam := new(credentialsClient)
178			// We cannot hold onto the first context: it might've been cancelled already.
179			ctx := context.Background()
180			opts.MakeSignBytes = iam.CreateMakeSignBytesWith(ctx, opts.GoogleAccessID)
181		}
182
183		client, err := gcp.NewHTTPClient(gcp.DefaultTransport(), creds.TokenSource)
184		if err != nil {
185			o.err = err
186			return
187		}
188		o.opener = &URLOpener{Client: client, Options: opts}
189	})
190	if o.err != nil {
191		return nil, fmt.Errorf("open bucket %v: %v", u, o.err)
192	}
193	return o.opener.OpenBucketURL(ctx, u)
194}
195
196// Scheme is the URL scheme gcsblob registers its URLOpener under on
197// blob.DefaultMux.
198const Scheme = "gs"
199
200// URLOpener opens GCS URLs like "gs://mybucket".
201//
202// The URL host is used as the bucket name.
203//
204// The following query parameters are supported:
205//
206//   - access_id: sets Options.GoogleAccessID
207//   - private_key_path: path to read for Options.PrivateKey
208//
209// Currently their use is limited to SignedURL.
210type URLOpener struct {
211	// Client must be set to a non-nil HTTP client authenticated with
212	// Cloud Storage scope or equivalent.
213	Client *gcp.HTTPClient
214
215	// Options specifies the default options to pass to OpenBucket.
216	Options Options
217}
218
219// OpenBucketURL opens the GCS bucket with the same name as the URL's host.
220func (o *URLOpener) OpenBucketURL(ctx context.Context, u *url.URL) (*blob.Bucket, error) {
221	opts, err := o.forParams(ctx, u.Query())
222	if err != nil {
223		return nil, fmt.Errorf("open bucket %v: %v", u, err)
224	}
225	return OpenBucket(ctx, o.Client, u.Host, opts)
226}
227
228func (o *URLOpener) forParams(ctx context.Context, q url.Values) (*Options, error) {
229	for k := range q {
230		if k != "access_id" && k != "private_key_path" {
231			return nil, fmt.Errorf("invalid query parameter %q", k)
232		}
233	}
234	opts := new(Options)
235	*opts = o.Options
236	if accessID := q.Get("access_id"); accessID != "" && accessID != opts.GoogleAccessID {
237		opts.GoogleAccessID = accessID
238		opts.PrivateKey = nil // Clear any previous key unrelated to the new accessID.
239
240		// Clear this as well to prevent calls with the old and mismatched accessID.
241		opts.MakeSignBytes = nil
242	}
243	if keyPath := q.Get("private_key_path"); keyPath != "" {
244		pk, err := ioutil.ReadFile(keyPath)
245		if err != nil {
246			return nil, err
247		}
248		opts.PrivateKey = pk
249	} else if _, exists := q["private_key_path"]; exists {
250		// A possible default value has been cleared by setting this to an empty value:
251		// The private key might have expired, or falling back to SignBytes/MakeSignBytes
252		// is intentional such as for tests or involving a key stored in a HSM/TPM.
253		opts.PrivateKey = nil
254	}
255	return opts, nil
256}
257
258// Options sets options for constructing a *blob.Bucket backed by GCS.
259type Options struct {
260	// GoogleAccessID represents the authorizer for SignedURL.
261	// Required to use SignedURL.
262	// See https://godoc.org/cloud.google.com/go/storage#SignedURLOptions.
263	GoogleAccessID string
264
265	// PrivateKey is the Google service account private key.
266	// Exactly one of PrivateKey or SignBytes must be non-nil to use SignedURL.
267	// See https://godoc.org/cloud.google.com/go/storage#SignedURLOptions.
268	PrivateKey []byte
269
270	// SignBytes is a function for implementing custom signing.
271	// Exactly one of PrivateKey, SignBytes, or MakeSignBytes must be non-nil to use SignedURL.
272	// See https://godoc.org/cloud.google.com/go/storage#SignedURLOptions.
273	SignBytes func([]byte) ([]byte, error)
274
275	// MakeSignBytes is a factory for functions that are being used in place of an empty SignBytes.
276	// If your implementation of 'SignBytes' needs a request context, set this instead.
277	MakeSignBytes func(requestCtx context.Context) SignBytesFunc
278}
279
280// SignBytesFunc is shorthand for the signature of Options.SignBytes.
281type SignBytesFunc func([]byte) ([]byte, error)
282
283// openBucket returns a GCS Bucket that communicates using the given HTTP client.
284func openBucket(ctx context.Context, client *gcp.HTTPClient, bucketName string, opts *Options) (*bucket, error) {
285	if client == nil {
286		return nil, errors.New("gcsblob.OpenBucket: client is required")
287	}
288	if bucketName == "" {
289		return nil, errors.New("gcsblob.OpenBucket: bucketName is required")
290	}
291
292	clientOpts := []option.ClientOption{option.WithHTTPClient(useragent.HTTPClient(&client.Client, "blob"))}
293	if host := os.Getenv("STORAGE_EMULATOR_HOST"); host != "" {
294		clientOpts = []option.ClientOption{
295			option.WithoutAuthentication(),
296			option.WithEndpoint("http://" + host + "/storage/v1/"),
297			option.WithHTTPClient(http.DefaultClient),
298		}
299	}
300
301	// We wrap the provided http.Client to add a Go CDK User-Agent.
302	c, err := storage.NewClient(ctx, clientOpts...)
303	if err != nil {
304		return nil, err
305	}
306	if opts == nil {
307		opts = &Options{}
308	}
309	return &bucket{name: bucketName, client: c, opts: opts}, nil
310}
311
312// OpenBucket returns a *blob.Bucket backed by an existing GCS bucket. See the
313// package documentation for an example.
314func OpenBucket(ctx context.Context, client *gcp.HTTPClient, bucketName string, opts *Options) (*blob.Bucket, error) {
315	drv, err := openBucket(ctx, client, bucketName, opts)
316	if err != nil {
317		return nil, err
318	}
319	return blob.NewBucket(drv), nil
320}
321
322// bucket represents a GCS bucket, which handles read, write and delete operations
323// on objects within it.
324type bucket struct {
325	name   string
326	client *storage.Client
327	opts   *Options
328}
329
330var emptyBody = ioutil.NopCloser(strings.NewReader(""))
331
332// reader reads a GCS object. It implements driver.Reader.
333type reader struct {
334	body  io.ReadCloser
335	attrs driver.ReaderAttributes
336	raw   *storage.Reader
337}
338
339func (r *reader) Read(p []byte) (int, error) {
340	return r.body.Read(p)
341}
342
343// Close closes the reader itself. It must be called when done reading.
344func (r *reader) Close() error {
345	return r.body.Close()
346}
347
348func (r *reader) Attributes() *driver.ReaderAttributes {
349	return &r.attrs
350}
351
352func (r *reader) As(i interface{}) bool {
353	p, ok := i.(**storage.Reader)
354	if !ok {
355		return false
356	}
357	*p = r.raw
358	return true
359}
360
361func (b *bucket) ErrorCode(err error) gcerrors.ErrorCode {
362	if err == storage.ErrObjectNotExist || err == storage.ErrBucketNotExist {
363		return gcerrors.NotFound
364	}
365	if gerr, ok := err.(*googleapi.Error); ok {
366		switch gerr.Code {
367		case http.StatusForbidden:
368			return gcerrors.PermissionDenied
369		case http.StatusNotFound:
370			return gcerrors.NotFound
371		case http.StatusPreconditionFailed:
372			return gcerrors.FailedPrecondition
373		case http.StatusTooManyRequests:
374			return gcerrors.ResourceExhausted
375		}
376	}
377	return gcerrors.Unknown
378}
379
380func (b *bucket) Close() error {
381	return nil
382}
383
384// ListPaged implements driver.ListPaged.
385func (b *bucket) ListPaged(ctx context.Context, opts *driver.ListOptions) (*driver.ListPage, error) {
386	bkt := b.client.Bucket(b.name)
387	query := &storage.Query{
388		Prefix:    escapeKey(opts.Prefix),
389		Delimiter: escapeKey(opts.Delimiter),
390	}
391	if opts.BeforeList != nil {
392		asFunc := func(i interface{}) bool {
393			p, ok := i.(**storage.Query)
394			if !ok {
395				return false
396			}
397			*p = query
398			return true
399		}
400		if err := opts.BeforeList(asFunc); err != nil {
401			return nil, err
402		}
403	}
404	pageSize := opts.PageSize
405	if pageSize == 0 {
406		pageSize = defaultPageSize
407	}
408	iter := bkt.Objects(ctx, query)
409	pager := iterator.NewPager(iter, pageSize, string(opts.PageToken))
410	var objects []*storage.ObjectAttrs
411	nextPageToken, err := pager.NextPage(&objects)
412	if err != nil {
413		return nil, err
414	}
415	page := driver.ListPage{NextPageToken: []byte(nextPageToken)}
416	if len(objects) > 0 {
417		page.Objects = make([]*driver.ListObject, len(objects))
418		for i, obj := range objects {
419			toCopy := obj
420			asFunc := func(val interface{}) bool {
421				p, ok := val.(*storage.ObjectAttrs)
422				if !ok {
423					return false
424				}
425				*p = *toCopy
426				return true
427			}
428			if obj.Prefix == "" {
429				// Regular blob.
430				page.Objects[i] = &driver.ListObject{
431					Key:     unescapeKey(obj.Name),
432					ModTime: obj.Updated,
433					Size:    obj.Size,
434					MD5:     obj.MD5,
435					AsFunc:  asFunc,
436				}
437			} else {
438				// "Directory".
439				page.Objects[i] = &driver.ListObject{
440					Key:    unescapeKey(obj.Prefix),
441					IsDir:  true,
442					AsFunc: asFunc,
443				}
444			}
445		}
446		// GCS always returns "directories" at the end; sort them.
447		sort.Slice(page.Objects, func(i, j int) bool {
448			return page.Objects[i].Key < page.Objects[j].Key
449		})
450	}
451	return &page, nil
452}
453
454// As implements driver.As.
455func (b *bucket) As(i interface{}) bool {
456	p, ok := i.(**storage.Client)
457	if !ok {
458		return false
459	}
460	*p = b.client
461	return true
462}
463
464// As implements driver.ErrorAs.
465func (b *bucket) ErrorAs(err error, i interface{}) bool {
466	switch v := err.(type) {
467	case *googleapi.Error:
468		if p, ok := i.(**googleapi.Error); ok {
469			*p = v
470			return true
471		}
472	}
473	return false
474}
475
476// Attributes implements driver.Attributes.
477func (b *bucket) Attributes(ctx context.Context, key string) (*driver.Attributes, error) {
478	key = escapeKey(key)
479	bkt := b.client.Bucket(b.name)
480	obj := bkt.Object(key)
481	attrs, err := obj.Attrs(ctx)
482	if err != nil {
483		return nil, err
484	}
485	// GCS seems to unquote the ETag; restore them.
486	// It should be of the form "xxxx" or W/"xxxx".
487	eTag := attrs.Etag
488	if !strings.HasPrefix(eTag, "W/\"") && !strings.HasPrefix(eTag, "\"") && !strings.HasSuffix(eTag, "\"") {
489		eTag = fmt.Sprintf("%q", eTag)
490	}
491	return &driver.Attributes{
492		CacheControl:       attrs.CacheControl,
493		ContentDisposition: attrs.ContentDisposition,
494		ContentEncoding:    attrs.ContentEncoding,
495		ContentLanguage:    attrs.ContentLanguage,
496		ContentType:        attrs.ContentType,
497		Metadata:           attrs.Metadata,
498		CreateTime:         attrs.Created,
499		ModTime:            attrs.Updated,
500		Size:               attrs.Size,
501		MD5:                attrs.MD5,
502		ETag:               eTag,
503		AsFunc: func(i interface{}) bool {
504			p, ok := i.(*storage.ObjectAttrs)
505			if !ok {
506				return false
507			}
508			*p = *attrs
509			return true
510		},
511	}, nil
512}
513
514// NewRangeReader implements driver.NewRangeReader.
515func (b *bucket) NewRangeReader(ctx context.Context, key string, offset, length int64, opts *driver.ReaderOptions) (driver.Reader, error) {
516	key = escapeKey(key)
517	bkt := b.client.Bucket(b.name)
518	obj := bkt.Object(key)
519
520	// Add an extra level of indirection so that BeforeRead can replace obj
521	// if needed. For example, ObjectHandle.If returns a new ObjectHandle.
522	// Also, make the Reader lazily in case this replacement happens.
523	objp := &obj
524	makeReader := func() (*storage.Reader, error) {
525		return (*objp).NewRangeReader(ctx, offset, length)
526	}
527
528	var r *storage.Reader
529	var rerr error
530	madeReader := false
531	if opts.BeforeRead != nil {
532		asFunc := func(i interface{}) bool {
533			if p, ok := i.(***storage.ObjectHandle); ok && !madeReader {
534				*p = objp
535				return true
536			}
537			if p, ok := i.(**storage.Reader); ok {
538				if !madeReader {
539					r, rerr = makeReader()
540					madeReader = true
541					if r == nil {
542						return false
543					}
544				}
545				*p = r
546				return true
547			}
548			return false
549		}
550		if err := opts.BeforeRead(asFunc); err != nil {
551			return nil, err
552		}
553	}
554	if !madeReader {
555		r, rerr = makeReader()
556	}
557	if rerr != nil {
558		return nil, rerr
559	}
560	return &reader{
561		body: r,
562		attrs: driver.ReaderAttributes{
563			ContentType: r.Attrs.ContentType,
564			ModTime:     r.Attrs.LastModified,
565			Size:        r.Attrs.Size,
566		},
567		raw: r,
568	}, nil
569}
570
571// escapeKey does all required escaping for UTF-8 strings to work with GCS.
572func escapeKey(key string) string {
573	return escape.HexEscape(key, func(r []rune, i int) bool {
574		switch {
575		// GCS doesn't handle these characters (determined via experimentation).
576		case r[i] == 10 || r[i] == 13:
577			return true
578		// For "../", escape the trailing slash.
579		case i > 1 && r[i] == '/' && r[i-1] == '.' && r[i-2] == '.':
580			return true
581		}
582		return false
583	})
584}
585
586// unescapeKey reverses escapeKey.
587func unescapeKey(key string) string {
588	return escape.HexUnescape(key)
589}
590
591// NewTypedWriter implements driver.NewTypedWriter.
592func (b *bucket) NewTypedWriter(ctx context.Context, key string, contentType string, opts *driver.WriterOptions) (driver.Writer, error) {
593	key = escapeKey(key)
594	bkt := b.client.Bucket(b.name)
595	obj := bkt.Object(key)
596
597	// Add an extra level of indirection so that BeforeWrite can replace obj
598	// if needed. For example, ObjectHandle.If returns a new ObjectHandle.
599	// Also, make the Writer lazily in case this replacement happens.
600	objp := &obj
601	makeWriter := func() *storage.Writer {
602		w := (*objp).NewWriter(ctx)
603		w.CacheControl = opts.CacheControl
604		w.ContentDisposition = opts.ContentDisposition
605		w.ContentEncoding = opts.ContentEncoding
606		w.ContentLanguage = opts.ContentLanguage
607		w.ContentType = contentType
608		w.ChunkSize = bufferSize(opts.BufferSize)
609		w.Metadata = opts.Metadata
610		w.MD5 = opts.ContentMD5
611		return w
612	}
613
614	var w *storage.Writer
615	if opts.BeforeWrite != nil {
616		asFunc := func(i interface{}) bool {
617			if p, ok := i.(***storage.ObjectHandle); ok && w == nil {
618				*p = objp
619				return true
620			}
621			if p, ok := i.(**storage.Writer); ok {
622				if w == nil {
623					w = makeWriter()
624				}
625				*p = w
626				return true
627			}
628			return false
629		}
630		if err := opts.BeforeWrite(asFunc); err != nil {
631			return nil, err
632		}
633	}
634	if w == nil {
635		w = makeWriter()
636	}
637	return w, nil
638}
639
640// CopyObjectHandles holds the ObjectHandles for the destination and source
641// of a Copy. It is used by the BeforeCopy As hook.
642type CopyObjectHandles struct {
643	Dst, Src *storage.ObjectHandle
644}
645
646// Copy implements driver.Copy.
647func (b *bucket) Copy(ctx context.Context, dstKey, srcKey string, opts *driver.CopyOptions) error {
648	dstKey = escapeKey(dstKey)
649	srcKey = escapeKey(srcKey)
650	bkt := b.client.Bucket(b.name)
651
652	// Add an extra level of indirection so that BeforeCopy can replace the
653	// dst or src ObjectHandles if needed.
654	// Also, make the Copier lazily in case this replacement happens.
655	handles := CopyObjectHandles{
656		Dst: bkt.Object(dstKey),
657		Src: bkt.Object(srcKey),
658	}
659	makeCopier := func() *storage.Copier {
660		return handles.Dst.CopierFrom(handles.Src)
661	}
662
663	var copier *storage.Copier
664	if opts.BeforeCopy != nil {
665		asFunc := func(i interface{}) bool {
666			if p, ok := i.(**CopyObjectHandles); ok && copier == nil {
667				*p = &handles
668				return true
669			}
670			if p, ok := i.(**storage.Copier); ok {
671				if copier == nil {
672					copier = makeCopier()
673				}
674				*p = copier
675				return true
676			}
677			return false
678		}
679		if err := opts.BeforeCopy(asFunc); err != nil {
680			return err
681		}
682	}
683	if copier == nil {
684		copier = makeCopier()
685	}
686	_, err := copier.Run(ctx)
687	return err
688}
689
690// Delete implements driver.Delete.
691func (b *bucket) Delete(ctx context.Context, key string) error {
692	key = escapeKey(key)
693	bkt := b.client.Bucket(b.name)
694	obj := bkt.Object(key)
695	return obj.Delete(ctx)
696}
697
698func (b *bucket) SignedURL(ctx context.Context, key string, dopts *driver.SignedURLOptions) (string, error) {
699	numSigners := 0
700	if b.opts.PrivateKey != nil {
701		numSigners++
702	}
703	if b.opts.SignBytes != nil {
704		numSigners++
705	}
706	if b.opts.MakeSignBytes != nil {
707		numSigners++
708	}
709	if b.opts.GoogleAccessID == "" || numSigners != 1 {
710		return "", gcerr.New(gcerr.Unimplemented, nil, 1, "gcsblob: to use SignedURL, you must call OpenBucket with a valid Options.GoogleAccessID and exactly one of Options.PrivateKey, Options.SignBytes, or Options.MakeSignBytes")
711	}
712
713	key = escapeKey(key)
714	opts := &storage.SignedURLOptions{
715		Expires:        time.Now().Add(dopts.Expiry),
716		Method:         dopts.Method,
717		ContentType:    dopts.ContentType,
718		GoogleAccessID: b.opts.GoogleAccessID,
719		PrivateKey:     b.opts.PrivateKey,
720		SignBytes:      b.opts.SignBytes,
721	}
722	if b.opts.MakeSignBytes != nil {
723		opts.SignBytes = b.opts.MakeSignBytes(ctx)
724	}
725	if dopts.BeforeSign != nil {
726		asFunc := func(i interface{}) bool {
727			v, ok := i.(**storage.SignedURLOptions)
728			if ok {
729				*v = opts
730			}
731			return ok
732		}
733		if err := dopts.BeforeSign(asFunc); err != nil {
734			return "", err
735		}
736	}
737	return storage.SignedURL(b.name, key, opts)
738}
739
740func bufferSize(size int) int {
741	if size == 0 {
742		return googleapi.DefaultUploadChunkSize
743	} else if size > 0 {
744		return size
745	}
746	return 0 // disable buffering
747}
748