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