6e1a3987b7
This commit adds a command called `self-update` which downloads the latest released version of restic from GitHub and replacing the current binary with it. It does not rely on any external program (so it'll work everywhere), but still verifies the GPG signature using the embedded GPG public key. By default, the `self-update` command is hidden behind the `selfupdate` built tag, which is only set when restic is built using `build.go`. The reason for this is that downstream distributions will then not include the command by default, so users are encouraged to use the platform-specific distribution mechanism.
173 lines
3.5 KiB
Go
173 lines
3.5 KiB
Go
package selfupdate
|
|
|
|
import (
|
|
"archive/zip"
|
|
"bufio"
|
|
"bytes"
|
|
"compress/bzip2"
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
func findHash(buf []byte, filename string) (hash []byte, err error) {
|
|
sc := bufio.NewScanner(bytes.NewReader(buf))
|
|
for sc.Scan() {
|
|
data := strings.Split(sc.Text(), " ")
|
|
if len(data) != 2 {
|
|
continue
|
|
}
|
|
|
|
if data[1] == filename {
|
|
h, err := hex.DecodeString(data[0])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return h, nil
|
|
}
|
|
}
|
|
|
|
return nil, fmt.Errorf("hash for file %v not found", filename)
|
|
}
|
|
|
|
func extractToFile(buf []byte, filename, target string, printf func(string, ...interface{})) error {
|
|
var mode = os.FileMode(0755)
|
|
|
|
// get information about the target file
|
|
fi, err := os.Lstat(target)
|
|
if err == nil {
|
|
mode = fi.Mode()
|
|
}
|
|
|
|
var rd io.Reader = bytes.NewReader(buf)
|
|
switch filepath.Ext(filename) {
|
|
case ".bz2":
|
|
rd = bzip2.NewReader(rd)
|
|
case ".zip":
|
|
zrd, err := zip.NewReader(bytes.NewReader(buf), int64(len(buf)))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(zrd.File) != 1 {
|
|
return errors.New("ZIP archive contains more than one file")
|
|
}
|
|
|
|
file, err := zrd.File[0].Open()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
defer func() {
|
|
_ = file.Close()
|
|
}()
|
|
|
|
rd = file
|
|
}
|
|
|
|
err = os.Remove(target)
|
|
if os.IsNotExist(err) {
|
|
err = nil
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("unable to remove target file: %v", err)
|
|
}
|
|
|
|
dest, err := os.OpenFile(target, os.O_CREATE|os.O_EXCL|os.O_WRONLY, mode)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
n, err := io.Copy(dest, rd)
|
|
if err != nil {
|
|
_ = dest.Close()
|
|
_ = os.Remove(dest.Name())
|
|
return err
|
|
}
|
|
|
|
err = dest.Close()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
printf("saved %d bytes in %v\n", n, dest.Name())
|
|
return nil
|
|
}
|
|
|
|
// DownloadLatestStableRelease downloads the latest stable released version of
|
|
// restic and saves it to target. It returns the version string for the newest
|
|
// version. The function printf is used to print progress information.
|
|
func DownloadLatestStableRelease(ctx context.Context, target string, printf func(string, ...interface{})) (version string, err error) {
|
|
if printf == nil {
|
|
printf = func(string, ...interface{}) {}
|
|
}
|
|
|
|
printf("find latest release of restic at GitHub\n")
|
|
|
|
rel, err := GitHubLatestRelease(ctx, "restic", "restic")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
printf("latest version is %v\n", rel.Version)
|
|
|
|
_, sha256sums, err := getGithubDataFile(ctx, rel.Assets, "SHA256SUMS", printf)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
_, sig, err := getGithubDataFile(ctx, rel.Assets, "SHA256SUMS.asc", printf)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
ok, err := GPGVerify(sha256sums, sig)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if !ok {
|
|
return "", errors.New("GPG signature verification of the file SHA256SUMS failed")
|
|
}
|
|
|
|
printf("GPG signature verification succeeded\n")
|
|
|
|
ext := "bz2"
|
|
if runtime.GOOS == "windows" {
|
|
ext = "zip"
|
|
}
|
|
|
|
suffix := fmt.Sprintf("%s_%s.%s", runtime.GOOS, runtime.GOARCH, ext)
|
|
downloadFilename, buf, err := getGithubDataFile(ctx, rel.Assets, suffix, printf)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
printf("downloaded %v\n", downloadFilename)
|
|
|
|
wantHash, err := findHash(sha256sums, downloadFilename)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
gotHash := sha256.Sum256(buf)
|
|
if !bytes.Equal(wantHash, gotHash[:]) {
|
|
return "", fmt.Errorf("SHA256 hash mismatch, want hash %02x, got %02x", wantHash, gotHash)
|
|
}
|
|
|
|
err = extractToFile(buf, downloadFilename, target, printf)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return rel.Version, nil
|
|
}
|