github.com/10XDev/rclone@v1.52.3-0.20200626220027-16af9ab76b2a/bin/get-github-release.go (about) 1 // +build ignore 2 3 // Get the latest release from a github project 4 // 5 // If GITHUB_USER and GITHUB_TOKEN are set then these will be used to 6 // authenticate the request which is useful to avoid rate limits. 7 8 package main 9 10 import ( 11 "archive/tar" 12 "compress/bzip2" 13 "compress/gzip" 14 "encoding/json" 15 "flag" 16 "fmt" 17 "io" 18 "io/ioutil" 19 "log" 20 "net/http" 21 "net/url" 22 "os" 23 "os/exec" 24 "path" 25 "path/filepath" 26 "regexp" 27 "runtime" 28 "strings" 29 "time" 30 31 "github.com/rclone/rclone/lib/rest" 32 "golang.org/x/net/html" 33 "golang.org/x/sys/unix" 34 ) 35 36 var ( 37 // Flags 38 install = flag.Bool("install", false, "Install the downloaded package using sudo dpkg -i.") 39 extract = flag.String("extract", "", "Extract the named executable from the .tar.gz and install into bindir.") 40 bindir = flag.String("bindir", defaultBinDir(), "Directory to install files downloaded with -extract.") 41 useAPI = flag.Bool("use-api", false, "Use the API for finding the release instead of scraping the page.") 42 // Globals 43 matchProject = regexp.MustCompile(`^([\w-]+)/([\w-]+)$`) 44 osAliases = map[string][]string{ 45 "darwin": []string{"macos", "osx"}, 46 } 47 archAliases = map[string][]string{ 48 "amd64": []string{"x86_64"}, 49 } 50 ) 51 52 // A github release 53 // 54 // Made by pasting the JSON into https://mholt.github.io/json-to-go/ 55 type Release struct { 56 URL string `json:"url"` 57 AssetsURL string `json:"assets_url"` 58 UploadURL string `json:"upload_url"` 59 HTMLURL string `json:"html_url"` 60 ID int `json:"id"` 61 TagName string `json:"tag_name"` 62 TargetCommitish string `json:"target_commitish"` 63 Name string `json:"name"` 64 Draft bool `json:"draft"` 65 Author struct { 66 Login string `json:"login"` 67 ID int `json:"id"` 68 AvatarURL string `json:"avatar_url"` 69 GravatarID string `json:"gravatar_id"` 70 URL string `json:"url"` 71 HTMLURL string `json:"html_url"` 72 FollowersURL string `json:"followers_url"` 73 FollowingURL string `json:"following_url"` 74 GistsURL string `json:"gists_url"` 75 StarredURL string `json:"starred_url"` 76 SubscriptionsURL string `json:"subscriptions_url"` 77 OrganizationsURL string `json:"organizations_url"` 78 ReposURL string `json:"repos_url"` 79 EventsURL string `json:"events_url"` 80 ReceivedEventsURL string `json:"received_events_url"` 81 Type string `json:"type"` 82 SiteAdmin bool `json:"site_admin"` 83 } `json:"author"` 84 Prerelease bool `json:"prerelease"` 85 CreatedAt time.Time `json:"created_at"` 86 PublishedAt time.Time `json:"published_at"` 87 Assets []struct { 88 URL string `json:"url"` 89 ID int `json:"id"` 90 Name string `json:"name"` 91 Label string `json:"label"` 92 Uploader struct { 93 Login string `json:"login"` 94 ID int `json:"id"` 95 AvatarURL string `json:"avatar_url"` 96 GravatarID string `json:"gravatar_id"` 97 URL string `json:"url"` 98 HTMLURL string `json:"html_url"` 99 FollowersURL string `json:"followers_url"` 100 FollowingURL string `json:"following_url"` 101 GistsURL string `json:"gists_url"` 102 StarredURL string `json:"starred_url"` 103 SubscriptionsURL string `json:"subscriptions_url"` 104 OrganizationsURL string `json:"organizations_url"` 105 ReposURL string `json:"repos_url"` 106 EventsURL string `json:"events_url"` 107 ReceivedEventsURL string `json:"received_events_url"` 108 Type string `json:"type"` 109 SiteAdmin bool `json:"site_admin"` 110 } `json:"uploader"` 111 ContentType string `json:"content_type"` 112 State string `json:"state"` 113 Size int `json:"size"` 114 DownloadCount int `json:"download_count"` 115 CreatedAt time.Time `json:"created_at"` 116 UpdatedAt time.Time `json:"updated_at"` 117 BrowserDownloadURL string `json:"browser_download_url"` 118 } `json:"assets"` 119 TarballURL string `json:"tarball_url"` 120 ZipballURL string `json:"zipball_url"` 121 Body string `json:"body"` 122 } 123 124 // checks if a path has write access 125 func writable(path string) bool { 126 return unix.Access(path, unix.W_OK) == nil 127 } 128 129 // Directory to install releases in by default 130 // 131 // Find writable directories on $PATH. Use $GOPATH/bin if that is on 132 // the path and writable or use the first writable directory which is 133 // in $HOME or failing that the first writable directory. 134 // 135 // Returns "" if none of the above were found 136 func defaultBinDir() string { 137 home := os.Getenv("HOME") 138 var ( 139 bin string 140 homeBin string 141 goHomeBin string 142 gopath = os.Getenv("GOPATH") 143 ) 144 for _, dir := range strings.Split(os.Getenv("PATH"), ":") { 145 if writable(dir) { 146 if strings.HasPrefix(dir, home) { 147 if homeBin != "" { 148 homeBin = dir 149 } 150 if gopath != "" && strings.HasPrefix(dir, gopath) && goHomeBin == "" { 151 goHomeBin = dir 152 } 153 } 154 if bin == "" { 155 bin = dir 156 } 157 } 158 } 159 if goHomeBin != "" { 160 return goHomeBin 161 } 162 if homeBin != "" { 163 return homeBin 164 } 165 return bin 166 } 167 168 // read the body or an error message 169 func readBody(in io.Reader) string { 170 data, err := ioutil.ReadAll(in) 171 if err != nil { 172 return fmt.Sprintf("Error reading body: %v", err.Error()) 173 } 174 return string(data) 175 } 176 177 // Get an asset URL and name 178 func getAsset(project string, matchName *regexp.Regexp) (string, string) { 179 url := "https://api.github.com/repos/" + project + "/releases/latest" 180 log.Printf("Fetching asset info for %q from %q", project, url) 181 user, pass := os.Getenv("GITHUB_USER"), os.Getenv("GITHUB_TOKEN") 182 req, err := http.NewRequest("GET", url, nil) 183 if err != nil { 184 log.Fatalf("Failed to make http request %q: %v", url, err) 185 } 186 if user != "" && pass != "" { 187 log.Printf("Fetching using GITHUB_USER and GITHUB_TOKEN") 188 req.SetBasicAuth(user, pass) 189 } 190 resp, err := http.DefaultClient.Do(req) 191 if err != nil { 192 log.Fatalf("Failed to fetch release info %q: %v", url, err) 193 } 194 if resp.StatusCode != http.StatusOK { 195 log.Printf("Error: %s", readBody(resp.Body)) 196 log.Fatalf("Bad status %d when fetching %q release info: %s", resp.StatusCode, url, resp.Status) 197 } 198 var release Release 199 err = json.NewDecoder(resp.Body).Decode(&release) 200 if err != nil { 201 log.Fatalf("Failed to decode release info: %v", err) 202 } 203 err = resp.Body.Close() 204 if err != nil { 205 log.Fatalf("Failed to close body: %v", err) 206 } 207 208 for _, asset := range release.Assets { 209 //log.Printf("Finding %s", asset.Name) 210 if matchName.MatchString(asset.Name) && isOurOsArch(asset.Name) { 211 return asset.BrowserDownloadURL, asset.Name 212 } 213 } 214 log.Fatalf("Didn't find asset in info") 215 return "", "" 216 } 217 218 // Get an asset URL and name by scraping the downloads page 219 // 220 // This doesn't use the API so isn't rate limited when not using GITHUB login details 221 func getAssetFromReleasesPage(project string, matchName *regexp.Regexp) (assetURL string, assetName string) { 222 baseURL := "https://github.com/" + project + "/releases" 223 log.Printf("Fetching asset info for %q from %q", project, baseURL) 224 base, err := url.Parse(baseURL) 225 if err != nil { 226 log.Fatalf("URL Parse failed: %v", err) 227 } 228 resp, err := http.Get(baseURL) 229 if err != nil { 230 log.Fatalf("Failed to fetch release info %q: %v", baseURL, err) 231 } 232 defer resp.Body.Close() 233 if resp.StatusCode != http.StatusOK { 234 log.Printf("Error: %s", readBody(resp.Body)) 235 log.Fatalf("Bad status %d when fetching %q release info: %s", resp.StatusCode, baseURL, resp.Status) 236 } 237 doc, err := html.Parse(resp.Body) 238 if err != nil { 239 log.Fatalf("Failed to parse web page: %v", err) 240 } 241 var walk func(*html.Node) 242 walk = func(n *html.Node) { 243 if n.Type == html.ElementNode && n.Data == "a" { 244 for _, a := range n.Attr { 245 if a.Key == "href" { 246 if name := path.Base(a.Val); matchName.MatchString(name) && isOurOsArch(name) { 247 if u, err := rest.URLJoin(base, a.Val); err == nil { 248 if assetName == "" { 249 assetName = name 250 assetURL = u.String() 251 } 252 } 253 } 254 break 255 } 256 } 257 } 258 for c := n.FirstChild; c != nil; c = c.NextSibling { 259 walk(c) 260 } 261 } 262 walk(doc) 263 if assetName == "" || assetURL == "" { 264 log.Fatalf("Didn't find URL in page") 265 } 266 return assetURL, assetName 267 } 268 269 // isOurOsArch returns true if s contains our OS and our Arch 270 func isOurOsArch(s string) bool { 271 s = strings.ToLower(s) 272 check := func(base string, aliases map[string][]string) bool { 273 names := []string{base} 274 names = append(names, aliases[base]...) 275 for _, name := range names { 276 if strings.Contains(s, name) { 277 return true 278 } 279 } 280 return false 281 } 282 return check(runtime.GOARCH, archAliases) && check(runtime.GOOS, osAliases) 283 } 284 285 // get a file for download 286 func getFile(url, fileName string) { 287 log.Printf("Downloading %q from %q", fileName, url) 288 289 out, err := os.Create(fileName) 290 if err != nil { 291 log.Fatalf("Failed to open %q: %v", fileName, err) 292 } 293 294 resp, err := http.Get(url) 295 if err != nil { 296 log.Fatalf("Failed to fetch asset %q: %v", url, err) 297 } 298 if resp.StatusCode != http.StatusOK { 299 log.Printf("Error: %s", readBody(resp.Body)) 300 log.Fatalf("Bad status %d when fetching %q asset: %s", resp.StatusCode, url, resp.Status) 301 } 302 303 n, err := io.Copy(out, resp.Body) 304 if err != nil { 305 log.Fatalf("Error while downloading: %v", err) 306 } 307 308 err = resp.Body.Close() 309 if err != nil { 310 log.Fatalf("Failed to close body: %v", err) 311 } 312 err = out.Close() 313 if err != nil { 314 log.Fatalf("Failed to close output file: %v", err) 315 } 316 317 log.Printf("Downloaded %q (%d bytes)", fileName, n) 318 } 319 320 // run a shell command 321 func run(args ...string) { 322 cmd := exec.Command(args[0], args[1:]...) 323 cmd.Stdout = os.Stdout 324 cmd.Stderr = os.Stderr 325 err := cmd.Run() 326 if err != nil { 327 log.Fatalf("Failed to run %v: %v", args, err) 328 } 329 } 330 331 // Untars fileName from srcFile 332 func untar(srcFile, fileName, extractDir string) { 333 f, err := os.Open(srcFile) 334 if err != nil { 335 log.Fatalf("Couldn't open tar: %v", err) 336 } 337 defer func() { 338 err := f.Close() 339 if err != nil { 340 log.Fatalf("Couldn't close tar: %v", err) 341 } 342 }() 343 344 var in io.Reader = f 345 346 srcExt := filepath.Ext(srcFile) 347 if srcExt == ".gz" || srcExt == ".tgz" { 348 gzf, err := gzip.NewReader(f) 349 if err != nil { 350 log.Fatalf("Couldn't open gzip: %v", err) 351 } 352 in = gzf 353 } else if srcExt == ".bz2" { 354 in = bzip2.NewReader(f) 355 } 356 357 tarReader := tar.NewReader(in) 358 359 for { 360 header, err := tarReader.Next() 361 if err == io.EOF { 362 break 363 } 364 if err != nil { 365 log.Fatalf("Trouble reading tar file: %v", err) 366 } 367 name := header.Name 368 switch header.Typeflag { 369 case tar.TypeReg: 370 baseName := filepath.Base(name) 371 if baseName == fileName { 372 outPath := filepath.Join(extractDir, fileName) 373 out, err := os.OpenFile(outPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0777) 374 if err != nil { 375 log.Fatalf("Couldn't open output file: %v", err) 376 } 377 defer func() { 378 err := out.Close() 379 if err != nil { 380 log.Fatalf("Couldn't close output: %v", err) 381 } 382 }() 383 n, err := io.Copy(out, tarReader) 384 if err != nil { 385 log.Fatalf("Couldn't write output file: %v", err) 386 } 387 log.Printf("Wrote %s (%d bytes) as %q", fileName, n, outPath) 388 } 389 } 390 } 391 } 392 393 func main() { 394 flag.Parse() 395 args := flag.Args() 396 if len(args) != 2 { 397 log.Fatalf("Syntax: %s <user/project> <name reg exp>", os.Args[0]) 398 } 399 project, nameRe := args[0], args[1] 400 if !matchProject.MatchString(project) { 401 log.Fatalf("Project %q must be in form user/project", project) 402 } 403 matchName, err := regexp.Compile(nameRe) 404 if err != nil { 405 log.Fatalf("Invalid regexp for name %q: %v", nameRe, err) 406 } 407 408 var assetURL, assetName string 409 if *useAPI { 410 assetURL, assetName = getAsset(project, matchName) 411 } else { 412 assetURL, assetName = getAssetFromReleasesPage(project, matchName) 413 } 414 fileName := filepath.Join(os.TempDir(), assetName) 415 getFile(assetURL, fileName) 416 417 if *install { 418 log.Printf("Installing %s", fileName) 419 run("sudo", "dpkg", "--force-bad-version", "-i", fileName) 420 log.Printf("Installed %s", fileName) 421 } else if *extract != "" { 422 if *bindir == "" { 423 log.Fatalf("Need to set -bindir") 424 } 425 log.Printf("Unpacking %s from %s and installing into %s", *extract, fileName, *bindir) 426 untar(fileName, *extract, *bindir+"/") 427 } 428 }