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