/
updater.go
287 lines (245 loc) · 8.67 KB
/
updater.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
package fs
import (
"bufio"
"fmt"
"github.com/google/go-github/github"
"github.com/hashicorp/go-version"
"github.com/kataras/go-errors"
"io"
"os"
"os/exec"
)
// updater.go Go app updater hosted on github, based on 'releases & tags',
// unique and simple source code, no signs or other 'secure' methods.
//
// Note: the previous installed files(in $GOPATH) should not be edited before, if edited the go get tool will fail to upgrade these packages.
//
// tag name (github version) should be compatible with the Semantic Versioning 2.0.0
// Read more about Semantic Versioning 2.0.0: http://semver.org/
//
// quick example:
// package main
//
// import (
// "github.com/kataras/go-fs"
// "fmt"
// )
//
// func main(){
// fmt.Println("Current version is: 0.0.3")
//
// updater, err := fs.GetUpdater("kataras","rizla", "0.0.3")
// if err !=nil{
// panic(err)
// }
//
// updated := updater.Run()
// _ = updated
// }
var (
errUpdaterUnknown = errors.New("Updater: Unknown error: %s")
errCantFetchRepo = errors.New("Updater: Error while trying to fetch the repository: %s. Trace: %s")
errAccessRepo = errors.New("Updater: Couldn't access to the github repository, please make sure you're connected to the internet")
)
// Updater is the base struct for the Updater feature
type Updater struct {
currentVersion *version.Version
latestVersion *version.Version
owner string
repo string
}
// GetUpdater returns a new Updater based on a github repository and the latest local release version(string, example: "4.2.3" or "v4.2.3-rc1")
func GetUpdater(owner string, repo string, currentReleaseVersion string) (*Updater, error) {
client := github.NewClient(nil) // unuthenticated client, 60 req/hour
///TODO: rate limit error catching( impossible to same client checks 60 times for github updates, but we should do that check)
// get the latest release, delay depends on the user's internet connection's download speed
latestRelease, response, err := client.Repositories.GetLatestRelease(owner, repo)
if err != nil {
return nil, errCantFetchRepo.Format(owner+":"+repo, err)
}
if c := response.StatusCode; c != 200 && c != 201 && c != 202 && c != 301 && c != 302 && c == 304 {
return nil, errAccessRepo
}
currentVersion, err := version.NewVersion(currentReleaseVersion)
if err != nil {
return nil, err
}
latestVersion, err := version.NewVersion(*latestRelease.TagName)
if err != nil {
return nil, err
}
u := &Updater{
currentVersion: currentVersion,
latestVersion: latestVersion,
owner: owner,
repo: repo,
}
return u, nil
}
// HasUpdate returns true if a new update is available
// the second output parameter is the latest ,full, version
func (u *Updater) HasUpdate() (bool, string) {
return u.currentVersion.LessThan(u.latestVersion), u.latestVersion.String()
}
var (
// DefaultUpdaterAlreadyInstalledMessage "\nThe latest version '%s' was already installed."
DefaultUpdaterAlreadyInstalledMessage = "\nThe latest version '%s' was already installed."
)
// Run runs the update, returns true if update has been found and installed, otherwise false
func (u *Updater) Run(setters ...optionSetter) bool {
opt := &Options{Stdin: os.Stdin, Stdout: os.Stdout, Stderr: os.Stderr, Silent: false} // default options
for _, setter := range setters {
setter.Set(opt)
}
writef := func(s string, a ...interface{}) {
if !opt.Silent {
opt.Stdout.Write([]byte(fmt.Sprintf(s, a...)))
}
}
has, v := u.HasUpdate()
if has {
var scanner *bufio.Scanner
if opt.Stdin != nil {
scanner = bufio.NewScanner(opt.Stdin)
}
shouldProceedUpdate := func() bool {
return shouldProceedUpdate(scanner)
}
writef("\nA newer version has been found[%s > %s].\n"+
"Release notes: %s\n"+
"Update now?[%s]: ",
u.latestVersion.String(), u.currentVersion.String(),
fmt.Sprintf("https://github.com/%s/%s/releases/latest", u.owner, u.repo),
DefaultUpdaterYesInput[0]+"/n")
if shouldProceedUpdate() {
if !opt.Silent {
finish := ShowIndicator(opt.Stdout, true)
defer func() {
finish <- true
}()
}
// go get -u github.com/:owner/:repo
cmd := exec.Command("go", "get", "-u", fmt.Sprintf("github.com/%s/%s", u.owner, u.repo))
cmd.Stdout = opt.Stdout
cmd.Stderr = opt.Stderr
if err := cmd.Run(); err != nil {
writef("\nError while trying to get the package: %s.", err.Error())
}
writef("\010\010\010") // remove the loading bars
writef("Update has been installed, current version: %s. Please re-start your App.\n", u.latestVersion.String())
// TODO: normally, this should be in dev-mode machine, so a 'go build' and' & './$executable' on the current working path should be ok
// for now just log a message to re-run the app manually
//writef("\nUpdater was not able to re-build and re-run your updated App.\nPlease run your App again, by yourself.")
return true
}
} else {
writef(fmt.Sprintf(DefaultUpdaterAlreadyInstalledMessage, v))
}
return false
}
// DefaultUpdaterYesInput the string or character which user should type to proceed the update, if !silent
var DefaultUpdaterYesInput = [...]string{"y", "yes", "nai", "si"}
func shouldProceedUpdate(sc *bufio.Scanner) bool {
silent := sc == nil
inputText := ""
if !silent {
if sc.Scan() {
inputText = sc.Text()
}
}
for _, s := range DefaultUpdaterYesInput {
if inputText == s {
return true
}
}
// if silent, then return 'yes/true' always
return silent
}
// Options the available options used iside the updater.Run func
type Options struct {
Silent bool
// Stdin specifies the process's standard input.
// If Stdin is nil, the process reads from the null device (os.DevNull).
// If Stdin is an *os.File, the process's standard input is connected
// directly to that file.
// Otherwise, during the execution of the command a separate
// goroutine reads from Stdin and delivers that data to the command
// over a pipe. In this case, Wait does not complete until the goroutine
// stops copying, either because it has reached the end of Stdin
// (EOF or a read error) or because writing to the pipe returned an error.
Stdin io.Reader
// Stdout and Stderr specify the process's standard output and error.
//
// If either is nil, Run connects the corresponding file descriptor
// to the null device (os.DevNull).
//
// If Stdout and Stderr are the same writer, at most one
// goroutine at a time will call Write.
Stdout io.Writer
Stderr io.Writer
}
// Set implements the optionSetter
func (o *Options) Set(main *Options) {
main.Silent = o.Silent
}
type optionSetter interface {
Set(*Options)
}
// OptionSet sets an option
type OptionSet func(*Options)
// Set implements the optionSetter
func (o OptionSet) Set(main *Options) {
o(main)
}
// Silent sets the Silent option to the 'val'
func Silent(val bool) OptionSet {
return func(o *Options) {
o.Silent = val
}
}
// Stdin specifies the process's standard input.
// If Stdin is nil, the process reads from the null device (os.DevNull).
// If Stdin is an *os.File, the process's standard input is connected
// directly to that file.
// Otherwise, during the execution of the command a separate
// goroutine reads from Stdin and delivers that data to the command
// over a pipe. In this case, Wait does not complete until the goroutine
// stops copying, either because it has reached the end of Stdin
// (EOF or a read error) or because writing to the pipe returned an error.
func Stdin(val io.Reader) OptionSet {
return func(o *Options) {
o.Stdin = val
}
}
// Stdout specify the process's standard output and error.
//
// If either is nil, Run connects the corresponding file descriptor
// to the null device (os.DevNull).
//
func Stdout(val io.Writer) OptionSet {
return func(o *Options) {
o.Stdout = val
}
}
// Stderr specify the process's standard output and error.
//
// If Stdout and Stderr are the same writer, at most one
// goroutine at a time will call Write.
func Stderr(val io.Writer) OptionSet {
return func(o *Options) {
o.Stderr = val
}
}
// simple way to compare version is to make them numbers
// and remove any dots and 'v' or 'version' or 'release'
// so
// the v4.2.2 will be 422
// which is bigger from v4.2.1 (which will be 421)
// also a version could be something like: 1.0.0-beta+exp.sha.5114f85
// so we should add a number of any alpha,beta,rc and so on
// maybe this way is not the best but I think it will cover our needs
// and the simplicity of source I keep to all of my packages.
//var removeChars = [...]string{".","v","version","prerelease","pre-release","release","-","alpha","beta","rc"}
// or just remove any non-numeric chars using regex...
// ok.. just found a better way, to use a third-party package 'go-version' which will cover all version formats
//func parseVersion(s string) int {