github.com/fluffynuts/lazygit@v0.8.1/pkg/updates/updates.go (about)

     1  package updates
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"io/ioutil"
     7  	"net/http"
     8  	"net/url"
     9  	"os"
    10  	"path/filepath"
    11  	"runtime"
    12  	"strings"
    13  	"time"
    14  
    15  	"github.com/go-errors/errors"
    16  
    17  	"github.com/kardianos/osext"
    18  
    19  	getter "github.com/jesseduffield/go-getter"
    20  	"github.com/jesseduffield/lazygit/pkg/commands"
    21  	"github.com/jesseduffield/lazygit/pkg/config"
    22  	"github.com/jesseduffield/lazygit/pkg/i18n"
    23  	"github.com/sirupsen/logrus"
    24  )
    25  
    26  // Updater checks for updates and does updates
    27  type Updater struct {
    28  	Log       *logrus.Entry
    29  	Config    config.AppConfigurer
    30  	OSCommand *commands.OSCommand
    31  	Tr        *i18n.Localizer
    32  }
    33  
    34  // Updaterer implements the check and update methods
    35  type Updaterer interface {
    36  	CheckForNewUpdate()
    37  	Update()
    38  }
    39  
    40  const (
    41  	PROJECT_URL = "https://github.com/jesseduffield/lazygit"
    42  )
    43  
    44  // NewUpdater creates a new updater
    45  func NewUpdater(log *logrus.Entry, config config.AppConfigurer, osCommand *commands.OSCommand, tr *i18n.Localizer) (*Updater, error) {
    46  	contextLogger := log.WithField("context", "updates")
    47  
    48  	return &Updater{
    49  		Log:       contextLogger,
    50  		Config:    config,
    51  		OSCommand: osCommand,
    52  		Tr:        tr,
    53  	}, nil
    54  }
    55  
    56  func (u *Updater) getLatestVersionNumber() (string, error) {
    57  	req, err := http.NewRequest("GET", PROJECT_URL+"/releases/latest", nil)
    58  	if err != nil {
    59  		return "", err
    60  	}
    61  	req.Header.Set("Accept", "application/json")
    62  
    63  	resp, err := http.DefaultClient.Do(req)
    64  	if err != nil {
    65  		return "", err
    66  	}
    67  	defer resp.Body.Close()
    68  
    69  	dec := json.NewDecoder(resp.Body)
    70  	data := struct {
    71  		TagName string `json:"tag_name"`
    72  	}{}
    73  	if err := dec.Decode(&data); err != nil {
    74  		return "", err
    75  	}
    76  
    77  	return data.TagName, nil
    78  }
    79  
    80  // RecordLastUpdateCheck records last time an update check was performed
    81  func (u *Updater) RecordLastUpdateCheck() error {
    82  	u.Config.GetAppState().LastUpdateCheck = time.Now().Unix()
    83  	return u.Config.SaveAppState()
    84  }
    85  
    86  // expecting version to be of the form `v12.34.56`
    87  func (u *Updater) majorVersionDiffers(oldVersion, newVersion string) bool {
    88  	if oldVersion == "unversioned" {
    89  		return false
    90  	}
    91  	oldVersion = strings.TrimPrefix(oldVersion, "v")
    92  	newVersion = strings.TrimPrefix(newVersion, "v")
    93  	return strings.Split(oldVersion, ".")[0] != strings.Split(newVersion, ".")[0]
    94  }
    95  
    96  func (u *Updater) checkForNewUpdate() (string, error) {
    97  	u.Log.Info("Checking for an updated version")
    98  	currentVersion := u.Config.GetVersion()
    99  	if err := u.RecordLastUpdateCheck(); err != nil {
   100  		return "", err
   101  	}
   102  
   103  	newVersion, err := u.getLatestVersionNumber()
   104  	if err != nil {
   105  		return "", err
   106  	}
   107  	u.Log.Info("Current version is " + currentVersion)
   108  	u.Log.Info("New version is " + newVersion)
   109  
   110  	if newVersion == currentVersion {
   111  		return "", errors.New(u.Tr.SLocalize("OnLatestVersionErr"))
   112  	}
   113  
   114  	if u.majorVersionDiffers(currentVersion, newVersion) {
   115  		errMessage := u.Tr.TemplateLocalize(
   116  			"MajorVersionErr",
   117  			i18n.Teml{
   118  				"newVersion":     newVersion,
   119  				"currentVersion": currentVersion,
   120  			},
   121  		)
   122  		return "", errors.New(errMessage)
   123  	}
   124  
   125  	rawUrl, err := u.getBinaryUrl(newVersion)
   126  	if err != nil {
   127  		return "", err
   128  	}
   129  	u.Log.Info("Checking for resource at url " + rawUrl)
   130  	if !u.verifyResourceFound(rawUrl) {
   131  		errMessage := u.Tr.TemplateLocalize(
   132  			"CouldNotFindBinaryErr",
   133  			i18n.Teml{
   134  				"url": rawUrl,
   135  			},
   136  		)
   137  		return "", errors.New(errMessage)
   138  	}
   139  	u.Log.Info("Verified resource is available, ready to update")
   140  
   141  	return newVersion, nil
   142  }
   143  
   144  // CheckForNewUpdate checks if there is an available update
   145  func (u *Updater) CheckForNewUpdate(onFinish func(string, error) error, userRequested bool) {
   146  	if !userRequested && u.skipUpdateCheck() {
   147  		return
   148  	}
   149  
   150  	go func() {
   151  		newVersion, err := u.checkForNewUpdate()
   152  		if err = onFinish(newVersion, err); err != nil {
   153  			u.Log.Error(err)
   154  		}
   155  	}()
   156  }
   157  
   158  func (u *Updater) skipUpdateCheck() bool {
   159  	// will remove the check for windows after adding a manifest file asking for
   160  	// the required permissions
   161  	if runtime.GOOS == "windows" {
   162  		u.Log.Info("Updating is currently not supported for windows until we can fix permission issues")
   163  		return true
   164  	}
   165  
   166  	if u.Config.GetVersion() == "unversioned" {
   167  		u.Log.Info("Current version is not built from an official release so we won't check for an update")
   168  		return true
   169  	}
   170  
   171  	if u.Config.GetBuildSource() != "buildBinary" {
   172  		u.Log.Info("Binary is not built with the buildBinary flag so we won't check for an update")
   173  		return true
   174  	}
   175  
   176  	userConfig := u.Config.GetUserConfig()
   177  	if userConfig.Get("update.method") == "never" {
   178  		u.Log.Info("Update method is set to never so we won't check for an update")
   179  		return true
   180  	}
   181  
   182  	currentTimestamp := time.Now().Unix()
   183  	lastUpdateCheck := u.Config.GetAppState().LastUpdateCheck
   184  	days := userConfig.GetInt64("update.days")
   185  
   186  	if (currentTimestamp-lastUpdateCheck)/(60*60*24) < days {
   187  		u.Log.Info("Last update was too recent so we won't check for an update")
   188  		return true
   189  	}
   190  
   191  	return false
   192  }
   193  
   194  func (u *Updater) mappedOs(os string) string {
   195  	osMap := map[string]string{
   196  		"darwin":  "Darwin",
   197  		"linux":   "Linux",
   198  		"windows": "Windows",
   199  	}
   200  	result, found := osMap[os]
   201  	if found {
   202  		return result
   203  	}
   204  	return os
   205  }
   206  
   207  func (u *Updater) mappedArch(arch string) string {
   208  	archMap := map[string]string{
   209  		"386":   "32-bit",
   210  		"amd64": "x86_64",
   211  	}
   212  	result, found := archMap[arch]
   213  	if found {
   214  		return result
   215  	}
   216  	return arch
   217  }
   218  
   219  // example: https://github.com/jesseduffield/lazygit/releases/download/v0.1.73/lazygit_0.1.73_Darwin_x86_64.tar.gz
   220  func (u *Updater) getBinaryUrl(newVersion string) (string, error) {
   221  	extension := "tar.gz"
   222  	if runtime.GOOS == "windows" {
   223  		extension = "zip"
   224  	}
   225  	url := fmt.Sprintf(
   226  		"%s/releases/download/%s/lazygit_%s_%s_%s.%s",
   227  		PROJECT_URL,
   228  		newVersion,
   229  		newVersion[1:],
   230  		u.mappedOs(runtime.GOOS),
   231  		u.mappedArch(runtime.GOARCH),
   232  		extension,
   233  	)
   234  	u.Log.Info("Url for latest release is " + url)
   235  	return url, nil
   236  }
   237  
   238  // Update downloads the latest binary and replaces the current binary with it
   239  func (u *Updater) Update(newVersion string, onFinish func(error) error) {
   240  	go func() {
   241  		err := u.update(newVersion)
   242  		if err = onFinish(err); err != nil {
   243  			u.Log.Error(err)
   244  		}
   245  	}()
   246  }
   247  
   248  func (u *Updater) update(newVersion string) error {
   249  	rawUrl, err := u.getBinaryUrl(newVersion)
   250  	if err != nil {
   251  		return err
   252  	}
   253  	u.Log.Info("Updating with url " + rawUrl)
   254  	return u.downloadAndInstall(rawUrl)
   255  }
   256  
   257  func (u *Updater) downloadAndInstall(rawUrl string) error {
   258  	url, err := url.Parse(rawUrl)
   259  	if err != nil {
   260  		return err
   261  	}
   262  
   263  	g := new(getter.HttpGetter)
   264  	tempDir, err := ioutil.TempDir("", "lazygit")
   265  	if err != nil {
   266  		return err
   267  	}
   268  	defer os.RemoveAll(tempDir)
   269  	u.Log.Info("Temp directory is " + tempDir)
   270  
   271  	// Get it!
   272  	if err := g.Get(tempDir, url); err != nil {
   273  		return err
   274  	}
   275  
   276  	// get the path of the current binary
   277  	binaryPath, err := osext.Executable()
   278  	if err != nil {
   279  		return err
   280  	}
   281  	u.Log.Info("Binary path is " + binaryPath)
   282  
   283  	binaryName := filepath.Base(binaryPath)
   284  	u.Log.Info("Binary name is " + binaryName)
   285  
   286  	// Verify the main file exists
   287  	tempPath := filepath.Join(tempDir, binaryName)
   288  	u.Log.Info("Temp path to binary is " + tempPath)
   289  	if _, err := os.Stat(tempPath); err != nil {
   290  		return err
   291  	}
   292  
   293  	// swap out the old binary for the new one
   294  	err = os.Rename(tempPath, binaryPath)
   295  	if err != nil {
   296  		return err
   297  	}
   298  	u.Log.Info("Update complete!")
   299  
   300  	return nil
   301  }
   302  
   303  func (u *Updater) verifyResourceFound(rawUrl string) bool {
   304  	resp, err := http.Head(rawUrl)
   305  	if err != nil {
   306  		return false
   307  	}
   308  	defer resp.Body.Close()
   309  	u.Log.Info("Received status code ", resp.StatusCode)
   310  	// 403 means the resource is there (not going to bother adding extra request headers)
   311  	// 404 means its not
   312  	return resp.StatusCode == 403
   313  }