]>
Commit | Line | Data |
---|---|---|
1 | // Copyright 2014 Google LLC | |
2 | // | |
3 | // Licensed under the Apache License, Version 2.0 (the "License"); | |
4 | // you may not use this file except in compliance with the License. | |
5 | // You may obtain a copy of the License at | |
6 | // | |
7 | // http://www.apache.org/licenses/LICENSE-2.0 | |
8 | // | |
9 | // Unless required by applicable law or agreed to in writing, software | |
10 | // distributed under the License is distributed on an "AS IS" BASIS, | |
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
12 | // See the License for the specific language governing permissions and | |
13 | // limitations under the License. | |
14 | ||
15 | package storage | |
16 | ||
17 | import ( | |
18 | "bytes" | |
19 | "context" | |
20 | "crypto" | |
21 | "crypto/rand" | |
22 | "crypto/rsa" | |
23 | "crypto/sha256" | |
24 | "crypto/x509" | |
25 | "encoding/base64" | |
26 | "encoding/pem" | |
27 | "errors" | |
28 | "fmt" | |
29 | "net/http" | |
30 | "net/url" | |
31 | "reflect" | |
32 | "regexp" | |
33 | "sort" | |
34 | "strconv" | |
35 | "strings" | |
36 | "time" | |
37 | "unicode/utf8" | |
38 | ||
39 | "cloud.google.com/go/internal/optional" | |
40 | "cloud.google.com/go/internal/trace" | |
41 | "cloud.google.com/go/internal/version" | |
42 | "google.golang.org/api/googleapi" | |
43 | "google.golang.org/api/option" | |
44 | raw "google.golang.org/api/storage/v1" | |
45 | htransport "google.golang.org/api/transport/http" | |
46 | ) | |
47 | ||
48 | var ( | |
49 | // ErrBucketNotExist indicates that the bucket does not exist. | |
50 | ErrBucketNotExist = errors.New("storage: bucket doesn't exist") | |
51 | // ErrObjectNotExist indicates that the object does not exist. | |
52 | ErrObjectNotExist = errors.New("storage: object doesn't exist") | |
53 | ) | |
54 | ||
55 | const userAgent = "gcloud-golang-storage/20151204" | |
56 | ||
57 | const ( | |
58 | // ScopeFullControl grants permissions to manage your | |
59 | // data and permissions in Google Cloud Storage. | |
60 | ScopeFullControl = raw.DevstorageFullControlScope | |
61 | ||
62 | // ScopeReadOnly grants permissions to | |
63 | // view your data in Google Cloud Storage. | |
64 | ScopeReadOnly = raw.DevstorageReadOnlyScope | |
65 | ||
66 | // ScopeReadWrite grants permissions to manage your | |
67 | // data in Google Cloud Storage. | |
68 | ScopeReadWrite = raw.DevstorageReadWriteScope | |
69 | ) | |
70 | ||
71 | var xGoogHeader = fmt.Sprintf("gl-go/%s gccl/%s", version.Go(), version.Repo) | |
72 | ||
73 | func setClientHeader(headers http.Header) { | |
74 | headers.Set("x-goog-api-client", xGoogHeader) | |
75 | } | |
76 | ||
77 | // Client is a client for interacting with Google Cloud Storage. | |
78 | // | |
79 | // Clients should be reused instead of created as needed. | |
80 | // The methods of Client are safe for concurrent use by multiple goroutines. | |
81 | type Client struct { | |
82 | hc *http.Client | |
83 | raw *raw.Service | |
84 | } | |
85 | ||
86 | // NewClient creates a new Google Cloud Storage client. | |
87 | // The default scope is ScopeFullControl. To use a different scope, like ScopeReadOnly, use option.WithScopes. | |
88 | func NewClient(ctx context.Context, opts ...option.ClientOption) (*Client, error) { | |
89 | o := []option.ClientOption{ | |
90 | option.WithScopes(ScopeFullControl), | |
91 | option.WithUserAgent(userAgent), | |
92 | } | |
93 | opts = append(o, opts...) | |
94 | hc, ep, err := htransport.NewClient(ctx, opts...) | |
95 | if err != nil { | |
96 | return nil, fmt.Errorf("dialing: %v", err) | |
97 | } | |
98 | rawService, err := raw.New(hc) | |
99 | if err != nil { | |
100 | return nil, fmt.Errorf("storage client: %v", err) | |
101 | } | |
102 | if ep != "" { | |
103 | rawService.BasePath = ep | |
104 | } | |
105 | return &Client{ | |
106 | hc: hc, | |
107 | raw: rawService, | |
108 | }, nil | |
109 | } | |
110 | ||
111 | // Close closes the Client. | |
112 | // | |
113 | // Close need not be called at program exit. | |
114 | func (c *Client) Close() error { | |
115 | // Set fields to nil so that subsequent uses will panic. | |
116 | c.hc = nil | |
117 | c.raw = nil | |
118 | return nil | |
119 | } | |
120 | ||
121 | // SignedURLOptions allows you to restrict the access to the signed URL. | |
122 | type SignedURLOptions struct { | |
123 | // GoogleAccessID represents the authorizer of the signed URL generation. | |
124 | // It is typically the Google service account client email address from | |
125 | // the Google Developers Console in the form of "xxx@developer.gserviceaccount.com". | |
126 | // Required. | |
127 | GoogleAccessID string | |
128 | ||
129 | // PrivateKey is the Google service account private key. It is obtainable | |
130 | // from the Google Developers Console. | |
131 | // At https://console.developers.google.com/project/<your-project-id>/apiui/credential, | |
132 | // create a service account client ID or reuse one of your existing service account | |
133 | // credentials. Click on the "Generate new P12 key" to generate and download | |
134 | // a new private key. Once you download the P12 file, use the following command | |
135 | // to convert it into a PEM file. | |
136 | // | |
137 | // $ openssl pkcs12 -in key.p12 -passin pass:notasecret -out key.pem -nodes | |
138 | // | |
139 | // Provide the contents of the PEM file as a byte slice. | |
140 | // Exactly one of PrivateKey or SignBytes must be non-nil. | |
141 | PrivateKey []byte | |
142 | ||
143 | // SignBytes is a function for implementing custom signing. | |
144 | // If your application is running on Google App Engine, you can use appengine's internal signing function: | |
145 | // ctx := appengine.NewContext(request) | |
146 | // acc, _ := appengine.ServiceAccount(ctx) | |
147 | // url, err := SignedURL("bucket", "object", &SignedURLOptions{ | |
148 | // GoogleAccessID: acc, | |
149 | // SignBytes: func(b []byte) ([]byte, error) { | |
150 | // _, signedBytes, err := appengine.SignBytes(ctx, b) | |
151 | // return signedBytes, err | |
152 | // }, | |
153 | // // etc. | |
154 | // }) | |
155 | // | |
156 | // Exactly one of PrivateKey or SignBytes must be non-nil. | |
157 | SignBytes func([]byte) ([]byte, error) | |
158 | ||
159 | // Method is the HTTP method to be used with the signed URL. | |
160 | // Signed URLs can be used with GET, HEAD, PUT, and DELETE requests. | |
161 | // Required. | |
162 | Method string | |
163 | ||
164 | // Expires is the expiration time on the signed URL. It must be | |
165 | // a datetime in the future. | |
166 | // Required. | |
167 | Expires time.Time | |
168 | ||
169 | // ContentType is the content type header the client must provide | |
170 | // to use the generated signed URL. | |
171 | // Optional. | |
172 | ContentType string | |
173 | ||
174 | // Headers is a list of extension headers the client must provide | |
175 | // in order to use the generated signed URL. | |
176 | // Optional. | |
177 | Headers []string | |
178 | ||
179 | // MD5 is the base64 encoded MD5 checksum of the file. | |
180 | // If provided, the client should provide the exact value on the request | |
181 | // header in order to use the signed URL. | |
182 | // Optional. | |
183 | MD5 string | |
184 | } | |
185 | ||
186 | var ( | |
187 | canonicalHeaderRegexp = regexp.MustCompile(`(?i)^(x-goog-[^:]+):(.*)?$`) | |
188 | excludedCanonicalHeaders = map[string]bool{ | |
189 | "x-goog-encryption-key": true, | |
190 | "x-goog-encryption-key-sha256": true, | |
191 | } | |
192 | ) | |
193 | ||
194 | // sanitizeHeaders applies the specifications for canonical extension headers at | |
195 | // https://cloud.google.com/storage/docs/access-control/signed-urls#about-canonical-extension-headers. | |
196 | func sanitizeHeaders(hdrs []string) []string { | |
197 | headerMap := map[string][]string{} | |
198 | for _, hdr := range hdrs { | |
199 | // No leading or trailing whitespaces. | |
200 | sanitizedHeader := strings.TrimSpace(hdr) | |
201 | ||
202 | // Only keep canonical headers, discard any others. | |
203 | headerMatches := canonicalHeaderRegexp.FindStringSubmatch(sanitizedHeader) | |
204 | if len(headerMatches) == 0 { | |
205 | continue | |
206 | } | |
207 | ||
208 | header := strings.ToLower(strings.TrimSpace(headerMatches[1])) | |
209 | if excludedCanonicalHeaders[headerMatches[1]] { | |
210 | // Do not keep any deliberately excluded canonical headers when signing. | |
211 | continue | |
212 | } | |
213 | value := strings.TrimSpace(headerMatches[2]) | |
214 | if len(value) > 0 { | |
215 | // Remove duplicate headers by appending the values of duplicates | |
216 | // in their order of appearance. | |
217 | headerMap[header] = append(headerMap[header], value) | |
218 | } | |
219 | } | |
220 | ||
221 | var sanitizedHeaders []string | |
222 | for header, values := range headerMap { | |
223 | // There should be no spaces around the colon separating the | |
224 | // header name from the header value or around the values | |
225 | // themselves. The values should be separated by commas. | |
226 | // NOTE: The semantics for headers without a value are not clear. | |
227 | // However from specifications these should be edge-cases | |
228 | // anyway and we should assume that there will be no | |
229 | // canonical headers using empty values. Any such headers | |
230 | // are discarded at the regexp stage above. | |
231 | sanitizedHeaders = append( | |
232 | sanitizedHeaders, | |
233 | fmt.Sprintf("%s:%s", header, strings.Join(values, ",")), | |
234 | ) | |
235 | } | |
236 | sort.Strings(sanitizedHeaders) | |
237 | return sanitizedHeaders | |
238 | } | |
239 | ||
240 | // SignedURL returns a URL for the specified object. Signed URLs allow | |
241 | // the users access to a restricted resource for a limited time without having a | |
242 | // Google account or signing in. For more information about the signed | |
243 | // URLs, see https://cloud.google.com/storage/docs/accesscontrol#Signed-URLs. | |
244 | func SignedURL(bucket, name string, opts *SignedURLOptions) (string, error) { | |
245 | if opts == nil { | |
246 | return "", errors.New("storage: missing required SignedURLOptions") | |
247 | } | |
248 | if opts.GoogleAccessID == "" { | |
249 | return "", errors.New("storage: missing required GoogleAccessID") | |
250 | } | |
251 | if (opts.PrivateKey == nil) == (opts.SignBytes == nil) { | |
252 | return "", errors.New("storage: exactly one of PrivateKey or SignedBytes must be set") | |
253 | } | |
254 | if opts.Method == "" { | |
255 | return "", errors.New("storage: missing required method option") | |
256 | } | |
257 | if opts.Expires.IsZero() { | |
258 | return "", errors.New("storage: missing required expires option") | |
259 | } | |
260 | if opts.MD5 != "" { | |
261 | md5, err := base64.StdEncoding.DecodeString(opts.MD5) | |
262 | if err != nil || len(md5) != 16 { | |
263 | return "", errors.New("storage: invalid MD5 checksum") | |
264 | } | |
265 | } | |
266 | opts.Headers = sanitizeHeaders(opts.Headers) | |
267 | ||
268 | signBytes := opts.SignBytes | |
269 | if opts.PrivateKey != nil { | |
270 | key, err := parseKey(opts.PrivateKey) | |
271 | if err != nil { | |
272 | return "", err | |
273 | } | |
274 | signBytes = func(b []byte) ([]byte, error) { | |
275 | sum := sha256.Sum256(b) | |
276 | return rsa.SignPKCS1v15( | |
277 | rand.Reader, | |
278 | key, | |
279 | crypto.SHA256, | |
280 | sum[:], | |
281 | ) | |
282 | } | |
283 | } | |
284 | ||
285 | u := &url.URL{ | |
286 | Path: fmt.Sprintf("/%s/%s", bucket, name), | |
287 | } | |
288 | ||
289 | buf := &bytes.Buffer{} | |
290 | fmt.Fprintf(buf, "%s\n", opts.Method) | |
291 | fmt.Fprintf(buf, "%s\n", opts.MD5) | |
292 | fmt.Fprintf(buf, "%s\n", opts.ContentType) | |
293 | fmt.Fprintf(buf, "%d\n", opts.Expires.Unix()) | |
294 | if len(opts.Headers) > 0 { | |
295 | fmt.Fprintf(buf, "%s\n", strings.Join(opts.Headers, "\n")) | |
296 | } | |
297 | fmt.Fprintf(buf, "%s", u.String()) | |
298 | ||
299 | b, err := signBytes(buf.Bytes()) | |
300 | if err != nil { | |
301 | return "", err | |
302 | } | |
303 | encoded := base64.StdEncoding.EncodeToString(b) | |
304 | u.Scheme = "https" | |
305 | u.Host = "storage.googleapis.com" | |
306 | q := u.Query() | |
307 | q.Set("GoogleAccessId", opts.GoogleAccessID) | |
308 | q.Set("Expires", fmt.Sprintf("%d", opts.Expires.Unix())) | |
309 | q.Set("Signature", string(encoded)) | |
310 | u.RawQuery = q.Encode() | |
311 | return u.String(), nil | |
312 | } | |
313 | ||
314 | // ObjectHandle provides operations on an object in a Google Cloud Storage bucket. | |
315 | // Use BucketHandle.Object to get a handle. | |
316 | type ObjectHandle struct { | |
317 | c *Client | |
318 | bucket string | |
319 | object string | |
320 | acl ACLHandle | |
321 | gen int64 // a negative value indicates latest | |
322 | conds *Conditions | |
323 | encryptionKey []byte // AES-256 key | |
324 | userProject string // for requester-pays buckets | |
325 | readCompressed bool // Accept-Encoding: gzip | |
326 | } | |
327 | ||
328 | // ACL provides access to the object's access control list. | |
329 | // This controls who can read and write this object. | |
330 | // This call does not perform any network operations. | |
331 | func (o *ObjectHandle) ACL() *ACLHandle { | |
332 | return &o.acl | |
333 | } | |
334 | ||
335 | // Generation returns a new ObjectHandle that operates on a specific generation | |
336 | // of the object. | |
337 | // By default, the handle operates on the latest generation. Not | |
338 | // all operations work when given a specific generation; check the API | |
339 | // endpoints at https://cloud.google.com/storage/docs/json_api/ for details. | |
340 | func (o *ObjectHandle) Generation(gen int64) *ObjectHandle { | |
341 | o2 := *o | |
342 | o2.gen = gen | |
343 | return &o2 | |
344 | } | |
345 | ||
346 | // If returns a new ObjectHandle that applies a set of preconditions. | |
347 | // Preconditions already set on the ObjectHandle are ignored. | |
348 | // Operations on the new handle will return an error if the preconditions are not | |
349 | // satisfied. See https://cloud.google.com/storage/docs/generations-preconditions | |
350 | // for more details. | |
351 | func (o *ObjectHandle) If(conds Conditions) *ObjectHandle { | |
352 | o2 := *o | |
353 | o2.conds = &conds | |
354 | return &o2 | |
355 | } | |
356 | ||
357 | // Key returns a new ObjectHandle that uses the supplied encryption | |
358 | // key to encrypt and decrypt the object's contents. | |
359 | // | |
360 | // Encryption key must be a 32-byte AES-256 key. | |
361 | // See https://cloud.google.com/storage/docs/encryption for details. | |
362 | func (o *ObjectHandle) Key(encryptionKey []byte) *ObjectHandle { | |
363 | o2 := *o | |
364 | o2.encryptionKey = encryptionKey | |
365 | return &o2 | |
366 | } | |
367 | ||
368 | // Attrs returns meta information about the object. | |
369 | // ErrObjectNotExist will be returned if the object is not found. | |
370 | func (o *ObjectHandle) Attrs(ctx context.Context) (attrs *ObjectAttrs, err error) { | |
371 | ctx = trace.StartSpan(ctx, "cloud.google.com/go/storage.Object.Attrs") | |
372 | defer func() { trace.EndSpan(ctx, err) }() | |
373 | ||
374 | if err := o.validate(); err != nil { | |
375 | return nil, err | |
376 | } | |
377 | call := o.c.raw.Objects.Get(o.bucket, o.object).Projection("full").Context(ctx) | |
378 | if err := applyConds("Attrs", o.gen, o.conds, call); err != nil { | |
379 | return nil, err | |
380 | } | |
381 | if o.userProject != "" { | |
382 | call.UserProject(o.userProject) | |
383 | } | |
384 | if err := setEncryptionHeaders(call.Header(), o.encryptionKey, false); err != nil { | |
385 | return nil, err | |
386 | } | |
387 | var obj *raw.Object | |
388 | setClientHeader(call.Header()) | |
389 | err = runWithRetry(ctx, func() error { obj, err = call.Do(); return err }) | |
390 | if e, ok := err.(*googleapi.Error); ok && e.Code == http.StatusNotFound { | |
391 | return nil, ErrObjectNotExist | |
392 | } | |
393 | if err != nil { | |
394 | return nil, err | |
395 | } | |
396 | return newObject(obj), nil | |
397 | } | |
398 | ||
399 | // Update updates an object with the provided attributes. | |
400 | // All zero-value attributes are ignored. | |
401 | // ErrObjectNotExist will be returned if the object is not found. | |
402 | func (o *ObjectHandle) Update(ctx context.Context, uattrs ObjectAttrsToUpdate) (oa *ObjectAttrs, err error) { | |
403 | ctx = trace.StartSpan(ctx, "cloud.google.com/go/storage.Object.Update") | |
404 | defer func() { trace.EndSpan(ctx, err) }() | |
405 | ||
406 | if err := o.validate(); err != nil { | |
407 | return nil, err | |
408 | } | |
409 | var attrs ObjectAttrs | |
410 | // Lists of fields to send, and set to null, in the JSON. | |
411 | var forceSendFields, nullFields []string | |
412 | if uattrs.ContentType != nil { | |
413 | attrs.ContentType = optional.ToString(uattrs.ContentType) | |
414 | // For ContentType, sending the empty string is a no-op. | |
415 | // Instead we send a null. | |
416 | if attrs.ContentType == "" { | |
417 | nullFields = append(nullFields, "ContentType") | |
418 | } else { | |
419 | forceSendFields = append(forceSendFields, "ContentType") | |
420 | } | |
421 | } | |
422 | if uattrs.ContentLanguage != nil { | |
423 | attrs.ContentLanguage = optional.ToString(uattrs.ContentLanguage) | |
424 | // For ContentLanguage it's an error to send the empty string. | |
425 | // Instead we send a null. | |
426 | if attrs.ContentLanguage == "" { | |
427 | nullFields = append(nullFields, "ContentLanguage") | |
428 | } else { | |
429 | forceSendFields = append(forceSendFields, "ContentLanguage") | |
430 | } | |
431 | } | |
432 | if uattrs.ContentEncoding != nil { | |
433 | attrs.ContentEncoding = optional.ToString(uattrs.ContentEncoding) | |
434 | forceSendFields = append(forceSendFields, "ContentEncoding") | |
435 | } | |
436 | if uattrs.ContentDisposition != nil { | |
437 | attrs.ContentDisposition = optional.ToString(uattrs.ContentDisposition) | |
438 | forceSendFields = append(forceSendFields, "ContentDisposition") | |
439 | } | |
440 | if uattrs.CacheControl != nil { | |
441 | attrs.CacheControl = optional.ToString(uattrs.CacheControl) | |
442 | forceSendFields = append(forceSendFields, "CacheControl") | |
443 | } | |
444 | if uattrs.EventBasedHold != nil { | |
445 | attrs.EventBasedHold = optional.ToBool(uattrs.EventBasedHold) | |
446 | forceSendFields = append(forceSendFields, "EventBasedHold") | |
447 | } | |
448 | if uattrs.TemporaryHold != nil { | |
449 | attrs.TemporaryHold = optional.ToBool(uattrs.TemporaryHold) | |
450 | forceSendFields = append(forceSendFields, "TemporaryHold") | |
451 | } | |
452 | if uattrs.Metadata != nil { | |
453 | attrs.Metadata = uattrs.Metadata | |
454 | if len(attrs.Metadata) == 0 { | |
455 | // Sending the empty map is a no-op. We send null instead. | |
456 | nullFields = append(nullFields, "Metadata") | |
457 | } else { | |
458 | forceSendFields = append(forceSendFields, "Metadata") | |
459 | } | |
460 | } | |
461 | if uattrs.ACL != nil { | |
462 | attrs.ACL = uattrs.ACL | |
463 | // It's an error to attempt to delete the ACL, so | |
464 | // we don't append to nullFields here. | |
465 | forceSendFields = append(forceSendFields, "Acl") | |
466 | } | |
467 | rawObj := attrs.toRawObject(o.bucket) | |
468 | rawObj.ForceSendFields = forceSendFields | |
469 | rawObj.NullFields = nullFields | |
470 | call := o.c.raw.Objects.Patch(o.bucket, o.object, rawObj).Projection("full").Context(ctx) | |
471 | if err := applyConds("Update", o.gen, o.conds, call); err != nil { | |
472 | return nil, err | |
473 | } | |
474 | if o.userProject != "" { | |
475 | call.UserProject(o.userProject) | |
476 | } | |
477 | if uattrs.PredefinedACL != "" { | |
478 | call.PredefinedAcl(uattrs.PredefinedACL) | |
479 | } | |
480 | if err := setEncryptionHeaders(call.Header(), o.encryptionKey, false); err != nil { | |
481 | return nil, err | |
482 | } | |
483 | var obj *raw.Object | |
484 | setClientHeader(call.Header()) | |
485 | err = runWithRetry(ctx, func() error { obj, err = call.Do(); return err }) | |
486 | if e, ok := err.(*googleapi.Error); ok && e.Code == http.StatusNotFound { | |
487 | return nil, ErrObjectNotExist | |
488 | } | |
489 | if err != nil { | |
490 | return nil, err | |
491 | } | |
492 | return newObject(obj), nil | |
493 | } | |
494 | ||
495 | // BucketName returns the name of the bucket. | |
496 | func (o *ObjectHandle) BucketName() string { | |
497 | return o.bucket | |
498 | } | |
499 | ||
500 | // ObjectName returns the name of the object. | |
501 | func (o *ObjectHandle) ObjectName() string { | |
502 | return o.object | |
503 | } | |
504 | ||
505 | // ObjectAttrsToUpdate is used to update the attributes of an object. | |
506 | // Only fields set to non-nil values will be updated. | |
507 | // Set a field to its zero value to delete it. | |
508 | // | |
509 | // For example, to change ContentType and delete ContentEncoding and | |
510 | // Metadata, use | |
511 | // ObjectAttrsToUpdate{ | |
512 | // ContentType: "text/html", | |
513 | // ContentEncoding: "", | |
514 | // Metadata: map[string]string{}, | |
515 | // } | |
516 | type ObjectAttrsToUpdate struct { | |
517 | EventBasedHold optional.Bool | |
518 | TemporaryHold optional.Bool | |
519 | ContentType optional.String | |
520 | ContentLanguage optional.String | |
521 | ContentEncoding optional.String | |
522 | ContentDisposition optional.String | |
523 | CacheControl optional.String | |
524 | Metadata map[string]string // set to map[string]string{} to delete | |
525 | ACL []ACLRule | |
526 | ||
527 | // If not empty, applies a predefined set of access controls. ACL must be nil. | |
528 | // See https://cloud.google.com/storage/docs/json_api/v1/objects/patch. | |
529 | PredefinedACL string | |
530 | } | |
531 | ||
532 | // Delete deletes the single specified object. | |
533 | func (o *ObjectHandle) Delete(ctx context.Context) error { | |
534 | if err := o.validate(); err != nil { | |
535 | return err | |
536 | } | |
537 | call := o.c.raw.Objects.Delete(o.bucket, o.object).Context(ctx) | |
538 | if err := applyConds("Delete", o.gen, o.conds, call); err != nil { | |
539 | return err | |
540 | } | |
541 | if o.userProject != "" { | |
542 | call.UserProject(o.userProject) | |
543 | } | |
544 | // Encryption doesn't apply to Delete. | |
545 | setClientHeader(call.Header()) | |
546 | err := runWithRetry(ctx, func() error { return call.Do() }) | |
547 | switch e := err.(type) { | |
548 | case nil: | |
549 | return nil | |
550 | case *googleapi.Error: | |
551 | if e.Code == http.StatusNotFound { | |
552 | return ErrObjectNotExist | |
553 | } | |
554 | } | |
555 | return err | |
556 | } | |
557 | ||
558 | // ReadCompressed when true causes the read to happen without decompressing. | |
559 | func (o *ObjectHandle) ReadCompressed(compressed bool) *ObjectHandle { | |
560 | o2 := *o | |
561 | o2.readCompressed = compressed | |
562 | return &o2 | |
563 | } | |
564 | ||
565 | // NewWriter returns a storage Writer that writes to the GCS object | |
566 | // associated with this ObjectHandle. | |
567 | // | |
568 | // A new object will be created unless an object with this name already exists. | |
569 | // Otherwise any previous object with the same name will be replaced. | |
570 | // The object will not be available (and any previous object will remain) | |
571 | // until Close has been called. | |
572 | // | |
573 | // Attributes can be set on the object by modifying the returned Writer's | |
574 | // ObjectAttrs field before the first call to Write. If no ContentType | |
575 | // attribute is specified, the content type will be automatically sniffed | |
576 | // using net/http.DetectContentType. | |
577 | // | |
578 | // It is the caller's responsibility to call Close when writing is done. To | |
579 | // stop writing without saving the data, cancel the context. | |
580 | func (o *ObjectHandle) NewWriter(ctx context.Context) *Writer { | |
581 | return &Writer{ | |
582 | ctx: ctx, | |
583 | o: o, | |
584 | donec: make(chan struct{}), | |
585 | ObjectAttrs: ObjectAttrs{Name: o.object}, | |
586 | ChunkSize: googleapi.DefaultUploadChunkSize, | |
587 | } | |
588 | } | |
589 | ||
590 | func (o *ObjectHandle) validate() error { | |
591 | if o.bucket == "" { | |
592 | return errors.New("storage: bucket name is empty") | |
593 | } | |
594 | if o.object == "" { | |
595 | return errors.New("storage: object name is empty") | |
596 | } | |
597 | if !utf8.ValidString(o.object) { | |
598 | return fmt.Errorf("storage: object name %q is not valid UTF-8", o.object) | |
599 | } | |
600 | return nil | |
601 | } | |
602 | ||
603 | // parseKey converts the binary contents of a private key file to an | |
604 | // *rsa.PrivateKey. It detects whether the private key is in a PEM container or | |
605 | // not. If so, it extracts the private key from PEM container before | |
606 | // conversion. It only supports PEM containers with no passphrase. | |
607 | func parseKey(key []byte) (*rsa.PrivateKey, error) { | |
608 | if block, _ := pem.Decode(key); block != nil { | |
609 | key = block.Bytes | |
610 | } | |
611 | parsedKey, err := x509.ParsePKCS8PrivateKey(key) | |
612 | if err != nil { | |
613 | parsedKey, err = x509.ParsePKCS1PrivateKey(key) | |
614 | if err != nil { | |
615 | return nil, err | |
616 | } | |
617 | } | |
618 | parsed, ok := parsedKey.(*rsa.PrivateKey) | |
619 | if !ok { | |
620 | return nil, errors.New("oauth2: private key is invalid") | |
621 | } | |
622 | return parsed, nil | |
623 | } | |
624 | ||
625 | // toRawObject copies the editable attributes from o to the raw library's Object type. | |
626 | func (o *ObjectAttrs) toRawObject(bucket string) *raw.Object { | |
627 | var ret string | |
628 | if !o.RetentionExpirationTime.IsZero() { | |
629 | ret = o.RetentionExpirationTime.Format(time.RFC3339) | |
630 | } | |
631 | return &raw.Object{ | |
632 | Bucket: bucket, | |
633 | Name: o.Name, | |
634 | EventBasedHold: o.EventBasedHold, | |
635 | TemporaryHold: o.TemporaryHold, | |
636 | RetentionExpirationTime: ret, | |
637 | ContentType: o.ContentType, | |
638 | ContentEncoding: o.ContentEncoding, | |
639 | ContentLanguage: o.ContentLanguage, | |
640 | CacheControl: o.CacheControl, | |
641 | ContentDisposition: o.ContentDisposition, | |
642 | StorageClass: o.StorageClass, | |
643 | Acl: toRawObjectACL(o.ACL), | |
644 | Metadata: o.Metadata, | |
645 | } | |
646 | } | |
647 | ||
648 | // ObjectAttrs represents the metadata for a Google Cloud Storage (GCS) object. | |
649 | type ObjectAttrs struct { | |
650 | // Bucket is the name of the bucket containing this GCS object. | |
651 | // This field is read-only. | |
652 | Bucket string | |
653 | ||
654 | // Name is the name of the object within the bucket. | |
655 | // This field is read-only. | |
656 | Name string | |
657 | ||
658 | // ContentType is the MIME type of the object's content. | |
659 | ContentType string | |
660 | ||
661 | // ContentLanguage is the content language of the object's content. | |
662 | ContentLanguage string | |
663 | ||
664 | // CacheControl is the Cache-Control header to be sent in the response | |
665 | // headers when serving the object data. | |
666 | CacheControl string | |
667 | ||
668 | // EventBasedHold specifies whether an object is under event-based hold. New | |
669 | // objects created in a bucket whose DefaultEventBasedHold is set will | |
670 | // default to that value. | |
671 | EventBasedHold bool | |
672 | ||
673 | // TemporaryHold specifies whether an object is under temporary hold. While | |
674 | // this flag is set to true, the object is protected against deletion and | |
675 | // overwrites. | |
676 | TemporaryHold bool | |
677 | ||
678 | // RetentionExpirationTime is a server-determined value that specifies the | |
679 | // earliest time that the object's retention period expires. | |
680 | // This is a read-only field. | |
681 | RetentionExpirationTime time.Time | |
682 | ||
683 | // ACL is the list of access control rules for the object. | |
684 | ACL []ACLRule | |
685 | ||
686 | // If not empty, applies a predefined set of access controls. It should be set | |
687 | // only when writing, copying or composing an object. When copying or composing, | |
688 | // it acts as the destinationPredefinedAcl parameter. | |
689 | // PredefinedACL is always empty for ObjectAttrs returned from the service. | |
690 | // See https://cloud.google.com/storage/docs/json_api/v1/objects/insert | |
691 | // for valid values. | |
692 | PredefinedACL string | |
693 | ||
694 | // Owner is the owner of the object. This field is read-only. | |
695 | // | |
696 | // If non-zero, it is in the form of "user-<userId>". | |
697 | Owner string | |
698 | ||
699 | // Size is the length of the object's content. This field is read-only. | |
700 | Size int64 | |
701 | ||
702 | // ContentEncoding is the encoding of the object's content. | |
703 | ContentEncoding string | |
704 | ||
705 | // ContentDisposition is the optional Content-Disposition header of the object | |
706 | // sent in the response headers. | |
707 | ContentDisposition string | |
708 | ||
709 | // MD5 is the MD5 hash of the object's content. This field is read-only, | |
710 | // except when used from a Writer. If set on a Writer, the uploaded | |
711 | // data is rejected if its MD5 hash does not match this field. | |
712 | MD5 []byte | |
713 | ||
714 | // CRC32C is the CRC32 checksum of the object's content using | |
715 | // the Castagnoli93 polynomial. This field is read-only, except when | |
716 | // used from a Writer. If set on a Writer and Writer.SendCRC32C | |
717 | // is true, the uploaded data is rejected if its CRC32c hash does not | |
718 | // match this field. | |
719 | CRC32C uint32 | |
720 | ||
721 | // MediaLink is an URL to the object's content. This field is read-only. | |
722 | MediaLink string | |
723 | ||
724 | // Metadata represents user-provided metadata, in key/value pairs. | |
725 | // It can be nil if no metadata is provided. | |
726 | Metadata map[string]string | |
727 | ||
728 | // Generation is the generation number of the object's content. | |
729 | // This field is read-only. | |
730 | Generation int64 | |
731 | ||
732 | // Metageneration is the version of the metadata for this | |
733 | // object at this generation. This field is used for preconditions | |
734 | // and for detecting changes in metadata. A metageneration number | |
735 | // is only meaningful in the context of a particular generation | |
736 | // of a particular object. This field is read-only. | |
737 | Metageneration int64 | |
738 | ||
739 | // StorageClass is the storage class of the object. | |
740 | // This value defines how objects in the bucket are stored and | |
741 | // determines the SLA and the cost of storage. Typical values are | |
742 | // "MULTI_REGIONAL", "REGIONAL", "NEARLINE", "COLDLINE", "STANDARD" | |
743 | // and "DURABLE_REDUCED_AVAILABILITY". | |
744 | // It defaults to "STANDARD", which is equivalent to "MULTI_REGIONAL" | |
745 | // or "REGIONAL" depending on the bucket's location settings. | |
746 | StorageClass string | |
747 | ||
748 | // Created is the time the object was created. This field is read-only. | |
749 | Created time.Time | |
750 | ||
751 | // Deleted is the time the object was deleted. | |
752 | // If not deleted, it is the zero value. This field is read-only. | |
753 | Deleted time.Time | |
754 | ||
755 | // Updated is the creation or modification time of the object. | |
756 | // For buckets with versioning enabled, changing an object's | |
757 | // metadata does not change this property. This field is read-only. | |
758 | Updated time.Time | |
759 | ||
760 | // CustomerKeySHA256 is the base64-encoded SHA-256 hash of the | |
761 | // customer-supplied encryption key for the object. It is empty if there is | |
762 | // no customer-supplied encryption key. | |
763 | // See // https://cloud.google.com/storage/docs/encryption for more about | |
764 | // encryption in Google Cloud Storage. | |
765 | CustomerKeySHA256 string | |
766 | ||
767 | // Cloud KMS key name, in the form | |
768 | // projects/P/locations/L/keyRings/R/cryptoKeys/K, used to encrypt this object, | |
769 | // if the object is encrypted by such a key. | |
770 | // | |
771 | // Providing both a KMSKeyName and a customer-supplied encryption key (via | |
772 | // ObjectHandle.Key) will result in an error when writing an object. | |
773 | KMSKeyName string | |
774 | ||
775 | // Prefix is set only for ObjectAttrs which represent synthetic "directory | |
776 | // entries" when iterating over buckets using Query.Delimiter. See | |
777 | // ObjectIterator.Next. When set, no other fields in ObjectAttrs will be | |
778 | // populated. | |
779 | Prefix string | |
780 | } | |
781 | ||
782 | // convertTime converts a time in RFC3339 format to time.Time. | |
783 | // If any error occurs in parsing, the zero-value time.Time is silently returned. | |
784 | func convertTime(t string) time.Time { | |
785 | var r time.Time | |
786 | if t != "" { | |
787 | r, _ = time.Parse(time.RFC3339, t) | |
788 | } | |
789 | return r | |
790 | } | |
791 | ||
792 | func newObject(o *raw.Object) *ObjectAttrs { | |
793 | if o == nil { | |
794 | return nil | |
795 | } | |
796 | owner := "" | |
797 | if o.Owner != nil { | |
798 | owner = o.Owner.Entity | |
799 | } | |
800 | md5, _ := base64.StdEncoding.DecodeString(o.Md5Hash) | |
801 | crc32c, _ := decodeUint32(o.Crc32c) | |
802 | var sha256 string | |
803 | if o.CustomerEncryption != nil { | |
804 | sha256 = o.CustomerEncryption.KeySha256 | |
805 | } | |
806 | return &ObjectAttrs{ | |
807 | Bucket: o.Bucket, | |
808 | Name: o.Name, | |
809 | ContentType: o.ContentType, | |
810 | ContentLanguage: o.ContentLanguage, | |
811 | CacheControl: o.CacheControl, | |
812 | EventBasedHold: o.EventBasedHold, | |
813 | TemporaryHold: o.TemporaryHold, | |
814 | RetentionExpirationTime: convertTime(o.RetentionExpirationTime), | |
815 | ACL: toObjectACLRules(o.Acl), | |
816 | Owner: owner, | |
817 | ContentEncoding: o.ContentEncoding, | |
818 | ContentDisposition: o.ContentDisposition, | |
819 | Size: int64(o.Size), | |
820 | MD5: md5, | |
821 | CRC32C: crc32c, | |
822 | MediaLink: o.MediaLink, | |
823 | Metadata: o.Metadata, | |
824 | Generation: o.Generation, | |
825 | Metageneration: o.Metageneration, | |
826 | StorageClass: o.StorageClass, | |
827 | CustomerKeySHA256: sha256, | |
828 | KMSKeyName: o.KmsKeyName, | |
829 | Created: convertTime(o.TimeCreated), | |
830 | Deleted: convertTime(o.TimeDeleted), | |
831 | Updated: convertTime(o.Updated), | |
832 | } | |
833 | } | |
834 | ||
835 | // Decode a uint32 encoded in Base64 in big-endian byte order. | |
836 | func decodeUint32(b64 string) (uint32, error) { | |
837 | d, err := base64.StdEncoding.DecodeString(b64) | |
838 | if err != nil { | |
839 | return 0, err | |
840 | } | |
841 | if len(d) != 4 { | |
842 | return 0, fmt.Errorf("storage: %q does not encode a 32-bit value", d) | |
843 | } | |
844 | return uint32(d[0])<<24 + uint32(d[1])<<16 + uint32(d[2])<<8 + uint32(d[3]), nil | |
845 | } | |
846 | ||
847 | // Encode a uint32 as Base64 in big-endian byte order. | |
848 | func encodeUint32(u uint32) string { | |
849 | b := []byte{byte(u >> 24), byte(u >> 16), byte(u >> 8), byte(u)} | |
850 | return base64.StdEncoding.EncodeToString(b) | |
851 | } | |
852 | ||
853 | // Query represents a query to filter objects from a bucket. | |
854 | type Query struct { | |
855 | // Delimiter returns results in a directory-like fashion. | |
856 | // Results will contain only objects whose names, aside from the | |
857 | // prefix, do not contain delimiter. Objects whose names, | |
858 | // aside from the prefix, contain delimiter will have their name, | |
859 | // truncated after the delimiter, returned in prefixes. | |
860 | // Duplicate prefixes are omitted. | |
861 | // Optional. | |
862 | Delimiter string | |
863 | ||
864 | // Prefix is the prefix filter to query objects | |
865 | // whose names begin with this prefix. | |
866 | // Optional. | |
867 | Prefix string | |
868 | ||
869 | // Versions indicates whether multiple versions of the same | |
870 | // object will be included in the results. | |
871 | Versions bool | |
872 | } | |
873 | ||
874 | // Conditions constrain methods to act on specific generations of | |
875 | // objects. | |
876 | // | |
877 | // The zero value is an empty set of constraints. Not all conditions or | |
878 | // combinations of conditions are applicable to all methods. | |
879 | // See https://cloud.google.com/storage/docs/generations-preconditions | |
880 | // for details on how these operate. | |
881 | type Conditions struct { | |
882 | // Generation constraints. | |
883 | // At most one of the following can be set to a non-zero value. | |
884 | ||
885 | // GenerationMatch specifies that the object must have the given generation | |
886 | // for the operation to occur. | |
887 | // If GenerationMatch is zero, it has no effect. | |
888 | // Use DoesNotExist to specify that the object does not exist in the bucket. | |
889 | GenerationMatch int64 | |
890 | ||
891 | // GenerationNotMatch specifies that the object must not have the given | |
892 | // generation for the operation to occur. | |
893 | // If GenerationNotMatch is zero, it has no effect. | |
894 | GenerationNotMatch int64 | |
895 | ||
896 | // DoesNotExist specifies that the object must not exist in the bucket for | |
897 | // the operation to occur. | |
898 | // If DoesNotExist is false, it has no effect. | |
899 | DoesNotExist bool | |
900 | ||
901 | // Metadata generation constraints. | |
902 | // At most one of the following can be set to a non-zero value. | |
903 | ||
904 | // MetagenerationMatch specifies that the object must have the given | |
905 | // metageneration for the operation to occur. | |
906 | // If MetagenerationMatch is zero, it has no effect. | |
907 | MetagenerationMatch int64 | |
908 | ||
909 | // MetagenerationNotMatch specifies that the object must not have the given | |
910 | // metageneration for the operation to occur. | |
911 | // If MetagenerationNotMatch is zero, it has no effect. | |
912 | MetagenerationNotMatch int64 | |
913 | } | |
914 | ||
915 | func (c *Conditions) validate(method string) error { | |
916 | if *c == (Conditions{}) { | |
917 | return fmt.Errorf("storage: %s: empty conditions", method) | |
918 | } | |
919 | if !c.isGenerationValid() { | |
920 | return fmt.Errorf("storage: %s: multiple conditions specified for generation", method) | |
921 | } | |
922 | if !c.isMetagenerationValid() { | |
923 | return fmt.Errorf("storage: %s: multiple conditions specified for metageneration", method) | |
924 | } | |
925 | return nil | |
926 | } | |
927 | ||
928 | func (c *Conditions) isGenerationValid() bool { | |
929 | n := 0 | |
930 | if c.GenerationMatch != 0 { | |
931 | n++ | |
932 | } | |
933 | if c.GenerationNotMatch != 0 { | |
934 | n++ | |
935 | } | |
936 | if c.DoesNotExist { | |
937 | n++ | |
938 | } | |
939 | return n <= 1 | |
940 | } | |
941 | ||
942 | func (c *Conditions) isMetagenerationValid() bool { | |
943 | return c.MetagenerationMatch == 0 || c.MetagenerationNotMatch == 0 | |
944 | } | |
945 | ||
946 | // applyConds modifies the provided call using the conditions in conds. | |
947 | // call is something that quacks like a *raw.WhateverCall. | |
948 | func applyConds(method string, gen int64, conds *Conditions, call interface{}) error { | |
949 | cval := reflect.ValueOf(call) | |
950 | if gen >= 0 { | |
951 | if !setConditionField(cval, "Generation", gen) { | |
952 | return fmt.Errorf("storage: %s: generation not supported", method) | |
953 | } | |
954 | } | |
955 | if conds == nil { | |
956 | return nil | |
957 | } | |
958 | if err := conds.validate(method); err != nil { | |
959 | return err | |
960 | } | |
961 | switch { | |
962 | case conds.GenerationMatch != 0: | |
963 | if !setConditionField(cval, "IfGenerationMatch", conds.GenerationMatch) { | |
964 | return fmt.Errorf("storage: %s: ifGenerationMatch not supported", method) | |
965 | } | |
966 | case conds.GenerationNotMatch != 0: | |
967 | if !setConditionField(cval, "IfGenerationNotMatch", conds.GenerationNotMatch) { | |
968 | return fmt.Errorf("storage: %s: ifGenerationNotMatch not supported", method) | |
969 | } | |
970 | case conds.DoesNotExist: | |
971 | if !setConditionField(cval, "IfGenerationMatch", int64(0)) { | |
972 | return fmt.Errorf("storage: %s: DoesNotExist not supported", method) | |
973 | } | |
974 | } | |
975 | switch { | |
976 | case conds.MetagenerationMatch != 0: | |
977 | if !setConditionField(cval, "IfMetagenerationMatch", conds.MetagenerationMatch) { | |
978 | return fmt.Errorf("storage: %s: ifMetagenerationMatch not supported", method) | |
979 | } | |
980 | case conds.MetagenerationNotMatch != 0: | |
981 | if !setConditionField(cval, "IfMetagenerationNotMatch", conds.MetagenerationNotMatch) { | |
982 | return fmt.Errorf("storage: %s: ifMetagenerationNotMatch not supported", method) | |
983 | } | |
984 | } | |
985 | return nil | |
986 | } | |
987 | ||
988 | func applySourceConds(gen int64, conds *Conditions, call *raw.ObjectsRewriteCall) error { | |
989 | if gen >= 0 { | |
990 | call.SourceGeneration(gen) | |
991 | } | |
992 | if conds == nil { | |
993 | return nil | |
994 | } | |
995 | if err := conds.validate("CopyTo source"); err != nil { | |
996 | return err | |
997 | } | |
998 | switch { | |
999 | case conds.GenerationMatch != 0: | |
1000 | call.IfSourceGenerationMatch(conds.GenerationMatch) | |
1001 | case conds.GenerationNotMatch != 0: | |
1002 | call.IfSourceGenerationNotMatch(conds.GenerationNotMatch) | |
1003 | case conds.DoesNotExist: | |
1004 | call.IfSourceGenerationMatch(0) | |
1005 | } | |
1006 | switch { | |
1007 | case conds.MetagenerationMatch != 0: | |
1008 | call.IfSourceMetagenerationMatch(conds.MetagenerationMatch) | |
1009 | case conds.MetagenerationNotMatch != 0: | |
1010 | call.IfSourceMetagenerationNotMatch(conds.MetagenerationNotMatch) | |
1011 | } | |
1012 | return nil | |
1013 | } | |
1014 | ||
1015 | // setConditionField sets a field on a *raw.WhateverCall. | |
1016 | // We can't use anonymous interfaces because the return type is | |
1017 | // different, since the field setters are builders. | |
1018 | func setConditionField(call reflect.Value, name string, value interface{}) bool { | |
1019 | m := call.MethodByName(name) | |
1020 | if !m.IsValid() { | |
1021 | return false | |
1022 | } | |
1023 | m.Call([]reflect.Value{reflect.ValueOf(value)}) | |
1024 | return true | |
1025 | } | |
1026 | ||
1027 | // conditionsQuery returns the generation and conditions as a URL query | |
1028 | // string suitable for URL.RawQuery. It assumes that the conditions | |
1029 | // have been validated. | |
1030 | func conditionsQuery(gen int64, conds *Conditions) string { | |
1031 | // URL escapes are elided because integer strings are URL-safe. | |
1032 | var buf []byte | |
1033 | ||
1034 | appendParam := func(s string, n int64) { | |
1035 | if len(buf) > 0 { | |
1036 | buf = append(buf, '&') | |
1037 | } | |
1038 | buf = append(buf, s...) | |
1039 | buf = strconv.AppendInt(buf, n, 10) | |
1040 | } | |
1041 | ||
1042 | if gen >= 0 { | |
1043 | appendParam("generation=", gen) | |
1044 | } | |
1045 | if conds == nil { | |
1046 | return string(buf) | |
1047 | } | |
1048 | switch { | |
1049 | case conds.GenerationMatch != 0: | |
1050 | appendParam("ifGenerationMatch=", conds.GenerationMatch) | |
1051 | case conds.GenerationNotMatch != 0: | |
1052 | appendParam("ifGenerationNotMatch=", conds.GenerationNotMatch) | |
1053 | case conds.DoesNotExist: | |
1054 | appendParam("ifGenerationMatch=", 0) | |
1055 | } | |
1056 | switch { | |
1057 | case conds.MetagenerationMatch != 0: | |
1058 | appendParam("ifMetagenerationMatch=", conds.MetagenerationMatch) | |
1059 | case conds.MetagenerationNotMatch != 0: | |
1060 | appendParam("ifMetagenerationNotMatch=", conds.MetagenerationNotMatch) | |
1061 | } | |
1062 | return string(buf) | |
1063 | } | |
1064 | ||
1065 | // composeSourceObj wraps a *raw.ComposeRequestSourceObjects, but adds the methods | |
1066 | // that modifyCall searches for by name. | |
1067 | type composeSourceObj struct { | |
1068 | src *raw.ComposeRequestSourceObjects | |
1069 | } | |
1070 | ||
1071 | func (c composeSourceObj) Generation(gen int64) { | |
1072 | c.src.Generation = gen | |
1073 | } | |
1074 | ||
1075 | func (c composeSourceObj) IfGenerationMatch(gen int64) { | |
1076 | // It's safe to overwrite ObjectPreconditions, since its only field is | |
1077 | // IfGenerationMatch. | |
1078 | c.src.ObjectPreconditions = &raw.ComposeRequestSourceObjectsObjectPreconditions{ | |
1079 | IfGenerationMatch: gen, | |
1080 | } | |
1081 | } | |
1082 | ||
1083 | func setEncryptionHeaders(headers http.Header, key []byte, copySource bool) error { | |
1084 | if key == nil { | |
1085 | return nil | |
1086 | } | |
1087 | // TODO(jbd): Ask the API team to return a more user-friendly error | |
1088 | // and avoid doing this check at the client level. | |
1089 | if len(key) != 32 { | |
1090 | return errors.New("storage: not a 32-byte AES-256 key") | |
1091 | } | |
1092 | var cs string | |
1093 | if copySource { | |
1094 | cs = "copy-source-" | |
1095 | } | |
1096 | headers.Set("x-goog-"+cs+"encryption-algorithm", "AES256") | |
1097 | headers.Set("x-goog-"+cs+"encryption-key", base64.StdEncoding.EncodeToString(key)) | |
1098 | keyHash := sha256.Sum256(key) | |
1099 | headers.Set("x-goog-"+cs+"encryption-key-sha256", base64.StdEncoding.EncodeToString(keyHash[:])) | |
1100 | return nil | |
1101 | } | |
1102 | ||
1103 | // ServiceAccount fetches the email address of the given project's Google Cloud Storage service account. | |
1104 | func (c *Client) ServiceAccount(ctx context.Context, projectID string) (string, error) { | |
1105 | r := c.raw.Projects.ServiceAccount.Get(projectID) | |
1106 | res, err := r.Context(ctx).Do() | |
1107 | if err != nil { | |
1108 | return "", err | |
1109 | } | |
1110 | return res.EmailAddress, nil | |
1111 | } |