github.com/fzfile/BaiduPCS-Go@v0.0.0-20200606205115-4408961cf336/internal/pcsfunctions/pcsdownload/download_task_unit.go (about)

     1  package pcsdownload
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"github.com/fzfile/BaiduPCS-Go/baidupcs"
     7  	"github.com/fzfile/BaiduPCS-Go/baidupcs/pcserror"
     8  	"github.com/fzfile/BaiduPCS-Go/internal/pcsconfig"
     9  	"github.com/fzfile/BaiduPCS-Go/internal/pcsfunctions"
    10  	"github.com/fzfile/BaiduPCS-Go/pcstable"
    11  	"github.com/fzfile/BaiduPCS-Go/pcsutil/converter"
    12  	"github.com/fzfile/BaiduPCS-Go/pcsutil/taskframework"
    13  	"github.com/fzfile/BaiduPCS-Go/pcsverbose"
    14  	"github.com/fzfile/BaiduPCS-Go/requester"
    15  	"github.com/fzfile/BaiduPCS-Go/requester/downloader"
    16  	"github.com/fzfile/BaiduPCS-Go/requester/transfer"
    17  	"io"
    18  	"net/http"
    19  	"os"
    20  	"path/filepath"
    21  	"strconv"
    22  	"strings"
    23  	"time"
    24  )
    25  
    26  type (
    27  	// DownloadMode 下载模式
    28  	DownloadMode int
    29  
    30  	// DownloadTaskUnit 下载的任务单元
    31  	DownloadTaskUnit struct {
    32  		taskInfo *taskframework.TaskInfo // 任务信息
    33  
    34  		Cfg                *downloader.Config
    35  		PCS                *baidupcs.BaiduPCS
    36  		ParentTaskExecutor *taskframework.TaskExecutor
    37  
    38  		DownloadStatistic *DownloadStatistic // 下载统计
    39  
    40  		// 可选项
    41  		VerbosePrinter       *pcsverbose.PCSVerbose
    42  		PrintFormat          string
    43  		IsPrintStatus        bool // 是否输出各个下载线程的详细信息
    44  		IsExecutedPermission bool // 下载成功后是否加上执行权限
    45  		IsOverwrite          bool // 是否覆盖已存在的文件
    46  		NoCheck              bool // 不校验文件
    47  
    48  		DownloadMode DownloadMode // 下载模式
    49  
    50  		PcsPath  string // 要下载的网盘文件路径
    51  		SavePath string // 保存的路径
    52  
    53  		fileInfo *baidupcs.FileDirectory // 文件或目录详情
    54  	}
    55  )
    56  
    57  const (
    58  	// DefaultPrintFormat 默认的下载进度输出格式
    59  	DefaultPrintFormat = "\r[%s] ↓ %s/%s %s/s in %s, left %s ............"
    60  	//DownloadSuffix 文件下载后缀
    61  	DownloadSuffix = ".BaiduPCS-Go-downloading"
    62  	//StrDownloadInitError 初始化下载发生错误
    63  	StrDownloadInitError = "初始化下载发生错误"
    64  	// StrDownloadFailed 下载文件失败
    65  	StrDownloadFailed = "下载文件失败"
    66  	// StrDownloadGetDlinkFailed 获取下载链接失败
    67  	StrDownloadGetDlinkFailed = "获取下载链接失败"
    68  	// StrDownloadChecksumFailed 检测文件有效性失败
    69  	StrDownloadChecksumFailed = "检测文件有效性失败"
    70  	// DefaultDownloadMaxRetry 默认下载失败最大重试次数
    71  	DefaultDownloadMaxRetry = 3
    72  )
    73  
    74  const (
    75  	DownloadModeLocate DownloadMode = iota
    76  	DownloadModePCS
    77  	DownloadModeStreaming
    78  )
    79  
    80  func (dtu *DownloadTaskUnit) SetTaskInfo(info *taskframework.TaskInfo) {
    81  	dtu.taskInfo = info
    82  }
    83  
    84  func (dtu *DownloadTaskUnit) verboseInfof(format string, a ...interface{}) {
    85  	if dtu.VerbosePrinter != nil {
    86  		dtu.VerbosePrinter.Infof(format, a...)
    87  	}
    88  }
    89  
    90  // download 执行下载
    91  func (dtu *DownloadTaskUnit) download(downloadURL string, client *requester.HTTPClient) (err error) {
    92  	var (
    93  		writer downloader.Writer
    94  		file   *os.File
    95  	)
    96  
    97  	if !dtu.Cfg.IsTest {
    98  		// 非测试下载
    99  		dtu.Cfg.InstanceStatePath = dtu.SavePath + DownloadSuffix
   100  
   101  		// 创建下载的目录
   102  		// 获取SavePath所在的目录
   103  		dir := filepath.Dir(dtu.SavePath)
   104  		fileInfo, err := os.Stat(dir)
   105  		if err != nil {
   106  			// 目录不存在, 创建
   107  			err = os.MkdirAll(dir, 0777)
   108  			if err != nil {
   109  				return err
   110  			}
   111  		} else if !fileInfo.IsDir() {
   112  			// SavePath所在的目录不是目录
   113  			return fmt.Errorf("%s, path %s: not a directory", StrDownloadInitError, dir)
   114  		}
   115  
   116  		// 打开文件
   117  		writer, file, err = downloader.NewDownloaderWriterByFilename(dtu.SavePath, os.O_CREATE|os.O_WRONLY, 0666)
   118  		if err != nil {
   119  			return fmt.Errorf("%s, %s", StrDownloadInitError, err)
   120  		}
   121  		defer file.Close()
   122  	}
   123  
   124  	der := downloader.NewDownloader(downloadURL, writer, dtu.Cfg)
   125  	der.SetClient(client)
   126  	der.SetDURLCheckFunc(BaiduPCSURLCheckFunc)
   127  	der.SetStatusCodeBodyCheckFunc(func(respBody io.Reader) error {
   128  		// 返回的错误可能是pcs的json
   129  		// 解析错误
   130  		return pcserror.DecodePCSJSONError(baidupcs.OperationDownloadFile, respBody)
   131  	})
   132  
   133  	// 检查输出格式
   134  	if dtu.PrintFormat == "" {
   135  		dtu.PrintFormat = DefaultPrintFormat
   136  	}
   137  
   138  	// 这里用共享变量的方式
   139  	isComplete := false
   140  	der.OnDownloadStatusEvent(func(status transfer.DownloadStatuser, workersCallback func(downloader.RangeWorkerFunc)) {
   141  		// 这里可能会下载结束了, 还会输出内容
   142  		builder := &strings.Builder{}
   143  		if dtu.IsPrintStatus {
   144  			// 输出所有的worker状态
   145  			var (
   146  				tb      = pcstable.NewTable(builder)
   147  			)
   148  			tb.SetHeader([]string{"#", "status", "range", "left", "speeds", "error"})
   149  			workersCallback(func(key int, worker *downloader.Worker) bool {
   150  				wrange := worker.GetRange()
   151  				tb.Append([]string{fmt.Sprint(worker.ID()), worker.GetStatus().StatusText(), wrange.ShowDetails(), strconv.FormatInt(wrange.Len(), 10), strconv.FormatInt(worker.GetSpeedsPerSecond(), 10), fmt.Sprint(worker.Err())})
   152  				return true
   153  			})
   154  
   155  			// 先空两行
   156  			builder.WriteString("\n\n")
   157  			tb.Render()
   158  		}
   159  
   160  		// 如果下载速度为0, 剩余下载时间未知, 则用 - 代替
   161  		var leftStr string
   162  		left := status.TimeLeft()
   163  		if left < 0 {
   164  			leftStr = "-"
   165  		} else {
   166  			leftStr = left.String()
   167  		}
   168  
   169  		fmt.Fprintf(builder,dtu.PrintFormat, dtu.taskInfo.Id(),
   170  			converter.ConvertFileSize(status.Downloaded(), 2),
   171  			converter.ConvertFileSize(status.TotalSize(), 2),
   172  			converter.ConvertFileSize(status.SpeedsPerSecond(), 2),
   173  			status.TimeElapsed()/1e7*1e7, leftStr,
   174  		)
   175  
   176  		if !isComplete {
   177  			// 如果未完成下载, 就输出
   178  			fmt.Print(builder.String())
   179  		}
   180  	})
   181  
   182  	der.OnExecute(func() {
   183  		if dtu.Cfg.IsTest {
   184  			fmt.Printf("[%s] 测试下载开始\n\n", dtu.taskInfo.Id())
   185  		}
   186  	})
   187  
   188  	err = der.Execute()
   189  	isComplete = true
   190  	fmt.Print("\n")
   191  
   192  	if err != nil {
   193  		// 下载发生错误
   194  		if !dtu.Cfg.IsTest {
   195  			// 下载失败, 删去空文件
   196  			if info, infoErr := file.Stat(); infoErr == nil {
   197  				if info.Size() == 0 {
   198  					// 空文件, 应该删除
   199  					dtu.verboseInfof("[%s] remove empty file: %s\n", dtu.taskInfo.Id(), dtu.SavePath)
   200  					removeErr := os.Remove(dtu.SavePath)
   201  					if removeErr != nil {
   202  						dtu.verboseInfof("[%s] remove file error: %s\n", dtu.taskInfo.Id(), removeErr)
   203  					}
   204  				}
   205  			}
   206  		}
   207  		return err
   208  	}
   209  
   210  	// 下载成功
   211  	if !dtu.Cfg.IsTest {
   212  		if dtu.IsExecutedPermission {
   213  			err = file.Chmod(0766)
   214  			if err != nil {
   215  				fmt.Printf("[%s] 警告, 加执行权限错误: %s\n", dtu.taskInfo.Id(), err)
   216  			}
   217  		}
   218  
   219  		fmt.Printf("[%s] 下载完成, 保存位置: %s\n", dtu.taskInfo.Id(), dtu.SavePath)
   220  	} else {
   221  		fmt.Printf("[%s] 测试下载结束\n", dtu.taskInfo.Id())
   222  	}
   223  
   224  	return nil
   225  }
   226  
   227  //panHTTPClient 获取包含特定User-Agent的HTTPClient
   228  func (dtu *DownloadTaskUnit) panHTTPClient() (client *requester.HTTPClient) {
   229  	client = pcsconfig.Config.PanHTTPClient()
   230  	client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
   231  		// 去掉 Referer
   232  		if !pcsconfig.Config.EnableHTTPS {
   233  			req.Header.Del("Referer")
   234  		}
   235  		if len(via) >= 10 {
   236  			return errors.New("stopped after 10 redirects")
   237  		}
   238  		return nil
   239  	}
   240  	client.SetTimeout(20 * time.Minute)
   241  	client.SetKeepAlive(true)
   242  	return client
   243  }
   244  
   245  func (dtu *DownloadTaskUnit) handleError(result *taskframework.TaskUnitRunResult) {
   246  	switch value := result.Err.(type) {
   247  	case pcserror.Error: // pcserror 接口
   248  		switch value.GetErrType() {
   249  		case pcserror.ErrTypeRemoteError:
   250  		// 远程服务器错误
   251  		case 31045: // user not exists
   252  			fallthrough
   253  		case 31066: // file does not exist
   254  			result.NeedRetry = false
   255  		case 31626: // user is not authorized
   256  			//可能是User-Agent不对
   257  			//重试
   258  			fallthrough
   259  		default:
   260  			result.NeedRetry = true
   261  		}
   262  	case *os.PathError:
   263  		// 系统级别的错误, 可能是权限问题
   264  		result.NeedRetry = false
   265  	default:
   266  		// 其他错误, 需要重试
   267  		result.NeedRetry = true
   268  	}
   269  }
   270  
   271  func (dtu *DownloadTaskUnit) execPanDownload(dlink string, result *taskframework.TaskUnitRunResult, okPtr *bool) {
   272  	dtu.verboseInfof("[%s] 获取到下载链接: %s\n", dtu.taskInfo.Id(), dlink)
   273  
   274  	client := dtu.panHTTPClient()
   275  	err := dtu.download(dlink, client)
   276  	if err != nil {
   277  		result.ResultMessage = StrDownloadFailed
   278  		result.Err = err
   279  		dtu.handleError(result)
   280  		return
   281  	}
   282  	*okPtr = true
   283  }
   284  
   285  func (dtu *DownloadTaskUnit) locateDownload(result *taskframework.TaskUnitRunResult) (ok bool) {
   286  	rawDlinks, err := GetLocateDownloadLinks(dtu.PCS, dtu.PcsPath)
   287  	if err != nil {
   288  		result.ResultMessage = StrDownloadGetDlinkFailed
   289  		result.Err = err
   290  		dtu.handleError(result)
   291  		return
   292  	}
   293  
   294  	// 更新链接的协议
   295  	FixHTTPLinkURL(rawDlinks[0])
   296  	dlink := rawDlinks[0].String()
   297  
   298  	dtu.execPanDownload(dlink, result, &ok)
   299  	return
   300  }
   301  
   302  func (dtu *DownloadTaskUnit) pcsOrStreamingDownload(mode DownloadMode, result *taskframework.TaskUnitRunResult) (ok bool) {
   303  	dfunc := func(downloadURL string, jar http.CookieJar) error {
   304  		client := pcsconfig.Config.PCSHTTPClient()
   305  		client.SetCookiejar(jar)
   306  		client.SetKeepAlive(true)
   307  		client.SetTimeout(10 * time.Minute)
   308  
   309  		return dtu.download(downloadURL, client)
   310  	}
   311  
   312  	var err error
   313  	switch mode {
   314  	case DownloadModePCS:
   315  		err = dtu.PCS.DownloadFile(dtu.PcsPath, dfunc)
   316  	case DownloadModeStreaming:
   317  		err = dtu.PCS.DownloadStreamFile(dtu.PcsPath, dfunc)
   318  	default:
   319  		panic("unreachable")
   320  	}
   321  
   322  	if err != nil {
   323  		result.ResultMessage = StrDownloadFailed
   324  		result.Err = err
   325  		dtu.handleError(result)
   326  		return
   327  	}
   328  	return true // 下载成功
   329  }
   330  
   331  //checkFileValid 检测文件有效性
   332  func (dtu *DownloadTaskUnit) checkFileValid(result *taskframework.TaskUnitRunResult) (ok bool) {
   333  	if dtu.Cfg.IsTest || dtu.NoCheck {
   334  		// 不检测文件有效性
   335  		return
   336  	}
   337  
   338  	if dtu.fileInfo.Size >= 128*converter.MB {
   339  		// 大文件, 输出一句提示消息
   340  		fmt.Printf("[%s] 开始检验文件有效性, 请稍候...\n", dtu.taskInfo.Id())
   341  	}
   342  
   343  	// 就在这里处理校验出错
   344  	err := CheckFileValid(dtu.SavePath, dtu.fileInfo)
   345  	if err != nil {
   346  		result.ResultMessage = StrDownloadChecksumFailed
   347  		result.Err = err
   348  		switch err {
   349  		case ErrDownloadNotSupportChecksum:
   350  			// 文件不支持校验
   351  			result.ResultMessage = "检验文件有效性"
   352  			result.Err = err
   353  			fmt.Printf("[%s] 检验文件有效性: %s\n", dtu.taskInfo.Id(), err)
   354  			return true
   355  		case ErrDownloadFileBanned:
   356  			// 违规文件
   357  			result.NeedRetry = false
   358  			return
   359  		case ErrDownloadChecksumFailed:
   360  			// 校验失败, 需要重新下载
   361  			result.NeedRetry = true
   362  			// 设置允许覆盖
   363  			dtu.IsOverwrite = true
   364  			return
   365  		default:
   366  			result.NeedRetry = false
   367  			return
   368  		}
   369  	}
   370  
   371  	fmt.Printf("[%s] 检验文件有效性成功: %s\n", dtu.taskInfo.Id(), dtu.SavePath)
   372  	return true
   373  }
   374  
   375  func (dtu *DownloadTaskUnit) OnRetry(lastRunResult *taskframework.TaskUnitRunResult) {
   376  	// 输出错误信息
   377  	if lastRunResult.Err == nil {
   378  		// result中不包含Err, 忽略输出
   379  		fmt.Printf("[%s] %s, 重试 %d/%d\n", dtu.taskInfo.Id(), lastRunResult.ResultMessage, dtu.taskInfo.Retry(), dtu.taskInfo.MaxRetry())
   380  		return
   381  	}
   382  	fmt.Printf("[%s] %s, %s, 重试 %d/%d\n", dtu.taskInfo.Id(), lastRunResult.ResultMessage, lastRunResult.Err, dtu.taskInfo.Retry(), dtu.taskInfo.MaxRetry())
   383  }
   384  
   385  func (dtu *DownloadTaskUnit) OnSuccess(lastRunResult *taskframework.TaskUnitRunResult) {
   386  }
   387  
   388  func (dtu *DownloadTaskUnit) OnFailed(lastRunResult *taskframework.TaskUnitRunResult) {
   389  	// 失败
   390  	if lastRunResult.Err == nil {
   391  		// result中不包含Err, 忽略输出
   392  		fmt.Printf("[%s] %s\n", dtu.taskInfo.Id(), lastRunResult.ResultMessage)
   393  		return
   394  	}
   395  	fmt.Printf("[%s] %s, %s\n", dtu.taskInfo.Id(), lastRunResult.ResultMessage, lastRunResult.Err)
   396  }
   397  
   398  func (dtu *DownloadTaskUnit) OnComplete(lastRunResult *taskframework.TaskUnitRunResult) {
   399  }
   400  
   401  func (dtu *DownloadTaskUnit) RetryWait() time.Duration {
   402  	return pcsfunctions.RetryWait(dtu.taskInfo.Retry())
   403  }
   404  
   405  func (dtu *DownloadTaskUnit) Run() (result *taskframework.TaskUnitRunResult) {
   406  	result = &taskframework.TaskUnitRunResult{}
   407  	// 获取文件信息
   408  	var err error
   409  	if dtu.fileInfo == nil || dtu.taskInfo.Retry() > 0 {
   410  		// 没有获取文件信息
   411  		// 如果是动态添加的下载任务, 是会写入文件信息的
   412  		// 如果该任务重试过, 则应该再获取一次文件信息
   413  		dtu.fileInfo, err = dtu.PCS.FilesDirectoriesMeta(dtu.PcsPath)
   414  		if err != nil {
   415  			// 如果不是未登录或文件不存在, 则不重试
   416  			result.ResultMessage = "获取下载路径信息错误"
   417  			result.Err = err
   418  			dtu.handleError(result)
   419  			return
   420  		}
   421  	}
   422  
   423  	// 输出文件信息
   424  	fmt.Print("\n")
   425  	fmt.Printf("[%s] ----\n%s\n", dtu.taskInfo.Id(), dtu.fileInfo.String())
   426  
   427  	// 如果是一个目录, 将子文件和子目录加入队列
   428  	if dtu.fileInfo.Isdir {
   429  		if !dtu.Cfg.IsTest { // 测试下载, 不建立空目录
   430  			os.MkdirAll(dtu.SavePath, 0777) // 首先在本地创建目录, 保证空目录也能被保存
   431  		}
   432  
   433  		// 获取该目录下的文件列表
   434  		fileList, err := dtu.PCS.FilesDirectoriesList(dtu.PcsPath, baidupcs.DefaultOrderOptions)
   435  		if err != nil {
   436  			result.ResultMessage = "获取目录信息错误"
   437  			result.Err = err
   438  			result.NeedRetry = true
   439  			return
   440  		}
   441  
   442  		for k := range fileList {
   443  			// 添加子任务
   444  			subUnit := *dtu
   445  			newCfg := *dtu.Cfg
   446  			subUnit.Cfg = &newCfg
   447  			subUnit.fileInfo = fileList[k] // 保存文件信息
   448  			subUnit.PcsPath = fileList[k].Path
   449  			subUnit.SavePath = filepath.Join(dtu.SavePath, fileList[k].Filename) // 保存位置
   450  
   451  			// 加入父队列
   452  			info := dtu.ParentTaskExecutor.Append(&subUnit, dtu.taskInfo.MaxRetry())
   453  			fmt.Printf("[%s] 加入下载队列: %s\n", info.Id(), fileList[k].Path)
   454  		}
   455  
   456  		result.Succeed = true // 执行成功
   457  		return
   458  	}
   459  
   460  	fmt.Printf("[%s] 准备下载: %s\n", dtu.taskInfo.Id(), dtu.PcsPath)
   461  
   462  	if !dtu.Cfg.IsTest && !dtu.IsOverwrite && FileExist(dtu.SavePath) {
   463  		fmt.Printf("[%s] 文件已经存在: %s, 跳过...\n", dtu.taskInfo.Id(), dtu.SavePath)
   464  		result.Succeed = true // 执行成功
   465  		return
   466  	}
   467  
   468  	if !dtu.Cfg.IsTest {
   469  		// 不是测试下载, 输出下载路径
   470  		fmt.Printf("[%s] 将会下载到路径: %s\n\n", dtu.taskInfo.Id(), dtu.SavePath)
   471  	}
   472  
   473  	var ok bool
   474  	// 获取下载链接
   475  	switch dtu.DownloadMode {
   476  	case DownloadModeLocate:
   477  		ok = dtu.locateDownload(result)
   478  	case DownloadModePCS, DownloadModeStreaming:
   479  		ok = dtu.pcsOrStreamingDownload(dtu.DownloadMode, result)
   480  	}
   481  
   482  	if !ok {
   483  		// 以上执行不成功, 返回
   484  		return result
   485  	}
   486  
   487  	// 检测文件有效性
   488  	ok = dtu.checkFileValid(result)
   489  	if !ok {
   490  		// 校验不成功, 返回结果
   491  		return result
   492  	}
   493  
   494  	// 统计下载
   495  	dtu.DownloadStatistic.AddTotalSize(dtu.fileInfo.Size)
   496  	// 下载成功
   497  	result.Succeed = true
   498  	return
   499  }