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 }