cmd: rclone selfupdate (#5080)

Implements self-update command
Fixes #548
Fixes #5076
This commit is contained in:
Ivan Andreev 2021-03-11 22:39:30 +03:00 committed by GitHub
parent 4d8ef7bca7
commit 6fa74340a0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 811 additions and 7 deletions

1
.gitignore vendored
View file

@ -1,6 +1,7 @@
*~
_junk/
rclone
rclone.exe
build
docs/public
rclone.iml

View file

@ -47,6 +47,7 @@ import (
_ "github.com/rclone/rclone/cmd/reveal"
_ "github.com/rclone/rclone/cmd/rmdir"
_ "github.com/rclone/rclone/cmd/rmdirs"
_ "github.com/rclone/rclone/cmd/selfupdate"
_ "github.com/rclone/rclone/cmd/serve"
_ "github.com/rclone/rclone/cmd/settier"
_ "github.com/rclone/rclone/cmd/sha1sum"

View file

@ -548,6 +548,9 @@ func Main() {
setupRootCommand(Root)
AddBackendFlags()
if err := Root.Execute(); err != nil {
if strings.HasPrefix(err.Error(), "unknown command") {
Root.PrintErrf("You could use '%s selfupdate' to get latest features.\n\n", Root.CommandPath())
}
log.Fatalf("Fatal error: %v", err)
}
}

22
cmd/selfupdate/help.go Normal file
View file

@ -0,0 +1,22 @@
package selfupdate
// Note: "|" will be replaced by backticks in the help string below
var selfUpdateHelp string = `
This command downloads the latest release of rclone and replaces
the currently running binary. The download is verified with a hashsum
and cryptographically signed signature.
The |--version VER| flag, if given, will update to a concrete version
instead of the latest one. If you omit micro version from |VER| (for
example |1.53|), the latest matching micro version will be used.
If you previously installed rclone via a package manager, the package may
include local documentation or configure services. You may wish to update
with the flag |--package deb| or |--package rpm| (whichever is correct for
your OS) to update these too. This command with the default |--package zip|
will update only the rclone executable so the local manual may become
inaccurate after it.
Note: Windows forbids deletion of a currently running executable so this
command will rename the old executable to 'rclone.old.exe' upon success.
`

View file

@ -0,0 +1,474 @@
package selfupdate
import (
"archive/zip"
"bufio"
"bytes"
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"os"
"os/exec"
"path/filepath"
"regexp"
"runtime"
"strings"
"github.com/pkg/errors"
"github.com/rclone/rclone/cmd"
"github.com/rclone/rclone/fs"
"github.com/rclone/rclone/fs/config/flags"
"github.com/rclone/rclone/fs/fshttp"
"github.com/rclone/rclone/lib/random"
"github.com/spf13/cobra"
versionCmd "github.com/rclone/rclone/cmd/version"
)
// Options contains options for the self-update command
type Options struct {
Check bool
Output string // output path
Beta bool // mutually exclusive with Stable (false means "stable")
Stable bool // mutually exclusive with Beta
Version string
Package string // package format: zip, deb, rpm (empty string means "zip")
}
// Opt is options set via command line
var Opt = Options{}
func init() {
cmd.Root.AddCommand(cmdSelfUpdate)
cmdFlags := cmdSelfUpdate.Flags()
flags.BoolVarP(cmdFlags, &Opt.Check, "check", "", Opt.Check, "Check for latest release, do not download.")
flags.StringVarP(cmdFlags, &Opt.Output, "output", "", Opt.Output, "Save the downloaded binary at a given path (default: replace running binary)")
flags.BoolVarP(cmdFlags, &Opt.Stable, "stable", "", Opt.Stable, "Install stable release (this is the default)")
flags.BoolVarP(cmdFlags, &Opt.Beta, "beta", "", Opt.Beta, "Install beta release.")
flags.StringVarP(cmdFlags, &Opt.Version, "version", "", Opt.Version, "Install the given rclone version (default: latest)")
flags.StringVarP(cmdFlags, &Opt.Package, "package", "", Opt.Package, "Package format: zip|deb|rpm (default: zip)")
}
var cmdSelfUpdate = &cobra.Command{
Use: "selfupdate",
Aliases: []string{"self-update"},
Short: `Update the rclone binary.`,
Long: strings.ReplaceAll(selfUpdateHelp, "|", "`"),
Run: func(command *cobra.Command, args []string) {
cmd.CheckArgs(0, 0, command, args)
if Opt.Package == "" {
Opt.Package = "zip"
}
gotActionFlags := Opt.Stable || Opt.Beta || Opt.Output != "" || Opt.Version != "" || Opt.Package != "zip"
if Opt.Check && !gotActionFlags {
versionCmd.CheckVersion()
return
}
if Opt.Package != "zip" {
if Opt.Package != "deb" && Opt.Package != "rpm" {
log.Fatalf("--package should be one of zip|deb|rpm")
}
if runtime.GOOS != "linux" {
log.Fatalf(".deb and .rpm packages are supported only on Linux")
} else if os.Geteuid() != 0 && !Opt.Check {
log.Fatalf(".deb and .rpm must be installed by root")
}
if Opt.Output != "" && !Opt.Check {
fmt.Println("Warning: --output is ignored with --package deb|rpm")
}
}
if err := InstallUpdate(context.Background(), &Opt); err != nil {
log.Fatalf("Error: %v", err)
}
},
}
// GetVersion can get the latest release number from the download site
// or massage a stable release number - prepend semantic "v" prefix
// or find the latest micro release for a given major.minor release.
// Note: this will not be applied to beta releases.
func GetVersion(ctx context.Context, beta bool, version string) (newVersion, siteURL string, err error) {
siteURL = "https://downloads.rclone.org"
if beta {
siteURL = "https://beta.rclone.org"
}
if version == "" {
// Request the latest release number from the download site
_, newVersion, _, err = versionCmd.GetVersion(siteURL + "/version.txt")
return
}
newVersion = version
if version[0] != 'v' {
newVersion = "v" + version
}
if beta {
return
}
if valid, _ := regexp.MatchString(`^v\d+\.\d+(\.\d+)?$`, newVersion); !valid {
return "", siteURL, errors.New("invalid semantic version")
}
// Find the latest stable micro release
if strings.Count(newVersion, ".") == 1 {
html, err := downloadFile(ctx, siteURL)
if err != nil {
return "", siteURL, errors.Wrap(err, "failed to get list of releases")
}
reSubver := fmt.Sprintf(`href="\./%s\.\d+/"`, regexp.QuoteMeta(newVersion))
allSubvers := regexp.MustCompile(reSubver).FindAllString(string(html), -1)
if allSubvers == nil {
return "", siteURL, errors.New("could not find the minor release")
}
// Use the fact that releases in the index are sorted by date
lastSubver := allSubvers[len(allSubvers)-1]
newVersion = lastSubver[8 : len(lastSubver)-2]
}
return
}
// InstallUpdate performs rclone self-update
func InstallUpdate(ctx context.Context, opt *Options) error {
// Find the latest release number
if opt.Stable && opt.Beta {
return errors.New("--stable and --beta are mutually exclusive")
}
newVersion, siteURL, err := GetVersion(ctx, opt.Beta, opt.Version)
if err != nil {
return errors.Wrap(err, "unable to detect new version")
}
if newVersion == "" {
var err error
_, newVersion, _, err = versionCmd.GetVersion(siteURL + "/version.txt")
if err != nil {
return errors.Wrap(err, "unable to detect new version")
}
}
if newVersion == fs.Version {
fmt.Println("rclone is up to date")
return nil
}
// Install .deb/.rpm package if requested by user
if opt.Package == "deb" || opt.Package == "rpm" {
if opt.Check {
fmt.Println("Warning: --package flag is ignored in --check mode")
} else {
err := installPackage(ctx, opt.Beta, newVersion, siteURL, opt.Package)
if err == nil {
fmt.Printf("Successfully updated rclone package to version %s\n", newVersion)
}
return err
}
}
// Get the current executable path
executable, err := os.Executable()
if err != nil {
return errors.Wrap(err, "unable to find executable")
}
targetFile := opt.Output
if targetFile == "" {
targetFile = executable
}
if opt.Check {
fmt.Printf("Without --check this would install rclone version %s at %s\n", newVersion, targetFile)
return nil
}
// Make temporary file names and check for possible access errors in advance
var newFile string
if newFile, err = makeRandomExeName(targetFile, "new"); err != nil {
return err
}
savedFile := ""
if runtime.GOOS == "windows" {
savedFile = targetFile
if strings.HasSuffix(savedFile, ".exe") {
savedFile = savedFile[:len(savedFile)-4]
}
savedFile += ".old.exe"
}
if savedFile == executable || newFile == executable {
return fmt.Errorf("%s: a temporary file would overwrite the executable, specify a different --output path", targetFile)
}
if err := verifyAccess(targetFile); err != nil {
return err
}
// Download the update as a temporary file
err = downloadUpdate(ctx, opt.Beta, newVersion, siteURL, newFile, "zip")
if err != nil {
return errors.Wrap(err, "failed to update rclone")
}
err = replaceExecutable(targetFile, newFile, savedFile)
if err == nil {
fmt.Printf("Successfully updated rclone to version %s\n", newVersion)
}
return err
}
func installPackage(ctx context.Context, beta bool, version, siteURL, packageFormat string) error {
tempFile, err := ioutil.TempFile("", "rclone.*."+packageFormat)
if err != nil {
return errors.Wrap(err, "unable to write temporary package")
}
packageFile := tempFile.Name()
_ = tempFile.Close()
defer func() {
if rmErr := os.Remove(packageFile); rmErr != nil {
fs.Errorf(nil, "%s: could not remove temporary package: %v", packageFile, rmErr)
}
}()
if err := downloadUpdate(ctx, beta, version, siteURL, packageFile, packageFormat); err != nil {
return err
}
packageCommand := "dpkg"
if packageFormat == "rpm" {
packageCommand = "rpm"
}
cmd := exec.Command(packageCommand, "-i", packageFile)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to run %s: %v", packageCommand, err)
}
return nil
}
func replaceExecutable(targetFile, newFile, savedFile string) error {
// Copy permission bits from the old executable
// (it was extracted with mode 0755)
fileInfo, err := os.Lstat(targetFile)
if err == nil {
if err = os.Chmod(newFile, fileInfo.Mode()); err != nil {
return errors.Wrap(err, "failed to set permission")
}
}
if err = os.Remove(targetFile); os.IsNotExist(err) {
err = nil
}
if err != nil && savedFile != "" {
// Windows forbids removal of a running executable so we rename it.
// For starters, rename download as the original file with ".old.exe" appended.
var saveErr error
if saveErr = os.Remove(savedFile); os.IsNotExist(saveErr) {
saveErr = nil
}
if saveErr == nil {
saveErr = os.Rename(targetFile, savedFile)
}
if saveErr != nil {
// The ".old" file cannot be removed or cannot be renamed to.
// This usually means that the running executable has a name with ".old".
// This can happen in very rare cases, but we ought to handle it.
// Try inserting a randomness in the name to mitigate it.
fs.Debugf(nil, "%s: cannot replace old file, randomizing name", savedFile)
savedFile, saveErr = makeRandomExeName(targetFile, "old")
if saveErr == nil {
if saveErr = os.Remove(savedFile); os.IsNotExist(saveErr) {
saveErr = nil
}
}
if saveErr == nil {
saveErr = os.Rename(targetFile, savedFile)
}
}
if saveErr == nil {
fmt.Printf("The old executable was saved as %s\n", savedFile)
err = nil
}
}
if err == nil {
err = os.Rename(newFile, targetFile)
}
if err != nil {
if rmErr := os.Remove(newFile); rmErr != nil {
fs.Errorf(nil, "%s: could not remove temporary file: %v", newFile, rmErr)
}
return err
}
return nil
}
func makeRandomExeName(baseName, extension string) (string, error) {
const maxAttempts = 5
if runtime.GOOS == "windows" {
if strings.HasSuffix(baseName, ".exe") {
baseName = baseName[:len(baseName)-4]
}
extension += ".exe"
}
for attempt := 0; attempt < maxAttempts; attempt++ {
filename := fmt.Sprintf("%s.%s.%s", baseName, random.String(4), extension)
if _, err := os.Stat(filename); os.IsNotExist(err) {
return filename, nil
}
}
return "", fmt.Errorf("cannot find a file name like %s.xxxx.%s", baseName, extension)
}
func downloadUpdate(ctx context.Context, beta bool, version, siteURL, newFile, packageFormat string) error {
osName := runtime.GOOS
arch := runtime.GOARCH
if arch == "darwin" {
arch = "osx"
}
archiveFilename := fmt.Sprintf("rclone-%s-%s-%s.%s", version, osName, arch, packageFormat)
archiveURL := fmt.Sprintf("%s/%s/%s", siteURL, version, archiveFilename)
archiveBuf, err := downloadFile(ctx, archiveURL)
if err != nil {
return err
}
gotHash := sha256.Sum256(archiveBuf)
strHash := hex.EncodeToString(gotHash[:])
fs.Debugf(nil, "downloaded release archive with hashsum %s from %s", strHash, archiveURL)
// CI/CD does not provide hashsums for beta releases
if !beta {
if err := verifyHashsum(ctx, siteURL, version, archiveFilename, gotHash[:]); err != nil {
return err
}
}
if packageFormat == "deb" || packageFormat == "rpm" {
if err := ioutil.WriteFile(newFile, archiveBuf, 0644); err != nil {
return errors.Wrap(err, "cannot write temporary ."+packageFormat)
}
return nil
}
entryName := fmt.Sprintf("rclone-%s-%s-%s/rclone", version, osName, arch)
if runtime.GOOS == "windows" {
entryName += ".exe"
}
// Extract executable to a temporary file, then replace it by an instant rename
err = extractZipToFile(archiveBuf, entryName, newFile)
if err != nil {
return err
}
fs.Debugf(nil, "extracted %s to %s", entryName, newFile)
return nil
}
func verifyAccess(file string) error {
admin := "root"
if runtime.GOOS == "windows" {
admin = "Administrator"
}
fileInfo, fileErr := os.Lstat(file)
if fileErr != nil {
dir := filepath.Dir(file)
dirInfo, dirErr := os.Lstat(dir)
if dirErr != nil {
return dirErr
}
if !dirInfo.Mode().IsDir() {
return fmt.Errorf("%s: parent path is not a directory, specify a different path using --output", dir)
}
if !writable(dir) {
return fmt.Errorf("%s: directory is not writable, please run self-update as %s", dir, admin)
}
}
if fileErr == nil && !fileInfo.Mode().IsRegular() {
return fmt.Errorf("%s: path is not a normal file, specify a different path using --output", file)
}
if fileErr == nil && !writable(file) {
return fmt.Errorf("%s: file is not writable, run self-update as %s", file, admin)
}
return nil
}
func findFileHash(buf []byte, filename string) (hash []byte, err error) {
lines := bufio.NewScanner(bytes.NewReader(buf))
for lines.Scan() {
tokens := strings.Split(lines.Text(), " ")
if len(tokens) == 2 && tokens[1] == filename {
if hash, err := hex.DecodeString(tokens[0]); err == nil {
return hash, nil
}
}
}
return nil, fmt.Errorf("%s: unable to find hash", filename)
}
func extractZipToFile(buf []byte, entryName, newFile string) error {
zipReader, err := zip.NewReader(bytes.NewReader(buf), int64(len(buf)))
if err != nil {
return err
}
var reader io.ReadCloser
for _, entry := range zipReader.File {
if entry.Name == entryName {
reader, err = entry.Open()
break
}
}
if reader == nil || err != nil {
return fmt.Errorf("%s: file not found in archive", entryName)
}
defer func() {
_ = reader.Close()
}()
err = os.Remove(newFile)
if err != nil && !os.IsNotExist(err) {
return fmt.Errorf("%s: unable to create new file: %v", newFile, err)
}
writer, err := os.OpenFile(newFile, os.O_CREATE|os.O_EXCL|os.O_WRONLY, os.FileMode(0755))
if err != nil {
return err
}
_, err = io.Copy(writer, reader)
_ = writer.Close()
if err != nil {
if rmErr := os.Remove(newFile); rmErr != nil {
fs.Errorf(nil, "%s: could not remove temporary file: %v", newFile, rmErr)
}
}
return err
}
func downloadFile(ctx context.Context, url string) ([]byte, error) {
resp, err := fshttp.NewClient(ctx).Get(url)
if err != nil {
return nil, err
}
defer fs.CheckClose(resp.Body, &err)
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("failed with %s downloading %s", resp.Status, url)
}
return ioutil.ReadAll(resp.Body)
}

View file

@ -0,0 +1,198 @@
package selfupdate
import (
"context"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"regexp"
"runtime"
"testing"
"time"
"github.com/rclone/rclone/fs"
"github.com/rclone/rclone/fstest/testy"
"github.com/rclone/rclone/lib/random"
"github.com/stretchr/testify/assert"
)
func TestGetVersion(t *testing.T) {
testy.SkipUnreliable(t)
ctx := context.Background()
// a beta version can only have "v" prepended
resultVer, _, err := GetVersion(ctx, true, "1.2.3.4")
assert.NoError(t, err)
assert.Equal(t, "v1.2.3.4", resultVer)
// but a stable version syntax should be checked
_, _, err = GetVersion(ctx, false, "1")
assert.Error(t, err)
_, _, err = GetVersion(ctx, false, "1.")
assert.Error(t, err)
_, _, err = GetVersion(ctx, false, "1.2.")
assert.Error(t, err)
_, _, err = GetVersion(ctx, false, "1.2.3.4")
assert.Error(t, err)
// incomplete stable version should have micro release added
resultVer, _, err = GetVersion(ctx, false, "1.52")
assert.NoError(t, err)
assert.Equal(t, "v1.52.3", resultVer)
}
func makeTestDir() (testDir string, err error) {
const maxAttempts = 5
testDirBase := filepath.Join(os.TempDir(), "rclone-test-selfupdate.")
for attempt := 0; attempt < maxAttempts; attempt++ {
testDir = testDirBase + random.String(4)
err = os.MkdirAll(testDir, os.ModePerm)
if err == nil {
break
}
}
return
}
func TestInstallOnLinux(t *testing.T) {
testy.SkipUnreliable(t)
if runtime.GOOS != "linux" {
t.Skip("this is a Linux only test")
}
// Prepare for test
ctx := context.Background()
testDir, err := makeTestDir()
assert.NoError(t, err)
path := filepath.Join(testDir, "rclone")
defer func() {
_ = os.Chmod(path, 0644)
_ = os.RemoveAll(testDir)
}()
regexVer := regexp.MustCompile(`v[0-9]\S+`)
betaVer, _, err := GetVersion(ctx, true, "")
assert.NoError(t, err)
// Must do nothing if version isn't changing
assert.NoError(t, InstallUpdate(ctx, &Options{Beta: true, Output: path, Version: fs.Version}))
// Must fail on non-writable file
assert.NoError(t, ioutil.WriteFile(path, []byte("test"), 0644))
assert.NoError(t, os.Chmod(path, 0000))
err = (InstallUpdate(ctx, &Options{Beta: true, Output: path}))
assert.Error(t, err)
assert.Contains(t, err.Error(), "run self-update as root")
// Must keep non-standard permissions
assert.NoError(t, os.Chmod(path, 0644))
assert.NoError(t, InstallUpdate(ctx, &Options{Beta: true, Output: path}))
info, err := os.Stat(path)
assert.NoError(t, err)
assert.Equal(t, os.FileMode(0644), info.Mode().Perm())
// Must remove temporary files
files, err := ioutil.ReadDir(testDir)
assert.NoError(t, err)
assert.Equal(t, 1, len(files))
// Must contain valid executable
assert.NoError(t, os.Chmod(path, 0755))
cmd := exec.Command(path, "version")
output, err := cmd.CombinedOutput()
assert.NoError(t, err)
assert.True(t, cmd.ProcessState.Success())
assert.Equal(t, betaVer, regexVer.FindString(string(output)))
}
func TestRenameOnWindows(t *testing.T) {
testy.SkipUnreliable(t)
if runtime.GOOS != "windows" {
t.Skip("this is a Windows only test")
}
// Prepare for test
ctx := context.Background()
testDir, err := makeTestDir()
assert.NoError(t, err)
defer func() {
_ = os.RemoveAll(testDir)
}()
path := filepath.Join(testDir, "rclone.exe")
regexVer := regexp.MustCompile(`v[0-9]\S+`)
stableVer, _, err := GetVersion(ctx, false, "")
assert.NoError(t, err)
betaVer, _, err := GetVersion(ctx, true, "")
assert.NoError(t, err)
// Must not create temporary files when target doesn't exist
assert.NoError(t, InstallUpdate(ctx, &Options{Beta: true, Output: path}))
files, err := ioutil.ReadDir(testDir)
assert.NoError(t, err)
assert.Equal(t, 1, len(files))
// Must save running executable as the "old" file
cmdWait := exec.Command(path, "config")
stdinWait, err := cmdWait.StdinPipe() // Make it run waiting for input
assert.NoError(t, err)
assert.NoError(t, cmdWait.Start())
assert.NoError(t, InstallUpdate(ctx, &Options{Beta: false, Output: path}))
files, err = ioutil.ReadDir(testDir)
assert.NoError(t, err)
assert.Equal(t, 2, len(files))
pathOld := filepath.Join(testDir, "rclone.old.exe")
_, err = os.Stat(pathOld)
assert.NoError(t, err)
cmd := exec.Command(path, "version")
output, err := cmd.CombinedOutput()
assert.NoError(t, err)
assert.True(t, cmd.ProcessState.Success())
assert.Equal(t, stableVer, regexVer.FindString(string(output)))
cmdOld := exec.Command(pathOld, "version")
output, err = cmdOld.CombinedOutput()
assert.NoError(t, err)
assert.True(t, cmdOld.ProcessState.Success())
assert.Equal(t, betaVer, regexVer.FindString(string(output)))
// Stop previous waiting executable, run new and saved executables
_ = stdinWait.Close()
_ = cmdWait.Wait()
time.Sleep(100 * time.Millisecond)
cmdWait = exec.Command(path, "config")
stdinWait, err = cmdWait.StdinPipe()
assert.NoError(t, err)
assert.NoError(t, cmdWait.Start())
cmdWaitOld := exec.Command(pathOld, "config")
stdinWaitOld, err := cmdWaitOld.StdinPipe()
assert.NoError(t, err)
assert.NoError(t, cmdWaitOld.Start())
// Updating when the "old" executable is running must produce a random "old" file
assert.NoError(t, InstallUpdate(ctx, &Options{Beta: true, Output: path}))
files, err = ioutil.ReadDir(testDir)
assert.NoError(t, err)
assert.Equal(t, 3, len(files))
// Stop all waiting executables
_ = stdinWait.Close()
_ = cmdWait.Wait()
_ = stdinWaitOld.Close()
_ = cmdWaitOld.Wait()
time.Sleep(100 * time.Millisecond)
}

72
cmd/selfupdate/verify.go Normal file
View file

@ -0,0 +1,72 @@
package selfupdate
import (
"bytes"
"context"
"fmt"
"strings"
"github.com/pkg/errors"
"github.com/rclone/rclone/fs"
"golang.org/x/crypto/openpgp"
"golang.org/x/crypto/openpgp/clearsign"
)
var ncwPublicKeyPGP = `-----BEGIN PGP PUBLIC KEY BLOCK-----
mQGiBDuy3V0RBADVQOAF5aFiCxD3t2h6iAF2WMiaMlgZ6kX2i/u7addNkzX71VU9
7NpI0SnsP5YWt+gEedST6OmFbtLfZWCR4KWn5XnNdjCMNhxaH6WccVqNm4ALPIqT
59uVjkgf8RISmmoNJ1d+2wMWjQTUfwOEmoIgH6n+2MYNUKuctBrwAACflwCg1I1Q
O/prv/5hczdpQCs+fL87DxsD/Rt7pIXvsIOZyQWbIhSvNpGalJuMkW5Jx92UjsE9
1Ipo3Xr6SGRPgW9+NxAZAsiZfCX/19knAyNrN9blwL0rcPDnkhdGwK69kfjF+wq+
QbogRGodbKhqY4v+cMNkKiemBuTQiWPkpKjifwNsD1fNjNKfDP3pJ64Yz7a4fuzV
X1YwBACpKVuEen34lmcX6ziY4jq8rKibKBs4JjQCRO24kYoHDULVe+RS9krQWY5b
e0foDhru4dsKccefK099G+WEzKVCKxupstWkTT/iJwajR8mIqd4AhD0wO9W3MCfV
Ov8ykMDZ7qBWk1DHc87Ep3W1o8t8wq74ifV+HjhhWg8QAylXg7QlTmljayBDcmFp
Zy1Xb29kIDxuaWNrQGNyYWlnLXdvb2QuY29tPohxBBMRCAAxBQsHCgMEAxUDAgMW
AgECF4AWIQT79zfs6firGGBL0qyTk14C/ztU+gUCXjg2UgIZAQAKCRCTk14C/ztU
+lmmAJ4jH5FyULzStjisuTvHLTVz6G44eQCfaR5QGZFPseenE5ic2WeQcBcmtoG5
Ag0EO7LdgRAIAI6QdFBg3/xa1gFKPYy1ihV9eSdGqwWZGJvokWsfCvHy5180tj/v
UNOLAJrdqglMSvevNTXe8bT65D6423AAsLhch9wq/aNqrHolTYABzxRigjcS1//T
yln5naGUzlVQXDVfrDk3Md/NrkdOFj7r/YyMF0+iWwpFz2qAjL95i5wfVZ1kWGrT
2AmivE1wD1sWT/Ja3FDI0NRkU0Nbz/a0TKe4ml8iLVtZXpTRbxxCCPdkHXXgSyu1
eZ4NrF/wTJuvwGn12TJ1EF95aVkHxAUw0+KmLGdcyBG+IKuHamrsjWIAXGXV///K
AxPgUthccQ03HMjltFsrdmen5Q034YM3eOsAAwUH/jAKiIAA8LpZmZPnt9GZ4+Ol
Zp22VAfyfDOFl4Ol+cWjkLAgjAFsm5gnOKcRSE/9XPxnQqkhw7+ZygYuUMgTDJ99
/5IM1UQL3ooS+oFrDaE99S8bLeOe17skcdXcA/K83VqD9m93rQRnbtD+75zqKkZn
9WNFyKCXg5P6PFPdNYRtlQKOcwFR9mHRLUmapQSAM8Y2pCgALZ7GViKQca8/TT1T
gZk9fJMZYGez+IlOPxTJxjn80+vywk4/wdIWSiQj+8u5RzT9sjmm77wbMVNGRqYd
W/EemW9Zz9vi0CIvJGgbPMqcuxw8e/5lnuQ6Mi3uDR0P2RNIAhFrdZpVSME8xQaI
RgQYEQIABgUCO7LdgQAKCRCTk14C/ztU+mLBAKC2cdFy7eLaQAvyzcE2VK6HVIjn
JACguA00bxLQuJ4+RCJrLFZP8ZlN2sc=
=TtR5
-----END PGP PUBLIC KEY BLOCK-----`
func verifyHashsum(ctx context.Context, siteURL, version, archive string, hash []byte) error {
sumsURL := fmt.Sprintf("%s/%s/SHA256SUMS", siteURL, version)
sumsBuf, err := downloadFile(ctx, sumsURL)
if err != nil {
return err
}
fs.Debugf(nil, "downloaded hashsum list: %s", sumsURL)
keyRing, err := openpgp.ReadArmoredKeyRing(strings.NewReader(ncwPublicKeyPGP))
if err != nil {
return errors.New("unsupported signing key")
}
block, rest := clearsign.Decode(sumsBuf)
// block.Bytes = block.Bytes[1:] // uncomment to test invalid signature
_, err = openpgp.CheckDetachedSignature(keyRing, bytes.NewReader(block.Bytes), block.ArmoredSignature.Body)
if err != nil || len(rest) > 0 {
return errors.New("invalid hashsum signature")
}
wantHash, err := findFileHash(sumsBuf, archive)
if err != nil {
return err
}
if !bytes.Equal(hash, wantHash) {
return fmt.Errorf("archive hash mismatch: want %02x vs got %02x", wantHash, hash)
}
return nil
}

View file

@ -0,0 +1,11 @@
// +build !windows,!plan9,!js
package selfupdate
import (
"golang.org/x/sys/unix"
)
func writable(path string) bool {
return unix.Access(path, unix.W_OK) == nil
}

View file

@ -0,0 +1,7 @@
// +build plan9 js
package selfupdate
func writable(path string) bool {
return true
}

View file

@ -0,0 +1,16 @@
// +build windows
package selfupdate
import (
"os"
)
func writable(path string) bool {
info, err := os.Stat(path)
const UserWritableBit = 128
if err == nil {
return info.Mode().Perm()&UserWritableBit != 0
}
return false
}

View file

@ -59,7 +59,7 @@ Or
Run: func(command *cobra.Command, args []string) {
cmd.CheckArgs(0, 0, command, args)
if check {
checkVersion()
CheckVersion()
} else {
cmd.ShowVersion()
}
@ -74,8 +74,8 @@ func stripV(s string) string {
return s
}
// getVersion gets the version by checking the download repository passed in
func getVersion(url string) (v *semver.Version, vs string, date time.Time, err error) {
// GetVersion gets the version available for download
func GetVersion(url string) (v *semver.Version, vs string, date time.Time, err error) {
resp, err := http.Get(url)
if err != nil {
return v, vs, date, err
@ -101,9 +101,8 @@ func getVersion(url string) (v *semver.Version, vs string, date time.Time, err e
return v, vs, date, err
}
// check the current version against available versions
func checkVersion() {
// Get Current version
// CheckVersion checks the installed version against available downloads
func CheckVersion() {
vCurrent, err := semver.NewVersion(stripV(fs.Version))
if err != nil {
fs.Errorf(nil, "Failed to parse version: %v", err)
@ -111,7 +110,7 @@ func checkVersion() {
const timeFormat = "2006-01-02"
printVersion := func(what, url string) {
v, vs, t, err := getVersion(url + "version.txt")
v, vs, t, err := GetVersion(url + "version.txt")
if err != nil {
fs.Errorf(nil, "Failed to get rclone %s version: %v", what, err)
return