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