blob: fe2e1ee0457ab246d29df4a47b2b261e1c560f57 [file] [log] [blame]
package goldclient
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"os/exec"
"runtime"
"strings"
gstorage "cloud.google.com/go/storage"
"go.skia.org/infra/go/gcs"
"go.skia.org/infra/go/skerr"
"google.golang.org/api/option"
)
const (
// GCS_PREFIX is the expected prefix for a GCS URL.
GCS_PREFIX = "gs://"
)
// GoldUploader implementations provide functions to upload to GCS.
type GoldUploader interface {
// copy copies a local file to GCS. If data is provided, those
// bytes may be used instead of read again from disk.
// The dst string is assumed to have a gs:// prefix.
// Currently only uploading from a local file to GCS is supported, that is
// one cannot use gs://foo/bar as 'fileName'
UploadBytes(data []byte, fileName, dst string) error
// UploadJSON serializes the given data to JSON and uploads the result to GCS.
// An implementation can use tempFileName for temporary storage of JSON data.
UploadJSON(data interface{}, tempFileName, gcsObjectPath string) error
}
// gsutilUploader implements the GoldUploader interface.
type gsutilUploader struct{}
// gsUtilUploadJson serializes the given data to JSON and writes the result to the given
// tempFileName, then it copies the file to the given path in GCS. gcsObjPath is assumed
// to have the form: <bucket_name>/path/to/object
func (g *gsutilUploader) UploadJSON(data interface{}, tempFileName, gcsObjPath string) error {
jsonBytes, err := json.Marshal(data)
if err != nil {
return err
}
if err := ioutil.WriteFile(tempFileName, jsonBytes, 0644); err != nil {
return err
}
// Upload the written file.
return g.UploadBytes(nil, tempFileName, prefixGCS(gcsObjPath))
}
// prefixGCS adds the "gs://" prefix to the given GCS path.
func prefixGCS(gcsPath string) string {
return fmt.Sprintf(GCS_PREFIX+"%s", gcsPath)
}
// gsutilCopy shells out to gsutil to copy the given src to the given target. A path
// starting with "gs://" is assumed to be in GCS.
func (g *gsutilUploader) UploadBytes(data []byte, fileName, dst string) error {
runCmd := exec.Command("gsutil", "cp", fileName, dst)
outBytes, err := runCmd.CombinedOutput()
if err != nil {
if runtime.GOOS == "windows" {
runCmd = exec.Command("python", "gsutil.py", "cp", fileName, dst)
outBytes, err = runCmd.CombinedOutput()
if err != nil {
return skerr.Fmt("Error running gsutil. Got output \n%s\n and error: %s", outBytes, err)
}
} else {
return skerr.Fmt("Error running gsutil. Got output \n%s\n and error: %s", outBytes, err)
}
}
return nil
}
// httpUploader implements the GoldUploader interface using an authenticated (via an OAuth service
// account) http client.
type httpUploader struct {
client *gstorage.Client
}
func newHttpUploader(ctx context.Context, httpClient *http.Client) (GoldUploader, error) {
ret := &httpUploader{}
var err error
ret.client, err = gstorage.NewClient(ctx, option.WithHTTPClient(httpClient))
if err != nil {
return nil, skerr.Fmt("Error instantiating storage client: %s", err)
}
return ret, nil
}
func (h *httpUploader) UploadBytes(data []byte, fallbackSrc, dst string) error {
if len(data) == 0 {
if strings.HasPrefix(fallbackSrc, GCS_PREFIX) {
return skerr.Fmt("Copying from a remote file is not supported")
}
var err error
data, err = ioutil.ReadFile(fallbackSrc)
if err != nil {
return skerr.Fmt("Error reading file %s: %s", fallbackSrc, err)
}
}
return h.copyBytes(data, dst)
}
func (h *httpUploader) UploadJSON(data interface{}, tempFileName, gcsObjectPath string) error {
jsonBytes, err := json.Marshal(data)
if err != nil {
return err
}
return h.copyBytes(jsonBytes, gcsObjectPath)
}
func (h *httpUploader) copyBytes(data []byte, dst string) error {
// Trim the prefix and upload the content to the cloud.
dst = strings.TrimPrefix(dst, GCS_PREFIX)
bucket, objPath := gcs.SplitGSPath(dst)
handle := h.client.Bucket(bucket).Object(objPath)
// TODO(kjlubick): Check if the file exists before-hand and skip uploading unless
// force is set. This could remove the need to read known_hashes
w := handle.NewWriter(context.Background())
_, err := w.Write(data)
if err != nil {
_ = w.CloseWithError(err) // Always returns nil, according to docs.
return err
}
return w.Close()
}
// dryRunUploader implements the GoldUploader interface (but doesn't
// actually upload anything)
type dryRunUploader struct{}
func (h *dryRunUploader) UploadBytes(data []byte, fallbackSrc, dst string) error {
fmt.Printf("dryrun -- upload bytes from %s to %s\n", fallbackSrc, dst)
return nil
}
func (h *dryRunUploader) UploadJSON(data interface{}, tempFileName, gcsObjectPath string) error {
fmt.Printf("dryrun -- upload JSON from %s to %s\n", tempFileName, gcsObjectPath)
return nil
}