Skip to content

Commit

Permalink
refactoring
Browse files Browse the repository at this point in the history
  • Loading branch information
creativeprojects committed Jun 16, 2024
1 parent 0e5ed1c commit fd4f7ea
Show file tree
Hide file tree
Showing 13 changed files with 216 additions and 147 deletions.
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,25 +35,27 @@ Self-Update library for Github, Gitea and Gitlab hosted applications in Go

# Introduction

go-selfupdate detects the information of the latest release via a source provider and
`go-selfupdate` detects the information of the latest release via a source provider and
checks the current version. If a newer version than itself is detected, it downloads the released binary from
the source provider and replaces itself.

- Automatically detect the latest version of released binary on the source provider
- Retrieve the proper binary for the OS and arch where the binary is running
- Update the binary with rollback support on failure
- Tested on Linux, macOS and Windows
- Support for different versions of ARM architectures
- Support macOS universal binaries
- Many archive and compression formats are supported (zip, tar, gzip, xzip, bzip2)
- Support private repositories
- Support hash, signature validation

Two source providers are available:
Three source providers are available:
- GitHub
- Gitea
- Gitlab

This library started as a fork of https://github.com/rhysd/go-github-selfupdate. A few things have changed from the original implementation:
- don't expose an external semver.Version type, but provide the same functionality through the API: LessThan, Equal and GreaterThan
- don't expose an external `semver.Version` type, but provide the same functionality through the API: `LessThan`, `Equal` and `GreaterThan`
- use an interface to send logs (compatible with standard log.Logger)
- able to detect different ARM CPU architectures (the original library wasn't working on my different versions of raspberry pi)
- support for assets compressed with bzip2 (.bz2)
Expand Down
9 changes: 0 additions & 9 deletions arm.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,8 @@ package selfupdate

import (
"debug/buildinfo"
"os"
)

var goarm uint8

//nolint:gochecknoinits
func init() {
// avoid using runtime.goarm directly
goarm = getGOARM(os.Args[0])
}

func getGOARM(goBinary string) uint8 {
build, err := buildinfo.ReadFile(goBinary)
if err != nil {
Expand Down
14 changes: 8 additions & 6 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,24 @@ package selfupdate

// Config represents the configuration of self-update.
type Config struct {
// Source where to load the releases from (example: GitHubSource)
// Source where to load the releases from (example: GitHubSource).
Source Source
// Validator represents types which enable additional validation of downloaded release.
Validator Validator
// Filters are regexp used to filter on specific assets for releases with multiple assets.
// An asset is selected if it matches any of those, in addition to the regular tag, os, arch, extensions.
// Please make sure that your filter(s) uniquely match an asset.
Filters []string
// OS is set to the value of runtime.GOOS by default, but you can force another value here
// OS is set to the value of runtime.GOOS by default, but you can force another value here.
OS string
// Arch is set to the value of runtime.GOARCH by default, but you can force another value here
// Arch is set to the value of runtime.GOARCH by default, but you can force another value here.
Arch string
// Arm 32bits version. Valid values are 0 (unknown), 5, 6 or 7. Default is detected value (if any)
// Arm 32bits version. Valid values are 0 (unknown), 5, 6 or 7. Default is detected value (if available).
Arm uint8
// Draft permits an upgrade to a "draft" version (default to false)
// Arch name to use when using a universal binary (macOS only). Default to none.
UniversalArch string
// Draft permits an upgrade to a "draft" version (default to false).
Draft bool
// Prerelease permits an upgrade to a "pre-release" version (default to false)
// Prerelease permits an upgrade to a "pre-release" version (default to false).
Prerelease bool
}
2 changes: 1 addition & 1 deletion detect.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ var reVersion = regexp.MustCompile(`\d+\.\d+\.\d+`)
// It fetches releases information from the source provider and find out the latest release with matching the tag names and asset names.
// Drafts and pre-releases are ignored.
// Assets would be suffixed by the OS name and the arch name such as 'foo_linux_amd64' where 'foo' is a command name.
// '-' can also be used as a separator. File can be compressed with zip, gzip, zxip, bzip2, tar&gzip or tar&zxip.
// '-' can also be used as a separator. File can be compressed with zip, gzip, xz, bzip2, tar&gzip or tar&xz.
// So the asset can have a file extension for the corresponding compression format such as '.zip'.
// On Windows, '.exe' also can be contained such as 'foo_windows_amd64.exe.zip'.
func (up *Updater) DetectLatest(ctx context.Context, repository Repository) (release *Release, found bool, err error) {
Expand Down
21 changes: 21 additions & 0 deletions internal/path.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package internal

import (
"os"
"path/filepath"
)

// GetExecutablePath returns the path of the executable file with all symlinks resolved.
func GetExecutablePath() (string, error) {
exe, err := os.Executable()
if err != nil {
return "", err
}

exe, err = filepath.EvalSymlinks(exe)
if err != nil {
return "", err
}

return exe, nil
}
15 changes: 15 additions & 0 deletions internal/path_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package internal

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestGetExecutablePath(t *testing.T) {
t.Parallel()

exe, err := GetExecutablePath()
assert.NoError(t, err)
assert.NotEmpty(t, exe)
}
3 changes: 2 additions & 1 deletion update.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"strings"

"github.com/Masterminds/semver/v3"
"github.com/creativeprojects/go-selfupdate/internal"
"github.com/creativeprojects/go-selfupdate/update"
)

Expand Down Expand Up @@ -82,7 +83,7 @@ func (up *Updater) UpdateCommand(ctx context.Context, cmdPath string, current st
// UpdateSelf updates the running executable itself to the latest version.
// 'current' is used to check the latest version against the current version.
func (up *Updater) UpdateSelf(ctx context.Context, current string, repository Repository) (*Release, error) {
cmdPath, err := os.Executable()
cmdPath, err := internal.GetExecutablePath()

Check warning on line 86 in update.go

View check run for this annotation

Codecov / codecov/patch

update.go#L86

Added line #L86 was not covered by tests
if err != nil {
return nil, err
}
Expand Down
108 changes: 9 additions & 99 deletions update/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ package update
import (
"bytes"
"crypto"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"io"
"os"
"path/filepath"

"github.com/creativeprojects/go-selfupdate/internal"
)

var (
Expand Down Expand Up @@ -37,7 +37,7 @@ var (
// back to /path/to/target.
//
// If the roll back operation fails, the file system is left in an inconsistent state (between steps 5 and 6) where
// there is no new executable file and the old executable file could not be be moved to its original location. In this
// there is no new executable file and the old executable file could not be moved to its original location. In this
// case you should notify the user of the bad news and ask them to recover manually. Applications can determine whether
// the rollback failed by calling RollbackError, see the documentation on that function for additional detail.
func Apply(update io.Reader, opts Options) error {
Expand All @@ -61,14 +61,16 @@ func Apply(update io.Reader, opts Options) error {
opts.Verifier = NewECDSAVerifier()
}
if opts.TargetMode == 0 {
opts.TargetMode = 0755
opts.TargetMode = 0o755
}

// get target path
var err error
opts.TargetPath, err = opts.getPath()
if err != nil {
return err
if len(opts.TargetPath) == 0 {
opts.TargetPath, err = internal.GetExecutablePath()
if err != nil {
return err

Check warning on line 72 in update/apply.go

View check run for this annotation

Codecov / codecov/patch

update/apply.go#L70-L72

Added lines #L70 - L72 were not covered by tests
}
}

var newBytes []byte
Expand Down Expand Up @@ -180,95 +182,3 @@ type rollbackErr struct {
error // original error
rollbackErr error // error encountered while rolling back
}

// Options for Apply update
type Options struct {
// TargetPath defines the path to the file to update.
// The emptry string means 'the executable file of the running program'.
TargetPath string

// Create TargetPath replacement with this file mode. If zero, defaults to 0755.
TargetMode os.FileMode

// Checksum of the new binary to verify against. If nil, no checksum or signature verification is done.
Checksum []byte

// Public key to use for signature verification. If nil, no signature verification is done.
PublicKey crypto.PublicKey

// Signature to verify the updated file. If nil, no signature verification is done.
Signature []byte

// Pluggable signature verification algorithm. If nil, ECDSA is used.
Verifier Verifier

// Use this hash function to generate the checksum. If not set, SHA256 is used.
Hash crypto.Hash

// Store the old executable file at this path after a successful update.
// The empty string means the old executable file will be removed after the update.
OldSavePath string
}

// SetPublicKeyPEM is a convenience method to set the PublicKey property
// used for checking a completed update's signature by parsing a
// Public Key formatted as PEM data.
func (o *Options) SetPublicKeyPEM(pembytes []byte) error {
block, _ := pem.Decode(pembytes)
if block == nil {
return errors.New("couldn't parse PEM data")
}

pub, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
return err
}
o.PublicKey = pub
return nil
}

func (o *Options) getPath() (string, error) {
if o.TargetPath != "" {
return o.TargetPath, nil
}
exe, err := os.Executable()
if err != nil {
return "", err
}

exe, err = filepath.EvalSymlinks(exe)
if err != nil {
return "", err
}

return exe, nil
}

func (o *Options) verifyChecksum(updated []byte) error {
checksum, err := checksumFor(o.Hash, updated)
if err != nil {
return err
}

if !bytes.Equal(o.Checksum, checksum) {
return fmt.Errorf("updated file has wrong checksum. Expected: %x, got: %x", o.Checksum, checksum)
}
return nil
}

func (o *Options) verifySignature(updated []byte) error {
checksum, err := checksumFor(o.Hash, updated)
if err != nil {
return err
}
return o.Verifier.VerifySignature(checksum, o.Signature, o.Hash, o.PublicKey)
}

func checksumFor(h crypto.Hash, payload []byte) ([]byte, error) {
if !h.Available() {
return nil, errors.New("requested hash function not available")
}
hash := h.New()
hash.Write(payload) // guaranteed not to error
return hash.Sum([]byte{}), nil
}
Loading

0 comments on commit fd4f7ea

Please sign in to comment.