From f1d66df72a2f331e3f8f9dda80b1df53d090f73e Mon Sep 17 00:00:00 2001 From: xalanq Date: Mon, 29 Apr 2019 22:31:18 +0800 Subject: [PATCH] Add upgrade command --- README.md | 4 +- README_zh_CN.md | 4 +- cf.go | 13 ++- cmd/cmd.go | 2 + cmd/upgrade.go | 216 +++++++++++++++++++++++++++++++++++++++++++++ config/template.go | 14 +-- util/util.go | 15 ++++ 7 files changed, 251 insertions(+), 17 deletions(-) create mode 100644 cmd/upgrade.go diff --git a/README.md b/README.md index 3b275f91..9c81b2e0 100644 --- a/README.md +++ b/README.md @@ -44,8 +44,6 @@ $ go build -ldflags "-s -w" cf.go ## Usage ```plain -Codeforces Tool (cf). https://github.com/xalanq/cf-tool - You should run "cf config login" and "cf config add" at first. If you want to compete, the best command is "cf race 1111" where "1111" is the contest id. @@ -63,6 +61,7 @@ Usage: cf stand [] cf race cf pull [ac] [] [] + cf upgrade Examples: cf config login Config your username and password. @@ -94,6 +93,7 @@ Examples: cf pull 100 a Pull the latest code of problem "a" of contest 100 into "./100/". cf pull ac 100 a Pull the "Accepted" or "Pretests passed" code of problem "a" of contest 100. cf pull Pull the latest code of current problem into current path. + cf upgrade Upgrade the "cf" to the latest version from GitHub. Notes: "a" or "A", case-insensitive. diff --git a/README_zh_CN.md b/README_zh_CN.md index 2c15c39e..1d100c6f 100644 --- a/README_zh_CN.md +++ b/README_zh_CN.md @@ -42,8 +42,6 @@ $ go build -ldflags "-s -w" cf.go ## 使用方法 ```plain -Codeforces Tool (cf). https://github.com/xalanq/cf-tool - 首先你得用 "cf config login"、 "cf config add" 命令来配置一下 如果你想用本工具打比赛,那么最好用 "cf race 1111" 命令,其中 "1111" 是比赛的 id @@ -61,6 +59,7 @@ Codeforces Tool (cf). https://github.com/xalanq/cf-tool cf stand [] cf race cf pull [ac] [] [] + cf upgrade 例子: cf config login 配置你的用户名和密码。 @@ -91,6 +90,7 @@ Codeforces Tool (cf). https://github.com/xalanq/cf-tool cf pull 100 a 拉取比赛 id 为 100 的题目 a 的最新代码到文件夹 "./100/a" 下。 cf pull ac 100 a 拉取比赛 id 为 100 的题目 a 的 AC 代码。 cf pull 拉取当前题目的最新代码到当前文件夹下。 + cf upgrade Upgrade the "cf" to the latest version from GitHub. 注意: 表示题目的 id,比如 "a" 或者 "A",不区分大小写。 diff --git a/cf.go b/cf.go index 8bde3ea2..62806934 100644 --- a/cf.go +++ b/cf.go @@ -1,6 +1,9 @@ package main import ( + "fmt" + "strings" + "github.com/fatih/color" ansi "github.com/k0kubun/go-ansi" "github.com/xalanq/cf-tool/cmd" @@ -9,8 +12,10 @@ import ( docopt "github.com/docopt/docopt-go" ) +const version = "v0.5.0" + func main() { - usage := `Codeforces Tool (cf). https://github.com/xalanq/cf-tool + usage := `Codeforces Tool $%version%$ (cf). https://github.com/xalanq/cf-tool You should run "cf config login" and "cf config add" at first. @@ -29,6 +34,7 @@ Usage: cf stand [] cf race cf pull [ac] [] [] + cf upgrade Examples: cf config login Config your username and password. @@ -60,6 +66,7 @@ Examples: cf pull 100 a Pull the latest code of problem "a" of contest 100 into "./100/". cf pull ac 100 a Pull the "Accepted" or "Pretests passed" code of problem "a" of contest 100. cf pull Pull the latest code of current problem into current path. + cf upgrade Upgrade the "cf" to the latest version from GitHub. Notes: "a" or "A", case-insensitive. @@ -106,8 +113,10 @@ Script in template: Options: -h --help --version` + usage = strings.Replace(usage, `$%version%$`, version, 1) - args, _ := docopt.Parse(usage, nil, true, "Codeforces Tool (cf) v0.4.0", false) + args, _ := docopt.Parse(usage, nil, true, fmt.Sprintf("Codeforces Tool (cf) %v", version), false) + args[`{version}`] = version color.Output = ansi.NewAnsiStdout() config.Init() err := cmd.Eval(args) diff --git a/cmd/cmd.go b/cmd/cmd.go index ee7a5000..e7d81060 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -40,6 +40,8 @@ func Eval(args map[string]interface{}) error { return Race(args) } else if args["pull"].(bool) { return Pull(args) + } else if args["upgrade"].(bool) { + return Upgrade(args["{version}"].(string)) } return nil } diff --git a/cmd/upgrade.go b/cmd/upgrade.go new file mode 100644 index 00000000..c37585cb --- /dev/null +++ b/cmd/upgrade.go @@ -0,0 +1,216 @@ +package cmd + +import ( + "archive/zip" + "bytes" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + "os" + "path/filepath" + "regexp" + "runtime" + "strconv" + "time" + + "github.com/fatih/color" + "github.com/xalanq/cf-tool/util" +) + +func less(a, b string) bool { + reg := regexp.MustCompile(`(\d+)\.(\d+)\.(\d+)`) + x := reg.FindSubmatch([]byte(a)) + y := reg.FindSubmatch([]byte(b)) + num := func(s []byte) int { + n, _ := strconv.Atoi(string(s)) + return n + } + for i := 1; i <= 3; i++ { + if num(x[i]) < num(y[i]) { + return true + } else if num(x[i]) > num(y[i]) { + return false + } + } + return false +} + +func getLatest() (version, note, ptime, url string, size uint, err error) { + goos := "" + switch runtime.GOOS { + case "darwin": + goos = "osx" + case "linux": + goos = "linux" + case "windows": + goos = "win" + default: + err = fmt.Errorf("Not support %v", runtime.GOOS) + return + } + + arch := "" + switch runtime.GOARCH { + case "386": + arch = "32" + case "amd64": + arch = "64" + default: + err = fmt.Errorf("Not support %v", runtime.GOARCH) + return + } + + resp, err := http.Get("https://api.github.com/repos/xalanq/cf-tool/releases/latest") + if err != nil { + return + } + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return + } + result := make(map[string]interface{}) + json.Unmarshal(body, &result) + version = result["tag_name"].(string) + note = result["body"].(string) + tm, _ := time.Parse("2006-01-02T15:04:05Z", result["published_at"].(string)) + ptime = tm.In(time.Local).Format("2006-01-02 15:04") + url = fmt.Sprintf("https://github.com/xalanq/cf-tool/releases/download/%v/cf_%v_%v_%v.zip", version, version, goos, arch) + assets, _ := result["assets"].([]interface{}) + for _, tmp := range assets { + asset, _ := tmp.(map[string]interface{}) + if url == asset["browser_download_url"] { + size = uint(asset["size"].(float64)) + break + } + } + return +} + +// WriteCounter progress counter +type WriteCounter struct { + Count, Total uint + last uint +} + +// Print print progress +func (w *WriteCounter) Print() { + fmt.Printf("\rProgress: %v/%v KB Speed: %v KB/s", w.Count/1024, w.Total/1024, (w.Count-w.last)/1024) + w.last = w.Count +} + +func (w *WriteCounter) Write(p []byte) (int, error) { + n := len(p) + w.Count += uint(n) + return n, nil +} + +func upgrade(url, exe string, size uint) (err error) { + color.Cyan("Download %v", url) + counter := &WriteCounter{Count: 0, Total: size, last: 0} + counter.Print() + + ticker := time.NewTicker(time.Second) + go func() { + for range ticker.C { + counter.Print() + } + }() + + resp, err := http.Get(url) + if err != nil { + return + } + defer resp.Body.Close() + + data, err := ioutil.ReadAll(io.TeeReader(resp.Body, counter)) + if err != nil { + ticker.Stop() + counter.Print() + fmt.Println() + return + } + ticker.Stop() + counter.Print() + fmt.Println() + reader, err := zip.NewReader(bytes.NewReader(data), int64(len(data))) + if err != nil { + return + } + + rc, err := reader.File[0].Open() + if err != nil { + return + } + defer rc.Close() + + newPath := filepath.Join(os.TempDir(), fmt.Sprintf(".%s.new", filepath.Base(exe))) + oldPath := filepath.Join(os.TempDir(), fmt.Sprintf(".%s.old", filepath.Base(exe))) + + file, err := os.OpenFile(newPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0755) + if err != nil { + return + } + defer file.Close() + + _, err = io.Copy(file, rc) + if err != nil { + return + } + file.Close() + + err = os.Rename(exe, oldPath) + if err != nil { + os.Remove(newPath) + return + } + + err = os.Rename(newPath, exe) + if err != nil { + os.Rename(oldPath, exe) + os.Remove(newPath) + return + } + + os.Remove(oldPath) + return nil +} + +// Upgrade itself +func Upgrade(version string) error { + color.Cyan("Checking version") + latest, note, ptime, url, size, err := getLatest() + if err != nil { + return err + } + if !less(version, latest) { + color.Green("Current version %v is the latest", version) + return nil + } + + color.Red("Current version is %v", version) + color.Green("The latest version is %v, published at %v", latest, ptime) + fmt.Println(note) + + if !util.YesOrNo("Do you want to upgrade (y/n)? ") { + return nil + } + + exe, err := os.Executable() + if err != nil { + return err + } + + if exe, err = filepath.EvalSymlinks(exe); err != nil { + return err + } + + if err = upgrade(url, exe, size); err != nil { + return err + } + + color.Green("Successfully updated to version %v", latest) + return nil +} diff --git a/config/template.go b/config/template.go index 9071bae0..956facbd 100644 --- a/config/template.go +++ b/config/template.go @@ -72,18 +72,10 @@ func (c *Config) AddTemplate() (err error) { beforeScript, script, afterScript, }) - color.Cyan("Make it default (y/n)? ") - for { - tmp := util.ScanlineTrim() - if tmp == "y" || tmp == "Y" { - c.Default = len(c.Template) - 1 - break - } - if tmp == "n" || tmp == "N" { - break - } - color.Red("Invalid input. Please input again: ") + if util.YesOrNo("Make it default (y/n)? ") { + c.Default = len(c.Template) - 1 } + return c.save() } diff --git a/util/util.go b/util/util.go index 1b3d611f..abf0f37f 100644 --- a/util/util.go +++ b/util/util.go @@ -51,3 +51,18 @@ func ChooseIndex(maxLen int) int { color.Red("Invalid index! Please try again: ") } } + +// YesOrNo must choose one +func YesOrNo(note string) bool { + color.Cyan(note) + for { + tmp := ScanlineTrim() + if tmp == "y" || tmp == "Y" { + return true + } + if tmp == "n" || tmp == "N" { + return false + } + color.Red("Invalid input. Please input again: ") + } +}