mirror of
https://github.com/grafana/grafana.git
synced 2024-11-29 04:04:00 -06:00
062d255124
* replace ioutil.ReadFile -> os.ReadFile * replace ioutil.ReadAll -> io.ReadAll * replace ioutil.TempFile -> os.CreateTemp * replace ioutil.NopCloser -> io.NopCloser * replace ioutil.WriteFile -> os.WriteFile * replace ioutil.TempDir -> os.MkdirTemp * replace ioutil.Discard -> io.Discard
230 lines
5.8 KiB
Go
230 lines
5.8 KiB
Go
// Package gcs provides an image uploader for GCS.
|
|
package gcs
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"cloud.google.com/go/storage"
|
|
"github.com/grafana/grafana/pkg/ifaces/gcsifaces"
|
|
"github.com/grafana/grafana/pkg/infra/log"
|
|
"github.com/grafana/grafana/pkg/util"
|
|
"golang.org/x/oauth2/google"
|
|
"golang.org/x/oauth2/jwt"
|
|
"google.golang.org/api/option"
|
|
)
|
|
|
|
// NewUploader returns a new Uploader.
|
|
func NewUploader(keyFile, bucket, path string, enableSignedURLs bool, signedURLExpiration time.Duration) (*Uploader, error) {
|
|
if signedURLExpiration <= 0 {
|
|
return nil, fmt.Errorf("invalid signed URL expiration: %q", signedURLExpiration)
|
|
}
|
|
uploader := &Uploader{
|
|
KeyFile: keyFile,
|
|
Bucket: bucket,
|
|
path: path,
|
|
log: log.New("gcsuploader"),
|
|
enableSignedURLs: enableSignedURLs,
|
|
signedURLExpiration: signedURLExpiration,
|
|
}
|
|
|
|
uploader.log.Debug("Created uploader", "key", keyFile, "bucket", bucket, "path", path, "enableSignedUrls",
|
|
enableSignedURLs, "signedUrlExpiration", signedURLExpiration.String())
|
|
|
|
return uploader, nil
|
|
}
|
|
|
|
// newClient returns a new GCS client.
|
|
// Stubbable by tests.
|
|
var newClient = func(ctx context.Context, opts ...option.ClientOption) (gcsifaces.StorageClient, error) {
|
|
client, err := storage.NewClient(ctx, opts...)
|
|
return clientWrapper{client}, err
|
|
}
|
|
|
|
// Uploader supports uploading images to GCS.
|
|
type Uploader struct {
|
|
KeyFile string
|
|
Bucket string
|
|
path string
|
|
log log.Logger
|
|
enableSignedURLs bool
|
|
signedURLExpiration time.Duration
|
|
}
|
|
|
|
// Upload uploads an image to GCS.
|
|
func (u *Uploader) Upload(ctx context.Context, imageDiskPath string) (string, error) {
|
|
fileName, err := util.GetRandomString(20)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
ext := filepath.Ext(imageDiskPath)
|
|
if ext == "" {
|
|
ext = ".png"
|
|
}
|
|
fileName += ext
|
|
|
|
key := path.Join(u.path, fileName)
|
|
|
|
var keyData []byte
|
|
if u.KeyFile != "" {
|
|
u.log.Debug("Opening key file ", u.KeyFile)
|
|
keyData, err = os.ReadFile(u.KeyFile)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
|
|
const scope = storage.ScopeReadWrite
|
|
|
|
var client gcsifaces.StorageClient
|
|
if u.KeyFile != "" {
|
|
u.log.Debug("Creating Google credentials from JSON")
|
|
creds, err := google.CredentialsFromJSON(ctx, keyData, scope)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
u.log.Debug("Creating GCS client")
|
|
client, err = newClient(ctx, option.WithCredentials(creds))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
} else {
|
|
u.log.Debug("Creating GCS client with default application credentials")
|
|
client, err = newClient(ctx, option.WithScopes(scope))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
|
|
if err := u.uploadFile(ctx, client, imageDiskPath, key); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if !u.enableSignedURLs {
|
|
return fmt.Sprintf("https://storage.googleapis.com/%s/%s", u.Bucket, key), nil
|
|
}
|
|
|
|
u.log.Debug("Signing GCS URL")
|
|
var jwtData []byte
|
|
if u.KeyFile != "" {
|
|
jwtData = keyData
|
|
} else {
|
|
creds, err := client.FindDefaultCredentials(ctx, scope)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to find default Google credentials: %s", err)
|
|
}
|
|
jwtData = creds.JSON
|
|
}
|
|
conf, err := client.JWTConfigFromJSON(jwtData)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
opts := &storage.SignedURLOptions{
|
|
Scheme: storage.SigningSchemeV4,
|
|
Method: "GET",
|
|
GoogleAccessID: conf.Email,
|
|
PrivateKey: conf.PrivateKey,
|
|
Expires: time.Now().Add(u.signedURLExpiration),
|
|
}
|
|
signedURL, err := client.SignedURL(u.Bucket, key, opts)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return signedURL, nil
|
|
}
|
|
|
|
func (u *Uploader) uploadFile(
|
|
ctx context.Context,
|
|
client gcsifaces.StorageClient,
|
|
imageDiskPath,
|
|
key string,
|
|
) error {
|
|
u.log.Debug("Opening image file", "path", imageDiskPath)
|
|
|
|
// We can ignore the gosec G304 warning on this one because `imageDiskPath` comes
|
|
// from alert notifiers and is only used to upload images generated by alerting.
|
|
// nolint:gosec
|
|
fileReader, err := os.Open(imageDiskPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() {
|
|
if err := fileReader.Close(); err != nil {
|
|
u.log.Warn("Failed to close file", "err", err, "path", imageDiskPath)
|
|
}
|
|
}()
|
|
|
|
// Set public access if not generating a signed URL
|
|
pubAcc := !u.enableSignedURLs
|
|
|
|
u.log.Debug("Uploading to GCS bucket using SDK", "bucket", u.Bucket, "key", key, "public", pubAcc)
|
|
|
|
uri := fmt.Sprintf("gs://%s/%s", u.Bucket, key)
|
|
|
|
wc := client.Bucket(u.Bucket).Object(key).NewWriter(ctx)
|
|
if pubAcc {
|
|
wc.SetACL("publicRead")
|
|
}
|
|
if _, err := io.Copy(wc, fileReader); err != nil {
|
|
_ = wc.Close()
|
|
return fmt.Errorf("failed to upload to %s: %s", uri, err)
|
|
}
|
|
if err := wc.Close(); err != nil {
|
|
return fmt.Errorf("failed to upload to %s: %s", uri, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type clientWrapper struct {
|
|
client *storage.Client
|
|
}
|
|
|
|
func (c clientWrapper) Bucket(key string) gcsifaces.StorageBucket {
|
|
return bucketWrapper{c.client.Bucket(key)}
|
|
}
|
|
|
|
func (c clientWrapper) FindDefaultCredentials(ctx context.Context, scope string) (*google.Credentials, error) {
|
|
return google.FindDefaultCredentials(ctx, scope)
|
|
}
|
|
|
|
func (c clientWrapper) JWTConfigFromJSON(keyJSON []byte) (*jwt.Config, error) {
|
|
return google.JWTConfigFromJSON(keyJSON)
|
|
}
|
|
|
|
func (c clientWrapper) SignedURL(bucket, name string, opts *storage.SignedURLOptions) (string, error) {
|
|
return storage.SignedURL(bucket, name, opts)
|
|
}
|
|
|
|
type bucketWrapper struct {
|
|
bucket *storage.BucketHandle
|
|
}
|
|
|
|
func (b bucketWrapper) Object(key string) gcsifaces.StorageObject {
|
|
return objectWrapper{b.bucket.Object(key)}
|
|
}
|
|
|
|
type objectWrapper struct {
|
|
object *storage.ObjectHandle
|
|
}
|
|
|
|
func (o objectWrapper) NewWriter(ctx context.Context) gcsifaces.StorageWriter {
|
|
return writerWrapper{o.object.NewWriter(ctx)}
|
|
}
|
|
|
|
type writerWrapper struct {
|
|
*storage.Writer
|
|
}
|
|
|
|
func (w writerWrapper) SetACL(acl string) {
|
|
w.ObjectAttrs.PredefinedACL = acl
|
|
}
|