• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2022 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5// This executable downloads, verifies, and uploads a given file to the Skia infra Bazel mirror.
6// Users should have gsutil installed, on the PATH and authenticated.
7// There are two modes of use:
8//   - Specify a single file via --url and --sha256.
9//   - Copy a JSON array of objects (or Starlark list of dictionaries) via standard in.
10// This should only need to be called when we add new dependencies or update existing ones. Calling
11// it with already archived files should be fine - the mirror is a CAS, so the update should be a
12// no-op. The files will be uploaded to the mirror with some metadata about where they came from.
13package main
14
15import (
16	"crypto/sha256"
17	"encoding/hex"
18	"flag"
19	"fmt"
20	"io"
21	"net/http"
22	"os"
23	"os/exec"
24	"path/filepath"
25	"strings"
26
27	"github.com/flynn/json5"
28
29	"go.skia.org/infra/go/skerr"
30)
31
32const (
33	gcsBucketAndPrefix = "gs://skia-world-readable/bazel/"
34)
35
36func main() {
37	var (
38		url           = flag.String("url", "", "The single url to mirror. --sha256 must be set.")
39		sha256Hash    = flag.String("sha256", "", "The sha256sum of the url to mirror. --url must also be set.")
40		jsonFromStdin = flag.Bool("json", false, "If set, read JSON from stdin that consists of a list of objects.")
41	)
42	flag.Parse()
43
44	if (*url != "" && *sha256Hash == "") || (*url == "" && *sha256Hash != "") {
45		flag.Usage()
46		fatalf("Must set both of or non of --url and --sha256")
47	} else if *url == "" && *sha256Hash == "" && !*jsonFromStdin {
48		fatalf("Must specify --url and --sha256 or --json")
49	}
50
51	workDir, err := os.MkdirTemp("", "bazel_gcs")
52	if err != nil {
53		fatalf("Could not make temp directory: %s", err)
54	}
55
56	if *jsonFromStdin {
57		fmt.Println("Waiting for input on std in. Use Ctrl+D (EOF) when done copying and pasting the array.")
58		b, err := io.ReadAll(os.Stdin)
59		if err != nil {
60			fatalf("Error while reading from stdin: %s", err)
61		}
62		if err := processJSON(workDir, b); err != nil {
63			fatalf("Could not process data from stdin: %s", err)
64		}
65	} else {
66		if err := processOne(workDir, *url, *sha256Hash); err != nil {
67			fatalf("Error while processing entry: %s", err)
68		}
69	}
70}
71
72type urlEntry struct {
73	SHA256 string `json:"sha256"`
74	URL    string `json:"url"`
75}
76
77func processJSON(workDir string, b []byte) error {
78	// We generally will be copying a list from Bazel files, written with Starlark (i.e. Pythonish).
79	// As a result, we need to turn the almost valid JSON array of objects into actually valid JSON.
80	// It is easier to just do string replacing rather than going line by line to remove the
81	// troublesome comments.
82	cleaned := fixStarlarkComments(b)
83	var entries []urlEntry
84	if err := json5.Unmarshal([]byte(cleaned), &entries); err != nil {
85		return skerr.Wrapf(err, "unmarshalling JSON")
86	}
87	for _, entry := range entries {
88		if err := processOne(workDir, entry.URL, entry.SHA256); err != nil {
89			return skerr.Wrapf(err, "while processing entry: %+v", entry)
90		}
91	}
92	return nil
93}
94
95// fixStarlarkComments replaces the Starlark comment symbol (#) with a JSON comment symbol (//).
96func fixStarlarkComments(b []byte) string {
97	return strings.ReplaceAll(string(b), "#", "//")
98}
99
100func processOne(workDir, url, hash string) error {
101	suf := getSuffix(url)
102	if suf == "" {
103		return skerr.Fmt("%s is not a supported file type", url)
104	}
105	fmt.Printf("Downloading and verifying %s...\n", url)
106	res, err := http.Get(url)
107	if err != nil {
108		return skerr.Wrapf(err, "downloading %s", url)
109	}
110	contents, err := io.ReadAll(res.Body)
111	if err != nil {
112		return skerr.Wrapf(err, "reading %s", url)
113	}
114	if err := res.Body.Close(); err != nil {
115		return skerr.Wrapf(err, "after reading %s", url)
116	}
117	// Verify
118	h := sha256.Sum256(contents)
119	if actual := hex.EncodeToString(h[:]); actual != hash {
120		return skerr.Fmt("Invalid hash of %s. %s != %s", url, actual, hash)
121	}
122	fmt.Printf("Uploading %s to GCS...\n", url)
123	// Write to disk so gsutil can access it
124	tmpFile := filepath.Join(workDir, hash+suf)
125	if err := os.WriteFile(tmpFile, contents, 0644); err != nil {
126		return skerr.Wrapf(err, "writing %d bytes to %s", len(contents), tmpFile)
127	}
128	// Upload using gsutil (which is assumed to be properly authed)
129	cmd := exec.Command("gsutil",
130		// Add custom metadata so we can figure out what the unrecognizable file name was created
131		// from. Custom metadata values must start with x-goog-meta-
132		"-h", "x-goog-meta-original-url:"+url,
133		"cp", tmpFile, gcsBucketAndPrefix+hash+suf)
134	cmd.Stdout = os.Stdout
135	cmd.Stderr = os.Stderr
136	return skerr.Wrapf(cmd.Run(), "uploading %s to GCS", tmpFile)
137}
138
139var supportedSuffixes = []string{".tar.gz", ".tar.xz", ".deb"}
140
141// getSuffix returns the filetype suffix of the file if it is in the list of supported suffixes.
142// Otherwise, it returns empty string.
143func getSuffix(url string) string {
144	for _, suf := range supportedSuffixes {
145		if strings.HasSuffix(url, suf) {
146			return suf
147		}
148	}
149	return ""
150}
151
152func fatalf(format string, args ...interface{}) {
153	// Ensure there is a newline at the end of the fatal message.
154	format = strings.TrimSuffix(format, "\n") + "\n"
155	fmt.Printf(format, args...)
156	os.Exit(1)
157}
158