mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-06 16:40:32 +00:00
811 lines
18 KiB
Go
811 lines
18 KiB
Go
// Package ftp downloads files and directory trees over FTP, FTPS (implicit
|
|
// TLS) and FTPES (explicit TLS, AUTH TLS).
|
|
//
|
|
// A file is split into segments of at least 1 MiB. Each segment is fetched on
|
|
// its own login with REST and RETR, and its data connection is closed once
|
|
// the segment's last byte has arrived. A server that refuses a login lowers
|
|
// the task's connection ceiling to the logins that worked, and a server that
|
|
// refuses REST gets one sequential stream per file.
|
|
package ftp
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/jlaffaye/ftp"
|
|
"github.com/rs/zerolog"
|
|
|
|
"github.com/GopeedLab/gopeed/internal/controller"
|
|
"github.com/GopeedLab/gopeed/internal/fetcher"
|
|
"github.com/GopeedLab/gopeed/pkg/base"
|
|
pftp "github.com/GopeedLab/gopeed/pkg/protocol/ftp"
|
|
)
|
|
|
|
const (
|
|
// Many servers cap logins per user, so FTP starts lower than HTTP.
|
|
defaultConnections = 4
|
|
maxConnections = 256
|
|
minSegmentSize = 1 << 20
|
|
maxRetries = 5
|
|
copyBufferSize = 256 << 10
|
|
)
|
|
|
|
// retryDelay is the first wait before a retry. Later retries wait longer.
|
|
var retryDelay = 500 * time.Millisecond
|
|
|
|
var (
|
|
errSizeChanged = errors.New("ftp: the remote file changed size since the task was resolved")
|
|
errShortTransfer = errors.New("ftp: the transfer ended before the segment was complete")
|
|
errRestUnsupported = errors.New("ftp: the server does not support REST")
|
|
errDiskWrite = errors.New("ftp: cannot write the downloaded data")
|
|
)
|
|
|
|
type config struct {
|
|
Connections int `json:"connections"`
|
|
}
|
|
|
|
// segment is a byte range of one file. End is inclusive, so an empty file
|
|
// has End == Begin-1 and nothing to fetch.
|
|
type segment struct {
|
|
File int `json:",omitempty"` // index in Res.Files
|
|
Begin int64
|
|
End int64
|
|
Downloaded int64
|
|
}
|
|
|
|
func (s *segment) remain() int64 {
|
|
return s.End - s.Begin + 1 - s.Downloaded
|
|
}
|
|
|
|
// lane is one worker that logged in, shown as one connection in the stats.
|
|
type lane struct {
|
|
downloaded int64
|
|
retryTimes int
|
|
failed bool
|
|
completed bool
|
|
}
|
|
|
|
// loginError marks a failure to connect or log in, as opposed to a failure
|
|
// during a transfer.
|
|
type loginError struct{ err error }
|
|
|
|
func (e *loginError) Error() string { return e.err.Error() }
|
|
func (e *loginError) Unwrap() error { return e.err }
|
|
|
|
type Fetcher struct {
|
|
manager *FetcherManager
|
|
ctl *controller.Controller
|
|
config *config
|
|
meta *fetcher.FetcherMeta
|
|
doneCh chan error
|
|
|
|
warnOnce sync.Once
|
|
// loginOK is set once these credentials have logged in, after which a
|
|
// 530 on another login means the server is full.
|
|
loginOK atomic.Bool
|
|
|
|
// mu guards the download state below.
|
|
mu sync.Mutex
|
|
segments []*segment
|
|
maxConns int
|
|
noRest bool
|
|
lanes []*lane
|
|
done bool
|
|
|
|
// runMu serializes Start and Pause.
|
|
runMu sync.Mutex
|
|
cancel context.CancelFunc
|
|
runDone chan struct{}
|
|
}
|
|
|
|
func (f *Fetcher) Setup(ctl *controller.Controller) {
|
|
f.ctl = ctl
|
|
f.doneCh = make(chan error, 1)
|
|
if f.meta == nil {
|
|
f.meta = &fetcher.FetcherMeta{}
|
|
}
|
|
if ctl != nil && ctl.GetConfig != nil {
|
|
ctl.GetConfig(&f.config)
|
|
}
|
|
if f.config == nil {
|
|
f.config = &config{Connections: defaultConnections}
|
|
}
|
|
}
|
|
|
|
func (f *Fetcher) logger() *zerolog.Logger {
|
|
if f.ctl == nil {
|
|
return nil
|
|
}
|
|
return f.ctl.Logger
|
|
}
|
|
|
|
func (f *Fetcher) warn(format string, args ...any) {
|
|
if l := f.logger(); l != nil {
|
|
l.Warn().Msgf(format, args...)
|
|
}
|
|
}
|
|
|
|
func (f *Fetcher) initOptions() error {
|
|
if f.meta.Opts == nil {
|
|
f.meta.Opts = &base.Options{}
|
|
}
|
|
opts := f.meta.Opts
|
|
if err := base.ParseOptExtra[pftp.OptsExtra](opts); err != nil {
|
|
return err
|
|
}
|
|
if opts.Extra == nil {
|
|
opts.Extra = &pftp.OptsExtra{}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (f *Fetcher) extra() *pftp.OptsExtra {
|
|
if f.meta.Opts != nil {
|
|
if e, ok := f.meta.Opts.Extra.(*pftp.OptsExtra); ok && e != nil {
|
|
return e
|
|
}
|
|
}
|
|
return &pftp.OptsExtra{}
|
|
}
|
|
|
|
// tlsMode is the request's "tls" field, empty when the request has none.
|
|
func (f *Fetcher) tlsMode() string {
|
|
if f.meta.Req != nil {
|
|
if e, ok := f.meta.Req.Extra.(*pftp.ReqExtra); ok && e != nil {
|
|
return e.TLS
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// connections is the number of parallel logins to use. The caller holds mu.
|
|
func (f *Fetcher) connections() int {
|
|
if f.noRest {
|
|
return 1
|
|
}
|
|
n := f.extra().Connections
|
|
if n <= 0 && f.config != nil {
|
|
n = f.config.Connections
|
|
}
|
|
if n <= 0 {
|
|
n = defaultConnections
|
|
}
|
|
n = min(n, maxConnections)
|
|
if f.maxConns > 0 {
|
|
n = min(n, f.maxConns)
|
|
}
|
|
return n
|
|
}
|
|
|
|
func (f *Fetcher) Resolve(req *base.Request) error {
|
|
if err := base.ParseReqExtra[pftp.ReqExtra](req); err != nil {
|
|
return err
|
|
}
|
|
f.meta.Req = req
|
|
t, err := parseTarget(req.URL, f.tlsMode())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
s, err := f.dial(context.Background(), t)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// quit, not abort: a server that caps logins must see this session end
|
|
// before the download's own logins arrive.
|
|
defer s.quit()
|
|
f.loginOK.Store(true)
|
|
res, err := f.resolveTarget(s, t)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
f.meta.Res = res
|
|
f.mu.Lock()
|
|
f.segments = nil
|
|
f.done = false
|
|
f.mu.Unlock()
|
|
return nil
|
|
}
|
|
|
|
// selected returns the indexes of the files to download, in SelectFiles
|
|
// order. The caller holds mu or owns the fetcher.
|
|
func (f *Fetcher) selected() []int {
|
|
if f.meta == nil || f.meta.Res == nil {
|
|
return nil
|
|
}
|
|
if f.meta.Opts != nil && len(f.meta.Opts.SelectFiles) > 0 {
|
|
return f.meta.Opts.SelectFiles
|
|
}
|
|
all := make([]int, len(f.meta.Res.Files))
|
|
for i := range all {
|
|
all[i] = i
|
|
}
|
|
return all
|
|
}
|
|
|
|
// localPath is where a file lands: under the task path, with the file's
|
|
// directories below the URL kept.
|
|
func (f *Fetcher) localPath(index int) string {
|
|
if f.meta.Res.Name == "" {
|
|
return f.meta.SingleFilepath()
|
|
}
|
|
file := f.meta.Res.Files[index]
|
|
return path.Join(f.meta.FolderPath(), file.Path, file.Name)
|
|
}
|
|
|
|
// remotePath is the server path of a file. A single file uses the URL's own
|
|
// path, which a local rename never touches.
|
|
func (f *Fetcher) remotePath(t *target, index int) string {
|
|
if f.meta.Res.Name == "" {
|
|
return t.path
|
|
}
|
|
file := f.meta.Res.Files[index]
|
|
return path.Join(t.path, file.Path, file.Name)
|
|
}
|
|
|
|
// plan splits every selected file into segments of at least minSegmentSize,
|
|
// one per connection at most. The caller holds mu.
|
|
func (f *Fetcher) plan() {
|
|
conns := f.connections()
|
|
f.segments = nil
|
|
for _, index := range f.selected() {
|
|
size := f.meta.Res.Files[index].Size
|
|
n := int64(1)
|
|
if !f.noRest {
|
|
n = max(1, min(int64(conns), size/minSegmentSize))
|
|
}
|
|
for i := int64(0); i < n; i++ {
|
|
f.segments = append(f.segments, &segment{
|
|
File: index,
|
|
Begin: i * size / n,
|
|
End: (i+1)*size/n - 1,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
// planSequential gives every unfinished file one segment from byte 0,
|
|
// because without REST a transfer cannot start in the middle. The caller
|
|
// holds mu.
|
|
func (f *Fetcher) planSequential() {
|
|
complete := map[int]bool{}
|
|
for _, s := range f.segments {
|
|
if _, seen := complete[s.File]; !seen {
|
|
complete[s.File] = true
|
|
}
|
|
if s.remain() > 0 {
|
|
complete[s.File] = false
|
|
}
|
|
}
|
|
var out []*segment
|
|
for _, index := range f.selected() {
|
|
size := f.meta.Res.Files[index].Size
|
|
if complete[index] {
|
|
out = append(out, &segment{File: index, Begin: 0, End: size - 1, Downloaded: size})
|
|
continue
|
|
}
|
|
out = append(out, &segment{File: index, Begin: 0, End: size - 1})
|
|
}
|
|
f.segments = out
|
|
// Bytes counted by the old segments are fetched again.
|
|
f.lanes = nil
|
|
}
|
|
|
|
// openFiles opens every selected file for writing. A fresh plan creates and
|
|
// sizes them; a resumed one keeps their content, and a file that has gone
|
|
// missing starts over.
|
|
func (f *Fetcher) openFiles(fresh bool) (map[int]*os.File, error) {
|
|
files := map[int]*os.File{}
|
|
closeAll := func() {
|
|
for _, file := range files {
|
|
_ = file.Close()
|
|
}
|
|
}
|
|
for _, index := range f.selected() {
|
|
name := f.localPath(index)
|
|
var (
|
|
file *os.File
|
|
err error
|
|
)
|
|
_, statErr := os.Stat(name)
|
|
switch {
|
|
case fresh || os.IsNotExist(statErr):
|
|
file, err = f.ctl.Touch(name, f.meta.Res.Files[index].Size)
|
|
if err == nil && !fresh {
|
|
f.mu.Lock()
|
|
for _, s := range f.segments {
|
|
if s.File == index {
|
|
s.Downloaded = 0
|
|
}
|
|
}
|
|
f.mu.Unlock()
|
|
}
|
|
case statErr != nil:
|
|
err = statErr
|
|
default:
|
|
file, err = os.OpenFile(name, os.O_RDWR, 0)
|
|
}
|
|
if err != nil {
|
|
closeAll()
|
|
return nil, err
|
|
}
|
|
files[index] = file
|
|
}
|
|
return files, nil
|
|
}
|
|
|
|
func (f *Fetcher) Start() error {
|
|
f.runMu.Lock()
|
|
defer f.runMu.Unlock()
|
|
if f.runDone != nil {
|
|
select {
|
|
case <-f.runDone:
|
|
default:
|
|
return nil // already running
|
|
}
|
|
}
|
|
if err := f.initOptions(); err != nil {
|
|
return err
|
|
}
|
|
if err := base.ParseReqExtra[pftp.ReqExtra](f.meta.Req); err != nil {
|
|
return err
|
|
}
|
|
t, err := parseTarget(f.meta.Req.URL, f.tlsMode())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// A paused or failed run may have left a result nobody read.
|
|
select {
|
|
case <-f.doneCh:
|
|
default:
|
|
}
|
|
|
|
f.mu.Lock()
|
|
if f.done {
|
|
f.mu.Unlock()
|
|
f.doneCh <- nil
|
|
return nil
|
|
}
|
|
fresh := len(f.segments) == 0
|
|
if fresh {
|
|
f.plan()
|
|
} else if f.noRest {
|
|
f.planSequential()
|
|
}
|
|
f.mu.Unlock()
|
|
|
|
files, err := f.openFiles(fresh)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
f.cancel = cancel
|
|
f.runDone = make(chan struct{})
|
|
go f.run(ctx, t, files, f.runDone)
|
|
return nil
|
|
}
|
|
|
|
func (f *Fetcher) run(ctx context.Context, t *target, files map[int]*os.File, done chan struct{}) {
|
|
defer close(done)
|
|
err := f.download(ctx, t, files)
|
|
for _, file := range files {
|
|
if cerr := file.Close(); cerr != nil && err == nil {
|
|
err = cerr
|
|
}
|
|
}
|
|
f.mu.Lock()
|
|
if err == nil {
|
|
f.done = true
|
|
for _, l := range f.lanes {
|
|
l.completed = true
|
|
}
|
|
}
|
|
f.mu.Unlock()
|
|
if err != nil && ctx.Err() != nil {
|
|
return // paused
|
|
}
|
|
select {
|
|
case f.doneCh <- err:
|
|
default:
|
|
}
|
|
}
|
|
|
|
func (f *Fetcher) download(ctx context.Context, t *target, files map[int]*os.File) error {
|
|
for {
|
|
err := f.runWorkers(ctx, t, files)
|
|
if !errors.Is(err, errRestUnsupported) {
|
|
return err
|
|
}
|
|
f.mu.Lock()
|
|
f.noRest = true
|
|
f.planSequential()
|
|
f.mu.Unlock()
|
|
f.warn("ftp: the server refused REST, so each file is downloaded in one stream")
|
|
}
|
|
}
|
|
|
|
// pool hands out the segments of one run to its workers. Every decision to
|
|
// leave the run is taken under mu together with the queue it depends on, so
|
|
// a segment put back can never be left without a worker.
|
|
type pool struct {
|
|
mu sync.Mutex
|
|
queue []*segment
|
|
running int // workers still in the run
|
|
}
|
|
|
|
// take returns the next segment. A worker that finds the queue empty leaves
|
|
// the run.
|
|
func (p *pool) take() *segment {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
if len(p.queue) == 0 {
|
|
p.running--
|
|
return nil
|
|
}
|
|
s := p.queue[0]
|
|
p.queue = p.queue[1:]
|
|
return s
|
|
}
|
|
|
|
func (p *pool) putBack(s *segment) {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
p.queue = append([]*segment{s}, p.queue...)
|
|
}
|
|
|
|
// refused puts back the segment of a worker whose login the server turned
|
|
// away. The worker leaves when another worker is still in the run to take
|
|
// the segment, and left is the number of workers that remain; the last
|
|
// worker stays and retries.
|
|
func (p *pool) refused(s *segment) (leave bool, left int) {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
p.queue = append([]*segment{s}, p.queue...)
|
|
if p.running > 1 {
|
|
p.running--
|
|
return true, p.running
|
|
}
|
|
return false, p.running
|
|
}
|
|
|
|
// leave takes a worker out of the run when it stops for any other reason.
|
|
func (p *pool) leave() {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
p.running--
|
|
}
|
|
|
|
func (f *Fetcher) runWorkers(ctx context.Context, t *target, files map[int]*os.File) error {
|
|
runCtx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
|
|
f.mu.Lock()
|
|
p := &pool{}
|
|
for _, s := range f.segments {
|
|
if s.remain() > 0 {
|
|
p.queue = append(p.queue, s)
|
|
}
|
|
}
|
|
workers := min(f.connections(), len(p.queue))
|
|
f.mu.Unlock()
|
|
if workers == 0 {
|
|
return nil
|
|
}
|
|
p.running = workers
|
|
|
|
var (
|
|
wg sync.WaitGroup
|
|
errMu sync.Mutex
|
|
firstErr error
|
|
)
|
|
for id := 0; id < workers; id++ {
|
|
wg.Add(1)
|
|
go func(id int) {
|
|
defer wg.Done()
|
|
if err := f.worker(runCtx, t, files, p, id); err != nil {
|
|
errMu.Lock()
|
|
if firstErr == nil {
|
|
firstErr = err
|
|
}
|
|
errMu.Unlock()
|
|
cancel()
|
|
}
|
|
}(id)
|
|
}
|
|
wg.Wait()
|
|
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
if firstErr != nil {
|
|
return firstErr
|
|
}
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
for _, s := range f.segments {
|
|
if s.remain() > 0 {
|
|
return errors.New("ftp: the download stopped with data missing")
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// worker fetches segments from the pool until it is empty. A worker whose
|
|
// login the server refuses (421, or 530 once these credentials have worked)
|
|
// hands its segment back and leaves while other workers remain, lowering the
|
|
// task's ceiling to the workers left. This holds whether or not the worker
|
|
// had logged in before: a slot can also go to another client mid-download.
|
|
func (f *Fetcher) worker(ctx context.Context, t *target, files map[int]*os.File, p *pool, id int) error {
|
|
var ln *lane
|
|
f.mu.Lock()
|
|
if id < len(f.lanes) {
|
|
ln = f.lanes[id]
|
|
}
|
|
f.mu.Unlock()
|
|
onLogin := func() {
|
|
f.mu.Lock()
|
|
if ln == nil {
|
|
ln = &lane{}
|
|
f.lanes = append(f.lanes, ln)
|
|
}
|
|
f.mu.Unlock()
|
|
}
|
|
|
|
failures := 0
|
|
pendingRetries := 0
|
|
for {
|
|
seg := p.take()
|
|
if seg == nil {
|
|
return nil
|
|
}
|
|
err := f.fetchSegment(ctx, t, files, seg, &ln, onLogin)
|
|
if err == nil {
|
|
failures = 0
|
|
continue
|
|
}
|
|
if ctx.Err() != nil {
|
|
p.putBack(seg)
|
|
p.leave()
|
|
return nil
|
|
}
|
|
|
|
var le *loginError
|
|
if errors.As(err, &le) && isLoginRefusal(le.err, f.loginOK.Load()) {
|
|
if leave, left := p.refused(seg); leave {
|
|
f.lowerCeiling(left)
|
|
return nil
|
|
}
|
|
// The last worker: wait for a slot.
|
|
} else {
|
|
p.putBack(seg)
|
|
if errors.Is(err, errRestUnsupported) {
|
|
p.leave()
|
|
return err
|
|
}
|
|
if isPermanent(err) || errors.Is(err, errDiskWrite) {
|
|
f.markFailed(ln)
|
|
p.leave()
|
|
return err
|
|
}
|
|
}
|
|
|
|
failures++
|
|
f.mu.Lock()
|
|
if ln != nil {
|
|
ln.retryTimes += pendingRetries + 1
|
|
pendingRetries = 0
|
|
} else {
|
|
pendingRetries++
|
|
}
|
|
f.mu.Unlock()
|
|
if failures > maxRetries {
|
|
f.markFailed(ln)
|
|
p.leave()
|
|
return fmt.Errorf("ftp: giving up after %d retries: %w", maxRetries, err)
|
|
}
|
|
select {
|
|
case <-ctx.Done():
|
|
p.leave()
|
|
return nil
|
|
case <-time.After(retryDelay * time.Duration(failures)):
|
|
}
|
|
}
|
|
}
|
|
|
|
func (f *Fetcher) lowerCeiling(n int) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
if f.maxConns == 0 || n < f.maxConns {
|
|
f.maxConns = n
|
|
}
|
|
}
|
|
|
|
func (f *Fetcher) markFailed(ln *lane) {
|
|
if ln == nil {
|
|
return
|
|
}
|
|
f.mu.Lock()
|
|
ln.failed = true
|
|
f.mu.Unlock()
|
|
}
|
|
|
|
// isRestRefused tells a REST the server does not implement. RETR itself
|
|
// fails with 550 and the like, never with these codes.
|
|
func isRestRefused(err error) bool {
|
|
switch replyCode(err) {
|
|
case ftp.StatusBadCommand, ftp.StatusBadArguments, ftp.StatusNotImplemented, ftp.StatusNotImplementedParameter:
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// fetchSegment logs in, retrieves the segment from its first missing byte
|
|
// and writes it in place. It stops reading at the segment's end and closes
|
|
// the data connection; the server then reports the transfer aborted, which
|
|
// is expected.
|
|
func (f *Fetcher) fetchSegment(ctx context.Context, t *target, files map[int]*os.File, seg *segment, ln **lane, onLogin func()) error {
|
|
s, err := f.dial(ctx, t)
|
|
if err != nil {
|
|
return &loginError{err}
|
|
}
|
|
defer s.quit()
|
|
f.loginOK.Store(true)
|
|
onLogin()
|
|
|
|
remote := f.remotePath(t, seg.File)
|
|
f.mu.Lock()
|
|
noRest := f.noRest
|
|
wantSize := f.meta.Res.Files[seg.File].Size
|
|
if noRest {
|
|
seg.Downloaded = 0
|
|
}
|
|
pos := seg.Begin + seg.Downloaded
|
|
end := seg.End
|
|
f.mu.Unlock()
|
|
|
|
// A file that changed on the server would mix old and new bytes.
|
|
s.deadline()
|
|
if size, err := s.FileSize(remote); err == nil && size != wantSize {
|
|
return errSizeChanged
|
|
} else if err != nil && replyCode(err) == 0 {
|
|
return err
|
|
}
|
|
|
|
s.deadline()
|
|
var resp *ftp.Response
|
|
if noRest {
|
|
resp, err = s.Retr(remote)
|
|
} else {
|
|
resp, err = s.RetrFrom(remote, uint64(pos))
|
|
}
|
|
if err != nil {
|
|
if !noRest && pos > 0 && isRestRefused(err) {
|
|
return errRestUnsupported
|
|
}
|
|
return err
|
|
}
|
|
defer resp.Close()
|
|
s.clearDeadline()
|
|
|
|
file := files[seg.File]
|
|
buf := make([]byte, copyBufferSize)
|
|
for pos <= end {
|
|
n := int(min(int64(len(buf)), end-pos+1))
|
|
_ = resp.SetDeadline(time.Now().Add(readTimeout))
|
|
nr, rerr := resp.Read(buf[:n])
|
|
if nr > 0 {
|
|
if _, werr := file.WriteAt(buf[:nr], pos); werr != nil {
|
|
return fmt.Errorf("%w: %w", errDiskWrite, werr)
|
|
}
|
|
pos += int64(nr)
|
|
f.mu.Lock()
|
|
seg.Downloaded += int64(nr)
|
|
if *ln != nil {
|
|
(*ln).downloaded += int64(nr)
|
|
}
|
|
f.mu.Unlock()
|
|
}
|
|
if rerr != nil {
|
|
if pos > end {
|
|
break
|
|
}
|
|
if errors.Is(rerr, io.EOF) {
|
|
return errShortTransfer
|
|
}
|
|
return rerr
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (f *Fetcher) Patch(req *base.Request, opts *base.Options) error {
|
|
if req == nil {
|
|
return nil
|
|
}
|
|
if req.URL != "" {
|
|
f.meta.Req.URL = req.URL
|
|
}
|
|
if req.Labels != nil {
|
|
if f.meta.Req.Labels == nil {
|
|
f.meta.Req.Labels = make(map[string]string)
|
|
}
|
|
for k, v := range req.Labels {
|
|
f.meta.Req.Labels[k] = v
|
|
}
|
|
}
|
|
if req.Proxy != nil {
|
|
f.meta.Req.Proxy = req.Proxy
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (f *Fetcher) Pause() error {
|
|
f.runMu.Lock()
|
|
defer f.runMu.Unlock()
|
|
if f.cancel != nil {
|
|
f.cancel()
|
|
}
|
|
if f.runDone != nil {
|
|
<-f.runDone
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (f *Fetcher) Close() error {
|
|
return f.Pause()
|
|
}
|
|
|
|
func (f *Fetcher) Meta() *fetcher.FetcherMeta {
|
|
return f.meta
|
|
}
|
|
|
|
// Stats reports one connection per worker that logged in, in the HTTP
|
|
// protocol's shape.
|
|
func (f *Fetcher) Stats() *fetcher.Stats {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
var total int64
|
|
if n := int64(len(f.lanes)); n > 0 && f.meta != nil && f.meta.Res != nil && f.meta.Res.Size > 0 {
|
|
total = (f.meta.Res.Size + n - 1) / n
|
|
}
|
|
conns := make([]*pftp.StatsConnection, 0, len(f.lanes))
|
|
for _, l := range f.lanes {
|
|
conns = append(conns, &pftp.StatsConnection{
|
|
Downloaded: l.downloaded,
|
|
Total: total,
|
|
Completed: l.completed || (f.done && !l.failed),
|
|
Failed: l.failed,
|
|
RetryTimes: l.retryTimes,
|
|
})
|
|
}
|
|
return &fetcher.Stats{Snapshot: &pftp.Stats{Connections: conns}}
|
|
}
|
|
|
|
// Progress returns the bytes downloaded of each selected file, in
|
|
// SelectFiles order.
|
|
func (f *Fetcher) Progress() fetcher.Progress {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
perFile := map[int]int64{}
|
|
for _, s := range f.segments {
|
|
perFile[s.File] += s.Downloaded
|
|
}
|
|
sel := f.selected()
|
|
p := make(fetcher.Progress, len(sel))
|
|
for i, index := range sel {
|
|
p[i] = perFile[index]
|
|
}
|
|
return p
|
|
}
|
|
|
|
func (f *Fetcher) Wait() error {
|
|
return <-f.doneCh
|
|
}
|