From fd492d6b01711968320e2145643930b0fdde0f54 Mon Sep 17 00:00:00 2001 From: rhysd Date: Fri, 19 Jan 2018 11:28:59 +0900 Subject: [PATCH] create Updater struct to use persistent GitHub API client also for downloading assets --- selfupdate/detect.go | 35 +++-------------------------- selfupdate/detect_test.go | 19 ++++------------ selfupdate/update.go | 24 +++++++++++++++----- selfupdate/update_test.go | 4 ++-- selfupdate/updater.go | 46 ++++++++++++++++++++++++++++++++++++++ selfupdate/updater_test.go | 15 +++++++++++++ 6 files changed, 89 insertions(+), 54 deletions(-) create mode 100644 selfupdate/updater.go create mode 100644 selfupdate/updater_test.go diff --git a/selfupdate/detect.go b/selfupdate/detect.go index 3bbaf45..b302eaf 100644 --- a/selfupdate/detect.go +++ b/selfupdate/detect.go @@ -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) } diff --git a/selfupdate/detect_test.go b/selfupdate/detect_test.go index 5a14e4a..e044382 100644 --- a/selfupdate/detect_test.go +++ b/selfupdate/detect_test.go @@ -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) } diff --git a/selfupdate/update.go b/selfupdate/update.go index 29f9eff..5814898 100644 --- a/selfupdate/update.go +++ b/selfupdate/update.go @@ -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) +} diff --git a/selfupdate/update_test.go b/selfupdate/update_test.go index 68b1326..a6a8b05 100644 --- a/selfupdate/update_test.go +++ b/selfupdate/update_test.go @@ -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") } diff --git a/selfupdate/updater.go b/selfupdate/updater.go new file mode 100644 index 0000000..284bac7 --- /dev/null +++ b/selfupdate/updater.go @@ -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} +} diff --git a/selfupdate/updater_test.go b/selfupdate/updater_test.go new file mode 100644 index 0000000..cb5b18b --- /dev/null +++ b/selfupdate/updater_test.go @@ -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}) +}