123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532 |
- // Copyright 2014 Google Inc. All Rights Reserved.
- //
- // 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 contains a Google Cloud Storage client.
- //
- // This package is experimental and may make backwards-incompatible changes.
- package storage // import "google.golang.org/cloud/storage"
- import (
- "crypto"
- "crypto/rand"
- "crypto/rsa"
- "crypto/sha256"
- "crypto/x509"
- "encoding/base64"
- "encoding/pem"
- "errors"
- "fmt"
- "net/http"
- "net/url"
- "strings"
- "time"
- "unicode/utf8"
- "google.golang.org/cloud"
- "google.golang.org/cloud/internal/transport"
- "golang.org/x/net/context"
- "google.golang.org/api/googleapi"
- raw "google.golang.org/api/storage/v1"
- )
- var (
- ErrBucketNotExist = errors.New("storage: bucket doesn't exist")
- ErrObjectNotExist = errors.New("storage: object doesn't exist")
- )
- const userAgent = "gcloud-golang-storage/20151204"
- 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
- )
- // AdminClient is a client type for performing admin operations on a project's
- // buckets.
- type AdminClient struct {
- hc *http.Client
- raw *raw.Service
- projectID string
- }
- // NewAdminClient creates a new AdminClient for a given project.
- func NewAdminClient(ctx context.Context, projectID string, opts ...cloud.ClientOption) (*AdminClient, error) {
- c, err := NewClient(ctx, opts...)
- if err != nil {
- return nil, err
- }
- return &AdminClient{
- hc: c.hc,
- raw: c.raw,
- projectID: projectID,
- }, nil
- }
- // Close closes the AdminClient.
- func (c *AdminClient) Close() error {
- c.hc = nil
- return nil
- }
- // Create creates a Bucket in the project.
- // If attrs is nil the API defaults will be used.
- func (c *AdminClient) CreateBucket(ctx context.Context, bucketName string, attrs *BucketAttrs) error {
- var bkt *raw.Bucket
- if attrs != nil {
- bkt = attrs.toRawBucket()
- } else {
- bkt = &raw.Bucket{}
- }
- bkt.Name = bucketName
- req := c.raw.Buckets.Insert(c.projectID, bkt)
- _, err := req.Context(ctx).Do()
- return err
- }
- // Delete deletes a Bucket in the project.
- func (c *AdminClient) DeleteBucket(ctx context.Context, bucketName string) error {
- req := c.raw.Buckets.Delete(bucketName)
- return req.Context(ctx).Do()
- }
- // Client is a client for interacting with Google Cloud Storage.
- type Client struct {
- hc *http.Client
- raw *raw.Service
- }
- // NewClient creates a new Google Cloud Storage client.
- // The default scope is ScopeFullControl. To use a different scope, like ScopeReadOnly, use cloud.WithScopes.
- func NewClient(ctx context.Context, opts ...cloud.ClientOption) (*Client, error) {
- o := []cloud.ClientOption{
- cloud.WithScopes(ScopeFullControl),
- cloud.WithUserAgent(userAgent),
- }
- opts = append(o, opts...)
- hc, _, err := transport.NewHTTPClient(ctx, opts...)
- if err != nil {
- return nil, fmt.Errorf("dialing: %v", err)
- }
- rawService, err := raw.New(hc)
- if err != nil {
- return nil, fmt.Errorf("storage client: %v", err)
- }
- return &Client{
- hc: hc,
- raw: rawService,
- }, nil
- }
- // Close closes the Client.
- func (c *Client) Close() error {
- c.hc = nil
- return nil
- }
- // BucketHandle provides operations on a Google Cloud Storage bucket.
- // Use Client.Bucket to get a handle.
- type BucketHandle struct {
- acl *ACLHandle
- defaultObjectACL *ACLHandle
- c *Client
- name string
- }
- // Bucket returns a BucketHandle, which provides operations on the named bucket.
- // This call does not perform any network operations.
- //
- // name must contain only lowercase letters, numbers, dashes, underscores, and
- // dots. The full specification for valid bucket names can be found at:
- // https://cloud.google.com/storage/docs/bucket-naming
- func (c *Client) Bucket(name string) *BucketHandle {
- return &BucketHandle{
- c: c,
- name: name,
- acl: &ACLHandle{
- c: c,
- bucket: name,
- },
- defaultObjectACL: &ACLHandle{
- c: c,
- bucket: name,
- isDefault: true,
- },
- }
- }
- // ACL returns an ACLHandle, which provides access to the bucket's access control list.
- // This controls who can list, create or overwrite the objects in a bucket.
- // This call does not perform any network operations.
- func (c *BucketHandle) ACL() *ACLHandle {
- return c.acl
- }
- // DefaultObjectACL returns an ACLHandle, which provides access to the bucket's default object ACLs.
- // These ACLs are applied to newly created objects in this bucket that do not have a defined ACL.
- // This call does not perform any network operations.
- func (c *BucketHandle) DefaultObjectACL() *ACLHandle {
- return c.defaultObjectACL
- }
- // 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
- }
- // Object returns an ObjectHandle, which provides operations on the named object.
- // This call does not perform any network operations.
- //
- // name must consist entirely of valid UTF-8-encoded runes. The full specification
- // for valid object names can be found at:
- // https://cloud.google.com/storage/docs/bucket-naming
- func (b *BucketHandle) Object(name string) *ObjectHandle {
- return &ObjectHandle{
- c: b.c,
- bucket: b.name,
- object: name,
- acl: &ACLHandle{
- c: b.c,
- bucket: b.name,
- object: name,
- },
- }
- }
- // TODO(jbd): Add storage.buckets.list.
- // TODO(jbd): Add storage.buckets.update.
- // TODO(jbd): Add storage.objects.watch.
- // Attrs returns the metadata for the bucket.
- func (b *BucketHandle) Attrs(ctx context.Context) (*BucketAttrs, error) {
- resp, err := b.c.raw.Buckets.Get(b.name).Projection("full").Context(ctx).Do()
- if e, ok := err.(*googleapi.Error); ok && e.Code == http.StatusNotFound {
- return nil, ErrBucketNotExist
- }
- if err != nil {
- return nil, err
- }
- return newBucket(resp), nil
- }
- // List lists objects from the bucket. You can specify a query
- // to filter the results. If q is nil, no filtering is applied.
- func (b *BucketHandle) List(ctx context.Context, q *Query) (*ObjectList, error) {
- req := b.c.raw.Objects.List(b.name)
- req.Projection("full")
- if q != nil {
- req.Delimiter(q.Delimiter)
- req.Prefix(q.Prefix)
- req.Versions(q.Versions)
- req.PageToken(q.Cursor)
- if q.MaxResults > 0 {
- req.MaxResults(int64(q.MaxResults))
- }
- }
- resp, err := req.Context(ctx).Do()
- if err != nil {
- return nil, err
- }
- objects := &ObjectList{
- Results: make([]*ObjectAttrs, len(resp.Items)),
- Prefixes: make([]string, len(resp.Prefixes)),
- }
- for i, item := range resp.Items {
- objects.Results[i] = newObject(item)
- }
- for i, prefix := range resp.Prefixes {
- objects.Prefixes[i] = prefix
- }
- if resp.NextPageToken != "" {
- next := Query{}
- if q != nil {
- // keep the other filtering
- // criteria if there is a query
- next = *q
- }
- next.Cursor = resp.NextPageToken
- objects.Next = &next
- }
- return objects, nil
- }
- // 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.
- // Required.
- PrivateKey []byte
- // 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.
- // 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 extention headers the client must provide
- // in order to use the generated signed URL.
- // Optional.
- Headers []string
- // 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 []byte
- }
- // SignedURL returns a URL for the specified object. Signed URLs allow
- // the users access to a restricted resource for a limited time without having a
- // Google account or signing in. For more information about the signed
- // URLs, see https://cloud.google.com/storage/docs/accesscontrol#Signed-URLs.
- func SignedURL(bucket, name string, opts *SignedURLOptions) (string, error) {
- if opts == nil {
- return "", errors.New("storage: missing required SignedURLOptions")
- }
- if opts.GoogleAccessID == "" || opts.PrivateKey == nil {
- return "", errors.New("storage: missing required credentials to generate a signed URL")
- }
- if opts.Method == "" {
- return "", errors.New("storage: missing required method option")
- }
- if opts.Expires.IsZero() {
- return "", errors.New("storage: missing required expires option")
- }
- key, err := parseKey(opts.PrivateKey)
- if err != nil {
- return "", err
- }
- h := sha256.New()
- fmt.Fprintf(h, "%s\n", opts.Method)
- fmt.Fprintf(h, "%s\n", opts.MD5)
- fmt.Fprintf(h, "%s\n", opts.ContentType)
- fmt.Fprintf(h, "%d\n", opts.Expires.Unix())
- fmt.Fprintf(h, "%s", strings.Join(opts.Headers, "\n"))
- fmt.Fprintf(h, "/%s/%s", bucket, name)
- b, err := rsa.SignPKCS1v15(
- rand.Reader,
- key,
- crypto.SHA256,
- h.Sum(nil),
- )
- if err != nil {
- return "", err
- }
- encoded := base64.StdEncoding.EncodeToString(b)
- u := &url.URL{
- Scheme: "https",
- Host: "storage.googleapis.com",
- Path: fmt.Sprintf("/%s/%s", bucket, name),
- }
- 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
- }
- // 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
- }
- // Attrs returns meta information about the object.
- // ErrObjectNotExist will be returned if the object is not found.
- func (o *ObjectHandle) Attrs(ctx context.Context) (*ObjectAttrs, error) {
- if !utf8.ValidString(o.object) {
- return nil, fmt.Errorf("storage: object name %q is not valid UTF-8", o.object)
- }
- obj, err := o.c.raw.Objects.Get(o.bucket, o.object).Projection("full").Context(ctx).Do()
- if e, ok := err.(*googleapi.Error); ok && e.Code == http.StatusNotFound {
- return nil, ErrObjectNotExist
- }
- if err != nil {
- return nil, err
- }
- return newObject(obj), nil
- }
- // Update updates an object with the provided attributes.
- // All zero-value attributes are ignored.
- // ErrObjectNotExist will be returned if the object is not found.
- func (o *ObjectHandle) Update(ctx context.Context, attrs ObjectAttrs) (*ObjectAttrs, error) {
- if !utf8.ValidString(o.object) {
- return nil, fmt.Errorf("storage: object name %q is not valid UTF-8", o.object)
- }
- obj, err := o.c.raw.Objects.Patch(o.bucket, o.object, attrs.toRawObject(o.bucket)).Projection("full").Context(ctx).Do()
- if e, ok := err.(*googleapi.Error); ok && e.Code == http.StatusNotFound {
- return nil, ErrObjectNotExist
- }
- if err != nil {
- return nil, err
- }
- return newObject(obj), nil
- }
- // Delete deletes the single specified object.
- func (o *ObjectHandle) Delete(ctx context.Context) error {
- if !utf8.ValidString(o.object) {
- return fmt.Errorf("storage: object name %q is not valid UTF-8", o.object)
- }
- return o.c.raw.Objects.Delete(o.bucket, o.object).Context(ctx).Do()
- }
- // CopyObject copies the source object to the destination.
- // The copied object's attributes are overwritten by attrs if non-nil.
- func (c *Client) CopyObject(ctx context.Context, srcBucket, srcName string, destBucket, destName string, attrs *ObjectAttrs) (*ObjectAttrs, error) {
- if srcBucket == "" || destBucket == "" {
- return nil, errors.New("storage: srcBucket and destBucket must both be non-empty")
- }
- if srcName == "" || destName == "" {
- return nil, errors.New("storage: srcName and destName must be non-empty")
- }
- if !utf8.ValidString(srcName) {
- return nil, fmt.Errorf("storage: srcName %q is not valid UTF-8", srcName)
- }
- if !utf8.ValidString(destName) {
- return nil, fmt.Errorf("storage: destName %q is not valid UTF-8", destName)
- }
- var rawObject *raw.Object
- if attrs != nil {
- attrs.Name = destName
- if attrs.ContentType == "" {
- return nil, errors.New("storage: attrs.ContentType must be non-empty")
- }
- rawObject = attrs.toRawObject(destBucket)
- }
- o, err := c.raw.Objects.Copy(
- srcBucket, srcName, destBucket, destName, rawObject).Projection("full").Context(ctx).Do()
- if err != nil {
- return nil, err
- }
- return newObject(o), nil
- }
- // NewReader creates a new Reader to read the contents of the
- // object.
- // ErrObjectNotExist will be returned if the object is not found.
- func (o *ObjectHandle) NewReader(ctx context.Context) (*Reader, error) {
- if !utf8.ValidString(o.object) {
- return nil, fmt.Errorf("storage: object name %q is not valid UTF-8", o.object)
- }
- u := &url.URL{
- Scheme: "https",
- Host: "storage.googleapis.com",
- Path: fmt.Sprintf("/%s/%s", o.bucket, o.object),
- }
- res, err := o.c.hc.Get(u.String())
- if err != nil {
- return nil, err
- }
- if res.StatusCode == http.StatusNotFound {
- res.Body.Close()
- return nil, ErrObjectNotExist
- }
- if res.StatusCode < 200 || res.StatusCode > 299 {
- res.Body.Close()
- return nil, fmt.Errorf("storage: can't read object %v/%v, status code: %v", o.bucket, o.object, res.Status)
- }
- return &Reader{
- body: res.Body,
- size: res.ContentLength,
- contentType: res.Header.Get("Content-Type"),
- }, nil
- }
- // NewWriter returns a storage Writer that writes to the GCS object
- // associated with this ObjectHandle.
- // If such an object doesn't exist, it creates one.
- // Attributes can be set on the object by modifying the returned Writer's
- // ObjectAttrs field before the first call to Write.
- //
- // It is the caller's responsibility to call Close when writing is done.
- //
- // The object is not available and any previous object with the same
- // name is not replaced on Cloud Storage until Close is called.
- func (o *ObjectHandle) NewWriter(ctx context.Context) *Writer {
- return &Writer{
- ctx: ctx,
- client: o.c,
- bucket: o.bucket,
- name: o.object,
- donec: make(chan struct{}),
- ObjectAttrs: ObjectAttrs{Name: o.object},
- }
- }
- // 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 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
- }
|