| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628 |
- 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:
- <tr>
- <td><a href="/rar/winrar-x64-720.exe">
- <b>WinRAR x64 (64 bit) 7.20</b></a></td>
- <td>Graphical and command line</td>
- <td align="center">Trial</td>
- <td align="right">3683 KB</td>
- </tr>
- ...
- <tr>
- <td><a href="/rar/rarlinux-x64-720.tar.gz">
- <b>RAR for Linux x64 7.20</b></a></td>
- <td>Command line only</td>
- <td align="center">Trial</td>
- <td align="right">728 KB</td>
- </tr>
- */
- 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()
- }
|