mirror of
https://gitee.com/zhangsken/aliyunpan.git
synced 2026-10-16 02:23:49 +08:00
@@ -25,6 +25,7 @@ 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"
|
||||||
@@ -280,8 +281,6 @@ 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()
|
||||||
)
|
)
|
||||||
@@ -300,10 +299,29 @@ 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 {
|
||||||
fmt.Printf("获取相簿列表失败: %s\n", err)
|
logf("获取相簿列表失败: %s\n", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for k := range albumNames {
|
for k := range albumNames {
|
||||||
@@ -323,11 +341,11 @@ func RunShareAlbumDownloadFile(albumNames []string, options *DownloadOptions) {
|
|||||||
Limit: 100,
|
Limit: 100,
|
||||||
})
|
})
|
||||||
if er != nil {
|
if er != nil {
|
||||||
fmt.Printf("获取相簿文件出错,请稍后重试: %s\n", albumNames[k])
|
logf("获取相簿文件出错,请稍后重试: %s\n", albumNames[k])
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if fileList == nil || len(fileList) == 0 {
|
if fileList == nil || len(fileList) == 0 {
|
||||||
fmt.Printf("相簿里面没有文件: %s\n", albumNames[k])
|
logf("相簿里面没有文件: %s\n", albumNames[k])
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -350,7 +368,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)
|
||||||
fmt.Printf("\n下载照片失败: %s\n", f.FileName)
|
logf("下载照片失败: %s\n", f.FileName)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if durl.StreamsUrl != nil { // 实况图片(照片+视频)下载链接
|
if durl.StreamsUrl != nil { // 实况图片(照片+视频)下载链接
|
||||||
@@ -404,6 +422,7 @@ func RunShareAlbumDownloadFile(albumNames []string, options *DownloadOptions) {
|
|||||||
DriveId: f.DriveId, // 一个相簿的文件会来自多个网盘(资源库/备份盘)
|
DriveId: f.DriveId, // 一个相簿的文件会来自多个网盘(资源库/备份盘)
|
||||||
GlobalSpeedsStat: globalSpeedsStat,
|
GlobalSpeedsStat: globalSpeedsStat,
|
||||||
FileRecorder: nil,
|
FileRecorder: nil,
|
||||||
|
UI: dashboard,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置相簿文件信息
|
// 设置相簿文件信息
|
||||||
@@ -419,7 +438,10 @@ 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)
|
||||||
fmt.Printf("[%s] 加入下载队列: %s\n", info.Id(), f.Path)
|
if dashboard != nil {
|
||||||
|
dashboard.RegisterTask(info.Id(), f.Path, f.FileSize, f.IsFile())
|
||||||
|
}
|
||||||
|
logf("[%s] 加入下载队列: %s\n", info.Id(), f.Path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -427,7 +449,13 @@ 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,6 +26,7 @@ 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"
|
||||||
@@ -688,8 +689,6 @@ 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()
|
||||||
)
|
)
|
||||||
@@ -707,6 +706,25 @@ 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])
|
||||||
@@ -718,11 +736,11 @@ func RunAlbumDownloadFile(albumNames []string, options *DownloadOptions) {
|
|||||||
AlbumId: record.AlbumId,
|
AlbumId: record.AlbumId,
|
||||||
})
|
})
|
||||||
if er != nil {
|
if er != nil {
|
||||||
fmt.Printf("获取相簿文件出错,请稍后重试: %s\n", albumNames[k])
|
logf("获取相簿文件出错,请稍后重试: %s\n", albumNames[k])
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if fileList == nil || len(fileList) == 0 {
|
if fileList == nil || len(fileList) == 0 {
|
||||||
fmt.Printf("相簿里面没有文件: %s\n", albumNames[k])
|
logf("相簿里面没有文件: %s\n", albumNames[k])
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for _, f := range fileList {
|
for _, f := range fileList {
|
||||||
@@ -746,6 +764,7 @@ 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: 相册下载需要重构
|
||||||
@@ -762,7 +781,10 @@ 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)
|
||||||
fmt.Printf("[%s] 加入下载队列: %s\n", info.Id(), f.Path)
|
if dashboard != nil {
|
||||||
|
dashboard.RegisterTask(info.Id(), f.Path, f.FileSize, f.IsFile())
|
||||||
|
}
|
||||||
|
logf("[%s] 加入下载队列: %s\n", info.Id(), f.Path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -770,7 +792,13 @@ 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))
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import (
|
|||||||
"github.com/tickstep/aliyunpan/internal/global"
|
"github.com/tickstep/aliyunpan/internal/global"
|
||||||
"github.com/tickstep/aliyunpan/internal/log"
|
"github.com/tickstep/aliyunpan/internal/log"
|
||||||
"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"
|
||||||
@@ -326,6 +327,8 @@ 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 {
|
||||||
@@ -349,21 +352,19 @@ func RunDownload(paths []string, options *DownloadOptions) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if subPanClientList == nil || len(subPanClientList) == 0 {
|
if subPanClientList == nil || len(subPanClientList) == 0 {
|
||||||
fmt.Printf("\n当前登录用户只有一个,无法启用多用户联合下载\n")
|
multiUserErrorNotice = "当前登录用户只有一个,无法启用多用户联合下载"
|
||||||
subPanClientList = nil
|
subPanClientList = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if subPanClientList != nil || len(subPanClientList) > 0 {
|
if subPanClientList != nil || len(subPanClientList) > 0 {
|
||||||
// 已启用多用户下载
|
// 已启用多用户下载
|
||||||
userCount := len(subPanClientList) + 1
|
userCount := len(subPanClientList) + 1
|
||||||
fmt.Printf("\n*** 已启用多用户联合下载,用户数: %d ***\n", userCount)
|
multiUserNotice = fmt.Sprintf("*** 已启用多用户联合下载,用户数: %d ***", 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
|
||||||
}
|
}
|
||||||
fmt.Printf("\n[0] 当前文件下载最大并发量为: %d, 单文件下载分片线程数为: %d, 下载缓存为: %s\n", options.Parallel, options.SliceParallel, converter.ConvertFileSize(int64(cfg.CacheSize), 2))
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
panClient = activeUser.PanClient()
|
panClient = activeUser.PanClient()
|
||||||
)
|
)
|
||||||
@@ -382,6 +383,39 @@ func RunDownload(paths []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 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")
|
||||||
|
|
||||||
@@ -390,12 +424,12 @@ func RunDownload(paths []string, options *DownloadOptions) {
|
|||||||
// 使用通配符匹配
|
// 使用通配符匹配
|
||||||
fileList, err2 := matchPathByShellPattern(options.DriveId, paths[k])
|
fileList, err2 := matchPathByShellPattern(options.DriveId, paths[k])
|
||||||
if err2 != nil {
|
if err2 != nil {
|
||||||
fmt.Printf("获取文件出错,请稍后重试: %s\n", paths[k])
|
logf("获取文件出错,请稍后重试: %s\n", paths[k])
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if fileList == nil || len(fileList) == 0 {
|
if fileList == nil || len(fileList) == 0 {
|
||||||
// 文件不存在
|
// 文件不存在
|
||||||
fmt.Printf("文件不存在: %s\n", paths[k])
|
logf("文件不存在: %s\n", paths[k])
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// 排序,按名称排序,从小到大
|
// 排序,按名称排序,从小到大
|
||||||
@@ -408,7 +442,7 @@ func RunDownload(paths []string, options *DownloadOptions) {
|
|||||||
|
|
||||||
// 是否排除下载
|
// 是否排除下载
|
||||||
if utils.IsExcludeFile(f.Path, &newCfg.ExcludeNames) {
|
if utils.IsExcludeFile(f.Path, &newCfg.ExcludeNames) {
|
||||||
fmt.Printf("排除文件: %s\n", f.Path)
|
logf("排除文件: %s\n", f.Path)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -431,6 +465,7 @@ func RunDownload(paths []string, options *DownloadOptions) {
|
|||||||
DriveId: options.DriveId,
|
DriveId: options.DriveId,
|
||||||
GlobalSpeedsStat: globalSpeedsStat,
|
GlobalSpeedsStat: globalSpeedsStat,
|
||||||
FileRecorder: fileRecorder,
|
FileRecorder: fileRecorder,
|
||||||
|
UI: dashboard,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置储存的路径
|
// 设置储存的路径
|
||||||
@@ -443,7 +478,10 @@ func RunDownload(paths []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)
|
||||||
fmt.Printf("[%s] 加入下载队列: %s\n", info.Id(), f.Path)
|
if dashboard != nil {
|
||||||
|
dashboard.RegisterTask(info.Id(), f.Path, f.FileSize, f.IsFile())
|
||||||
|
}
|
||||||
|
logf("[%s] 加入下载队列: %s\n", info.Id(), f.Path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -451,7 +489,13 @@ func RunDownload(paths []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,6 +26,7 @@ import (
|
|||||||
"github.com/tickstep/aliyunpan/internal/log"
|
"github.com/tickstep/aliyunpan/internal/log"
|
||||||
"github.com/tickstep/aliyunpan/internal/plugins"
|
"github.com/tickstep/aliyunpan/internal/plugins"
|
||||||
"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"
|
||||||
@@ -74,6 +75,9 @@ type (
|
|||||||
|
|
||||||
// 下载文件记录器
|
// 下载文件记录器
|
||||||
FileRecorder *log.FileRecorder
|
FileRecorder *log.FileRecorder
|
||||||
|
|
||||||
|
UI *ui.DownloadDashboard
|
||||||
|
skipReason string
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -110,6 +114,21 @@ func (dtu *DownloadTaskUnit) verboseInfof(format string, a ...interface{}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (dtu *DownloadTaskUnit) logf(format string, a ...interface{}) {
|
||||||
|
if dtu.UI != nil {
|
||||||
|
dtu.UI.Logf(format, a...)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Printf(format, a...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dtu *DownloadTaskUnit) setUIState(state ui.TaskState, message string) {
|
||||||
|
if dtu.UI == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dtu.UI.MarkTaskState(dtu.taskInfo.Id(), state, message)
|
||||||
|
}
|
||||||
|
|
||||||
// download 执行下载文件(非目录)
|
// download 执行下载文件(非目录)
|
||||||
func (dtu *DownloadTaskUnit) download() (err error) {
|
func (dtu *DownloadTaskUnit) download() (err error) {
|
||||||
var (
|
var (
|
||||||
@@ -189,6 +208,10 @@ 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)) {
|
||||||
|
if dtu.UI != nil {
|
||||||
|
dtu.UI.UpdateTaskProgress(dtu.taskInfo.Id(), status.Downloaded(), status.TotalSize(), status.SpeedsPerSecond(), status.TimeLeft())
|
||||||
|
return
|
||||||
|
}
|
||||||
// 这里可能会下载结束了, 还会输出内容
|
// 这里可能会下载结束了, 还会输出内容
|
||||||
builder := &strings.Builder{}
|
builder := &strings.Builder{}
|
||||||
if dtu.IsPrintStatus {
|
if dtu.IsPrintStatus {
|
||||||
@@ -236,7 +259,7 @@ func (dtu *DownloadTaskUnit) download() (err error) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
der.OnExecute(func() {
|
der.OnExecute(func() {
|
||||||
fmt.Printf("[%s] 下载开始\n", dtu.taskInfo.Id())
|
dtu.logf("[%s] 下载开始\n", dtu.taskInfo.Id())
|
||||||
})
|
})
|
||||||
|
|
||||||
err = der.Execute()
|
err = der.Execute()
|
||||||
@@ -253,7 +276,7 @@ func (dtu *DownloadTaskUnit) download() (err error) {
|
|||||||
if removeErr != nil {
|
if removeErr != nil {
|
||||||
dtu.verboseInfof("[%s] remove file error: %s\n", dtu.taskInfo.Id(), removeErr)
|
dtu.verboseInfof("[%s] remove file error: %s\n", dtu.taskInfo.Id(), removeErr)
|
||||||
}
|
}
|
||||||
fmt.Printf("[%s] 下载失败,文件不合法或者被禁止下载: %s\n", dtu.taskInfo.Id(), dtu.SavePath)
|
dtu.logf("[%s] 下载失败,文件不合法或者被禁止下载: %s\n", dtu.taskInfo.Id(), dtu.SavePath)
|
||||||
return err
|
return err
|
||||||
} else {
|
} else {
|
||||||
// 下载发生错误
|
// 下载发生错误
|
||||||
@@ -278,10 +301,12 @@ func (dtu *DownloadTaskUnit) download() (err error) {
|
|||||||
if dtu.IsExecutedPermission {
|
if dtu.IsExecutedPermission {
|
||||||
err = file.Chmod(0766)
|
err = file.Chmod(0766)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("[%s] 警告, 加执行权限错误: %s\n", dtu.taskInfo.Id(), err)
|
dtu.logf("[%s] 警告, 加执行权限错误: %s\n", dtu.taskInfo.Id(), err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fmt.Printf("\n[%s] 下载完成, 保存位置: %s\n", dtu.taskInfo.Id(), dtu.SavePath)
|
if dtu.UI == nil {
|
||||||
|
fmt.Printf("\n[%s] 下载完成, 保存位置: %s\n", dtu.taskInfo.Id(), dtu.SavePath)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -321,7 +346,9 @@ func (dtu *DownloadTaskUnit) checkFileValid(result *taskframework.TaskUnitRunRes
|
|||||||
|
|
||||||
if dtu.fileInfo.FileSize >= 128*converter.MB {
|
if dtu.fileInfo.FileSize >= 128*converter.MB {
|
||||||
// 大文件, 输出一句提示消息
|
// 大文件, 输出一句提示消息
|
||||||
fmt.Printf("[%s] 开始检验文件有效性, 请稍候...\n", dtu.taskInfo.Id())
|
if dtu.UI == nil {
|
||||||
|
fmt.Printf("[%s] 开始检验文件有效性, 请稍候...\n", dtu.taskInfo.Id())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 就在这里处理校验出错
|
// 就在这里处理校验出错
|
||||||
@@ -334,7 +361,9 @@ func (dtu *DownloadTaskUnit) checkFileValid(result *taskframework.TaskUnitRunRes
|
|||||||
// 文件不支持校验
|
// 文件不支持校验
|
||||||
result.ResultMessage = "检验文件有效性"
|
result.ResultMessage = "检验文件有效性"
|
||||||
result.Err = err
|
result.Err = err
|
||||||
fmt.Printf("[%s] 检验文件有效性: %s\n", dtu.taskInfo.Id(), err)
|
if dtu.UI == nil {
|
||||||
|
fmt.Printf("[%s] 检验文件有效性: %s\n", dtu.taskInfo.Id(), err)
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
case ErrDownloadFileBanned:
|
case ErrDownloadFileBanned:
|
||||||
// 违规文件
|
// 违规文件
|
||||||
@@ -352,7 +381,9 @@ func (dtu *DownloadTaskUnit) checkFileValid(result *taskframework.TaskUnitRunRes
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("[%s] 检验文件有效性成功: %s\n", dtu.taskInfo.Id(), dtu.SavePath)
|
if dtu.UI == nil {
|
||||||
|
fmt.Printf("[%s] 检验文件有效性成功: %s\n", dtu.taskInfo.Id(), dtu.SavePath)
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -360,15 +391,20 @@ func (dtu *DownloadTaskUnit) OnRetry(lastRunResult *taskframework.TaskUnitRunRes
|
|||||||
// 输出错误信息
|
// 输出错误信息
|
||||||
if lastRunResult.Err == nil {
|
if lastRunResult.Err == nil {
|
||||||
// result中不包含Err, 忽略输出
|
// result中不包含Err, 忽略输出
|
||||||
fmt.Printf("[%s] %s, 重试 %d/%d\n", dtu.taskInfo.Id(), lastRunResult.ResultMessage, dtu.taskInfo.Retry(), dtu.taskInfo.MaxRetry())
|
dtu.logf("[%s] %s, 重试 %d/%d\n", dtu.taskInfo.Id(), lastRunResult.ResultMessage, dtu.taskInfo.Retry(), dtu.taskInfo.MaxRetry())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fmt.Printf("[%s] %s, %s, 重试 %d/%d\n", dtu.taskInfo.Id(), lastRunResult.ResultMessage, lastRunResult.Err, dtu.taskInfo.Retry(), dtu.taskInfo.MaxRetry())
|
dtu.logf("[%s] %s, %s, 重试 %d/%d\n", dtu.taskInfo.Id(), lastRunResult.ResultMessage, lastRunResult.Err, dtu.taskInfo.Retry(), dtu.taskInfo.MaxRetry())
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
|
} else {
|
||||||
|
dtu.setUIState(ui.TaskSuccess, "")
|
||||||
|
}
|
||||||
|
|
||||||
// 下载文件数据记录
|
// 下载文件数据记录
|
||||||
if config.Config.FileRecordConfig == "1" {
|
if config.Config.FileRecordConfig == "1" {
|
||||||
@@ -388,14 +424,19 @@ 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")
|
||||||
|
failedMessage := lastRunResult.ResultMessage
|
||||||
|
if lastRunResult.Err != nil {
|
||||||
|
failedMessage = fmt.Sprintf("%s, %s", lastRunResult.ResultMessage, lastRunResult.Err)
|
||||||
|
}
|
||||||
|
dtu.setUIState(ui.TaskFailed, failedMessage)
|
||||||
|
|
||||||
// 失败
|
// 失败
|
||||||
if lastRunResult.Err == nil {
|
if lastRunResult.Err == nil {
|
||||||
// result中不包含Err, 忽略输出
|
// result中不包含Err, 忽略输出
|
||||||
fmt.Printf("[%s] %s\n", dtu.taskInfo.Id(), lastRunResult.ResultMessage)
|
dtu.logf("[%s] %s\n", dtu.taskInfo.Id(), lastRunResult.ResultMessage)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fmt.Printf("[%s] %s, %s\n", dtu.taskInfo.Id(), lastRunResult.ResultMessage, lastRunResult.Err)
|
dtu.logf("[%s] %s, %s\n", dtu.taskInfo.Id(), lastRunResult.ResultMessage, lastRunResult.Err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (dtu *DownloadTaskUnit) pluginCallback(result string) {
|
func (dtu *DownloadTaskUnit) pluginCallback(result string) {
|
||||||
@@ -428,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)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (dtu *DownloadTaskUnit) RetryWait() time.Duration {
|
func (dtu *DownloadTaskUnit) RetryWait() time.Duration {
|
||||||
@@ -437,6 +478,7 @@ 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 {
|
||||||
@@ -453,10 +495,15 @@ 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())
|
||||||
|
}
|
||||||
|
|
||||||
// 输出文件信息
|
// 输出文件信息
|
||||||
fmt.Print("\n")
|
if dtu.UI == nil {
|
||||||
fmt.Printf("[%s] ----\n%s\n", dtu.taskInfo.Id(), dtu.fileInfo.String())
|
fmt.Print("\n")
|
||||||
|
fmt.Printf("[%s] ----\n%s\n", dtu.taskInfo.Id(), dtu.fileInfo.String())
|
||||||
|
}
|
||||||
|
|
||||||
// 调用插件
|
// 调用插件
|
||||||
ft := "file"
|
ft := "file"
|
||||||
@@ -481,7 +528,8 @@ func (dtu *DownloadTaskUnit) Run() (result *taskframework.TaskUnitRunResult) {
|
|||||||
if downloadFilePrepareResult, er := plugin.DownloadFilePrepareCallback(plugins.GetContext(config.Config.ActiveUser()), pluginParam); er == nil && downloadFilePrepareResult != nil {
|
if downloadFilePrepareResult, er := plugin.DownloadFilePrepareCallback(plugins.GetContext(config.Config.ActiveUser()), pluginParam); er == nil && downloadFilePrepareResult != nil {
|
||||||
if strings.Compare("yes", downloadFilePrepareResult.DownloadApproved) != 0 {
|
if strings.Compare("yes", downloadFilePrepareResult.DownloadApproved) != 0 {
|
||||||
// skip download this file
|
// skip download this file
|
||||||
fmt.Printf("插件取消了该文件下载: %s\n", dtu.fileInfo.Path)
|
dtu.logf("插件取消了该文件下载: %s\n", dtu.fileInfo.Path)
|
||||||
|
dtu.setUIState(ui.TaskCanceled, "取消: "+dtu.fileInfo.Path)
|
||||||
result.Succeed = false
|
result.Succeed = false
|
||||||
result.Cancel = true
|
result.Cancel = true
|
||||||
return
|
return
|
||||||
@@ -490,7 +538,7 @@ func (dtu *DownloadTaskUnit) Run() (result *taskframework.TaskUnitRunResult) {
|
|||||||
targetSaveRelativePath := strings.TrimPrefix(downloadFilePrepareResult.LocalFilePath, "/")
|
targetSaveRelativePath := strings.TrimPrefix(downloadFilePrepareResult.LocalFilePath, "/")
|
||||||
targetSaveRelativePath = strings.TrimPrefix(targetSaveRelativePath, "\\")
|
targetSaveRelativePath = strings.TrimPrefix(targetSaveRelativePath, "\\")
|
||||||
dtu.SavePath = path.Clean(dtu.OriginSaveRootPath + string(os.PathSeparator) + targetSaveRelativePath)
|
dtu.SavePath = path.Clean(dtu.OriginSaveRootPath + string(os.PathSeparator) + targetSaveRelativePath)
|
||||||
fmt.Printf("插件修改文件下载保存路径为: %s\n", dtu.SavePath)
|
dtu.logf("插件修改文件下载保存路径为: %s\n", dtu.SavePath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -563,7 +611,7 @@ func (dtu *DownloadTaskUnit) Run() (result *taskframework.TaskUnitRunResult) {
|
|||||||
|
|
||||||
// 是否排除下载
|
// 是否排除下载
|
||||||
if utils.IsExcludeFile(fileList[k].Path, &dtu.Cfg.ExcludeNames) {
|
if utils.IsExcludeFile(fileList[k].Path, &dtu.Cfg.ExcludeNames) {
|
||||||
fmt.Printf("排除文件: %s\n", fileList[k].Path)
|
dtu.logf("排除文件: %s\n", fileList[k].Path)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -580,10 +628,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())
|
||||||
fmt.Printf("[%s] 加入下载队列: %s\n", info.Id(), fileList[k].Path)
|
if dtu.UI != nil {
|
||||||
|
dtu.UI.RegisterTask(info.Id(), fileList[k].Path, fileList[k].FileSize, fileList[k].IsFile())
|
||||||
|
}
|
||||||
|
dtu.logf("[%s] 加入下载队列: %s\n", info.Id(), fileList[k].Path)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 本下载任务执行成功
|
// 本下载任务执行成功
|
||||||
@@ -591,7 +643,7 @@ func (dtu *DownloadTaskUnit) Run() (result *taskframework.TaskUnitRunResult) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("[%s] 准备下载: %s\n", dtu.taskInfo.Id(), dtu.FilePanPath)
|
dtu.logf("[%s] 准备下载: %s\n", dtu.taskInfo.Id(), dtu.FilePanPath)
|
||||||
|
|
||||||
//if !dtu.IsOverwrite && FileExist(dtu.SavePath) {
|
//if !dtu.IsOverwrite && FileExist(dtu.SavePath) {
|
||||||
// fmt.Printf("[%s] 文件已经存在: %s, 跳过...\n", dtu.taskInfo.Id(), dtu.SavePath)
|
// fmt.Printf("[%s] 文件已经存在: %s, 跳过...\n", dtu.taskInfo.Id(), dtu.SavePath)
|
||||||
@@ -600,12 +652,14 @@ func (dtu *DownloadTaskUnit) Run() (result *taskframework.TaskUnitRunResult) {
|
|||||||
//}
|
//}
|
||||||
// 支持符号文件,逻辑和注释代码一致
|
// 支持符号文件,逻辑和注释代码一致
|
||||||
if !dtu.IsOverwrite && SymlinkFileExist(dtu.SavePath, dtu.OriginSaveRootPath) {
|
if !dtu.IsOverwrite && SymlinkFileExist(dtu.SavePath, dtu.OriginSaveRootPath) {
|
||||||
fmt.Printf("[%s] 文件已经存在: %s, 跳过...\n", dtu.taskInfo.Id(), dtu.SavePath)
|
dtu.skipReason = "文件已存在,跳过: " + dtu.SavePath
|
||||||
|
dtu.setUIState(ui.TaskSkipped, dtu.skipReason)
|
||||||
|
dtu.logf("[%s] 文件已经存在: %s, 跳过...\n", dtu.taskInfo.Id(), dtu.SavePath)
|
||||||
result.Succeed = true // 执行成功
|
result.Succeed = true // 执行成功
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("[%s] 将会下载到路径: %s\n", dtu.taskInfo.Id(), dtu.SavePath)
|
dtu.logf("[%s] 将会下载到路径: %s\n", dtu.taskInfo.Id(), dtu.SavePath)
|
||||||
|
|
||||||
var ok bool
|
var ok bool
|
||||||
er := dtu.download()
|
er := dtu.download()
|
||||||
|
|||||||
@@ -0,0 +1,771 @@
|
|||||||
|
// Copyright (c) 2026.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"math"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/tickstep/library-go/converter"
|
||||||
|
"github.com/tickstep/library-go/requester/rio/speeds"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TaskState int
|
||||||
|
|
||||||
|
const (
|
||||||
|
TaskQueued TaskState = iota
|
||||||
|
TaskRunning
|
||||||
|
TaskSuccess
|
||||||
|
TaskFailed
|
||||||
|
TaskSkipped
|
||||||
|
TaskCanceled
|
||||||
|
)
|
||||||
|
|
||||||
|
type DownloadDashboardOptions struct {
|
||||||
|
Title string
|
||||||
|
MaxHistory int
|
||||||
|
ActiveSlots int
|
||||||
|
Output io.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
type DownloadDashboard struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
tasks map[string]*downloadTask
|
||||||
|
order []string
|
||||||
|
history []string
|
||||||
|
maxHistory int
|
||||||
|
parallel int
|
||||||
|
activeSlots int
|
||||||
|
globalSpeeds *speeds.Speeds
|
||||||
|
title string
|
||||||
|
startTime time.Time
|
||||||
|
renderEvery time.Duration
|
||||||
|
stopCh chan struct{}
|
||||||
|
doneCh chan struct{}
|
||||||
|
startOnce sync.Once
|
||||||
|
closeOnce sync.Once
|
||||||
|
out io.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
type downloadTask struct {
|
||||||
|
id string
|
||||||
|
path string
|
||||||
|
name string
|
||||||
|
total int64
|
||||||
|
downloaded int64
|
||||||
|
speed int64
|
||||||
|
eta time.Duration
|
||||||
|
state TaskState
|
||||||
|
message string
|
||||||
|
isFile bool
|
||||||
|
hasType bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDownloadDashboard(parallel int, globalSpeeds *speeds.Speeds, opts *DownloadDashboardOptions) *DownloadDashboard {
|
||||||
|
if parallel < 1 {
|
||||||
|
parallel = 1
|
||||||
|
}
|
||||||
|
db := &DownloadDashboard{
|
||||||
|
tasks: make(map[string]*downloadTask),
|
||||||
|
order: make([]string, 0, 128),
|
||||||
|
history: make([]string, 0, 64),
|
||||||
|
maxHistory: 8,
|
||||||
|
parallel: parallel,
|
||||||
|
activeSlots: 5,
|
||||||
|
globalSpeeds: globalSpeeds,
|
||||||
|
title: "AliyunPan Download",
|
||||||
|
renderEvery: 500 * time.Millisecond,
|
||||||
|
stopCh: make(chan struct{}),
|
||||||
|
doneCh: make(chan struct{}),
|
||||||
|
out: os.Stdout,
|
||||||
|
}
|
||||||
|
if opts != nil {
|
||||||
|
if opts.Title != "" {
|
||||||
|
db.title = opts.Title
|
||||||
|
}
|
||||||
|
if opts.MaxHistory > 0 {
|
||||||
|
db.maxHistory = opts.MaxHistory
|
||||||
|
}
|
||||||
|
if opts.ActiveSlots > 0 {
|
||||||
|
db.activeSlots = opts.ActiveSlots
|
||||||
|
}
|
||||||
|
if opts.Output != nil {
|
||||||
|
db.out = opts.Output
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if db.activeSlots < 1 {
|
||||||
|
db.activeSlots = 1
|
||||||
|
}
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DownloadDashboard) Start() {
|
||||||
|
if db == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
db.startOnce.Do(func() {
|
||||||
|
db.startTime = time.Now()
|
||||||
|
enableVirtualTerminalProcessing()
|
||||||
|
fmt.Fprint(db.out, "\x1b[?25l")
|
||||||
|
db.render()
|
||||||
|
ticker := time.NewTicker(db.renderEvery)
|
||||||
|
go func() {
|
||||||
|
defer close(db.doneCh)
|
||||||
|
defer ticker.Stop()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-db.stopCh:
|
||||||
|
db.render()
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
db.render()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DownloadDashboard) Close() {
|
||||||
|
if db == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
db.closeOnce.Do(func() {
|
||||||
|
close(db.stopCh)
|
||||||
|
<-db.doneCh
|
||||||
|
fmt.Fprint(db.out, "\x1b[?25h")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DownloadDashboard) RegisterTask(id, path string, total int64, isFile bool) {
|
||||||
|
if db == nil || id == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
db.mu.Lock()
|
||||||
|
defer db.mu.Unlock()
|
||||||
|
task := db.getOrCreateTaskLocked(id)
|
||||||
|
if path != "" {
|
||||||
|
task.path = path
|
||||||
|
}
|
||||||
|
if task.name == "" {
|
||||||
|
task.name = baseName(path)
|
||||||
|
}
|
||||||
|
if total > 0 {
|
||||||
|
task.total = total
|
||||||
|
}
|
||||||
|
task.hasType = true
|
||||||
|
task.isFile = isFile
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DownloadDashboard) UpdateTaskInfo(id, name string, total int64, isFile bool) {
|
||||||
|
if db == nil || id == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
db.mu.Lock()
|
||||||
|
defer db.mu.Unlock()
|
||||||
|
task := db.getOrCreateTaskLocked(id)
|
||||||
|
if name != "" {
|
||||||
|
task.name = name
|
||||||
|
}
|
||||||
|
if total > 0 {
|
||||||
|
task.total = total
|
||||||
|
}
|
||||||
|
task.hasType = true
|
||||||
|
task.isFile = isFile
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DownloadDashboard) UpdateTaskProgress(id string, downloaded, total, speed int64, eta time.Duration) {
|
||||||
|
if db == nil || id == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
db.mu.Lock()
|
||||||
|
defer db.mu.Unlock()
|
||||||
|
task := db.getOrCreateTaskLocked(id)
|
||||||
|
task.downloaded = downloaded
|
||||||
|
if total > 0 {
|
||||||
|
task.total = total
|
||||||
|
}
|
||||||
|
task.speed = speed
|
||||||
|
task.eta = eta
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DownloadDashboard) MarkTaskState(id string, state TaskState, message string) {
|
||||||
|
if db == nil || id == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
db.mu.Lock()
|
||||||
|
defer db.mu.Unlock()
|
||||||
|
task := db.getOrCreateTaskLocked(id)
|
||||||
|
task.state = state
|
||||||
|
if message != "" {
|
||||||
|
task.message = message
|
||||||
|
}
|
||||||
|
switch state {
|
||||||
|
case TaskSuccess, TaskFailed, TaskSkipped, TaskCanceled:
|
||||||
|
db.appendHistoryLocked(db.defaultHistoryMessage(task, message))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DownloadDashboard) Logf(format string, a ...interface{}) {
|
||||||
|
if db == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
msg := fmt.Sprintf(format, a...)
|
||||||
|
msg = strings.TrimRight(msg, "\r\n")
|
||||||
|
if msg == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
db.mu.Lock()
|
||||||
|
defer db.mu.Unlock()
|
||||||
|
db.appendHistoryLocked(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DownloadDashboard) getOrCreateTaskLocked(id string) *downloadTask {
|
||||||
|
if task, ok := db.tasks[id]; ok {
|
||||||
|
return task
|
||||||
|
}
|
||||||
|
task := &downloadTask{
|
||||||
|
id: id,
|
||||||
|
state: TaskQueued,
|
||||||
|
}
|
||||||
|
db.tasks[id] = task
|
||||||
|
db.order = append(db.order, id)
|
||||||
|
return task
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DownloadDashboard) appendHistoryLocked(message string) {
|
||||||
|
if message == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
timestamp := time.Now().Format("15:04:05")
|
||||||
|
db.history = append(db.history, fmt.Sprintf("[%s] %s", timestamp, message))
|
||||||
|
if len(db.history) > db.maxHistory*3 {
|
||||||
|
db.history = db.history[len(db.history)-db.maxHistory*2:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DownloadDashboard) defaultHistoryMessage(task *downloadTask, message string) string {
|
||||||
|
if message != "" {
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
name := task.path
|
||||||
|
if name == "" {
|
||||||
|
name = task.name
|
||||||
|
}
|
||||||
|
switch task.state {
|
||||||
|
case TaskSuccess:
|
||||||
|
return "完成: " + name
|
||||||
|
case TaskFailed:
|
||||||
|
return "失败: " + name
|
||||||
|
case TaskSkipped:
|
||||||
|
return "跳过: " + name
|
||||||
|
case TaskCanceled:
|
||||||
|
return "取消: " + name
|
||||||
|
default:
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DownloadDashboard) render() {
|
||||||
|
if db == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
width, height := terminalSize()
|
||||||
|
if width < 60 {
|
||||||
|
width = 60
|
||||||
|
}
|
||||||
|
innerWidth := width - 2
|
||||||
|
snapshot := db.snapshot()
|
||||||
|
|
||||||
|
headerLines := db.headerLines(snapshot, innerWidth)
|
||||||
|
activeLines := db.activeLines(snapshot, innerWidth)
|
||||||
|
activeTitle := db.activeTitle(snapshot)
|
||||||
|
|
||||||
|
headerHeight := len(headerLines) + 2
|
||||||
|
activeHeight := len(activeLines) + 4
|
||||||
|
historyBase := 4
|
||||||
|
minHistory := 1
|
||||||
|
minHeight := headerHeight + activeHeight + historyBase + minHistory
|
||||||
|
if height < minHeight {
|
||||||
|
height = minHeight
|
||||||
|
}
|
||||||
|
historyLines := height - (headerHeight + activeHeight + historyBase)
|
||||||
|
if historyLines < minHistory {
|
||||||
|
historyLines = minHistory
|
||||||
|
}
|
||||||
|
historyBody := db.historyLines(snapshot, innerWidth, historyLines)
|
||||||
|
|
||||||
|
builder := &strings.Builder{}
|
||||||
|
builder.WriteString("\x1b[H\x1b[2J")
|
||||||
|
builder.WriteString(renderBox(headerLines, width))
|
||||||
|
builder.WriteString("\n")
|
||||||
|
builder.WriteString(renderTitledBox(activeTitle, activeLines, width))
|
||||||
|
builder.WriteString("\n")
|
||||||
|
builder.WriteString(renderTitledBox("History", historyBody, width))
|
||||||
|
|
||||||
|
fmt.Fprint(db.out, builder.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
type dashboardSnapshot struct {
|
||||||
|
tasks []*downloadTask
|
||||||
|
history []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DownloadDashboard) snapshot() dashboardSnapshot {
|
||||||
|
db.mu.Lock()
|
||||||
|
defer db.mu.Unlock()
|
||||||
|
tasks := make([]*downloadTask, 0, len(db.order))
|
||||||
|
for _, id := range db.order {
|
||||||
|
if task, ok := db.tasks[id]; ok {
|
||||||
|
copied := *task
|
||||||
|
tasks = append(tasks, &copied)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
history := append([]string(nil), db.history...)
|
||||||
|
return dashboardSnapshot{tasks: tasks, history: history}
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderBox(lines []string, width int) string {
|
||||||
|
if width < 4 {
|
||||||
|
width = 4
|
||||||
|
}
|
||||||
|
innerWidth := width - 2
|
||||||
|
builder := &strings.Builder{}
|
||||||
|
builder.WriteString("\u250c")
|
||||||
|
builder.WriteString(strings.Repeat("\u2500", innerWidth))
|
||||||
|
builder.WriteString("\u2510")
|
||||||
|
for _, line := range lines {
|
||||||
|
builder.WriteString("\n")
|
||||||
|
builder.WriteString("\u2502")
|
||||||
|
builder.WriteString(fitLine(line, innerWidth))
|
||||||
|
builder.WriteString("\u2502")
|
||||||
|
}
|
||||||
|
builder.WriteString("\n")
|
||||||
|
builder.WriteString("\u2514")
|
||||||
|
builder.WriteString(strings.Repeat("\u2500", innerWidth))
|
||||||
|
builder.WriteString("\u2518")
|
||||||
|
return builder.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderTitledBox(title string, body []string, width int) string {
|
||||||
|
if width < 4 {
|
||||||
|
width = 4
|
||||||
|
}
|
||||||
|
innerWidth := width - 2
|
||||||
|
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 {
|
||||||
|
builder.WriteString("\n")
|
||||||
|
builder.WriteString("\u2502")
|
||||||
|
builder.WriteString(fitLine(line, innerWidth))
|
||||||
|
builder.WriteString("\u2502")
|
||||||
|
}
|
||||||
|
builder.WriteString("\n")
|
||||||
|
builder.WriteString("\u2514")
|
||||||
|
builder.WriteString(strings.Repeat("\u2500", innerWidth))
|
||||||
|
builder.WriteString("\u2518")
|
||||||
|
return builder.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DownloadDashboard) headerLines(snapshot dashboardSnapshot, innerWidth int) []string {
|
||||||
|
totalFiles, doneFiles, failedFiles := db.fileCounts(snapshot.tasks)
|
||||||
|
downloaded, total := db.totalProgress(snapshot.tasks)
|
||||||
|
speed := db.totalSpeed(snapshot.tasks)
|
||||||
|
elapsed := time.Since(db.startTime)
|
||||||
|
left := "-"
|
||||||
|
if speed > 0 && total > downloaded {
|
||||||
|
left = formatDurationShort(time.Duration((total-downloaded)/speed) * time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
status := "Running"
|
||||||
|
if totalFiles > 0 && doneFiles >= totalFiles {
|
||||||
|
if failedFiles > 0 {
|
||||||
|
status = "Completed (errors)"
|
||||||
|
} else {
|
||||||
|
status = "Completed"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
headerLine := fmt.Sprintf("%s [%s]", db.title, status)
|
||||||
|
line1 := fitLine(headerLine, innerWidth)
|
||||||
|
|
||||||
|
speedStr := converter.ConvertFileSize(speed, 2) + "/s"
|
||||||
|
line2 := fmt.Sprintf("Total Speed: %s | Files: %d/%d | Failed: %d | Elapsed: %s | Left: %s",
|
||||||
|
speedStr, doneFiles, totalFiles, failedFiles, formatDurationShort(elapsed), left)
|
||||||
|
line2 = fitLine(line2, innerWidth)
|
||||||
|
|
||||||
|
progressPct := 0.0
|
||||||
|
if total > 0 {
|
||||||
|
progressPct = float64(downloaded) / float64(total)
|
||||||
|
}
|
||||||
|
progress := fmt.Sprintf("%s/%s", converter.ConvertFileSize(downloaded, 2), converter.ConvertFileSize(total, 2))
|
||||||
|
label := "Total Progress: "
|
||||||
|
suffix := fmt.Sprintf(" %5.1f%% (%s)", progressPct*100, progress)
|
||||||
|
barWidth := innerWidth - displayWidth(label) - displayWidth(suffix) - 2
|
||||||
|
if barWidth < 10 {
|
||||||
|
barWidth = 10
|
||||||
|
}
|
||||||
|
bar := renderBar(progressPct, barWidth)
|
||||||
|
line3 := fmt.Sprintf("%s[%s]%s", label, bar, suffix)
|
||||||
|
line3 = fitLine(line3, innerWidth)
|
||||||
|
|
||||||
|
return []string{line1, line2, line3}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DownloadDashboard) activeTitle(snapshot dashboardSnapshot) string {
|
||||||
|
running := 0
|
||||||
|
queued := 0
|
||||||
|
for _, task := range snapshot.tasks {
|
||||||
|
if task.hasType && !task.isFile {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch task.state {
|
||||||
|
case TaskRunning:
|
||||||
|
running++
|
||||||
|
case TaskQueued:
|
||||||
|
queued++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("Active Downloads (slots %d, running %d, queued %d)", db.activeSlots, running, queued)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DownloadDashboard) activeLines(snapshot dashboardSnapshot, innerWidth int) []string {
|
||||||
|
active := db.pickActiveTasks(snapshot.tasks)
|
||||||
|
lines := make([]string, 0, db.activeSlots)
|
||||||
|
for i := 0; i < db.activeSlots; i++ {
|
||||||
|
if i < len(active) {
|
||||||
|
lines = append(lines, renderTaskLine(active[i], i, innerWidth))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
placeholder := &downloadTask{name: "waiting...", state: TaskQueued}
|
||||||
|
lines = append(lines, renderTaskLine(placeholder, i, innerWidth))
|
||||||
|
}
|
||||||
|
return lines
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DownloadDashboard) historyLines(snapshot dashboardSnapshot, innerWidth int, lines int) []string {
|
||||||
|
if lines < 1 {
|
||||||
|
lines = 1
|
||||||
|
}
|
||||||
|
maxLines := lines
|
||||||
|
if db.maxHistory > 0 && maxLines > db.maxHistory {
|
||||||
|
maxLines = db.maxHistory
|
||||||
|
}
|
||||||
|
if maxLines > len(snapshot.history) {
|
||||||
|
maxLines = len(snapshot.history)
|
||||||
|
}
|
||||||
|
result := make([]string, 0, lines)
|
||||||
|
start := len(snapshot.history) - maxLines
|
||||||
|
if start < 0 {
|
||||||
|
start = 0
|
||||||
|
}
|
||||||
|
for i := len(snapshot.history) - 1; i >= start && len(result) < maxLines; i-- {
|
||||||
|
result = append(result, truncateText(snapshot.history[i], innerWidth))
|
||||||
|
}
|
||||||
|
if len(result) == 0 {
|
||||||
|
result = append(result, truncateText("No recent events.", innerWidth))
|
||||||
|
}
|
||||||
|
for len(result) < lines {
|
||||||
|
result = append(result, "")
|
||||||
|
}
|
||||||
|
return result[:lines]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DownloadDashboard) fileCounts(tasks []*downloadTask) (total int, done int, failed int) {
|
||||||
|
for _, task := range tasks {
|
||||||
|
if task.hasType && !task.isFile {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
total++
|
||||||
|
switch task.state {
|
||||||
|
case TaskSuccess, TaskSkipped, TaskFailed, TaskCanceled:
|
||||||
|
done++
|
||||||
|
}
|
||||||
|
if task.state == TaskFailed {
|
||||||
|
failed++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DownloadDashboard) totalProgress(tasks []*downloadTask) (downloaded int64, total int64) {
|
||||||
|
for _, task := range tasks {
|
||||||
|
if task.hasType && !task.isFile {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if task.total > 0 {
|
||||||
|
total += task.total
|
||||||
|
if task.downloaded > task.total {
|
||||||
|
downloaded += task.total
|
||||||
|
} else {
|
||||||
|
downloaded += task.downloaded
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DownloadDashboard) totalSpeed(tasks []*downloadTask) int64 {
|
||||||
|
if db.globalSpeeds != nil {
|
||||||
|
return db.globalSpeeds.GetSpeeds()
|
||||||
|
}
|
||||||
|
var sum int64
|
||||||
|
for _, task := range tasks {
|
||||||
|
sum += task.speed
|
||||||
|
}
|
||||||
|
return sum
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DownloadDashboard) pickActiveTasks(tasks []*downloadTask) []*downloadTask {
|
||||||
|
slotCap := db.activeSlots
|
||||||
|
if slotCap < 1 {
|
||||||
|
slotCap = 1
|
||||||
|
}
|
||||||
|
active := make([]*downloadTask, 0, slotCap)
|
||||||
|
queued := make([]*downloadTask, 0, slotCap)
|
||||||
|
for _, task := range tasks {
|
||||||
|
if task.hasType && !task.isFile {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch task.state {
|
||||||
|
case TaskRunning:
|
||||||
|
active = append(active, task)
|
||||||
|
case TaskQueued:
|
||||||
|
queued = append(queued, task)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
active = append(active, queued...)
|
||||||
|
if len(active) > slotCap {
|
||||||
|
active = active[:slotCap]
|
||||||
|
}
|
||||||
|
return active
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderTaskLine(task *downloadTask, index int, width int) string {
|
||||||
|
if width < 20 {
|
||||||
|
width = 20
|
||||||
|
}
|
||||||
|
prefix := fmt.Sprintf("%2d. ", index+1)
|
||||||
|
name := task.name
|
||||||
|
if name == "" {
|
||||||
|
name = baseName(task.path)
|
||||||
|
}
|
||||||
|
|
||||||
|
progressPct := 0.0
|
||||||
|
percent := "--.-%"
|
||||||
|
if task.total > 0 {
|
||||||
|
progressPct = float64(task.downloaded) / float64(task.total)
|
||||||
|
percent = fmt.Sprintf("%5.1f%%", math.Min(100, progressPct*100))
|
||||||
|
}
|
||||||
|
|
||||||
|
rate := "-"
|
||||||
|
eta := "-"
|
||||||
|
switch task.state {
|
||||||
|
case TaskQueued:
|
||||||
|
rate = "waiting"
|
||||||
|
case TaskRunning:
|
||||||
|
if task.speed > 0 {
|
||||||
|
rate = converter.ConvertFileSize(task.speed, 2) + "/s"
|
||||||
|
} else {
|
||||||
|
rate = "0 B/s"
|
||||||
|
}
|
||||||
|
if task.eta > 0 {
|
||||||
|
eta = formatDurationShort(task.eta)
|
||||||
|
}
|
||||||
|
case TaskSuccess:
|
||||||
|
rate = "done"
|
||||||
|
case TaskFailed:
|
||||||
|
rate = "failed"
|
||||||
|
case TaskSkipped:
|
||||||
|
rate = "skipped"
|
||||||
|
case TaskCanceled:
|
||||||
|
rate = "canceled"
|
||||||
|
}
|
||||||
|
|
||||||
|
barWidth := 20
|
||||||
|
rateWidth := 12
|
||||||
|
etaWidth := 8
|
||||||
|
percentWidth := displayWidth(percent)
|
||||||
|
fixed := displayWidth(prefix) + 1 + 2 + barWidth + 1 + percentWidth + 1 + rateWidth + 1 + etaWidth
|
||||||
|
nameWidth := width - fixed
|
||||||
|
if nameWidth < 8 {
|
||||||
|
reduce := 8 - nameWidth
|
||||||
|
if barWidth > 10 {
|
||||||
|
cut := minInt(reduce, barWidth-10)
|
||||||
|
barWidth -= cut
|
||||||
|
reduce -= cut
|
||||||
|
}
|
||||||
|
if rateWidth > 8 && reduce > 0 {
|
||||||
|
cut := minInt(reduce, rateWidth-8)
|
||||||
|
rateWidth -= cut
|
||||||
|
reduce -= cut
|
||||||
|
}
|
||||||
|
if etaWidth > 5 && reduce > 0 {
|
||||||
|
cut := minInt(reduce, etaWidth-5)
|
||||||
|
etaWidth -= cut
|
||||||
|
reduce -= cut
|
||||||
|
}
|
||||||
|
fixed = displayWidth(prefix) + 1 + 2 + barWidth + 1 + percentWidth + 1 + rateWidth + 1 + etaWidth
|
||||||
|
nameWidth = width - fixed
|
||||||
|
if nameWidth < 4 {
|
||||||
|
nameWidth = 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
name = padRight(truncateText(name, nameWidth), nameWidth)
|
||||||
|
bar := renderBar(progressPct, barWidth)
|
||||||
|
rate = padRight(truncateText(rate, rateWidth), rateWidth)
|
||||||
|
eta = padRight(truncateText(eta, etaWidth), etaWidth)
|
||||||
|
line := fmt.Sprintf("%s%s [%s] %s %s %s", prefix, name, bar, percent, rate, eta)
|
||||||
|
return fitLine(line, width)
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderBar(pct float64, width int) string {
|
||||||
|
if width < 5 {
|
||||||
|
width = 5
|
||||||
|
}
|
||||||
|
if pct < 0 {
|
||||||
|
pct = 0
|
||||||
|
}
|
||||||
|
if pct > 1 {
|
||||||
|
pct = 1
|
||||||
|
}
|
||||||
|
filled := int(math.Floor(pct * float64(width)))
|
||||||
|
return strings.Repeat("\u2588", filled) + strings.Repeat("\u2591", width-filled)
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatDurationShort(d time.Duration) string {
|
||||||
|
if d <= 0 {
|
||||||
|
return "-"
|
||||||
|
}
|
||||||
|
seconds := int(d.Seconds())
|
||||||
|
if seconds < 60 {
|
||||||
|
return fmt.Sprintf("%ds", seconds)
|
||||||
|
}
|
||||||
|
minutes := seconds / 60
|
||||||
|
seconds = seconds % 60
|
||||||
|
if minutes < 60 {
|
||||||
|
return fmt.Sprintf("%dm%02ds", minutes, seconds)
|
||||||
|
}
|
||||||
|
hours := minutes / 60
|
||||||
|
minutes = minutes % 60
|
||||||
|
if hours < 100 {
|
||||||
|
return fmt.Sprintf("%dh%02dm", hours, minutes)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%dh", hours)
|
||||||
|
}
|
||||||
|
|
||||||
|
func truncateText(text string, max int) string {
|
||||||
|
if max <= 0 || text == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if displayWidth(text) <= max {
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
if max <= 3 {
|
||||||
|
return takeWidth(text, max)
|
||||||
|
}
|
||||||
|
return takeWidth(text, max-3) + "..."
|
||||||
|
}
|
||||||
|
|
||||||
|
func padRight(text string, width int) string {
|
||||||
|
if width <= 0 {
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
if displayWidth(text) >= width {
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
return text + strings.Repeat(" ", width-displayWidth(text))
|
||||||
|
}
|
||||||
|
|
||||||
|
func fitLine(text string, width int) string {
|
||||||
|
return padRight(truncateText(text, width), width)
|
||||||
|
}
|
||||||
|
|
||||||
|
func displayWidth(text string) int {
|
||||||
|
width := 0
|
||||||
|
for _, r := range text {
|
||||||
|
width += runeWidth(r)
|
||||||
|
}
|
||||||
|
return width
|
||||||
|
}
|
||||||
|
|
||||||
|
func takeWidth(text string, max int) string {
|
||||||
|
if max <= 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
used := 0
|
||||||
|
for idx, r := range text {
|
||||||
|
w := runeWidth(r)
|
||||||
|
if used+w > max {
|
||||||
|
return text[:idx]
|
||||||
|
}
|
||||||
|
used += w
|
||||||
|
}
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|
||||||
|
func runeWidth(r rune) int {
|
||||||
|
if r == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if r < 0x1100 {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case r >= 0x1100 && r <= 0x115f,
|
||||||
|
r == 0x2329 || r == 0x232a,
|
||||||
|
r >= 0x2e80 && r <= 0xa4cf && r != 0x303f,
|
||||||
|
r >= 0xac00 && r <= 0xd7a3,
|
||||||
|
r >= 0xf900 && r <= 0xfaff,
|
||||||
|
r >= 0xfe10 && r <= 0xfe19,
|
||||||
|
r >= 0xfe30 && r <= 0xfe6f,
|
||||||
|
r >= 0xff00 && r <= 0xff60,
|
||||||
|
r >= 0xffe0 && r <= 0xffe6,
|
||||||
|
r >= 0x20000 && r <= 0x2fffd,
|
||||||
|
r >= 0x30000 && r <= 0x3fffd:
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func minInt(a, b int) int {
|
||||||
|
if a < b {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func baseName(path string) string {
|
||||||
|
if path == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
path = strings.ReplaceAll(path, "\\", "/")
|
||||||
|
if idx := strings.LastIndex(path, "/"); idx >= 0 {
|
||||||
|
return path[idx+1:]
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
// Copyright (c) 2026.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
package ui
|
||||||
|
|
||||||
|
import "os"
|
||||||
|
|
||||||
|
func IsTerminal(file *os.File) bool {
|
||||||
|
if file == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
fi, err := file.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return (fi.Mode() & os.ModeCharDevice) != 0
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
// Copyright (c) 2026.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
func terminalSize() (int, int) {
|
||||||
|
ws, err := unix.IoctlGetWinsize(int(os.Stdout.Fd()), unix.TIOCGWINSZ)
|
||||||
|
if err != nil || ws == nil || ws.Col == 0 || ws.Row == 0 {
|
||||||
|
return 80, 24
|
||||||
|
}
|
||||||
|
return int(ws.Col), int(ws.Row)
|
||||||
|
}
|
||||||
|
|
||||||
|
func enableVirtualTerminalProcessing() {}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
// Copyright (c) 2026.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
func terminalSize() (int, int) {
|
||||||
|
handle := windows.Handle(os.Stdout.Fd())
|
||||||
|
var info windows.ConsoleScreenBufferInfo
|
||||||
|
if err := windows.GetConsoleScreenBufferInfo(handle, &info); err != nil {
|
||||||
|
return 80, 24
|
||||||
|
}
|
||||||
|
width := int(info.Window.Right - info.Window.Left + 1)
|
||||||
|
height := int(info.Window.Bottom - info.Window.Top + 1)
|
||||||
|
if width <= 0 || height <= 0 {
|
||||||
|
return 80, 24
|
||||||
|
}
|
||||||
|
return width, height
|
||||||
|
}
|
||||||
|
|
||||||
|
func enableVirtualTerminalProcessing() {
|
||||||
|
handle := windows.Handle(os.Stdout.Fd())
|
||||||
|
var mode uint32
|
||||||
|
if err := windows.GetConsoleMode(handle, &mode); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mode |= windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING
|
||||||
|
_ = windows.SetConsoleMode(handle, mode)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user