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 package selfupdate
import ( import (
"context"
"fmt" "fmt"
"net/http"
"os"
"regexp" "regexp"
"runtime" "runtime"
"strings" "strings"
"github.com/blang/semver" "github.com/blang/semver"
"github.com/google/go-github/github" "github.com/google/go-github/github"
gitconfig "github.com/tcnksm/go-gitconfig"
"golang.org/x/oauth2"
) )
var reVersion = regexp.MustCompile(`\d+\.\d+\.\d+`) 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) { func findSuitableReleaseAndAsset(rels []*github.RepositoryRelease) (*github.RepositoryRelease, *github.ReleaseAsset, bool) {
// Generate candidates // Generate candidates
cs := make([]string, 0, 8) cs := make([]string, 0, 8)
@ -64,33 +53,15 @@ func findSuitableReleaseAndAsset(rels []*github.RepositoryRelease) (*github.Repo
return nil, nil, false 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. // 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, "/") repo := strings.Split(slug, "/")
if len(repo) != 2 || repo[0] == "" || repo[1] == "" { if len(repo) != 2 || repo[0] == "" || repo[1] == "" {
err = fmt.Errorf("Invalid slug format. It should be 'owner/name': %s", slug) err = fmt.Errorf("Invalid slug format. It should be 'owner/name': %s", slug)
return 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 { if err != nil {
log.Println("API returned an error response:", err) log.Println("API returned an error response:", err)
if res != nil && res.StatusCode == 404 { 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). // DetectLatest detects the latest release of the slug (owner/repo).
func DetectLatest(slug string) (*Release, bool, error) { 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 ( import (
"fmt" "fmt"
"github.com/blang/semver" "github.com/blang/semver"
"os"
"strings" "strings"
"testing" "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) { func TestDetectReleaseWithVersionPrefix(t *testing.T) {
r, ok, err := DetectLatest("rhysd/github-clone-all") r, ok, err := DetectLatest("rhysd/github-clone-all")
if err != nil { if err != nil {
@ -119,7 +110,7 @@ func TestDetectNoRelease(t *testing.T) {
} }
func TestInvalidSlug(t *testing.T) { func TestInvalidSlug(t *testing.T) {
d := NewDetector() up := NewUpdater(Config{})
for _, slug := range []string{ for _, slug := range []string{
"foo", "foo",
@ -128,7 +119,7 @@ func TestInvalidSlug(t *testing.T) {
"/bar", "/bar",
"foo/bar/piyo", "foo/bar/piyo",
} { } {
_, _, err := d.DetectLatest(slug) _, _, err := up.DetectLatest(slug)
if err == nil { if err == nil {
t.Error(slug, "should be invalid slug") t.Error(slug, "should be invalid slug")
} }
@ -139,8 +130,7 @@ func TestInvalidSlug(t *testing.T) {
} }
func TestNonExistingRepo(t *testing.T) { func TestNonExistingRepo(t *testing.T) {
d := NewDetector() v, ok, err := DetectLatest("rhysd/non-existing-repo")
v, ok, err := d.DetectLatest("rhysd/non-existing-repo")
if err != nil { if err != nil {
t.Fatal("Non-existing repo should not cause an error:", v) 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) { func TestNoReleaseFound(t *testing.T) {
d := NewDetector() _, ok, err := DetectLatest("rhysd/misc")
_, ok, err := d.DetectLatest("rhysd/misc")
if err != nil { if err != nil {
t.Fatal("Repo having no release should not cause an error:", err) 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. // 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) res, err := http.Get(assetURL)
if err != nil { if err != nil {
return fmt.Errorf("Failed to download a release file from %s: %s", assetURL, err) 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. // UpdateCommand updates a given command binary to the latest version.
// 'slug' represents 'owner/name' repository on GitHub and 'current' means the current 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") { if runtime.GOOS == "windows" && !strings.HasSuffix(cmdPath, ".exe") {
// Ensure to add '.exe' to given path on Windows // Ensure to add '.exe' to given path on Windows
cmdPath = cmdPath + ".exe" cmdPath = cmdPath + ".exe"
@ -56,7 +58,7 @@ func UpdateCommand(cmdPath string, current semver.Version, slug string) (*Releas
cmdPath = p cmdPath = p
} }
rel, ok, err := DetectLatest(slug) rel, ok, err := up.DetectLatest(slug)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -69,7 +71,7 @@ func UpdateCommand(cmdPath string, current semver.Version, slug string) (*Releas
return rel, nil return rel, nil
} }
log.Println("Will update", cmdPath, "to the latest version", rel.Version) 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 nil, err
} }
return rel, nil 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. // UpdateSelf updates the running executable itself to the latest version.
// 'slug' represents 'owner/name' repository on GitHub and 'current' means the current 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() cmdPath, err := os.Executable()
if err != nil { if err != nil {
return nil, err return nil, err
} }
return UpdateCommand(cmdPath, current, slug) 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) { 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 { if err == nil {
t.Fatal("Error should occur for URL not found") t.Fatal("Error should occur for URL not found")
} }
@ -248,7 +248,7 @@ func TestInvalidAssetURL(t *testing.T) {
func TestBrokenAsset(t *testing.T) { func TestBrokenAsset(t *testing.T) {
asset := "https://github.com/rhysd-test/test-incorrect-release/releases/download/invalid/broken-zip.zip" 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 { if err == nil {
t.Fatal("Error should occur for URL not found") 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})
}