rar-upgrade.go 14 KB

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