123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426 |
- // +build ignore
- // Get the latest release from a github project
- //
- // If GITHUB_USER and GITHUB_TOKEN are set then these will be used to
- // authenticate the request which is useful to avoid rate limits.
- package main
- import (
- "archive/tar"
- "compress/bzip2"
- "compress/gzip"
- "encoding/json"
- "flag"
- "fmt"
- "io"
- "io/ioutil"
- "log"
- "net/http"
- "net/url"
- "os"
- "os/exec"
- "path"
- "path/filepath"
- "regexp"
- "runtime"
- "strings"
- "time"
- "github.com/rclone/rclone/lib/rest"
- "golang.org/x/net/html"
- "golang.org/x/sys/unix"
- )
- var (
- // Flags
- install = flag.Bool("install", false, "Install the downloaded package using sudo dpkg -i.")
- extract = flag.String("extract", "", "Extract the named executable from the .tar.gz and install into bindir.")
- bindir = flag.String("bindir", defaultBinDir(), "Directory to install files downloaded with -extract.")
- useAPI = flag.Bool("use-api", false, "Use the API for finding the release instead of scraping the page.")
- // Globals
- matchProject = regexp.MustCompile(`^([\w-]+)/([\w-]+)$`)
- osAliases = map[string][]string{
- "darwin": {"macos", "osx"},
- }
- archAliases = map[string][]string{
- "amd64": {"x86_64"},
- }
- )
- // A github release
- //
- // Made by pasting the JSON into https://mholt.github.io/json-to-go/
- type Release struct {
- URL string `json:"url"`
- AssetsURL string `json:"assets_url"`
- UploadURL string `json:"upload_url"`
- HTMLURL string `json:"html_url"`
- ID int `json:"id"`
- TagName string `json:"tag_name"`
- TargetCommitish string `json:"target_commitish"`
- Name string `json:"name"`
- Draft bool `json:"draft"`
- Author struct {
- Login string `json:"login"`
- ID int `json:"id"`
- AvatarURL string `json:"avatar_url"`
- GravatarID string `json:"gravatar_id"`
- URL string `json:"url"`
- HTMLURL string `json:"html_url"`
- FollowersURL string `json:"followers_url"`
- FollowingURL string `json:"following_url"`
- GistsURL string `json:"gists_url"`
- StarredURL string `json:"starred_url"`
- SubscriptionsURL string `json:"subscriptions_url"`
- OrganizationsURL string `json:"organizations_url"`
- ReposURL string `json:"repos_url"`
- EventsURL string `json:"events_url"`
- ReceivedEventsURL string `json:"received_events_url"`
- Type string `json:"type"`
- SiteAdmin bool `json:"site_admin"`
- } `json:"author"`
- Prerelease bool `json:"prerelease"`
- CreatedAt time.Time `json:"created_at"`
- PublishedAt time.Time `json:"published_at"`
- Assets []struct {
- URL string `json:"url"`
- ID int `json:"id"`
- Name string `json:"name"`
- Label string `json:"label"`
- Uploader struct {
- Login string `json:"login"`
- ID int `json:"id"`
- AvatarURL string `json:"avatar_url"`
- GravatarID string `json:"gravatar_id"`
- URL string `json:"url"`
- HTMLURL string `json:"html_url"`
- FollowersURL string `json:"followers_url"`
- FollowingURL string `json:"following_url"`
- GistsURL string `json:"gists_url"`
- StarredURL string `json:"starred_url"`
- SubscriptionsURL string `json:"subscriptions_url"`
- OrganizationsURL string `json:"organizations_url"`
- ReposURL string `json:"repos_url"`
- EventsURL string `json:"events_url"`
- ReceivedEventsURL string `json:"received_events_url"`
- Type string `json:"type"`
- SiteAdmin bool `json:"site_admin"`
- } `json:"uploader"`
- ContentType string `json:"content_type"`
- State string `json:"state"`
- Size int `json:"size"`
- DownloadCount int `json:"download_count"`
- CreatedAt time.Time `json:"created_at"`
- UpdatedAt time.Time `json:"updated_at"`
- BrowserDownloadURL string `json:"browser_download_url"`
- } `json:"assets"`
- TarballURL string `json:"tarball_url"`
- ZipballURL string `json:"zipball_url"`
- Body string `json:"body"`
- }
- // checks if a path has write access
- func writable(path string) bool {
- return unix.Access(path, unix.W_OK) == nil
- }
- // Directory to install releases in by default
- //
- // Find writable directories on $PATH. Use $GOPATH/bin if that is on
- // the path and writable or use the first writable directory which is
- // in $HOME or failing that the first writable directory.
- //
- // Returns "" if none of the above were found
- func defaultBinDir() string {
- home := os.Getenv("HOME")
- var (
- bin string
- homeBin string
- goHomeBin string
- gopath = os.Getenv("GOPATH")
- )
- for _, dir := range strings.Split(os.Getenv("PATH"), ":") {
- if writable(dir) {
- if strings.HasPrefix(dir, home) {
- if homeBin != "" {
- homeBin = dir
- }
- if gopath != "" && strings.HasPrefix(dir, gopath) && goHomeBin == "" {
- goHomeBin = dir
- }
- }
- if bin == "" {
- bin = dir
- }
- }
- }
- if goHomeBin != "" {
- return goHomeBin
- }
- if homeBin != "" {
- return homeBin
- }
- return bin
- }
- // read the body or an error message
- func readBody(in io.Reader) string {
- data, err := ioutil.ReadAll(in)
- if err != nil {
- return fmt.Sprintf("Error reading body: %v", err.Error())
- }
- return string(data)
- }
- // Get an asset URL and name
- func getAsset(project string, matchName *regexp.Regexp) (string, string) {
- url := "https://api.github.com/repos/" + project + "/releases/latest"
- log.Printf("Fetching asset info for %q from %q", project, url)
- user, pass := os.Getenv("GITHUB_USER"), os.Getenv("GITHUB_TOKEN")
- req, err := http.NewRequest("GET", url, nil)
- if err != nil {
- log.Fatalf("Failed to make http request %q: %v", url, err)
- }
- if user != "" && pass != "" {
- log.Printf("Fetching using GITHUB_USER and GITHUB_TOKEN")
- req.SetBasicAuth(user, pass)
- }
- resp, err := http.DefaultClient.Do(req)
- if err != nil {
- log.Fatalf("Failed to fetch release info %q: %v", url, err)
- }
- if resp.StatusCode != http.StatusOK {
- log.Printf("Error: %s", readBody(resp.Body))
- log.Fatalf("Bad status %d when fetching %q release info: %s", resp.StatusCode, url, resp.Status)
- }
- var release Release
- err = json.NewDecoder(resp.Body).Decode(&release)
- if err != nil {
- log.Fatalf("Failed to decode release info: %v", err)
- }
- err = resp.Body.Close()
- if err != nil {
- log.Fatalf("Failed to close body: %v", err)
- }
- for _, asset := range release.Assets {
- //log.Printf("Finding %s", asset.Name)
- if matchName.MatchString(asset.Name) && isOurOsArch(asset.Name) {
- return asset.BrowserDownloadURL, asset.Name
- }
- }
- log.Fatalf("Didn't find asset in info")
- return "", ""
- }
- // Get an asset URL and name by scraping the downloads page
- //
- // This doesn't use the API so isn't rate limited when not using GITHUB login details
- func getAssetFromReleasesPage(project string, matchName *regexp.Regexp) (assetURL string, assetName string) {
- baseURL := "https://github.com/" + project + "/releases"
- log.Printf("Fetching asset info for %q from %q", project, baseURL)
- base, err := url.Parse(baseURL)
- if err != nil {
- log.Fatalf("URL Parse failed: %v", err)
- }
- resp, err := http.Get(baseURL)
- if err != nil {
- log.Fatalf("Failed to fetch release info %q: %v", baseURL, err)
- }
- defer resp.Body.Close()
- if resp.StatusCode != http.StatusOK {
- log.Printf("Error: %s", readBody(resp.Body))
- log.Fatalf("Bad status %d when fetching %q release info: %s", resp.StatusCode, baseURL, resp.Status)
- }
- doc, err := html.Parse(resp.Body)
- if err != nil {
- log.Fatalf("Failed to parse web page: %v", err)
- }
- var walk func(*html.Node)
- walk = func(n *html.Node) {
- if n.Type == html.ElementNode && n.Data == "a" {
- for _, a := range n.Attr {
- if a.Key == "href" {
- if name := path.Base(a.Val); matchName.MatchString(name) && isOurOsArch(name) {
- if u, err := rest.URLJoin(base, a.Val); err == nil {
- if assetName == "" {
- assetName = name
- assetURL = u.String()
- }
- }
- }
- break
- }
- }
- }
- for c := n.FirstChild; c != nil; c = c.NextSibling {
- walk(c)
- }
- }
- walk(doc)
- if assetName == "" || assetURL == "" {
- log.Fatalf("Didn't find URL in page")
- }
- return assetURL, assetName
- }
- // isOurOsArch returns true if s contains our OS and our Arch
- func isOurOsArch(s string) bool {
- s = strings.ToLower(s)
- check := func(base string, aliases map[string][]string) bool {
- names := []string{base}
- names = append(names, aliases[base]...)
- for _, name := range names {
- if strings.Contains(s, name) {
- return true
- }
- }
- return false
- }
- return check(runtime.GOARCH, archAliases) && check(runtime.GOOS, osAliases)
- }
- // get a file for download
- func getFile(url, fileName string) {
- log.Printf("Downloading %q from %q", fileName, url)
- out, err := os.Create(fileName)
- if err != nil {
- log.Fatalf("Failed to open %q: %v", fileName, err)
- }
- resp, err := http.Get(url)
- if err != nil {
- log.Fatalf("Failed to fetch asset %q: %v", url, err)
- }
- if resp.StatusCode != http.StatusOK {
- log.Printf("Error: %s", readBody(resp.Body))
- log.Fatalf("Bad status %d when fetching %q asset: %s", resp.StatusCode, url, resp.Status)
- }
- n, err := io.Copy(out, resp.Body)
- if err != nil {
- log.Fatalf("Error while downloading: %v", err)
- }
- err = resp.Body.Close()
- if err != nil {
- log.Fatalf("Failed to close body: %v", err)
- }
- err = out.Close()
- if err != nil {
- log.Fatalf("Failed to close output file: %v", err)
- }
- log.Printf("Downloaded %q (%d bytes)", fileName, n)
- }
- // run a shell command
- func run(args ...string) {
- cmd := exec.Command(args[0], args[1:]...)
- cmd.Stdout = os.Stdout
- cmd.Stderr = os.Stderr
- err := cmd.Run()
- if err != nil {
- log.Fatalf("Failed to run %v: %v", args, err)
- }
- }
- // Untars fileName from srcFile
- func untar(srcFile, fileName, extractDir string) {
- f, err := os.Open(srcFile)
- if err != nil {
- log.Fatalf("Couldn't open tar: %v", err)
- }
- defer func() {
- err := f.Close()
- if err != nil {
- log.Fatalf("Couldn't close tar: %v", err)
- }
- }()
- var in io.Reader = f
- srcExt := filepath.Ext(srcFile)
- if srcExt == ".gz" || srcExt == ".tgz" {
- gzf, err := gzip.NewReader(f)
- if err != nil {
- log.Fatalf("Couldn't open gzip: %v", err)
- }
- in = gzf
- } else if srcExt == ".bz2" {
- in = bzip2.NewReader(f)
- }
- tarReader := tar.NewReader(in)
- for {
- header, err := tarReader.Next()
- if err == io.EOF {
- break
- }
- if err != nil {
- log.Fatalf("Trouble reading tar file: %v", err)
- }
- name := header.Name
- switch header.Typeflag {
- case tar.TypeReg:
- baseName := filepath.Base(name)
- if baseName == fileName {
- outPath := filepath.Join(extractDir, fileName)
- out, err := os.OpenFile(outPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0777)
- if err != nil {
- log.Fatalf("Couldn't open output file: %v", err)
- }
- n, err := io.Copy(out, tarReader)
- if err != nil {
- log.Fatalf("Couldn't write output file: %v", err)
- }
- if err = out.Close(); err != nil {
- log.Fatalf("Couldn't close output: %v", err)
- }
- log.Printf("Wrote %s (%d bytes) as %q", fileName, n, outPath)
- }
- }
- }
- }
- func main() {
- flag.Parse()
- args := flag.Args()
- if len(args) != 2 {
- log.Fatalf("Syntax: %s <user/project> <name reg exp>", os.Args[0])
- }
- project, nameRe := args[0], args[1]
- if !matchProject.MatchString(project) {
- log.Fatalf("Project %q must be in form user/project", project)
- }
- matchName, err := regexp.Compile(nameRe)
- if err != nil {
- log.Fatalf("Invalid regexp for name %q: %v", nameRe, err)
- }
- var assetURL, assetName string
- if *useAPI {
- assetURL, assetName = getAsset(project, matchName)
- } else {
- assetURL, assetName = getAssetFromReleasesPage(project, matchName)
- }
- fileName := filepath.Join(os.TempDir(), assetName)
- getFile(assetURL, fileName)
- if *install {
- log.Printf("Installing %s", fileName)
- run("sudo", "dpkg", "--force-bad-version", "-i", fileName)
- log.Printf("Installed %s", fileName)
- } else if *extract != "" {
- if *bindir == "" {
- log.Fatalf("Need to set -bindir")
- }
- log.Printf("Unpacking %s from %s and installing into %s", *extract, fileName, *bindir)
- untar(fileName, *extract, *bindir+"/")
- }
- }
|