| 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 |
| } |