2
2
mirror of https://github.com/octoleo/restic.git synced 2024-12-23 19:38:57 +00:00
restic/internal/selfupdate/download.go
Alexander Neumann 6e1a3987b7 Add 'self-update' command
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.
2018-08-12 23:34:47 +02:00

174 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
}