selfupdate.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502
  1. //go:build !noselfupdate
  2. // Package selfupdate provides the selfupdate command.
  3. package selfupdate
  4. import (
  5. "archive/zip"
  6. "bufio"
  7. "bytes"
  8. "context"
  9. "crypto/sha256"
  10. _ "embed"
  11. "encoding/hex"
  12. "errors"
  13. "fmt"
  14. "io"
  15. "log"
  16. "net/http"
  17. "os"
  18. "os/exec"
  19. "path/filepath"
  20. "regexp"
  21. "runtime"
  22. "strings"
  23. "github.com/rclone/rclone/cmd"
  24. "github.com/rclone/rclone/cmd/cmount"
  25. "github.com/rclone/rclone/fs"
  26. "github.com/rclone/rclone/fs/config/flags"
  27. "github.com/rclone/rclone/fs/fshttp"
  28. "github.com/rclone/rclone/lib/buildinfo"
  29. "github.com/rclone/rclone/lib/random"
  30. "github.com/spf13/cobra"
  31. versionCmd "github.com/rclone/rclone/cmd/version"
  32. )
  33. //go:embed selfupdate.md
  34. var selfUpdateHelp string
  35. // Options contains options for the self-update command
  36. type Options struct {
  37. Check bool
  38. Output string // output path
  39. Beta bool // mutually exclusive with Stable (false means "stable")
  40. Stable bool // mutually exclusive with Beta
  41. Version string
  42. Package string // package format: zip, deb, rpm (empty string means "zip")
  43. }
  44. // Opt is options set via command line
  45. var Opt = Options{}
  46. func init() {
  47. cmd.Root.AddCommand(cmdSelfUpdate)
  48. cmdFlags := cmdSelfUpdate.Flags()
  49. flags.BoolVarP(cmdFlags, &Opt.Check, "check", "", Opt.Check, "Check for latest release, do not download", "")
  50. flags.StringVarP(cmdFlags, &Opt.Output, "output", "", Opt.Output, "Save the downloaded binary at a given path (default: replace running binary)", "")
  51. flags.BoolVarP(cmdFlags, &Opt.Stable, "stable", "", Opt.Stable, "Install stable release (this is the default)", "")
  52. flags.BoolVarP(cmdFlags, &Opt.Beta, "beta", "", Opt.Beta, "Install beta release", "")
  53. flags.StringVarP(cmdFlags, &Opt.Version, "version", "", Opt.Version, "Install the given rclone version (default: latest)", "")
  54. flags.StringVarP(cmdFlags, &Opt.Package, "package", "", Opt.Package, "Package format: zip|deb|rpm (default: zip)", "")
  55. }
  56. var cmdSelfUpdate = &cobra.Command{
  57. Use: "selfupdate",
  58. Aliases: []string{"self-update"},
  59. Short: `Update the rclone binary.`,
  60. Long: selfUpdateHelp,
  61. Annotations: map[string]string{
  62. "versionIntroduced": "v1.55",
  63. },
  64. Run: func(command *cobra.Command, args []string) {
  65. ctx := context.Background()
  66. cmd.CheckArgs(0, 0, command, args)
  67. if Opt.Package == "" {
  68. Opt.Package = "zip"
  69. }
  70. gotActionFlags := Opt.Stable || Opt.Beta || Opt.Output != "" || Opt.Version != "" || Opt.Package != "zip"
  71. if Opt.Check && !gotActionFlags {
  72. versionCmd.CheckVersion(ctx)
  73. return
  74. }
  75. if Opt.Package != "zip" {
  76. if Opt.Package != "deb" && Opt.Package != "rpm" {
  77. log.Fatalf("--package should be one of zip|deb|rpm")
  78. }
  79. if runtime.GOOS != "linux" {
  80. log.Fatalf(".deb and .rpm packages are supported only on Linux")
  81. } else if os.Geteuid() != 0 && !Opt.Check {
  82. log.Fatalf(".deb and .rpm must be installed by root")
  83. }
  84. if Opt.Output != "" && !Opt.Check {
  85. fmt.Println("Warning: --output is ignored with --package deb|rpm")
  86. }
  87. }
  88. if err := InstallUpdate(context.Background(), &Opt); err != nil {
  89. log.Fatalf("Error: %v", err)
  90. }
  91. },
  92. }
  93. // GetVersion can get the latest release number from the download site
  94. // or massage a stable release number - prepend semantic "v" prefix
  95. // or find the latest micro release for a given major.minor release.
  96. // Note: this will not be applied to beta releases.
  97. func GetVersion(ctx context.Context, beta bool, version string) (newVersion, siteURL string, err error) {
  98. siteURL = "https://downloads.rclone.org"
  99. if beta {
  100. siteURL = "https://beta.rclone.org"
  101. }
  102. if version == "" {
  103. // Request the latest release number from the download site
  104. _, newVersion, _, err = versionCmd.GetVersion(ctx, siteURL+"/version.txt")
  105. return
  106. }
  107. newVersion = version
  108. if version[0] != 'v' {
  109. newVersion = "v" + version
  110. }
  111. if beta {
  112. return
  113. }
  114. if valid, _ := regexp.MatchString(`^v\d+\.\d+(\.\d+)?$`, newVersion); !valid {
  115. return "", siteURL, errors.New("invalid semantic version")
  116. }
  117. // Find the latest stable micro release
  118. if strings.Count(newVersion, ".") == 1 {
  119. html, err := downloadFile(ctx, siteURL)
  120. if err != nil {
  121. return "", siteURL, fmt.Errorf("failed to get list of releases: %w", err)
  122. }
  123. reSubver := fmt.Sprintf(`href="\./%s\.\d+/"`, regexp.QuoteMeta(newVersion))
  124. allSubvers := regexp.MustCompile(reSubver).FindAllString(string(html), -1)
  125. if allSubvers == nil {
  126. return "", siteURL, errors.New("could not find the minor release")
  127. }
  128. // Use the fact that releases in the index are sorted by date
  129. lastSubver := allSubvers[len(allSubvers)-1]
  130. newVersion = lastSubver[8 : len(lastSubver)-2]
  131. }
  132. return
  133. }
  134. // InstallUpdate performs rclone self-update
  135. func InstallUpdate(ctx context.Context, opt *Options) error {
  136. // Find the latest release number
  137. if opt.Stable && opt.Beta {
  138. return errors.New("--stable and --beta are mutually exclusive")
  139. }
  140. // The `cmount` tag is added by cmd/cmount/mount.go only if build is static.
  141. _, tags := buildinfo.GetLinkingAndTags()
  142. if strings.Contains(" "+tags+" ", " cmount ") && !cmount.ProvidedBy(runtime.GOOS) {
  143. return errors.New("updating would discard the mount FUSE capability, aborting")
  144. }
  145. newVersion, siteURL, err := GetVersion(ctx, opt.Beta, opt.Version)
  146. if err != nil {
  147. return fmt.Errorf("unable to detect new version: %w", err)
  148. }
  149. oldVersion := fs.Version
  150. if newVersion == oldVersion {
  151. fs.Logf(nil, "rclone is up to date")
  152. return nil
  153. }
  154. // Install .deb/.rpm package if requested by user
  155. if opt.Package == "deb" || opt.Package == "rpm" {
  156. if opt.Check {
  157. fmt.Println("Warning: --package flag is ignored in --check mode")
  158. } else {
  159. err := installPackage(ctx, opt.Beta, newVersion, siteURL, opt.Package)
  160. if err == nil {
  161. fs.Logf(nil, "Successfully updated rclone package from version %s to version %s", oldVersion, newVersion)
  162. }
  163. return err
  164. }
  165. }
  166. // Get the current executable path
  167. executable, err := os.Executable()
  168. if err != nil {
  169. return fmt.Errorf("unable to find executable: %w", err)
  170. }
  171. targetFile := opt.Output
  172. if targetFile == "" {
  173. targetFile = executable
  174. }
  175. if opt.Check {
  176. fmt.Printf("Without --check this would install rclone version %s at %s\n", newVersion, targetFile)
  177. return nil
  178. }
  179. // Make temporary file names and check for possible access errors in advance
  180. var newFile string
  181. if newFile, err = makeRandomExeName(targetFile, "new"); err != nil {
  182. return err
  183. }
  184. savedFile := ""
  185. if runtime.GOOS == "windows" {
  186. savedFile = targetFile
  187. savedFile = strings.TrimSuffix(savedFile, ".exe")
  188. savedFile += ".old.exe"
  189. }
  190. if savedFile == executable || newFile == executable {
  191. return fmt.Errorf("%s: a temporary file would overwrite the executable, specify a different --output path", targetFile)
  192. }
  193. if err := verifyAccess(targetFile); err != nil {
  194. return err
  195. }
  196. // Download the update as a temporary file
  197. err = downloadUpdate(ctx, opt.Beta, newVersion, siteURL, newFile, "zip")
  198. if err != nil {
  199. return fmt.Errorf("failed to update rclone: %w", err)
  200. }
  201. err = replaceExecutable(targetFile, newFile, savedFile)
  202. if err == nil {
  203. fs.Logf(nil, "Successfully updated rclone from version %s to version %s", oldVersion, newVersion)
  204. }
  205. return err
  206. }
  207. func installPackage(ctx context.Context, beta bool, version, siteURL, packageFormat string) error {
  208. tempFile, err := os.CreateTemp("", "rclone.*."+packageFormat)
  209. if err != nil {
  210. return fmt.Errorf("unable to write temporary package: %w", err)
  211. }
  212. packageFile := tempFile.Name()
  213. _ = tempFile.Close()
  214. defer func() {
  215. if rmErr := os.Remove(packageFile); rmErr != nil {
  216. fs.Errorf(nil, "%s: could not remove temporary package: %v", packageFile, rmErr)
  217. }
  218. }()
  219. if err := downloadUpdate(ctx, beta, version, siteURL, packageFile, packageFormat); err != nil {
  220. return err
  221. }
  222. packageCommand := "dpkg"
  223. if packageFormat == "rpm" {
  224. packageCommand = "rpm"
  225. }
  226. cmd := exec.Command(packageCommand, "-i", packageFile)
  227. cmd.Stdout = os.Stdout
  228. cmd.Stderr = os.Stderr
  229. if err := cmd.Run(); err != nil {
  230. return fmt.Errorf("failed to run %s: %v", packageCommand, err)
  231. }
  232. return nil
  233. }
  234. func replaceExecutable(targetFile, newFile, savedFile string) error {
  235. // Copy permission bits from the old executable
  236. // (it was extracted with mode 0755)
  237. fileInfo, err := os.Lstat(targetFile)
  238. if err == nil {
  239. if err = os.Chmod(newFile, fileInfo.Mode()); err != nil {
  240. return fmt.Errorf("failed to set permission: %w", err)
  241. }
  242. }
  243. if err = os.Remove(targetFile); os.IsNotExist(err) {
  244. err = nil
  245. }
  246. if err != nil && savedFile != "" {
  247. // Windows forbids removal of a running executable so we rename it.
  248. // For starters, rename download as the original file with ".old.exe" appended.
  249. var saveErr error
  250. if saveErr = os.Remove(savedFile); os.IsNotExist(saveErr) {
  251. saveErr = nil
  252. }
  253. if saveErr == nil {
  254. saveErr = os.Rename(targetFile, savedFile)
  255. }
  256. if saveErr != nil {
  257. // The ".old" file cannot be removed or cannot be renamed to.
  258. // This usually means that the running executable has a name with ".old".
  259. // This can happen in very rare cases, but we ought to handle it.
  260. // Try inserting a randomness in the name to mitigate it.
  261. fs.Debugf(nil, "%s: cannot replace old file, randomizing name", savedFile)
  262. savedFile, saveErr = makeRandomExeName(targetFile, "old")
  263. if saveErr == nil {
  264. if saveErr = os.Remove(savedFile); os.IsNotExist(saveErr) {
  265. saveErr = nil
  266. }
  267. }
  268. if saveErr == nil {
  269. saveErr = os.Rename(targetFile, savedFile)
  270. }
  271. }
  272. if saveErr == nil {
  273. fs.Infof(nil, "The old executable was saved as %s", savedFile)
  274. err = nil
  275. }
  276. }
  277. if err == nil {
  278. err = os.Rename(newFile, targetFile)
  279. }
  280. if err != nil {
  281. if rmErr := os.Remove(newFile); rmErr != nil {
  282. fs.Errorf(nil, "%s: could not remove temporary file: %v", newFile, rmErr)
  283. }
  284. return err
  285. }
  286. return nil
  287. }
  288. func makeRandomExeName(baseName, extension string) (string, error) {
  289. const maxAttempts = 5
  290. if runtime.GOOS == "windows" {
  291. baseName = strings.TrimSuffix(baseName, ".exe")
  292. extension += ".exe"
  293. }
  294. for attempt := 0; attempt < maxAttempts; attempt++ {
  295. filename := fmt.Sprintf("%s.%s.%s", baseName, random.String(4), extension)
  296. if _, err := os.Stat(filename); os.IsNotExist(err) {
  297. return filename, nil
  298. }
  299. }
  300. return "", fmt.Errorf("cannot find a file name like %s.xxxx.%s", baseName, extension)
  301. }
  302. func downloadUpdate(ctx context.Context, beta bool, version, siteURL, newFile, packageFormat string) error {
  303. osName := runtime.GOOS
  304. if osName == "darwin" {
  305. osName = "osx"
  306. }
  307. arch := runtime.GOARCH
  308. if arch == "arm" {
  309. // Check the ARM compatibility level of the current CPU.
  310. // We don't know if this matches the rclone binary currently running, it
  311. // could for example be a ARMv6 variant running on a ARMv7 compatible CPU,
  312. // so we will simply pick the best possible variant.
  313. switch buildinfo.GetSupportedGOARM() {
  314. case 7:
  315. // This system can run any binaries built with GOARCH=arm, including GOARM=7.
  316. // Pick the ARMv7 variant of rclone, published with suffix "arm-v7".
  317. arch = "arm-v7"
  318. case 6:
  319. // This system can run binaries built with GOARCH=arm and GOARM=6 or lower.
  320. // Pick the ARMv6 variant of rclone, published with suffix "arm-v6".
  321. arch = "arm-v6"
  322. case 5:
  323. // This system can only run binaries built with GOARCH=arm and GOARM=5.
  324. // Pick the ARMv5 variant of rclone, which also works without hardfloat,
  325. // published with suffix "arm".
  326. arch = "arm"
  327. }
  328. }
  329. archiveFilename := fmt.Sprintf("rclone-%s-%s-%s.%s", version, osName, arch, packageFormat)
  330. archiveURL := fmt.Sprintf("%s/%s/%s", siteURL, version, archiveFilename)
  331. archiveBuf, err := downloadFile(ctx, archiveURL)
  332. if err != nil {
  333. return err
  334. }
  335. gotHash := sha256.Sum256(archiveBuf)
  336. strHash := hex.EncodeToString(gotHash[:])
  337. fs.Debugf(nil, "downloaded release archive with hashsum %s from %s", strHash, archiveURL)
  338. // CI/CD does not provide hashsums for beta releases
  339. if !beta {
  340. if err := verifyHashsum(ctx, siteURL, version, archiveFilename, gotHash[:]); err != nil {
  341. return err
  342. }
  343. }
  344. if packageFormat == "deb" || packageFormat == "rpm" {
  345. if err := os.WriteFile(newFile, archiveBuf, 0644); err != nil {
  346. return fmt.Errorf("cannot write temporary .%s: %w", packageFormat, err)
  347. }
  348. return nil
  349. }
  350. entryName := fmt.Sprintf("rclone-%s-%s-%s/rclone", version, osName, arch)
  351. if runtime.GOOS == "windows" {
  352. entryName += ".exe"
  353. }
  354. // Extract executable to a temporary file, then replace it by an instant rename
  355. err = extractZipToFile(archiveBuf, entryName, newFile)
  356. if err != nil {
  357. return err
  358. }
  359. fs.Debugf(nil, "extracted %s to %s", entryName, newFile)
  360. return nil
  361. }
  362. func verifyAccess(file string) error {
  363. admin := "root"
  364. if runtime.GOOS == "windows" {
  365. admin = "Administrator"
  366. }
  367. fileInfo, fileErr := os.Lstat(file)
  368. if fileErr != nil {
  369. dir := filepath.Dir(file)
  370. dirInfo, dirErr := os.Lstat(dir)
  371. if dirErr != nil {
  372. return dirErr
  373. }
  374. if !dirInfo.Mode().IsDir() {
  375. return fmt.Errorf("%s: parent path is not a directory, specify a different path using --output", dir)
  376. }
  377. if !writable(dir) {
  378. return fmt.Errorf("%s: directory is not writable, please run self-update as %s", dir, admin)
  379. }
  380. }
  381. if fileErr == nil && !fileInfo.Mode().IsRegular() {
  382. return fmt.Errorf("%s: path is not a normal file, specify a different path using --output", file)
  383. }
  384. if fileErr == nil && !writable(file) {
  385. return fmt.Errorf("%s: file is not writable, run self-update as %s", file, admin)
  386. }
  387. return nil
  388. }
  389. func findFileHash(buf []byte, filename string) (hash []byte, err error) {
  390. lines := bufio.NewScanner(bytes.NewReader(buf))
  391. for lines.Scan() {
  392. tokens := strings.Split(lines.Text(), " ")
  393. if len(tokens) == 2 && tokens[1] == filename {
  394. if hash, err := hex.DecodeString(tokens[0]); err == nil {
  395. return hash, nil
  396. }
  397. }
  398. }
  399. return nil, fmt.Errorf("%s: unable to find hash", filename)
  400. }
  401. func extractZipToFile(buf []byte, entryName, newFile string) error {
  402. zipReader, err := zip.NewReader(bytes.NewReader(buf), int64(len(buf)))
  403. if err != nil {
  404. return err
  405. }
  406. var reader io.ReadCloser
  407. for _, entry := range zipReader.File {
  408. if entry.Name == entryName {
  409. reader, err = entry.Open()
  410. break
  411. }
  412. }
  413. if reader == nil || err != nil {
  414. return fmt.Errorf("%s: file not found in archive", entryName)
  415. }
  416. defer func() {
  417. _ = reader.Close()
  418. }()
  419. err = os.Remove(newFile)
  420. if err != nil && !os.IsNotExist(err) {
  421. return fmt.Errorf("%s: unable to create new file: %v", newFile, err)
  422. }
  423. writer, err := os.OpenFile(newFile, os.O_CREATE|os.O_EXCL|os.O_WRONLY, os.FileMode(0755))
  424. if err != nil {
  425. return err
  426. }
  427. _, err = io.Copy(writer, reader)
  428. _ = writer.Close()
  429. if err != nil {
  430. if rmErr := os.Remove(newFile); rmErr != nil {
  431. fs.Errorf(nil, "%s: could not remove temporary file: %v", newFile, rmErr)
  432. }
  433. }
  434. return err
  435. }
  436. func downloadFile(ctx context.Context, url string) ([]byte, error) {
  437. resp, err := fshttp.NewClient(ctx).Get(url)
  438. if err != nil {
  439. return nil, err
  440. }
  441. defer fs.CheckClose(resp.Body, &err)
  442. if resp.StatusCode != http.StatusOK {
  443. return nil, fmt.Errorf("failed with %s downloading %s", resp.Status, url)
  444. }
  445. return io.ReadAll(resp.Body)
  446. }