create Updater struct to use persistent GitHub API client also for downloading assets

This commit is contained in:
rhysd 2018-01-19 11:28:59 +09:00
parent 54df18b98c
commit fd492d6b01
6 changed files with 89 additions and 54 deletions

View File

@ -1,28 +1,17 @@
package selfupdate
import (
"context"
"fmt"
"net/http"
"os"
"regexp"
"runtime"
"strings"
"github.com/blang/semver"
"github.com/google/go-github/github"
gitconfig "github.com/tcnksm/go-gitconfig"
"golang.org/x/oauth2"
)
var reVersion = regexp.MustCompile(`\d+\.\d+\.\d+`)
// ReleaseDetector is responsible for detecting the latest release using GitHub Releases API.
type ReleaseDetector struct {
api *github.Client
apiCtx context.Context
}
func findSuitableReleaseAndAsset(rels []*github.RepositoryRelease) (*github.RepositoryRelease, *github.ReleaseAsset, bool) {
// Generate candidates
cs := make([]string, 0, 8)
@ -64,33 +53,15 @@ func findSuitableReleaseAndAsset(rels []*github.RepositoryRelease) (*github.Repo
return nil, nil, false
}
// NewDetector crates a new detector instance. It initializes GitHub API client.
func NewDetector() *ReleaseDetector {
token := os.Getenv("GITHUB_TOKEN")
if token == "" {
token, _ = gitconfig.GithubToken()
}
ctx := context.Background()
var auth *http.Client
if token != "" {
src := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token})
auth = oauth2.NewClient(ctx, src)
}
client := github.NewClient(auth)
return &ReleaseDetector{client, ctx}
}
// DetectLatest tries to get the latest version of the repository on GitHub. 'slug' means 'owner/name' formatted string.
func (d *ReleaseDetector) DetectLatest(slug string) (release *Release, found bool, err error) {
func (up *Updater) DetectLatest(slug string) (release *Release, found bool, err error) {
repo := strings.Split(slug, "/")
if len(repo) != 2 || repo[0] == "" || repo[1] == "" {
err = fmt.Errorf("Invalid slug format. It should be 'owner/name': %s", slug)
return
}
rels, res, err := d.api.Repositories.ListReleases(d.apiCtx, repo[0], repo[1], nil)
rels, res, err := up.api.Repositories.ListReleases(up.apiCtx, repo[0], repo[1], nil)
if err != nil {
log.Println("API returned an error response:", err)
if res != nil && res.StatusCode == 404 {
@ -133,5 +104,5 @@ func (d *ReleaseDetector) DetectLatest(slug string) (release *Release, found boo
// DetectLatest detects the latest release of the slug (owner/repo).
func DetectLatest(slug string) (*Release, bool, error) {
return NewDetector().DetectLatest(slug)
return NewUpdater(Config{}).DetectLatest(slug)
}

View File

@ -3,19 +3,10 @@ package selfupdate
import (
"fmt"
"github.com/blang/semver"
"os"
"strings"
"testing"
)
func TestGitHubTokenEnv(t *testing.T) {
token := os.Getenv("GITHUB_TOKEN")
if token == "" {
t.Skip("because $GITHUB_TOKEN is not set")
}
_ = NewDetector()
}
func TestDetectReleaseWithVersionPrefix(t *testing.T) {
r, ok, err := DetectLatest("rhysd/github-clone-all")
if err != nil {
@ -119,7 +110,7 @@ func TestDetectNoRelease(t *testing.T) {
}
func TestInvalidSlug(t *testing.T) {
d := NewDetector()
up := NewUpdater(Config{})
for _, slug := range []string{
"foo",
@ -128,7 +119,7 @@ func TestInvalidSlug(t *testing.T) {
"/bar",
"foo/bar/piyo",
} {
_, _, err := d.DetectLatest(slug)
_, _, err := up.DetectLatest(slug)
if err == nil {
t.Error(slug, "should be invalid slug")
}
@ -139,8 +130,7 @@ func TestInvalidSlug(t *testing.T) {
}
func TestNonExistingRepo(t *testing.T) {
d := NewDetector()
v, ok, err := d.DetectLatest("rhysd/non-existing-repo")
v, ok, err := DetectLatest("rhysd/non-existing-repo")
if err != nil {
t.Fatal("Non-existing repo should not cause an error:", v)
}
@ -150,8 +140,7 @@ func TestNonExistingRepo(t *testing.T) {
}
func TestNoReleaseFound(t *testing.T) {
d := NewDetector()
_, ok, err := d.DetectLatest("rhysd/misc")
_, ok, err := DetectLatest("rhysd/misc")
if err != nil {
t.Fatal("Repo having no release should not cause an error:", err)
}

View File

@ -13,7 +13,9 @@ import (
)
// UpdateTo download an executable from assetURL and replace the current binary with the downloaded one. cmdPath is a file path to command executable.
func UpdateTo(assetURL, cmdPath string) error {
func (up *Updater) UpdateTo(assetURL, cmdPath string) error {
// TODO: Use GitHub API client to download assets
res, err := http.Get(assetURL)
if err != nil {
return fmt.Errorf("Failed to download a release file from %s: %s", assetURL, err)
@ -38,7 +40,7 @@ func UpdateTo(assetURL, cmdPath string) error {
// UpdateCommand updates a given command binary to the latest version.
// 'slug' represents 'owner/name' repository on GitHub and 'current' means the current version.
func UpdateCommand(cmdPath string, current semver.Version, slug string) (*Release, error) {
func (up *Updater) UpdateCommand(cmdPath string, current semver.Version, slug string) (*Release, error) {
if runtime.GOOS == "windows" && !strings.HasSuffix(cmdPath, ".exe") {
// Ensure to add '.exe' to given path on Windows
cmdPath = cmdPath + ".exe"
@ -56,7 +58,7 @@ func UpdateCommand(cmdPath string, current semver.Version, slug string) (*Releas
cmdPath = p
}
rel, ok, err := DetectLatest(slug)
rel, ok, err := up.DetectLatest(slug)
if err != nil {
return nil, err
}
@ -69,7 +71,7 @@ func UpdateCommand(cmdPath string, current semver.Version, slug string) (*Releas
return rel, nil
}
log.Println("Will update", cmdPath, "to the latest version", rel.Version)
if err := UpdateTo(rel.AssetURL, cmdPath); err != nil {
if err := up.UpdateTo(rel.AssetURL, cmdPath); err != nil {
return nil, err
}
return rel, nil
@ -77,10 +79,22 @@ func UpdateCommand(cmdPath string, current semver.Version, slug string) (*Releas
// UpdateSelf updates the running executable itself to the latest version.
// 'slug' represents 'owner/name' repository on GitHub and 'current' means the current version.
func UpdateSelf(current semver.Version, slug string) (*Release, error) {
func (up *Updater) UpdateSelf(current semver.Version, slug string) (*Release, error) {
cmdPath, err := os.Executable()
if err != nil {
return nil, err
}
return UpdateCommand(cmdPath, current, slug)
}
// UpdateCommand updates a given command binary to the latest version.
// This function is a shortcut version of updater.UpdateCommand.
func UpdateCommand(cmdPath string, current semver.Version, slug string) (*Release, error) {
return NewUpdater(Config{}).UpdateCommand(cmdPath, current, slug)
}
// UpdateSelf updates the running executable itself to the latest version.
// This function is a shortcut version of updater.UpdateSelf.
func UpdateSelf(current semver.Version, slug string) (*Release, error) {
return NewUpdater(Config{}).UpdateSelf(current, slug)
}

View File

@ -237,7 +237,7 @@ func TestInvalidSlugForUpdate(t *testing.T) {
}
func TestInvalidAssetURL(t *testing.T) {
err := UpdateTo("https://github.com/rhysd/non-existing-repo/releases/download/v1.2.3/foo.zip", "foo")
err := NewUpdater(Config{}).UpdateTo("https://github.com/rhysd/non-existing-repo/releases/download/v1.2.3/foo.zip", "foo")
if err == nil {
t.Fatal("Error should occur for URL not found")
}
@ -248,7 +248,7 @@ func TestInvalidAssetURL(t *testing.T) {
func TestBrokenAsset(t *testing.T) {
asset := "https://github.com/rhysd-test/test-incorrect-release/releases/download/invalid/broken-zip.zip"
err := UpdateTo(asset, "foo")
err := NewUpdater(Config{}).UpdateTo(asset, "foo")
if err == nil {
t.Fatal("Error should occur for URL not found")
}

46
selfupdate/updater.go Normal file
View File

@ -0,0 +1,46 @@
package selfupdate
import (
"context"
"net/http"
"os"
"github.com/google/go-github/github"
gitconfig "github.com/tcnksm/go-gitconfig"
"golang.org/x/oauth2"
)
// Updater is responsible for managing the context of self-update.
// It contains GitHub client and its context.
type Updater struct {
api *github.Client
apiCtx context.Context
}
// Config represents the configuration of self-update.
type Config struct {
// APIToken represents GitHub API token. If it's not empty, it will be used for authentication of GitHub API
APIToken string
// TODO: Add host URL for API endpoint
}
// NewUpdater crates a new detector instance. It initializes GitHub API client.
func NewUpdater(config Config) *Updater {
token := config.APIToken
if token == "" {
token = os.Getenv("GITHUB_TOKEN")
}
if token == "" {
token, _ = gitconfig.GithubToken()
}
ctx := context.Background()
var auth *http.Client
if token != "" {
src := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token})
auth = oauth2.NewClient(ctx, src)
}
client := github.NewClient(auth)
return &Updater{client, ctx}
}

View File

@ -0,0 +1,15 @@
package selfupdate
import (
"os"
"testing"
)
func TestGitHubTokenEnv(t *testing.T) {
token := os.Getenv("GITHUB_TOKEN")
if token == "" {
t.Skip("because $GITHUB_TOKEN is not set")
}
_ = NewUpdater(Config{})
_ = NewUpdater(Config{APIToken: token})
}