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 }