rar-upgrade.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490
  1. package main
  2. import (
  3. "bytes"
  4. "flag"
  5. "fmt"
  6. "io"
  7. "io/fs"
  8. "net/http"
  9. "net/url"
  10. "os"
  11. "os/exec"
  12. "path/filepath"
  13. "runtime/debug"
  14. "strings"
  15. "time"
  16. "golang.org/x/net/html"
  17. )
  18. /* UserAgent */
  19. // The value I want is (unfortunately) private http.Request.defaultUserAgent
  20. const DefaultUserAgent = "Go-http-client/2.0"
  21. // const UserAgent = "Go-updater/0.1.0"
  22. type AddHeaderTransport struct {
  23. Transport http.RoundTripper
  24. }
  25. func (adt *AddHeaderTransport) RoundTrip(req *http.Request) (*http.Response, error) {
  26. req.Header.Set("User-Agent", fmt.Sprintf("Rar-updater/%s %s", buildVersion, DefaultUserAgent))
  27. return adt.Transport.RoundTrip(req)
  28. }
  29. var client *http.Client
  30. func client_init() {
  31. client = &http.Client{
  32. Transport: &AddHeaderTransport{
  33. Transport: http.DefaultTransport,
  34. },
  35. }
  36. }
  37. var buildInfo *debug.BuildInfo
  38. var buildGoVersion, buildVersion string
  39. func init() {
  40. var ok bool
  41. buildInfo, ok = debug.ReadBuildInfo()
  42. if !ok {
  43. return
  44. }
  45. buildVersion = buildInfo.Main.Version
  46. buildGoVersion = buildInfo.GoVersion
  47. client_init()
  48. }
  49. // Get go version from the download URL.
  50. func GetVersionFromUrl(url string, arch string) string {
  51. part, _, _ := strings.Cut(url, arch)
  52. part, _ = strings.CutSuffix(part, ".")
  53. _, version, _ := strings.Cut(part, "/go")
  54. return version
  55. }
  56. func ParseHtml(read io.Reader, os_arch string) string {
  57. var tokens = html.NewTokenizer(read)
  58. var arch_link string
  59. tloop:
  60. for {
  61. tt := tokens.Next()
  62. switch tt {
  63. case html.ErrorToken:
  64. break tloop
  65. case html.StartTagToken:
  66. if len(arch_link) != 0 {
  67. continue
  68. }
  69. tn, _ := tokens.TagName()
  70. if bytes.Equal(tn, []byte("a")) {
  71. /*
  72. This is specific to how the HTML is defined.
  73. If it changes, this will definitely need to be updated.
  74. We look for class=download with href containing /go.
  75. */
  76. key, value, more := tokens.TagAttr()
  77. _ = more
  78. if bytes.Equal(key, []byte("class")) {
  79. if bytes.Equal(value, []byte("download")) {
  80. // Ok! This is the href we want.
  81. key, value, more = tokens.TagAttr()
  82. if bytes.Equal(key, []byte("href")) {
  83. // Does it contain link to go?
  84. if bytes.Contains(value, []byte("/go")) {
  85. if bytes.Contains(value, []byte(os_arch)) {
  86. // This contains our OS and ARCH!
  87. arch_link = string(value)
  88. // a href=/dl/go1.25.6.linux-amd64.tar.gz
  89. fmt.Printf("a href=%s\n", value)
  90. }
  91. }
  92. }
  93. }
  94. }
  95. }
  96. }
  97. }
  98. return arch_link
  99. }
  100. // This function has been fixed. It works correctly now.
  101. func RelativeToAbsoluteUrl(base string, href string) (string, error) {
  102. var result string
  103. base_url, err := url.Parse(base)
  104. if err != nil {
  105. fmt.Printf("Failed to parse %s\n", base)
  106. return result, err
  107. }
  108. abs_url, err := base_url.Parse(href)
  109. if err != nil {
  110. fmt.Printf("Failed to parse %s\n", href)
  111. return result, err
  112. }
  113. // fmt.Printf("Base %s Href %s => %s\n", base, href, abs_url)
  114. result = abs_url.String()
  115. return result, nil
  116. }
  117. // Does this handle read-only? GOPATH has files marked readonly. :(
  118. // Mark directories as 0755 and files as 0666.
  119. func RemoveReadOnly(dir string) error {
  120. var err = filepath.Walk(dir, func(path string, info fs.FileInfo, err error) error {
  121. if err != nil {
  122. return err
  123. }
  124. // var p = info.Mode().Perm()
  125. if info.IsDir() {
  126. err = os.Chmod(path, 0755)
  127. if err != nil {
  128. return err
  129. }
  130. } else {
  131. // Ok, it's a file then.
  132. err := os.Chmod(path, 0666)
  133. if err != nil {
  134. return err
  135. }
  136. }
  137. return nil
  138. })
  139. return err
  140. }
  141. // Remove directory contents
  142. //
  143. // Use this to clear out the GOROOT directory.
  144. // This works, if RemoveReadOnly has been called on the dir.
  145. func RemoveContents(dir string) error {
  146. d, err := os.Open(dir)
  147. if err != nil {
  148. return err
  149. }
  150. defer d.Close()
  151. names, err := d.Readdirnames(-1)
  152. if err != nil {
  153. return err
  154. }
  155. for _, name := range names {
  156. err = os.RemoveAll(filepath.Join(dir, name))
  157. if err != nil {
  158. return err
  159. }
  160. }
  161. return nil
  162. }
  163. // Convert URL into usable filename for cache.
  164. //
  165. // This looks for the last '/' and uses everything after that
  166. // as the filename.
  167. func UrlToFilename(url string) string {
  168. var result string
  169. if strings.HasSuffix(url, "/") {
  170. url = strings.TrimRight(url, "/")
  171. }
  172. idx := strings.LastIndex(url, "/")
  173. if idx == -1 {
  174. fmt.Printf("filename from url %s : failed.\n", url)
  175. os.Exit(10)
  176. }
  177. result = url[idx+1:]
  178. return result
  179. }
  180. /*
  181. fn extract_tarball(tarball: &str, target: &str) -> Result<()> {
  182. println!("Extract {} to {}", tarball, target);
  183. let output = Command::new("tar")
  184. // Extract, gzipped, from file
  185. .arg("-xzf")
  186. .arg(tarball)
  187. // archive contains go directory. Strip that out.
  188. .arg("--strip-components=1")
  189. // Set target to extract to.
  190. .arg("-C")
  191. .arg(target)
  192. .output()?;
  193. if output.status.success() {
  194. return Ok(());
  195. }
  196. bail!("Extract {} failed.", tarball);
  197. }
  198. */
  199. func ExtractTarball(tarball string, target string) error {
  200. var cmd = []string{"tar", "-xzf", tarball, "--strip-components=1", "-C", target}
  201. _, err := exec.Command(cmd[0], cmd[1:]...).Output()
  202. return err
  203. }
  204. // Expire files from cache over N days old.
  205. func CacheExpire(dir string, days int) (int, error) {
  206. var result int
  207. oldDate := time.Now().AddDate(0, 0, -days) // N days ago
  208. err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
  209. if err != nil {
  210. return err
  211. }
  212. if info.ModTime().Before(oldDate) {
  213. err := os.Remove(path)
  214. if err != nil {
  215. fmt.Println("Error deleting file:", err)
  216. return err
  217. } else {
  218. result += 1
  219. // fmt.Println("Deleted:", path)
  220. }
  221. }
  222. return nil
  223. })
  224. return result, err
  225. }
  226. func FindRar() (string, error) {
  227. return exec.LookPath("rar")
  228. }
  229. func RarVersion() string {
  230. output, _ := exec.Command("rar").Output()
  231. // fmt.Printf("RAR:\n%s\n", output)
  232. buffer := bytes.NewBuffer(output)
  233. line, err := buffer.ReadString('\n')
  234. if err != nil {
  235. return ""
  236. }
  237. line, _ = strings.CutSuffix(line, "\n")
  238. for len(line) == 0 {
  239. // First line is blank.
  240. line, err = buffer.ReadString('\n')
  241. if err != nil {
  242. return ""
  243. }
  244. line, _ = strings.CutSuffix(line, "\n")
  245. }
  246. fmt.Printf("RAR: [%s]\n", line)
  247. if line != "" {
  248. rarver, _, found := strings.Cut(line, " ")
  249. if found {
  250. line = rarver
  251. }
  252. }
  253. // Ok, we should have something like "RAR 7.10"
  254. _, version, found := strings.Cut(line, " ")
  255. if found {
  256. return version
  257. }
  258. return line
  259. }
  260. /*
  261. I also would like this program to be able to fresh/new install go!
  262. This would require writing/updating .bash_aliases with:
  263. export GOROOT=$HOME/go
  264. export GOPATH=$HOME/gopath
  265. export PATH=$PATH:$GOROOT/bin:$GOPATH/bin
  266. */
  267. // Main go download page.
  268. const RAR_URL = "https://www.rarlab.com/download.htm"
  269. // Save html to this filename.
  270. var RAR_URL_FILE = UrlToFilename(RAR_URL)
  271. // "go.dev-dl.html"
  272. func main() {
  273. HOME := os.Getenv("HOME")
  274. if len(HOME) == 0 {
  275. fmt.Printf("Expected $HOME environment variable to be set.")
  276. os.Exit(2)
  277. }
  278. CACHE_DIR := fmt.Sprintf("%s/.cache/rar-upgrade", HOME)
  279. // Display information?
  280. var (
  281. info bool
  282. stop bool
  283. expire int
  284. )
  285. flag.BoolVar(&info, "info", false, "Display information")
  286. flag.IntVar(&expire, "expire", 30, "Number of days to expire")
  287. flag.BoolVar(&stop, "stop", false, "Stop execution (before fetching anything)")
  288. flag.Parse()
  289. if os.Geteuid() == 0 {
  290. fmt.Println("HEY! This program should never be run as root.")
  291. fmt.Println("This program manages a user install of rar (possibly in ~/bin).")
  292. return
  293. }
  294. // Ok, we're not running as root, so we can create/check the cache directory.
  295. {
  296. err := os.MkdirAll(CACHE_DIR, 0755)
  297. if err != nil {
  298. fmt.Printf("Unable to create cache directory %s: %s\n", CACHE_DIR, err)
  299. return
  300. }
  301. }
  302. // Expire the cache
  303. count, ceerr := CacheExpire(CACHE_DIR, expire)
  304. if ceerr != nil {
  305. fmt.Printf("CacheExpire error: %s\n", ceerr)
  306. return
  307. }
  308. if count != 0 {
  309. fmt.Printf("CacheExpire: removed %d file(s).\n", count)
  310. }
  311. // Is rar installed?
  312. rar, rarerr := FindRar()
  313. var rarversion string
  314. if rarerr == nil {
  315. // Ok, rar has been located in the path.
  316. rarversion = RarVersion()
  317. fmt.Printf("%s => %s\n", rar, rarversion)
  318. }
  319. var arch_link string
  320. if info {
  321. /*
  322. fmt.Printf("Current version: %s\n", go_version)
  323. fmt.Printf("OS Arch: %s\n", go_os_arch)
  324. fmt.Printf("GO ENV:\n%+v\n", goenv)
  325. fmt.Printf("Built by: %s\n", buildGoVersion)
  326. fmt.Printf("Go-Update: %s\n", buildVersion)
  327. */
  328. }
  329. if stop {
  330. fmt.Println("And we'll stop right here for now...")
  331. var resp, _ = client.Get("https://httpbin.org/user-agent")
  332. defer resp.Body.Close()
  333. io.Copy(os.Stdout, resp.Body)
  334. return
  335. }
  336. var resp, err = client.Get(RAR_URL)
  337. if err != nil {
  338. fmt.Printf("Get %s error: %s\n", RAR_URL, err)
  339. return
  340. }
  341. defer resp.Body.Close()
  342. // Verify that we got a status 200.
  343. if resp.StatusCode != 200 {
  344. fmt.Printf("From %s: %s\n", RAR_URL, resp.Status)
  345. return
  346. }
  347. // Possibly save the header into the cache directory as well.
  348. // resp.Header
  349. var filename = CACHE_DIR + "/" + RAR_URL_FILE
  350. fp, err := os.Create(filename)
  351. if err != nil {
  352. fmt.Printf("Create %s failed: %s\n", filename, err)
  353. return
  354. }
  355. fmt.Printf("%s : %s\n", RAR_URL, resp.Status)
  356. // Read from resp.Body and write it to the filename, and to parse_html.
  357. var read = io.TeeReader(resp.Body, fp)
  358. _ = read
  359. var download_link, rar_version string
  360. // arch_link = ParseHtml(read, go_os_arch)
  361. if len(arch_link) == 0 {
  362. fmt.Printf("I wasn't able to locate the go download URL link for %s.\n", download_link)
  363. fmt.Printf("Check the file %s, maybe the link has changed?\n", filename)
  364. return
  365. }
  366. dl_version := GetVersionFromUrl(arch_link, download_link)
  367. if dl_version == rar_version {
  368. fmt.Println("You're already good to GO.")
  369. return
  370. }
  371. fmt.Printf("Version: %s [have %s]\n", dl_version, rar_version)
  372. var arch_filename = UrlToFilename(arch_link)
  373. // Download
  374. //
  375. var dl_url string
  376. dl_url, err = RelativeToAbsoluteUrl(RAR_URL, arch_link)
  377. if err != nil {
  378. fmt.Printf("URL %s / href %s\n", RAR_URL, arch_link)
  379. fmt.Printf("Failed converting relative to absolute: %s\n", err)
  380. return
  381. }
  382. fmt.Printf("URL: %s\n", dl_url)
  383. resp, err = client.Get(dl_url)
  384. if err != nil {
  385. fmt.Printf("Get %s error: %s\n", dl_url, err)
  386. return
  387. }
  388. defer resp.Body.Close()
  389. // Verify that we got a status 200.
  390. if resp.StatusCode != 200 {
  391. fmt.Printf("From %s: %s\n", dl_url, resp.Status)
  392. return
  393. }
  394. // I've tried using/setting headers, etc for 304 Not modified, but
  395. // the website ignores it/doesn't use such things. Revisit.
  396. // Save file to cache
  397. filename = CACHE_DIR + "/" + arch_filename
  398. fp, err = os.Create(filename)
  399. if err != nil {
  400. fmt.Printf("Create %s failed: %s\n", filename, err)
  401. return
  402. }
  403. _, err = io.Copy(fp, resp.Body)
  404. if err != nil {
  405. fmt.Printf("Error saving %s\n", err)
  406. return
  407. }
  408. // Unarchive arch_filename into GOROOT.
  409. fmt.Printf("Extracting %s to %s ...", filename, "DERP")
  410. // err = ExtractTarball(filename, path)
  411. fmt.Println()
  412. }