RealXLFD
4 weeks ago
11 changed files with 438 additions and 35 deletions
@ -0,0 +1,26 @@ |
|||
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 |
|||
) |
@ -0,0 +1,45 @@ |
|||
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() |
|||
} |
@ -0,0 +1,32 @@ |
|||
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), |
|||
), |
|||
}, |
|||
} |
|||
} |
@ -0,0 +1,218 @@ |
|||
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() { |
|||
|
|||
} |
@ -0,0 +1,5 @@ |
|||
package mdown |
|||
|
|||
type Thread struct { |
|||
Target *T |
|||
} |
@ -0,0 +1,73 @@ |
|||
package state |
|||
|
|||
import "sync" |
|||
|
|||
type Manager struct { |
|||
State State |
|||
lock *sync.Mutex |
|||
cond *sync.Cond |
|||
exit bool |
|||
} |
|||
|
|||
type State int |
|||
|
|||
const ( |
|||
FAILED State = iota - 1 |
|||
WAITING |
|||
READY |
|||
RUNNING |
|||
SUCCESS |
|||
) |
|||
|
|||
func New(state State) *Manager { |
|||
lock := &sync.Mutex{} |
|||
return &Manager{ |
|||
State: state, |
|||
lock: lock, |
|||
cond: sync.NewCond(lock), |
|||
} |
|||
} |
|||
|
|||
func (m *Manager) ToState(state State) { |
|||
m.lock.Lock() |
|||
defer m.lock.Unlock() |
|||
m.State = state |
|||
m.cond.Broadcast() |
|||
} |
|||
|
|||
func (m *Manager) WaitState(state State) (ok bool) { |
|||
m.lock.Lock() |
|||
defer m.lock.Unlock() |
|||
for m.State != state { |
|||
m.cond.Wait() |
|||
if m.exit { |
|||
return false |
|||
} |
|||
} |
|||
return true |
|||
} |
|||
|
|||
func (m *Manager) WaitChange() { |
|||
m.lock.Lock() |
|||
defer m.lock.Unlock() |
|||
m.cond.Wait() |
|||
return |
|||
} |
|||
|
|||
func (m *Manager) Get() State { |
|||
return m.State |
|||
} |
|||
|
|||
func (m *Manager) QuitAll() { |
|||
m.exit = true |
|||
m.cond.Broadcast() |
|||
} |
|||
|
|||
func (m *Manager) Is(states ...State) bool { |
|||
for _, state := range states { |
|||
if m.State == state { |
|||
return true |
|||
} |
|||
} |
|||
return false |
|||
} |
@ -0,0 +1,8 @@ |
|||
package utils |
|||
|
|||
func JoinMap[A string | int | float64, B any](dst, src map[A]B) map[A]B { |
|||
for k := range src { |
|||
dst[k] = src[k] |
|||
} |
|||
return dst |
|||
} |
Loading…
Reference in new issue