| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462 |
- 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()
- }
|