github.com/google/syzkaller@v0.0.0-20251211124644-a066d2bc4b02/pkg/gcs/gcs.go (about)

     1  // Copyright 2017 syzkaller project authors. All rights reserved.
     2  // Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
     3  
     4  // Package gcs provides wrappers around Google Cloud Storage (GCS) APIs.
     5  // Package uses Application Default Credentials assuming that the program runs on GCE.
     6  //
     7  // See the following links for details and API reference:
     8  // https://cloud.google.com/go/getting-started/using-cloud-storage
     9  // https://godoc.org/cloud.google.com/go/storage
    10  
    11  package gcs
    12  
    13  import (
    14  	"context"
    15  	"errors"
    16  	"fmt"
    17  	"io"
    18  	"strings"
    19  	"time"
    20  
    21  	"cloud.google.com/go/storage"
    22  	"google.golang.org/api/iterator"
    23  )
    24  
    25  type Client interface {
    26  	Close() error
    27  	FileReader(path string) (io.ReadCloser, error)
    28  	FileWriter(path string, contentType string, contentEncoding string) (io.WriteCloser, error)
    29  	DeleteFile(path string) error
    30  	FileExists(path string) (bool, error)
    31  	ListObjects(path string) ([]*Object, error)
    32  
    33  	Publish(path string) error
    34  }
    35  
    36  type UploadOptions struct {
    37  	Publish         bool
    38  	ContentEncoding string
    39  	GCSClientMock   Client
    40  }
    41  
    42  func UploadFile(ctx context.Context, srcFile io.Reader, destURL string, opts UploadOptions) error {
    43  	destURL = strings.TrimPrefix(destURL, "gs://")
    44  	var err error
    45  	gcsClient := opts.GCSClientMock
    46  	if gcsClient == nil {
    47  		if gcsClient, err = NewClient(ctx); err != nil {
    48  			return fmt.Errorf("func NewClient: %w", err)
    49  		}
    50  	}
    51  	defer gcsClient.Close()
    52  	gcsWriter, err := gcsClient.FileWriter(destURL, "", opts.ContentEncoding)
    53  	if err != nil {
    54  		return fmt.Errorf("client.FileWriter: %w", err)
    55  	}
    56  	if _, err := io.Copy(gcsWriter, srcFile); err != nil {
    57  		gcsWriter.Close()
    58  		return fmt.Errorf("io.Copy: %w", err)
    59  	}
    60  	if err := gcsWriter.Close(); err != nil {
    61  		return fmt.Errorf("gcsWriter.Close: %w", err)
    62  	}
    63  	if opts.Publish {
    64  		return gcsClient.Publish(destURL)
    65  	}
    66  	return nil
    67  }
    68  
    69  type client struct {
    70  	client *storage.Client
    71  	ctx    context.Context
    72  }
    73  
    74  func NewClient(ctx context.Context) (Client, error) {
    75  	storageClient, err := storage.NewClient(ctx)
    76  	if err != nil {
    77  		return nil, err
    78  	}
    79  	c := &client{
    80  		client: storageClient,
    81  		ctx:    ctx,
    82  	}
    83  	return c, nil
    84  }
    85  
    86  func (c *client) Close() error {
    87  	return c.client.Close()
    88  }
    89  
    90  func (c *client) FileReader(gcsFile string) (io.ReadCloser, error) {
    91  	bucket, filename, err := split(gcsFile)
    92  	if err != nil {
    93  		return nil, err
    94  	}
    95  	bkt := c.client.Bucket(bucket)
    96  	f := bkt.Object(filename)
    97  	attrs, err := f.Attrs(c.ctx)
    98  	if err != nil {
    99  		return nil, fmt.Errorf("failed to read %v attributes: %w", gcsFile, err)
   100  	}
   101  	if !attrs.Deleted.IsZero() {
   102  		return nil, fmt.Errorf("file %v is deleted", gcsFile)
   103  	}
   104  	handle := f.If(storage.Conditions{
   105  		GenerationMatch:     attrs.Generation,
   106  		MetagenerationMatch: attrs.Metageneration,
   107  	})
   108  	return handle.NewReader(c.ctx)
   109  }
   110  
   111  func (c *client) FileWriter(gcsFile, contentType, contentEncoding string) (io.WriteCloser, error) {
   112  	bucket, filename, err := split(gcsFile)
   113  	if err != nil {
   114  		return nil, err
   115  	}
   116  	bkt := c.client.Bucket(bucket)
   117  	f := bkt.Object(filename)
   118  	w := f.NewWriter(c.ctx)
   119  	if contentType != "" {
   120  		w.ContentType = contentType
   121  	}
   122  	if contentEncoding != "" {
   123  		w.ContentEncoding = contentEncoding
   124  	}
   125  	return w, nil
   126  }
   127  
   128  // Publish lets any user read gcsFile.
   129  func (c *client) Publish(gcsFile string) error {
   130  	bucket, filename, err := split(gcsFile)
   131  	if err != nil {
   132  		return err
   133  	}
   134  	obj := c.client.Bucket(bucket).Object(filename)
   135  	return obj.ACL().Set(c.ctx, storage.AllUsers, storage.RoleReader)
   136  }
   137  
   138  var ErrFileNotFound = errors.New("the requested files does not exist")
   139  
   140  func (c *client) DeleteFile(gcsFile string) error {
   141  	bucket, filename, err := split(gcsFile)
   142  	if err != nil {
   143  		return err
   144  	}
   145  	err = c.client.Bucket(bucket).Object(filename).Delete(c.ctx)
   146  	if errors.Is(err, storage.ErrObjectNotExist) {
   147  		return ErrFileNotFound
   148  	}
   149  	return err
   150  }
   151  
   152  func (c *client) FileExists(gcsFile string) (bool, error) {
   153  	bucket, filename, err := split(gcsFile)
   154  	if err != nil {
   155  		return false, err
   156  	}
   157  	_, err = c.client.Bucket(bucket).Object(filename).Attrs(c.ctx)
   158  	if errors.Is(err, storage.ErrObjectNotExist) {
   159  		return false, nil
   160  	} else if err != nil {
   161  		return false, err
   162  	}
   163  	return true, nil
   164  }
   165  
   166  // Where things get published.
   167  const (
   168  	PublicPrefix        = "https://storage.googleapis.com/"
   169  	AuthenticatedPrefix = "https://storage.cloud.google.com/"
   170  )
   171  
   172  func GetDownloadURL(gcsFile string, publicURL bool) string {
   173  	gcsFile = strings.TrimPrefix(gcsFile, "/")
   174  	if publicURL {
   175  		return PublicPrefix + gcsFile
   176  	}
   177  	return AuthenticatedPrefix + gcsFile
   178  }
   179  
   180  type Object struct {
   181  	Path      string
   182  	CreatedAt time.Time
   183  }
   184  
   185  // ListObjects expects "bucket/path" or "bucket" as input.
   186  func (c *client) ListObjects(bucketObjectPath string) ([]*Object, error) {
   187  	bucket, objectPath, err := split(bucketObjectPath)
   188  	if err != nil { // no path specified
   189  		bucket = bucketObjectPath
   190  	}
   191  	query := &storage.Query{Prefix: objectPath}
   192  	it := c.client.Bucket(bucket).Objects(c.ctx, query)
   193  	ret := []*Object{}
   194  	for {
   195  		objAttrs, err := it.Next()
   196  		if err == iterator.Done {
   197  			break
   198  		}
   199  		if err != nil {
   200  			return nil, fmt.Errorf("failed to query GCS objects: %w", err)
   201  		}
   202  		ret = append(ret, &Object{
   203  			Path:      objAttrs.Name,
   204  			CreatedAt: objAttrs.Created,
   205  		})
   206  	}
   207  	return ret, nil
   208  }
   209  
   210  func split(file string) (bucket, filename string, err error) {
   211  	pos := strings.IndexByte(file, '/')
   212  	if pos == -1 {
   213  		return "", "", fmt.Errorf("invalid GCS file name: %v", file)
   214  	}
   215  	return file[:pos], file[pos+1:], nil
   216  }