package main import ( "archive/tar" "bytes" "compress/gzip" "flag" "fmt" "io" "io/fs" "log" "net/http" "net/url" "os" "os/exec" "path/filepath" "runtime/debug" "strings" "time" "golang.org/x/net/html" ) var DEBUG = false /* 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. // // /rar/rarlinux-x64-720.tar.gz func GetVersionFromUrl(url string) string { part, _, _ := strings.Cut(url, ".") // /rar/rarlinux-x64-720 index := strings.LastIndex(part, "-") if index != -1 { return part[index+1:] } return "" } /* This is what I'm looking for: WinRAR x64 (64 bit) 7.20 Graphical and command line Trial 3683 KB ... RAR for Linux x64 7.20 Command line only Trial 728 KB */ func ParseHtml(read io.Reader, os_arch string) string { var tokens = html.NewTokenizer(read) var result string var arch_link string var found_links bool = false var done bool = false tloop: for { tt := tokens.Next() switch tt { case html.ErrorToken: break tloop case html.StartTagToken: if done { 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("href")) { // Does it contain link to go? if bytes.Contains(value, []byte("/rar/")) { found_links = true arch_link = string(value) if strings.Contains(arch_link, os_arch) { result = arch_link } if DEBUG { // a href=/dl/go1.25.6.linux-amd64.tar.gz fmt.Printf("a href=%s\n", value) } } } } case html.EndTagToken: tn, _ := tokens.TagName() // fmt.Printf("End token : [%s]\n", tn) if bytes.Equal(tn, []byte("table")) { if found_links == true { done = true } } } } return result } // 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 if DEBUG { 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") } if DEBUG { 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 } /* Tarball /home/thor/.cache/rar-upgrade/rarlinux-x64-720.tar.gz: Name: unrar Mode 755 Wrote 441632 bytes. Name: default.sfx Mode 755 Wrote 248960 bytes. Name: rar Mode 755 Wrote 798760 bytes. */ func read_tarball(fp *os.File) { // https://gauravgahlot.in/extracting-files-gzipped-tar-archive-go/ gzipReader, err := gzip.NewReader(fp) if err != nil { panic(err) // for now } tarReader := tar.NewReader(gzipReader) for { header, err := tarReader.Next() if err == io.EOF { break } if err != nil { log.Fatalln(err) } if header.Typeflag == tar.TypeReg { var basefilename string index := strings.LastIndex(header.Name, "/") if index != -1 { basefilename = header.Name[index+1:] } else { basefilename = header.Name } if header.Mode == 0755 { // This is an executable, extract it! fmt.Printf("Name: %s Mode %o\n", basefilename, header.Mode) fpout, err := os.OpenFile(basefilename, os.O_CREATE|os.O_WRONLY, os.FileMode(header.Mode)) if err != nil { panic(err) } defer fpout.Close() size, err := io.Copy(fpout, tarReader) if err != nil { panic(err) } else { fmt.Printf("Wrote %d bytes.\n", size) } } // Within the header, you have Name, Mode, UID, GID, etc. // fmt.Printf("Header: %+v\n", header) } } } /* 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) 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.BoolVar(&DEBUG, "debug", false, "Debug 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() } var arch_link string if info { fmt.Printf("RAR @ %s => Version %s\n", rar, rarversion) /* 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("Ok, parse HTML file:") var filename = CACHE_DIR + "/" + RAR_URL_FILE var parse_link string fp, err := os.Open(filename) if err == nil { defer fp.Close() parse_link = ParseHtml(fp, "linux") fmt.Printf("Parse HTML link: %s\n", parse_link) } else { fmt.Printf("Missing cache file: %s\n", filename) } // parse_link => /rar/rarlinux-x64-720.tar.gz index := strings.LastIndex(parse_link, "/") if index != -1 { parse_link = parse_link[index+1:] } filename = CACHE_DIR + "/" + parse_link fp, err = os.Open(filename) if err == nil { defer fp.Close() // Ok! We have open tarball file. fmt.Printf("Tarball %s:\n", filename) read_tarball(fp) } else { fmt.Printf("Missing cache file: %s\n", filename) } fmt.Println("Get user-agent string from httpbin site:") var resp, _ = client.Get("https://httpbin.org/user-agent") defer resp.Body.Close() io.Copy(os.Stdout, resp.Body) fmt.Println("And we'll stop right here for now...") 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 // Remove . from version rar_version = strings.ReplaceAll(rarversion, ".", "") arch_link = ParseHtml(read, "linux") 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) if dl_version == rar_version { fmt.Println("You have the latest version of RAR.") 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() }