Skip to content

Commit 5ba75dd

Browse files
authored
Add install command (#9)
* Add install command * Add the usage of hd install command to readme file
1 parent d283c32 commit 5ba75dd

File tree

5 files changed

+331
-136
lines changed

5 files changed

+331
-136
lines changed

README-zh.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,18 @@ hd get https://github.com/jenkins-zh/jenkins-cli/releases/latest/download/jcli-l
2525
或者,用一个更加简便的办法:
2626

2727
```
28-
hd get jenkins-zh/jenkins-cli/jcli --thread 6
28+
hd get jenkins-zh/jenkins-cli/jcli -t 6
29+
```
30+
31+
获取,你也可以安装一个来自 GitHub 的软件包:
32+
33+
```
34+
hd install jenkins-zh/jenkins-cli/jcli -t 6
2935
```
3036

3137
# 功能
3238

3339
* 基于 HTTP 协议下载文件的 Golang 工具库
3440
* 多线程
3541
* 断点续传 (TODO)
36-
* 对 GitHub release 文件下载友好
42+
* 对 GitHub release 文件下载(安装)友好

README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,13 @@ hd get https://github.com/jenkins-zh/jenkins-cli/releases/latest/download/jcli-l
2525
Or use a simple way:
2626

2727
```
28-
hd get jenkins-zh/jenkins-cli/jcli --thread 6
28+
hd get jenkins-zh/jenkins-cli/jcli -t 6
29+
```
30+
31+
Or you can also install a package from GitHub:
32+
33+
```
34+
hd install jenkins-zh/jenkins-cli/jcli -t 6
2935
```
3036

3137
# Features

cmd/get.go

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
"github.com/linuxsuren/http-downloader/pkg"
6+
"github.com/spf13/cobra"
7+
"net/url"
8+
"path"
9+
"runtime"
10+
"strings"
11+
)
12+
13+
// NewGetCmd return the get command
14+
func NewGetCmd() (cmd *cobra.Command) {
15+
opt := &downloadOption{}
16+
cmd = &cobra.Command{
17+
Use: "get",
18+
Short: "download the file",
19+
Example: "hd get jenkins-zh/jenkins-cli/jcli --thread 6",
20+
PreRunE: opt.preRunE,
21+
RunE: opt.runE,
22+
}
23+
24+
// set flags
25+
flags := cmd.Flags()
26+
flags.StringVarP(&opt.Output, "output", "o", "", "Write output to <file> instead of stdout.")
27+
flags.BoolVarP(&opt.ShowProgress, "show-progress", "", true, "If show the progress of download")
28+
flags.Int64VarP(&opt.ContinueAt, "continue-at", "", -1, "ContinueAt")
29+
flags.IntVarP(&opt.Thread, "thread", "t", 0,
30+
`Download file with multi-threads. It only works when its value is bigger than 1`)
31+
flags.BoolVarP(&opt.KeepPart, "keep-part", "", false,
32+
"If you want to keep the part files instead of deleting them")
33+
flags.StringVarP(&opt.Provider, "provider", "", ProviderGitHub, "The file provider")
34+
flags.StringVarP(&opt.OS, "os", "", runtime.GOOS, "The OS of target binary file")
35+
flags.StringVarP(&opt.Arch, "arch", "", runtime.GOARCH, "The arch of target binary file")
36+
return
37+
}
38+
39+
type downloadOption struct {
40+
URL string
41+
Output string
42+
ShowProgress bool
43+
44+
ContinueAt int64
45+
46+
Provider string
47+
Arch string
48+
OS string
49+
50+
Thread int
51+
KeepPart bool
52+
53+
// inner fields
54+
name string
55+
}
56+
57+
const (
58+
// ProviderGitHub represents https://github.com
59+
ProviderGitHub = "github"
60+
)
61+
62+
func (o *downloadOption) providerURLParse(path string) (url string, err error) {
63+
url = path
64+
if o.Provider != ProviderGitHub {
65+
return
66+
}
67+
68+
var (
69+
org string
70+
repo string
71+
name string
72+
version string
73+
)
74+
75+
addr := strings.Split(url, "/")
76+
if len(addr) >= 2 {
77+
org = addr[0]
78+
repo = addr[1]
79+
name = repo
80+
} else {
81+
err = fmt.Errorf("only support format xx/xx or xx/xx/xx")
82+
return
83+
}
84+
85+
if len(addr) == 3 {
86+
name = addr[2]
87+
} else if len(addr) > 3 {
88+
err = fmt.Errorf("only support format xx/xx or xx/xx/xx")
89+
}
90+
91+
// extract version from name
92+
if strings.Contains(name, "@") {
93+
nameWithVer := strings.Split(name, "@")
94+
name = nameWithVer[0]
95+
version = nameWithVer[1]
96+
97+
url = fmt.Sprintf("https://github.com/%s/%s/releases/download/%s/%s-%s-%s.tar.gz",
98+
org, repo, version, name, o.OS, o.Arch)
99+
} else {
100+
version = "latest"
101+
url = fmt.Sprintf("https://github.com/%s/%s/releases/%s/download/%s-%s-%s.tar.gz",
102+
org, repo, version, name, o.OS, o.Arch)
103+
}
104+
o.name = name
105+
return
106+
}
107+
108+
func (o *downloadOption) preRunE(cmd *cobra.Command, args []string) (err error) {
109+
if len(args) <= 0 {
110+
return fmt.Errorf("no URL provided")
111+
}
112+
113+
targetURL := args[0]
114+
if !strings.HasPrefix(targetURL, "http://") && !strings.HasPrefix(targetURL, "https://") {
115+
if targetURL, err = o.providerURLParse(targetURL); err != nil {
116+
err = fmt.Errorf("only http:// or https:// supported, error: %v", err)
117+
return
118+
}
119+
cmd.Printf("start to download from %s\n", targetURL)
120+
}
121+
o.URL = targetURL
122+
123+
if o.Output == "" {
124+
var urlObj *url.URL
125+
if urlObj, err = url.Parse(o.URL); err == nil {
126+
o.Output = path.Base(urlObj.Path)
127+
128+
if o.Output == "" {
129+
err = fmt.Errorf("output cannot be empty")
130+
}
131+
} else {
132+
err = fmt.Errorf("cannot parse the target URL, error: '%v'", err)
133+
}
134+
}
135+
return
136+
}
137+
138+
func (o *downloadOption) runE(cmd *cobra.Command, args []string) (err error) {
139+
if o.Thread <= 1 {
140+
err = pkg.DownloadWithContinue(o.URL, o.Output, o.ContinueAt, 0, o.ShowProgress)
141+
} else {
142+
err = pkg.DownloadFileWithMultipleThreadKeepParts(o.URL, o.Output, o.Thread, o.KeepPart, o.ShowProgress)
143+
}
144+
return
145+
}

cmd/install.go

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
package cmd
2+
3+
import (
4+
"archive/tar"
5+
"compress/gzip"
6+
"fmt"
7+
"github.com/spf13/cobra"
8+
"io"
9+
"os"
10+
"os/exec"
11+
"path/filepath"
12+
"runtime"
13+
"sync"
14+
"syscall"
15+
)
16+
17+
// NewInstallCmd returns the install command
18+
func NewInstallCmd() (cmd *cobra.Command) {
19+
opt := &installOption{}
20+
cmd = &cobra.Command{
21+
Use: "install",
22+
PreRunE: opt.preRunE,
23+
RunE: opt.runE,
24+
}
25+
26+
flags := cmd.Flags()
27+
//flags.StringVarP(&opt.Mode, "mode", "m", "package",
28+
// "If you want to install it via platform package manager")
29+
flags.BoolVarP(&opt.ShowProgress, "show-progress", "", true, "If show the progress of download")
30+
flags.IntVarP(&opt.Thread, "thread", "t", 0,
31+
`Download file with multi-threads. It only works when its value is bigger than 1`)
32+
flags.BoolVarP(&opt.KeepPart, "keep-part", "", false,
33+
"If you want to keep the part files instead of deleting them")
34+
flags.StringVarP(&opt.Provider, "provider", "", ProviderGitHub, "The file provider")
35+
flags.StringVarP(&opt.OS, "os", "", runtime.GOOS, "The OS of target binary file")
36+
flags.StringVarP(&opt.Arch, "arch", "", runtime.GOARCH, "The arch of target binary file")
37+
return
38+
}
39+
40+
type installOption struct {
41+
downloadOption
42+
Mode string
43+
}
44+
45+
func (o *installOption) runE(cmd *cobra.Command, args []string) (err error) {
46+
if err = o.downloadOption.runE(cmd, args); err != nil {
47+
return
48+
}
49+
50+
if err = o.extractFiles(o.Output, o.name); err == nil {
51+
err = o.overWriteBinary(fmt.Sprintf("%s/%s", filepath.Dir(o.Output), o.name), fmt.Sprintf("/usr/local/bin/%s", o.name))
52+
} else {
53+
err = fmt.Errorf("cannot extract %s from tar file, error: %v", o.Output, err)
54+
}
55+
return
56+
}
57+
58+
func (o *installOption) overWriteBinary(sourceFile, targetPath string) (err error) {
59+
switch runtime.GOOS {
60+
case "linux":
61+
var cp string
62+
if cp, err = exec.LookPath("cp"); err == nil {
63+
err = syscall.Exec(cp, []string{"cp", sourceFile, targetPath}, os.Environ())
64+
}
65+
default:
66+
sourceF, _ := os.Open(sourceFile)
67+
targetF, _ := os.OpenFile(targetPath, os.O_CREATE|os.O_RDWR, 0664)
68+
if _, err = io.Copy(targetF, sourceF); err != nil {
69+
err = fmt.Errorf("cannot copy %s from %s to %v, error: %v", o.name, sourceFile, targetPath, err)
70+
}
71+
}
72+
return
73+
}
74+
75+
func (o *installOption) extractFiles(tarFile, targetName string) (err error) {
76+
var f *os.File
77+
var gzf *gzip.Reader
78+
if f, err = os.Open(tarFile); err != nil {
79+
return
80+
}
81+
defer func() {
82+
_ = f.Close()
83+
}()
84+
85+
if gzf, err = gzip.NewReader(f); err != nil {
86+
return
87+
}
88+
89+
tarReader := tar.NewReader(gzf)
90+
var header *tar.Header
91+
for {
92+
if header, err = tarReader.Next(); err == io.EOF {
93+
err = nil
94+
break
95+
} else if err != nil {
96+
break
97+
}
98+
name := header.Name
99+
100+
switch header.Typeflag {
101+
case tar.TypeReg:
102+
if name != targetName {
103+
continue
104+
}
105+
var targetFile *os.File
106+
if targetFile, err = os.OpenFile(fmt.Sprintf("%s/%s", filepath.Dir(tarFile), name),
107+
os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode)); err != nil {
108+
break
109+
}
110+
if _, err = io.Copy(targetFile, tarReader); err != nil {
111+
break
112+
}
113+
_ = targetFile.Close()
114+
}
115+
}
116+
return
117+
}
118+
119+
func execCommand(name string, arg ...string) (err error) {
120+
command := exec.Command(name, arg...)
121+
122+
var stdout []byte
123+
var errStdout error
124+
stdoutIn, _ := command.StdoutPipe()
125+
stderrIn, _ := command.StderrPipe()
126+
err = command.Start()
127+
if err != nil {
128+
return err
129+
}
130+
131+
// cmd.Wait() should be called only after we finish reading
132+
// from stdoutIn and stderrIn.
133+
// wg ensures that we finish
134+
var wg sync.WaitGroup
135+
wg.Add(1)
136+
go func() {
137+
stdout, errStdout = copyAndCapture(os.Stdout, stdoutIn)
138+
wg.Done()
139+
}()
140+
141+
copyAndCapture(os.Stderr, stderrIn)
142+
143+
wg.Wait()
144+
145+
err = command.Wait()
146+
return
147+
}
148+
149+
func copyAndCapture(w io.Writer, r io.Reader) ([]byte, error) {
150+
var out []byte
151+
buf := make([]byte, 1024, 1024)
152+
for {
153+
n, err := r.Read(buf[:])
154+
if n > 0 {
155+
d := buf[:n]
156+
out = append(out, d...)
157+
_, err := w.Write(d)
158+
if err != nil {
159+
return out, err
160+
}
161+
}
162+
if err != nil {
163+
// Read returns io.EOF at the end of file, which is not an error for us
164+
if err == io.EOF {
165+
err = nil
166+
}
167+
return out, err
168+
}
169+
}
170+
}

0 commit comments

Comments
 (0)