Passed
Push — master ( c2f968...f94c9e )
by Abouzar
01:57
created
Severity
1
package main
2
3
import (
4
	"crypto/tls"
5
	"fmt"
6
	"io"
7
	"net"
8
	"net/http"
9
	stdurl "net/url"
10
	"os"
11
	"path/filepath"
12
	"strconv"
13
	"strings"
14
	"sync"
15
16
	"github.com/alecthomas/units"
17
	"github.com/fatih/color"
18
	"github.com/fujiwara/shapeio"
19
	"golang.org/x/net/proxy"
20
	pb "gopkg.in/cheggaaa/pb.v1"
21
)
22
23
var (
24
	tr = &http.Transport{
25
		TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
26
	}
27
	client = &http.Client{Transport: tr}
28
)
29
30
var (
31
	acceptRangeHeader   = "Accept-Ranges"
32
	contentLengthHeader = "Content-Length"
33
)
34
35
// HTTPDownloader holds the required configurations
36
type HTTPDownloader struct {
37
	proxy     string
38
	rate      int64
39
	url       string
40
	file      string
41
	par       int64
42
	len       int64
43
	ips       []string
44
	skipTLS   bool
45
	parts     []Part
46
	resumable bool
47
}
48
49
// NewHttpDownloader returns a ProxyAwareHttpClient with given configurations.
50
func NewHttpDownloader(url string, par int, skipTLS bool, proxyServer string, bwLimit string) *HTTPDownloader {
0 ignored issues
show
func NewHttpDownloader should be NewHTTPDownloader
Loading history...
51
	var resumable = true
52
53
	client := ProxyAwareHTTPClient(proxyServer)
54
55
	parsed, err := stdurl.Parse(url)
56
	FatalCheck(err)
57
58
	ips, err := net.LookupIP(parsed.Host)
59
	FatalCheck(err)
60
61
	ipstr := FilterIPV4(ips)
62
	Printf("Resolve ip: %s\n", strings.Join(ipstr, " | "))
63
64
	req, err := http.NewRequest("GET", url, nil)
65
	FatalCheck(err)
66
67
	resp, err := client.Do(req)
68
	FatalCheck(err)
69
70
	if resp.Header.Get(acceptRangeHeader) == "" {
71
		Printf("Target url is not supported range download, fallback to parallel 1\n")
72
		//fallback to par = 1
73
		par = 1
74
	}
75
76
	//get download range
77
	clen := resp.Header.Get(contentLengthHeader)
78
	if clen == "" {
79
		Printf("Target url not contain Content-Length header, fallback to parallel 1\n")
80
		clen = "1" //set 1 because of progress bar not accept 0 length
81
		par = 1
82
		resumable = false
83
	}
84
85
	Printf("Start download with %d connections \n", par)
86
87
	len, err := strconv.ParseInt(clen, 10, 64)
88
	FatalCheck(err)
89
90
	sizeInMb := float64(len) / (1024 * 1024)
91
92
	if clen == "1" {
93
		Printf("Download size: not specified\n")
94
	} else if sizeInMb < 1024 {
95
		Printf("Download target size: %.1f MB\n", sizeInMb)
96
	} else {
97
		Printf("Download target size: %.1f GB\n", sizeInMb/1024)
98
	}
99
100
	file := filepath.Base(url)
101
	ret := new(HTTPDownloader)
102
	ret.rate = 0
103
	bandwidthLimit, err := units.ParseStrictBytes(bwLimit)
104
	if err == nil {
105
		ret.rate = bandwidthLimit
106
		Printf("Download with bandwidth limit set to %s[%d]\n", bwLimit, ret.rate)
107
	}
108
	ret.url = url
109
	ret.file = file
110
	ret.par = int64(par)
111
	ret.len = len
112
	ret.ips = ipstr
113
	ret.skipTLS = skipTLS
114
	ret.parts = partCalculate(int64(par), len, url)
115
	ret.resumable = resumable
116
	ret.proxy = proxyServer
117
118
	return ret
119
}
120
121
func partCalculate(par int64, len int64, url string) []Part {
122
	// Pre-allocate, perf tunning
123
	ret := make([]Part, par)
124
	for j := int64(0); j < par; j++ {
125
		from := (len / par) * j
126
		var to int64
127
		if j < par-1 {
128
			to = (len/par)*(j+1) - 1
129
		} else {
130
			to = len
131
		}
132
133
		file := filepath.Base(url)
134
		folder := FolderOf(url)
135
		if err := MkdirIfNotExist(folder); err != nil {
136
			Errorf("%v", err)
137
			os.Exit(1)
138
		}
139
140
		// Padding 0 before path name as filename will be sorted as string
141
		fname := fmt.Sprintf("%s.part%06d", file, j)
142
		path := filepath.Join(folder, fname) // ~/.hget/download-file-name/part-name
143
		ret[j] = Part{Index: j, Url: url, Path: path, RangeFrom: from, RangeTo: to}
144
	}
145
146
	return ret
147
}
148
149
// ProxyAwareHTTPClient will use http or socks5 proxy if given one.
150
func ProxyAwareHTTPClient(proxyServer string) *http.Client {
151
	// setup a http client
152
	httpTransport := &http.Transport{}
153
	httpClient := &http.Client{Transport: httpTransport}
154
	var dialer proxy.Dialer
155
	dialer = proxy.Direct
156
157
	if len(proxyServer) > 0 {
158
		if strings.HasPrefix(proxyServer, "http") {
159
			proxyURL, err := stdurl.Parse(proxyServer)
160
			if err != nil {
161
				fmt.Fprintln(os.Stderr, "invalid proxy: ", err)
162
			}
163
			// create a http dialer
164
			dialer, err = proxy.FromURL(proxyURL, proxy.Direct)
165
			if err == nil {
166
				httpTransport.Dial = dialer.Dial
167
			}
168
		} else {
169
			// create a socks5 dialer
170
			dialer, err := proxy.SOCKS5("tcp", proxyServer, nil, proxy.Direct)
171
			if err == nil {
172
				httpTransport.Dial = dialer.Dial
173
			}
174
		}
175
176
	}
177
	return httpClient
178
}
179
180
// Do is where the magic happens.
181
func (d *HTTPDownloader) Do(doneChan chan bool, fileChan chan string, errorChan chan error, interruptChan chan bool, stateSaveChan chan Part) {
182
	var ws sync.WaitGroup
183
	var bars []*pb.ProgressBar
184
	var barpool *pb.Pool
185
	var err error
186
187
	for _, p := range d.parts {
188
189
		if p.RangeTo <= p.RangeFrom {
190
			fileChan <- p.Path
191
			stateSaveChan <- Part{
192
				Index:     p.Index,
193
				Url:       d.url,
194
				Path:      p.Path,
195
				RangeFrom: p.RangeFrom,
196
				RangeTo:   p.RangeTo,
197
			}
198
199
			continue
200
		}
201
202
		var bar *pb.ProgressBar
203
204
		if DisplayProgressBar() {
205
			bar = pb.New64(p.RangeTo - p.RangeFrom).SetUnits(pb.U_BYTES).Prefix(color.YellowString(fmt.Sprintf("%s-%d", d.file, p.Index)))
206
			bars = append(bars, bar)
207
		}
208
209
		ws.Add(1)
210
		go func(d *HTTPDownloader, bar *pb.ProgressBar, part Part) {
211
			client := ProxyAwareHTTPClient(d.proxy)
212
			defer ws.Done()
213
214
			var ranges string
215
			if part.RangeTo != d.len {
216
				ranges = fmt.Sprintf("bytes=%d-%d", part.RangeFrom, part.RangeTo)
217
			} else {
218
				ranges = fmt.Sprintf("bytes=%d-", part.RangeFrom) //get all
219
			}
220
221
			//send request
222
			req, err := http.NewRequest("GET", d.url, nil)
223
			if err != nil {
224
				errorChan <- err
225
				return
226
			}
227
228
			if d.par > 1 { //support range download just in case parallel factor is over 1
229
				req.Header.Add("Range", ranges)
230
				if err != nil {
231
					errorChan <- err
232
					return
233
				}
234
			}
235
236
			//write to file
237
			resp, err := client.Do(req)
238
			if err != nil {
239
				errorChan <- err
240
				return
241
			}
242
			defer resp.Body.Close()
243
			f, err := os.OpenFile(part.Path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600)
244
245
			defer f.Close()
246
			if err != nil {
247
				Errorf("%v\n", err)
248
				errorChan <- err
249
				return
250
			}
251
252
			var writer io.Writer
253
			if DisplayProgressBar() {
254
				writer = io.MultiWriter(f, bar)
255
			} else {
256
				writer = io.MultiWriter(f)
257
			}
258
259
			current := int64(0)
260
			finishDownloadChan := make(chan bool)
261
262
			go func() {
263
				var written int64
264
				if d.rate != 0 {
265
					reader := shapeio.NewReader(resp.Body)
266
					reader.SetRateLimit(float64(d.rate))
267
					written, _ = io.Copy(writer, reader)
268
				} else {
269
					written, _ = io.Copy(writer, resp.Body)
270
				}
271
				current += written
272
				fileChan <- part.Path
273
				finishDownloadChan <- true
274
			}()
275
276
			select {
277
			case <-interruptChan:
278
				// interrupt download by forcefully close the input stream
279
				resp.Body.Close()
280
				<-finishDownloadChan
281
			case <-finishDownloadChan:
282
			}
283
284
			stateSaveChan <- Part{
285
				Index:     part.Index,
286
				Url:       d.url,
287
				Path:      part.Path,
288
				RangeFrom: current + part.RangeFrom,
289
				RangeTo:   part.RangeTo,
290
			}
291
292
			if DisplayProgressBar() {
293
				bar.Update()
294
				bar.Finish()
295
			}
296
		}(d, bar, p)
297
	}
298
299
	barpool, err = pb.StartPool(bars...)
300
	FatalCheck(err)
301
302
	ws.Wait()
303
	doneChan <- true
304
	barpool.Stop()
305
}
306