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 }