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 }