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, err := buffer.ReadString('\n') if err != nil { return "" } line, _ = strings.CutSuffix(line, "\n") for len(line) == 0 { // First line is blank. line, err = buffer.ReadString('\n') if err != nil { return "" } line, _ = strings.CutSuffix(line, "\n") } fmt.Printf("RAR: [%s]\n", line) if line != "" { rarver, _, found := strings.Cut(line, " ") if found { line = rarver } } // Ok, we should have something like "RAR 7.10" _, version, found := strings.Cut(line, " ") if found { return version } 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() }