425 lines · 11.9 KB · Go
RawDownload
// +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 accessfunc 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 foundfunc 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 messagefunc 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 namefunc 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 detailsfunc 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 Archfunc 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 downloadfunc 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 commandfunc 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 srcFilefunc 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+"/")	}}