Skip to content

Commit

Permalink
check for latest version
Browse files Browse the repository at this point in the history
  • Loading branch information
hbagdi committed May 20, 2022
1 parent 2dda0ac commit fcddebd
Show file tree
Hide file tree
Showing 6 changed files with 236 additions and 7 deletions.
1 change: 0 additions & 1 deletion .golangci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ linters:
- gocritic
- gocyclo
- godot
- godox
- gofmt
- gofumpt
- goheader
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/hbagdi/hit
go 1.17

require (
github.com/blang/semver/v4 v4.0.0
github.com/fatih/color v1.13.0
github.com/ghodss/yaml v1.0.0
github.com/hokaccha/go-prettyjson v0.0.0-20211117102719-0474bc63780f
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
Expand Down
31 changes: 25 additions & 6 deletions pkg/cache/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,43 @@ const (

var cacheFilePath string

func getUserCacheDir() (string, error) {
userCacheDir, err := os.UserCacheDir()
if err != nil {
return "", fmt.Errorf("failed to find user's cache directory: %w", err)
}
return userCacheDir, nil
}

func HitCacheDir() (string, error) {
userCacheDir, err := getUserCacheDir()
if err != nil {
return "", err
}
return filepath.Join(userCacheDir, cacheDir), nil
}

// init ensures that the cache files are correctly setup.
func init() {
userCacheDir, err := os.UserCacheDir()
userCacheDir, err := getUserCacheDir()
if err != nil {
panic(fmt.Sprintf("failed to find cache directory: %v", err))
}
if err = ensureDir(userCacheDir, os.ModePerm); err != nil {
panic(err)
}
if err = ensureDir(filepath.Join(userCacheDir, cacheDir),
os.ModePerm); err != nil {
hitCacheDir, err := HitCacheDir()
if err != nil {
panic(err)
}
if err = ensureDir(hitCacheDir, os.ModePerm); err != nil {
panic(err)
}
if err = ensureFile(filepath.Join(userCacheDir,
cacheDir, cacheFile), fileMode); err != nil {
hitCacheFile := filepath.Join(hitCacheDir, cacheFile)
if err = ensureFile(hitCacheFile, fileMode); err != nil {
panic(err)
}
cacheFilePath = filepath.Join(userCacheDir, cacheDir, cacheFile)
cacheFilePath = hitCacheFile
}

func ensureFile(path string, perm os.FileMode) error {
Expand Down
59 changes: 59 additions & 0 deletions pkg/cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,43 @@ import (
"context"
"fmt"
"log"
"strings"
"sync"

"github.com/blang/semver/v4"
"github.com/hbagdi/hit/pkg/cache"
executorPkg "github.com/hbagdi/hit/pkg/executor"
"github.com/hbagdi/hit/pkg/version"
)

const (
minArgs = 2
)

var (
versionLoadMutex sync.Mutex
latestVersion string
)

func init() {
go func() {
versionLoadMutex.Lock()
defer versionLoadMutex.Unlock()
version, err := version.LoadLatestVersion()
if err != nil {
// TODO(hbagdi): add logging
return
}
latestVersion = version
}()
}

func getLatestVersion() string {
versionLoadMutex.Lock()
defer versionLoadMutex.Unlock()
return latestVersion
}

func Run(ctx context.Context, args ...string) (err error) {
if len(args) < minArgs {
return fmt.Errorf("need a request to execute")
Expand Down Expand Up @@ -80,5 +108,36 @@ func Run(ctx context.Context, args ...string) (err error) {
return err
}

printLatestVersion()
return err
}

func printLatestVersion() {
latestVersion := getLatestVersion()
if latestVersion == "" {
return
}
latest, err := semver.New(cleanVersionString(latestVersion))
if err != nil {
// TODO(hbagdi): log error
return
}

current, err := semver.New(cleanVersionString(version.Version))
if err != nil {
// TODO(hbagdi): log error
return
}
if latest.GT(*current) {
fmt.Printf("New version(%s) available! Current installed version is"+
" %s.\nCheckout https://hit.yolo42.com for details.\n",
latestVersion, version.Version)
}
}

func cleanVersionString(v string) string {
if strings.HasPrefix(v, "v") {
return v[1:]
}
return v
}
149 changes: 149 additions & 0 deletions pkg/version/check_version.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package version

import (
"context"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"time"

"github.com/hbagdi/hit/pkg/cache"
)

const (
versionEndpoint = "https://hit-server.yolo42.com/api/v1/latest-version"
requestTimeout = 3 * time.Second
)

func checkForUpdate() (string, error) {
ctx, cancel := context.WithTimeout(context.Background(), requestTimeout)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet,
versionEndpoint, nil)
req.Header.Add("user-agent", "hit/"+Version)
if err != nil {
return "", err
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}
defer func() {
// TODO(hbagdi): handle err
_ = res.Body.Close()
}()
if res.StatusCode != http.StatusOK {
return "", fmt.Errorf("unexpected status code: %v", res.StatusCode)
}
js, err := ioutil.ReadAll(res.Body)
if err != nil {
return "", err
}
return parseVersionFromResponseOrFile(js)
}

func parseVersionFromResponseOrFile(js []byte) (string, error) {
var m map[string]interface{}
if err := json.Unmarshal(js, &m); err != nil {
return "", err
}
v, ok := m["version"]
if !ok {
return "", fmt.Errorf("no 'version' field in the response")
}
version, ok := v.(string)
if !ok {
return "", fmt.Errorf("expected 'version' field to be a string, "+
"but got %T", v)
}
return version, nil
}

var versionCacheFullPath string

func versionCacheFileName() (string, error) {
if versionCacheFullPath != "" {
return versionCacheFullPath, nil
}
const versionCacheFilename = "latest_version.json"
hitCacheDir, err := cache.HitCacheDir()
if err != nil {
return "", err
}
return filepath.Join(hitCacheDir, versionCacheFilename), nil
}

var errCacheMiss = fmt.Errorf("cache miss")

func loadVersionFromCache() (string, error) {
filename, err := versionCacheFileName()
if err != nil {
return "", err
}
js, err := ioutil.ReadFile(filename)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return "", errCacheMiss
}
return "", err
}
cacheInfo, err := os.Stat(filename)
if err != nil {
return "", err
}
lastUpdated := cacheInfo.ModTime()
cutoff := time.Now().Add(-24 * time.Hour)
if lastUpdated.Before(cutoff) {
return "", errCacheMiss
}
return parseVersionFromResponseOrFile(js)
}

func refreshVersionCache() (string, error) {
version, err := checkForUpdate()
if err != nil {
return "", err
}
// TODO(hbagdi): log this error
_ = updateCache(version)

return version, nil
}

func updateCache(version string) error {
const fileMode = 0o0600
js, err := json.Marshal(map[string]string{
"version": version,
})
if err != nil {
return err
}
filename, err := versionCacheFileName()
if err != nil {
return err
}
err = ioutil.WriteFile(filename, js, fileMode)
if err != nil {
return fmt.Errorf("update version cache: %w", err)
}
return nil
}

func LoadLatestVersion() (string, error) {
version, err := loadVersionFromCache()
if err != nil {
if err == errCacheMiss {
updatedVersion, err := refreshVersionCache()
if err != nil {
return "", err
}
return updatedVersion, nil
}
return "", err
}
return version, nil
}

0 comments on commit fcddebd

Please sign in to comment.