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 }