|
@@ -0,0 +1,462 @@
|
|
|
|
|
+package main
|
|
|
|
|
+
|
|
|
|
|
+import (
|
|
|
|
|
+ "bytes"
|
|
|
|
|
+ "flag"
|
|
|
|
|
+ "fmt"
|
|
|
|
|
+ "io"
|
|
|
|
|
+ "io/fs"
|
|
|
|
|
+ "net/http"
|
|
|
|
|
+ "net/url"
|
|
|
|
|
+ "os"
|
|
|
|
|
+ "os/exec"
|
|
|
|
|
+ "path/filepath"
|
|
|
|
|
+ "runtime/debug"
|
|
|
|
|
+ "strings"
|
|
|
|
|
+ "time"
|
|
|
|
|
+
|
|
|
|
|
+ "golang.org/x/net/html"
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
|
|
+/* UserAgent */
|
|
|
|
|
+
|
|
|
|
|
+// The value I want is (unfortunately) private http.Request.defaultUserAgent
|
|
|
|
|
+const DefaultUserAgent = "Go-http-client/2.0"
|
|
|
|
|
+
|
|
|
|
|
+// const UserAgent = "Go-updater/0.1.0"
|
|
|
|
|
+
|
|
|
|
|
+type AddHeaderTransport struct {
|
|
|
|
|
+ Transport http.RoundTripper
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func (adt *AddHeaderTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
|
|
|
|
+ req.Header.Set("User-Agent", fmt.Sprintf("Rar-updater/%s %s", buildVersion, DefaultUserAgent))
|
|
|
|
|
+ return adt.Transport.RoundTrip(req)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+var client *http.Client
|
|
|
|
|
+
|
|
|
|
|
+func client_init() {
|
|
|
|
|
+ client = &http.Client{
|
|
|
|
|
+ Transport: &AddHeaderTransport{
|
|
|
|
|
+ Transport: http.DefaultTransport,
|
|
|
|
|
+ },
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+var buildInfo *debug.BuildInfo
|
|
|
|
|
+var buildGoVersion, buildVersion string
|
|
|
|
|
+
|
|
|
|
|
+func init() {
|
|
|
|
|
+ var ok bool
|
|
|
|
|
+ buildInfo, ok = debug.ReadBuildInfo()
|
|
|
|
|
+ if !ok {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ buildVersion = buildInfo.Main.Version
|
|
|
|
|
+ buildGoVersion = buildInfo.GoVersion
|
|
|
|
|
+ client_init()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Get go version from the download URL.
|
|
|
|
|
+func GetVersionFromUrl(url string, arch string) string {
|
|
|
|
|
+ part, _, _ := strings.Cut(url, arch)
|
|
|
|
|
+ part, _ = strings.CutSuffix(part, ".")
|
|
|
|
|
+ _, version, _ := strings.Cut(part, "/go")
|
|
|
|
|
+ return version
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func ParseHtml(read io.Reader, os_arch string) string {
|
|
|
|
|
+ var tokens = html.NewTokenizer(read)
|
|
|
|
|
+ var arch_link string
|
|
|
|
|
+
|
|
|
|
|
+tloop:
|
|
|
|
|
+ for {
|
|
|
|
|
+ tt := tokens.Next()
|
|
|
|
|
+
|
|
|
|
|
+ switch tt {
|
|
|
|
|
+ case html.ErrorToken:
|
|
|
|
|
+ break tloop
|
|
|
|
|
+
|
|
|
|
|
+ case html.StartTagToken:
|
|
|
|
|
+ if len(arch_link) != 0 {
|
|
|
|
|
+ continue
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ tn, _ := tokens.TagName()
|
|
|
|
|
+ if bytes.Equal(tn, []byte("a")) {
|
|
|
|
|
+ /*
|
|
|
|
|
+ This is specific to how the HTML is defined.
|
|
|
|
|
+ If it changes, this will definitely need to be updated.
|
|
|
|
|
+
|
|
|
|
|
+ We look for class=download with href containing /go.
|
|
|
|
|
+
|
|
|
|
|
+ */
|
|
|
|
|
+
|
|
|
|
|
+ key, value, more := tokens.TagAttr()
|
|
|
|
|
+ _ = more
|
|
|
|
|
+
|
|
|
|
|
+ if bytes.Equal(key, []byte("class")) {
|
|
|
|
|
+ if bytes.Equal(value, []byte("download")) {
|
|
|
|
|
+ // Ok! This is the href we want.
|
|
|
|
|
+ key, value, more = tokens.TagAttr()
|
|
|
|
|
+ if bytes.Equal(key, []byte("href")) {
|
|
|
|
|
+ // Does it contain link to go?
|
|
|
|
|
+ if bytes.Contains(value, []byte("/go")) {
|
|
|
|
|
+ if bytes.Contains(value, []byte(os_arch)) {
|
|
|
|
|
+ // This contains our OS and ARCH!
|
|
|
|
|
+ arch_link = string(value)
|
|
|
|
|
+ // a href=/dl/go1.25.6.linux-amd64.tar.gz
|
|
|
|
|
+ fmt.Printf("a href=%s\n", value)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return arch_link
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// This function has been fixed. It works correctly now.
|
|
|
|
|
+
|
|
|
|
|
+func RelativeToAbsoluteUrl(base string, href string) (string, error) {
|
|
|
|
|
+ var result string
|
|
|
|
|
+ base_url, err := url.Parse(base)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ fmt.Printf("Failed to parse %s\n", base)
|
|
|
|
|
+ return result, err
|
|
|
|
|
+ }
|
|
|
|
|
+ abs_url, err := base_url.Parse(href)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ fmt.Printf("Failed to parse %s\n", href)
|
|
|
|
|
+ return result, err
|
|
|
|
|
+ }
|
|
|
|
|
+ // fmt.Printf("Base %s Href %s => %s\n", base, href, abs_url)
|
|
|
|
|
+ result = abs_url.String()
|
|
|
|
|
+ return result, nil
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Does this handle read-only? GOPATH has files marked readonly. :(
|
|
|
|
|
+
|
|
|
|
|
+// Mark directories as 0755 and files as 0666.
|
|
|
|
|
+func RemoveReadOnly(dir string) error {
|
|
|
|
|
+ var err = filepath.Walk(dir, func(path string, info fs.FileInfo, err error) error {
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return err
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // var p = info.Mode().Perm()
|
|
|
|
|
+ if info.IsDir() {
|
|
|
|
|
+ err = os.Chmod(path, 0755)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return err
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // Ok, it's a file then.
|
|
|
|
|
+ err := os.Chmod(path, 0666)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return err
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return nil
|
|
|
|
|
+ })
|
|
|
|
|
+ return err
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Remove directory contents
|
|
|
|
|
+//
|
|
|
|
|
+// Use this to clear out the GOROOT directory.
|
|
|
|
|
+// This works, if RemoveReadOnly has been called on the dir.
|
|
|
|
|
+func RemoveContents(dir string) error {
|
|
|
|
|
+ d, err := os.Open(dir)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return err
|
|
|
|
|
+ }
|
|
|
|
|
+ defer d.Close()
|
|
|
|
|
+ names, err := d.Readdirnames(-1)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return err
|
|
|
|
|
+ }
|
|
|
|
|
+ for _, name := range names {
|
|
|
|
|
+
|
|
|
|
|
+ err = os.RemoveAll(filepath.Join(dir, name))
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return err
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return nil
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Convert URL into usable filename for cache.
|
|
|
|
|
+//
|
|
|
|
|
+// This looks for the last '/' and uses everything after that
|
|
|
|
|
+// as the filename.
|
|
|
|
|
+func UrlToFilename(url string) string {
|
|
|
|
|
+ var result string
|
|
|
|
|
+ if strings.HasSuffix(url, "/") {
|
|
|
|
|
+ url = strings.TrimRight(url, "/")
|
|
|
|
|
+ }
|
|
|
|
|
+ idx := strings.LastIndex(url, "/")
|
|
|
|
|
+ if idx == -1 {
|
|
|
|
|
+ fmt.Printf("filename from url %s : failed.\n", url)
|
|
|
|
|
+ os.Exit(10)
|
|
|
|
|
+ }
|
|
|
|
|
+ result = url[idx+1:]
|
|
|
|
|
+ return result
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/*
|
|
|
|
|
+
|
|
|
|
|
+fn extract_tarball(tarball: &str, target: &str) -> Result<()> {
|
|
|
|
|
+ println!("Extract {} to {}", tarball, target);
|
|
|
|
|
+ let output = Command::new("tar")
|
|
|
|
|
+ // Extract, gzipped, from file
|
|
|
|
|
+ .arg("-xzf")
|
|
|
|
|
+ .arg(tarball)
|
|
|
|
|
+ // archive contains go directory. Strip that out.
|
|
|
|
|
+ .arg("--strip-components=1")
|
|
|
|
|
+ // Set target to extract to.
|
|
|
|
|
+ .arg("-C")
|
|
|
|
|
+ .arg(target)
|
|
|
|
|
+ .output()?;
|
|
|
|
|
+ if output.status.success() {
|
|
|
|
|
+ return Ok(());
|
|
|
|
|
+ }
|
|
|
|
|
+ bail!("Extract {} failed.", tarball);
|
|
|
|
|
+}
|
|
|
|
|
+*/
|
|
|
|
|
+
|
|
|
|
|
+func ExtractTarball(tarball string, target string) error {
|
|
|
|
|
+ var cmd = []string{"tar", "-xzf", tarball, "--strip-components=1", "-C", target}
|
|
|
|
|
+ _, err := exec.Command(cmd[0], cmd[1:]...).Output()
|
|
|
|
|
+ return err
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Expire files from cache over N days old.
|
|
|
|
|
+func CacheExpire(dir string, days int) (int, error) {
|
|
|
|
|
+ var result int
|
|
|
|
|
+ oldDate := time.Now().AddDate(0, 0, -days) // N days ago
|
|
|
|
|
+ err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return err
|
|
|
|
|
+ }
|
|
|
|
|
+ if info.ModTime().Before(oldDate) {
|
|
|
|
|
+ err := os.Remove(path)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ fmt.Println("Error deleting file:", err)
|
|
|
|
|
+ return err
|
|
|
|
|
+ } else {
|
|
|
|
|
+ result += 1
|
|
|
|
|
+ // fmt.Println("Deleted:", path)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return nil
|
|
|
|
|
+ })
|
|
|
|
|
+ return result, err
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func FindRar() (string, error) {
|
|
|
|
|
+ return exec.LookPath("rar")
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func RarVersion() string {
|
|
|
|
|
+ output, _ := exec.Command("rar").Output()
|
|
|
|
|
+ fmt.Printf("RAR:\n%s\n", output)
|
|
|
|
|
+ buffer := bytes.NewBuffer(output)
|
|
|
|
|
+ line, _ := buffer.ReadString('\n')
|
|
|
|
|
+ line, _ = buffer.ReadString('\n')
|
|
|
|
|
+ return line
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/*
|
|
|
|
|
+I also would like this program to be able to fresh/new install go!
|
|
|
|
|
+
|
|
|
|
|
+This would require writing/updating .bash_aliases with:
|
|
|
|
|
+export GOROOT=$HOME/go
|
|
|
|
|
+export GOPATH=$HOME/gopath
|
|
|
|
|
+export PATH=$PATH:$GOROOT/bin:$GOPATH/bin
|
|
|
|
|
+*/
|
|
|
|
|
+
|
|
|
|
|
+// Main go download page.
|
|
|
|
|
+const RAR_URL = "https://www.rarlab.com/download.htm"
|
|
|
|
|
+
|
|
|
|
|
+// Save html to this filename.
|
|
|
|
|
+var RAR_URL_FILE = UrlToFilename(RAR_URL)
|
|
|
|
|
+
|
|
|
|
|
+// "go.dev-dl.html"
|
|
|
|
|
+
|
|
|
|
|
+func main() {
|
|
|
|
|
+ HOME := os.Getenv("HOME")
|
|
|
|
|
+ if len(HOME) == 0 {
|
|
|
|
|
+ fmt.Printf("Expected $HOME environment variable to be set.")
|
|
|
|
|
+ os.Exit(2)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ CACHE_DIR := fmt.Sprintf("%s/.cache/rar-upgrade", HOME)
|
|
|
|
|
+
|
|
|
|
|
+ // Display information?
|
|
|
|
|
+ var (
|
|
|
|
|
+ info bool
|
|
|
|
|
+ stop bool
|
|
|
|
|
+ expire int
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ flag.BoolVar(&info, "info", false, "Display information")
|
|
|
|
|
+ flag.IntVar(&expire, "expire", 30, "Number of days to expire")
|
|
|
|
|
+ flag.BoolVar(&stop, "stop", false, "Stop execution (before fetching anything)")
|
|
|
|
|
+ flag.Parse()
|
|
|
|
|
+
|
|
|
|
|
+ if os.Geteuid() == 0 {
|
|
|
|
|
+ fmt.Println("HEY! This program should never be run as root.")
|
|
|
|
|
+ fmt.Println("This program manages a user install of rar (possibly in ~/bin).")
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Ok, we're not running as root, so we can create/check the cache directory.
|
|
|
|
|
+
|
|
|
|
|
+ {
|
|
|
|
|
+ err := os.MkdirAll(CACHE_DIR, 0755)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ fmt.Printf("Unable to create cache directory %s: %s\n", CACHE_DIR, err)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Expire the cache
|
|
|
|
|
+ count, ceerr := CacheExpire(CACHE_DIR, expire)
|
|
|
|
|
+ if ceerr != nil {
|
|
|
|
|
+ fmt.Printf("CacheExpire error: %s\n", ceerr)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ if count != 0 {
|
|
|
|
|
+ fmt.Printf("CacheExpire: removed %d file(s).\n", count)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Is rar installed?
|
|
|
|
|
+ rar, rarerr := FindRar()
|
|
|
|
|
+ var rarversion string
|
|
|
|
|
+
|
|
|
|
|
+ if rarerr == nil {
|
|
|
|
|
+ // Ok, rar has been located in the path.
|
|
|
|
|
+ rarversion = RarVersion()
|
|
|
|
|
+ fmt.Printf("%s => %s\n", rar, rarversion)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ var arch_link string
|
|
|
|
|
+
|
|
|
|
|
+ if info {
|
|
|
|
|
+ /*
|
|
|
|
|
+ fmt.Printf("Current version: %s\n", go_version)
|
|
|
|
|
+ fmt.Printf("OS Arch: %s\n", go_os_arch)
|
|
|
|
|
+
|
|
|
|
|
+ fmt.Printf("GO ENV:\n%+v\n", goenv)
|
|
|
|
|
+ fmt.Printf("Built by: %s\n", buildGoVersion)
|
|
|
|
|
+ fmt.Printf("Go-Update: %s\n", buildVersion)
|
|
|
|
|
+ */
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if stop {
|
|
|
|
|
+ fmt.Println("And we'll stop right here for now...")
|
|
|
|
|
+ var resp, _ = client.Get("https://httpbin.org/user-agent")
|
|
|
|
|
+ defer resp.Body.Close()
|
|
|
|
|
+ io.Copy(os.Stdout, resp.Body)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ var resp, err = client.Get(RAR_URL)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ fmt.Printf("Get %s error: %s\n", RAR_URL, err)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ defer resp.Body.Close()
|
|
|
|
|
+
|
|
|
|
|
+ // Verify that we got a status 200.
|
|
|
|
|
+
|
|
|
|
|
+ if resp.StatusCode != 200 {
|
|
|
|
|
+ fmt.Printf("From %s: %s\n", RAR_URL, resp.Status)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Possibly save the header into the cache directory as well.
|
|
|
|
|
+ // resp.Header
|
|
|
|
|
+
|
|
|
|
|
+ var filename = CACHE_DIR + "/" + RAR_URL_FILE
|
|
|
|
|
+ fp, err := os.Create(filename)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ fmt.Printf("Create %s failed: %s\n", filename, err)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ fmt.Printf("%s : %s\n", RAR_URL, resp.Status)
|
|
|
|
|
+
|
|
|
|
|
+ // Read from resp.Body and write it to the filename, and to parse_html.
|
|
|
|
|
+ var read = io.TeeReader(resp.Body, fp)
|
|
|
|
|
+ _ = read
|
|
|
|
|
+ var download_link, rar_version string
|
|
|
|
|
+ // arch_link = ParseHtml(read, go_os_arch)
|
|
|
|
|
+
|
|
|
|
|
+ if len(arch_link) == 0 {
|
|
|
|
|
+ fmt.Printf("I wasn't able to locate the go download URL link for %s.\n", download_link)
|
|
|
|
|
+ fmt.Printf("Check the file %s, maybe the link has changed?\n", filename)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ dl_version := GetVersionFromUrl(arch_link, download_link)
|
|
|
|
|
+ if dl_version == rar_version {
|
|
|
|
|
+ fmt.Println("You're already good to GO.")
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ fmt.Printf("Version: %s [have %s]\n", dl_version, rar_version)
|
|
|
|
|
+ var arch_filename = UrlToFilename(arch_link)
|
|
|
|
|
+
|
|
|
|
|
+ // Download
|
|
|
|
|
+ //
|
|
|
|
|
+ var dl_url string
|
|
|
|
|
+ dl_url, err = RelativeToAbsoluteUrl(RAR_URL, arch_link)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ fmt.Printf("URL %s / href %s\n", RAR_URL, arch_link)
|
|
|
|
|
+ fmt.Printf("Failed converting relative to absolute: %s\n", err)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ fmt.Printf("URL: %s\n", dl_url)
|
|
|
|
|
+
|
|
|
|
|
+ resp, err = client.Get(dl_url)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ fmt.Printf("Get %s error: %s\n", dl_url, err)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ defer resp.Body.Close()
|
|
|
|
|
+
|
|
|
|
|
+ // Verify that we got a status 200.
|
|
|
|
|
+
|
|
|
|
|
+ if resp.StatusCode != 200 {
|
|
|
|
|
+ fmt.Printf("From %s: %s\n", dl_url, resp.Status)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // I've tried using/setting headers, etc for 304 Not modified, but
|
|
|
|
|
+ // the website ignores it/doesn't use such things. Revisit.
|
|
|
|
|
+
|
|
|
|
|
+ // Save file to cache
|
|
|
|
|
+ filename = CACHE_DIR + "/" + arch_filename
|
|
|
|
|
+ fp, err = os.Create(filename)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ fmt.Printf("Create %s failed: %s\n", filename, err)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ _, err = io.Copy(fp, resp.Body)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ fmt.Printf("Error saving %s\n", err)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Unarchive arch_filename into GOROOT.
|
|
|
|
|
+ fmt.Printf("Extracting %s to %s ...", filename, "DERP")
|
|
|
|
|
+ // err = ExtractTarball(filename, path)
|
|
|
|
|
+ fmt.Println()
|
|
|
|
|
+}
|