...

Source file src/gitlab.com/tslocum/gophast/pkg/download/download.go

Documentation: gitlab.com/tslocum/gophast/pkg/download

     1  package download
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"io"
     7  	"math"
     8  	"net/http"
     9  	"os"
    10  	"path"
    11  	"path/filepath"
    12  	"strconv"
    13  	"strings"
    14  	"sync"
    15  	"time"
    16  
    17  	"github.com/dustin/go-humanize"
    18  	"github.com/pkg/errors"
    19  	"gitlab.com/tslocum/gophast/pkg/config"
    20  	"gitlab.com/tslocum/gophast/pkg/log"
    21  	. "gitlab.com/tslocum/gophast/pkg/utils"
    22  	"gitlab.com/tslocum/preallocate"
    23  )
    24  
    25  const (
    26  	WriteBufferSize           = 64 * 1024        // 64 KiB
    27  	DiskAllocationWarningSize = 24 * 1024 * 1024 // 24 MiB
    28  	ProgressCompletedSize     = 10000
    29  )
    30  
    31  type Download struct {
    32  	ID             int64
    33  	Status         int
    34  	URL            string
    35  	Name           string
    36  	Size           int64
    37  	Remaining      int64
    38  	SupportsRange  bool
    39  	Ranges         []*ByteRange
    40  	Writers        []*DownloadWriter
    41  	ControlWriters []*ControlWriter
    42  	FilePath       string
    43  	Started        time.Time
    44  	Finished       time.Time
    45  	WasPaused      bool
    46  
    47  	file            *os.File
    48  	controlFile     *os.File
    49  	lastWrote       time.Time
    50  	wg              *sync.WaitGroup
    51  	rangeWG         *sync.WaitGroup
    52  	Retries         int
    53  	InProgress      bool
    54  	Paused          bool
    55  	Cancelled       bool
    56  	rangeDownloaded int64
    57  	resume          chan bool
    58  	err             error
    59  
    60  	downloading          bool
    61  	printedDownloadError bool
    62  
    63  	*sync.RWMutex
    64  }
    65  
    66  var (
    67  	NewID            chan int64
    68  	redirections     = make(map[string]string)
    69  	redirectionsLock = &sync.RWMutex{}
    70  )
    71  
    72  func distributeNewDownloadIDs() {
    73  	var id int64
    74  	for {
    75  		id++
    76  		NewID <- id
    77  	}
    78  }
    79  
    80  func NewDownload(url string, postData []byte) (*Download, error) {
    81  	if NewID == nil {
    82  		NewID = make(chan int64)
    83  		go distributeNewDownloadIDs()
    84  	}
    85  
    86  	log.Standard("Fetching metadata...")
    87  	metadata, err := FetchMetadata(url, postData)
    88  	if err != nil {
    89  		return nil, errors.Wrap(err, "failed to fetch metadata")
    90  	}
    91  
    92  	downloadPath := DownloadPath(metadata.Name)
    93  
    94  	waitgroup := new(sync.WaitGroup)
    95  	waitgroup.Add(1)
    96  
    97  	d := &Download{ID: <-NewID, Started: time.Now(), SupportsRange: metadata.SupportsRange, resume: make(chan bool), wg: waitgroup, rangeWG: new(sync.WaitGroup), RWMutex: new(sync.RWMutex)}
    98  	d.Name = filepath.Base(downloadPath)
    99  	d.Size = metadata.Size
   100  	d.URL = url
   101  	d.FilePath = downloadPath
   102  
   103  	return d, nil
   104  }
   105  
   106  func (d *Download) GetStatus() int {
   107  	d.RLock()
   108  	defer d.RUnlock()
   109  
   110  	return d.Status
   111  }
   112  
   113  func (d *Download) GetDownloaded() int64 {
   114  	d.RLock()
   115  	defer d.RUnlock()
   116  
   117  	if d.Size == 0 {
   118  		return 0
   119  	} else if d.Status > 0 {
   120  		return d.Size
   121  	}
   122  
   123  	var downloaded int64
   124  	if d.Remaining > 0 {
   125  		downloaded = d.Size - d.Remaining
   126  	}
   127  	for _, r := range d.Ranges {
   128  		downloaded += r.Wrote
   129  	}
   130  
   131  	return downloaded
   132  }
   133  
   134  func (d *Download) GetVerboseDownloaded() []byte {
   135  	d.RLock()
   136  	defer d.RUnlock()
   137  
   138  	if d.Status > 0 {
   139  		return []byte(strconv.Itoa(int(ProgressCompletedSize)))
   140  	} else if d.Size == 0 || len(d.Ranges) == 0 {
   141  		return []byte(fmt.Sprintf("0,%d", ProgressCompletedSize))
   142  	}
   143  
   144  	var out []byte
   145  	var rangeSize, progressSize, progress, remaining, totalProgress int64
   146  	lastRange := len(d.Ranges) - 1
   147  
   148  	if d.Remaining != d.Size && d.Remaining > 0 {
   149  		progress = (d.Size - d.Remaining) * ProgressCompletedSize / d.Size
   150  
   151  		out = []byte(fmt.Sprintf("%d,%d", progress, 0))
   152  		totalProgress += progress
   153  	}
   154  
   155  	for i, r := range d.Ranges {
   156  		rangeSize = r.End - r.Start + 1
   157  		progressSize = (rangeSize * ProgressCompletedSize / d.Size)
   158  		progress = (r.Progress * ProgressCompletedSize / d.Size)
   159  
   160  		if i == lastRange && r.Progress == rangeSize && totalProgress+progress < ProgressCompletedSize {
   161  			progress = ProgressCompletedSize - totalProgress
   162  
   163  			remaining = 0
   164  		} else {
   165  			remaining = (rangeSize - r.Progress) * ProgressCompletedSize / d.Size
   166  			if i == lastRange && totalProgress+progress+remaining != ProgressCompletedSize {
   167  				remaining = ProgressCompletedSize - (totalProgress + progress)
   168  			}
   169  		}
   170  
   171  		if progress+remaining < progressSize {
   172  			d := progressSize - (progress + remaining)
   173  			if totalProgress+progress+remaining+d < ProgressCompletedSize {
   174  				if remaining > 0 {
   175  					remaining += d
   176  				} else {
   177  					progress += d
   178  				}
   179  			}
   180  		}
   181  
   182  		if out != nil {
   183  			out = append(out, byte(','))
   184  		}
   185  		out = append(out, []byte(fmt.Sprintf("%d,%d", progress, remaining))...)
   186  		totalProgress += progress + remaining
   187  	}
   188  
   189  	return out
   190  }
   191  
   192  func DownloadPath(defaultFileName string) string {
   193  	downloadName := config.C.DownloadName
   194  	if downloadName == "" {
   195  		downloadName = defaultFileName
   196  	}
   197  	if filepath.IsAbs(downloadName) {
   198  		return downloadName
   199  	}
   200  
   201  	downloadDir, err := filepath.Abs(config.C.DownloadDir)
   202  	if err != nil {
   203  		return ""
   204  	}
   205  	return filepath.Join(downloadDir, downloadName)
   206  }
   207  
   208  func FetchMetadata(url string, postData []byte) (*Metadata, error) {
   209  	resp, err := request("HEAD", url, postData, 0, 0)
   210  	if err != nil {
   211  		return nil, err
   212  	}
   213  	defer resp.Body.Close()
   214  
   215  	if resp.StatusCode == 404 {
   216  		return nil, errors.New("file not found (404)")
   217  	} else if resp.StatusCode != 200 {
   218  		return nil, errors.New(fmt.Sprintf("server responded with status %d (expected 200 OK)", resp.StatusCode))
   219  	}
   220  
   221  	// Use file name from final URL
   222  	fileName := path.Base(resp.Request.URL.String())
   223  
   224  	// Extract filename from Content-Disposition when present
   225  	dispositionSplit := strings.Split(resp.Header.Get("Content-Disposition"), ";")
   226  	for _, ds := range dispositionSplit {
   227  		ds = strings.TrimSpace(ds)
   228  
   229  		if !strings.HasPrefix(ds, "filename=") {
   230  			continue
   231  		}
   232  
   233  		ds = ds[9:]
   234  
   235  		if ds[0] == '"' && ds[len(ds)-1] == '"' && len(ds) > 2 {
   236  			ds = ds[1 : len(ds)-1]
   237  		}
   238  
   239  		if path.Base(ds) != "" {
   240  			fileName = path.Base(ds)
   241  
   242  			break
   243  		}
   244  	}
   245  
   246  	supportsRange := resp.ContentLength > 0 && strings.Contains(resp.Header.Get("Accept-Ranges"), "bytes")
   247  	if !supportsRange {
   248  		log.Standardf("%s does not support partial downloads", url)
   249  	}
   250  
   251  	return &Metadata{Name: fileName, Size: resp.ContentLength, SupportsRange: supportsRange}, nil
   252  }
   253  
   254  func request(method string, url string, postData []byte, rangeStart int64, rangeEnd int64) (*http.Response, error) {
   255  	client := &http.Client{}
   256  
   257  	var (
   258  		timeout = time.NewTimer(config.C.ConnectTimeout)
   259  		req     *http.Request
   260  		err     error
   261  	)
   262  
   263  	requestComplete := make(chan bool)
   264  	go func() {
   265  		req, err = http.NewRequest(method, url, bytes.NewReader(postData))
   266  		requestComplete <- true
   267  	}()
   268  
   269  	select {
   270  	case <-requestComplete:
   271  		if err != nil {
   272  			return nil, err
   273  		}
   274  	case <-timeout.C:
   275  		return nil, errors.New("connection timed out")
   276  	}
   277  
   278  	if config.C.UserAgent == "" {
   279  		config.C.SetUserAgent()
   280  	}
   281  	req.Header.Add("User-Agent", config.C.UserAgent)
   282  
   283  	if rangeEnd > 0 {
   284  		req.Header.Add("Range", fmt.Sprintf("bytes=%d-%d", rangeStart, rangeEnd))
   285  	}
   286  
   287  	resp, err := client.Do(req)
   288  	if err == nil {
   289  		// Request URL is updated to the final URL after redirecting
   290  		printRedirectionWarning(url, resp.Request.URL.String())
   291  	}
   292  
   293  	return resp, err
   294  }
   295  
   296  func (d *Download) downloadRange(postData []byte, r *ByteRange, id int, maxid int, w *DownloadWriter) {
   297  	var (
   298  		done      bool
   299  		wrote     int64
   300  		start     = time.Now()
   301  		rangeSize = r.End - r.Start + 1
   302  	)
   303  
   304  	defer func() {
   305  		done = true
   306  		d.rangeWG.Done()
   307  	}()
   308  
   309  	rangeStart := r.Start
   310  	rangeEnd := r.End
   311  	expectedStatusCode := http.StatusPartialContent
   312  	if rangeStart == 0 && rangeEnd == d.Size-1 {
   313  		// Non-range download
   314  		rangeEnd = 0
   315  		expectedStatusCode = http.StatusOK
   316  	}
   317  
   318  	resp, err := request("GET", d.URL, postData, rangeStart, rangeEnd)
   319  	if err != nil {
   320  		d.Status = -1
   321  		log.Verbosef("Range %d failed to connect", id+1)
   322  		d.err = errors.Wrap(err, "file transfer interrupted")
   323  		return
   324  	}
   325  	defer resp.Body.Close()
   326  
   327  	if resp.StatusCode != expectedStatusCode {
   328  		if resp.StatusCode == http.StatusServiceUnavailable {
   329  			d.Status = -1
   330  		} else {
   331  			d.Cancel()
   332  		}
   333  		d.err = errors.Errorf("server sent response code: %d (expected: %d)", resp.StatusCode, expectedStatusCode)
   334  		return
   335  	}
   336  
   337  	go func() {
   338  		for {
   339  			d.Lock()
   340  			if config.C.TransferTimeout == 0 || d.Cancelled || d.Status > 0 || done {
   341  				d.Unlock()
   342  				return
   343  			}
   344  
   345  			if d.downloading {
   346  				if r.LastWrote.After(d.lastWrote) {
   347  					d.lastWrote = r.LastWrote
   348  				}
   349  
   350  				if time.Now().Sub(d.lastWrote) >= config.C.TransferTimeout {
   351  					d.Status = -1
   352  					log.Verbosef("Range %d interrupted", id+1)
   353  					d.err = errors.Wrap(errors.New("timed out"), "file transfer interrupted")
   354  					d.Unlock()
   355  					return
   356  				}
   357  			}
   358  			d.Unlock()
   359  
   360  			time.Sleep(config.C.TransferTimeout)
   361  		}
   362  	}()
   363  
   364  	wrote, err = io.CopyBuffer(w, resp.Body, make([]byte, WriteBufferSize))
   365  	if err != nil {
   366  		if !d.Paused && !d.Cancelled && err.Error() != "cancelled" && !strings.Contains(err.Error(), "failed to write to disk") {
   367  			d.Lock()
   368  			log.Verbosef("Range %d interrupted", id+1)
   369  			d.Status = -1
   370  			d.err = errors.Wrap(err, "file transfer interrupted")
   371  			d.Unlock()
   372  		}
   373  
   374  		return
   375  	}
   376  	if wrote != rangeSize {
   377  		log.Verbosef("Range %d interrupted", id+1)
   378  		d.Lock()
   379  		d.Status = -1
   380  		d.err = errors.New("file transfer interrupted")
   381  		d.Unlock()
   382  		return
   383  	}
   384  
   385  	log.Verbosef("Finished range %d/%d (%s in %s at %s/s)", id+1, maxid, humanize.Bytes(uint64(rangeSize)), FormatDuration(time.Since(start)), humanize.Bytes(uint64(float64(float64(rangeSize))/time.Since(start).Seconds())))
   386  
   387  	d.Lock()
   388  	d.rangeDownloaded += rangeSize
   389  	d.Unlock()
   390  }
   391  
   392  func (d *Download) Download(postData []byte) error {
   393  	defer d.wg.Done()
   394  
   395  	d.Lock()
   396  	if d.InProgress {
   397  		d.Unlock()
   398  		return errors.New("download already in progress")
   399  	} else if d.Cancelled {
   400  		d.Unlock()
   401  		return errors.New("download cancelled")
   402  	}
   403  	d.lastWrote = time.Now()
   404  	d.InProgress = true
   405  	d.downloading = true
   406  	d.Unlock()
   407  
   408  	var (
   409  		resumeDownload bool
   410  		fileName       = filepath.Base(d.FilePath)
   411  		existingSize   int64
   412  		control        *ControlFile
   413  		err            error
   414  	)
   415  	d.Started = time.Now()
   416  
   417  	if config.C.Resume {
   418  		if !d.WasPaused {
   419  			_, err := os.Stat(d.FilePath + ".gophast")
   420  			if err == nil || os.IsExist(err) {
   421  				resumeDownload = true
   422  
   423  				control, err = ParseControlFile(d.FilePath + ".gophast")
   424  				if err != nil {
   425  					d.Status = -1
   426  					return errors.Wrap(err, "failed to read control file")
   427  				} else if control != nil && len(control.URLs) > 0 && len(control.Ranges) > 0 {
   428  					resumeDownload = true
   429  
   430  					d.Ranges = control.Ranges
   431  					d.resetRanges()
   432  				}
   433  			}
   434  
   435  			if ex, err := os.Stat(d.FilePath); err == nil {
   436  				existingSize = ex.Size()
   437  
   438  				if !config.C.Force {
   439  					if !resumeDownload && existingSize == d.Size {
   440  						d.Status = 2
   441  						return errors.New("already downloaded (use --force to download anyway)")
   442  					} else if resumeDownload && existingSize > 0 && existingSize != d.Size {
   443  						d.Cancel()
   444  						return errors.New("failed to resume: existing file does not match expected size (use --force to download anyway)")
   445  					}
   446  				}
   447  			} else if !os.IsNotExist(err) {
   448  				d.Cancel()
   449  				return errors.Errorf("failed to check file status: %v", err)
   450  			}
   451  		} else {
   452  			resumeDownload = true
   453  			d.Paused = false
   454  
   455  			d.Writers = nil
   456  			d.ControlWriters = nil
   457  			d.Remaining = 0
   458  			d.rangeDownloaded = 0
   459  			existingSize = d.Size
   460  
   461  			d.resetRanges()
   462  		}
   463  	}
   464  
   465  	d.file, err = os.OpenFile(d.FilePath, os.O_CREATE|os.O_WRONLY, 0644)
   466  	if err != nil {
   467  		d.Cancel()
   468  		return errors.Wrap(err, "failed to open/create file")
   469  	}
   470  
   471  	if existingSize != d.Size {
   472  		if existingSize < d.Size && !resumeDownload {
   473  			if d.Size >= DiskAllocationWarningSize {
   474  				log.Standard("Allocating disk space...")
   475  			}
   476  			err = preallocate.File(d.file, d.Size)
   477  		} else if existingSize > d.Size {
   478  			err = d.file.Truncate(d.Size)
   479  		}
   480  		if err != nil {
   481  			_ = d.file.Close()
   482  			d.Cancel()
   483  			return errors.Wrap(err, "failed to resize file")
   484  		}
   485  	}
   486  
   487  	if d.Size == 0 {
   488  		log.Standardf("%s downloaded", d.Name)
   489  
   490  		_ = d.file.Close()
   491  		d.Status = 1
   492  		return nil
   493  	}
   494  
   495  	if d.SupportsRange {
   496  		if len(d.Ranges) == 0 { // New download
   497  			d.Remaining = d.Size
   498  
   499  			pieceCount, pieceSize := d.pieceCountAndSize()
   500  
   501  			var (
   502  				rangeStart, rangeEnd int64
   503  				r                    *ByteRange
   504  			)
   505  			for i := int64(1); i <= pieceCount; i++ {
   506  				if i < pieceCount {
   507  					rangeEnd += pieceSize
   508  				} else {
   509  					rangeEnd = d.Size - 1
   510  				}
   511  
   512  				r = NewByteRange(rangeStart, rangeEnd, 0)
   513  
   514  				d.Ranges = append(d.Ranges, r)
   515  
   516  				rangeStart = rangeEnd + 1
   517  			}
   518  		} else { // Resume from control file
   519  			for _, r := range d.Ranges {
   520  				d.Remaining += r.End - (r.Start + r.Wrote) + 1
   521  			}
   522  		}
   523  	} else { // Non-range download
   524  		d.Remaining = d.Size
   525  
   526  		r := NewByteRange(0, d.Size-1, 0)
   527  		d.Ranges = []*ByteRange{r}
   528  	}
   529  	pieceCount := len(d.Ranges)
   530  
   531  	if d.Remaining == 0 && !config.C.Force {
   532  		_ = d.file.Close()
   533  		d.Status = 2
   534  		return errors.New("already downloaded (use --force to download anyway)")
   535  	}
   536  
   537  	logMessageLabel := "Downloading"
   538  	logMessageSize := humanize.Bytes(uint64(d.Size))
   539  	if resumeDownload && d.Remaining > 0 && d.Remaining < d.Size {
   540  		logMessageLabel = "Resuming"
   541  		logMessageSize = fmt.Sprintf("%.0f%% - %s / %s", math.Floor(float64(d.Size-d.Remaining)/float64(d.Size)*100), humanize.Bytes(uint64(d.Size-d.Remaining)), logMessageSize)
   542  	}
   543  	log.Standardf("%s %s... (%s)", logMessageLabel, d.Name, logMessageSize)
   544  
   545  	if config.C.ProgressLevel == config.ProgressDynamic && pieceCount > 1 {
   546  		log.ForcePrint("")
   547  	}
   548  
   549  	var (
   550  		controlWrote  int
   551  		controlCursor int
   552  
   553  		cw *ControlWriter
   554  	)
   555  	if config.C.Resume {
   556  		d.controlFile, err = os.OpenFile(d.FilePath+".gophast", os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)
   557  		if err != nil {
   558  			d.Status = -1
   559  			return errors.Errorf("failed to open/create control file: %v (use --no-resume to download anyway)", err)
   560  		}
   561  
   562  		controlWrote, _ = d.controlFile.WriteString(d.URL + "\n")
   563  		controlCursor += controlWrote
   564  	}
   565  
   566  	config.C.TaskFormat = "Range %" + strconv.Itoa(len(fmt.Sprintf("%d", pieceCount))) + "d"
   567  	config.C.TaskWidth = int(math.Max(float64(len(Ellipsize(fileName))), float64(len(fmt.Sprintf(config.C.TaskFormat, pieceCount)))))
   568  
   569  	var (
   570  		controlLine           string
   571  		controlLineMaxSizeLen int
   572  	)
   573  	for i, r := range d.Ranges {
   574  		if r.Wrote == r.End-r.Start+1 {
   575  			continue
   576  		}
   577  
   578  		log.Verbosef("Starting range %d/%d %s", i+1, pieceCount, r)
   579  
   580  		if d.Retries == 0 {
   581  			AddLocalProgressBar(i, pieceCount, r)
   582  		}
   583  
   584  		controlLineMaxSizeLen = len(fmt.Sprintf("%d", r.End-r.Start))
   585  
   586  		if config.C.Resume {
   587  			controlLine = fmt.Sprintf("%d,%d,%0."+strconv.Itoa(controlLineMaxSizeLen)+"d\n", r.Start, r.End, 0)
   588  
   589  			cw = NewControlWriter(d.controlFile, int64(controlCursor+strings.LastIndex(controlLine, ",")+1))
   590  			d.ControlWriters = append(d.ControlWriters, cw)
   591  
   592  			controlWrote, _ = d.controlFile.WriteString(controlLine)
   593  			controlCursor += controlWrote
   594  		}
   595  
   596  		go d.handleWriteControlFile(cw, r, controlLineMaxSizeLen)
   597  
   598  		w := NewDownloadWriter(d.file, r)
   599  		d.Writers = append(d.Writers, w)
   600  
   601  		d.rangeWG.Add(1)
   602  		go d.downloadRange(postData, r, i, pieceCount, w)
   603  	}
   604  
   605  	if d.Retries == 0 {
   606  		AddGlobalProgressBar(d)
   607  	}
   608  
   609  	// Wait for ranges to complete (or encounter an error)
   610  	d.rangeWG.Wait()
   611  
   612  	d.Lock()
   613  
   614  	d.downloading = false
   615  	if d.Remaining > 0 && d.rangeDownloaded == d.Remaining {
   616  		d.Status = 1
   617  	}
   618  
   619  	var retry bool
   620  	if d.Status <= 0 && !d.Cancelled && (d.err == nil || strings.Contains(d.err.Error(), "file transfer interrupted")) && (d.Paused || config.C.MaxRetries == 0 || d.Retries < config.C.MaxRetries) {
   621  		retry = true
   622  	}
   623  
   624  	d.Unlock()
   625  
   626  	_ = d.file.Close()
   627  
   628  	if config.C.Resume {
   629  		if d.GetStatus() <= 0 {
   630  			d.cancelWriters()
   631  			_ = d.controlFile.Close()
   632  		} else {
   633  			_ = d.controlFile.Close()
   634  			_ = os.Remove(d.FilePath + ".gophast")
   635  		}
   636  	}
   637  
   638  	if retry {
   639  		d.Lock()
   640  		d.Retries++
   641  		d.Unlock()
   642  
   643  		if !d.Paused {
   644  			retryDelay := ""
   645  			if config.C.RetryDelay > 0 {
   646  				retryDelay = fmt.Sprintf(" in %s", config.C.RetryDelay)
   647  			}
   648  			log.Verbosef("Download interrupted, retrying%s...", retryDelay)
   649  			time.Sleep(config.C.RetryDelay)
   650  		} else {
   651  			<-d.resume
   652  		}
   653  
   654  		d.Lock()
   655  		if !d.Cancelled {
   656  			d.InProgress = false
   657  			d.Unlock()
   658  			return d.Download(postData)
   659  		} else {
   660  			d.Unlock()
   661  			return nil
   662  		}
   663  	} else if d.GetStatus() > 0 {
   664  		d.Finished = time.Now()
   665  		log.Verbosef("%s downloaded (%s in %s at %s/s)", Ellipsize(d.Name), humanize.Bytes(uint64(d.Remaining)), FormatDuration(time.Since(d.Started)), humanize.Bytes(uint64(float64(float64(d.Remaining))/time.Since(d.Started).Seconds())))
   666  
   667  		return nil
   668  	} else {
   669  		d.Cancel()
   670  		return d.err
   671  	}
   672  }
   673  
   674  func (d *Download) handleWriteControlFile(cw *ControlWriter, r *ByteRange, maxsizelen int) {
   675  	if cw == nil {
   676  		return
   677  	}
   678  
   679  	var autoSave <-chan time.Time
   680  	if config.C.AutoSaveControl > 0 {
   681  		ticker := time.NewTicker(config.C.AutoSaveControl)
   682  		autoSave = ticker.C
   683  	}
   684  
   685  	var (
   686  		format       = "%0." + strconv.Itoa(maxsizelen) + "d"
   687  		pbytes       []byte
   688  		lastProgress int64
   689  		cancelled    bool
   690  		rangeSize    = r.End - r.Start + 1
   691  	)
   692  	for {
   693  		select {
   694  		case <-cw.Cancelled:
   695  			cancelled = true
   696  		case <-autoSave:
   697  		}
   698  
   699  		if r.Wrote != lastProgress {
   700  			pbytes = []byte(fmt.Sprintf(format, r.Wrote))
   701  			ReverseBytes(pbytes)
   702  
   703  			_, err := cw.Write(pbytes)
   704  			if err != nil || d.GetStatus() > 0 || r.Wrote == rangeSize {
   705  				return
   706  			}
   707  
   708  			lastProgress = r.Wrote
   709  		}
   710  
   711  		if cancelled {
   712  			return
   713  		}
   714  	}
   715  }
   716  
   717  func (d *Download) Pause() {
   718  	d.Lock()
   719  
   720  	if d.Paused {
   721  		d.Unlock()
   722  		return
   723  	}
   724  
   725  	d.Paused = true
   726  	d.WasPaused = true
   727  
   728  	d.Unlock()
   729  
   730  	log.Verbosef("Paused %s", d.Name)
   731  
   732  	d.cancelWriters()
   733  }
   734  
   735  func (d *Download) Resume() {
   736  	d.Lock()
   737  	defer d.Unlock()
   738  	if !d.Paused {
   739  		return
   740  	}
   741  
   742  	log.Verbosef("Resuming %s", d.Name)
   743  
   744  	d.Paused = false
   745  	go func(d *Download) {
   746  		d.resume <- true
   747  	}(d)
   748  }
   749  
   750  func (d *Download) Cancel() {
   751  	d.Lock()
   752  
   753  	if d.Cancelled || d.Status > 0 {
   754  		d.Unlock()
   755  		return
   756  	}
   757  
   758  	d.Status = -7
   759  	d.Cancelled = true
   760  	if d.Paused {
   761  		d.Paused = false
   762  	}
   763  
   764  	d.Unlock()
   765  
   766  	d.cancelWriters()
   767  }
   768  
   769  func (d *Download) cancelWriters() {
   770  	d.Lock()
   771  	defer d.Unlock()
   772  
   773  	for _, w := range d.Writers {
   774  		w.Cancelled = true
   775  	}
   776  	d.Writers = nil
   777  
   778  	var cancelledAll bool
   779  	for {
   780  		cancelledAll = true
   781  
   782  		for _, cw := range d.ControlWriters {
   783  			select {
   784  			case cw.Cancelled <- true:
   785  				cancelledAll = false
   786  			default:
   787  			}
   788  		}
   789  
   790  		if cancelledAll {
   791  			d.ControlWriters = nil
   792  
   793  			return
   794  		}
   795  
   796  		time.Sleep(5 * time.Millisecond)
   797  	}
   798  }
   799  
   800  func (d *Download) resetRanges() {
   801  	for _, r := range d.Ranges {
   802  		if r.Wrote > 0 {
   803  			r.Start += r.Wrote
   804  
   805  			r.Wrote = 0
   806  		}
   807  	}
   808  }
   809  
   810  func (d *Download) remainingRanges() int {
   811  	var remainingRanges int
   812  
   813  	for _, r := range d.Ranges {
   814  		if r.Wrote < (r.End-r.Start)+1 {
   815  			remainingRanges++
   816  		}
   817  	}
   818  
   819  	return remainingRanges
   820  }
   821  
   822  func (d *Download) Wait() {
   823  	if d.wg == nil {
   824  		return
   825  	}
   826  
   827  	d.wg.Wait()
   828  }
   829  
   830  func (d *Download) pieceCountAndSize() (int64, int64) {
   831  	pieceCount := int64(1)
   832  
   833  	if config.C.MinSplitSize == 0 {
   834  		if config.C.MaxConnections > 0 {
   835  			pieceCount = config.C.MaxConnections
   836  		}
   837  	} else if d.Size >= (config.C.MinSplitSize * 2) {
   838  		pieceCount = int64(d.Size / config.C.MinSplitSize)
   839  		if pieceCount > config.C.MaxConnections {
   840  			pieceCount = config.C.MaxConnections
   841  		}
   842  	}
   843  	if pieceCount > d.Size {
   844  		pieceCount = d.Size
   845  	}
   846  
   847  	return pieceCount, int64(d.Size / pieceCount)
   848  }
   849  
   850  func printRedirectionWarning(original, final string) {
   851  	if original == final {
   852  		return
   853  	}
   854  
   855  	redirectionsLock.RLock()
   856  	if destination, ok := redirections[original]; ok {
   857  		if destination == final {
   858  			redirectionsLock.RUnlock()
   859  			return
   860  		}
   861  	}
   862  	redirectionsLock.RUnlock()
   863  
   864  	redirectionsLock.Lock()
   865  	redirections[original] = final
   866  	log.Verbosef("Redirected from %s to %s", original, final)
   867  	redirectionsLock.Unlock()
   868  }
   869  

View as plain text