github.com/qjfoidnh/BaiduPCS-Go@v0.0.0-20231011165705-caa18a3765f3/internal/pcsfunctions/pcsdownload/download_task_unit.go (about)

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