Skip to content

Commit d2d6c89

Browse files
authored
Add http download command (#2)
* Return a custom error for http download * Finish the basic function of download file
1 parent 19888a6 commit d2d6c89

File tree

9 files changed

+238
-30
lines changed

9 files changed

+238
-30
lines changed

.github/settings.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
repository:
22
name: http-downloader
3-
description: This is a template project
3+
description: HTTP download tool
44
homepage: https://github.com/linuxsuren
55
private: false
66
has_issues: true

.goreleaser.yml

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ project_name: http-downloader
33
builds:
44
- env:
55
- CGO_ENABLED=0
6-
binary: http-downloader
6+
binary: hd
77
goarch:
88
- amd64
99
- arm64
@@ -15,9 +15,9 @@ builds:
1515
post:
1616
- upx "{{ .Path }}"
1717
ldflags:
18-
- -X github.com/linuxsuren/cgit/app.version={{.Version}}
19-
- -X github.com/linuxsuren/cgit/app.commit={{.ShortCommit}}
20-
- -X github.com/linuxsuren/cgit/app.date={{.Date}}
18+
- -X github.com/linuxsuren/cobra-extension/version.version={{.Version}}
19+
- -X github.com/linuxsuren/cobra-extension/version.commit={{.ShortCommit}}
20+
- -X github.com/linuxsuren/cobra-extension/version.date={{.Date}}
2121
- -w
2222
dist: release
2323
archives:
@@ -46,20 +46,18 @@ changelog:
4646
- '^test:'
4747
brews:
4848
-
49-
name: http-downloader
49+
name: hd
5050
tap:
5151
owner: linuxsuren
5252
name: homebrew-linuxsuren
5353
folder: Formula
5454
homepage: "https://github.com/linuxsuren/http-downloader"
55-
description: cgit is a tiny tool for Chinese developers.
55+
description: HTTP download tool
5656
dependencies:
57-
- name: vim
58-
type: optional
5957
- name: bash-completion
6058
type: optional
6159
test: |
62-
version_output = shell_output("#{bin}/http-downloader version")
60+
version_output = shell_output("#{bin}/hd version")
6361
assert_match version.to_s, version_output
6462
install: |
6563
bin.install name
@@ -68,7 +66,7 @@ brews:
6866
nfpms:
6967
- file_name_template: "{{ .Binary }}-{{.Os}}-{{.Arch}}"
7068
homepage: https://github.com/linuxsuren/http-downloader
71-
description: cgit is a tiny tool for Chinese developers.
69+
description: HTTP download tool
7270
maintainer: rick <rick@jenkins-zh.cn>
7371
license: MIT
7472
vendor: Jenkins

Makefile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
build: fmt
2-
CGO_ENABLE=0 go build -ldflags "-w -s" -o bin/github-go
2+
CGO_ENABLE=0 go build -ldflags "-w -s" -o bin/hd
33

44
run:
55
go run main.go
@@ -8,4 +8,4 @@ fmt:
88
go fmt ./...
99

1010
copy: build
11-
sudo cp bin/github-go /usr/local/bin/github-go
11+
sudo cp bin/hd /usr/local/bin/

cmd/root.go

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
"github.com/linuxsuren/http-downloader/pkg"
6+
extver "github.com/linuxsuren/cobra-extension/version"
7+
"github.com/spf13/cobra"
8+
"io/ioutil"
9+
"net/http"
10+
"os"
11+
"strconv"
12+
"strings"
13+
"sync"
14+
)
15+
16+
// NewRoot returns the root command
17+
func NewRoot() (cmd *cobra.Command) {
18+
opt := &downloadOption{}
19+
cmd = &cobra.Command{
20+
Use: "hd",
21+
Short: "HTTP download tool",
22+
PreRunE: opt.preRunE,
23+
RunE: opt.runE,
24+
}
25+
26+
// set flags
27+
flags := cmd.Flags()
28+
flags.StringVarP(&opt.Output, "output", "o", "", "Write output to <file> instead of stdout.")
29+
flags.BoolVarP(&opt.ShowProgress, "show-progress", "", true, "If show the progress of download")
30+
flags.Int64VarP(&opt.ContinueAt, "continue-at", "", -1, "ContinueAt")
31+
flags.IntVarP(&opt.Thread, "thread", "", 0, "")
32+
33+
cmd.AddCommand(
34+
extver.NewVersionCmd("linuxsuren", "http-downloader", "hd", nil))
35+
return
36+
}
37+
38+
type downloadOption struct {
39+
URL string
40+
Output string
41+
ShowProgress bool
42+
43+
ContinueAt int64
44+
45+
Thread int
46+
}
47+
48+
func (o *downloadOption) preRunE(cmd *cobra.Command, args []string) (err error) {
49+
if len(args) <= 0 {
50+
return fmt.Errorf("no URL provided")
51+
}
52+
53+
url := args[0]
54+
if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") {
55+
err = fmt.Errorf("only http:// or https:// supported")
56+
}
57+
o.URL = url
58+
59+
if o.Output == "" {
60+
err = fmt.Errorf("output cannot be empty")
61+
}
62+
return
63+
}
64+
65+
func (o *downloadOption) runE(cmd *cobra.Command, args []string) (err error) {
66+
if o.Thread <= 1 {
67+
err = o.download(o.Output, o.ContinueAt, 0)
68+
} else {
69+
// get the total size of the target file
70+
var total int64
71+
var rangeSupport bool
72+
if total, rangeSupport, err = o.detectSize(o.Output); err != nil {
73+
return
74+
}
75+
76+
if rangeSupport {
77+
unit := total / int64(o.Thread)
78+
offset := total - unit*int64(o.Thread)
79+
var wg sync.WaitGroup
80+
81+
cmd.Printf("start to download with %d threads, size: %d, unit: %d\n", o.Thread, total, unit)
82+
for i := 0; i < o.Thread; i++ {
83+
wg.Add(1)
84+
go func(index int, wg *sync.WaitGroup) {
85+
defer wg.Done()
86+
87+
end := unit*int64(index+1) - 1
88+
if index == o.Thread-1 {
89+
// this is the last part
90+
end += offset
91+
}
92+
start := unit * int64(index)
93+
94+
if downloadErr := o.download(fmt.Sprintf("%s-%d", o.Output, index), start, end); downloadErr != nil {
95+
cmd.PrintErrln(downloadErr)
96+
}
97+
}(i, &wg)
98+
}
99+
100+
wg.Wait()
101+
102+
// concat all these partial files
103+
var f *os.File
104+
if f, err = os.OpenFile(o.Output, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644); err == nil {
105+
defer func() {
106+
_ = f.Close()
107+
}()
108+
109+
for i := 0; i < o.Thread; i++ {
110+
partFile := fmt.Sprintf("%s-%d", o.Output, i)
111+
if data, ferr := ioutil.ReadFile(partFile); ferr == nil {
112+
if _, err = f.Write(data); err != nil {
113+
err = fmt.Errorf("failed to write file: '%s'", partFile)
114+
break
115+
} else {
116+
_ = os.RemoveAll(partFile)
117+
}
118+
} else {
119+
err = fmt.Errorf("failed to read file: '%s'", partFile)
120+
break
121+
}
122+
}
123+
}
124+
} else {
125+
cmd.Println("cannot download it using multiple threads, failed to one")
126+
err = o.download(o.Output, o.ContinueAt, 0)
127+
}
128+
}
129+
return
130+
}
131+
132+
func (o *downloadOption) detectSize(output string) (total int64, rangeSupport bool, err error) {
133+
downloader := pkg.HTTPDownloader{
134+
TargetFilePath: output,
135+
URL: o.URL,
136+
ShowProgress: o.ShowProgress,
137+
}
138+
139+
var detectOffset int64
140+
var lenErr error
141+
142+
detectOffset = 2
143+
downloader.Header = make(map[string]string, 1)
144+
downloader.Header["Range"] = fmt.Sprintf("bytes=%d-", detectOffset)
145+
146+
downloader.PreStart = func(resp *http.Response) bool {
147+
rangeSupport = resp.StatusCode == http.StatusPartialContent
148+
contentLen := resp.Header.Get("Content-Length")
149+
if total, lenErr = strconv.ParseInt(contentLen, 10, 0); lenErr == nil {
150+
total += detectOffset
151+
}
152+
// always return false because we just want to get the header from response
153+
return false
154+
}
155+
156+
if err = downloader.DownloadFile(); err != nil || lenErr != nil {
157+
err = fmt.Errorf("cannot download from %s, response error: %v, content length error: %v", o.URL, err, lenErr)
158+
}
159+
return
160+
}
161+
162+
func (o *downloadOption) download(output string, continueAt, end int64) (err error) {
163+
downloader := pkg.HTTPDownloader{
164+
TargetFilePath: output,
165+
URL: o.URL,
166+
ShowProgress: o.ShowProgress,
167+
}
168+
169+
if continueAt >= 0 {
170+
downloader.Header = make(map[string]string, 1)
171+
172+
//fmt.Println("range", continueAt, end)
173+
if end > continueAt {
174+
downloader.Header["Range"] = fmt.Sprintf("bytes=%d-%d", continueAt, end)
175+
} else {
176+
downloader.Header["Range"] = fmt.Sprintf("bytes=%d-", continueAt)
177+
}
178+
}
179+
180+
if err = downloader.DownloadFile(); err != nil {
181+
err = fmt.Errorf("cannot download from %s, error: %v", o.URL, err)
182+
}
183+
return
184+
}

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ require (
66
github.com/golang/mock v1.4.4
77
github.com/gosuri/uiprogress v0.0.1
88
github.com/jenkins-zh/jenkins-cli v0.0.32
9+
github.com/linuxsuren/cobra-extension v0.0.6
910
github.com/onsi/ginkgo v1.14.2
1011
github.com/onsi/gomega v1.10.3
1112
github.com/spf13/cobra v1.1.1

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,8 @@ github.com/kr/pty v1.1.4/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
155155
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
156156
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
157157
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
158+
github.com/linuxsuren/cobra-extension v0.0.6/go.mod h1:qcEJv7BbL0UpK6MbrTESP/nKf1+z1wQdMAnE1NBl3QQ=
159+
github.com/linuxsuren/http-downloader v0.0.2-0.20201207132639-19888a6beaec/go.mod h1:zRZY9FCDBuYNDxbI2Ny5suasZsMk7J6q9ecQ3V3PIqI=
158160
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
159161
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
160162
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=

main.go

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,12 @@
11
package main
22

33
import (
4-
"github.com/spf13/cobra"
4+
"github.com/linuxsuren/http-downloader/cmd"
5+
"os"
56
)
67

7-
func main() {
8-
cmd := &cobra.Command{
9-
Use: "github-go",
10-
Run: func(cmd *cobra.Command, args []string) {
11-
cmd.Println("hello")
12-
},
13-
}
14-
if err := cmd.Execute(); err != nil {
15-
panic(err)
8+
func main() {
9+
if err := cmd.NewRoot().Execute(); err != nil {
10+
os.Exit(1)
1611
}
1712
}

pkg/error.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package pkg
2+
3+
import "fmt"
4+
5+
// DownloadError represents the error of HTTP download
6+
type DownloadError struct {
7+
StatusCode int
8+
Message string
9+
}
10+
11+
// Error print the error message
12+
func (e *DownloadError) Error() string {
13+
return fmt.Sprintf("%s: status code: %d", e.Message, e.StatusCode)
14+
}

pkg/http.go

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import (
55
"encoding/base64"
66
"fmt"
77
"io"
8-
"io/ioutil"
98
"net/http"
109
"net/url"
1110
"os"
@@ -35,6 +34,11 @@ type HTTPDownloader struct {
3534
Proxy string
3635
ProxyAuth string
3736

37+
Header map[string]string
38+
39+
// PreStart returns false will don't continue
40+
PreStart func(*http.Response) bool
41+
3842
Debug bool
3943
RoundTripper http.RoundTripper
4044
}
@@ -81,6 +85,9 @@ func (h *HTTPDownloader) fetchProxyFromEnv(scheme string) {
8185
}
8286
}
8387

88+
//Range: bytes=10-
89+
//HTTP/1.1 206 Partial Content
90+
8491
// DownloadFile download a file with the progress
8592
func (h *HTTPDownloader) DownloadFile() error {
8693
filepath, downloadURL, showProgress := h.TargetFilePath, h.URL, h.ShowProgress
@@ -90,6 +97,10 @@ func (h *HTTPDownloader) DownloadFile() error {
9097
return err
9198
}
9299

100+
for k, v := range h.Header {
101+
req.Header.Set(k, v)
102+
}
103+
93104
if h.UserName != "" && h.Password != "" {
94105
req.SetBasicAuth(h.UserName, h.Password)
95106
}
@@ -118,13 +129,16 @@ func (h *HTTPDownloader) DownloadFile() error {
118129
return err
119130
}
120131

121-
if resp.StatusCode != 200 {
122-
if h.Debug {
123-
if data, err := ioutil.ReadAll(resp.Body); err == nil {
124-
ioutil.WriteFile("debug-download.html", data, 0664)
125-
}
132+
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusPartialContent {
133+
return &DownloadError{
134+
Message: fmt.Sprintf("failed to download from '%s'", downloadURL),
135+
StatusCode: resp.StatusCode,
126136
}
127-
return fmt.Errorf("invalidate status code: %d", resp.StatusCode)
137+
}
138+
139+
// pre-hook before get started to download file
140+
if h.PreStart != nil && !h.PreStart(resp) {
141+
return nil
128142
}
129143

130144
writer := &ProgressIndicator{

0 commit comments

Comments
 (0)