| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382 |
- // Copyright 2014 Google LLC
- //
- // Licensed under the Apache License, Version 2.0 (the "License");
- // you may not use this file except in compliance with the License.
- // You may obtain a copy of the License at
- //
- // http://www.apache.org/licenses/LICENSE-2.0
- //
- // Unless required by applicable law or agreed to in writing, software
- // distributed under the License is distributed on an "AS IS" BASIS,
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- // See the License for the specific language governing permissions and
- // limitations under the License.
- package storage
- import (
- "bytes"
- "context"
- "crypto"
- "crypto/rand"
- "crypto/rsa"
- "crypto/sha256"
- "crypto/x509"
- "encoding/base64"
- "encoding/hex"
- "encoding/pem"
- "errors"
- "fmt"
- "net/http"
- "net/url"
- "os"
- "reflect"
- "regexp"
- "sort"
- "strconv"
- "strings"
- "time"
- "unicode/utf8"
- "cloud.google.com/go/internal/optional"
- "cloud.google.com/go/internal/trace"
- "cloud.google.com/go/storage/internal"
- "cloud.google.com/go/storage/internal/apiv2/storagepb"
- "github.com/googleapis/gax-go/v2"
- "golang.org/x/oauth2/google"
- "google.golang.org/api/googleapi"
- "google.golang.org/api/option"
- "google.golang.org/api/option/internaloption"
- raw "google.golang.org/api/storage/v1"
- "google.golang.org/api/transport"
- htransport "google.golang.org/api/transport/http"
- "google.golang.org/protobuf/proto"
- "google.golang.org/protobuf/reflect/protoreflect"
- "google.golang.org/protobuf/types/known/fieldmaskpb"
- "google.golang.org/protobuf/types/known/timestamppb"
- )
- // Methods which can be used in signed URLs.
- var signedURLMethods = map[string]bool{"DELETE": true, "GET": true, "HEAD": true, "POST": true, "PUT": true}
- var (
- // ErrBucketNotExist indicates that the bucket does not exist.
- ErrBucketNotExist = errors.New("storage: bucket doesn't exist")
- // ErrObjectNotExist indicates that the object does not exist.
- ErrObjectNotExist = errors.New("storage: object doesn't exist")
- // errMethodNotSupported indicates that the method called is not currently supported by the client.
- // TODO: Export this error when launching the transport-agnostic client.
- errMethodNotSupported = errors.New("storage: method is not currently supported")
- // errMethodNotValid indicates that given HTTP method is not valid.
- errMethodNotValid = fmt.Errorf("storage: HTTP method should be one of %v", reflect.ValueOf(signedURLMethods).MapKeys())
- )
- var userAgent = fmt.Sprintf("gcloud-golang-storage/%s", internal.Version)
- const (
- // ScopeFullControl grants permissions to manage your
- // data and permissions in Google Cloud Storage.
- ScopeFullControl = raw.DevstorageFullControlScope
- // ScopeReadOnly grants permissions to
- // view your data in Google Cloud Storage.
- ScopeReadOnly = raw.DevstorageReadOnlyScope
- // ScopeReadWrite grants permissions to manage your
- // data in Google Cloud Storage.
- ScopeReadWrite = raw.DevstorageReadWriteScope
- // aes256Algorithm is the AES256 encryption algorithm used with the
- // Customer-Supplied Encryption Keys feature.
- aes256Algorithm = "AES256"
- // defaultGen indicates the latest object generation by default,
- // using a negative value.
- defaultGen = int64(-1)
- )
- // TODO: remove this once header with invocation ID is applied to all methods.
- func setClientHeader(headers http.Header) {
- headers.Set("x-goog-api-client", xGoogDefaultHeader)
- }
- // Client is a client for interacting with Google Cloud Storage.
- //
- // Clients should be reused instead of created as needed.
- // The methods of Client are safe for concurrent use by multiple goroutines.
- type Client struct {
- hc *http.Client
- raw *raw.Service
- // Scheme describes the scheme under the current host.
- scheme string
- // xmlHost is the default host used for XML requests.
- xmlHost string
- // May be nil.
- creds *google.Credentials
- retry *retryConfig
- // tc is the transport-agnostic client implemented with either gRPC or HTTP.
- tc storageClient
- // useGRPC flags whether the client uses gRPC. This is needed while the
- // integration piece is only partially complete.
- // TODO: remove before merging to main.
- useGRPC bool
- }
- // NewClient creates a new Google Cloud Storage client using the HTTP transport.
- // The default scope is ScopeFullControl. To use a different scope, like
- // ScopeReadOnly, use option.WithScopes.
- //
- // Clients should be reused instead of created as needed. The methods of Client
- // are safe for concurrent use by multiple goroutines.
- //
- // You may configure the client by passing in options from the [google.golang.org/api/option]
- // package. You may also use options defined in this package, such as [WithJSONReads].
- func NewClient(ctx context.Context, opts ...option.ClientOption) (*Client, error) {
- var creds *google.Credentials
- // In general, it is recommended to use raw.NewService instead of htransport.NewClient
- // since raw.NewService configures the correct default endpoints when initializing the
- // internal http client. However, in our case, "NewRangeReader" in reader.go needs to
- // access the http client directly to make requests, so we create the client manually
- // here so it can be re-used by both reader.go and raw.NewService. This means we need to
- // manually configure the default endpoint options on the http client. Furthermore, we
- // need to account for STORAGE_EMULATOR_HOST override when setting the default endpoints.
- if host := os.Getenv("STORAGE_EMULATOR_HOST"); host == "" {
- // Prepend default options to avoid overriding options passed by the user.
- opts = append([]option.ClientOption{option.WithScopes(ScopeFullControl, "https://www.googleapis.com/auth/cloud-platform"), option.WithUserAgent(userAgent)}, opts...)
- opts = append(opts, internaloption.WithDefaultEndpointTemplate("https://storage.UNIVERSE_DOMAIN/storage/v1/"),
- internaloption.WithDefaultMTLSEndpoint("https://storage.mtls.googleapis.com/storage/v1/"),
- internaloption.WithDefaultUniverseDomain("googleapis.com"),
- )
- // Don't error out here. The user may have passed in their own HTTP
- // client which does not auth with ADC or other common conventions.
- c, err := transport.Creds(ctx, opts...)
- if err == nil {
- creds = c
- opts = append(opts, internaloption.WithCredentials(creds))
- }
- } else {
- var hostURL *url.URL
- if strings.Contains(host, "://") {
- h, err := url.Parse(host)
- if err != nil {
- return nil, err
- }
- hostURL = h
- } else {
- // Add scheme for user if not supplied in STORAGE_EMULATOR_HOST
- // URL is only parsed correctly if it has a scheme, so we build it ourselves
- hostURL = &url.URL{Scheme: "http", Host: host}
- }
- hostURL.Path = "storage/v1/"
- endpoint := hostURL.String()
- // Append the emulator host as default endpoint for the user
- opts = append([]option.ClientOption{
- option.WithoutAuthentication(),
- internaloption.SkipDialSettingsValidation(),
- internaloption.WithDefaultEndpoint(endpoint),
- internaloption.WithDefaultMTLSEndpoint(endpoint),
- }, opts...)
- }
- // htransport selects the correct endpoint among WithEndpoint (user override), WithDefaultEndpoint, and WithDefaultMTLSEndpoint.
- hc, ep, err := htransport.NewClient(ctx, opts...)
- if err != nil {
- return nil, fmt.Errorf("dialing: %w", err)
- }
- // RawService should be created with the chosen endpoint to take account of user override.
- rawService, err := raw.NewService(ctx, option.WithEndpoint(ep), option.WithHTTPClient(hc))
- if err != nil {
- return nil, fmt.Errorf("storage client: %w", err)
- }
- // Update xmlHost and scheme with the chosen endpoint.
- u, err := url.Parse(ep)
- if err != nil {
- return nil, fmt.Errorf("supplied endpoint %q is not valid: %w", ep, err)
- }
- tc, err := newHTTPStorageClient(ctx, withClientOptions(opts...))
- if err != nil {
- return nil, fmt.Errorf("storage: %w", err)
- }
- return &Client{
- hc: hc,
- raw: rawService,
- scheme: u.Scheme,
- xmlHost: u.Host,
- creds: creds,
- tc: tc,
- }, nil
- }
- // NewGRPCClient creates a new Storage client using the gRPC transport and API.
- // Client methods which have not been implemented in gRPC will return an error.
- // In particular, methods for Cloud Pub/Sub notifications are not supported.
- // Using a non-default universe domain is also not supported with the Storage
- // gRPC client.
- //
- // The storage gRPC API is still in preview and not yet publicly available.
- // If you would like to use the API, please first contact your GCP account rep to
- // request access. The API may be subject to breaking changes.
- //
- // Clients should be reused instead of created as needed. The methods of Client
- // are safe for concurrent use by multiple goroutines.
- //
- // You may configure the client by passing in options from the [google.golang.org/api/option]
- // package.
- func NewGRPCClient(ctx context.Context, opts ...option.ClientOption) (*Client, error) {
- opts = append(defaultGRPCOptions(), opts...)
- tc, err := newGRPCStorageClient(ctx, withClientOptions(opts...))
- if err != nil {
- return nil, err
- }
- return &Client{tc: tc, useGRPC: true}, nil
- }
- // Close closes the Client.
- //
- // Close need not be called at program exit.
- func (c *Client) Close() error {
- // Set fields to nil so that subsequent uses will panic.
- c.hc = nil
- c.raw = nil
- c.creds = nil
- if c.tc != nil {
- return c.tc.Close()
- }
- return nil
- }
- // SigningScheme determines the API version to use when signing URLs.
- type SigningScheme int
- const (
- // SigningSchemeDefault is presently V2 and will change to V4 in the future.
- SigningSchemeDefault SigningScheme = iota
- // SigningSchemeV2 uses the V2 scheme to sign URLs.
- SigningSchemeV2
- // SigningSchemeV4 uses the V4 scheme to sign URLs.
- SigningSchemeV4
- )
- // URLStyle determines the style to use for the signed URL. PathStyle is the
- // default. All non-default options work with V4 scheme only. See
- // https://cloud.google.com/storage/docs/request-endpoints for details.
- type URLStyle interface {
- // host should return the host portion of the signed URL, not including
- // the scheme (e.g. storage.googleapis.com).
- host(hostname, bucket string) string
- // path should return the path portion of the signed URL, which may include
- // both the bucket and object name or only the object name depending on the
- // style.
- path(bucket, object string) string
- }
- type pathStyle struct{}
- type virtualHostedStyle struct{}
- type bucketBoundHostname struct {
- hostname string
- }
- func (s pathStyle) host(hostname, bucket string) string {
- if hostname != "" {
- return stripScheme(hostname)
- }
- if host := os.Getenv("STORAGE_EMULATOR_HOST"); host != "" {
- return stripScheme(host)
- }
- return "storage.googleapis.com"
- }
- func (s virtualHostedStyle) host(hostname, bucket string) string {
- if hostname != "" {
- return bucket + "." + stripScheme(hostname)
- }
- if host := os.Getenv("STORAGE_EMULATOR_HOST"); host != "" {
- return bucket + "." + stripScheme(host)
- }
- return bucket + ".storage.googleapis.com"
- }
- func (s bucketBoundHostname) host(_, bucket string) string {
- return s.hostname
- }
- func (s pathStyle) path(bucket, object string) string {
- p := bucket
- if object != "" {
- p += "/" + object
- }
- return p
- }
- func (s virtualHostedStyle) path(bucket, object string) string {
- return object
- }
- func (s bucketBoundHostname) path(bucket, object string) string {
- return object
- }
- // PathStyle is the default style, and will generate a URL of the form
- // "<host-name>/<bucket-name>/<object-name>". By default, <host-name> is
- // storage.googleapis.com, but setting an endpoint on the storage Client or
- // through STORAGE_EMULATOR_HOST overrides this. Setting Hostname on
- // SignedURLOptions or PostPolicyV4Options overrides everything else.
- func PathStyle() URLStyle {
- return pathStyle{}
- }
- // VirtualHostedStyle generates a URL relative to the bucket's virtual
- // hostname, e.g. "<bucket-name>.storage.googleapis.com/<object-name>".
- func VirtualHostedStyle() URLStyle {
- return virtualHostedStyle{}
- }
- // BucketBoundHostname generates a URL with a custom hostname tied to a
- // specific GCS bucket. The desired hostname should be passed in using the
- // hostname argument. Generated urls will be of the form
- // "<bucket-bound-hostname>/<object-name>". See
- // https://cloud.google.com/storage/docs/request-endpoints#cname and
- // https://cloud.google.com/load-balancing/docs/https/adding-backend-buckets-to-load-balancers
- // for details. Note that for CNAMEs, only HTTP is supported, so Insecure must
- // be set to true.
- func BucketBoundHostname(hostname string) URLStyle {
- return bucketBoundHostname{hostname: hostname}
- }
- // Strips the scheme from a host if it contains it
- func stripScheme(host string) string {
- if strings.Contains(host, "://") {
- host = strings.SplitN(host, "://", 2)[1]
- }
- return host
- }
- // SignedURLOptions allows you to restrict the access to the signed URL.
- type SignedURLOptions struct {
- // GoogleAccessID represents the authorizer of the signed URL generation.
- // It is typically the Google service account client email address from
- // the Google Developers Console in the form of "xxx@developer.gserviceaccount.com".
- // Required.
- GoogleAccessID string
- // PrivateKey is the Google service account private key. It is obtainable
- // from the Google Developers Console.
- // At https://console.developers.google.com/project/<your-project-id>/apiui/credential,
- // create a service account client ID or reuse one of your existing service account
- // credentials. Click on the "Generate new P12 key" to generate and download
- // a new private key. Once you download the P12 file, use the following command
- // to convert it into a PEM file.
- //
- // $ openssl pkcs12 -in key.p12 -passin pass:notasecret -out key.pem -nodes
- //
- // Provide the contents of the PEM file as a byte slice.
- // Exactly one of PrivateKey or SignBytes must be non-nil.
- PrivateKey []byte
- // SignBytes is a function for implementing custom signing. For example, if
- // your application is running on Google App Engine, you can use
- // appengine's internal signing function:
- // ctx := appengine.NewContext(request)
- // acc, _ := appengine.ServiceAccount(ctx)
- // url, err := SignedURL("bucket", "object", &SignedURLOptions{
- // GoogleAccessID: acc,
- // SignBytes: func(b []byte) ([]byte, error) {
- // _, signedBytes, err := appengine.SignBytes(ctx, b)
- // return signedBytes, err
- // },
- // // etc.
- // })
- //
- // Exactly one of PrivateKey or SignBytes must be non-nil.
- SignBytes func([]byte) ([]byte, error)
- // Method is the HTTP method to be used with the signed URL.
- // Signed URLs can be used with GET, HEAD, PUT, and DELETE requests.
- // Required.
- Method string
- // Expires is the expiration time on the signed URL. It must be
- // a datetime in the future. For SigningSchemeV4, the expiration may be no
- // more than seven days in the future.
- // Required.
- Expires time.Time
- // ContentType is the content type header the client must provide
- // to use the generated signed URL.
- // Optional.
- ContentType string
- // Headers is a list of extension headers the client must provide
- // in order to use the generated signed URL. Each must be a string of the
- // form "key:values", with multiple values separated by a semicolon.
- // Optional.
- Headers []string
- // QueryParameters is a map of additional query parameters. When
- // SigningScheme is V4, this is used in computing the signature, and the
- // client must use the same query parameters when using the generated signed
- // URL.
- // Optional.
- QueryParameters url.Values
- // MD5 is the base64 encoded MD5 checksum of the file.
- // If provided, the client should provide the exact value on the request
- // header in order to use the signed URL.
- // Optional.
- MD5 string
- // Style provides options for the type of URL to use. Options are
- // PathStyle (default), BucketBoundHostname, and VirtualHostedStyle. See
- // https://cloud.google.com/storage/docs/request-endpoints for details.
- // Only supported for V4 signing.
- // Optional.
- Style URLStyle
- // Insecure determines whether the signed URL should use HTTPS (default) or
- // HTTP.
- // Only supported for V4 signing.
- // Optional.
- Insecure bool
- // Scheme determines the version of URL signing to use. Default is
- // SigningSchemeV2.
- Scheme SigningScheme
- // Hostname sets the host of the signed URL. This field overrides any
- // endpoint set on a storage Client or through STORAGE_EMULATOR_HOST.
- // Only compatible with PathStyle and VirtualHostedStyle URLStyles.
- // Optional.
- Hostname string
- }
- func (opts *SignedURLOptions) clone() *SignedURLOptions {
- return &SignedURLOptions{
- GoogleAccessID: opts.GoogleAccessID,
- SignBytes: opts.SignBytes,
- PrivateKey: opts.PrivateKey,
- Method: opts.Method,
- Expires: opts.Expires,
- ContentType: opts.ContentType,
- Headers: opts.Headers,
- QueryParameters: opts.QueryParameters,
- MD5: opts.MD5,
- Style: opts.Style,
- Insecure: opts.Insecure,
- Scheme: opts.Scheme,
- Hostname: opts.Hostname,
- }
- }
- var (
- tabRegex = regexp.MustCompile(`[\t]+`)
- // I was tempted to call this spacex. :)
- spaceRegex = regexp.MustCompile(` +`)
- canonicalHeaderRegexp = regexp.MustCompile(`(?i)^(x-goog-[^:]+):(.*)?$`)
- excludedCanonicalHeaders = map[string]bool{
- "x-goog-encryption-key": true,
- "x-goog-encryption-key-sha256": true,
- }
- )
- // v2SanitizeHeaders applies the specifications for canonical extension headers at
- // https://cloud.google.com/storage/docs/access-control/signed-urls-v2#about-canonical-extension-headers
- func v2SanitizeHeaders(hdrs []string) []string {
- headerMap := map[string][]string{}
- for _, hdr := range hdrs {
- // No leading or trailing whitespaces.
- sanitizedHeader := strings.TrimSpace(hdr)
- var header, value string
- // Only keep canonical headers, discard any others.
- headerMatches := canonicalHeaderRegexp.FindStringSubmatch(sanitizedHeader)
- if len(headerMatches) == 0 {
- continue
- }
- header = headerMatches[1]
- value = headerMatches[2]
- header = strings.ToLower(strings.TrimSpace(header))
- value = strings.TrimSpace(value)
- if excludedCanonicalHeaders[header] {
- // Do not keep any deliberately excluded canonical headers when signing.
- continue
- }
- if len(value) > 0 {
- // Remove duplicate headers by appending the values of duplicates
- // in their order of appearance.
- headerMap[header] = append(headerMap[header], value)
- }
- }
- var sanitizedHeaders []string
- for header, values := range headerMap {
- // There should be no spaces around the colon separating the header name
- // from the header value or around the values themselves. The values
- // should be separated by commas.
- //
- // NOTE: The semantics for headers without a value are not clear.
- // However from specifications these should be edge-cases anyway and we
- // should assume that there will be no canonical headers using empty
- // values. Any such headers are discarded at the regexp stage above.
- sanitizedHeaders = append(sanitizedHeaders, fmt.Sprintf("%s:%s", header, strings.Join(values, ",")))
- }
- sort.Strings(sanitizedHeaders)
- return sanitizedHeaders
- }
- // v4SanitizeHeaders applies the specifications for canonical extension headers
- // at https://cloud.google.com/storage/docs/authentication/canonical-requests#about-headers.
- //
- // V4 does a couple things differently from V2:
- // - Headers get sorted by key, instead of by key:value. We do this in
- // signedURLV4.
- // - There's no canonical regexp: we simply split headers on :.
- // - We don't exclude canonical headers.
- // - We replace leading and trailing spaces in header values, like v2, but also
- // all intermediate space duplicates get stripped. That is, there's only ever
- // a single consecutive space.
- func v4SanitizeHeaders(hdrs []string) []string {
- headerMap := map[string][]string{}
- for _, hdr := range hdrs {
- // No leading or trailing whitespaces.
- sanitizedHeader := strings.TrimSpace(hdr)
- var key, value string
- headerMatches := strings.SplitN(sanitizedHeader, ":", 2)
- if len(headerMatches) < 2 {
- continue
- }
- key = headerMatches[0]
- value = headerMatches[1]
- key = strings.ToLower(strings.TrimSpace(key))
- value = strings.TrimSpace(value)
- value = string(spaceRegex.ReplaceAll([]byte(value), []byte(" ")))
- value = string(tabRegex.ReplaceAll([]byte(value), []byte("\t")))
- if len(value) > 0 {
- // Remove duplicate headers by appending the values of duplicates
- // in their order of appearance.
- headerMap[key] = append(headerMap[key], value)
- }
- }
- var sanitizedHeaders []string
- for header, values := range headerMap {
- // There should be no spaces around the colon separating the header name
- // from the header value or around the values themselves. The values
- // should be separated by commas.
- //
- // NOTE: The semantics for headers without a value are not clear.
- // However from specifications these should be edge-cases anyway and we
- // should assume that there will be no canonical headers using empty
- // values. Any such headers are discarded at the regexp stage above.
- sanitizedHeaders = append(sanitizedHeaders, fmt.Sprintf("%s:%s", header, strings.Join(values, ",")))
- }
- return sanitizedHeaders
- }
- // SignedURL returns a URL for the specified object. Signed URLs allow anyone
- // access to a restricted resource for a limited time without needing a
- // Google account or signing in. For more information about signed URLs, see
- // https://cloud.google.com/storage/docs/accesscontrol#signed_urls_query_string_authentication
- // If initializing a Storage Client, instead use the Bucket.SignedURL method
- // which uses the Client's credentials to handle authentication.
- func SignedURL(bucket, object string, opts *SignedURLOptions) (string, error) {
- now := utcNow()
- if err := validateOptions(opts, now); err != nil {
- return "", err
- }
- switch opts.Scheme {
- case SigningSchemeV2:
- opts.Headers = v2SanitizeHeaders(opts.Headers)
- return signedURLV2(bucket, object, opts)
- case SigningSchemeV4:
- opts.Headers = v4SanitizeHeaders(opts.Headers)
- return signedURLV4(bucket, object, opts, now)
- default: // SigningSchemeDefault
- opts.Headers = v2SanitizeHeaders(opts.Headers)
- return signedURLV2(bucket, object, opts)
- }
- }
- func validateOptions(opts *SignedURLOptions, now time.Time) error {
- if opts == nil {
- return errors.New("storage: missing required SignedURLOptions")
- }
- if opts.GoogleAccessID == "" {
- return errors.New("storage: missing required GoogleAccessID")
- }
- if (opts.PrivateKey == nil) == (opts.SignBytes == nil) {
- return errors.New("storage: exactly one of PrivateKey or SignedBytes must be set")
- }
- opts.Method = strings.ToUpper(opts.Method)
- if _, ok := signedURLMethods[opts.Method]; !ok {
- return errMethodNotValid
- }
- if opts.Expires.IsZero() {
- return errors.New("storage: missing required expires option")
- }
- if opts.MD5 != "" {
- md5, err := base64.StdEncoding.DecodeString(opts.MD5)
- if err != nil || len(md5) != 16 {
- return errors.New("storage: invalid MD5 checksum")
- }
- }
- if opts.Style == nil {
- opts.Style = PathStyle()
- }
- if _, ok := opts.Style.(pathStyle); !ok && opts.Scheme == SigningSchemeV2 {
- return errors.New("storage: only path-style URLs are permitted with SigningSchemeV2")
- }
- if opts.Scheme == SigningSchemeV4 {
- cutoff := now.Add(604801 * time.Second) // 7 days + 1 second
- if !opts.Expires.Before(cutoff) {
- return errors.New("storage: expires must be within seven days from now")
- }
- }
- return nil
- }
- const (
- iso8601 = "20060102T150405Z"
- yearMonthDay = "20060102"
- )
- // utcNow returns the current time in UTC and is a variable to allow for
- // reassignment in tests to provide deterministic signed URL values.
- var utcNow = func() time.Time {
- return time.Now().UTC()
- }
- // extractHeaderNames takes in a series of key:value headers and returns the
- // header names only.
- func extractHeaderNames(kvs []string) []string {
- var res []string
- for _, header := range kvs {
- nameValue := strings.SplitN(header, ":", 2)
- res = append(res, nameValue[0])
- }
- return res
- }
- // pathEncodeV4 creates an encoded string that matches the v4 signature spec.
- // Following the spec precisely is necessary in order to ensure that the URL
- // and signing string are correctly formed, and Go's url.PathEncode and
- // url.QueryEncode don't generate an exact match without some additional logic.
- func pathEncodeV4(path string) string {
- segments := strings.Split(path, "/")
- var encodedSegments []string
- for _, s := range segments {
- encodedSegments = append(encodedSegments, url.QueryEscape(s))
- }
- encodedStr := strings.Join(encodedSegments, "/")
- encodedStr = strings.Replace(encodedStr, "+", "%20", -1)
- return encodedStr
- }
- // signedURLV4 creates a signed URL using the sigV4 algorithm.
- func signedURLV4(bucket, name string, opts *SignedURLOptions, now time.Time) (string, error) {
- buf := &bytes.Buffer{}
- fmt.Fprintf(buf, "%s\n", opts.Method)
- u := &url.URL{Path: opts.Style.path(bucket, name)}
- u.RawPath = pathEncodeV4(u.Path)
- // Note: we have to add a / here because GCS does so auto-magically, despite
- // our encoding not doing so (and we have to exactly match their
- // canonical query).
- fmt.Fprintf(buf, "/%s\n", u.RawPath)
- headerNames := append(extractHeaderNames(opts.Headers), "host")
- if opts.ContentType != "" {
- headerNames = append(headerNames, "content-type")
- }
- if opts.MD5 != "" {
- headerNames = append(headerNames, "content-md5")
- }
- sort.Strings(headerNames)
- signedHeaders := strings.Join(headerNames, ";")
- timestamp := now.Format(iso8601)
- credentialScope := fmt.Sprintf("%s/auto/storage/goog4_request", now.Format(yearMonthDay))
- canonicalQueryString := url.Values{
- "X-Goog-Algorithm": {"GOOG4-RSA-SHA256"},
- "X-Goog-Credential": {fmt.Sprintf("%s/%s", opts.GoogleAccessID, credentialScope)},
- "X-Goog-Date": {timestamp},
- "X-Goog-Expires": {fmt.Sprintf("%d", int(opts.Expires.Sub(now).Seconds()))},
- "X-Goog-SignedHeaders": {signedHeaders},
- }
- // Add user-supplied query parameters to the canonical query string. For V4,
- // it's necessary to include these.
- for k, v := range opts.QueryParameters {
- canonicalQueryString[k] = append(canonicalQueryString[k], v...)
- }
- // url.Values.Encode escaping is correct, except that a space must be replaced
- // by `%20` rather than `+`.
- escapedQuery := strings.Replace(canonicalQueryString.Encode(), "+", "%20", -1)
- fmt.Fprintf(buf, "%s\n", escapedQuery)
- // Fill in the hostname based on the desired URL style.
- u.Host = opts.Style.host(opts.Hostname, bucket)
- // Fill in the URL scheme.
- if opts.Insecure {
- u.Scheme = "http"
- } else {
- u.Scheme = "https"
- }
- var headersWithValue []string
- headersWithValue = append(headersWithValue, "host:"+u.Hostname())
- headersWithValue = append(headersWithValue, opts.Headers...)
- if opts.ContentType != "" {
- headersWithValue = append(headersWithValue, "content-type:"+opts.ContentType)
- }
- if opts.MD5 != "" {
- headersWithValue = append(headersWithValue, "content-md5:"+opts.MD5)
- }
- // Trim extra whitespace from headers and replace with a single space.
- var trimmedHeaders []string
- for _, h := range headersWithValue {
- trimmedHeaders = append(trimmedHeaders, strings.Join(strings.Fields(h), " "))
- }
- canonicalHeaders := strings.Join(sortHeadersByKey(trimmedHeaders), "\n")
- fmt.Fprintf(buf, "%s\n\n", canonicalHeaders)
- fmt.Fprintf(buf, "%s\n", signedHeaders)
- // If the user provides a value for X-Goog-Content-SHA256, we must use
- // that value in the request string. If not, we use UNSIGNED-PAYLOAD.
- sha256Header := false
- for _, h := range trimmedHeaders {
- if strings.HasPrefix(strings.ToLower(h), "x-goog-content-sha256") && strings.Contains(h, ":") {
- sha256Header = true
- fmt.Fprintf(buf, "%s", strings.SplitN(h, ":", 2)[1])
- break
- }
- }
- if !sha256Header {
- fmt.Fprint(buf, "UNSIGNED-PAYLOAD")
- }
- sum := sha256.Sum256(buf.Bytes())
- hexDigest := hex.EncodeToString(sum[:])
- signBuf := &bytes.Buffer{}
- fmt.Fprint(signBuf, "GOOG4-RSA-SHA256\n")
- fmt.Fprintf(signBuf, "%s\n", timestamp)
- fmt.Fprintf(signBuf, "%s\n", credentialScope)
- fmt.Fprintf(signBuf, "%s", hexDigest)
- signBytes := opts.SignBytes
- if opts.PrivateKey != nil {
- key, err := parseKey(opts.PrivateKey)
- if err != nil {
- return "", err
- }
- signBytes = func(b []byte) ([]byte, error) {
- sum := sha256.Sum256(b)
- return rsa.SignPKCS1v15(
- rand.Reader,
- key,
- crypto.SHA256,
- sum[:],
- )
- }
- }
- b, err := signBytes(signBuf.Bytes())
- if err != nil {
- return "", err
- }
- signature := hex.EncodeToString(b)
- canonicalQueryString.Set("X-Goog-Signature", string(signature))
- u.RawQuery = canonicalQueryString.Encode()
- return u.String(), nil
- }
- // takes a list of headerKey:headervalue1,headervalue2,etc and sorts by header
- // key.
- func sortHeadersByKey(hdrs []string) []string {
- headersMap := map[string]string{}
- var headersKeys []string
- for _, h := range hdrs {
- parts := strings.SplitN(h, ":", 2)
- k := parts[0]
- v := parts[1]
- headersMap[k] = v
- headersKeys = append(headersKeys, k)
- }
- sort.Strings(headersKeys)
- var sorted []string
- for _, k := range headersKeys {
- v := headersMap[k]
- sorted = append(sorted, fmt.Sprintf("%s:%s", k, v))
- }
- return sorted
- }
- func signedURLV2(bucket, name string, opts *SignedURLOptions) (string, error) {
- signBytes := opts.SignBytes
- if opts.PrivateKey != nil {
- key, err := parseKey(opts.PrivateKey)
- if err != nil {
- return "", err
- }
- signBytes = func(b []byte) ([]byte, error) {
- sum := sha256.Sum256(b)
- return rsa.SignPKCS1v15(
- rand.Reader,
- key,
- crypto.SHA256,
- sum[:],
- )
- }
- }
- u := &url.URL{
- Path: fmt.Sprintf("/%s/%s", bucket, name),
- }
- buf := &bytes.Buffer{}
- fmt.Fprintf(buf, "%s\n", opts.Method)
- fmt.Fprintf(buf, "%s\n", opts.MD5)
- fmt.Fprintf(buf, "%s\n", opts.ContentType)
- fmt.Fprintf(buf, "%d\n", opts.Expires.Unix())
- if len(opts.Headers) > 0 {
- fmt.Fprintf(buf, "%s\n", strings.Join(opts.Headers, "\n"))
- }
- fmt.Fprintf(buf, "%s", u.String())
- b, err := signBytes(buf.Bytes())
- if err != nil {
- return "", err
- }
- encoded := base64.StdEncoding.EncodeToString(b)
- u.Scheme = "https"
- u.Host = PathStyle().host(opts.Hostname, bucket)
- q := u.Query()
- q.Set("GoogleAccessId", opts.GoogleAccessID)
- q.Set("Expires", fmt.Sprintf("%d", opts.Expires.Unix()))
- q.Set("Signature", string(encoded))
- u.RawQuery = q.Encode()
- return u.String(), nil
- }
- // ObjectHandle provides operations on an object in a Google Cloud Storage bucket.
- // Use BucketHandle.Object to get a handle.
- type ObjectHandle struct {
- c *Client
- bucket string
- object string
- acl ACLHandle
- gen int64 // a negative value indicates latest
- conds *Conditions
- encryptionKey []byte // AES-256 key
- userProject string // for requester-pays buckets
- readCompressed bool // Accept-Encoding: gzip
- retry *retryConfig
- overrideRetention *bool
- }
- // ACL provides access to the object's access control list.
- // This controls who can read and write this object.
- // This call does not perform any network operations.
- func (o *ObjectHandle) ACL() *ACLHandle {
- return &o.acl
- }
- // Generation returns a new ObjectHandle that operates on a specific generation
- // of the object.
- // By default, the handle operates on the latest generation. Not
- // all operations work when given a specific generation; check the API
- // endpoints at https://cloud.google.com/storage/docs/json_api/ for details.
- func (o *ObjectHandle) Generation(gen int64) *ObjectHandle {
- o2 := *o
- o2.gen = gen
- return &o2
- }
- // If returns a new ObjectHandle that applies a set of preconditions.
- // Preconditions already set on the ObjectHandle are ignored. The supplied
- // Conditions must have at least one field set to a non-default value;
- // otherwise an error will be returned from any operation on the ObjectHandle.
- // Operations on the new handle will return an error if the preconditions are not
- // satisfied. See https://cloud.google.com/storage/docs/generations-preconditions
- // for more details.
- func (o *ObjectHandle) If(conds Conditions) *ObjectHandle {
- o2 := *o
- o2.conds = &conds
- return &o2
- }
- // Key returns a new ObjectHandle that uses the supplied encryption
- // key to encrypt and decrypt the object's contents.
- //
- // Encryption key must be a 32-byte AES-256 key.
- // See https://cloud.google.com/storage/docs/encryption for details.
- func (o *ObjectHandle) Key(encryptionKey []byte) *ObjectHandle {
- o2 := *o
- o2.encryptionKey = encryptionKey
- return &o2
- }
- // Attrs returns meta information about the object.
- // ErrObjectNotExist will be returned if the object is not found.
- func (o *ObjectHandle) Attrs(ctx context.Context) (attrs *ObjectAttrs, err error) {
- ctx = trace.StartSpan(ctx, "cloud.google.com/go/storage.Object.Attrs")
- defer func() { trace.EndSpan(ctx, err) }()
- if err := o.validate(); err != nil {
- return nil, err
- }
- opts := makeStorageOpts(true, o.retry, o.userProject)
- return o.c.tc.GetObject(ctx, o.bucket, o.object, o.gen, o.encryptionKey, o.conds, opts...)
- }
- // Update updates an object with the provided attributes. See
- // ObjectAttrsToUpdate docs for details on treatment of zero values.
- // ErrObjectNotExist will be returned if the object is not found.
- func (o *ObjectHandle) Update(ctx context.Context, uattrs ObjectAttrsToUpdate) (oa *ObjectAttrs, err error) {
- ctx = trace.StartSpan(ctx, "cloud.google.com/go/storage.Object.Update")
- defer func() { trace.EndSpan(ctx, err) }()
- if err := o.validate(); err != nil {
- return nil, err
- }
- isIdempotent := o.conds != nil && o.conds.MetagenerationMatch != 0
- opts := makeStorageOpts(isIdempotent, o.retry, o.userProject)
- return o.c.tc.UpdateObject(ctx,
- &updateObjectParams{
- bucket: o.bucket,
- object: o.object,
- uattrs: &uattrs,
- gen: o.gen,
- encryptionKey: o.encryptionKey,
- conds: o.conds,
- overrideRetention: o.overrideRetention}, opts...)
- }
- // BucketName returns the name of the bucket.
- func (o *ObjectHandle) BucketName() string {
- return o.bucket
- }
- // ObjectName returns the name of the object.
- func (o *ObjectHandle) ObjectName() string {
- return o.object
- }
- // ObjectAttrsToUpdate is used to update the attributes of an object.
- // Only fields set to non-nil values will be updated.
- // For all fields except CustomTime and Retention, set the field to its zero
- // value to delete it. CustomTime cannot be deleted or changed to an earlier
- // time once set. Retention can be deleted (only if the Mode is Unlocked) by
- // setting it to an empty value (not nil).
- //
- // For example, to change ContentType and delete ContentEncoding, Metadata and
- // Retention, use:
- //
- // ObjectAttrsToUpdate{
- // ContentType: "text/html",
- // ContentEncoding: "",
- // Metadata: map[string]string{},
- // Retention: &ObjectRetention{},
- // }
- type ObjectAttrsToUpdate struct {
- EventBasedHold optional.Bool
- TemporaryHold optional.Bool
- ContentType optional.String
- ContentLanguage optional.String
- ContentEncoding optional.String
- ContentDisposition optional.String
- CacheControl optional.String
- CustomTime time.Time // Cannot be deleted or backdated from its current value.
- Metadata map[string]string // Set to map[string]string{} to delete.
- ACL []ACLRule
- // If not empty, applies a predefined set of access controls. ACL must be nil.
- // See https://cloud.google.com/storage/docs/json_api/v1/objects/patch.
- PredefinedACL string
- // Retention contains the retention configuration for this object.
- // Operations other than setting the retention for the first time or
- // extending the RetainUntil time on the object retention must be done
- // on an ObjectHandle with OverrideUnlockedRetention set to true.
- Retention *ObjectRetention
- }
- // Delete deletes the single specified object.
- func (o *ObjectHandle) Delete(ctx context.Context) error {
- if err := o.validate(); err != nil {
- return err
- }
- // Delete is idempotent if GenerationMatch or Generation have been passed in.
- // The default generation is negative to get the latest version of the object.
- isIdempotent := (o.conds != nil && o.conds.GenerationMatch != 0) || o.gen >= 0
- opts := makeStorageOpts(isIdempotent, o.retry, o.userProject)
- return o.c.tc.DeleteObject(ctx, o.bucket, o.object, o.gen, o.conds, opts...)
- }
- // ReadCompressed when true causes the read to happen without decompressing.
- func (o *ObjectHandle) ReadCompressed(compressed bool) *ObjectHandle {
- o2 := *o
- o2.readCompressed = compressed
- return &o2
- }
- // OverrideUnlockedRetention provides an option for overriding an Unlocked
- // Retention policy. This must be set to true in order to change a policy
- // from Unlocked to Locked, to set it to null, or to reduce its
- // RetainUntil attribute. It is not required for setting the ObjectRetention for
- // the first time nor for extending the RetainUntil time.
- func (o *ObjectHandle) OverrideUnlockedRetention(override bool) *ObjectHandle {
- o2 := *o
- o2.overrideRetention = &override
- return &o2
- }
- // NewWriter returns a storage Writer that writes to the GCS object
- // associated with this ObjectHandle.
- //
- // A new object will be created unless an object with this name already exists.
- // Otherwise any previous object with the same name will be replaced.
- // The object will not be available (and any previous object will remain)
- // until Close has been called.
- //
- // Attributes can be set on the object by modifying the returned Writer's
- // ObjectAttrs field before the first call to Write. If no ContentType
- // attribute is specified, the content type will be automatically sniffed
- // using net/http.DetectContentType.
- //
- // Note that each Writer allocates an internal buffer of size Writer.ChunkSize.
- // See the ChunkSize docs for more information.
- //
- // It is the caller's responsibility to call Close when writing is done. To
- // stop writing without saving the data, cancel the context.
- func (o *ObjectHandle) NewWriter(ctx context.Context) *Writer {
- ctx = trace.StartSpan(ctx, "cloud.google.com/go/storage.Object.Writer")
- return &Writer{
- ctx: ctx,
- o: o,
- donec: make(chan struct{}),
- ObjectAttrs: ObjectAttrs{Name: o.object},
- ChunkSize: googleapi.DefaultUploadChunkSize,
- }
- }
- func (o *ObjectHandle) validate() error {
- if o.bucket == "" {
- return errors.New("storage: bucket name is empty")
- }
- if o.object == "" {
- return errors.New("storage: object name is empty")
- }
- if !utf8.ValidString(o.object) {
- return fmt.Errorf("storage: object name %q is not valid UTF-8", o.object)
- }
- // Names . and .. are not valid; see https://cloud.google.com/storage/docs/objects#naming
- if o.object == "." || o.object == ".." {
- return fmt.Errorf("storage: object name %q is not valid", o.object)
- }
- return nil
- }
- // parseKey converts the binary contents of a private key file to an
- // *rsa.PrivateKey. It detects whether the private key is in a PEM container or
- // not. If so, it extracts the private key from PEM container before
- // conversion. It only supports PEM containers with no passphrase.
- func parseKey(key []byte) (*rsa.PrivateKey, error) {
- if block, _ := pem.Decode(key); block != nil {
- key = block.Bytes
- }
- parsedKey, err := x509.ParsePKCS8PrivateKey(key)
- if err != nil {
- parsedKey, err = x509.ParsePKCS1PrivateKey(key)
- if err != nil {
- return nil, err
- }
- }
- parsed, ok := parsedKey.(*rsa.PrivateKey)
- if !ok {
- return nil, errors.New("oauth2: private key is invalid")
- }
- return parsed, nil
- }
- // toRawObject copies the editable attributes from o to the raw library's Object type.
- func (o *ObjectAttrs) toRawObject(bucket string) *raw.Object {
- var ret string
- if !o.RetentionExpirationTime.IsZero() {
- ret = o.RetentionExpirationTime.Format(time.RFC3339)
- }
- var ct string
- if !o.CustomTime.IsZero() {
- ct = o.CustomTime.Format(time.RFC3339)
- }
- return &raw.Object{
- Bucket: bucket,
- Name: o.Name,
- EventBasedHold: o.EventBasedHold,
- TemporaryHold: o.TemporaryHold,
- RetentionExpirationTime: ret,
- ContentType: o.ContentType,
- ContentEncoding: o.ContentEncoding,
- ContentLanguage: o.ContentLanguage,
- CacheControl: o.CacheControl,
- ContentDisposition: o.ContentDisposition,
- StorageClass: o.StorageClass,
- Acl: toRawObjectACL(o.ACL),
- Metadata: o.Metadata,
- CustomTime: ct,
- Retention: o.Retention.toRawObjectRetention(),
- }
- }
- // toProtoObject copies the editable attributes from o to the proto library's Object type.
- func (o *ObjectAttrs) toProtoObject(b string) *storagepb.Object {
- // For now, there are only globally unique buckets, and "_" is the alias
- // project ID for such buckets. If the bucket is not provided, like in the
- // destination ObjectAttrs of a Copy, do not attempt to format it.
- if b != "" {
- b = bucketResourceName(globalProjectAlias, b)
- }
- return &storagepb.Object{
- Bucket: b,
- Name: o.Name,
- EventBasedHold: proto.Bool(o.EventBasedHold),
- TemporaryHold: o.TemporaryHold,
- ContentType: o.ContentType,
- ContentEncoding: o.ContentEncoding,
- ContentLanguage: o.ContentLanguage,
- CacheControl: o.CacheControl,
- ContentDisposition: o.ContentDisposition,
- StorageClass: o.StorageClass,
- Acl: toProtoObjectACL(o.ACL),
- Metadata: o.Metadata,
- CreateTime: toProtoTimestamp(o.Created),
- CustomTime: toProtoTimestamp(o.CustomTime),
- DeleteTime: toProtoTimestamp(o.Deleted),
- RetentionExpireTime: toProtoTimestamp(o.RetentionExpirationTime),
- UpdateTime: toProtoTimestamp(o.Updated),
- KmsKey: o.KMSKeyName,
- Generation: o.Generation,
- Size: o.Size,
- }
- }
- // toProtoObject copies the attributes to update from uattrs to the proto library's Object type.
- func (uattrs *ObjectAttrsToUpdate) toProtoObject(bucket, object string) *storagepb.Object {
- o := &storagepb.Object{
- Name: object,
- Bucket: bucket,
- }
- if uattrs == nil {
- return o
- }
- if uattrs.EventBasedHold != nil {
- o.EventBasedHold = proto.Bool(optional.ToBool(uattrs.EventBasedHold))
- }
- if uattrs.TemporaryHold != nil {
- o.TemporaryHold = optional.ToBool(uattrs.TemporaryHold)
- }
- if uattrs.ContentType != nil {
- o.ContentType = optional.ToString(uattrs.ContentType)
- }
- if uattrs.ContentLanguage != nil {
- o.ContentLanguage = optional.ToString(uattrs.ContentLanguage)
- }
- if uattrs.ContentEncoding != nil {
- o.ContentEncoding = optional.ToString(uattrs.ContentEncoding)
- }
- if uattrs.ContentDisposition != nil {
- o.ContentDisposition = optional.ToString(uattrs.ContentDisposition)
- }
- if uattrs.CacheControl != nil {
- o.CacheControl = optional.ToString(uattrs.CacheControl)
- }
- if !uattrs.CustomTime.IsZero() {
- o.CustomTime = toProtoTimestamp(uattrs.CustomTime)
- }
- if uattrs.ACL != nil {
- o.Acl = toProtoObjectACL(uattrs.ACL)
- }
- o.Metadata = uattrs.Metadata
- return o
- }
- // ObjectAttrs represents the metadata for a Google Cloud Storage (GCS) object.
- type ObjectAttrs struct {
- // Bucket is the name of the bucket containing this GCS object.
- // This field is read-only.
- Bucket string
- // Name is the name of the object within the bucket.
- // This field is read-only.
- Name string
- // ContentType is the MIME type of the object's content.
- ContentType string
- // ContentLanguage is the content language of the object's content.
- ContentLanguage string
- // CacheControl is the Cache-Control header to be sent in the response
- // headers when serving the object data.
- CacheControl string
- // EventBasedHold specifies whether an object is under event-based hold. New
- // objects created in a bucket whose DefaultEventBasedHold is set will
- // default to that value.
- EventBasedHold bool
- // TemporaryHold specifies whether an object is under temporary hold. While
- // this flag is set to true, the object is protected against deletion and
- // overwrites.
- TemporaryHold bool
- // RetentionExpirationTime is a server-determined value that specifies the
- // earliest time that the object's retention period expires.
- // This is a read-only field.
- RetentionExpirationTime time.Time
- // ACL is the list of access control rules for the object.
- ACL []ACLRule
- // If not empty, applies a predefined set of access controls. It should be set
- // only when writing, copying or composing an object. When copying or composing,
- // it acts as the destinationPredefinedAcl parameter.
- // PredefinedACL is always empty for ObjectAttrs returned from the service.
- // See https://cloud.google.com/storage/docs/json_api/v1/objects/insert
- // for valid values.
- PredefinedACL string
- // Owner is the owner of the object. This field is read-only.
- //
- // If non-zero, it is in the form of "user-<userId>".
- Owner string
- // Size is the length of the object's content. This field is read-only.
- Size int64
- // ContentEncoding is the encoding of the object's content.
- ContentEncoding string
- // ContentDisposition is the optional Content-Disposition header of the object
- // sent in the response headers.
- ContentDisposition string
- // MD5 is the MD5 hash of the object's content. This field is read-only,
- // except when used from a Writer. If set on a Writer, the uploaded
- // data is rejected if its MD5 hash does not match this field.
- MD5 []byte
- // CRC32C is the CRC32 checksum of the object's content using the Castagnoli93
- // polynomial. This field is read-only, except when used from a Writer or
- // Composer. In those cases, if the SendCRC32C field in the Writer or Composer
- // is set to is true, the uploaded data is rejected if its CRC32C hash does
- // not match this field.
- //
- // Note: For a Writer, SendCRC32C must be set to true BEFORE the first call to
- // Writer.Write() in order to send the checksum.
- CRC32C uint32
- // MediaLink is an URL to the object's content. This field is read-only.
- MediaLink string
- // Metadata represents user-provided metadata, in key/value pairs.
- // It can be nil if no metadata is provided.
- //
- // For object downloads using Reader, metadata keys are sent as headers.
- // Therefore, avoid setting metadata keys using characters that are not valid
- // for headers. See https://www.rfc-editor.org/rfc/rfc7230#section-3.2.6.
- Metadata map[string]string
- // Generation is the generation number of the object's content.
- // This field is read-only.
- Generation int64
- // Metageneration is the version of the metadata for this
- // object at this generation. This field is used for preconditions
- // and for detecting changes in metadata. A metageneration number
- // is only meaningful in the context of a particular generation
- // of a particular object. This field is read-only.
- Metageneration int64
- // StorageClass is the storage class of the object. This defines
- // how objects are stored and determines the SLA and the cost of storage.
- // Typical values are "STANDARD", "NEARLINE", "COLDLINE" and "ARCHIVE".
- // Defaults to "STANDARD".
- // See https://cloud.google.com/storage/docs/storage-classes for all
- // valid values.
- StorageClass string
- // Created is the time the object was created. This field is read-only.
- Created time.Time
- // Deleted is the time the object was deleted.
- // If not deleted, it is the zero value. This field is read-only.
- Deleted time.Time
- // Updated is the creation or modification time of the object.
- // For buckets with versioning enabled, changing an object's
- // metadata does not change this property. This field is read-only.
- Updated time.Time
- // CustomerKeySHA256 is the base64-encoded SHA-256 hash of the
- // customer-supplied encryption key for the object. It is empty if there is
- // no customer-supplied encryption key.
- // See // https://cloud.google.com/storage/docs/encryption for more about
- // encryption in Google Cloud Storage.
- CustomerKeySHA256 string
- // Cloud KMS key name, in the form
- // projects/P/locations/L/keyRings/R/cryptoKeys/K, used to encrypt this object,
- // if the object is encrypted by such a key.
- //
- // Providing both a KMSKeyName and a customer-supplied encryption key (via
- // ObjectHandle.Key) will result in an error when writing an object.
- KMSKeyName string
- // Prefix is set only for ObjectAttrs which represent synthetic "directory
- // entries" when iterating over buckets using Query.Delimiter. See
- // ObjectIterator.Next. When set, no other fields in ObjectAttrs will be
- // populated.
- Prefix string
- // Etag is the HTTP/1.1 Entity tag for the object.
- // This field is read-only.
- Etag string
- // A user-specified timestamp which can be applied to an object. This is
- // typically set in order to use the CustomTimeBefore and DaysSinceCustomTime
- // LifecycleConditions to manage object lifecycles.
- //
- // CustomTime cannot be removed once set on an object. It can be updated to a
- // later value but not to an earlier one. For more information see
- // https://cloud.google.com/storage/docs/metadata#custom-time .
- CustomTime time.Time
- // ComponentCount is the number of objects contained within a composite object.
- // For non-composite objects, the value will be zero.
- // This field is read-only.
- ComponentCount int64
- // Retention contains the retention configuration for this object.
- // ObjectRetention cannot be configured or reported through the gRPC API.
- Retention *ObjectRetention
- }
- // ObjectRetention contains the retention configuration for this object.
- type ObjectRetention struct {
- // Mode is the retention policy's mode on this object. Valid values are
- // "Locked" and "Unlocked".
- // Locked retention policies cannot be changed. Unlocked policies require an
- // override to change.
- Mode string
- // RetainUntil is the time this object will be retained until.
- RetainUntil time.Time
- }
- func (r *ObjectRetention) toRawObjectRetention() *raw.ObjectRetention {
- if r == nil {
- return nil
- }
- return &raw.ObjectRetention{
- Mode: r.Mode,
- RetainUntilTime: r.RetainUntil.Format(time.RFC3339),
- }
- }
- func toObjectRetention(r *raw.ObjectRetention) *ObjectRetention {
- if r == nil {
- return nil
- }
- return &ObjectRetention{
- Mode: r.Mode,
- RetainUntil: convertTime(r.RetainUntilTime),
- }
- }
- // convertTime converts a time in RFC3339 format to time.Time.
- // If any error occurs in parsing, the zero-value time.Time is silently returned.
- func convertTime(t string) time.Time {
- var r time.Time
- if t != "" {
- r, _ = time.Parse(time.RFC3339, t)
- }
- return r
- }
- func convertProtoTime(t *timestamppb.Timestamp) time.Time {
- var r time.Time
- if t != nil {
- r = t.AsTime()
- }
- return r
- }
- func toProtoTimestamp(t time.Time) *timestamppb.Timestamp {
- if t.IsZero() {
- return nil
- }
- return timestamppb.New(t)
- }
- func newObject(o *raw.Object) *ObjectAttrs {
- if o == nil {
- return nil
- }
- owner := ""
- if o.Owner != nil {
- owner = o.Owner.Entity
- }
- md5, _ := base64.StdEncoding.DecodeString(o.Md5Hash)
- crc32c, _ := decodeUint32(o.Crc32c)
- var sha256 string
- if o.CustomerEncryption != nil {
- sha256 = o.CustomerEncryption.KeySha256
- }
- return &ObjectAttrs{
- Bucket: o.Bucket,
- Name: o.Name,
- ContentType: o.ContentType,
- ContentLanguage: o.ContentLanguage,
- CacheControl: o.CacheControl,
- EventBasedHold: o.EventBasedHold,
- TemporaryHold: o.TemporaryHold,
- RetentionExpirationTime: convertTime(o.RetentionExpirationTime),
- ACL: toObjectACLRules(o.Acl),
- Owner: owner,
- ContentEncoding: o.ContentEncoding,
- ContentDisposition: o.ContentDisposition,
- Size: int64(o.Size),
- MD5: md5,
- CRC32C: crc32c,
- MediaLink: o.MediaLink,
- Metadata: o.Metadata,
- Generation: o.Generation,
- Metageneration: o.Metageneration,
- StorageClass: o.StorageClass,
- CustomerKeySHA256: sha256,
- KMSKeyName: o.KmsKeyName,
- Created: convertTime(o.TimeCreated),
- Deleted: convertTime(o.TimeDeleted),
- Updated: convertTime(o.Updated),
- Etag: o.Etag,
- CustomTime: convertTime(o.CustomTime),
- ComponentCount: o.ComponentCount,
- Retention: toObjectRetention(o.Retention),
- }
- }
- func newObjectFromProto(o *storagepb.Object) *ObjectAttrs {
- if o == nil {
- return nil
- }
- return &ObjectAttrs{
- Bucket: parseBucketName(o.Bucket),
- Name: o.Name,
- ContentType: o.ContentType,
- ContentLanguage: o.ContentLanguage,
- CacheControl: o.CacheControl,
- EventBasedHold: o.GetEventBasedHold(),
- TemporaryHold: o.TemporaryHold,
- RetentionExpirationTime: convertProtoTime(o.GetRetentionExpireTime()),
- ACL: toObjectACLRulesFromProto(o.GetAcl()),
- Owner: o.GetOwner().GetEntity(),
- ContentEncoding: o.ContentEncoding,
- ContentDisposition: o.ContentDisposition,
- Size: int64(o.Size),
- MD5: o.GetChecksums().GetMd5Hash(),
- CRC32C: o.GetChecksums().GetCrc32C(),
- Metadata: o.Metadata,
- Generation: o.Generation,
- Metageneration: o.Metageneration,
- StorageClass: o.StorageClass,
- // CustomerKeySHA256 needs to be presented as base64 encoded, but the response from gRPC is not.
- CustomerKeySHA256: base64.StdEncoding.EncodeToString(o.GetCustomerEncryption().GetKeySha256Bytes()),
- KMSKeyName: o.GetKmsKey(),
- Created: convertProtoTime(o.GetCreateTime()),
- Deleted: convertProtoTime(o.GetDeleteTime()),
- Updated: convertProtoTime(o.GetUpdateTime()),
- CustomTime: convertProtoTime(o.GetCustomTime()),
- ComponentCount: int64(o.ComponentCount),
- }
- }
- // Decode a uint32 encoded in Base64 in big-endian byte order.
- func decodeUint32(b64 string) (uint32, error) {
- d, err := base64.StdEncoding.DecodeString(b64)
- if err != nil {
- return 0, err
- }
- if len(d) != 4 {
- return 0, fmt.Errorf("storage: %q does not encode a 32-bit value", d)
- }
- return uint32(d[0])<<24 + uint32(d[1])<<16 + uint32(d[2])<<8 + uint32(d[3]), nil
- }
- // Encode a uint32 as Base64 in big-endian byte order.
- func encodeUint32(u uint32) string {
- b := []byte{byte(u >> 24), byte(u >> 16), byte(u >> 8), byte(u)}
- return base64.StdEncoding.EncodeToString(b)
- }
- // Projection is enumerated type for Query.Projection.
- type Projection int
- const (
- // ProjectionDefault returns all fields of objects.
- ProjectionDefault Projection = iota
- // ProjectionFull returns all fields of objects.
- ProjectionFull
- // ProjectionNoACL returns all fields of objects except for Owner and ACL.
- ProjectionNoACL
- )
- func (p Projection) String() string {
- switch p {
- case ProjectionFull:
- return "full"
- case ProjectionNoACL:
- return "noAcl"
- default:
- return ""
- }
- }
- // Query represents a query to filter objects from a bucket.
- type Query struct {
- // Delimiter returns results in a directory-like fashion.
- // Results will contain only objects whose names, aside from the
- // prefix, do not contain delimiter. Objects whose names,
- // aside from the prefix, contain delimiter will have their name,
- // truncated after the delimiter, returned in prefixes.
- // Duplicate prefixes are omitted.
- // Must be set to / when used with the MatchGlob parameter to filter results
- // in a directory-like mode.
- // Optional.
- Delimiter string
- // Prefix is the prefix filter to query objects
- // whose names begin with this prefix.
- // Optional.
- Prefix string
- // Versions indicates whether multiple versions of the same
- // object will be included in the results.
- Versions bool
- // attrSelection is used to select only specific fields to be returned by
- // the query. It is set by the user calling SetAttrSelection. These
- // are used by toFieldMask and toFieldSelection for gRPC and HTTP/JSON
- // clients respectively.
- attrSelection []string
- // StartOffset is used to filter results to objects whose names are
- // lexicographically equal to or after startOffset. If endOffset is also set,
- // the objects listed will have names between startOffset (inclusive) and
- // endOffset (exclusive).
- StartOffset string
- // EndOffset is used to filter results to objects whose names are
- // lexicographically before endOffset. If startOffset is also set, the objects
- // listed will have names between startOffset (inclusive) and endOffset (exclusive).
- EndOffset string
- // Projection defines the set of properties to return. It will default to ProjectionFull,
- // which returns all properties. Passing ProjectionNoACL will omit Owner and ACL,
- // which may improve performance when listing many objects.
- Projection Projection
- // IncludeTrailingDelimiter controls how objects which end in a single
- // instance of Delimiter (for example, if Query.Delimiter = "/" and the
- // object name is "foo/bar/") are included in the results. By default, these
- // objects only show up as prefixes. If IncludeTrailingDelimiter is set to
- // true, they will also be included as objects and their metadata will be
- // populated in the returned ObjectAttrs.
- IncludeTrailingDelimiter bool
- // MatchGlob is a glob pattern used to filter results (for example, foo*bar). See
- // https://cloud.google.com/storage/docs/json_api/v1/objects/list#list-object-glob
- // for syntax details. When Delimiter is set in conjunction with MatchGlob,
- // it must be set to /.
- MatchGlob string
- // IncludeFoldersAsPrefixes includes Folders and Managed Folders in the set of
- // prefixes returned by the query. Only applicable if Delimiter is set to /.
- // IncludeFoldersAsPrefixes is not yet implemented in the gRPC API.
- IncludeFoldersAsPrefixes bool
- }
- // attrToFieldMap maps the field names of ObjectAttrs to the underlying field
- // names in the API call. Only the ObjectAttrs field names are visible to users
- // because they are already part of the public API of the package.
- var attrToFieldMap = map[string]string{
- "Bucket": "bucket",
- "Name": "name",
- "ContentType": "contentType",
- "ContentLanguage": "contentLanguage",
- "CacheControl": "cacheControl",
- "EventBasedHold": "eventBasedHold",
- "TemporaryHold": "temporaryHold",
- "RetentionExpirationTime": "retentionExpirationTime",
- "ACL": "acl",
- "Owner": "owner",
- "ContentEncoding": "contentEncoding",
- "ContentDisposition": "contentDisposition",
- "Size": "size",
- "MD5": "md5Hash",
- "CRC32C": "crc32c",
- "MediaLink": "mediaLink",
- "Metadata": "metadata",
- "Generation": "generation",
- "Metageneration": "metageneration",
- "StorageClass": "storageClass",
- "CustomerKeySHA256": "customerEncryption",
- "KMSKeyName": "kmsKeyName",
- "Created": "timeCreated",
- "Deleted": "timeDeleted",
- "Updated": "updated",
- "Etag": "etag",
- "CustomTime": "customTime",
- "ComponentCount": "componentCount",
- "Retention": "retention",
- }
- // attrToProtoFieldMap maps the field names of ObjectAttrs to the underlying field
- // names in the protobuf Object message.
- var attrToProtoFieldMap = map[string]string{
- "Name": "name",
- "Bucket": "bucket",
- "Etag": "etag",
- "Generation": "generation",
- "Metageneration": "metageneration",
- "StorageClass": "storage_class",
- "Size": "size",
- "ContentEncoding": "content_encoding",
- "ContentDisposition": "content_disposition",
- "CacheControl": "cache_control",
- "ACL": "acl",
- "ContentLanguage": "content_language",
- "Deleted": "delete_time",
- "ContentType": "content_type",
- "Created": "create_time",
- "CRC32C": "checksums.crc32c",
- "MD5": "checksums.md5_hash",
- "Updated": "update_time",
- "KMSKeyName": "kms_key",
- "TemporaryHold": "temporary_hold",
- "RetentionExpirationTime": "retention_expire_time",
- "Metadata": "metadata",
- "EventBasedHold": "event_based_hold",
- "Owner": "owner",
- "CustomerKeySHA256": "customer_encryption",
- "CustomTime": "custom_time",
- "ComponentCount": "component_count",
- // MediaLink was explicitly excluded from the proto as it is an HTTP-ism.
- // "MediaLink": "mediaLink",
- // TODO: add object retention - b/308194853
- }
- // SetAttrSelection makes the query populate only specific attributes of
- // objects. When iterating over objects, if you only need each object's name
- // and size, pass []string{"Name", "Size"} to this method. Only these fields
- // will be fetched for each object across the network; the other fields of
- // ObjectAttr will remain at their default values. This is a performance
- // optimization; for more information, see
- // https://cloud.google.com/storage/docs/json_api/v1/how-tos/performance
- func (q *Query) SetAttrSelection(attrs []string) error {
- // Validate selections.
- for _, attr := range attrs {
- // If the attr is acceptable for one of the two sets, then it is OK.
- // If it is not acceptable for either, then return an error.
- // The respective masking implementations ignore unknown attrs which
- // makes switching between transports a little easier.
- _, okJSON := attrToFieldMap[attr]
- _, okGRPC := attrToProtoFieldMap[attr]
- if !okJSON && !okGRPC {
- return fmt.Errorf("storage: attr %v is not valid", attr)
- }
- }
- q.attrSelection = attrs
- return nil
- }
- func (q *Query) toFieldSelection() string {
- if q == nil || len(q.attrSelection) == 0 {
- return ""
- }
- fieldSet := make(map[string]bool)
- for _, attr := range q.attrSelection {
- field, ok := attrToFieldMap[attr]
- if !ok {
- // Future proofing, skip unknown fields, let SetAttrSelection handle
- // error modes.
- continue
- }
- fieldSet[field] = true
- }
- var s string
- if len(fieldSet) > 0 {
- var b bytes.Buffer
- b.WriteString("prefixes,items(")
- first := true
- for field := range fieldSet {
- if !first {
- b.WriteString(",")
- }
- first = false
- b.WriteString(field)
- }
- b.WriteString(")")
- s = b.String()
- }
- return s
- }
- func (q *Query) toFieldMask() *fieldmaskpb.FieldMask {
- // The default behavior with no Query is ProjectionDefault (i.e. ProjectionFull).
- if q == nil {
- return &fieldmaskpb.FieldMask{Paths: []string{"*"}}
- }
- // User selected attributes via q.SetAttrSeleciton. This takes precedence
- // over the Projection.
- if numSelected := len(q.attrSelection); numSelected > 0 {
- protoFieldPaths := make([]string, 0, numSelected)
- for _, attr := range q.attrSelection {
- pf, ok := attrToProtoFieldMap[attr]
- if !ok {
- // Future proofing, skip unknown fields, let SetAttrSelection
- // handle error modes.
- continue
- }
- protoFieldPaths = append(protoFieldPaths, pf)
- }
- return &fieldmaskpb.FieldMask{Paths: protoFieldPaths}
- }
- // ProjectDefault == ProjectionFull which means all fields.
- fm := &fieldmaskpb.FieldMask{Paths: []string{"*"}}
- if q.Projection == ProjectionNoACL {
- paths := make([]string, 0, len(attrToProtoFieldMap)-2) // omitting two fields
- for _, f := range attrToProtoFieldMap {
- // Skip the acl and owner fields for "NoACL".
- if f == "acl" || f == "owner" {
- continue
- }
- paths = append(paths, f)
- }
- fm.Paths = paths
- }
- return fm
- }
- // Conditions constrain methods to act on specific generations of
- // objects.
- //
- // The zero value is an empty set of constraints. Not all conditions or
- // combinations of conditions are applicable to all methods.
- // See https://cloud.google.com/storage/docs/generations-preconditions
- // for details on how these operate.
- type Conditions struct {
- // Generation constraints.
- // At most one of the following can be set to a non-zero value.
- // GenerationMatch specifies that the object must have the given generation
- // for the operation to occur.
- // If GenerationMatch is zero, it has no effect.
- // Use DoesNotExist to specify that the object does not exist in the bucket.
- GenerationMatch int64
- // GenerationNotMatch specifies that the object must not have the given
- // generation for the operation to occur.
- // If GenerationNotMatch is zero, it has no effect.
- // This condition only works for object reads if the WithJSONReads client
- // option is set.
- GenerationNotMatch int64
- // DoesNotExist specifies that the object must not exist in the bucket for
- // the operation to occur.
- // If DoesNotExist is false, it has no effect.
- DoesNotExist bool
- // Metadata generation constraints.
- // At most one of the following can be set to a non-zero value.
- // MetagenerationMatch specifies that the object must have the given
- // metageneration for the operation to occur.
- // If MetagenerationMatch is zero, it has no effect.
- MetagenerationMatch int64
- // MetagenerationNotMatch specifies that the object must not have the given
- // metageneration for the operation to occur.
- // If MetagenerationNotMatch is zero, it has no effect.
- // This condition only works for object reads if the WithJSONReads client
- // option is set.
- MetagenerationNotMatch int64
- }
- func (c *Conditions) validate(method string) error {
- if *c == (Conditions{}) {
- return fmt.Errorf("storage: %s: empty conditions", method)
- }
- if !c.isGenerationValid() {
- return fmt.Errorf("storage: %s: multiple conditions specified for generation", method)
- }
- if !c.isMetagenerationValid() {
- return fmt.Errorf("storage: %s: multiple conditions specified for metageneration", method)
- }
- return nil
- }
- func (c *Conditions) isGenerationValid() bool {
- n := 0
- if c.GenerationMatch != 0 {
- n++
- }
- if c.GenerationNotMatch != 0 {
- n++
- }
- if c.DoesNotExist {
- n++
- }
- return n <= 1
- }
- func (c *Conditions) isMetagenerationValid() bool {
- return c.MetagenerationMatch == 0 || c.MetagenerationNotMatch == 0
- }
- // applyConds modifies the provided call using the conditions in conds.
- // call is something that quacks like a *raw.WhateverCall.
- func applyConds(method string, gen int64, conds *Conditions, call interface{}) error {
- cval := reflect.ValueOf(call)
- if gen >= 0 {
- if !setGeneration(cval, gen) {
- return fmt.Errorf("storage: %s: generation not supported", method)
- }
- }
- if conds == nil {
- return nil
- }
- if err := conds.validate(method); err != nil {
- return err
- }
- switch {
- case conds.GenerationMatch != 0:
- if !setIfGenerationMatch(cval, conds.GenerationMatch) {
- return fmt.Errorf("storage: %s: ifGenerationMatch not supported", method)
- }
- case conds.GenerationNotMatch != 0:
- if !setIfGenerationNotMatch(cval, conds.GenerationNotMatch) {
- return fmt.Errorf("storage: %s: ifGenerationNotMatch not supported", method)
- }
- case conds.DoesNotExist:
- if !setIfGenerationMatch(cval, int64(0)) {
- return fmt.Errorf("storage: %s: DoesNotExist not supported", method)
- }
- }
- switch {
- case conds.MetagenerationMatch != 0:
- if !setIfMetagenerationMatch(cval, conds.MetagenerationMatch) {
- return fmt.Errorf("storage: %s: ifMetagenerationMatch not supported", method)
- }
- case conds.MetagenerationNotMatch != 0:
- if !setIfMetagenerationNotMatch(cval, conds.MetagenerationNotMatch) {
- return fmt.Errorf("storage: %s: ifMetagenerationNotMatch not supported", method)
- }
- }
- return nil
- }
- func applySourceConds(gen int64, conds *Conditions, call *raw.ObjectsRewriteCall) error {
- if gen >= 0 {
- call.SourceGeneration(gen)
- }
- if conds == nil {
- return nil
- }
- if err := conds.validate("CopyTo source"); err != nil {
- return err
- }
- switch {
- case conds.GenerationMatch != 0:
- call.IfSourceGenerationMatch(conds.GenerationMatch)
- case conds.GenerationNotMatch != 0:
- call.IfSourceGenerationNotMatch(conds.GenerationNotMatch)
- case conds.DoesNotExist:
- call.IfSourceGenerationMatch(0)
- }
- switch {
- case conds.MetagenerationMatch != 0:
- call.IfSourceMetagenerationMatch(conds.MetagenerationMatch)
- case conds.MetagenerationNotMatch != 0:
- call.IfSourceMetagenerationNotMatch(conds.MetagenerationNotMatch)
- }
- return nil
- }
- func applySourceCondsProto(gen int64, conds *Conditions, call *storagepb.RewriteObjectRequest) error {
- if gen >= 0 {
- call.SourceGeneration = gen
- }
- if conds == nil {
- return nil
- }
- if err := conds.validate("CopyTo source"); err != nil {
- return err
- }
- switch {
- case conds.GenerationMatch != 0:
- call.IfSourceGenerationMatch = proto.Int64(conds.GenerationMatch)
- case conds.GenerationNotMatch != 0:
- call.IfSourceGenerationNotMatch = proto.Int64(conds.GenerationNotMatch)
- case conds.DoesNotExist:
- call.IfSourceGenerationMatch = proto.Int64(0)
- }
- switch {
- case conds.MetagenerationMatch != 0:
- call.IfSourceMetagenerationMatch = proto.Int64(conds.MetagenerationMatch)
- case conds.MetagenerationNotMatch != 0:
- call.IfSourceMetagenerationNotMatch = proto.Int64(conds.MetagenerationNotMatch)
- }
- return nil
- }
- // setGeneration sets Generation on a *raw.WhateverCall.
- // We can't use anonymous interfaces because the return type is
- // different, since the field setters are builders.
- // We also make sure to supply a compile-time constant to MethodByName;
- // otherwise, the Go Linker will disable dead code elimination, leading
- // to larger binaries for all packages that import storage.
- func setGeneration(cval reflect.Value, value interface{}) bool {
- return setCondition(cval.MethodByName("Generation"), value)
- }
- // setIfGenerationMatch sets IfGenerationMatch on a *raw.WhateverCall.
- // See also setGeneration.
- func setIfGenerationMatch(cval reflect.Value, value interface{}) bool {
- return setCondition(cval.MethodByName("IfGenerationMatch"), value)
- }
- // setIfGenerationNotMatch sets IfGenerationNotMatch on a *raw.WhateverCall.
- // See also setGeneration.
- func setIfGenerationNotMatch(cval reflect.Value, value interface{}) bool {
- return setCondition(cval.MethodByName("IfGenerationNotMatch"), value)
- }
- // setIfMetagenerationMatch sets IfMetagenerationMatch on a *raw.WhateverCall.
- // See also setGeneration.
- func setIfMetagenerationMatch(cval reflect.Value, value interface{}) bool {
- return setCondition(cval.MethodByName("IfMetagenerationMatch"), value)
- }
- // setIfMetagenerationNotMatch sets IfMetagenerationNotMatch on a *raw.WhateverCall.
- // See also setGeneration.
- func setIfMetagenerationNotMatch(cval reflect.Value, value interface{}) bool {
- return setCondition(cval.MethodByName("IfMetagenerationNotMatch"), value)
- }
- func setCondition(setter reflect.Value, value interface{}) bool {
- if setter.IsValid() {
- setter.Call([]reflect.Value{reflect.ValueOf(value)})
- }
- return setter.IsValid()
- }
- // Retryer returns an object handle that is configured with custom retry
- // behavior as specified by the options that are passed to it. All operations
- // on the new handle will use the customized retry configuration.
- // These retry options will merge with the bucket's retryer (if set) for the
- // returned handle. Options passed into this method will take precedence over
- // retry options on the bucket and client. Note that you must explicitly pass in
- // each option you want to override.
- func (o *ObjectHandle) Retryer(opts ...RetryOption) *ObjectHandle {
- o2 := *o
- var retry *retryConfig
- if o.retry != nil {
- // merge the options with the existing retry
- retry = o.retry
- } else {
- retry = &retryConfig{}
- }
- for _, opt := range opts {
- opt.apply(retry)
- }
- o2.retry = retry
- o2.acl.retry = retry
- return &o2
- }
- // SetRetry configures the client with custom retry behavior as specified by the
- // options that are passed to it. All operations using this client will use the
- // customized retry configuration.
- // This should be called once before using the client for network operations, as
- // there could be indeterminate behaviour with operations in progress.
- // Retry options set on a bucket or object handle will take precedence over
- // these options.
- func (c *Client) SetRetry(opts ...RetryOption) {
- var retry *retryConfig
- if c.retry != nil {
- // merge the options with the existing retry
- retry = c.retry
- } else {
- retry = &retryConfig{}
- }
- for _, opt := range opts {
- opt.apply(retry)
- }
- c.retry = retry
- }
- // RetryOption allows users to configure non-default retry behavior for API
- // calls made to GCS.
- type RetryOption interface {
- apply(config *retryConfig)
- }
- // WithBackoff allows configuration of the backoff timing used for retries.
- // Available configuration options (Initial, Max and Multiplier) are described
- // at https://pkg.go.dev/github.com/googleapis/gax-go/v2#Backoff. If any fields
- // are not supplied by the user, gax default values will be used.
- func WithBackoff(backoff gax.Backoff) RetryOption {
- return &withBackoff{
- backoff: backoff,
- }
- }
- type withBackoff struct {
- backoff gax.Backoff
- }
- func (wb *withBackoff) apply(config *retryConfig) {
- config.backoff = &wb.backoff
- }
- // WithMaxAttempts configures the maximum number of times an API call can be made
- // in the case of retryable errors.
- // For example, if you set WithMaxAttempts(5), the operation will be attempted up to 5
- // times total (initial call plus 4 retries).
- // Without this setting, operations will continue retrying indefinitely
- // until either the context is canceled or a deadline is reached.
- func WithMaxAttempts(maxAttempts int) RetryOption {
- return &withMaxAttempts{
- maxAttempts: maxAttempts,
- }
- }
- type withMaxAttempts struct {
- maxAttempts int
- }
- func (wb *withMaxAttempts) apply(config *retryConfig) {
- config.maxAttempts = &wb.maxAttempts
- }
- // RetryPolicy describes the available policies for which operations should be
- // retried. The default is `RetryIdempotent`.
- type RetryPolicy int
- const (
- // RetryIdempotent causes only idempotent operations to be retried when the
- // service returns a transient error. Using this policy, fully idempotent
- // operations (such as `ObjectHandle.Attrs()`) will always be retried.
- // Conditionally idempotent operations (for example `ObjectHandle.Update()`)
- // will be retried only if the necessary conditions have been supplied (in
- // the case of `ObjectHandle.Update()` this would mean supplying a
- // `Conditions.MetagenerationMatch` condition is required).
- RetryIdempotent RetryPolicy = iota
- // RetryAlways causes all operations to be retried when the service returns a
- // transient error, regardless of idempotency considerations.
- RetryAlways
- // RetryNever causes the client to not perform retries on failed operations.
- RetryNever
- )
- // WithPolicy allows the configuration of which operations should be performed
- // with retries for transient errors.
- func WithPolicy(policy RetryPolicy) RetryOption {
- return &withPolicy{
- policy: policy,
- }
- }
- type withPolicy struct {
- policy RetryPolicy
- }
- func (ws *withPolicy) apply(config *retryConfig) {
- config.policy = ws.policy
- }
- // WithErrorFunc allows users to pass a custom function to the retryer. Errors
- // will be retried if and only if `shouldRetry(err)` returns true.
- // By default, the following errors are retried (see ShouldRetry for the default
- // function):
- //
- // - HTTP responses with codes 408, 429, 502, 503, and 504.
- //
- // - Transient network errors such as connection reset and io.ErrUnexpectedEOF.
- //
- // - Errors which are considered transient using the Temporary() interface.
- //
- // - Wrapped versions of these errors.
- //
- // This option can be used to retry on a different set of errors than the
- // default. Users can use the default ShouldRetry function inside their custom
- // function if they only want to make minor modifications to default behavior.
- func WithErrorFunc(shouldRetry func(err error) bool) RetryOption {
- return &withErrorFunc{
- shouldRetry: shouldRetry,
- }
- }
- type withErrorFunc struct {
- shouldRetry func(err error) bool
- }
- func (wef *withErrorFunc) apply(config *retryConfig) {
- config.shouldRetry = wef.shouldRetry
- }
- type retryConfig struct {
- backoff *gax.Backoff
- policy RetryPolicy
- shouldRetry func(err error) bool
- maxAttempts *int
- }
- func (r *retryConfig) clone() *retryConfig {
- if r == nil {
- return nil
- }
- var bo *gax.Backoff
- if r.backoff != nil {
- bo = &gax.Backoff{
- Initial: r.backoff.Initial,
- Max: r.backoff.Max,
- Multiplier: r.backoff.Multiplier,
- }
- }
- return &retryConfig{
- backoff: bo,
- policy: r.policy,
- shouldRetry: r.shouldRetry,
- maxAttempts: r.maxAttempts,
- }
- }
- // composeSourceObj wraps a *raw.ComposeRequestSourceObjects, but adds the methods
- // that modifyCall searches for by name.
- type composeSourceObj struct {
- src *raw.ComposeRequestSourceObjects
- }
- func (c composeSourceObj) Generation(gen int64) {
- c.src.Generation = gen
- }
- func (c composeSourceObj) IfGenerationMatch(gen int64) {
- // It's safe to overwrite ObjectPreconditions, since its only field is
- // IfGenerationMatch.
- c.src.ObjectPreconditions = &raw.ComposeRequestSourceObjectsObjectPreconditions{
- IfGenerationMatch: gen,
- }
- }
- func setEncryptionHeaders(headers http.Header, key []byte, copySource bool) error {
- if key == nil {
- return nil
- }
- // TODO(jbd): Ask the API team to return a more user-friendly error
- // and avoid doing this check at the client level.
- if len(key) != 32 {
- return errors.New("storage: not a 32-byte AES-256 key")
- }
- var cs string
- if copySource {
- cs = "copy-source-"
- }
- headers.Set("x-goog-"+cs+"encryption-algorithm", aes256Algorithm)
- headers.Set("x-goog-"+cs+"encryption-key", base64.StdEncoding.EncodeToString(key))
- keyHash := sha256.Sum256(key)
- headers.Set("x-goog-"+cs+"encryption-key-sha256", base64.StdEncoding.EncodeToString(keyHash[:]))
- return nil
- }
- // toProtoCommonObjectRequestParams sets customer-supplied encryption to the proto library's CommonObjectRequestParams.
- func toProtoCommonObjectRequestParams(key []byte) *storagepb.CommonObjectRequestParams {
- if key == nil {
- return nil
- }
- keyHash := sha256.Sum256(key)
- return &storagepb.CommonObjectRequestParams{
- EncryptionAlgorithm: aes256Algorithm,
- EncryptionKeyBytes: key,
- EncryptionKeySha256Bytes: keyHash[:],
- }
- }
- func toProtoChecksums(sendCRC32C bool, attrs *ObjectAttrs) *storagepb.ObjectChecksums {
- var checksums *storagepb.ObjectChecksums
- if sendCRC32C {
- checksums = &storagepb.ObjectChecksums{
- Crc32C: proto.Uint32(attrs.CRC32C),
- }
- }
- if len(attrs.MD5) != 0 {
- if checksums == nil {
- checksums = &storagepb.ObjectChecksums{
- Md5Hash: attrs.MD5,
- }
- } else {
- checksums.Md5Hash = attrs.MD5
- }
- }
- return checksums
- }
- // ServiceAccount fetches the email address of the given project's Google Cloud Storage service account.
- func (c *Client) ServiceAccount(ctx context.Context, projectID string) (string, error) {
- o := makeStorageOpts(true, c.retry, "")
- return c.tc.GetServiceAccount(ctx, projectID, o...)
- }
- // bucketResourceName formats the given project ID and bucketResourceName ID
- // into a Bucket resource name. This is the format necessary for the gRPC API as
- // it conforms to the Resource-oriented design practices in https://google.aip.dev/121.
- func bucketResourceName(p, b string) string {
- return fmt.Sprintf("projects/%s/buckets/%s", p, b)
- }
- // parseBucketName strips the leading resource path segment and returns the
- // bucket ID, which is the simple Bucket name typical of the v1 API.
- func parseBucketName(b string) string {
- sep := strings.LastIndex(b, "/")
- return b[sep+1:]
- }
- // parseProjectNumber consume the given resource name and parses out the project
- // number if one is present i.e. it is not a project ID.
- func parseProjectNumber(r string) uint64 {
- projectID := regexp.MustCompile(`projects\/([0-9]+)\/?`)
- if matches := projectID.FindStringSubmatch(r); len(matches) > 0 {
- // Capture group follows the matched segment. For example:
- // input: projects/123/bars/456
- // output: [projects/123/, 123]
- number, err := strconv.ParseUint(matches[1], 10, 64)
- if err != nil {
- return 0
- }
- return number
- }
- return 0
- }
- // toProjectResource accepts a project ID and formats it as a Project resource
- // name.
- func toProjectResource(project string) string {
- return fmt.Sprintf("projects/%s", project)
- }
- // setConditionProtoField uses protobuf reflection to set named condition field
- // to the given condition value if supported on the protobuf message.
- func setConditionProtoField(m protoreflect.Message, f string, v int64) bool {
- fields := m.Descriptor().Fields()
- if rf := fields.ByName(protoreflect.Name(f)); rf != nil {
- m.Set(rf, protoreflect.ValueOfInt64(v))
- return true
- }
- return false
- }
- // applyCondsProto validates and attempts to set the conditions on a protobuf
- // message using protobuf reflection.
- func applyCondsProto(method string, gen int64, conds *Conditions, msg proto.Message) error {
- rmsg := msg.ProtoReflect()
- if gen >= 0 {
- if !setConditionProtoField(rmsg, "generation", gen) {
- return fmt.Errorf("storage: %s: generation not supported", method)
- }
- }
- if conds == nil {
- return nil
- }
- if err := conds.validate(method); err != nil {
- return err
- }
- switch {
- case conds.GenerationMatch != 0:
- if !setConditionProtoField(rmsg, "if_generation_match", conds.GenerationMatch) {
- return fmt.Errorf("storage: %s: ifGenerationMatch not supported", method)
- }
- case conds.GenerationNotMatch != 0:
- if !setConditionProtoField(rmsg, "if_generation_not_match", conds.GenerationNotMatch) {
- return fmt.Errorf("storage: %s: ifGenerationNotMatch not supported", method)
- }
- case conds.DoesNotExist:
- if !setConditionProtoField(rmsg, "if_generation_match", int64(0)) {
- return fmt.Errorf("storage: %s: DoesNotExist not supported", method)
- }
- }
- switch {
- case conds.MetagenerationMatch != 0:
- if !setConditionProtoField(rmsg, "if_metageneration_match", conds.MetagenerationMatch) {
- return fmt.Errorf("storage: %s: ifMetagenerationMatch not supported", method)
- }
- case conds.MetagenerationNotMatch != 0:
- if !setConditionProtoField(rmsg, "if_metageneration_not_match", conds.MetagenerationNotMatch) {
- return fmt.Errorf("storage: %s: ifMetagenerationNotMatch not supported", method)
- }
- }
- return nil
- }
|