mirror of
https://gitee.com/zhangsken/aliyunpan.git
synced 2026-10-16 02:23:49 +08:00
add -ui option for download
This commit is contained in:
@@ -25,7 +25,6 @@ import (
|
|||||||
"github.com/tickstep/aliyunpan/internal/functions/pandownload"
|
"github.com/tickstep/aliyunpan/internal/functions/pandownload"
|
||||||
"github.com/tickstep/aliyunpan/internal/global"
|
"github.com/tickstep/aliyunpan/internal/global"
|
||||||
"github.com/tickstep/aliyunpan/internal/taskframework"
|
"github.com/tickstep/aliyunpan/internal/taskframework"
|
||||||
"github.com/tickstep/aliyunpan/internal/ui"
|
|
||||||
"github.com/tickstep/aliyunpan/internal/utils"
|
"github.com/tickstep/aliyunpan/internal/utils"
|
||||||
"github.com/tickstep/aliyunpan/library/requester/transfer"
|
"github.com/tickstep/aliyunpan/library/requester/transfer"
|
||||||
"github.com/tickstep/library-go/converter"
|
"github.com/tickstep/library-go/converter"
|
||||||
@@ -281,6 +280,8 @@ func RunShareAlbumDownloadFile(albumNames []string, options *DownloadOptions) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fmt.Printf("\n[0] 当前文件下载最大并发量为: %d, 下载缓存为: %s\n\n", options.Parallel, converter.ConvertFileSize(int64(cfg.CacheSize), 2))
|
||||||
|
|
||||||
var (
|
var (
|
||||||
panClient = activeUser.PanClient()
|
panClient = activeUser.PanClient()
|
||||||
)
|
)
|
||||||
@@ -299,29 +300,10 @@ func RunShareAlbumDownloadFile(albumNames []string, options *DownloadOptions) {
|
|||||||
// 全局速度统计
|
// 全局速度统计
|
||||||
globalSpeedsStat := &speeds.Speeds{}
|
globalSpeedsStat := &speeds.Speeds{}
|
||||||
|
|
||||||
var dashboard *ui.DownloadDashboard
|
|
||||||
if options.ShowProgress && !options.IsPrintStatus && ui.IsTerminal(os.Stdout) {
|
|
||||||
dashboard = ui.NewDownloadDashboard(cfg.MaxParallel, globalSpeedsStat, &ui.DownloadDashboardOptions{
|
|
||||||
Title: "AliyunPan CLI - 下载中心",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
logf := func(format string, a ...interface{}) {
|
|
||||||
if dashboard != nil {
|
|
||||||
dashboard.Logf(format, a...)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fmt.Printf(format, a...)
|
|
||||||
}
|
|
||||||
if dashboard != nil {
|
|
||||||
dashboard.Logf("[0] 当前文件下载最大并发量为: %d, 下载缓存为: %s", options.Parallel, converter.ConvertFileSize(int64(cfg.CacheSize), 2))
|
|
||||||
} else {
|
|
||||||
fmt.Printf("\n[0] 当前文件下载最大并发量为: %d, 下载缓存为: %s\n\n", options.Parallel, converter.ConvertFileSize(int64(cfg.CacheSize), 2))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理队列
|
// 处理队列
|
||||||
allShareAlbumList, err := activeUser.PanClient().OpenapiPanClient().ShareAlbumListGetAll()
|
allShareAlbumList, err := activeUser.PanClient().OpenapiPanClient().ShareAlbumListGetAll()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logf("获取相簿列表失败: %s\n", err)
|
fmt.Printf("获取相簿列表失败: %s\n", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for k := range albumNames {
|
for k := range albumNames {
|
||||||
@@ -341,11 +323,11 @@ func RunShareAlbumDownloadFile(albumNames []string, options *DownloadOptions) {
|
|||||||
Limit: 100,
|
Limit: 100,
|
||||||
})
|
})
|
||||||
if er != nil {
|
if er != nil {
|
||||||
logf("获取相簿文件出错,请稍后重试: %s\n", albumNames[k])
|
fmt.Printf("获取相簿文件出错,请稍后重试: %s\n", albumNames[k])
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if fileList == nil || len(fileList) == 0 {
|
if fileList == nil || len(fileList) == 0 {
|
||||||
logf("相簿里面没有文件: %s\n", albumNames[k])
|
fmt.Printf("相簿里面没有文件: %s\n", albumNames[k])
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -368,7 +350,7 @@ func RunShareAlbumDownloadFile(albumNames []string, options *DownloadOptions) {
|
|||||||
})
|
})
|
||||||
if apierr != nil {
|
if apierr != nil {
|
||||||
logger.Verbosef("ERROR: get album file download url error: %s\n", f.FileId)
|
logger.Verbosef("ERROR: get album file download url error: %s\n", f.FileId)
|
||||||
logf("下载照片失败: %s\n", f.FileName)
|
fmt.Printf("\n下载照片失败: %s\n", f.FileName)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if durl.StreamsUrl != nil { // 实况图片(照片+视频)下载链接
|
if durl.StreamsUrl != nil { // 实况图片(照片+视频)下载链接
|
||||||
@@ -422,7 +404,6 @@ func RunShareAlbumDownloadFile(albumNames []string, options *DownloadOptions) {
|
|||||||
DriveId: f.DriveId, // 一个相簿的文件会来自多个网盘(资源库/备份盘)
|
DriveId: f.DriveId, // 一个相簿的文件会来自多个网盘(资源库/备份盘)
|
||||||
GlobalSpeedsStat: globalSpeedsStat,
|
GlobalSpeedsStat: globalSpeedsStat,
|
||||||
FileRecorder: nil,
|
FileRecorder: nil,
|
||||||
UI: dashboard,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置相簿文件信息
|
// 设置相簿文件信息
|
||||||
@@ -438,10 +419,7 @@ func RunShareAlbumDownloadFile(albumNames []string, options *DownloadOptions) {
|
|||||||
unit.SavePath = GetActiveUser().GetSavePath(f.Path)
|
unit.SavePath = GetActiveUser().GetSavePath(f.Path)
|
||||||
}
|
}
|
||||||
info := executor.Append(&unit, options.MaxRetry)
|
info := executor.Append(&unit, options.MaxRetry)
|
||||||
if dashboard != nil {
|
fmt.Printf("[%s] 加入下载队列: %s\n", info.Id(), f.Path)
|
||||||
dashboard.RegisterTask(info.Id(), f.Path, f.FileSize, f.IsFile())
|
|
||||||
}
|
|
||||||
logf("[%s] 加入下载队列: %s\n", info.Id(), f.Path)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -449,13 +427,7 @@ func RunShareAlbumDownloadFile(albumNames []string, options *DownloadOptions) {
|
|||||||
statistic.StartTimer()
|
statistic.StartTimer()
|
||||||
|
|
||||||
// 开始执行
|
// 开始执行
|
||||||
if dashboard != nil {
|
|
||||||
dashboard.Start()
|
|
||||||
}
|
|
||||||
executor.Execute()
|
executor.Execute()
|
||||||
if dashboard != nil {
|
|
||||||
dashboard.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("\n下载结束, 时间: %s, 数据总量: %s\n", utils.ConvertTime(statistic.Elapsed()), converter.ConvertFileSize(statistic.TotalSize(), 2))
|
fmt.Printf("\n下载结束, 时间: %s, 数据总量: %s\n", utils.ConvertTime(statistic.Elapsed()), converter.ConvertFileSize(statistic.TotalSize(), 2))
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ import (
|
|||||||
"github.com/tickstep/aliyunpan/internal/functions/pandownload"
|
"github.com/tickstep/aliyunpan/internal/functions/pandownload"
|
||||||
"github.com/tickstep/aliyunpan/internal/functions/panlogin"
|
"github.com/tickstep/aliyunpan/internal/functions/panlogin"
|
||||||
"github.com/tickstep/aliyunpan/internal/taskframework"
|
"github.com/tickstep/aliyunpan/internal/taskframework"
|
||||||
"github.com/tickstep/aliyunpan/internal/ui"
|
|
||||||
"github.com/tickstep/aliyunpan/internal/utils"
|
"github.com/tickstep/aliyunpan/internal/utils"
|
||||||
"github.com/tickstep/aliyunpan/library/requester/transfer"
|
"github.com/tickstep/aliyunpan/library/requester/transfer"
|
||||||
"github.com/tickstep/library-go/converter"
|
"github.com/tickstep/library-go/converter"
|
||||||
@@ -689,6 +688,8 @@ func RunAlbumDownloadFile(albumNames []string, options *DownloadOptions) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fmt.Printf("\n[0] 当前文件下载最大并发量为: %d, 下载缓存为: %s\n\n", options.Parallel, converter.ConvertFileSize(int64(cfg.CacheSize), 2))
|
||||||
|
|
||||||
var (
|
var (
|
||||||
panClient = activeUser.PanClient()
|
panClient = activeUser.PanClient()
|
||||||
)
|
)
|
||||||
@@ -706,25 +707,6 @@ func RunAlbumDownloadFile(albumNames []string, options *DownloadOptions) {
|
|||||||
// 全局速度统计
|
// 全局速度统计
|
||||||
globalSpeedsStat := &speeds.Speeds{}
|
globalSpeedsStat := &speeds.Speeds{}
|
||||||
|
|
||||||
var dashboard *ui.DownloadDashboard
|
|
||||||
if options.ShowProgress && !options.IsPrintStatus && ui.IsTerminal(os.Stdout) {
|
|
||||||
dashboard = ui.NewDownloadDashboard(cfg.MaxParallel, globalSpeedsStat, &ui.DownloadDashboardOptions{
|
|
||||||
Title: "AliyunPan CLI - 下载中心",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
logf := func(format string, a ...interface{}) {
|
|
||||||
if dashboard != nil {
|
|
||||||
dashboard.Logf(format, a...)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fmt.Printf(format, a...)
|
|
||||||
}
|
|
||||||
if dashboard != nil {
|
|
||||||
dashboard.Logf("[0] 当前文件下载最大并发量为: %d, 下载缓存为: %s", options.Parallel, converter.ConvertFileSize(int64(cfg.CacheSize), 2))
|
|
||||||
} else {
|
|
||||||
fmt.Printf("\n[0] 当前文件下载最大并发量为: %d, 下载缓存为: %s\n\n", options.Parallel, converter.ConvertFileSize(int64(cfg.CacheSize), 2))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理队列
|
// 处理队列
|
||||||
for k := range albumNames {
|
for k := range albumNames {
|
||||||
record := getAlbumFromName(activeUser, albumNames[k])
|
record := getAlbumFromName(activeUser, albumNames[k])
|
||||||
@@ -736,11 +718,11 @@ func RunAlbumDownloadFile(albumNames []string, options *DownloadOptions) {
|
|||||||
AlbumId: record.AlbumId,
|
AlbumId: record.AlbumId,
|
||||||
})
|
})
|
||||||
if er != nil {
|
if er != nil {
|
||||||
logf("获取相簿文件出错,请稍后重试: %s\n", albumNames[k])
|
fmt.Printf("获取相簿文件出错,请稍后重试: %s\n", albumNames[k])
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if fileList == nil || len(fileList) == 0 {
|
if fileList == nil || len(fileList) == 0 {
|
||||||
logf("相簿里面没有文件: %s\n", albumNames[k])
|
fmt.Printf("相簿里面没有文件: %s\n", albumNames[k])
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for _, f := range fileList {
|
for _, f := range fileList {
|
||||||
@@ -764,7 +746,6 @@ func RunAlbumDownloadFile(albumNames []string, options *DownloadOptions) {
|
|||||||
DriveId: f.DriveId, // 必须使用文件的DriveId,因为一个相簿的文件会来自多个网盘(资源库/备份盘)
|
DriveId: f.DriveId, // 必须使用文件的DriveId,因为一个相簿的文件会来自多个网盘(资源库/备份盘)
|
||||||
GlobalSpeedsStat: globalSpeedsStat,
|
GlobalSpeedsStat: globalSpeedsStat,
|
||||||
FileRecorder: nil,
|
FileRecorder: nil,
|
||||||
UI: dashboard,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: 相册下载需要重构
|
// TODO: 相册下载需要重构
|
||||||
@@ -781,10 +762,7 @@ func RunAlbumDownloadFile(albumNames []string, options *DownloadOptions) {
|
|||||||
unit.SavePath = GetActiveUser().GetSavePath(f.Path)
|
unit.SavePath = GetActiveUser().GetSavePath(f.Path)
|
||||||
}
|
}
|
||||||
info := executor.Append(&unit, options.MaxRetry)
|
info := executor.Append(&unit, options.MaxRetry)
|
||||||
if dashboard != nil {
|
fmt.Printf("[%s] 加入下载队列: %s\n", info.Id(), f.Path)
|
||||||
dashboard.RegisterTask(info.Id(), f.Path, f.FileSize, f.IsFile())
|
|
||||||
}
|
|
||||||
logf("[%s] 加入下载队列: %s\n", info.Id(), f.Path)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -792,13 +770,7 @@ func RunAlbumDownloadFile(albumNames []string, options *DownloadOptions) {
|
|||||||
statistic.StartTimer()
|
statistic.StartTimer()
|
||||||
|
|
||||||
// 开始执行
|
// 开始执行
|
||||||
if dashboard != nil {
|
|
||||||
dashboard.Start()
|
|
||||||
}
|
|
||||||
executor.Execute()
|
executor.Execute()
|
||||||
if dashboard != nil {
|
|
||||||
dashboard.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("\n下载结束, 时间: %s, 数据总量: %s\n", utils.ConvertTime(statistic.Elapsed()), converter.ConvertFileSize(statistic.TotalSize(), 2))
|
fmt.Printf("\n下载结束, 时间: %s, 数据总量: %s\n", utils.ConvertTime(statistic.Elapsed()), converter.ConvertFileSize(statistic.TotalSize(), 2))
|
||||||
|
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ type (
|
|||||||
DriveId string
|
DriveId string
|
||||||
ExcludeNames []string // 排除的文件名,包括文件夹和文件。即这些文件/文件夹不进行下载,支持正则表达式
|
ExcludeNames []string // 排除的文件名,包括文件夹和文件。即这些文件/文件夹不进行下载,支持正则表达式
|
||||||
IsMultiUserDownload bool // 是否启用多用户联合下载
|
IsMultiUserDownload bool // 是否启用多用户联合下载
|
||||||
|
IsUseUIDashboard bool // 是否使用UI下载面板显示下载进度
|
||||||
}
|
}
|
||||||
|
|
||||||
// LocateDownloadOption 获取下载链接可选参数
|
// LocateDownloadOption 获取下载链接可选参数
|
||||||
@@ -149,22 +150,10 @@ func CmdDownload() cli.Command {
|
|||||||
DriveId: parseDriveId(c),
|
DriveId: parseDriveId(c),
|
||||||
ExcludeNames: c.StringSlice("exn"),
|
ExcludeNames: c.StringSlice("exn"),
|
||||||
IsMultiUserDownload: c.Bool("md"),
|
IsMultiUserDownload: c.Bool("md"),
|
||||||
|
IsUseUIDashboard: c.Bool("ui"),
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取下载文件锁,保证下载操作单实例
|
|
||||||
//locker := filelocker.NewFileLocker(config.GetLockerDir() + "/aliyunpan-download")
|
|
||||||
//if e := filelocker.LockFile(locker, 0755, true, 5*time.Second); e != nil {
|
|
||||||
// logger.Verboseln(e)
|
|
||||||
// fmt.Println("本应用其他实例正在执行下载,请先停止或者等待其完成")
|
|
||||||
// return nil
|
|
||||||
//}
|
|
||||||
|
|
||||||
RunDownload(c.Args(), do)
|
RunDownload(c.Args(), do)
|
||||||
|
|
||||||
// 释放文件锁
|
|
||||||
//if locker != nil {
|
|
||||||
// filelocker.UnlockFile(locker)
|
|
||||||
//}
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
@@ -225,6 +214,10 @@ func CmdDownload() cli.Command {
|
|||||||
Name: "md",
|
Name: "md",
|
||||||
Usage: "(BETA) Multi-User Download,使用多用户联合下载,可以对单一文件叠加所有登录用户的下载速度",
|
Usage: "(BETA) Multi-User Download,使用多用户联合下载,可以对单一文件叠加所有登录用户的下载速度",
|
||||||
},
|
},
|
||||||
|
cli.BoolFlag{
|
||||||
|
Name: "ui",
|
||||||
|
Usage: "(BETA) 使用UI面板显示下载详情和进度,更加直观和友好",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -327,8 +320,6 @@ func RunDownload(paths []string, options *DownloadOptions) {
|
|||||||
|
|
||||||
// 多用户下载的辅助账号列表
|
// 多用户下载的辅助账号列表
|
||||||
var subPanClientList []*config.PanClient
|
var subPanClientList []*config.PanClient
|
||||||
var multiUserNotice string
|
|
||||||
var multiUserErrorNotice string
|
|
||||||
if options.IsMultiUserDownload { // 多用户下载
|
if options.IsMultiUserDownload { // 多用户下载
|
||||||
c := config.Config
|
c := config.Config
|
||||||
for _, u := range config.Config.UserList {
|
for _, u := range config.Config.UserList {
|
||||||
@@ -352,19 +343,20 @@ func RunDownload(paths []string, options *DownloadOptions) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if subPanClientList == nil || len(subPanClientList) == 0 {
|
if subPanClientList == nil || len(subPanClientList) == 0 {
|
||||||
multiUserErrorNotice = "当前登录用户只有一个,无法启用多用户联合下载"
|
fmt.Printf("\n当前登录用户只有一个,无法启用多用户联合下载\n")
|
||||||
subPanClientList = nil
|
subPanClientList = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if subPanClientList != nil || len(subPanClientList) > 0 {
|
if subPanClientList != nil || len(subPanClientList) > 0 {
|
||||||
// 已启用多用户下载
|
// 已启用多用户下载
|
||||||
userCount := len(subPanClientList) + 1
|
userCount := len(subPanClientList) + 1
|
||||||
multiUserNotice = fmt.Sprintf("*** 已启用多用户联合下载,用户数: %d ***", userCount)
|
fmt.Printf("\n*** 已启用多用户联合下载,用户数: %d ***\n", userCount)
|
||||||
// 多用户下载,并发数必须为1,以获得最大下载速度
|
// 多用户下载,并发数必须为1,以获得最大下载速度
|
||||||
options.Parallel = 1
|
options.Parallel = 1
|
||||||
// 阿里OpenAPI规定:文件分片下载的并发数为3,即某用户使用 App 时,可以同时下载 1 个文件的 3 个分片,或者同时下载 3 个文件的各 1 个分片。
|
// 阿里OpenAPI规定:文件分片下载的并发数为3,即某用户使用 App 时,可以同时下载 1 个文件的 3 个分片,或者同时下载 3 个文件的各 1 个分片。
|
||||||
options.SliceParallel = userCount * 3
|
options.SliceParallel = userCount * 3
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
panClient = activeUser.PanClient()
|
panClient = activeUser.PanClient()
|
||||||
)
|
)
|
||||||
@@ -383,10 +375,13 @@ func RunDownload(paths []string, options *DownloadOptions) {
|
|||||||
// 全局速度统计
|
// 全局速度统计
|
||||||
globalSpeedsStat := &speeds.Speeds{}
|
globalSpeedsStat := &speeds.Speeds{}
|
||||||
|
|
||||||
var dashboard *ui.DownloadDashboard
|
// 下载统计UI面板
|
||||||
if options.ShowProgress && !options.IsPrintStatus && ui.IsTerminal(os.Stdout) {
|
var dashboard *ui.DownloadDashboard = nil
|
||||||
|
if options.IsUseUIDashboard && options.ShowProgress && !options.IsPrintStatus && ui.IsTerminal(os.Stdout) {
|
||||||
dashboard = ui.NewDownloadDashboard(cfg.MaxParallel, globalSpeedsStat, &ui.DownloadDashboardOptions{
|
dashboard = ui.NewDownloadDashboard(cfg.MaxParallel, globalSpeedsStat, &ui.DownloadDashboardOptions{
|
||||||
Title: "AliyunPan CLI - 下载中心",
|
Title: "下载统计UI面板",
|
||||||
|
ActiveSlots: 3, // 下载文件进度展示,最大同时展示3个
|
||||||
|
MaxHistory: 50, // 下载日志显示,最多同时显示50条,这个会按照窗口大小进行自适应显示
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
logf := func(format string, a ...interface{}) {
|
logf := func(format string, a ...interface{}) {
|
||||||
@@ -396,29 +391,13 @@ func RunDownload(paths []string, options *DownloadOptions) {
|
|||||||
}
|
}
|
||||||
fmt.Printf(format, a...)
|
fmt.Printf(format, a...)
|
||||||
}
|
}
|
||||||
if multiUserErrorNotice != "" {
|
|
||||||
if dashboard != nil {
|
|
||||||
dashboard.Logf("%s", multiUserErrorNotice)
|
|
||||||
} else {
|
|
||||||
fmt.Printf("\n%s\n", multiUserErrorNotice)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if multiUserNotice != "" {
|
|
||||||
if dashboard != nil {
|
|
||||||
dashboard.Logf("%s", multiUserNotice)
|
|
||||||
} else {
|
|
||||||
fmt.Printf("\n%s\n", multiUserNotice)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if dashboard != nil {
|
|
||||||
dashboard.Logf("[0] 当前文件下载最大并发量为: %d, 单文件下载分片线程数为: %d, 下载缓存为: %s", options.Parallel, options.SliceParallel, converter.ConvertFileSize(int64(cfg.CacheSize), 2))
|
|
||||||
} else {
|
|
||||||
fmt.Printf("\n[0] 当前文件下载最大并发量为: %d, 单文件下载分片线程数为: %d, 下载缓存为: %s\n", options.Parallel, options.SliceParallel, converter.ConvertFileSize(int64(cfg.CacheSize), 2))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 下载记录器
|
// 下载记录器
|
||||||
fileRecorder := log.NewFileRecorder(config.GetLogDir() + "/download_file_records.csv")
|
fileRecorder := log.NewFileRecorder(config.GetLogDir() + "/download_file_records.csv")
|
||||||
|
|
||||||
|
// 输出当前下载配置信息
|
||||||
|
logf("\n[0] 当前文件下载最大并发量为: %d, 单文件下载分片线程数为: %d, 下载缓存为: %s\n", options.Parallel, options.SliceParallel, converter.ConvertFileSize(int64(cfg.CacheSize), 2))
|
||||||
|
|
||||||
// 处理队列
|
// 处理队列
|
||||||
for k := range paths {
|
for k := range paths {
|
||||||
// 使用通配符匹配
|
// 使用通配符匹配
|
||||||
@@ -488,15 +467,20 @@ func RunDownload(paths []string, options *DownloadOptions) {
|
|||||||
// 开始计时
|
// 开始计时
|
||||||
statistic.StartTimer()
|
statistic.StartTimer()
|
||||||
|
|
||||||
// 开始执行
|
// 启动UI面板显示
|
||||||
if dashboard != nil {
|
if dashboard != nil {
|
||||||
dashboard.Start()
|
dashboard.Start()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 开始执行
|
||||||
executor.Execute()
|
executor.Execute()
|
||||||
|
|
||||||
|
// 关闭UI面板
|
||||||
if dashboard != nil {
|
if dashboard != nil {
|
||||||
dashboard.Close()
|
dashboard.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 完成下载,输出统计结果
|
||||||
fmt.Printf("\n下载结束, 时间: %s, 数据总量: %s\n", utils.ConvertTime(statistic.Elapsed()), converter.ConvertFileSize(statistic.TotalSize(), 2))
|
fmt.Printf("\n下载结束, 时间: %s, 数据总量: %s\n", utils.ConvertTime(statistic.Elapsed()), converter.ConvertFileSize(statistic.TotalSize(), 2))
|
||||||
|
|
||||||
// 输出失败的文件列表
|
// 输出失败的文件列表
|
||||||
|
|||||||
@@ -76,8 +76,7 @@ type (
|
|||||||
// 下载文件记录器
|
// 下载文件记录器
|
||||||
FileRecorder *log.FileRecorder
|
FileRecorder *log.FileRecorder
|
||||||
|
|
||||||
UI *ui.DownloadDashboard
|
UI *ui.DownloadDashboard // 下载统计面板UI
|
||||||
skipReason string
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -114,15 +113,20 @@ func (dtu *DownloadTaskUnit) verboseInfof(format string, a ...interface{}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// logf 打印文本日志
|
||||||
func (dtu *DownloadTaskUnit) logf(format string, a ...interface{}) {
|
func (dtu *DownloadTaskUnit) logf(format string, a ...interface{}) {
|
||||||
if dtu.UI != nil {
|
if dtu.UI != nil {
|
||||||
|
// 使用UI面板显示下载日志
|
||||||
dtu.UI.Logf(format, a...)
|
dtu.UI.Logf(format, a...)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 使用传统单行文本显示下载日志,直接输出到控制台
|
||||||
fmt.Printf(format, a...)
|
fmt.Printf(format, a...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (dtu *DownloadTaskUnit) setUIState(state ui.TaskState, message string) {
|
// updateUITaskState 更新UI下载面板的任务状态
|
||||||
|
func (dtu *DownloadTaskUnit) updateUITaskState(state ui.TaskState, message string) {
|
||||||
if dtu.UI == nil {
|
if dtu.UI == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -208,10 +212,14 @@ func (dtu *DownloadTaskUnit) download() (err error) {
|
|||||||
// 这里用共享变量的方式
|
// 这里用共享变量的方式
|
||||||
isComplete := false
|
isComplete := false
|
||||||
der.OnDownloadStatusEvent(func(status transfer.DownloadStatuser, workersCallback func(downloader.RangeWorkerFunc)) {
|
der.OnDownloadStatusEvent(func(status transfer.DownloadStatuser, workersCallback func(downloader.RangeWorkerFunc)) {
|
||||||
|
// 方式1:使用UI统计面板显示下载进度
|
||||||
if dtu.UI != nil {
|
if dtu.UI != nil {
|
||||||
|
// 更新UI面板任务状态
|
||||||
dtu.UI.UpdateTaskProgress(dtu.taskInfo.Id(), status.Downloaded(), status.TotalSize(), status.SpeedsPerSecond(), status.TimeLeft())
|
dtu.UI.UpdateTaskProgress(dtu.taskInfo.Id(), status.Downloaded(), status.TotalSize(), status.SpeedsPerSecond(), status.TimeLeft())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 方式2:使用传统单行显示下载进度
|
||||||
// 这里可能会下载结束了, 还会输出内容
|
// 这里可能会下载结束了, 还会输出内容
|
||||||
builder := &strings.Builder{}
|
builder := &strings.Builder{}
|
||||||
if dtu.IsPrintStatus {
|
if dtu.IsPrintStatus {
|
||||||
@@ -304,9 +312,7 @@ func (dtu *DownloadTaskUnit) download() (err error) {
|
|||||||
dtu.logf("[%s] 警告, 加执行权限错误: %s\n", dtu.taskInfo.Id(), err)
|
dtu.logf("[%s] 警告, 加执行权限错误: %s\n", dtu.taskInfo.Id(), err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if dtu.UI == nil {
|
dtu.logf("\n[%s] 下载完成, 保存位置: %s\n", dtu.taskInfo.Id(), dtu.SavePath)
|
||||||
fmt.Printf("\n[%s] 下载完成, 保存位置: %s\n", dtu.taskInfo.Id(), dtu.SavePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -346,9 +352,7 @@ func (dtu *DownloadTaskUnit) checkFileValid(result *taskframework.TaskUnitRunRes
|
|||||||
|
|
||||||
if dtu.fileInfo.FileSize >= 128*converter.MB {
|
if dtu.fileInfo.FileSize >= 128*converter.MB {
|
||||||
// 大文件, 输出一句提示消息
|
// 大文件, 输出一句提示消息
|
||||||
if dtu.UI == nil {
|
dtu.logf("[%s] 开始检验文件有效性, 请稍候...\n", dtu.taskInfo.Id())
|
||||||
fmt.Printf("[%s] 开始检验文件有效性, 请稍候...\n", dtu.taskInfo.Id())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 就在这里处理校验出错
|
// 就在这里处理校验出错
|
||||||
@@ -361,9 +365,7 @@ func (dtu *DownloadTaskUnit) checkFileValid(result *taskframework.TaskUnitRunRes
|
|||||||
// 文件不支持校验
|
// 文件不支持校验
|
||||||
result.ResultMessage = "检验文件有效性"
|
result.ResultMessage = "检验文件有效性"
|
||||||
result.Err = err
|
result.Err = err
|
||||||
if dtu.UI == nil {
|
dtu.logf("[%s] 检验文件有效性: %s\n", dtu.taskInfo.Id(), err)
|
||||||
fmt.Printf("[%s] 检验文件有效性: %s\n", dtu.taskInfo.Id(), err)
|
|
||||||
}
|
|
||||||
return true
|
return true
|
||||||
case ErrDownloadFileBanned:
|
case ErrDownloadFileBanned:
|
||||||
// 违规文件
|
// 违规文件
|
||||||
@@ -381,9 +383,7 @@ func (dtu *DownloadTaskUnit) checkFileValid(result *taskframework.TaskUnitRunRes
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if dtu.UI == nil {
|
dtu.logf("[%s] 检验文件有效性成功: %s\n", dtu.taskInfo.Id(), dtu.SavePath)
|
||||||
fmt.Printf("[%s] 检验文件有效性成功: %s\n", dtu.taskInfo.Id(), dtu.SavePath)
|
|
||||||
}
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -400,11 +400,9 @@ func (dtu *DownloadTaskUnit) OnRetry(lastRunResult *taskframework.TaskUnitRunRes
|
|||||||
func (dtu *DownloadTaskUnit) OnSuccess(lastRunResult *taskframework.TaskUnitRunResult) {
|
func (dtu *DownloadTaskUnit) OnSuccess(lastRunResult *taskframework.TaskUnitRunResult) {
|
||||||
// 执行插件
|
// 执行插件
|
||||||
dtu.pluginCallback("success")
|
dtu.pluginCallback("success")
|
||||||
if dtu.skipReason != "" {
|
|
||||||
dtu.setUIState(ui.TaskSkipped, dtu.skipReason)
|
// 更新UI面板任务状态
|
||||||
} else {
|
dtu.updateUITaskState(ui.TaskSuccess, "")
|
||||||
dtu.setUIState(ui.TaskSuccess, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 下载文件数据记录
|
// 下载文件数据记录
|
||||||
if config.Config.FileRecordConfig == "1" {
|
if config.Config.FileRecordConfig == "1" {
|
||||||
@@ -424,11 +422,13 @@ func (dtu *DownloadTaskUnit) OnSuccess(lastRunResult *taskframework.TaskUnitRunR
|
|||||||
func (dtu *DownloadTaskUnit) OnFailed(lastRunResult *taskframework.TaskUnitRunResult) {
|
func (dtu *DownloadTaskUnit) OnFailed(lastRunResult *taskframework.TaskUnitRunResult) {
|
||||||
// 失败
|
// 失败
|
||||||
dtu.pluginCallback("fail")
|
dtu.pluginCallback("fail")
|
||||||
|
|
||||||
|
// 更新UI面板
|
||||||
failedMessage := lastRunResult.ResultMessage
|
failedMessage := lastRunResult.ResultMessage
|
||||||
if lastRunResult.Err != nil {
|
if lastRunResult.Err != nil {
|
||||||
failedMessage = fmt.Sprintf("%s, %s", lastRunResult.ResultMessage, lastRunResult.Err)
|
failedMessage = fmt.Sprintf("%s, %s", lastRunResult.ResultMessage, lastRunResult.Err)
|
||||||
}
|
}
|
||||||
dtu.setUIState(ui.TaskFailed, failedMessage)
|
dtu.updateUITaskState(ui.TaskFailed, failedMessage)
|
||||||
|
|
||||||
// 失败
|
// 失败
|
||||||
if lastRunResult.Err == nil {
|
if lastRunResult.Err == nil {
|
||||||
@@ -469,7 +469,7 @@ func (dtu *DownloadTaskUnit) OnComplete(lastRunResult *taskframework.TaskUnitRun
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (dtu *DownloadTaskUnit) OnCancel(lastRunResult *taskframework.TaskUnitRunResult) {
|
func (dtu *DownloadTaskUnit) OnCancel(lastRunResult *taskframework.TaskUnitRunResult) {
|
||||||
dtu.setUIState(ui.TaskCanceled, "取消: "+dtu.FilePanPath)
|
dtu.updateUITaskState(ui.TaskCanceled, "取消: "+dtu.FilePanPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (dtu *DownloadTaskUnit) RetryWait() time.Duration {
|
func (dtu *DownloadTaskUnit) RetryWait() time.Duration {
|
||||||
@@ -478,7 +478,6 @@ func (dtu *DownloadTaskUnit) RetryWait() time.Duration {
|
|||||||
|
|
||||||
func (dtu *DownloadTaskUnit) Run() (result *taskframework.TaskUnitRunResult) {
|
func (dtu *DownloadTaskUnit) Run() (result *taskframework.TaskUnitRunResult) {
|
||||||
result = &taskframework.TaskUnitRunResult{}
|
result = &taskframework.TaskUnitRunResult{}
|
||||||
dtu.setUIState(ui.TaskRunning, "")
|
|
||||||
// 获取文件信息
|
// 获取文件信息
|
||||||
var apierr *apierror.ApiError
|
var apierr *apierror.ApiError
|
||||||
if dtu.fileInfo == nil || dtu.taskInfo.Retry() > 0 {
|
if dtu.fileInfo == nil || dtu.taskInfo.Retry() > 0 {
|
||||||
@@ -495,15 +494,10 @@ func (dtu *DownloadTaskUnit) Run() (result *taskframework.TaskUnitRunResult) {
|
|||||||
}
|
}
|
||||||
time.Sleep(1 * time.Second)
|
time.Sleep(1 * time.Second)
|
||||||
}
|
}
|
||||||
if dtu.UI != nil && dtu.fileInfo != nil {
|
|
||||||
dtu.UI.UpdateTaskInfo(dtu.taskInfo.Id(), dtu.fileInfo.FileName, dtu.fileInfo.FileSize, dtu.fileInfo.IsFile())
|
|
||||||
}
|
|
||||||
|
|
||||||
// 输出文件信息
|
// 输出文件信息
|
||||||
if dtu.UI == nil {
|
dtu.logf("\n")
|
||||||
fmt.Print("\n")
|
dtu.logf("[%s] ----\n%s\n", dtu.taskInfo.Id(), dtu.fileInfo.String())
|
||||||
fmt.Printf("[%s] ----\n%s\n", dtu.taskInfo.Id(), dtu.fileInfo.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
// 调用插件
|
// 调用插件
|
||||||
ft := "file"
|
ft := "file"
|
||||||
@@ -529,7 +523,7 @@ func (dtu *DownloadTaskUnit) Run() (result *taskframework.TaskUnitRunResult) {
|
|||||||
if strings.Compare("yes", downloadFilePrepareResult.DownloadApproved) != 0 {
|
if strings.Compare("yes", downloadFilePrepareResult.DownloadApproved) != 0 {
|
||||||
// skip download this file
|
// skip download this file
|
||||||
dtu.logf("插件取消了该文件下载: %s\n", dtu.fileInfo.Path)
|
dtu.logf("插件取消了该文件下载: %s\n", dtu.fileInfo.Path)
|
||||||
dtu.setUIState(ui.TaskCanceled, "取消: "+dtu.fileInfo.Path)
|
dtu.updateUITaskState(ui.TaskCanceled, "插件取消了该文件下载: "+dtu.fileInfo.Path)
|
||||||
result.Succeed = false
|
result.Succeed = false
|
||||||
result.Cancel = true
|
result.Cancel = true
|
||||||
return
|
return
|
||||||
@@ -628,14 +622,14 @@ func (dtu *DownloadTaskUnit) Run() (result *taskframework.TaskUnitRunResult) {
|
|||||||
subUnit.FilePanSource = dtu.FilePanSource
|
subUnit.FilePanSource = dtu.FilePanSource
|
||||||
subUnit.FilePanPath = fileList[k].Path
|
subUnit.FilePanPath = fileList[k].Path
|
||||||
subUnit.SavePath = filepath.Join(dtu.OriginSaveRootPath, fileList[k].Path) // 保存位置
|
subUnit.SavePath = filepath.Join(dtu.OriginSaveRootPath, fileList[k].Path) // 保存位置
|
||||||
subUnit.skipReason = ""
|
|
||||||
|
|
||||||
// 加入父队列,按照队列调度进行下载
|
// 加入父队列,按照队列调度进行下载
|
||||||
info := dtu.ParentTaskExecutor.Append(&subUnit, dtu.taskInfo.MaxRetry())
|
info := dtu.ParentTaskExecutor.Append(&subUnit, dtu.taskInfo.MaxRetry())
|
||||||
|
dtu.logf("[%s] 加入下载队列: %s\n", info.Id(), fileList[k].Path)
|
||||||
|
// UI面板注册任务
|
||||||
if dtu.UI != nil {
|
if dtu.UI != nil {
|
||||||
dtu.UI.RegisterTask(info.Id(), fileList[k].Path, fileList[k].FileSize, fileList[k].IsFile())
|
dtu.UI.RegisterTask(info.Id(), fileList[k].Path, fileList[k].FileSize, fileList[k].IsFile())
|
||||||
}
|
}
|
||||||
dtu.logf("[%s] 加入下载队列: %s\n", info.Id(), fileList[k].Path)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 本下载任务执行成功
|
// 本下载任务执行成功
|
||||||
@@ -652,8 +646,7 @@ func (dtu *DownloadTaskUnit) Run() (result *taskframework.TaskUnitRunResult) {
|
|||||||
//}
|
//}
|
||||||
// 支持符号文件,逻辑和注释代码一致
|
// 支持符号文件,逻辑和注释代码一致
|
||||||
if !dtu.IsOverwrite && SymlinkFileExist(dtu.SavePath, dtu.OriginSaveRootPath) {
|
if !dtu.IsOverwrite && SymlinkFileExist(dtu.SavePath, dtu.OriginSaveRootPath) {
|
||||||
dtu.skipReason = "文件已存在,跳过: " + dtu.SavePath
|
dtu.updateUITaskState(ui.TaskSkipped, "文件已存在,跳过: "+dtu.SavePath)
|
||||||
dtu.setUIState(ui.TaskSkipped, dtu.skipReason)
|
|
||||||
dtu.logf("[%s] 文件已经存在: %s, 跳过...\n", dtu.taskInfo.Id(), dtu.SavePath)
|
dtu.logf("[%s] 文件已经存在: %s, 跳过...\n", dtu.taskInfo.Id(), dtu.SavePath)
|
||||||
result.Succeed = true // 执行成功
|
result.Succeed = true // 执行成功
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2026.
|
// Copyright (c) 2020 tickstep.
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
@@ -44,6 +44,7 @@ type DownloadDashboardOptions struct {
|
|||||||
Output io.Writer
|
Output io.Writer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DownloadDashboard 下载任务统计面板使用了很多ANSI转义序列。并不是所有的终端都支持ANSI转义序列,所以需要做兼容性处理。
|
||||||
type DownloadDashboard struct {
|
type DownloadDashboard struct {
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
tasks map[string]*downloadTask
|
tasks map[string]*downloadTask
|
||||||
@@ -74,9 +75,9 @@ type downloadTask struct {
|
|||||||
state TaskState
|
state TaskState
|
||||||
message string
|
message string
|
||||||
isFile bool
|
isFile bool
|
||||||
hasType bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewDownloadDashboard 创建下载任务统计面板
|
||||||
func NewDownloadDashboard(parallel int, globalSpeeds *speeds.Speeds, opts *DownloadDashboardOptions) *DownloadDashboard {
|
func NewDownloadDashboard(parallel int, globalSpeeds *speeds.Speeds, opts *DownloadDashboardOptions) *DownloadDashboard {
|
||||||
if parallel < 1 {
|
if parallel < 1 {
|
||||||
parallel = 1
|
parallel = 1
|
||||||
@@ -89,7 +90,7 @@ func NewDownloadDashboard(parallel int, globalSpeeds *speeds.Speeds, opts *Downl
|
|||||||
parallel: parallel,
|
parallel: parallel,
|
||||||
activeSlots: 5,
|
activeSlots: 5,
|
||||||
globalSpeeds: globalSpeeds,
|
globalSpeeds: globalSpeeds,
|
||||||
title: "AliyunPan Download",
|
title: "aliyunpan下载统计",
|
||||||
renderEvery: 500 * time.Millisecond,
|
renderEvery: 500 * time.Millisecond,
|
||||||
stopCh: make(chan struct{}),
|
stopCh: make(chan struct{}),
|
||||||
doneCh: make(chan struct{}),
|
doneCh: make(chan struct{}),
|
||||||
@@ -115,6 +116,7 @@ func NewDownloadDashboard(parallel int, globalSpeeds *speeds.Speeds, opts *Downl
|
|||||||
return db
|
return db
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Start 启动面板
|
||||||
func (db *DownloadDashboard) Start() {
|
func (db *DownloadDashboard) Start() {
|
||||||
if db == nil {
|
if db == nil {
|
||||||
return
|
return
|
||||||
@@ -122,7 +124,7 @@ func (db *DownloadDashboard) Start() {
|
|||||||
db.startOnce.Do(func() {
|
db.startOnce.Do(func() {
|
||||||
db.startTime = time.Now()
|
db.startTime = time.Now()
|
||||||
enableVirtualTerminalProcessing()
|
enableVirtualTerminalProcessing()
|
||||||
fmt.Fprint(db.out, "\x1b[?25l")
|
fmt.Fprint(db.out, "\x1b[?25l") // 隐藏光标
|
||||||
db.render()
|
db.render()
|
||||||
ticker := time.NewTicker(db.renderEvery)
|
ticker := time.NewTicker(db.renderEvery)
|
||||||
go func() {
|
go func() {
|
||||||
@@ -131,6 +133,9 @@ func (db *DownloadDashboard) Start() {
|
|||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-db.stopCh:
|
case <-db.stopCh:
|
||||||
|
// 延迟1秒
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
// 渲染
|
||||||
db.render()
|
db.render()
|
||||||
return
|
return
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
@@ -141,6 +146,7 @@ func (db *DownloadDashboard) Start() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Close 关闭面板
|
||||||
func (db *DownloadDashboard) Close() {
|
func (db *DownloadDashboard) Close() {
|
||||||
if db == nil {
|
if db == nil {
|
||||||
return
|
return
|
||||||
@@ -148,10 +154,11 @@ func (db *DownloadDashboard) Close() {
|
|||||||
db.closeOnce.Do(func() {
|
db.closeOnce.Do(func() {
|
||||||
close(db.stopCh)
|
close(db.stopCh)
|
||||||
<-db.doneCh
|
<-db.doneCh
|
||||||
fmt.Fprint(db.out, "\x1b[?25h")
|
fmt.Fprint(db.out, "\x1b[?25h") // 显示光标
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RegisterTask 注册任务信息
|
||||||
func (db *DownloadDashboard) RegisterTask(id, path string, total int64, isFile bool) {
|
func (db *DownloadDashboard) RegisterTask(id, path string, total int64, isFile bool) {
|
||||||
if db == nil || id == "" {
|
if db == nil || id == "" {
|
||||||
return
|
return
|
||||||
@@ -168,10 +175,10 @@ func (db *DownloadDashboard) RegisterTask(id, path string, total int64, isFile b
|
|||||||
if total > 0 {
|
if total > 0 {
|
||||||
task.total = total
|
task.total = total
|
||||||
}
|
}
|
||||||
task.hasType = true
|
|
||||||
task.isFile = isFile
|
task.isFile = isFile
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateTaskInfo 更新任务信息
|
||||||
func (db *DownloadDashboard) UpdateTaskInfo(id, name string, total int64, isFile bool) {
|
func (db *DownloadDashboard) UpdateTaskInfo(id, name string, total int64, isFile bool) {
|
||||||
if db == nil || id == "" {
|
if db == nil || id == "" {
|
||||||
return
|
return
|
||||||
@@ -185,10 +192,10 @@ func (db *DownloadDashboard) UpdateTaskInfo(id, name string, total int64, isFile
|
|||||||
if total > 0 {
|
if total > 0 {
|
||||||
task.total = total
|
task.total = total
|
||||||
}
|
}
|
||||||
task.hasType = true
|
|
||||||
task.isFile = isFile
|
task.isFile = isFile
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateTaskProgress 更新任务进度
|
||||||
func (db *DownloadDashboard) UpdateTaskProgress(id string, downloaded, total, speed int64, eta time.Duration) {
|
func (db *DownloadDashboard) UpdateTaskProgress(id string, downloaded, total, speed int64, eta time.Duration) {
|
||||||
if db == nil || id == "" {
|
if db == nil || id == "" {
|
||||||
return
|
return
|
||||||
@@ -204,6 +211,7 @@ func (db *DownloadDashboard) UpdateTaskProgress(id string, downloaded, total, sp
|
|||||||
task.eta = eta
|
task.eta = eta
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MarkTaskState 标记任务状态
|
||||||
func (db *DownloadDashboard) MarkTaskState(id string, state TaskState, message string) {
|
func (db *DownloadDashboard) MarkTaskState(id string, state TaskState, message string) {
|
||||||
if db == nil || id == "" {
|
if db == nil || id == "" {
|
||||||
return
|
return
|
||||||
@@ -221,6 +229,7 @@ func (db *DownloadDashboard) MarkTaskState(id string, state TaskState, message s
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Logf 增加日志
|
||||||
func (db *DownloadDashboard) Logf(format string, a ...interface{}) {
|
func (db *DownloadDashboard) Logf(format string, a ...interface{}) {
|
||||||
if db == nil {
|
if db == nil {
|
||||||
return
|
return
|
||||||
@@ -230,6 +239,10 @@ func (db *DownloadDashboard) Logf(format string, a ...interface{}) {
|
|||||||
if msg == "" {
|
if msg == "" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// 去掉开头的换行符
|
||||||
|
msg = strings.TrimPrefix(msg, "\n")
|
||||||
|
// 去掉回车换行符,UI面板的日志只能单行显示
|
||||||
|
msg = strings.ReplaceAll(msg, "\n", " ")
|
||||||
db.mu.Lock()
|
db.mu.Lock()
|
||||||
defer db.mu.Unlock()
|
defer db.mu.Unlock()
|
||||||
db.appendHistoryLocked(msg)
|
db.appendHistoryLocked(msg)
|
||||||
@@ -281,6 +294,7 @@ func (db *DownloadDashboard) defaultHistoryMessage(task *downloadTask, message s
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// render 渲染面板
|
||||||
func (db *DownloadDashboard) render() {
|
func (db *DownloadDashboard) render() {
|
||||||
if db == nil {
|
if db == nil {
|
||||||
return
|
return
|
||||||
@@ -292,13 +306,17 @@ func (db *DownloadDashboard) render() {
|
|||||||
innerWidth := width - 2
|
innerWidth := width - 2
|
||||||
snapshot := db.snapshot()
|
snapshot := db.snapshot()
|
||||||
|
|
||||||
|
// 统计总览
|
||||||
headerLines := db.headerLines(snapshot, innerWidth)
|
headerLines := db.headerLines(snapshot, innerWidth)
|
||||||
activeLines := db.activeLines(snapshot, innerWidth)
|
|
||||||
activeTitle := db.activeTitle(snapshot)
|
|
||||||
|
|
||||||
|
// 下载任务队列详情
|
||||||
|
//activeTitle := db.activeTitle(snapshot)
|
||||||
|
activeLines := db.activeLines(snapshot, innerWidth)
|
||||||
|
|
||||||
|
// 下载日志
|
||||||
headerHeight := len(headerLines) + 2
|
headerHeight := len(headerLines) + 2
|
||||||
activeHeight := len(activeLines) + 4
|
activeHeight := len(activeLines) + 1
|
||||||
historyBase := 4
|
historyBase := 3
|
||||||
minHistory := 1
|
minHistory := 1
|
||||||
minHeight := headerHeight + activeHeight + historyBase + minHistory
|
minHeight := headerHeight + activeHeight + historyBase + minHistory
|
||||||
if height < minHeight {
|
if height < minHeight {
|
||||||
@@ -310,12 +328,11 @@ func (db *DownloadDashboard) render() {
|
|||||||
}
|
}
|
||||||
historyBody := db.historyLines(snapshot, innerWidth, historyLines)
|
historyBody := db.historyLines(snapshot, innerWidth, historyLines)
|
||||||
|
|
||||||
|
// 拼接并格式化输出
|
||||||
builder := &strings.Builder{}
|
builder := &strings.Builder{}
|
||||||
builder.WriteString("\x1b[H\x1b[2J")
|
builder.WriteString("\x1b[H\x1b[2J") // 清屏操作
|
||||||
builder.WriteString(renderBox(headerLines, width))
|
builder.WriteString(renderBox(headerLines, width))
|
||||||
builder.WriteString("\n")
|
builder.WriteString(renderTitledBox("ActiveDownloads", activeLines, width))
|
||||||
builder.WriteString(renderTitledBox(activeTitle, activeLines, width))
|
|
||||||
builder.WriteString("\n")
|
|
||||||
builder.WriteString(renderTitledBox("History", historyBody, width))
|
builder.WriteString(renderTitledBox("History", historyBody, width))
|
||||||
|
|
||||||
fmt.Fprint(db.out, builder.String())
|
fmt.Fprint(db.out, builder.String())
|
||||||
@@ -351,14 +368,16 @@ func renderBox(lines []string, width int) string {
|
|||||||
builder.WriteString("\u2510")
|
builder.WriteString("\u2510")
|
||||||
for _, line := range lines {
|
for _, line := range lines {
|
||||||
builder.WriteString("\n")
|
builder.WriteString("\n")
|
||||||
builder.WriteString("\u2502")
|
builder.WriteString("\u2502") // 左边框
|
||||||
builder.WriteString(fitLine(line, innerWidth))
|
builder.WriteString(fitLine(line, innerWidth))
|
||||||
builder.WriteString("\u2502")
|
builder.WriteString("\u2502") // 右边框
|
||||||
}
|
}
|
||||||
builder.WriteString("\n")
|
builder.WriteString("\n")
|
||||||
builder.WriteString("\u2514")
|
builder.WriteString("\u2502") // 左边框
|
||||||
|
//builder.WriteString("\u2514") // 左下角
|
||||||
builder.WriteString(strings.Repeat("\u2500", innerWidth))
|
builder.WriteString(strings.Repeat("\u2500", innerWidth))
|
||||||
builder.WriteString("\u2518")
|
builder.WriteString("\u2502") // 右边框
|
||||||
|
//builder.WriteString("\u2518") // 右下角
|
||||||
return builder.String()
|
return builder.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -368,25 +387,26 @@ func renderTitledBox(title string, body []string, width int) string {
|
|||||||
}
|
}
|
||||||
innerWidth := width - 2
|
innerWidth := width - 2
|
||||||
builder := &strings.Builder{}
|
builder := &strings.Builder{}
|
||||||
builder.WriteString("\u250c")
|
|
||||||
builder.WriteString(strings.Repeat("\u2500", innerWidth))
|
|
||||||
builder.WriteString("\u2510\n")
|
|
||||||
builder.WriteString("\u2502")
|
|
||||||
builder.WriteString(fitLine(title, innerWidth))
|
|
||||||
builder.WriteString("\u2502\n")
|
|
||||||
builder.WriteString("\u251c")
|
|
||||||
builder.WriteString(strings.Repeat("\u2500", innerWidth))
|
|
||||||
builder.WriteString("\u2524")
|
|
||||||
for _, line := range body {
|
for _, line := range body {
|
||||||
builder.WriteString("\n")
|
builder.WriteString("\n")
|
||||||
builder.WriteString("\u2502")
|
builder.WriteString("\u2502") // 左边框
|
||||||
builder.WriteString(fitLine(line, innerWidth))
|
builder.WriteString(fitLine(line, innerWidth)) // 内容,使用空格补足宽度
|
||||||
builder.WriteString("\u2502")
|
builder.WriteString("\u2502") // 右边框
|
||||||
}
|
}
|
||||||
|
|
||||||
builder.WriteString("\n")
|
builder.WriteString("\n")
|
||||||
builder.WriteString("\u2514")
|
if strings.HasPrefix(title, "History") {
|
||||||
builder.WriteString(strings.Repeat("\u2500", innerWidth))
|
builder.WriteString("\u2514") // 左下角
|
||||||
builder.WriteString("\u2518")
|
} else {
|
||||||
|
builder.WriteString("\u2502") // 左边框
|
||||||
|
}
|
||||||
|
builder.WriteString(strings.Repeat("\u2500", innerWidth)) // 表格横线
|
||||||
|
if strings.HasPrefix(title, "History") {
|
||||||
|
builder.WriteString("\u2518") // 右下角
|
||||||
|
} else {
|
||||||
|
builder.WriteString("\u2502") // 右边框
|
||||||
|
}
|
||||||
return builder.String()
|
return builder.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -400,46 +420,43 @@ func (db *DownloadDashboard) headerLines(snapshot dashboardSnapshot, innerWidth
|
|||||||
left = formatDurationShort(time.Duration((total-downloaded)/speed) * time.Second)
|
left = formatDurationShort(time.Duration((total-downloaded)/speed) * time.Second)
|
||||||
}
|
}
|
||||||
|
|
||||||
status := "Running"
|
status := "下载中"
|
||||||
if totalFiles > 0 && doneFiles >= totalFiles {
|
if totalFiles > 0 && doneFiles >= totalFiles {
|
||||||
if failedFiles > 0 {
|
if failedFiles > 0 {
|
||||||
status = "Completed (errors)"
|
status = "已完成 (有错误)"
|
||||||
} else {
|
} else {
|
||||||
status = "Completed"
|
status = "已完成"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
headerLine := fmt.Sprintf("%s [%s]", db.title, status)
|
|
||||||
line1 := fitLine(headerLine, innerWidth)
|
|
||||||
|
|
||||||
speedStr := converter.ConvertFileSize(speed, 2) + "/s"
|
speedStr := converter.ConvertFileSize(speed, 2) + "/s"
|
||||||
line2 := fmt.Sprintf("Total Speed: %s | Files: %d/%d | Failed: %d | Elapsed: %s | Left: %s",
|
line1 := fmt.Sprintf("总速度: %s | 文件: %d/%d | 失败: %d | 已用时间: %s | 剩余时间: %s | 状态: %s",
|
||||||
speedStr, doneFiles, totalFiles, failedFiles, formatDurationShort(elapsed), left)
|
speedStr, doneFiles, totalFiles, failedFiles, formatDurationShort(elapsed), left, status)
|
||||||
line2 = fitLine(line2, innerWidth)
|
line1 = fitLine(line1, innerWidth)
|
||||||
|
|
||||||
progressPct := 0.0
|
progressPct := 0.0
|
||||||
if total > 0 {
|
if total > 0 {
|
||||||
progressPct = float64(downloaded) / float64(total)
|
progressPct = float64(downloaded) / float64(total)
|
||||||
}
|
}
|
||||||
progress := fmt.Sprintf("%s/%s", converter.ConvertFileSize(downloaded, 2), converter.ConvertFileSize(total, 2))
|
progress := fmt.Sprintf("%s/%s", converter.ConvertFileSize(downloaded, 2), converter.ConvertFileSize(total, 2))
|
||||||
label := "Total Progress: "
|
label := "总进度: "
|
||||||
suffix := fmt.Sprintf(" %5.1f%% (%s)", progressPct*100, progress)
|
suffix := fmt.Sprintf(" %5.1f%% (%s)", progressPct*100, progress)
|
||||||
barWidth := innerWidth - displayWidth(label) - displayWidth(suffix) - 2
|
barWidth := innerWidth - displayWidth(label) - displayWidth(suffix) - 2
|
||||||
if barWidth < 10 {
|
if barWidth < 10 {
|
||||||
barWidth = 10
|
barWidth = 10
|
||||||
}
|
}
|
||||||
bar := renderBar(progressPct, barWidth)
|
bar := renderBar(progressPct, barWidth)
|
||||||
line3 := fmt.Sprintf("%s[%s]%s", label, bar, suffix)
|
line2 := fmt.Sprintf("%s[%s]%s", label, bar, suffix)
|
||||||
line3 = fitLine(line3, innerWidth)
|
line2 = fitLine(line2, innerWidth)
|
||||||
|
|
||||||
return []string{line1, line2, line3}
|
return []string{line1, line2}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *DownloadDashboard) activeTitle(snapshot dashboardSnapshot) string {
|
func (db *DownloadDashboard) activeTitle(snapshot dashboardSnapshot) string {
|
||||||
running := 0
|
running := 0
|
||||||
queued := 0
|
queued := 0
|
||||||
for _, task := range snapshot.tasks {
|
for _, task := range snapshot.tasks {
|
||||||
if task.hasType && !task.isFile {
|
if !task.isFile {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
switch task.state {
|
switch task.state {
|
||||||
@@ -460,7 +477,7 @@ func (db *DownloadDashboard) activeLines(snapshot dashboardSnapshot, innerWidth
|
|||||||
lines = append(lines, renderTaskLine(active[i], i, innerWidth))
|
lines = append(lines, renderTaskLine(active[i], i, innerWidth))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
placeholder := &downloadTask{name: "waiting...", state: TaskQueued}
|
placeholder := &downloadTask{name: "[空闲...]", state: TaskQueued}
|
||||||
lines = append(lines, renderTaskLine(placeholder, i, innerWidth))
|
lines = append(lines, renderTaskLine(placeholder, i, innerWidth))
|
||||||
}
|
}
|
||||||
return lines
|
return lines
|
||||||
@@ -488,6 +505,15 @@ func (db *DownloadDashboard) historyLines(snapshot dashboardSnapshot, innerWidth
|
|||||||
if len(result) == 0 {
|
if len(result) == 0 {
|
||||||
result = append(result, truncateText("No recent events.", innerWidth))
|
result = append(result, truncateText("No recent events.", innerWidth))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 倒序数组
|
||||||
|
reversedResult := make([]string, 0, len(result))
|
||||||
|
for i := len(result) - 1; i >= 0; i-- {
|
||||||
|
reversedResult = append(reversedResult, result[i])
|
||||||
|
}
|
||||||
|
result = reversedResult
|
||||||
|
|
||||||
|
// 补全空行
|
||||||
for len(result) < lines {
|
for len(result) < lines {
|
||||||
result = append(result, "")
|
result = append(result, "")
|
||||||
}
|
}
|
||||||
@@ -496,7 +522,7 @@ func (db *DownloadDashboard) historyLines(snapshot dashboardSnapshot, innerWidth
|
|||||||
|
|
||||||
func (db *DownloadDashboard) fileCounts(tasks []*downloadTask) (total int, done int, failed int) {
|
func (db *DownloadDashboard) fileCounts(tasks []*downloadTask) (total int, done int, failed int) {
|
||||||
for _, task := range tasks {
|
for _, task := range tasks {
|
||||||
if task.hasType && !task.isFile {
|
if !task.isFile {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
total++
|
total++
|
||||||
@@ -513,7 +539,7 @@ func (db *DownloadDashboard) fileCounts(tasks []*downloadTask) (total int, done
|
|||||||
|
|
||||||
func (db *DownloadDashboard) totalProgress(tasks []*downloadTask) (downloaded int64, total int64) {
|
func (db *DownloadDashboard) totalProgress(tasks []*downloadTask) (downloaded int64, total int64) {
|
||||||
for _, task := range tasks {
|
for _, task := range tasks {
|
||||||
if task.hasType && !task.isFile {
|
if !task.isFile {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if task.total > 0 {
|
if task.total > 0 {
|
||||||
@@ -547,7 +573,7 @@ func (db *DownloadDashboard) pickActiveTasks(tasks []*downloadTask) []*downloadT
|
|||||||
active := make([]*downloadTask, 0, slotCap)
|
active := make([]*downloadTask, 0, slotCap)
|
||||||
queued := make([]*downloadTask, 0, slotCap)
|
queued := make([]*downloadTask, 0, slotCap)
|
||||||
for _, task := range tasks {
|
for _, task := range tasks {
|
||||||
if task.hasType && !task.isFile {
|
if !task.isFile {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
switch task.state {
|
switch task.state {
|
||||||
@@ -574,6 +600,7 @@ func renderTaskLine(task *downloadTask, index int, width int) string {
|
|||||||
name = baseName(task.path)
|
name = baseName(task.path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 百分比
|
||||||
progressPct := 0.0
|
progressPct := 0.0
|
||||||
percent := "--.-%"
|
percent := "--.-%"
|
||||||
if task.total > 0 {
|
if task.total > 0 {
|
||||||
@@ -581,11 +608,13 @@ func renderTaskLine(task *downloadTask, index int, width int) string {
|
|||||||
percent = fmt.Sprintf("%5.1f%%", math.Min(100, progressPct*100))
|
percent = fmt.Sprintf("%5.1f%%", math.Min(100, progressPct*100))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 速度
|
||||||
rate := "-"
|
rate := "-"
|
||||||
|
// 剩余时间
|
||||||
eta := "-"
|
eta := "-"
|
||||||
switch task.state {
|
switch task.state {
|
||||||
case TaskQueued:
|
case TaskQueued:
|
||||||
rate = "waiting"
|
rate = "等待中"
|
||||||
case TaskRunning:
|
case TaskRunning:
|
||||||
if task.speed > 0 {
|
if task.speed > 0 {
|
||||||
rate = converter.ConvertFileSize(task.speed, 2) + "/s"
|
rate = converter.ConvertFileSize(task.speed, 2) + "/s"
|
||||||
@@ -643,6 +672,7 @@ func renderTaskLine(task *downloadTask, index int, width int) string {
|
|||||||
return fitLine(line, width)
|
return fitLine(line, width)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// renderBar 绘制进度条
|
||||||
func renderBar(pct float64, width int) string {
|
func renderBar(pct float64, width int) string {
|
||||||
if width < 5 {
|
if width < 5 {
|
||||||
width = 5
|
width = 5
|
||||||
@@ -654,7 +684,11 @@ func renderBar(pct float64, width int) string {
|
|||||||
pct = 1
|
pct = 1
|
||||||
}
|
}
|
||||||
filled := int(math.Floor(pct * float64(width)))
|
filled := int(math.Floor(pct * float64(width)))
|
||||||
return strings.Repeat("\u2588", filled) + strings.Repeat("\u2591", width-filled)
|
// 已经下载的部分使用 => 表示,没有下载的部分使用 空格 表示
|
||||||
|
if filled >= 1 {
|
||||||
|
return strings.Repeat("=", filled-1) + ">" + strings.Repeat(" ", width-filled)
|
||||||
|
}
|
||||||
|
return strings.Repeat(" ", width-filled)
|
||||||
}
|
}
|
||||||
|
|
||||||
func formatDurationShort(d time.Duration) string {
|
func formatDurationShort(d time.Duration) string {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2026.
|
// Copyright (c) 2020 tickstep.
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
@@ -15,6 +15,7 @@ package ui
|
|||||||
|
|
||||||
import "os"
|
import "os"
|
||||||
|
|
||||||
|
// IsTerminal 判断输出环境,是否是输出到终端界面,如果重定向到文件则不用进行下载统计打印输出了
|
||||||
func IsTerminal(file *os.File) bool {
|
func IsTerminal(file *os.File) bool {
|
||||||
if file == nil {
|
if file == nil {
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
//go:build !windows
|
//go:build !windows
|
||||||
|
|
||||||
// Copyright (c) 2026.
|
// Copyright (c) 2020 tickstep.
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
//go:build windows
|
//go:build windows
|
||||||
|
|
||||||
// Copyright (c) 2026.
|
// Copyright (c) 2020 tickstep.
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
|
|||||||
Reference in New Issue
Block a user