18 changed files with 155 additions and 865 deletions
@ -1,26 +0,0 @@ |
|||||
package mdown |
|
||||
|
|
||||
import ( |
|
||||
"time" |
|
||||
|
|
||||
"git.realxlfd.cc/RealXLFD/golib/cli/logger" |
|
||||
) |
|
||||
|
|
||||
var ( |
|
||||
log = logger.New() |
|
||||
) |
|
||||
|
|
||||
// WAITING -> HEAD -> READY
|
|
||||
const ( |
|
||||
FAILED = iota - 1 |
|
||||
WAITING |
|
||||
HEAD |
|
||||
READY |
|
||||
RUNNING |
|
||||
SUCCESS |
|
||||
) |
|
||||
|
|
||||
var ( |
|
||||
HeadRetryCount = 2 |
|
||||
HeadRetryGap = 300 * time.Millisecond |
|
||||
) |
|
@ -1,45 +0,0 @@ |
|||||
package mdown |
|
||||
|
|
||||
import ( |
|
||||
"errors" |
|
||||
"io" |
|
||||
"net/http" |
|
||||
"strconv" |
|
||||
|
|
||||
"git.realxlfd.cc/RealXLFD/golib/utils/str" |
|
||||
) |
|
||||
|
|
||||
type ErrRequestFailed struct { |
|
||||
StatusCode int |
|
||||
Msg string |
|
||||
} |
|
||||
|
|
||||
func (e ErrRequestFailed) Error() string { |
|
||||
return str.Join( |
|
||||
"status: ", strconv.Itoa(e.StatusCode), " msg: ", e.Msg, |
|
||||
) |
|
||||
} |
|
||||
|
|
||||
func requestFailed(resp *http.Response) error { |
|
||||
body, _ := io.ReadAll(resp.Body) |
|
||||
return ErrRequestFailed{ |
|
||||
StatusCode: resp.StatusCode, |
|
||||
Msg: string(body), |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
type ErrNetworkErr error |
|
||||
|
|
||||
type ErrFileOpenFailed error |
|
||||
|
|
||||
func errorDetail(err error) string { |
|
||||
var errNetworkErr ErrNetworkErr |
|
||||
var errFileOpenFailed ErrFileOpenFailed |
|
||||
switch { |
|
||||
case errors.As(err, &errNetworkErr): |
|
||||
return str.Join("网络错误:", err.Error()) |
|
||||
case errors.As(err, &errFileOpenFailed): |
|
||||
return str.Join("写入失败:", err.Error()) |
|
||||
} |
|
||||
return err.Error() |
|
||||
} |
|
@ -1,32 +0,0 @@ |
|||||
package mdown |
|
||||
|
|
||||
import ( |
|
||||
"net/http" |
|
||||
"strconv" |
|
||||
|
|
||||
"git.realxlfd.cc/RealXLFD/golib/utils/str" |
|
||||
) |
|
||||
|
|
||||
func rangeGenerator(total, parts int) (ranges []http.Header) { |
|
||||
each := total / parts |
|
||||
for i := range parts - 1 { |
|
||||
ranges = append( |
|
||||
ranges, rangeHeader(i*each, (i+1)*each-1), |
|
||||
) |
|
||||
} |
|
||||
ranges = append(ranges, rangeHeader((parts-1)*each, total)) |
|
||||
return ranges |
|
||||
} |
|
||||
|
|
||||
func rangeHeader(start, end int) http.Header { |
|
||||
return http.Header{ |
|
||||
"Range": []string{ |
|
||||
str.Join( |
|
||||
"bytes=", |
|
||||
strconv.Itoa(start), |
|
||||
"-", |
|
||||
strconv.Itoa(end), |
|
||||
), |
|
||||
}, |
|
||||
} |
|
||||
} |
|
@ -1,218 +0,0 @@ |
|||||
package mdown |
|
||||
|
|
||||
import ( |
|
||||
"errors" |
|
||||
"io" |
|
||||
"mime" |
|
||||
"net/http" |
|
||||
"net/url" |
|
||||
"os" |
|
||||
"path/filepath" |
|
||||
"strconv" |
|
||||
"strings" |
|
||||
|
|
||||
"git.realxlfd.cc/RealXLFD/golib/proc/state" |
|
||||
"git.realxlfd.cc/RealXLFD/golib/utils" |
|
||||
"git.realxlfd.cc/RealXLFD/golib/utils/ioplus" |
|
||||
"git.realxlfd.cc/RealXLFD/golib/utils/str" |
|
||||
) |
|
||||
|
|
||||
type Target struct { |
|
||||
URL *url.URL |
|
||||
cookies http.Header |
|
||||
Path string |
|
||||
proxy *url.URL |
|
||||
state *state.Manager |
|
||||
client *http.Client |
|
||||
details struct { |
|
||||
Filename string |
|
||||
ContentSize int |
|
||||
AcceptRanges bool |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
func NewTarget(URL, Path string) *Target { |
|
||||
target, err := url.Parse(URL) |
|
||||
if err != nil { |
|
||||
panic("错误的URL") |
|
||||
} |
|
||||
stat, err := os.Stat(Path) |
|
||||
switch { |
|
||||
case err != nil: |
|
||||
err = os.MkdirAll(Path, os.ModePerm) |
|
||||
if err != nil { |
|
||||
panic("创建文件夹失败") |
|
||||
} |
|
||||
case !stat.IsDir(): |
|
||||
panic("路径应是文件夹而不是文件") |
|
||||
} |
|
||||
return &Target{ |
|
||||
URL: target, |
|
||||
Path: Path, |
|
||||
state: state.New(WAITING), |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
func (t *Target) JustDownload() error { |
|
||||
switch t.state.Get() { |
|
||||
case FAILED: |
|
||||
return errors.New("下载失败") |
|
||||
case SUCCESS: |
|
||||
return nil |
|
||||
default: |
|
||||
} |
|
||||
t.getClient() // TODO
|
|
||||
file := filepath.Join(t.Path, t.details.Filename) |
|
||||
err := t.request(nil, file, nil) |
|
||||
if err != nil { |
|
||||
|
|
||||
log.Error(err.Error()) |
|
||||
} |
|
||||
|
|
||||
} |
|
||||
|
|
||||
// 注意!内部将修改Range
|
|
||||
func (t *Target) request( |
|
||||
rangeHeader http.Header, file string, s *ioplus.SWrite, |
|
||||
) error { |
|
||||
req := &http.Request{ |
|
||||
URL: t.URL, |
|
||||
Header: t.cookies, |
|
||||
} |
|
||||
if rangeHeader == nil { |
|
||||
utils.JoinMap(rangeHeader, t.cookies) |
|
||||
req.Header = rangeHeader |
|
||||
} |
|
||||
resp, err := t.client.Do(req) |
|
||||
if err != nil { |
|
||||
return ErrNetworkErr(err) |
|
||||
} |
|
||||
defer func(Body io.ReadCloser) { |
|
||||
_ = Body.Close() |
|
||||
}(resp.Body) |
|
||||
if resp.StatusCode >= 300 || resp.StatusCode < 200 { |
|
||||
t.state.ToState(FAILED) |
|
||||
return requestFailed(resp) |
|
||||
} |
|
||||
// write to file
|
|
||||
entry, err := os.OpenFile( |
|
||||
file, os.O_CREATE|os.O_WRONLY|os.O_APPEND, os.ModePerm, |
|
||||
) // TODO: 处理断点续传
|
|
||||
if err != nil { |
|
||||
return ErrFileOpenFailed(err) |
|
||||
} |
|
||||
defer func(entry *os.File) { |
|
||||
_ = entry.Close() |
|
||||
}(entry) |
|
||||
writer := s.Bind(entry) // 将写入器绑定以便测量速度
|
|
||||
_, err = io.Copy(writer, resp.Body) |
|
||||
if err != nil { |
|
||||
return ErrFileOpenFailed(err) |
|
||||
} |
|
||||
return nil |
|
||||
} |
|
||||
|
|
||||
func (t *Target) getClient() { |
|
||||
t.client = &http.Client{} |
|
||||
if t.proxy != nil { |
|
||||
t.client.Transport = &http.Transport{ |
|
||||
Proxy: http.ProxyURL(t.proxy), |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
func (t *Target) Proxy(proxy string) *Target { |
|
||||
proxyURL, err := url.Parse(proxy) |
|
||||
if err != nil { |
|
||||
panic("URL格式不正确:") |
|
||||
} |
|
||||
t.proxy = proxyURL |
|
||||
return t |
|
||||
} |
|
||||
|
|
||||
func (t *Target) Cookies(cookies string) *Target { |
|
||||
t.cookies = http.Header{ |
|
||||
"Cookie": []string{cookies}, |
|
||||
} |
|
||||
return t |
|
||||
} |
|
||||
|
|
||||
func (t *Target) Get() *Target { |
|
||||
if t.state.Is(HEAD, READY) { |
|
||||
return t |
|
||||
} |
|
||||
t.state.ToState(HEAD) |
|
||||
go func() { |
|
||||
err := t.head() |
|
||||
if err != nil { |
|
||||
log.Error(err.Error()) |
|
||||
t.state.ToState(FAILED) |
|
||||
t.state.QuitAll() |
|
||||
return |
|
||||
} |
|
||||
t.state.ToState(READY) |
|
||||
}() |
|
||||
return t |
|
||||
} |
|
||||
|
|
||||
func (t *Target) head() error { |
|
||||
query := &http.Request{ |
|
||||
Method: "HEAD", |
|
||||
URL: t.URL, |
|
||||
Header: t.cookies, |
|
||||
} |
|
||||
resp, err := t.client.Do(query) |
|
||||
if err != nil { |
|
||||
return ErrNetworkErr(err) |
|
||||
} |
|
||||
defer func(Body io.ReadCloser) { |
|
||||
_ = Body.Close() |
|
||||
}(resp.Body) |
|
||||
if resp.StatusCode >= 300 || resp.StatusCode < 200 { |
|
||||
return requestFailed(resp) |
|
||||
} |
|
||||
var ok bool |
|
||||
for i := range HeadRetryCount + 1 { |
|
||||
ok = t.fromHeadGetDetails(resp) |
|
||||
if ok { |
|
||||
goto SUCCESS |
|
||||
} |
|
||||
log.Error("获取文件信息失败,重试次数:", strconv.Itoa(i), t.URL.String()) |
|
||||
} |
|
||||
return ErrRequestFailed{ |
|
||||
StatusCode: resp.StatusCode, |
|
||||
Msg: str.Join("无法获取文件信息:", t.URL.String()), |
|
||||
} |
|
||||
SUCCESS: |
|
||||
return nil |
|
||||
} |
|
||||
func (t *Target) fromHeadGetDetails(resp *http.Response) (ok bool) { |
|
||||
contentLength := resp.Header.Get("Content-Length") |
|
||||
var err error |
|
||||
if t.details.ContentSize, err = strconv.Atoi(contentLength); contentLength == "" || err != nil { |
|
||||
return false |
|
||||
} |
|
||||
acceptRanges := resp.Header.Get("Accept-Ranges") |
|
||||
t.details.AcceptRanges = acceptRanges == "bytes" |
|
||||
disposition := resp.Header.Get("Content-Disposition") |
|
||||
_, params, err := mime.ParseMediaType(disposition) |
|
||||
if t.details.Filename, ok = params["filename"]; !ok { |
|
||||
contentType := resp.Header.Get("Content-Type") |
|
||||
base := filepath.Base(t.URL.String()) |
|
||||
if strings.Contains(base, ".") { |
|
||||
t.details.Filename = base |
|
||||
} else { |
|
||||
var ext string |
|
||||
exts, _ := mime.ExtensionsByType(contentType) |
|
||||
if len(exts) != 0 { |
|
||||
ext = exts[1] |
|
||||
} |
|
||||
t.details.Filename = str.Join(base, ext) |
|
||||
} |
|
||||
} |
|
||||
return true |
|
||||
} |
|
||||
|
|
||||
func (t *Target) Wait() { |
|
||||
|
|
||||
} |
|
@ -1,5 +0,0 @@ |
|||||
package mdown |
|
||||
|
|
||||
type Thread struct { |
|
||||
Target *T |
|
||||
} |
|
@ -1,34 +0,0 @@ |
|||||
package mdown |
|
||||
|
|
||||
import ( |
|
||||
"net/http" |
|
||||
"net/url" |
|
||||
) |
|
||||
|
|
||||
type Option func(*Client) |
|
||||
|
|
||||
func WithTransportProxy(proxyURL *url.URL) Option { |
|
||||
return func(client *Client) { |
|
||||
client.Transport = &http.Transport{ |
|
||||
Proxy: http.ProxyURL(proxyURL), |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
func WithMaxThread(count int) Option { |
|
||||
if count <= 0 { |
|
||||
panic("count must be greater than zero") |
|
||||
} |
|
||||
return func(client *Client) { |
|
||||
client.MaxThread = count |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
func WithMaxThreadPerReq(count int) Option { |
|
||||
if count <= 0 { |
|
||||
panic("count must be greater than zero") |
|
||||
} |
|
||||
return func(client *Client) { |
|
||||
client.MaxThreadPerURL = count |
|
||||
} |
|
||||
} |
|
@ -1,50 +0,0 @@ |
|||||
package mdown |
|
||||
|
|
||||
import ( |
|
||||
"net/http" |
|
||||
"time" |
|
||||
|
|
||||
"git.realxlfd.cc/RealXLFD/golib/proc/thread/pool" |
|
||||
) |
|
||||
|
|
||||
var ( |
|
||||
TempFilesLocation = "./temp" |
|
||||
) |
|
||||
|
|
||||
const ( |
|
||||
DefaultMaxThread = 8 // 默认单文件最大线程数
|
|
||||
RetryTimeGap = 500 * time.Millisecond // 重试间隔
|
|
||||
) |
|
||||
|
|
||||
type Client struct { |
|
||||
Transport *http.Transport |
|
||||
tasks []*Task |
|
||||
pool *pool.Pool[*Worker, struct{}] |
|
||||
MaxThread int |
|
||||
MaxThreadPerURL int |
|
||||
} |
|
||||
|
|
||||
func NewClient(opts ...Option) *Client { |
|
||||
client := &Client{ |
|
||||
nil, |
|
||||
make([]*Task, 0), |
|
||||
nil, |
|
||||
DefaultMaxThread, |
|
||||
DefaultMaxThread, |
|
||||
} |
|
||||
for _, opt := range opts { |
|
||||
opt(client) |
|
||||
} |
|
||||
return client |
|
||||
} |
|
||||
|
|
||||
// Download 异步提交一个下载任务
|
|
||||
// func (c *Client) Download(
|
|
||||
// URL string, file string, header http.Header,
|
|
||||
// ) (*Task, error) {
|
|
||||
// targetURL, err := url.Parse(URL)
|
|
||||
// if err != nil {
|
|
||||
// return nil, err
|
|
||||
// }
|
|
||||
// return nil, err
|
|
||||
// }
|
|
@ -1,268 +0,0 @@ |
|||||
package mdown |
|
||||
|
|
||||
import ( |
|
||||
"errors" |
|
||||
"io" |
|
||||
"mime" |
|
||||
"net/http" |
|
||||
"net/url" |
|
||||
"os" |
|
||||
"path/filepath" |
|
||||
"strconv" |
|
||||
"strings" |
|
||||
"sync" |
|
||||
"time" |
|
||||
|
|
||||
"git.realxlfd.cc/RealXLFD/golib/cli/logger" |
|
||||
"git.realxlfd.cc/RealXLFD/golib/utils/str" |
|
||||
) |
|
||||
|
|
||||
func init() { |
|
||||
_ = mime.AddExtensionType(".zip", "application/zip") |
|
||||
_ = os.MkdirAll(TempFilesLocation, os.ModePerm) |
|
||||
} |
|
||||
|
|
||||
type Task struct { |
|
||||
client *http.Client |
|
||||
TargetURL *url.URL |
|
||||
Path string |
|
||||
Settings *Settings |
|
||||
Details Details |
|
||||
State State |
|
||||
group *sync.WaitGroup |
|
||||
workers []*Worker |
|
||||
pool ThreadPool |
|
||||
} |
|
||||
|
|
||||
type Settings struct { |
|
||||
TempDir string |
|
||||
Retry int |
|
||||
Headers http.Header |
|
||||
Condition func(*Details) int // 控制线程数量
|
|
||||
} |
|
||||
|
|
||||
type Details struct { |
|
||||
Filename string |
|
||||
ContentSize int |
|
||||
AcceptRanges bool |
|
||||
} |
|
||||
|
|
||||
type State int |
|
||||
|
|
||||
const ( |
|
||||
FAILED State = iota - 1 |
|
||||
WAITING |
|
||||
READY |
|
||||
DOWNLOADING |
|
||||
SUCCESS |
|
||||
) |
|
||||
|
|
||||
var ( |
|
||||
l = logger.Logger{} |
|
||||
) |
|
||||
|
|
||||
func NewTask(URL string, path string, opts ...TaskOptions) (*Task, error) { |
|
||||
target, err := url.Parse(URL) |
|
||||
if err != nil { |
|
||||
return nil, err |
|
||||
} |
|
||||
stat, err := os.Stat(path) |
|
||||
if err != nil || !stat.IsDir() { |
|
||||
return nil, errors.New(str.Join("invalid path: ", path)) |
|
||||
} |
|
||||
task := &Task{ |
|
||||
client: &http.Client{}, |
|
||||
TargetURL: target, |
|
||||
Path: path, |
|
||||
Settings: &Settings{}, |
|
||||
Details: Details{}, |
|
||||
State: WAITING, |
|
||||
group: &sync.WaitGroup{}, |
|
||||
workers: make([]*Worker, 0), |
|
||||
pool: nil, |
|
||||
} |
|
||||
for i := range opts { |
|
||||
err = opts[i](task) |
|
||||
if err != nil { |
|
||||
return nil, err |
|
||||
} |
|
||||
} |
|
||||
task.group.Add(1) |
|
||||
go func() { |
|
||||
var err error |
|
||||
for range task.Settings.Retry + 1 { |
|
||||
err = task.head() |
|
||||
if err != nil { |
|
||||
l.Error(err.Error()) |
|
||||
time.Sleep(RetryTimeGap) |
|
||||
continue |
|
||||
} |
|
||||
break |
|
||||
} |
|
||||
if err != nil { |
|
||||
task.State = FAILED |
|
||||
} else { |
|
||||
task.State = READY |
|
||||
} |
|
||||
task.group.Done() |
|
||||
}() |
|
||||
return task, nil |
|
||||
} |
|
||||
|
|
||||
func (d *Task) Start() *Task { |
|
||||
switch d.State { |
|
||||
case READY: |
|
||||
break |
|
||||
case DOWNLOADING: |
|
||||
fallthrough |
|
||||
case SUCCESS: |
|
||||
return d |
|
||||
case WAITING: |
|
||||
d.group.Wait() |
|
||||
default: |
|
||||
l.Error("任务失败,Start()调用无效") |
|
||||
return d |
|
||||
} |
|
||||
threads := DefaultMaxThread |
|
||||
if d.Settings.Condition != nil { |
|
||||
threads = d.Settings.Condition(&d.Details) |
|
||||
} |
|
||||
d.spawn(threads) |
|
||||
return d |
|
||||
} |
|
||||
|
|
||||
func (d *Task) spawn(threads int) { |
|
||||
var workers []*Worker |
|
||||
var ranges []requestRange |
|
||||
if d.Details.AcceptRanges || d.pool != nil { |
|
||||
ranges = rangeGenerator(d.Details.ContentSize, threads) |
|
||||
} else { |
|
||||
threads = 1 |
|
||||
ranges = []requestRange{ |
|
||||
{ |
|
||||
0, d.Details.ContentSize, |
|
||||
}, |
|
||||
} |
|
||||
} |
|
||||
for i := range threads { |
|
||||
w := &Worker{ |
|
||||
d, |
|
||||
false, |
|
||||
ranges[i], |
|
||||
d.TargetURL, |
|
||||
filepath.Join( |
|
||||
d.Settings.TempDir, |
|
||||
str.Join(d.Details.Filename, ".part", strconv.Itoa(i)), |
|
||||
), |
|
||||
d.group, |
|
||||
nil, |
|
||||
} |
|
||||
workers = append(workers, w) |
|
||||
} |
|
||||
d.group.Add(len(workers)) |
|
||||
if d.pool != nil { |
|
||||
d.pool.Push(workers...) |
|
||||
} else { |
|
||||
workers[0].Do() |
|
||||
} |
|
||||
d.workers = workers |
|
||||
d.State = DOWNLOADING |
|
||||
return |
|
||||
} |
|
||||
|
|
||||
func (d *Task) head() error { |
|
||||
headQuery := &http.Request{ |
|
||||
Method: "HEAD", |
|
||||
URL: d.TargetURL, |
|
||||
Header: d.Settings.Headers, |
|
||||
} |
|
||||
resp, err := d.client.Do(headQuery) |
|
||||
if err != nil { |
|
||||
return err |
|
||||
} |
|
||||
defer func() { |
|
||||
_ = resp.Body.Close() |
|
||||
}() |
|
||||
contentLength := resp.Header.Get("Content-Length") |
|
||||
if d.Details.ContentSize, err = strconv.Atoi(contentLength); contentLength == "" || err != nil { |
|
||||
return errors.New("无法获取文件大小") |
|
||||
} |
|
||||
acceptRanges := resp.Header.Get("Accept-Ranges") |
|
||||
d.Details.AcceptRanges = acceptRanges == "bytes" |
|
||||
disposition := resp.Header.Get("Content-Disposition") |
|
||||
_, params, err := mime.ParseMediaType(disposition) |
|
||||
var ok bool |
|
||||
if d.Details.Filename, ok = params["filename"]; !ok { |
|
||||
contentType := resp.Header.Get("Content-Type") |
|
||||
base := filepath.Base(d.TargetURL.String()) |
|
||||
if strings.Contains(base, ".") { |
|
||||
d.Details.Filename = base |
|
||||
} else { |
|
||||
var ext string |
|
||||
exts, _ := mime.ExtensionsByType(contentType) |
|
||||
if len(exts) != 0 { |
|
||||
ext = exts[1] |
|
||||
} |
|
||||
d.Details.Filename = str.Join(base, ext) |
|
||||
} |
|
||||
} |
|
||||
return nil |
|
||||
} |
|
||||
|
|
||||
func (d *Task) End() error { |
|
||||
switch d.State { |
|
||||
case READY: |
|
||||
d.Start() |
|
||||
case DOWNLOADING: |
|
||||
break |
|
||||
case SUCCESS: |
|
||||
return nil |
|
||||
case WAITING: |
|
||||
d.group.Wait() |
|
||||
d.Start() |
|
||||
default: |
|
||||
return errors.New("任务失败") |
|
||||
} |
|
||||
d.group.Wait() |
|
||||
if d.State == FAILED { |
|
||||
return errors.New(str.Join("下载失败:", d.Details.Filename)) |
|
||||
} |
|
||||
path := filepath.Join(d.Path, d.Details.Filename) |
|
||||
|
|
||||
targetFile, err := os.Create(path) |
|
||||
if err != nil { |
|
||||
d.State = FAILED |
|
||||
l.Error(str.Join("无法创建文件:", d.Details.Filename)) |
|
||||
return errors.New("无法创建文件") |
|
||||
} |
|
||||
defer func() { |
|
||||
_ = targetFile.Close() |
|
||||
}() |
|
||||
for i := range d.workers { |
|
||||
var part *os.File |
|
||||
part, err = os.OpenFile( |
|
||||
d.workers[i].File, |
|
||||
os.O_RDONLY, |
|
||||
os.ModePerm, |
|
||||
) |
|
||||
if err != nil { |
|
||||
l.Error(str.Join("文件合并失败:", d.Details.Filename)) |
|
||||
return err |
|
||||
} |
|
||||
_, err = io.Copy(targetFile, part) |
|
||||
if err != nil { |
|
||||
l.Error(str.Join("文件合并失败:", d.Details.Filename)) |
|
||||
return err |
|
||||
} |
|
||||
_ = part.Close() |
|
||||
} |
|
||||
d.State = SUCCESS |
|
||||
defer d.clearTempFiles() |
|
||||
return nil |
|
||||
} |
|
||||
|
|
||||
func (d *Task) clearTempFiles() { |
|
||||
for i := range d.workers { |
|
||||
_ = os.Remove(d.workers[i].File) |
|
||||
} |
|
||||
} |
|
@ -1,20 +0,0 @@ |
|||||
package mdown |
|
||||
|
|
||||
type requestRange struct { |
|
||||
Start int |
|
||||
End int |
|
||||
} |
|
||||
|
|
||||
func rangeGenerator(total, parts int) (ranges []requestRange) { |
|
||||
each := total / parts |
|
||||
for i := range parts { |
|
||||
ranges = append( |
|
||||
ranges, requestRange{ |
|
||||
i * each, |
|
||||
(i+1)*each - 1, |
|
||||
}, |
|
||||
) |
|
||||
} |
|
||||
ranges[len(ranges)-1].End = total |
|
||||
return ranges |
|
||||
} |
|
@ -1,41 +0,0 @@ |
|||||
package mdown |
|
||||
|
|
||||
import ( |
|
||||
"net/http" |
|
||||
"net/url" |
|
||||
) |
|
||||
|
|
||||
type ThreadPool interface { |
|
||||
Push(worker ...*Worker) |
|
||||
} |
|
||||
|
|
||||
type TaskOptions func(*Task) error |
|
||||
|
|
||||
func WithProxy(URL string) TaskOptions { |
|
||||
proxy, err := url.Parse(URL) |
|
||||
if err != nil { |
|
||||
return func(_ *Task) error { |
|
||||
return err |
|
||||
} |
|
||||
} |
|
||||
return func(task *Task) error { |
|
||||
task.client.Transport = &http.Transport{ |
|
||||
Proxy: http.ProxyURL(proxy), |
|
||||
} |
|
||||
return nil |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
func WithThreadPool(pool ThreadPool) TaskOptions { |
|
||||
return func(task *Task) error { |
|
||||
task.pool = pool |
|
||||
return nil |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
func WithSetting(settings *Settings) TaskOptions { |
|
||||
return func(task *Task) error { |
|
||||
task.Settings = settings |
|
||||
return nil |
|
||||
} |
|
||||
} |
|
@ -1,116 +0,0 @@ |
|||||
package mdown |
|
||||
|
|
||||
import ( |
|
||||
"io" |
|
||||
"net/http" |
|
||||
"net/url" |
|
||||
"os" |
|
||||
"strconv" |
|
||||
"sync" |
|
||||
|
|
||||
"git.realxlfd.cc/RealXLFD/golib/utils/ioplus" |
|
||||
"git.realxlfd.cc/RealXLFD/golib/utils/str" |
|
||||
"github.com/dustin/go-humanize" |
|
||||
) |
|
||||
|
|
||||
type Worker struct { |
|
||||
Task *Task |
|
||||
Failed bool |
|
||||
Range requestRange |
|
||||
Url *url.URL |
|
||||
File string |
|
||||
WG *sync.WaitGroup |
|
||||
Writer *ioplus.SWrite |
|
||||
} |
|
||||
|
|
||||
func (w *Worker) Do() { |
|
||||
header := http.Header{ |
|
||||
"Range": []string{ |
|
||||
str.Join( |
|
||||
"bytes=", |
|
||||
strconv.Itoa(w.Range.Start), |
|
||||
"-", |
|
||||
strconv.Itoa(w.Range.End), |
|
||||
), |
|
||||
}, |
|
||||
} |
|
||||
for k := range w.Task.Settings.Headers { |
|
||||
header[k] = w.Task.Settings.Headers[k] |
|
||||
} |
|
||||
req := &http.Request{ |
|
||||
Header: header, |
|
||||
URL: w.Url, |
|
||||
} |
|
||||
l.Info(str.Join("发起下载:", w.File)) |
|
||||
for range w.Task.Settings.Retry + 1 { |
|
||||
switch w.request(req) { |
|
||||
case Retry: |
|
||||
l.Info(str.Join("重试:", w.File)) |
|
||||
continue |
|
||||
case Fatal: |
|
||||
break |
|
||||
case Success: |
|
||||
l.Info( |
|
||||
str.Join( |
|
||||
"下载完成,一共写入:", |
|
||||
humanize.Bytes(uint64(w.Writer.Total())), |
|
||||
), |
|
||||
) |
|
||||
w.WG.Done() |
|
||||
return |
|
||||
} |
|
||||
break |
|
||||
} |
|
||||
l.Error(str.Join("失败:", w.File)) |
|
||||
w.Failed = true |
|
||||
w.Task.State = FAILED |
|
||||
w.WG.Done() |
|
||||
return |
|
||||
} |
|
||||
|
|
||||
type ReqStatus int |
|
||||
|
|
||||
const ( |
|
||||
Fatal ReqStatus = iota - 1 |
|
||||
Retry |
|
||||
Success |
|
||||
) |
|
||||
|
|
||||
func (w *Worker) request(req *http.Request) (status ReqStatus) { |
|
||||
resp, err := w.Task.client.Do(req) |
|
||||
if err != nil { |
|
||||
return Retry |
|
||||
} |
|
||||
defer func() { |
|
||||
_ = resp.Body.Close() |
|
||||
}() |
|
||||
if resp.StatusCode >= 300 { |
|
||||
l.Error(str.Join("响应:", strconv.Itoa(resp.StatusCode))) |
|
||||
return Retry |
|
||||
} |
|
||||
file, err := os.OpenFile(w.File, os.O_CREATE, os.ModePerm) |
|
||||
if err != nil { |
|
||||
l.Error(str.Join("无法打开文件:", w.File)) |
|
||||
return Fatal |
|
||||
} |
|
||||
err = file.Truncate(0) |
|
||||
if err != nil { |
|
||||
l.Error(str.Join("无法覆写文件:", w.File)) |
|
||||
return Fatal |
|
||||
} |
|
||||
_, err = file.Seek(0, 0) |
|
||||
if err != nil { |
|
||||
l.Error(str.Join("无法覆写文件:", w.File)) |
|
||||
return Fatal |
|
||||
} |
|
||||
defer func() { |
|
||||
_ = file.Close() |
|
||||
}() |
|
||||
w.Writer = ioplus.NewSWriteCloser(file) |
|
||||
_, err = io.Copy(w.Writer, resp.Body) |
|
||||
if err != nil { |
|
||||
l.Error(str.Join("无法写入文件:", w.File)) |
|
||||
return Fatal |
|
||||
} |
|
||||
return Success |
|
||||
} |
|
@ -0,0 +1,29 @@ |
|||||
|
package speed |
||||
|
|
||||
|
import ( |
||||
|
"github.com/dustin/go-humanize" |
||||
|
) |
||||
|
|
||||
|
type Speed struct { |
||||
|
current int64 |
||||
|
total int64 |
||||
|
average int64 |
||||
|
} |
||||
|
|
||||
|
func (s Speed) Average() int64 { |
||||
|
return s.average |
||||
|
} |
||||
|
|
||||
|
func (s Speed) Current() int64 { |
||||
|
return s.current |
||||
|
} |
||||
|
|
||||
|
func (s Speed) Total() int64 { |
||||
|
return s.total |
||||
|
} |
||||
|
|
||||
|
func (s Speed) String() (cur, avg string) { |
||||
|
cur = humanize.Bytes(uint64(s.current)) |
||||
|
avg = humanize.Bytes(uint64(s.average)) |
||||
|
return |
||||
|
} |
@ -0,0 +1,77 @@ |
|||||
|
package speed |
||||
|
|
||||
|
import ( |
||||
|
"sync" |
||||
|
"time" |
||||
|
|
||||
|
"git.realxlfd.cc/RealXLFD/golib/utils/containers/queue" |
||||
|
) |
||||
|
|
||||
|
type WriteCloser struct { |
||||
|
Speed Speed |
||||
|
queue *queue.Queue[int64] |
||||
|
lock *sync.Mutex |
||||
|
start int64 |
||||
|
lastCalc int64 |
||||
|
last int64 |
||||
|
refresh time.Duration |
||||
|
running bool |
||||
|
promise *sync.WaitGroup |
||||
|
} |
||||
|
|
||||
|
func (w *WriteCloser) Running() bool { |
||||
|
return w.running |
||||
|
} |
||||
|
|
||||
|
func (w *WriteCloser) Write(p []byte) (n int, err error) { |
||||
|
w.lock.Lock() |
||||
|
defer w.lock.Unlock() |
||||
|
n = len(p) |
||||
|
if n > 0 { |
||||
|
w.queue.Push(int64(n)) |
||||
|
} |
||||
|
return n, nil |
||||
|
} |
||||
|
|
||||
|
func (w *WriteCloser) Close() { |
||||
|
w.running = false |
||||
|
w.promise.Wait() |
||||
|
calc(w) |
||||
|
} |
||||
|
|
||||
|
func (w *WriteCloser) Recover() *WriteCloser { |
||||
|
if w.running { |
||||
|
return w |
||||
|
} |
||||
|
w.promise.Wait() |
||||
|
w.Run() |
||||
|
return w |
||||
|
} |
||||
|
|
||||
|
func (w *WriteCloser) Pause() *WriteCloser { |
||||
|
if !w.running { |
||||
|
return w |
||||
|
} |
||||
|
w.running = false |
||||
|
return w |
||||
|
} |
||||
|
|
||||
|
func (w *WriteCloser) Run() *WriteCloser { |
||||
|
if w.running { |
||||
|
return w |
||||
|
} |
||||
|
w.running = true |
||||
|
w.promise.Add(1) |
||||
|
go daemon(w) |
||||
|
return w |
||||
|
} |
||||
|
|
||||
|
func NewWriter(interval time.Duration) *WriteCloser { |
||||
|
return &WriteCloser{ |
||||
|
queue: queue.New[int64](), |
||||
|
lock: &sync.Mutex{}, |
||||
|
refresh: interval, |
||||
|
running: false, |
||||
|
promise: &sync.WaitGroup{}, |
||||
|
} |
||||
|
} |
@ -0,0 +1,36 @@ |
|||||
|
package speed |
||||
|
|
||||
|
import ( |
||||
|
"time" |
||||
|
) |
||||
|
|
||||
|
func daemon(w *WriteCloser) { |
||||
|
w.start = time.Now().UnixMilli() |
||||
|
w.lastCalc = w.start |
||||
|
time.Sleep(time.Millisecond * 50) |
||||
|
for w.running { |
||||
|
calc(w) |
||||
|
time.Sleep(w.refresh) |
||||
|
} |
||||
|
w.last += time.Now().UnixMilli() - w.start |
||||
|
w.promise.Done() |
||||
|
} |
||||
|
|
||||
|
func calc(w *WriteCloser) { |
||||
|
w.lock.Lock() |
||||
|
defer w.lock.Unlock() |
||||
|
var written int64 |
||||
|
for w.queue.Size() > 0 { |
||||
|
written += w.queue.Pop() |
||||
|
} |
||||
|
now := time.Now().UnixMilli() |
||||
|
interval := now - w.lastCalc |
||||
|
w.lastCalc = now |
||||
|
if written > 0 { |
||||
|
w.Speed.current = written * 1000 / interval |
||||
|
w.Speed.total += written |
||||
|
} |
||||
|
if w.Speed.total > 0 { |
||||
|
w.Speed.average = w.Speed.total * 1000 / (now - w.start + w.last) |
||||
|
} |
||||
|
} |
Loading…
Reference in new issue