Преглед изворни кода

Initial setup of rar updater.

We locate rar.
We get rar version (line 1).
Steve Thielemann пре 1 дан
комит
889d6f3cac
4 измењених фајлова са 470 додато и 0 уклоњено
  1. 1 0
      .gitignore
  2. 5 0
      go.mod
  3. 2 0
      go.sum
  4. 462 0
      rar-upgrade.go

+ 1 - 0
.gitignore

@@ -0,0 +1 @@
+rar-upgrade

+ 5 - 0
go.mod

@@ -0,0 +1,5 @@
+module rar-upgrade
+
+go 1.25.0
+
+require golang.org/x/net v0.51.0

+ 2 - 0
go.sum

@@ -0,0 +1,2 @@
+golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
+golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=

+ 462 - 0
rar-upgrade.go

@@ -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()
+}