多线程分片下载
This commit is contained in:
parent
0f510c10db
commit
74cccba197
28
app/wails/pkg/sdk/downloader/download_test.go
Normal file
28
app/wails/pkg/sdk/downloader/download_test.go
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
package downloader
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDownloader(t *testing.T) {
|
||||||
|
url := "https://mirrors.tuna.tsinghua.edu.cn/Adoptium/8/jdk/x64/windows/OpenJDK8U-jdk_x64_windows_hotspot_8u382b05.zip"
|
||||||
|
dir := os.TempDir()
|
||||||
|
fmt.Println(dir)
|
||||||
|
fileName := "OpenJDK8U-jdk_x64_windows_hotspot_8u382b05.zip"
|
||||||
|
|
||||||
|
a, _ := getRedirectInfo(url, UserAgent)
|
||||||
|
location := a.Header.Get("Location")
|
||||||
|
if len(location) == 0 {
|
||||||
|
location = url
|
||||||
|
}
|
||||||
|
startTime := time.Now()
|
||||||
|
d := NewDownloader(location, dir, fileName, 5)
|
||||||
|
if err := d.Run(); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
fmt.Printf("\n 文件下载完成耗时: %f second\n", time.Now().Sub(startTime).Seconds())
|
||||||
|
}
|
286
app/wails/pkg/sdk/downloader/downloader.go
Normal file
286
app/wails/pkg/sdk/downloader/downloader.go
Normal file
@ -0,0 +1,286 @@
|
|||||||
|
package downloader
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"mime"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
const UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36"
|
||||||
|
|
||||||
|
// PartFile 文件切片
|
||||||
|
type PartFile struct {
|
||||||
|
Index int64
|
||||||
|
From int64
|
||||||
|
To int64
|
||||||
|
Data []byte
|
||||||
|
Done bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type Downloader struct {
|
||||||
|
FileSize int64
|
||||||
|
Url string
|
||||||
|
FileName string
|
||||||
|
Path string
|
||||||
|
PartNum int64
|
||||||
|
donePart []PartFile
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDownloader 创建下载器
|
||||||
|
func NewDownloader(url, outputDir, outputFileName string, partNum int64) *Downloader {
|
||||||
|
if outputDir == "" {
|
||||||
|
wd, err := os.Getwd() //获取当前工作目录
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
outputDir = wd
|
||||||
|
}
|
||||||
|
return &Downloader{
|
||||||
|
FileSize: 0,
|
||||||
|
Url: url,
|
||||||
|
FileName: outputFileName,
|
||||||
|
Path: outputDir,
|
||||||
|
PartNum: partNum,
|
||||||
|
donePart: make([]PartFile, partNum),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Downloader) getNewRequest(method string) (*http.Request, error) {
|
||||||
|
req, err := http.NewRequest(
|
||||||
|
method,
|
||||||
|
d.Url,
|
||||||
|
nil)
|
||||||
|
|
||||||
|
return req, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Downloader) head() (int64, error) {
|
||||||
|
req, err := d.getNewRequest(http.MethodHead)
|
||||||
|
req = setNewHeader(req)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if resp.StatusCode > 299 {
|
||||||
|
return 0, errors.New(fmt.Sprintf("Can't process, response is %v", resp.StatusCode))
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.Header.Get("Accept-Ranges") != "bytes" {
|
||||||
|
return 0, errors.New("服务器不支持文件断点续传")
|
||||||
|
}
|
||||||
|
|
||||||
|
d.FileName = GetFileInfoFromResponse(resp)
|
||||||
|
length, err := strconv.Atoi(resp.Header.Get("Content-Length"))
|
||||||
|
|
||||||
|
return int64(length), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下载切片
|
||||||
|
func (d *Downloader) downloadPart(c PartFile, f *os.File) error {
|
||||||
|
r, err := d.getNewRequest("GET")
|
||||||
|
r = setNewHeader(r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Printf("开始[%d]下载from:%d to:%d\n", c.Index, c.From, c.To)
|
||||||
|
r.Header.Set("Range", fmt.Sprintf("bytes=%v-%v", c.From, c.To))
|
||||||
|
resp, err := http.DefaultClient.Do(r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if resp.StatusCode > 299 {
|
||||||
|
return errors.New(fmt.Sprintf("服务器错误状态码: %v", resp.StatusCode))
|
||||||
|
}
|
||||||
|
defer func(Body io.ReadCloser) {
|
||||||
|
_ = Body.Close()
|
||||||
|
}(resp.Body)
|
||||||
|
|
||||||
|
bs, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(bs) != int(c.To-c.From+1) {
|
||||||
|
}
|
||||||
|
c.Data = bs
|
||||||
|
c.Done = true
|
||||||
|
|
||||||
|
d.donePart[c.Index] = c
|
||||||
|
|
||||||
|
_, err = f.WriteAt(bs, c.From)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
c.Done = true
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("结束[%d]下载", c.Index)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Downloader) checkIntegrity(t *os.File) error {
|
||||||
|
log.Println("开始合并文件")
|
||||||
|
|
||||||
|
totalSize := 0
|
||||||
|
|
||||||
|
for _, s := range d.donePart {
|
||||||
|
//hash.Write(s.Data)
|
||||||
|
totalSize += len(s.Data)
|
||||||
|
}
|
||||||
|
|
||||||
|
if int64(totalSize) != d.FileSize {
|
||||||
|
return errors.New("文件不完整")
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = t.Close()
|
||||||
|
return os.Rename(filepath.Join(d.Path, d.FileName+".tmp"), filepath.Join(d.Path, d.FileName))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run 开始下载任务
|
||||||
|
func (d *Downloader) Run() error {
|
||||||
|
fileTotalSize, err := d.head()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
d.FileSize = fileTotalSize
|
||||||
|
|
||||||
|
jobs := make([]PartFile, d.PartNum)
|
||||||
|
eachSize := fileTotalSize / d.PartNum
|
||||||
|
|
||||||
|
path := filepath.Join(d.Path, d.FileName+".tmp")
|
||||||
|
|
||||||
|
tmpFile := new(os.File)
|
||||||
|
|
||||||
|
fByte := make([]byte, d.FileSize)
|
||||||
|
|
||||||
|
if exists(path) {
|
||||||
|
tmpFile, err = os.OpenFile(path, os.O_RDWR, 0)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fByte, err = ioutil.ReadAll(tmpFile)
|
||||||
|
} else {
|
||||||
|
tmpFile, err = os.Create(path)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func(tmpFile *os.File) {
|
||||||
|
_ = tmpFile.Close()
|
||||||
|
}(tmpFile)
|
||||||
|
|
||||||
|
for i := range jobs {
|
||||||
|
i64 := int64(i)
|
||||||
|
jobs[i64].Index = i64
|
||||||
|
if i == 0 {
|
||||||
|
jobs[i64].From = 0
|
||||||
|
} else {
|
||||||
|
jobs[i64].From = jobs[i64-1].To + 1
|
||||||
|
}
|
||||||
|
if i64 < d.PartNum-1 {
|
||||||
|
jobs[i64].To = jobs[i64].From + eachSize
|
||||||
|
} else {
|
||||||
|
//the last filePart
|
||||||
|
jobs[i64].To = fileTotalSize - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, j := range jobs {
|
||||||
|
tmpJob := j
|
||||||
|
emptyTmp := make([]byte, tmpJob.To-j.From)
|
||||||
|
if bytes.Compare(emptyTmp, fByte[tmpJob.From:j.To]) != 0 {
|
||||||
|
tmpJob.Data = fByte[j.From : j.To+1]
|
||||||
|
tmpJob.Done = true
|
||||||
|
d.donePart[tmpJob.Index] = tmpJob
|
||||||
|
} else {
|
||||||
|
tmpJob.Done = false
|
||||||
|
}
|
||||||
|
jobs[i] = tmpJob
|
||||||
|
}
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
for _, j := range jobs {
|
||||||
|
if !j.Done {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(job PartFile) {
|
||||||
|
defer wg.Done()
|
||||||
|
err := d.downloadPart(job, tmpFile)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("下载文件失败:", err, job)
|
||||||
|
}
|
||||||
|
}(j)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
return d.checkIntegrity(tmpFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRedirectInfo(u, userAgent string) (*http.Response, error) {
|
||||||
|
log.Println("获取重定向信息")
|
||||||
|
var a *url.URL
|
||||||
|
a, _ = url.Parse(u)
|
||||||
|
header := http.Header{}
|
||||||
|
|
||||||
|
//header.Add("Cookie", rawCookies)
|
||||||
|
header.Add("User-Agent", userAgent)
|
||||||
|
request := http.Request{
|
||||||
|
Header: header,
|
||||||
|
Method: "GET",
|
||||||
|
URL: a,
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &http.Client{
|
||||||
|
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||||
|
return http.ErrUseLastResponse
|
||||||
|
},
|
||||||
|
}
|
||||||
|
response, err := client.Do(&request)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return response, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func setNewHeader(r *http.Request) *http.Request {
|
||||||
|
r.Header.Add("User-Agent", UserAgent)
|
||||||
|
r.Header.Add("Upgrade-Insecure-Requests", "1")
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func exists(path string) bool {
|
||||||
|
_, err := os.Stat(path) //os.Stat获取文件信息
|
||||||
|
if err != nil {
|
||||||
|
if os.IsExist(err) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetFileInfoFromResponse(resp *http.Response) string {
|
||||||
|
contentDisposition := resp.Header.Get("Content-Disposition")
|
||||||
|
if contentDisposition != "" {
|
||||||
|
_, params, err := mime.ParseMediaType(contentDisposition)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return params["filename"]
|
||||||
|
}
|
||||||
|
filename := filepath.Base(resp.Request.URL.Path)
|
||||||
|
return filename
|
||||||
|
}
|
13
app/wails/pkg/sdk/downloader/support.go
Normal file
13
app/wails/pkg/sdk/downloader/support.go
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
package downloader
|
||||||
|
|
||||||
|
import "skapp/pkg/core"
|
||||||
|
|
||||||
|
type Support struct {
|
||||||
|
app *core.App
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(app *core.App) *Support {
|
||||||
|
return &Support{
|
||||||
|
app,
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user