From 7a27747cb4b50ebf0a4cea6778924ae3a56be7a4 Mon Sep 17 00:00:00 2001 From: Zhao Xiaojie Date: Sat, 30 Jan 2021 14:25:01 +0800 Subject: [PATCH] Add support to parse URL from hd-home (#10) * Add support to parse URL from hd-home * Parse binary field * Test pass with install kind and kubekey * Fix the errors found by gosec * Typo fixes found by typoci --- .github/.typo-ci.yml | 16 ++++++++ Makefile | 5 +++ cmd/get.go | 94 ++++++++++++++++++++++++++++++++++++++++++++ cmd/install.go | 40 ++++++++++++++++--- go.mod | 3 ++ go.sum | 2 + pkg/release.go | 71 +++++++++++++++++++++++++++++++++ 7 files changed, 226 insertions(+), 5 deletions(-) create mode 100644 .github/.typo-ci.yml create mode 100644 pkg/release.go diff --git a/.github/.typo-ci.yml b/.github/.typo-ci.yml new file mode 100644 index 0000000..3c68881 --- /dev/null +++ b/.github/.typo-ci.yml @@ -0,0 +1,16 @@ +dictionaries: + - en + - en_GB +exclude_fiels: + - ".github/**/*" + - "go.mod" + - "go.sum" + - Makefile +excluded_words: + - KubeSphere + - kubespheredev + - minio + - jcli + - ioutil + - Unmarshal + - Errorf diff --git a/Makefile b/Makefile index 8820d23..ed20992 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,11 @@ build: fmt export GOPROXY=https://goproxy.io CGO_ENABLE=0 go build -ldflags "-w -s" -o bin/hd +build-linux: fmt + export GOPROXY=https://goproxy.io + CGO_ENABLE=0 GOOS=linux go build -ldflags "-w -s" -o bin/linux/hd + upx bin/linux/hd + run: go run main.go diff --git a/cmd/get.go b/cmd/get.go index 36aed05..5a8232d 100644 --- a/cmd/get.go +++ b/cmd/get.go @@ -1,13 +1,19 @@ package cmd import ( + "bytes" "fmt" + "github.com/ghodss/yaml" "github.com/linuxsuren/http-downloader/pkg" + "github.com/mitchellh/go-homedir" "github.com/spf13/cobra" + "io/ioutil" "net/url" + "os" "path" "runtime" "strings" + "text/template" ) // NewGetCmd return the get command @@ -52,6 +58,7 @@ type downloadOption struct { // inner fields name string + Tar bool } const ( @@ -102,9 +109,72 @@ func (o *downloadOption) providerURLParse(path string) (url string, err error) { org, repo, version, name, o.OS, o.Arch) } o.name = name + + // try to parse from config + userHome, _ := homedir.Dir() + configDir := userHome + "/.config/hd-home" + matchedFile := configDir + "/config/" + org + "/" + repo + ".yml" + if ok, _ := pathExists(matchedFile); ok { + var data []byte + if data, err = ioutil.ReadFile(matchedFile); err == nil { + cfg := hdConfig{} + + if err = yaml.Unmarshal(data, &cfg); err == nil { + hdPackage := &hdPackage{ + Name: o.name, + Version: version, + OS: runtime.GOOS, + Arch: runtime.GOARCH, + } + if version == "latest" { + ghClient := pkg.ReleaseClient{ + Org: org, + Repo: repo, + } + ghClient.Init() + if asset, err := ghClient.GetLatestJCLIAsset(); err == nil { + hdPackage.Version = asset.TagName + } else { + fmt.Println(err, "cannot get the asset") + } + } + + if cfg.Filename != "" { + tmp, _ := template.New("hd").Parse(cfg.Filename) + + var buf bytes.Buffer + if err = tmp.Execute(&buf, hdPackage); err == nil { + url = fmt.Sprintf("https://github.com/%s/%s/releases/%s/download/%s", + org, repo, version, buf.String()) + + o.Output = buf.String() + } + } + + o.Tar = cfg.Tar + if cfg.Binary != "" { + o.name = cfg.Binary + } + } + } + } return } +type hdConfig struct { + Name string + Filename string + Binary string + Tar bool +} + +type hdPackage struct { + Name string + Version string + OS string + Arch string +} + func (o *downloadOption) preRunE(cmd *cobra.Command, args []string) (err error) { if len(args) <= 0 { return fmt.Errorf("no URL provided") @@ -143,3 +213,27 @@ func (o *downloadOption) runE(cmd *cobra.Command, args []string) (err error) { } return } + +func (o *downloadOption) fetchHomeConfig() (err error) { + userHome, _ := homedir.Dir() + configDir := userHome + "/.config/hd-home" + if ok, _ := pathExists(configDir); ok { + err = execCommand("git", "pull", "-C", configDir) + } else { + if err = os.MkdirAll(configDir, 0644); err == nil { + err = execCommand("git", "clone", "https://github.com/LinuxSuRen/hd-home", configDir) + } + } + return +} + +func pathExists(path string) (bool, error) { + _, err := os.Stat(path) + if err == nil { + return true, nil + } + if os.IsNotExist(err) { + return false, nil + } + return false, err +} diff --git a/cmd/install.go b/cmd/install.go index 709cc9c..6c14d01 100644 --- a/cmd/install.go +++ b/cmd/install.go @@ -27,7 +27,7 @@ func NewInstallCmd() (cmd *cobra.Command) { //flags.StringVarP(&opt.Mode, "mode", "m", "package", // "If you want to install it via platform package manager") flags.BoolVarP(&opt.ShowProgress, "show-progress", "", true, "If show the progress of download") - flags.IntVarP(&opt.Thread, "thread", "t", 0, + flags.IntVarP(&opt.Thread, "thread", "t", 4, `Download file with multi-threads. It only works when its value is bigger than 1`) flags.BoolVarP(&opt.KeepPart, "keep-part", "", false, "If you want to keep the part files instead of deleting them") @@ -42,22 +42,52 @@ type installOption struct { Mode string } +func (o *installOption) preRunE(cmd *cobra.Command, args []string) (err error) { + if err = o.fetchHomeConfig(); err != nil { + // this is not a fatal, don't block the process + cmd.Printf("Failed with fetching home config: %v\n", err) + } + err = o.downloadOption.preRunE(cmd, args) + return +} + func (o *installOption) runE(cmd *cobra.Command, args []string) (err error) { if err = o.downloadOption.runE(cmd, args); err != nil { return } - if err = o.extractFiles(o.Output, o.name); err == nil { - err = o.overWriteBinary(fmt.Sprintf("%s/%s", filepath.Dir(o.Output), o.name), fmt.Sprintf("/usr/local/bin/%s", o.name)) + var source string + var target string + if o.Tar { + if err = o.extractFiles(o.Output, o.name); err == nil { + source = fmt.Sprintf("%s/%s", filepath.Dir(o.Output), o.name) + target = fmt.Sprintf("/usr/local/bin/%s", o.name) + } else { + err = fmt.Errorf("cannot extract %s from tar file, error: %v", o.Output, err) + } } else { - err = fmt.Errorf("cannot extract %s from tar file, error: %v", o.Output, err) + source = o.downloadOption.Output + target = fmt.Sprintf("/usr/local/bin/%s", o.name) + } + + if err == nil { + fmt.Println("install", source, "to", target) + err = o.overWriteBinary(source, target) } return } func (o *installOption) overWriteBinary(sourceFile, targetPath string) (err error) { switch runtime.GOOS { - case "linux": + case "linux", "darwin": + if err = execCommand("chmod", "u+x", sourceFile); err != nil { + return + } + + if err = execCommand("rm", "-rf", targetPath); err != nil { + return + } + var cp string if cp, err = exec.LookPath("cp"); err == nil { err = syscall.Exec(cp, []string{"cp", sourceFile, targetPath}, os.Environ()) diff --git a/go.mod b/go.mod index 12576ea..2f11b30 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,12 @@ module github.com/linuxsuren/http-downloader go 1.15 require ( + github.com/ghodss/yaml v1.0.0 github.com/golang/mock v1.4.4 + github.com/google/go-github/v29 v29.0.3 github.com/gosuri/uiprogress v0.0.1 github.com/linuxsuren/cobra-extension v0.0.10 + github.com/mitchellh/go-homedir v1.1.0 github.com/onsi/ginkgo v1.14.2 github.com/onsi/gomega v1.10.4 github.com/spf13/cobra v1.1.1 diff --git a/go.sum b/go.sum index e83c2b9..f67fd54 100644 --- a/go.sum +++ b/go.sum @@ -55,6 +55,7 @@ github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= @@ -180,6 +181,7 @@ github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyex github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= diff --git a/pkg/release.go b/pkg/release.go new file mode 100644 index 0000000..3fbdf6c --- /dev/null +++ b/pkg/release.go @@ -0,0 +1,71 @@ +package pkg + +import ( + "context" + "github.com/google/go-github/v29/github" +) + +// ReleaseClient is the client of jcli github +type ReleaseClient struct { + Client *github.Client + Org string + Repo string +} + +// ReleaseAsset is the asset from GitHub release +type ReleaseAsset struct { + TagName string + Body string +} + +// Init init the GitHub client +func (g *ReleaseClient) Init() { + g.Client = github.NewClient(nil) +} + +// GetLatestJCLIAsset returns the latest jcli asset +func (g *ReleaseClient) GetLatestJCLIAsset() (*ReleaseAsset, error) { + return g.GetLatestReleaseAsset(g.Org, g.Repo) +} + +// GetLatestReleaseAsset returns the latest release asset +func (g *ReleaseClient) GetLatestReleaseAsset(owner, repo string) (ra *ReleaseAsset, err error) { + ctx := context.Background() + + var release *github.RepositoryRelease + if release, _, err = g.Client.Repositories.GetLatestRelease(ctx, owner, repo); err == nil { + ra = &ReleaseAsset{ + TagName: release.GetTagName(), + Body: release.GetBody(), + } + } + return +} + +// GetJCLIAsset returns the asset from a tag name +func (g *ReleaseClient) GetJCLIAsset(tagName string) (*ReleaseAsset, error) { + return g.GetReleaseAssetByTagName(g.Org, g.Repo, tagName) +} + +// GetReleaseAssetByTagName returns the release asset by tag name +func (g *ReleaseClient) GetReleaseAssetByTagName(owner, repo, tagName string) (ra *ReleaseAsset, err error) { + ctx := context.Background() + + opt := &github.ListOptions{ + PerPage: 99999, + } + + var releaseList []*github.RepositoryRelease + if releaseList, _, err = g.Client.Repositories.ListReleases(ctx, owner, repo, opt); err == nil { + for _, item := range releaseList { + if item.GetTagName() == tagName { + ra = &ReleaseAsset{ + TagName: item.GetTagName(), + Body: item.GetBody(), + } + break + } + } + } + return +}