xref: /aosp_15_r20/external/skia/bazel/gcs_mirror/gcs_mirror.go (revision c8dee2aa9b3f27cf6c858bd81872bdeb2c07ed17)
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//
11// This should only need to be called when we add new dependencies or update existing ones. Calling
12// it with already archived files should be fine - the mirror is a CAS, so the update should be a
13// no-op. The files will be uploaded to the mirror with some metadata about where they came from.
14package main
15
16import (
17	"crypto/sha256"
18	"encoding/hex"
19	"flag"
20	"fmt"
21	"io"
22	"net/http"
23	"os"
24	"os/exec"
25	"path/filepath"
26	"strings"
27
28	"github.com/flynn/json5"
29
30	"go.skia.org/infra/go/skerr"
31)
32
33const (
34	gcsBucketAndPrefix = "gs://skia-world-readable/bazel/"
35)
36
37func main() {
38	var (
39		file          = flag.String("file", "", "A local file on disk to upload. --sha256 must be set.")
40		url           = flag.String("url", "", "The single url to mirror. --sha256 must be set.")
41		sha256Hash    = flag.String("sha256", "", "The sha256sum of the url to mirror. --url must also be set.")
42		jsonFromStdin = flag.Bool("json", false, "If set, read JSON from stdin that consists of a list of objects.")
43		noSuffix      = flag.Bool("no_suffix", false, "If true, this is presumed to be a binary which needs no suffix (e.g. executable)")
44		addSuffix     = flag.String("add_suffix", "", "If set, this will be the suffix of the file uploaded")
45	)
46	flag.Parse()
47
48	if (*file != "" && *sha256Hash != "") || (*url != "" && *sha256Hash != "") {
49		// ok
50	} else if *jsonFromStdin {
51		// ok
52	} else {
53		flag.Usage()
54		fatalf("Must specify --url/--file and --sha256 or --json")
55	}
56
57	workDir, err := os.MkdirTemp("", "bazel_gcs")
58	if err != nil {
59		fatalf("Could not make temp directory: %s", err)
60	}
61
62	if *jsonFromStdin {
63		fmt.Println("Waiting for input on std in. Use Ctrl+D (EOF) when done copying and pasting the array.")
64		b, err := io.ReadAll(os.Stdin)
65		if err != nil {
66			fatalf("Error while reading from stdin: %s", err)
67		}
68		if err := processJSON(workDir, b); err != nil {
69			fatalf("Could not process data from stdin: %s", err)
70		}
71	} else if *url != "" {
72		if err := processOneDownload(workDir, *url, *sha256Hash, *addSuffix, *noSuffix); err != nil {
73			fatalf("Error while processing entry: %s", err)
74		}
75		fmt.Printf("https://storage.googleapis.com/skia-world-readable/bazel/%s%s%s\n", *sha256Hash, getSuffix(*url), *addSuffix)
76	} else {
77		if err := processOneLocalFile(*file, *sha256Hash); err != nil {
78			fatalf("Error while processing entry: %s", err)
79		}
80		fmt.Printf("https://storage.googleapis.com/skia-world-readable/bazel/%s%s\n", *sha256Hash, getSuffix(*file))
81	}
82}
83
84type urlEntry struct {
85	SHA256 string `json:"sha256"`
86	URL    string `json:"url"`
87}
88
89func processJSON(workDir string, b []byte) error {
90	// We generally will be copying a list from Bazel files, written with Starlark (i.e. Pythonish).
91	// As a result, we need to turn the almost valid JSON array of objects into actually valid JSON.
92	// It is easier to just do string replacing rather than going line by line to remove the
93	// troublesome comments.
94	cleaned := fixStarlarkComments(b)
95	var entries []urlEntry
96	if err := json5.Unmarshal([]byte(cleaned), &entries); err != nil {
97		return skerr.Wrapf(err, "unmarshalling JSON")
98	}
99	for _, entry := range entries {
100		if err := processOneDownload(workDir, entry.URL, entry.SHA256, "", false); err != nil {
101			return skerr.Wrapf(err, "while processing entry: %+v", entry)
102		}
103	}
104	return nil
105}
106
107// fixStarlarkComments replaces the Starlark comment symbol (#) with a JSON comment symbol (//).
108func fixStarlarkComments(b []byte) string {
109	return strings.ReplaceAll(string(b), "#", "//")
110}
111
112func processOneDownload(workDir, url, hash, addSuffix string, noSuffix bool) error {
113	suf := getSuffix(url) + addSuffix
114	if !noSuffix && suf == "" {
115		return skerr.Fmt("%s is not a supported file type", url)
116	}
117	fmt.Printf("Downloading and verifying %s...\n", url)
118	res, err := http.Get(url)
119	if err != nil {
120		return skerr.Wrapf(err, "downloading %s", url)
121	}
122	contents, err := io.ReadAll(res.Body)
123	if err != nil {
124		return skerr.Wrapf(err, "reading %s", url)
125	}
126	if err := res.Body.Close(); err != nil {
127		return skerr.Wrapf(err, "after reading %s", url)
128	}
129	// Verify
130	h := sha256.Sum256(contents)
131	if actual := hex.EncodeToString(h[:]); actual != hash {
132		return skerr.Fmt("Invalid hash of %s. %s != %s", url, actual, hash)
133	}
134	fmt.Printf("Uploading %s to GCS...\n", url)
135	// Write to disk so gsutil can access it
136	tmpFile := filepath.Join(workDir, hash+suf)
137	if err := os.WriteFile(tmpFile, contents, 0644); err != nil {
138		return skerr.Wrapf(err, "writing %d bytes to %s", len(contents), tmpFile)
139	}
140	// Upload using gsutil (which is assumed to be properly authed)
141	cmd := exec.Command("gsutil",
142		// Add custom metadata so we can figure out what the unrecognizable file name was created
143		// from. Custom metadata values must start with x-goog-meta-
144		"-h", "x-goog-meta-original-url:"+url,
145		"cp", tmpFile, gcsBucketAndPrefix+hash+suf)
146	cmd.Stdout = os.Stdout
147	cmd.Stderr = os.Stderr
148	return skerr.Wrapf(cmd.Run(), "uploading %s to GCS", tmpFile)
149}
150
151func processOneLocalFile(file, hash string) error {
152	file, err := filepath.Abs(file)
153	if err != nil {
154		return skerr.Wrap(err)
155	}
156	suf := getSuffix(file)
157	if suf == "" {
158		return skerr.Fmt("%s is not a supported file type", file)
159	}
160	contents, err := os.ReadFile(file)
161	if err != nil {
162		return skerr.Wrapf(err, "reading %s", file)
163	}
164	// Verify
165	h := sha256.Sum256(contents)
166	if actual := hex.EncodeToString(h[:]); actual != hash {
167		return skerr.Fmt("Invalid hash of %s. %s != %s", file, actual, hash)
168	}
169	fmt.Printf("Uploading %s to GCS...\n", file)
170	// Upload using gsutil (which is assumed to be properly authed)
171	cmd := exec.Command("gsutil",
172		// Add custom metadata so we can figure out what the unrecognizable file name was created
173		// from. Custom metadata values must start with x-goog-meta-
174		"-h", "x-goog-meta-original-file:"+file,
175		"cp", file, gcsBucketAndPrefix+hash+suf)
176	cmd.Stdout = os.Stdout
177	cmd.Stderr = os.Stderr
178	return skerr.Wrapf(cmd.Run(), "uploading %s to GCS", file)
179}
180
181var supportedSuffixes = []string{".tar.gz", ".tgz", ".tar.xz", ".deb", ".zip"}
182
183// getSuffix returns the filetype suffix of the file if it is in the list of supported suffixes.
184// Otherwise, it returns empty string.
185func getSuffix(url string) string {
186	for _, suf := range supportedSuffixes {
187		if strings.HasSuffix(url, suf) {
188			return suf
189		}
190	}
191	return ""
192}
193
194func fatalf(format string, args ...interface{}) {
195	// Ensure there is a newline at the end of the fatal message.
196	format = strings.TrimSuffix(format, "\n") + "\n"
197	fmt.Printf(format, args...)
198	os.Exit(1)
199}
200