rar-upgrade.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614
  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. func read_tarball(fp *os.File) {
  299. // https://gauravgahlot.in/extracting-files-gzipped-tar-archive-go/
  300. gzipReader, err := gzip.NewReader(fp)
  301. if err != nil {
  302. panic(err) // for now
  303. }
  304. tarReader := tar.NewReader(gzipReader)
  305. for {
  306. header, err := tarReader.Next()
  307. if err == io.EOF {
  308. break
  309. }
  310. if err != nil {
  311. log.Fatalln(err)
  312. }
  313. if header.Typeflag == tar.TypeReg {
  314. var basefilename string
  315. index := strings.LastIndex(header.Name, "/")
  316. if index != -1 {
  317. basefilename = header.Name[index+1:]
  318. } else {
  319. basefilename = header.Name
  320. }
  321. if header.Mode == 0755 {
  322. fmt.Printf("Name: %s Mode %o\n", basefilename, header.Mode)
  323. fpout, err := os.OpenFile(basefilename, os.O_CREATE|os.O_WRONLY, os.FileMode(header.Mode))
  324. if err != nil {
  325. panic(err)
  326. }
  327. defer fpout.Close()
  328. size, err := io.Copy(fpout, tarReader)
  329. if err != nil {
  330. panic(err)
  331. } else {
  332. fmt.Printf("Wrote %d bytes.\n", size)
  333. }
  334. }
  335. fmt.Printf("Header: %+v\n", header)
  336. }
  337. }
  338. }
  339. /*
  340. I also would like this program to be able to fresh/new install go!
  341. This would require writing/updating .bash_aliases with:
  342. export GOROOT=$HOME/go
  343. export GOPATH=$HOME/gopath
  344. export PATH=$PATH:$GOROOT/bin:$GOPATH/bin
  345. */
  346. // Main go download page.
  347. const RAR_URL = "https://www.rarlab.com/download.htm"
  348. // Save html to this filename.
  349. var RAR_URL_FILE = UrlToFilename(RAR_URL)
  350. func main() {
  351. HOME := os.Getenv("HOME")
  352. if len(HOME) == 0 {
  353. fmt.Printf("Expected $HOME environment variable to be set.")
  354. os.Exit(2)
  355. }
  356. CACHE_DIR := fmt.Sprintf("%s/.cache/rar-upgrade", HOME)
  357. // Display information?
  358. var (
  359. info bool
  360. stop bool
  361. expire int
  362. )
  363. flag.BoolVar(&info, "info", false, "Display information")
  364. flag.BoolVar(&DEBUG, "debug", false, "Debug information")
  365. flag.IntVar(&expire, "expire", 30, "Number of days to expire")
  366. flag.BoolVar(&stop, "stop", false, "Stop execution (before fetching anything)")
  367. flag.Parse()
  368. if os.Geteuid() == 0 {
  369. fmt.Println("HEY! This program should never be run as root.")
  370. fmt.Println("This program manages a user install of rar (possibly in ~/bin).")
  371. return
  372. }
  373. // Ok, we're not running as root, so we can create/check the cache directory.
  374. {
  375. err := os.MkdirAll(CACHE_DIR, 0755)
  376. if err != nil {
  377. fmt.Printf("Unable to create cache directory %s: %s\n", CACHE_DIR, err)
  378. return
  379. }
  380. }
  381. // Expire the cache
  382. count, ceerr := CacheExpire(CACHE_DIR, expire)
  383. if ceerr != nil {
  384. fmt.Printf("CacheExpire error: %s\n", ceerr)
  385. return
  386. }
  387. if count != 0 {
  388. fmt.Printf("CacheExpire: removed %d file(s).\n", count)
  389. }
  390. // Is rar installed?
  391. rar, rarerr := FindRar()
  392. var rarversion string
  393. if rarerr == nil {
  394. // Ok, rar has been located in the path.
  395. rarversion = RarVersion()
  396. }
  397. var arch_link string
  398. if info {
  399. fmt.Printf("RAR @ %s => Version %s\n", rar, rarversion)
  400. /*
  401. fmt.Printf("Current version: %s\n", go_version)
  402. fmt.Printf("OS Arch: %s\n", go_os_arch)
  403. fmt.Printf("GO ENV:\n%+v\n", goenv)
  404. fmt.Printf("Built by: %s\n", buildGoVersion)
  405. fmt.Printf("Go-Update: %s\n", buildVersion)
  406. */
  407. }
  408. if stop {
  409. fmt.Println("Ok, parse HTML file:")
  410. var filename = CACHE_DIR + "/" + RAR_URL_FILE
  411. var parse_link string
  412. fp, err := os.Open(filename)
  413. if err == nil {
  414. defer fp.Close()
  415. parse_link = ParseHtml(fp, "linux")
  416. fmt.Printf("Parse HTML link: %s\n", parse_link)
  417. } else {
  418. fmt.Printf("Missing cache file: %s\n", filename)
  419. }
  420. // parse_link => /rar/rarlinux-x64-720.tar.gz
  421. index := strings.LastIndex(parse_link, "/")
  422. if index != -1 {
  423. parse_link = parse_link[index+1:]
  424. }
  425. filename = CACHE_DIR + "/" + parse_link
  426. fp, err = os.Open(filename)
  427. if err == nil {
  428. defer fp.Close()
  429. // Ok! We have open tarball file.
  430. fmt.Printf("Tarball %s:\n", filename)
  431. read_tarball(fp)
  432. } else {
  433. fmt.Printf("Missing cache file: %s\n", filename)
  434. }
  435. fmt.Println("Get user-agent string from httpbin site:")
  436. var resp, _ = client.Get("https://httpbin.org/user-agent")
  437. defer resp.Body.Close()
  438. io.Copy(os.Stdout, resp.Body)
  439. fmt.Println("And we'll stop right here for now...")
  440. return
  441. }
  442. var resp, err = client.Get(RAR_URL)
  443. if err != nil {
  444. fmt.Printf("Get %s error: %s\n", RAR_URL, err)
  445. return
  446. }
  447. defer resp.Body.Close()
  448. // Verify that we got a status 200.
  449. if resp.StatusCode != 200 {
  450. fmt.Printf("From %s: %s\n", RAR_URL, resp.Status)
  451. return
  452. }
  453. // Possibly save the header into the cache directory as well.
  454. // resp.Header
  455. var filename = CACHE_DIR + "/" + RAR_URL_FILE
  456. fp, err := os.Create(filename)
  457. if err != nil {
  458. fmt.Printf("Create %s failed: %s\n", filename, err)
  459. return
  460. }
  461. fmt.Printf("%s : %s\n", RAR_URL, resp.Status)
  462. // Read from resp.Body and write it to the filename, and to parse_html.
  463. var read = io.TeeReader(resp.Body, fp)
  464. _ = read
  465. var download_link, rar_version string
  466. // Remove . from version
  467. rar_version = strings.ReplaceAll(rarversion, ".", "")
  468. arch_link = ParseHtml(read, "linux")
  469. if len(arch_link) == 0 {
  470. fmt.Printf("I wasn't able to locate the go download URL link for %s.\n", download_link)
  471. fmt.Printf("Check the file %s, maybe the link has changed?\n", filename)
  472. return
  473. }
  474. dl_version := GetVersionFromUrl(arch_link)
  475. if dl_version == rar_version {
  476. fmt.Println("You have the latest version of RAR.")
  477. return
  478. }
  479. fmt.Printf("Version: %s [have %s]\n", dl_version, rar_version)
  480. var arch_filename = UrlToFilename(arch_link)
  481. // Download
  482. //
  483. var dl_url string
  484. dl_url, err = RelativeToAbsoluteUrl(RAR_URL, arch_link)
  485. if err != nil {
  486. fmt.Printf("URL %s / href %s\n", RAR_URL, arch_link)
  487. fmt.Printf("Failed converting relative to absolute: %s\n", err)
  488. return
  489. }
  490. fmt.Printf("URL: %s\n", dl_url)
  491. resp, err = client.Get(dl_url)
  492. if err != nil {
  493. fmt.Printf("Get %s error: %s\n", dl_url, err)
  494. return
  495. }
  496. defer resp.Body.Close()
  497. // Verify that we got a status 200.
  498. if resp.StatusCode != 200 {
  499. fmt.Printf("From %s: %s\n", dl_url, resp.Status)
  500. return
  501. }
  502. // I've tried using/setting headers, etc for 304 Not modified, but
  503. // the website ignores it/doesn't use such things. Revisit.
  504. // Save file to cache
  505. filename = CACHE_DIR + "/" + arch_filename
  506. fp, err = os.Create(filename)
  507. if err != nil {
  508. fmt.Printf("Create %s failed: %s\n", filename, err)
  509. return
  510. }
  511. _, err = io.Copy(fp, resp.Body)
  512. if err != nil {
  513. fmt.Printf("Error saving %s\n", err)
  514. return
  515. }
  516. // Unarchive arch_filename into GOROOT.
  517. fmt.Printf("Extracting %s to %s ...", filename, "DERP")
  518. // err = ExtractTarball(filename, path)
  519. fmt.Println()
  520. }