github.com/trevoraustin/hub@v2.2.0-preview1.0.20141105230840-96d8bfc654cc+incompatible/commands/updater.go (about)

     1  package commands
     2  
     3  import (
     4  	"archive/zip"
     5  	"fmt"
     6  	"io"
     7  	"io/ioutil"
     8  	"math/rand"
     9  	"net/http"
    10  	"os"
    11  	"path/filepath"
    12  	"runtime"
    13  	"strings"
    14  	"time"
    15  
    16  	"github.com/github/hub/git"
    17  	"github.com/github/hub/github"
    18  	"github.com/github/hub/utils"
    19  	goupdate "github.com/inconshreveable/go-update"
    20  )
    21  
    22  const (
    23  	hubAutoUpdateConfig = "hub.autoUpdate"
    24  )
    25  
    26  func NewUpdater() *Updater {
    27  	version := os.Getenv("GH_VERSION")
    28  	if version == "" {
    29  		version = Version
    30  	}
    31  
    32  	timestampPath := filepath.Join(os.Getenv("HOME"), ".config", "hub-update")
    33  	return &Updater{
    34  		Host:           github.DefaultGitHubHost(),
    35  		CurrentVersion: version,
    36  		timestampPath:  timestampPath,
    37  	}
    38  }
    39  
    40  type Updater struct {
    41  	Host           string
    42  	CurrentVersion string
    43  	timestampPath  string
    44  }
    45  
    46  func (updater *Updater) timeToUpdate() bool {
    47  	if updater.CurrentVersion == "dev" || readTime(updater.timestampPath).After(time.Now()) {
    48  		return false
    49  	}
    50  
    51  	// the next update is in about 14 days
    52  	wait := 13*24*time.Hour + randDuration(24*time.Hour)
    53  	return writeTime(updater.timestampPath, time.Now().Add(wait))
    54  }
    55  
    56  func (updater *Updater) PromptForUpdate() (err error) {
    57  	config := autoUpdateConfig()
    58  	if config == "never" || !updater.timeToUpdate() {
    59  		return
    60  	}
    61  
    62  	releaseName, version := updater.latestReleaseNameAndVersion()
    63  	if version != "" && version != updater.CurrentVersion {
    64  		switch config {
    65  		case "always":
    66  			err = updater.updateTo(releaseName, version)
    67  		default:
    68  			fmt.Println("There is a newer version of hub available.")
    69  			fmt.Print("Would you like to update? ([Y]es/[N]o/[A]lways/N[e]ver): ")
    70  			var confirm string
    71  			fmt.Scan(&confirm)
    72  
    73  			always := utils.IsOption(confirm, "a", "always")
    74  			if always || utils.IsOption(confirm, "y", "yes") {
    75  				err = updater.updateTo(releaseName, version)
    76  			}
    77  
    78  			saveAutoUpdateConfiguration(confirm, always)
    79  		}
    80  	}
    81  
    82  	return
    83  }
    84  
    85  func (updater *Updater) Update() (err error) {
    86  	config := autoUpdateConfig()
    87  	if config == "never" {
    88  		fmt.Println("Update is disabled")
    89  		return
    90  	}
    91  
    92  	releaseName, version := updater.latestReleaseNameAndVersion()
    93  	if version == "" {
    94  		fmt.Println("There is no newer version of hub available.")
    95  		return
    96  	}
    97  
    98  	if version == updater.CurrentVersion {
    99  		fmt.Printf("You're already on the latest version: %s\n", version)
   100  	} else {
   101  		err = updater.updateTo(releaseName, version)
   102  	}
   103  
   104  	return
   105  }
   106  
   107  func (updater *Updater) latestReleaseNameAndVersion() (name, version string) {
   108  	// Create Client with a stub Host
   109  	c := github.Client{Host: &github.Host{Host: updater.Host}}
   110  	name, _ = c.GhLatestTagName()
   111  	version = strings.TrimPrefix(name, "v")
   112  
   113  	return
   114  }
   115  
   116  func (updater *Updater) updateTo(releaseName, version string) (err error) {
   117  	fmt.Printf("Updating gh to %s...\n", version)
   118  	downloadURL := fmt.Sprintf("https://%s/github/hub/releases/download/%s/hub%s_%s_%s.zip", updater.Host, releaseName, version, runtime.GOOS, runtime.GOARCH)
   119  	path, err := downloadFile(downloadURL)
   120  	if err != nil {
   121  		return
   122  	}
   123  
   124  	exec, err := unzipExecutable(path)
   125  	if err != nil {
   126  		return
   127  	}
   128  
   129  	err, _ = goupdate.New().FromFile(exec)
   130  	if err == nil {
   131  		fmt.Println("Done!")
   132  	}
   133  
   134  	return
   135  }
   136  
   137  func unzipExecutable(path string) (exec string, err error) {
   138  	rc, err := zip.OpenReader(path)
   139  	if err != nil {
   140  		err = fmt.Errorf("Can't open zip file %s: %s", path, err)
   141  		return
   142  	}
   143  	defer rc.Close()
   144  
   145  	for _, file := range rc.File {
   146  		if !strings.HasPrefix(file.Name, "gh") {
   147  			continue
   148  		}
   149  
   150  		dir := filepath.Dir(path)
   151  		exec, err = unzipFile(file, dir)
   152  		break
   153  	}
   154  
   155  	if exec == "" && err == nil {
   156  		err = fmt.Errorf("No gh executable is found in %s", path)
   157  	}
   158  
   159  	return
   160  }
   161  
   162  func unzipFile(file *zip.File, to string) (exec string, err error) {
   163  	frc, err := file.Open()
   164  	if err != nil {
   165  		err = fmt.Errorf("Can't open zip entry %s when reading: %s", file.Name, err)
   166  		return
   167  	}
   168  	defer frc.Close()
   169  
   170  	dest := filepath.Join(to, filepath.Base(file.Name))
   171  	f, err := os.Create(dest)
   172  	if err != nil {
   173  		return
   174  	}
   175  	defer f.Close()
   176  
   177  	copied, err := io.Copy(f, frc)
   178  	if err != nil {
   179  		return
   180  	}
   181  
   182  	if uint32(copied) != file.UncompressedSize {
   183  		err = fmt.Errorf("Zip entry %s is corrupted", file.Name)
   184  		return
   185  	}
   186  
   187  	exec = f.Name()
   188  
   189  	return
   190  }
   191  
   192  func downloadFile(url string) (path string, err error) {
   193  	dir, err := ioutil.TempDir("", "gh-update")
   194  	if err != nil {
   195  		return
   196  	}
   197  
   198  	resp, err := http.Get(url)
   199  	if err != nil {
   200  		return
   201  	}
   202  	defer resp.Body.Close()
   203  
   204  	if resp.StatusCode >= 300 || resp.StatusCode < 200 {
   205  		err = fmt.Errorf("Can't download %s: %d", url, resp.StatusCode)
   206  		return
   207  	}
   208  
   209  	file, err := os.Create(filepath.Join(dir, filepath.Base(url)))
   210  	if err != nil {
   211  		return
   212  	}
   213  	defer file.Close()
   214  
   215  	_, err = io.Copy(file, resp.Body)
   216  	if err != nil {
   217  		return
   218  	}
   219  
   220  	path = file.Name()
   221  
   222  	return
   223  }
   224  
   225  func randDuration(n time.Duration) time.Duration {
   226  	return time.Duration(rand.Int63n(int64(n)))
   227  }
   228  
   229  func readTime(path string) time.Time {
   230  	p, err := ioutil.ReadFile(path)
   231  	if os.IsNotExist(err) {
   232  		return time.Time{}
   233  	}
   234  	if err != nil {
   235  		return time.Now().Add(1000 * time.Hour)
   236  	}
   237  
   238  	t, err := time.Parse(time.RFC3339, strings.TrimSpace(string(p)))
   239  	if err != nil {
   240  		return time.Time{}
   241  	}
   242  
   243  	return t
   244  }
   245  
   246  func writeTime(path string, t time.Time) bool {
   247  	return ioutil.WriteFile(path, []byte(t.Format(time.RFC3339)), 0644) == nil
   248  }
   249  
   250  func saveAutoUpdateConfiguration(confirm string, always bool) {
   251  	if always {
   252  		git.SetGlobalConfig(hubAutoUpdateConfig, "always")
   253  	} else if utils.IsOption(confirm, "e", "never") {
   254  		git.SetGlobalConfig(hubAutoUpdateConfig, "never")
   255  	}
   256  }
   257  
   258  func autoUpdateConfig() (opt string) {
   259  	opt = os.Getenv("HUB_AUTOUPDATE")
   260  	if opt == "" {
   261  		opt, _ = git.GlobalConfig(hubAutoUpdateConfig)
   262  	}
   263  
   264  	return
   265  }