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 }