Compare commits

..

47 Commits

Author SHA1 Message Date
xiaoyaofenfen d05d19acd8 Merge pull request #548 from qinweixian/patch-1
修复后台上传脚本bug
2026-09-07 09:29:06 +08:00
秦伟先 5179f914ed 修复后台上传脚本bug 2026-09-03 10:42:52 +08:00
tickstep 2feaa3cf82 update readme file 2026-07-05 09:42:32 +08:00
tickstep studio team cedd43d402 update version to v0.4.0 2026-06-29 10:42:16 +08:00
tickstep studio team e62bdbb9f1 refractor OSS url parser 2026-06-29 10:27:00 +08:00
tickstep studio team acc408f0a0 fix download url expired calc error issue #544 #543 #542 2026-06-29 09:29:05 +08:00
tickstep studio team db28ea6732 update version to v0.3.9 2026-04-20 19:41:14 +08:00
tickstep studio team a9bc1076a5 update aliyunpan-api to v0.2.9 2026-04-20 19:12:16 +08:00
tickstep e20ea479c4 fix update config file error 2026-04-12 08:32:26 +08:00
tickstep studio team 42f044ba38 update readme file 2026-03-02 11:17:31 +08:00
tickstep studio team 88f1ff2688 update manual file 2026-03-02 11:05:41 +08:00
feny ca09039e48 fix the issue that -ui print output flickering 2026-02-18 08:24:10 +08:00
tickstep studio team cf38ad51fd add -ui option for upload 2026-02-13 17:27:50 +08:00
tickstep studio team 30cbced50c rename download_dashboard to dashboard_panel 2026-02-13 17:23:09 +08:00
tickstep studio team 0946939325 update manual file 2026-02-12 11:03:58 +08:00
tickstep studio team da13b046b8 update readme file 2026-02-12 10:56:37 +08:00
tickstep studio team 268e4a30d1 update readme file 2026-02-12 10:51:48 +08:00
tickstep studio team a0ca0f8089 update readme file 2026-02-12 10:44:34 +08:00
tickstep studio team 2d99a39606 Merge branch 'main' of github.com:tickstep/aliyunpan 2026-02-12 10:35:25 +08:00
tickstep 2965179ae8 update version to v0.3.8 2026-02-11 20:23:40 +08:00
tickstep 0565482121 update golang.org/x/sys to v0.15.0 2026-02-11 20:21:14 +08:00
tickstep 1822d3c2b2 Merge branch 'main' of github.com:tickstep/aliyunpan 2026-02-11 19:33:53 +08:00
tickstep 6672aa4cd4 update aliyunpan-api to v0.2.8 2026-02-11 19:33:37 +08:00
tickstep studio team c69a27c9b1 add verbose log for upload 2026-02-10 15:34:07 +08:00
tickstep studio team 4feacbcdbf fix login get ip error issue 2026-02-10 15:33:49 +08:00
tickstep studio team 1916dda8f8 fix update issue 2026-02-10 14:52:34 +08:00
tickstep studio team 8b5eb14eb6 add more log to track download url expired issue #501 2026-02-05 20:47:19 +08:00
tickstep studio team d3d9690d55 beautify download ui dashboard 2026-02-05 20:14:56 +08:00
tickstep studio team 89a6ef326c beautify download ui dashboard 2026-02-05 17:31:21 +08:00
tickstep studio team 35dfb35429 handle file already closed error for upload #509 #512 2026-02-04 17:46:11 +08:00
tickstep studio team 3c7ebb5f62 optimize rm command, faster remove file without wildcard #530 2026-02-04 11:00:38 +08:00
tickstep f796f40664 optimize update logic 2026-02-03 21:25:21 +08:00
tickstep studio team 0d9700c84c fix the error in the total download size statistics 2026-02-03 16:41:40 +08:00
tickstep studio team 09ee162ba4 add slice parallel option for downoad #529 2026-02-03 16:30:36 +08:00
tickstep studio team ff0e3dbfa9 optimize downoad ui dashboard 2026-02-03 15:52:06 +08:00
tickstep studio team 4c5f45e5ab delete useless code 2026-02-03 15:02:20 +08:00
tickstep studio team 02326b0bd7 fix terminal abnormalities 2026-02-03 14:40:04 +08:00
tickstep 350fec4275 add -ui option for download 2026-02-03 11:23:57 +08:00
xiaoyaofenfen 70a14dd1a6 Merge pull request #525 from SpookySandwich/main
【功能建议】改善下载界面
2026-02-02 18:30:37 +08:00
CharliePu 23dad72914 improve download tui 2026-01-06 17:50:41 +08:00
xiaoyaofenfen 9fa547ddc8 Merge pull request #520 from sikaisky/main
fix uploadEntity.PartInfoList len 0 error
2025-11-25 14:54:25 +08:00
sikai 2898037ec5 fix uploadEntity.PartInfoList len 0 error 2025-11-09 10:26:19 +08:00
tickstep 70c14fd66f open file with readonly mode 2025-08-11 11:17:20 +08:00
tickstep 8913597716 optimize login routine logic 2025-08-07 17:43:14 +08:00
tickstep 9b276f2caf optimize token refresh task 2025-08-06 15:34:36 +08:00
tickstep e8bda159fa handle api common error 2025-08-05 23:08:24 +08:00
tickstep e1ac6a95b9 fix share url error 2025-04-15 21:47:00 +08:00
40 changed files with 1977 additions and 369 deletions
+45 -8
View File
@@ -66,9 +66,9 @@
参考例子:
```shell
wget https://github.com/tickstep/aliyunpan/releases/download/v0.3.7/aliyunpan-v0.3.7-linux-amd64.zip
unzip aliyunpan-v0.3.7-linux-amd64.zip
cd aliyunpan-v0.3.7-linux-amd64
wget https://github.com/tickstep/aliyunpan/releases/download/v0.4.0/aliyunpan-v0.4.0-linux-amd64.zip
unzip aliyunpan-v0.4.0-linux-amd64.zip
cd aliyunpan-v0.4.0-linux-amd64
./aliyunpan
```
@@ -107,7 +107,7 @@ winget source update
```
安装
```powershell
winget install tickstep.aliyunpan --silent
winget install tickstep.aliyunpan --source winget --silent
```
@@ -126,7 +126,7 @@ winget install tickstep.aliyunpan --silent
![](./assets/images/sync_command-basic_logic.jpg)
```
docker run -d --name=aliyunpan-sync --restart=always -v "<your aliyunpan_config.json>:/home/app/config/aliyunpan_config.json" -v "<your local dir>:/home/app/data" -e ALIYUNPAN_PAN_DIR="<your drive pan dir>" -e ALIYUNPAN_SYNC_MODE="upload" -e ALIYUNPAN_SYNC_POLICY="increment" -e ALIYUNPAN_SYNC_DRIVE="backup" -e ALIYUNPAN_SYNC_LOG="true" tickstep/aliyunpan-sync:v0.3.7
docker run -d --name=aliyunpan-sync --restart=always -v "<your aliyunpan_config.json>:/home/app/config/aliyunpan_config.json" -v "<your local dir>:/home/app/data" -e ALIYUNPAN_PAN_DIR="<your drive pan dir>" -e ALIYUNPAN_SYNC_MODE="upload" -e ALIYUNPAN_SYNC_POLICY="increment" -e ALIYUNPAN_SYNC_DRIVE="backup" -e ALIYUNPAN_SYNC_LOG="true" tickstep/aliyunpan-sync:v0.4.0
<your aliyunpan_config.json>: 用户已经登录成功并保存好的aliyunpan_config.json凭据文件
<your local dir>:本地目标目录,绝对路径,例如:/tickstep/Documents/设计文档
@@ -241,7 +241,7 @@ aliyunpan:/ tickstep$ ls
```
### 下载文件
通过 `aliyunpan config set -savedir <savedir>` 可以自定义下载文件保存的目录。
如果需要更改默认下载目录路径,可以通过 `aliyunpan config set -savedir <savedir>` 自定义下载文件保存的目录。
```shell
aliyunpan:/ tickstep$ download IMG_0106.JPG
@@ -261,7 +261,25 @@ aliyunpan:/ tickstep$ download IMG_0106.JPG
[1] 检验文件有效性成功: /root/Downloads/4d001d48564f43b..62874f04bbe6/IMG_0106.JPG
下载结束, 时间: 4秒, 数据总量: 1.48MB
aliyunpan:/ tickstep$
```
或者使用UI面板展示下载进度(使用-ui选项),更加直观和友好。
```shell
aliyunpan:/ tickstep$ download /photo -ui
┌──────────────────────────────────────────────────────────────────────────────────────────┐
│总速度: 341.59 KB/s | 文件: 0/7 | 失败: 0 | 状态: 下载中 | 已用时间: 9s | 剩余时间: 51s │
│总进度: [===> ] 7.4% (896.00KB/11.83MB) │
│──────────────────────────────────────────────────────────────────────────────────────────│
│ 1. IMG_0005.JPG [============> ] 66.9% 258.42KB/s 3s │
│ 2. IMG_0007.JPG [============> ] 67.3% 258.54KB/s 3s │
│ 3. IMG_0009.JPG [ ] 0.0% 等待中 - │
│──────────────────────────────────────────────────────────────────────────────────────────│
│[10:39:24] [0] 当前文件下载最大并发量为: 2, 单文件下载分片线程数为: 1, 下载缓存为: 64.00KB │
│[10:39:27] [2] 加入下载队列: /photo/p/IMG_0007.JPG │
│[10:39:27] [2] ---- 文件ID: 67dfe476a7573e76a2034626bd013f36581296ed 文件名: IMG_0007.... │
│[10:39:27] [3] 准备下载: /photo/p/IMG_0007.JPG │
│[10:39:27] [2] 将会下载到路径: /Users/tickstep/Downloads/4d001d48564f43b3bc5662874f04bb... │
│[10:39:28] [2] 下载开始 │
└──────────────────────────────────────────────────────────────────────────────────────────┘
```
### 上传文件
@@ -281,9 +299,28 @@ aliyunpan:/ tickstep$ upload /Users/tickstep/Downloads/apt.zip /tmp
上传结束, 时间: 18秒, 数据总量: 21.00MB
```
或者使用UI面板展示上传进度(使用-ui选项),更加直观和友好。
```shell
aliyunpan:/ tickstep$ upload /Users/tickstep/Downloads /photo -ui
┌─────────────────────────────────────────────────────────────────────────────────────────┐
│总速度: 3.62 MB/s | 文件: 9/14 | 失败: 0 | 状态: 上传中 | 已用时间: 12s | 剩余时间: 17s │
│总进度: [==========================> ] 51.7% (68.56MB/132.73MB) │
│─────────────────────────────────────────────────────────────────────────────────────────│
│ 1. System Design Interview 2.zip [====> ] 26.2% 3.25MB/s 18s │
│ 2. ips_20260228.txt [ ] 0.0% 0 B/s - │
│ 3. neko-agent [ ] 0.0% 0 B/s - │
│─────────────────────────────────────────────────────────────────────────────────────────│
│[11:14:49] [9] 2026-03-02 11:14:26 正在计算文件SHA1: /Users/tickstep/Downloads/test/da... │
│[11:14:49] [13] 2026-03-02 11:14:26 正在检测和创建云盘文件夹: /photo/p2/test/output │
│[11:14:49] [9] 2026-03-02 11:14:26 检测秒传中, 请稍候... │
│[11:14:49] [9] 2026-03-02 11:14:26 秒传成功, 保存到网盘路径: /photo/p2/test/data/ips_2... │
│[11:14:49] [9] 2026-03-02 11:14:26 文件上传结果: 成功! 耗时 12秒 │
│[11:14:49] 完成: /Users/tickstep/Downloads/test/data/ips_20260226.txt │
└─────────────────────────────────────────────────────────────────────────────────────────┘
```
### 同步备份文件
同步备份功能,支持备份本地文件到云盘,备份云盘文件到本地,双向同步备份三种模式。支持JavaScript插件对备份文件进行过滤。
同步备份功能,支持备份本地文件到云盘,备份云盘文件到本地两种模式。支持JavaScript插件对备份文件进行过滤。
例如:将本地目录 `/tickstep/Documents/设计文档` 中的文件备份上传到云盘目录 `/备份盘/我的文档`
```shell
+1 -1
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
<assemblyIdentity version="0.3.7.0" processorArchitecture="*" name="com.tickstep.aliyunpan" type="win32"/>
<assemblyIdentity version="0.4.0.0" processorArchitecture="*" name="com.tickstep.aliyunpan" type="win32"/>
<dependency>
<dependentAssembly>
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
+47 -43
View File
@@ -20,7 +20,6 @@
* [创建目录](#创建目录)
* [删除文件/目录](#删除文件目录)
* [移动文件/目录](#移动文件目录)
* [备份盘和资源库之间转存文件](#备份盘和资源库之间转存文件)
* [重命名文件/目录](#重命名文件目录)
* [分享文件/目录](#分享文件目录)
+ [设置分享文件/目录](#设置分享文件目录)
@@ -217,18 +216,20 @@ aliyunpan d <网盘文件或目录的路径1> <文件或目录2> <文件或目
### 可选参数
```
--ow overwrite, 覆盖已存在的文件
--skip skip same name, 跳过已存在的同名文件,即使文件内容不一致(不检查SHA1)
--status 输出所有线程的工作状态
--save 将下载的文件直接保存到当前工作目录
--saveto value 将下载的文件直接保存到指定的目录
-x 为文件加上执行权限, (windows系统无效)
-p value 指定下载线程数 (default: 0)
-l value 指定同时进行下载文件的数量 (default: 0)
--retry value 下载失败最大重试次数 (default: 3)
--nocheck 下载文件完成后不校验文件
--exn value 指定排除的文件夹或者文件的名称,只支持正则表达式。支持排除多个名称,每一个名称就是一个exn参数
--md (BETA) Multi-User Download,使用多用户联合下载,可以对单一文件叠加所有登录用户的下载速度
--ow overwrite, 覆盖已存在的文件
--status 输出所有线程的工作状态
--save 将下载的文件直接保存到当前工作目录
--saveto value 将下载的文件直接保存到指定的目录
-x 为文件加上执行权限, (windows系统无效)
-p value parallel,指定同时进行下载文件的数量(取值范围:1 ~ 3) (default: 1)
--sp value slice parallel,指定单个文件下载的最大线程(分片)数(取值范围:1 ~ 3) (default: 0)
--retry value 下载失败最大重试次数 (default: 3)
--nocheck 下载文件完成后不校验文件
--np no progress 不展示下载进度条
--driveId value 网盘ID
--exn value exclude name,指定排除的文件夹或者文件的名称,被排除的文件不会进行下载,只支持正则表达式。支持同时排除多个名称,每一个名称就是一个exn参数
--md (BETA) Multi-User Download,使用多用户联合下载,可以对单一文件叠加所有登录用户的下载速度
--ui (BETA) 使用UI面板显示下载详情和进度,更加直观和友好
```
@@ -243,6 +244,9 @@ aliyunpan d /我的文档/1.mp4
# 下载 /我的文档 整个目录!!
aliyunpan d /我的文档
# 下载 /photo 整个目录,并使用UI面板展示下载详情
aliyunpan d /photo -ui
```
下载的文件默认保存到 **程序所在目录** 的 download/ 目录, 支持设置指定目录, 重名的文件会自动跳过!
@@ -314,6 +318,9 @@ aliyunpan upload C:/Users/Administrator/Desktop/1.mp4 C:/Users/Administrator/Des
# 将本地的 C:\Users\Administrator\Desktop 整个目录上传到网盘 /视频 目录
aliyunpan upload C:/Users/Administrator/Desktop /视频
# 将本地的 C:\Users\Administrator\Desktop 整个目录上传到网盘 /视频 目录,并使用UI面板展示下载详情
aliyunpan upload C:/Users/Administrator/Desktop /视频 -ui
## 下面演示文件或者文件夹排除功能
# 将本地的 C:\Users\Administrator\Video 整个目录上传到网盘 /视频 目录,但是排除所有的.jpg文件
@@ -336,20 +343,36 @@ aliyunpan upload -exn "\.jpg$" -exn "\.mp3$" C:/Users/Administrator/Video /视
```
#!/bin/bash
# 是否开启调试日志
# 是否开启调试日志 (1=开启, 0=关闭)
export ALIYUNPAN_VERBOSE=0
# (可选)配置目录的绝对路径(请更改成你自己的目录)
export ALIYUNPAN_CONFIG_DIR=/Users/tickstep/Applications/adrive/config
# aliyunpan程序所在的绝对路径(请更改成你自己的目录)
export ALIYUNPAN_BIN=/Users/tickstep/Applications/adrive/aliyunpan
# 本地目录(请更改成你自己的目录)
# (可选)配置目录的绝对路径
export ALIYUNPAN_CONFIG_DIR="/Users/tickstep/Applications/adrive/config"
# aliyunpan程序所在的绝对路径
export ALIYUNPAN_BIN="/Users/tickstep/Applications/adrive/aliyunpan"
# 本地目录
LOCAL_DIR="/tickstep/Documents/我的文档"
# 网盘目录(请更改成你自己的目录)
# 网盘目录
PAN_DIR="/我的文档"
# 执行上传
$ALIYUNPAN_BIN upload -exn "^\." -exn "^@eadir$" "$LOCAL_DIR" $PAN_DIR"
# 检查本地目录是否存在
if [ ! -d "$LOCAL_DIR" ]; then
echo "[ERROR] 本地目录不存在: $LOCAL_DIR"
exit 1
fi
# 执行上传(已修复引号、添加安全引用和覆盖策略)
"$ALIYUNPAN_BIN" upload \
-exn "^\." \
-exn "^@eadir$" \
-ow \
"$LOCAL_DIR" \
"$PAN_DIR"
echo "[INFO] 上传任务执行完毕"
```
增加脚本执行权限
@@ -407,25 +430,6 @@ aliyunpan mv <文件/目录1> <文件/目录2> <文件/目录3> ... <目标目
aliyunpan mv /我的文档/1.mp4 /
```
## 备份盘和资源库之间转存文件
```
aliyunpan xcp <文件/目录1> <文件/目录2> <文件/目录3> ... <目标盘目录>
```
注意: 拷贝多个文件和目录时, 请确保每一个文件和目录都存在, 否则拷贝操作会失败
### 例子
```
当前程序工作在备份盘下,将备份盘 /1.mp4 文件转存复制到 资源库下的 /来自备份盘/video 目录下
aliyunpan xcp /1.mp4 /来自备份盘/video
当前程序工作在资源库下,将资源库 /1.mp4 文件转存复制到 备份盘下的 /来自资源库/video 目录下
aliyunpan xcp /1.mp4 /来自资源库/video
当前程序工作在备份盘下,将 /我的资源 目录下所有的.mp4文件 复制到 /来自备份盘/video 目录下面,使用通配符匹配
aliyunpan xcp /我的资源/*.mp4 /来自备份盘/video
```
## 重命名文件/目录
```
aliyunpan rename <旧文件/目录名> <新文件/目录名>
@@ -535,7 +539,7 @@ aliyunpan sync start -dp 2 -up 1 -dbs 256 -ubs 1024
### 备份配置文件说明
如果你只有一个文件夹进行备份建议直接使用命令行配置启动即可。如果需要同时启动多个备份任务,则可以使用备份配置文件启动同步备份任务。
配置文件如下所示,如果你有通过环境变量ALIYUNPAN_CONFIG_DIR设置配置目录,则需要将sync_drive文件夹拷贝到配置的目录中才可以生效。
配置文件如下所示,如果你有通过环境变量```ALIYUNPAN_CONFIG_DIR```设置配置目录,则需要将```sync_drive```文件夹拷贝到配置的目录中才可以生效。
```
配置文件需要保存在:(配置目录)/sync_drive/sync_drive_config.json,样例如下:
@@ -786,4 +790,4 @@ Windows
### 第二步
打开aliyunpan命令行程序,任何云盘命令都有类似如下日志输出
![](../assets/images/debug-log-screenshot.png)
![](../assets/images/debug-log-screenshot.png)
+2 -2
View File
@@ -14,10 +14,11 @@ require (
github.com/olekukonko/tablewriter v0.0.2-0.20190618033246-cc27d85e17ce
github.com/peterh/liner v1.2.1
github.com/satori/go.uuid v1.2.0
github.com/tickstep/aliyunpan-api v0.2.6
github.com/tickstep/aliyunpan-api v0.2.9
github.com/tickstep/bolt v1.3.4
github.com/tickstep/library-go v0.1.3
github.com/urfave/cli v1.21.1-0.20190817182405-23c83030263f
golang.org/x/sys v0.15.0
)
require (
@@ -32,7 +33,6 @@ require (
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/russross/blackfriday v1.5.2 // indirect
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a // indirect
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359 // indirect
golang.org/x/text v0.3.7 // indirect
)
+4 -3
View File
@@ -101,8 +101,8 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/tickstep/aliyunpan-api v0.2.6 h1:cjn+bBLcPbquKV80aXv07a7808csWche5ctZ4yLljso=
github.com/tickstep/aliyunpan-api v0.2.6/go.mod h1:UvQgQYMIgA5k1QsfKIVnEJ2h/HqDXIkAb16oookNc8o=
github.com/tickstep/aliyunpan-api v0.2.9 h1:0n7BNjmKEi/JYqNLhotBnCQJmJE3OAqiDX28q0Gknxs=
github.com/tickstep/aliyunpan-api v0.2.9/go.mod h1:UvQgQYMIgA5k1QsfKIVnEJ2h/HqDXIkAb16oookNc8o=
github.com/tickstep/bolt v1.3.4 h1:UN1txRsauOfR8a5mUWm5So0ng233PsIMVR5/+3WowCE=
github.com/tickstep/bolt v1.3.4/go.mod h1:Y24RwCywbOsKAyHiVcJ4K2QQfED+q67x7PEOH9OG7Q4=
github.com/tickstep/library-go v0.1.3 h1:OUj6nkimTsqhHXh5s+PbmHq5hR0m739Y1tu9QpfA2ng=
@@ -123,8 +123,9 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359 h1:2B5p2L5IfGiD7+b9BOoRMC6DgObAVZV+Fsp050NqXik=
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
+9 -3
View File
@@ -132,6 +132,7 @@ func CmdAlbum() cli.Command {
IsOverwrite: c.Bool("ow"),
SaveTo: saveTo,
Parallel: 0,
SliceParallel: 0,
Load: 0,
MaxRetry: pandownload.DefaultDownloadMaxRetry,
NoCheck: false,
@@ -258,8 +259,14 @@ func RunShareAlbumDownloadFile(albumNames []string, options *DownloadOptions) {
options.Parallel = config.MaxFileDownloadParallelNum
}
// 设置单个文件下载分片线程数
if options.SliceParallel < 1 {
options.SliceParallel = 1
if options.SliceParallel < 1 { // 用户没有主动设置,则使用下面自动配置的策略
if options.Parallel > 1 {
// 如果是多文件下载,则一个文件只能同时使用1个线程
options.SliceParallel = 1
} else {
// 如果是单文件下载,则可以使用3个最大线程
options.SliceParallel = 3
}
}
// 保存文件的本地根文件夹
@@ -392,7 +399,6 @@ func RunShareAlbumDownloadFile(albumNames []string, options *DownloadOptions) {
Cfg: &newCfg, // 复制一份新的cfg
PanClient: panClient,
VerbosePrinter: panCommandVerbose,
PrintFormat: downloadPrintFormat(options.Load),
ParentTaskExecutor: &executor,
DownloadStatistic: statistic,
IsPrintStatus: options.IsPrintStatus,
+3 -2
View File
@@ -24,6 +24,7 @@ import (
"github.com/tickstep/aliyunpan/internal/config"
"github.com/tickstep/aliyunpan/internal/file/downloader"
"github.com/tickstep/aliyunpan/internal/functions/pandownload"
"github.com/tickstep/aliyunpan/internal/functions/panlogin"
"github.com/tickstep/aliyunpan/internal/taskframework"
"github.com/tickstep/aliyunpan/internal/utils"
"github.com/tickstep/aliyunpan/library/requester/transfer"
@@ -628,7 +629,8 @@ func RunAlbumDownloadFile(albumNames []string, options *DownloadOptions) {
go func(flag *int32) {
for atomic.LoadInt32(flag) == 0 {
time.Sleep(time.Duration(1) * time.Minute)
if RefreshWebTokenInNeed(activeUser, config.Config.DeviceName) {
re := RefreshWebTokenInNeed(activeUser)
if re.Code == panlogin.SUCCESS {
logger.Verboseln("update access token for download task")
}
}
@@ -733,7 +735,6 @@ func RunAlbumDownloadFile(albumNames []string, options *DownloadOptions) {
Cfg: &newCfg, // 复制一份新的cfg
PanClient: panClient,
VerbosePrinter: panCommandVerbose,
PrintFormat: downloadPrintFormat(options.Load),
ParentTaskExecutor: &executor,
DownloadStatistic: statistic,
IsPrintStatus: options.IsPrintStatus,
+73 -41
View File
@@ -15,6 +15,12 @@ package command
import (
"fmt"
"os"
"path"
"path/filepath"
"runtime"
"sort"
"github.com/tickstep/aliyunpan/cmder"
"github.com/tickstep/aliyunpan/cmder/cmdtable"
"github.com/tickstep/aliyunpan/internal/config"
@@ -23,17 +29,13 @@ import (
"github.com/tickstep/aliyunpan/internal/global"
"github.com/tickstep/aliyunpan/internal/log"
"github.com/tickstep/aliyunpan/internal/taskframework"
"github.com/tickstep/aliyunpan/internal/ui"
"github.com/tickstep/aliyunpan/internal/utils"
"github.com/tickstep/aliyunpan/library/requester/transfer"
"github.com/tickstep/library-go/converter"
"github.com/tickstep/library-go/logger"
"github.com/tickstep/library-go/requester/rio/speeds"
"github.com/urfave/cli"
"os"
"path"
"path/filepath"
"runtime"
"sort"
)
type (
@@ -45,8 +47,8 @@ type (
IsExecutedPermission bool
IsOverwrite bool
SaveTo string
Parallel int // 文件下载最大线程数
SliceParallel int // 单个文件分片下载最大线程数
Parallel int // 文件下载最大线程数,即同时进行下载文件的数量,阿里云盘限制最大为3
SliceParallel int // 单个文件分片下载最大线程数,阿里云盘限制最大为3
Load int
MaxRetry int
NoCheck bool
@@ -54,6 +56,7 @@ type (
DriveId string
ExcludeNames []string // 排除的文件名,包括文件夹和文件。即这些文件/文件夹不进行下载,支持正则表达式
IsMultiUserDownload bool // 是否启用多用户联合下载
IsUseUIDashboard bool // 是否使用UI下载面板显示下载进度
}
// LocateDownloadOption 获取下载链接可选参数
@@ -140,7 +143,7 @@ func CmdDownload() cli.Command {
IsOverwrite: c.Bool("ow"),
SaveTo: saveTo,
Parallel: c.Int("p"),
SliceParallel: 3,
SliceParallel: c.Int("sp"),
Load: 0,
MaxRetry: c.Int("retry"),
NoCheck: c.Bool("nocheck"),
@@ -148,22 +151,10 @@ func CmdDownload() cli.Command {
DriveId: parseDriveId(c),
ExcludeNames: c.StringSlice("exn"),
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)
// 释放文件锁
//if locker != nil {
// filelocker.UnlockFile(locker)
//}
return nil
},
Flags: []cli.Flag{
@@ -192,11 +183,11 @@ func CmdDownload() cli.Command {
Usage: "parallel,指定同时进行下载文件的数量(取值范围:1 ~ 3)",
Value: 1,
},
//cli.IntFlag{
// Name: "sp",
// Usage: "slice parallel,指定单个文件下载的最大线程(分片)数(取值范围:1 ~ 3)",
// Value: 1,
//},
cli.IntFlag{
Name: "sp",
Usage: "slice parallel,指定单个文件下载的最大线程(分片)数(取值范围:1 ~ 3)",
Value: 0,
},
cli.IntFlag{
Name: "retry",
Usage: "下载失败最大重试次数",
@@ -224,17 +215,14 @@ func CmdDownload() cli.Command {
Name: "md",
Usage: "(BETA) Multi-User Download,使用多用户联合下载,可以对单一文件叠加所有登录用户的下载速度",
},
cli.BoolFlag{
Name: "ui",
Usage: "(BETA) 使用UI面板显示下载详情和进度,更加直观和友好",
},
},
}
}
func downloadPrintFormat(load int) string {
if load <= 1 {
return pandownload.DefaultPrintFormat
}
return "\r[%s] ↓ %s/%s %s/s in %s, left %s ..."
}
// RunDownload 执行下载网盘内文件
func RunDownload(paths []string, options *DownloadOptions) {
activeUser := GetActiveUser()
@@ -296,8 +284,14 @@ func RunDownload(paths []string, options *DownloadOptions) {
}
// 设置单个文件下载分片线程数
if options.SliceParallel < 1 {
options.SliceParallel = 1
if options.SliceParallel < 1 { // 用户没有主动设置,则使用下面自动配置的策略
if options.Parallel > 1 {
// 如果是多文件下载,则一个文件只能同时使用1个线程
options.SliceParallel = 1
} else {
// 如果是单文件下载,则可以使用3个最大线程
options.SliceParallel = 3
}
}
// 保存文件的本地根文件夹
@@ -362,7 +356,6 @@ func RunDownload(paths []string, options *DownloadOptions) {
// 阿里OpenAPI规定:文件分片下载的并发数为3,即某用户使用 App 时,可以同时下载 1 个文件的 3 个分片,或者同时下载 3 个文件的各 1 个分片。
options.SliceParallel = userCount * 3
}
fmt.Printf("\n[0] 当前文件下载最大并发量为: %d, 单文件下载分片线程数为: %d, 下载缓存为: %s\n", options.Parallel, options.SliceParallel, converter.ConvertFileSize(int64(cfg.CacheSize), 2))
var (
panClient = activeUser.PanClient()
@@ -382,20 +375,45 @@ func RunDownload(paths []string, options *DownloadOptions) {
// 全局速度统计
globalSpeedsStat := &speeds.Speeds{}
// 下载统计UI面板
var dashboard *ui.DashboardPanel = nil
if options.IsUseUIDashboard && options.ShowProgress && !options.IsPrintStatus && ui.IsTerminal(os.Stdout) {
dashboard = ui.NewDashboardPanel(ui.DashboardPanelDownload, cfg.MaxParallel, globalSpeedsStat, &ui.DashboardOptions{
Title: "下载统计UI面板",
ActiveSlots: 3, // 下载文件进度展示,最大同时展示3个
MaxHistory: 50, // 下载日志显示,最多同时显示50条,这个会按照窗口大小进行自适应显示
})
}
logf := func(format string, a ...interface{}) {
if dashboard != nil {
dashboard.Logf(format, a...)
return
}
fmt.Printf(format, a...)
}
// 下载记录器
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))
// 阿里云盘下载并发超额警告,阿里云盘下载最大并发数为3
if options.Parallel*options.SliceParallel > 3 {
logf("[0] 警告:当前下载并发配置数已超过阿里云盘限制的最大并发数,下载任务会被风控容易导致下载失败\n")
}
// 处理队列
for k := range paths {
// 使用通配符匹配
fileList, err2 := matchPathByShellPattern(options.DriveId, paths[k])
if err2 != nil {
fmt.Printf("获取文件出错,请稍后重试: %s\n", paths[k])
logf("获取文件出错,请稍后重试: %s\n", paths[k])
continue
}
if fileList == nil || len(fileList) == 0 {
// 文件不存在
fmt.Printf("文件不存在: %s\n", paths[k])
logf("文件不存在: %s\n", paths[k])
continue
}
// 排序,按名称排序,从小到大
@@ -408,7 +426,7 @@ func RunDownload(paths []string, options *DownloadOptions) {
// 是否排除下载
if utils.IsExcludeFile(f.Path, &newCfg.ExcludeNames) {
fmt.Printf("排除文件: %s\n", f.Path)
logf("排除文件: %s\n", f.Path)
continue
}
@@ -419,7 +437,6 @@ func RunDownload(paths []string, options *DownloadOptions) {
PanClient: panClient,
SubPanClientList: subPanClientList,
VerbosePrinter: panCommandVerbose,
PrintFormat: downloadPrintFormat(options.Load),
ParentTaskExecutor: &executor,
DownloadStatistic: statistic,
IsPrintStatus: options.IsPrintStatus,
@@ -431,6 +448,7 @@ func RunDownload(paths []string, options *DownloadOptions) {
DriveId: options.DriveId,
GlobalSpeedsStat: globalSpeedsStat,
FileRecorder: fileRecorder,
UI: dashboard,
}
// 设置储存的路径
@@ -443,16 +461,30 @@ func RunDownload(paths []string, options *DownloadOptions) {
unit.SavePath = GetActiveUser().GetSavePath(f.Path)
}
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)
}
}
// 开始计时
statistic.StartTimer()
// 启动UI面板显示
if dashboard != nil {
dashboard.Start()
}
// 开始执行
executor.Execute()
// 关闭UI面板
if dashboard != nil {
dashboard.Close()
}
// 完成下载,输出统计结果
fmt.Printf("\n下载结束, 时间: %s, 数据总量: %s\n", utils.ConvertTime(statistic.Elapsed()), converter.ConvertFileSize(statistic.TotalSize(), 2))
// 输出失败的文件列表
+1
View File
@@ -70,6 +70,7 @@ func CmdLs() cli.Command {
`,
Category: "阿里云盘",
Before: ReloadConfigFunc,
After: SaveConfigFunc,
Action: func(c *cli.Context) error {
if config.Config.ActiveUser() == nil {
fmt.Println("未登录账号")
+18 -1
View File
@@ -74,6 +74,21 @@ func CmdRm() cli.Command {
}
}
func queryFileListByPath(activeUser *config.PanUser, driveId, filePathPattern string) (files []*aliyunpan.FileEntity, e error) {
if !strings.ContainsAny(filePathPattern, aliyunpan.ShellPatternCharacters) {
// 没有通配符,直接查询
fi, err := activeUser.PanClient().OpenapiPanClient().FileInfoByPath(driveId, filePathPattern)
if err != nil {
return nil, err
}
files = append(files, fi)
} else {
// 包含通配符,使用通配符查询
files, e = matchPathByShellPattern(driveId, filePathPattern)
}
return files, e
}
// RunRemove 执行 批量删除文件/目录
func RunRemove(driveId string, paths ...string) {
activeUser := GetActiveUser()
@@ -86,7 +101,9 @@ func RunRemove(driveId string, paths ...string) {
for _, p := range paths {
absolutePath := path.Clean(activeUser.PathJoin(driveId, p))
fileList, err1 := matchPathByShellPattern(driveId, absolutePath)
// 查询路径对应的文件信息
fileList, err1 := queryFileListByPath(activeUser, driveId, absolutePath)
if err1 != nil {
failedRmPaths = append(failedRmPaths, absolutePath)
continue
+5 -3
View File
@@ -21,6 +21,7 @@ import (
"github.com/tickstep/aliyunpan/internal/config"
"github.com/urfave/cli"
"path"
"strings"
"time"
)
@@ -196,7 +197,7 @@ func RunOpenShareSet(modeFlag, driveId string, paths []string, expiredTime strin
}
fmt.Printf("创建快传链接成功\n")
fmt.Printf("链接:%s\n", r.ShareUrl)
fmt.Printf("链接:%s\n", strings.ReplaceAll(r.ShareUrl, "https://www.aliyundrive.com", "https://www.alipan.com"))
} else {
// 分享
r, err1 := panClient.OpenapiPanClient().ShareLinkCreate(aliyunpan.ShareCreateParam{
@@ -219,10 +220,11 @@ func RunOpenShareSet(modeFlag, driveId string, paths []string, expiredTime strin
} else if modeFlag == "2" {
fmt.Printf("创建公开分享链接成功\n")
}
shareUrl := strings.ReplaceAll(r.ShareUrl, "https://www.aliyundrive.com", "https://www.alipan.com")
if len(sharePwd) > 0 {
fmt.Printf("链接:%s 提取码:%s\n", r.ShareUrl, r.SharePwd)
fmt.Printf("链接:%s 提取码:%s\n", shareUrl, r.SharePwd)
} else {
fmt.Printf("链接:%s\n", r.ShareUrl)
fmt.Printf("链接:%s\n", shareUrl)
}
}
}
+76 -36
View File
@@ -15,12 +15,6 @@ package command
import (
"fmt"
"github.com/tickstep/aliyunpan-api/aliyunpan/apierror"
"github.com/tickstep/aliyunpan/cmder"
"github.com/tickstep/aliyunpan/internal/log"
"github.com/tickstep/aliyunpan/internal/plugins"
"github.com/tickstep/aliyunpan/internal/utils"
"github.com/tickstep/library-go/requester/rio/speeds"
"os"
"path"
"path/filepath"
@@ -28,6 +22,14 @@ import (
"sync"
"time"
"github.com/tickstep/aliyunpan-api/aliyunpan/apierror"
"github.com/tickstep/aliyunpan/cmder"
"github.com/tickstep/aliyunpan/internal/log"
"github.com/tickstep/aliyunpan/internal/plugins"
"github.com/tickstep/aliyunpan/internal/ui"
"github.com/tickstep/aliyunpan/internal/utils"
"github.com/tickstep/library-go/requester/rio/speeds"
"github.com/tickstep/library-go/logger"
"github.com/urfave/cli"
@@ -54,17 +56,18 @@ const (
type (
// UploadOptions 上传可选项
UploadOptions struct {
AllParallel int // 所有文件并发上传数量,即可以同时并发上传多少个文件
Parallel int // 单个文件并发上传数量
MaxRetry int
MaxTimeoutSec int // http请求超时时间,单位秒
NoRapidUpload bool
ShowProgress bool
IsOverwrite bool // 覆盖已存在的文件,如果同名文件已存在则移到回收站里
IsSkipSameName bool // 跳过已存在的文件,即使文件内容不一致(不检查SHA1)
DriveId string
ExcludeNames []string // 排除的文件名,包括文件夹和文件。即这些文件/文件夹不进行上传,支持正则表达式
BlockSize int64 // 分片大小
AllParallel int // 所有文件并发上传数量,即可以同时并发上传多少个文件
Parallel int // 单个文件并发上传数量
MaxRetry int
MaxTimeoutSec int // http请求超时时间,单位秒
NoRapidUpload bool
ShowProgress bool
IsOverwrite bool // 覆盖已存在的文件,如果同名文件已存在则移到回收站里
IsSkipSameName bool // 跳过已存在的文件,即使文件内容不一致(不检查SHA1)
DriveId string
ExcludeNames []string // 排除的文件名,包括文件夹和文件。即这些文件/文件夹不进行上传,支持正则表达式
BlockSize int64 // 分片大小
IsUseUIDashboard bool // 是否使用UI面板显示上传进度
}
)
@@ -114,6 +117,10 @@ var UploadFlags = []cli.Flag{
Usage: "block size,上传分片大小,单位KB。推荐值:1024 ~ 10240。当上传极大单文件时候请适当调高该值",
Value: 10240,
},
cli.BoolFlag{
Name: "ui",
Usage: "(BETA) 使用UI面板显示上传详情和进度,更加直观和友好",
},
}
func CmdUpload() cli.Command {
@@ -192,17 +199,18 @@ func CmdUpload() cli.Command {
//}
RunUpload(subArgs[:c.NArg()-1], subArgs[c.NArg()-1], &UploadOptions{
AllParallel: c.Int("p"), // 多文件上传的时候,允许同时并行上传的文件数量
Parallel: 1, // 一个文件同时多少个线程并发上传的数量。阿里云盘只支持单线程按顺序进行文件part数据上传,所以只能是1
MaxRetry: c.Int("retry"),
MaxTimeoutSec: timeout,
NoRapidUpload: c.Bool("norapid"),
ShowProgress: !c.Bool("np"),
IsOverwrite: c.Bool("ow"),
IsSkipSameName: c.Bool("skip"),
DriveId: parseDriveId(c),
ExcludeNames: c.StringSlice("exn"),
BlockSize: int64(c.Int("bs") * 1024),
AllParallel: c.Int("p"), // 多文件上传的时候,允许同时并行上传的文件数量
Parallel: 1, // 一个文件同时多少个线程并发上传的数量。阿里云盘只支持单线程按顺序进行文件part数据上传,所以只能是1
MaxRetry: c.Int("retry"),
MaxTimeoutSec: timeout,
NoRapidUpload: c.Bool("norapid"),
ShowProgress: !c.Bool("np"),
IsOverwrite: c.Bool("ow"),
IsSkipSameName: c.Bool("skip"),
DriveId: parseDriveId(c),
ExcludeNames: c.StringSlice("exn"),
BlockSize: int64(c.Int("bs") * 1024),
IsUseUIDashboard: c.Bool("ui"),
})
// 释放文件锁
@@ -290,6 +298,23 @@ func RunUpload(localPaths []string, savePath string, opt *UploadOptions) {
// 全局速度统计
globalSpeedsStat := &speeds.Speeds{}
// 上传统计UI面板
var dashboard *ui.DashboardPanel = nil
if opt.IsUseUIDashboard && opt.ShowProgress && ui.IsTerminal(os.Stdout) {
dashboard = ui.NewDashboardPanel(ui.DashboardPanelUpload, opt.AllParallel, globalSpeedsStat, &ui.DashboardOptions{
Title: "上传统计UI面板",
ActiveSlots: 3, // 下载文件进度展示,最大同时展示3个
MaxHistory: 50, // 下载日志显示,最多同时显示50条,这个会按照窗口大小进行自适应显示
})
}
logf := func(format string, a ...interface{}) {
if dashboard != nil {
dashboard.Logf(format, a...)
return
}
fmt.Printf(format, a...)
}
// 获取当前插件
plugin, _ := pluginManger.GetPlugin()
@@ -304,7 +329,7 @@ func RunUpload(localPaths []string, savePath string, opt *UploadOptions) {
// 是否排除上传
if utils.IsExcludeFile(curPath, &opt.ExcludeNames) {
fmt.Printf("排除文件: %s\n", curPath)
logf("排除文件: %s\n", curPath)
continue
}
@@ -326,7 +351,7 @@ func RunUpload(localPaths []string, savePath string, opt *UploadOptions) {
// 是否排除上传
if utils.IsExcludeFile(file.LogicPath, &opt.ExcludeNames) {
fmt.Printf("排除文件: %s\n", file.LogicPath)
logf("排除文件: %s\n", file.LogicPath)
return filepath.SkipDir
}
@@ -355,13 +380,13 @@ func RunUpload(localPaths []string, savePath string, opt *UploadOptions) {
if uploadFilePrepareResult, er := plugin.UploadFilePrepareCallback(plugins.GetContext(activeUser), pluginParam); er == nil && uploadFilePrepareResult != nil {
if strings.Compare("yes", uploadFilePrepareResult.UploadApproved) != 0 {
// skip upload this file
fmt.Printf("插件禁止该文件上传: %s\n", file.LogicPath)
logf("插件禁止该文件上传: %s\n", file.LogicPath)
return filepath.SkipDir
}
if uploadFilePrepareResult.DriveFilePath != "" {
targetSavePanRelativePath := strings.TrimPrefix(uploadFilePrepareResult.DriveFilePath, "/")
subSavePath = path.Clean(savePath + aliyunpan.PathSeparator + targetSavePanRelativePath)
fmt.Printf("插件修改文件网盘保存路径为: %s\n", subSavePath)
logf("插件修改文件网盘保存路径为: %s\n", subSavePath)
}
}
@@ -384,22 +409,26 @@ func RunUpload(localPaths []string, savePath string, opt *UploadOptions) {
IsSkipSameName: opt.IsSkipSameName,
GlobalSpeedsStat: globalSpeedsStat,
FileRecorder: fileRecorder,
UI: dashboard,
}, opt.MaxRetry)
fmt.Printf("[%s] 加入上传队列: %s\n", taskinfo.Id(), file.LogicPath)
logf("[%s] 加入上传队列: %s\n", taskinfo.Id(), file.LogicPath)
if dashboard != nil {
dashboard.RegisterTask(taskinfo.Id(), file.LogicPath, fi.Size(), !fi.IsDir())
}
} else {
// 创建文件夹
// 这样空文件夹也可以正确上传
saveFilePath := subSavePath
if saveFilePath != "/" {
folderCreateMutex.Lock()
fmt.Printf("正在检测和创建云盘文件夹: %s\n", saveFilePath)
logf("正在检测和创建云盘文件夹: %s\n", saveFilePath)
_, apierr1 := activeUser.PanClient().OpenapiPanClient().FileInfoByPath(opt.DriveId, saveFilePath)
time.Sleep(1 * time.Second)
if apierr1 != nil && apierr1.Code == apierror.ApiCodeFileNotFoundCode {
logger.Verbosef("%s 创建云盘文件夹: %s\n", utils.NowTimeStr(), saveFilePath)
rs, apierr := activeUser.PanClient().OpenapiPanClient().MkdirByFullPath(opt.DriveId, saveFilePath)
if apierr != nil || rs.FileId == "" {
fmt.Printf("创建云盘文件夹失败: %s\n", saveFilePath)
logf("创建云盘文件夹失败: %s\n", saveFilePath)
}
}
folderCreateMutex.Unlock()
@@ -411,19 +440,30 @@ func RunUpload(localPaths []string, savePath string, opt *UploadOptions) {
file := localfile.NewSymlinkFile(curPath)
if err = localfile.WalkAllFile(file, walkFunc); err != nil {
if err != filepath.SkipDir {
fmt.Printf("警告: 遍历错误: %s\n", err)
logf("警告: 遍历错误: %s\n", err)
}
}
}
// 执行上传任务
var failedList []*lane.Deque
// 启动UI面板显示
if dashboard != nil {
dashboard.Start()
}
executor.Execute()
failed := executor.FailedDeque()
if failed.Size() > 0 {
failedList = append(failedList, failed)
}
// 关闭UI面板
if dashboard != nil {
dashboard.Close()
}
fmt.Printf("\n")
fmt.Printf("上传结束, 时间: %s, 数据总量: %s\n", utils.ConvertTime(statistic.Elapsed()), converter.ConvertFileSize(statistic.TotalSize(), 2))
+128 -22
View File
@@ -23,6 +23,7 @@ import (
"github.com/tickstep/aliyunpan/internal/utils"
"github.com/tickstep/library-go/logger"
"github.com/urfave/cli"
"math"
"math/rand"
"net/url"
"path"
@@ -142,9 +143,9 @@ func NewWebLoginToken(accessToken string, expired int64) aliyunpan_web.WebLoginT
}
// RefreshWebTokenInNeed 刷新 webapi access token
func RefreshWebTokenInNeed(activeUser *config.PanUser, deviceName string) bool {
func RefreshWebTokenInNeed(activeUser *config.PanUser) *panlogin.CommonResultError {
if activeUser == nil {
return false
return panlogin.NewCommonResultError("activeUser is nil")
}
// refresh expired web token
@@ -153,7 +154,7 @@ func RefreshWebTokenInNeed(activeUser *config.PanUser, deviceName string) bool {
cz := time.FixedZone("CST", 8*3600) // 东8区
expiredTime := time.Unix(activeUser.WebapiToken.Expired, 0).In(cz)
now := time.Now()
if (expiredTime.Unix() - now.Unix()) <= (2 * 60) { // 有效期小于2min就刷新
if (expiredTime.Unix() - now.Unix()) <= (10 * 60) { // 有效期小于10min就刷新
pluginManger := plugins.NewPluginManager(config.GetPluginDir())
plugin, _ := pluginManger.GetPlugin()
params := &plugins.UserTokenRefreshFinishParams{
@@ -167,9 +168,12 @@ func RefreshWebTokenInNeed(activeUser *config.PanUser, deviceName string) bool {
// need update refresh token
logger.Verboseln("web access token expired, get new from server")
loginHelper := panlogin.NewLoginHelper(config.DefaultTokenServiceWebHost)
wt, e := loginHelper.GetWebapiNewToken(activeUser.TicketId, activeUser.UserId, activeUser.PanClient().WebapiPanClient().GetAccessToken())
if e != nil {
logger.Verboseln("get web token from server error: ", e)
wt, re := loginHelper.GetWebapiNewToken(activeUser.TicketId, activeUser.UserId, activeUser.PanClient().WebapiPanClient().GetAccessToken())
if re != nil {
logger.Verboseln("get web token from server error: ", re.Msg)
if re.Code != panlogin.SUCCESS {
return re
}
}
if wt != nil {
params.Result = "success"
@@ -191,42 +195,51 @@ func RefreshWebTokenInNeed(activeUser *config.PanUser, deviceName string) bool {
}
// create new signature
_, e := activeUser.PanClient().WebapiPanClient().CreateSession(nil)
if e != nil {
logger.Verboseln("call CreateSession error in RefreshWebTokenInNeed: " + e.Error())
_, e1 := activeUser.PanClient().WebapiPanClient().CreateSession(nil)
if e1 != nil {
logger.Verboseln("call CreateSession error in RefreshWebTokenInNeed: " + e1.Error())
}
return &panlogin.CommonResultError{
Code: 0,
Msg: "",
}
return true
} else {
// token refresh error
// if token has expired, callback plugin api for notify
if now.Unix() >= expiredTime.Unix() {
params.Result = "fail"
params.Message = e.Error()
params.Message = re.Msg
params.OldToken = activeUser.WebapiToken.AccessToken
if er1 := plugin.UserTokenRefreshFinishCallback(plugins.GetContext(activeUser), params); er1 != nil {
logger.Verbosef("UserTokenRefreshFinishCallback error: " + er1.Error())
}
}
}
} else {
return &panlogin.CommonResultError{
Code: 0,
Msg: "web token is validate, not need to refresh",
}
}
}
}
return false
return panlogin.NewCommonResultError("activeUser is invalidate")
}
// RefreshOpenTokenInNeed 刷新 openapi access token
func RefreshOpenTokenInNeed(activeUser *config.PanUser) bool {
func RefreshOpenTokenInNeed(activeUser *config.PanUser) *panlogin.CommonResultError {
if activeUser == nil {
return false
return panlogin.NewCommonResultError("activeUser is nil")
}
// OpenAPI的token只有2个小时有效期,需要在token过期之前进行刷新并把获取到的新token存储在本地
// refresh expired openapi token
if activeUser.PanClient().OpenapiPanClient() != nil {
if activeUser.OpenapiToken != nil && len(activeUser.OpenapiToken.AccessToken) > 0 {
cz := time.FixedZone("CST", 8*3600) // 东8区
expiredTime := time.Unix(activeUser.OpenapiToken.Expired, 0).In(cz)
now := time.Now().In(cz)
if (expiredTime.Unix() - now.Unix()) <= (2 * 60) { // 有效期小于2min就刷新
if (expiredTime.Unix() - now.Unix()) <= (10 * 60) { // 有效期小于10min就刷新
pluginManger := plugins.NewPluginManager(config.GetPluginDir())
plugin, _ := pluginManger.GetPlugin()
params := &plugins.UserTokenRefreshFinishParams{
@@ -240,11 +253,15 @@ func RefreshOpenTokenInNeed(activeUser *config.PanUser) bool {
// need update refresh token
logger.Verboseln("openapi access token expired, get new from server")
loginHelper := panlogin.NewLoginHelper(config.DefaultTokenServiceWebHost)
wt, e := loginHelper.GetOpenapiNewToken(activeUser.TicketId, activeUser.UserId, activeUser.PanClient().OpenapiPanClient().GetAccessToken())
if e != nil {
logger.Verboseln("get openapi token from server error: ", e)
wt, re := loginHelper.GetOpenapiNewToken(activeUser.TicketId, activeUser.UserId, activeUser.PanClient().OpenapiPanClient().GetAccessToken())
if re != nil {
logger.Verboseln("get openapi token from server error: ", re.Msg)
if re.Code != panlogin.SUCCESS {
return re
}
}
if wt != nil {
// 存储新token到本地
params.Result = "success"
params.OldToken = activeUser.OpenapiToken.AccessToken
params.NewToken = wt.AccessToken
@@ -261,23 +278,112 @@ func RefreshOpenTokenInNeed(activeUser *config.PanUser) bool {
logger.Verbosef("UserTokenRefreshFinishCallback error: " + er1.Error())
}
return true
return &panlogin.CommonResultError{
Code: 0,
Msg: "",
}
} else {
// token refresh error
// if token has expired, callback plugin api for notify
if now.Unix() >= expiredTime.Unix() {
params.Result = "fail"
params.Message = e.Error()
params.OldToken = activeUser.WebapiToken.AccessToken
params.Message = re.Msg
params.OldToken = activeUser.OpenapiToken.AccessToken
if er1 := plugin.UserTokenRefreshFinishCallback(plugins.GetContext(activeUser), params); er1 != nil {
logger.Verbosef("UserTokenRefreshFinishCallback error: " + er1.Error())
}
}
}
} else {
return &panlogin.CommonResultError{
Code: 0,
Msg: "openapi token is validate, not need to refresh",
}
}
}
}
return false
return panlogin.NewCommonResultError("activeUser is invalidate")
}
// AutomaticallyRefreshTokenTask 自动刷新Token的后台任务
func AutomaticallyRefreshTokenTask() {
go func() {
// 延迟启动自动刷新Token任务,因为主程序启动的时候默认会进行Token的初始化和刷新,避免无效的并行初始化Token
time.Sleep(time.Duration(15) * time.Minute)
// 检测延迟时间(分钟)
delayMinuteTime := time.Duration(1) * time.Minute
//delayMinuteTime := time.Duration(10) * time.Second // for test
// 请求重试次数
attempt := 1
for {
// Token刷新进程,不管是CLI命令行模式,还是直接命令模式,本刷新任务都会执行
time.Sleep(delayMinuteTime) // 定时器,每1分钟调用一次
// 重新加载配置文件
ReloadConfigFunc(nil)
activeUser := config.Config.ActiveUser()
if activeUser != nil && activeUser.UserId != "" && activeUser.PanClient() != nil {
// 刷新Openapi端Token
errResult := RefreshOpenTokenInNeed(activeUser)
attempt += 1
if errResult != nil && errResult.Code == panlogin.SUCCESS {
// 刷新成功,重置定时器
attempt = 1
delayMinuteTime = time.Duration(1) * time.Minute
// 刷新Webapi端Token
RefreshWebTokenInNeed(activeUser)
// 保存新token到配置文件
SaveConfigFunc(nil)
} else {
// 刷新Token失败
logger.Verboseln("刷新Token失败:", errResult.Msg)
if errResult.Code == panlogin.ERROR_NEED_LOGIN_AGAIN {
// token已经失效,需要重新登录
logger.Verboseln("token已经失效,需要重新登录,退出自动刷新Token任务")
return
} else if errResult.Code == panlogin.ERROR {
// 访问错误
delayMinuteTime = ProgressiveBackoffAlg(attempt)
} else if errResult.Code == panlogin.ERROR_TOO_MANY_REQUESTS {
// 请求过于频繁
delayMinuteTime = ProgressiveBackoffAlg(attempt)
}
}
}
}
}()
}
// ProgressiveBackoffAlg 指数退降算法
func ProgressiveBackoffAlg(attempt int) time.Duration {
maxRetries := 120
baseDelay := float64(1)
maxDelay := float64(1440)
if attempt >= maxRetries {
return time.Duration(maxDelay) * time.Minute
}
// 计算当前延迟
var delay float64
if attempt < 100 {
// 前 100 次:线性增长 + 小指数基数(缓慢增加)
delay = baseDelay * (1 + float64(attempt)/5) // 调整分母控制增长速度
} else {
// 100 次后:标准指数退避
delay = baseDelay * math.Pow(1.5, float64(attempt-100))
if delay > maxDelay {
delay = maxDelay
}
}
// 添加随机抖动(最多 20% 的扰动)
jitterSec := rand.Float64() * delay * 0.2 // ±20% 随机扰动
delay += jitterSec
return time.Duration(delay * float64(time.Minute))
}
func isIncludeFile(pattern string, fileName string) bool {
+13
View File
@@ -0,0 +1,13 @@
package command
import (
"fmt"
"testing"
)
func TestProgressiveBackoffAlg(t *testing.T) {
for attempt := 1; attempt <= 150; attempt++ {
delayTime := ProgressiveBackoffAlg(attempt)
fmt.Printf("第%d次,延迟分钟: %.2f\n", attempt, delayTime.Minutes())
}
}
-2
View File
@@ -43,8 +43,6 @@ func CmdLocalCd() cli.Command {
切换根目录:
aliyunpan lcd /
`,
Before: command.ReloadConfigFunc,
After: command.SaveConfigFunc,
Action: func(c *cli.Context) error {
if c.NArg() == 0 {
cli.ShowCommandHelp(c, c.Command.Name)
-2
View File
@@ -18,7 +18,6 @@ import (
"github.com/olekukonko/tablewriter"
"github.com/tickstep/aliyunpan-api/aliyunpan"
"github.com/tickstep/aliyunpan/cmder/cmdtable"
"github.com/tickstep/aliyunpan/internal/command"
"github.com/tickstep/library-go/converter"
"github.com/urfave/cli"
"log"
@@ -47,7 +46,6 @@ func CmdLocalLs() cli.Command {
详细列出 我的资源 内的文件和目录
aliyunpan lls /我的资源
`,
Before: command.ReloadConfigFunc,
Action: func(c *cli.Context) error {
var (
orderBy aliyunpan.FileOrderBy = aliyunpan.FileOrderByUpdatedAt
+1 -1
View File
@@ -54,7 +54,7 @@ const (
DefaultFileDownloadParallelNum = 5
// MaxFileDownloadParallelNum 最大文件下载并发数量。过大会被阿里云盘风控,导致无法下载
MaxFileDownloadParallelNum = 20
MaxFileDownloadParallelNum = 3
// DefaultTokenServiceWebHost 默认的token服务
DefaultTokenServiceWebHost = "https://api.tickstep.com"
+1 -1
View File
@@ -109,7 +109,7 @@ func (c *PanConfig) PrintTable() {
tb.SetColumnAlignment([]int{tablewriter.ALIGN_DEFAULT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_LEFT})
tb.AppendBulk([][]string{
[]string{"cache_size", converter.ConvertFileSize(int64(c.CacheSize), 2), "1KB ~ 256KB", "下载缓存, 如果硬盘占用高或下载速度慢, 请尝试调大此值"},
[]string{"max_download_parallel", strconv.Itoa(c.MaxDownloadParallel), "1 ~ 20", "最大下载并发量,即同时下载文件最大数量"},
[]string{"max_download_parallel", strconv.Itoa(c.MaxDownloadParallel), "1 ~ 3", "最大下载并发量,即同时下载文件最大数量"},
[]string{"max_upload_parallel", strconv.Itoa(c.MaxUploadParallel), "1 ~ 20", "最大上传并发量,即同时上传文件最大数量"},
[]string{"max_download_rate", showMaxRate(c.MaxDownloadRate), "", "限制单个文件最大下载速度, 0代表不限制"},
[]string{"max_upload_rate", showMaxRate(c.MaxUploadRate), "", "限制单个文件最大上传速度, 0代表不限制"},
+17 -5
View File
@@ -152,16 +152,23 @@ doOpenLoginAct:
AccessToken: openapiToken.AccessToken,
ExpiredAt: openapiToken.Expired,
}, nil)
openPanClient.SetAccessTokenRefreshCallback(func(userId string, newToken openapi.ApiToken) error {
// api层自动刷新Token,回调通知程序
logger.Verboseln("openapi token refresh, use new token")
openapiToken.AccessToken = newToken.AccessToken
openapiToken.Expired = newToken.ExpiredAt
return nil
})
// open api token maybe expired
// check & refresh new one
openUserInfo, err := openPanClient.GetUserInfo()
if err != nil {
if err.Code == apierror.ApiCodeTokenExpiredCode && tryRefreshOpenToken {
if err.Code == apierror.ApiCodeTokenExpiredCode && tryRefreshOpenToken { // 如果是token过期,则尝试刷新
tryRefreshOpenToken = false
wt, e := loginHelper.GetOpenapiNewToken(ticketId, userId, openapiToken.AccessToken)
if e != nil {
logger.Verboseln("get openapi token from server error: ", e)
wt, re := loginHelper.GetOpenapiNewToken(ticketId, userId, openapiToken.AccessToken)
if re != nil {
logger.Verboseln("get openapi token from server error: ", re.Msg)
return nil, apierror.NewFailedApiError("get new openapi token error, try login again")
}
if wt != nil {
@@ -169,7 +176,11 @@ doOpenLoginAct:
AccessToken: wt.AccessToken,
Expired: wt.Expired,
}
// TODO: 检查token有效期
}
// 回到token检测流程,必须确保token是有效
time.Sleep(time.Duration(1) * time.Second)
goto doOpenLoginAct
}
@@ -207,7 +218,7 @@ doWebLoginAct:
tryRefreshWebToken = false
wt, e := loginHelper.GetWebapiNewToken(ticketId, userId, webapiToken.AccessToken)
if e != nil {
logger.Verboseln("get web token from server error: ", e)
logger.Verboseln("get web token from server error: ", e.Msg)
}
if wt != nil {
webapiToken = &PanClientToken{
@@ -246,6 +257,7 @@ doWebLoginAct:
WorkdirFileEntity: *aliyunpan.NewFileEntityForRootDir(),
}
u.PanClient().OpenapiPanClient().SetAccessTokenRefreshCallback(func(userId string, newToken openapi.ApiToken) error {
// api层自动刷新Token,回调通知程序
logger.Verboseln("openapi token refresh, update for user")
u.OpenapiToken = &PanClientToken{
AccessToken: newToken.AccessToken,
+1
View File
@@ -346,6 +346,7 @@ func (mt *Monitor) ResetWorker(worker *Worker) {
// 忽略, 返回
return
case StatusCodeDownloadUrlExpired: // 下载链接已经过期
logger.Verbosef("download url expired, reset worker: %d\n", worker.ID())
worker.RefreshDownloadUrl()
break
case StatusCodeDownloadUrlExceedMaxConcurrency: // 下载遇到限流报错
+7 -5
View File
@@ -15,8 +15,6 @@ package downloader
import (
"errors"
"github.com/tickstep/library-go/logger"
"github.com/tickstep/library-go/requester"
mathrand "math/rand"
"mime"
"net/url"
@@ -24,6 +22,10 @@ import (
"regexp"
"strconv"
"time"
"github.com/tickstep/aliyunpan/internal/utils"
"github.com/tickstep/library-go/logger"
"github.com/tickstep/library-go/requester"
)
var (
@@ -102,15 +104,15 @@ func fixCacheSize(size *int) {
// IsUrlExpired 下载链接是否已过期。过期返回True
func IsUrlExpired(urlStr string) bool {
u, err := url.Parse(urlStr)
expiredTimeSec, err := utils.GetExpiredTimeSecFromOSSUrl(urlStr)
if err != nil {
// 解析错误,默认过期
return true
}
expiredTimeSecStr := u.Query().Get("x-oss-expires")
expiredTimeSec, _ := strconv.ParseInt(expiredTimeSecStr, 10, 64)
if (expiredTimeSec - time.Now().Unix()) <= 5 { // 小于5秒钟
// expired
return true
}
// 返回未过期
return false
}
+5 -1
View File
@@ -205,6 +205,7 @@ func (wer *Worker) Cancel() error {
// Reset 重设连接
func (wer *Worker) Reset() {
logger.Verbosef("Reset worker: %d\n", wer.ID())
if wer.resetFunc == nil {
logger.Verbosef("DEBUG: worker: resetFunc not set")
return
@@ -220,13 +221,15 @@ func (wer *Worker) Reset() {
// RefreshDownloadUrl 重新刷新下载链接
func (wer *Worker) RefreshDownloadUrl() {
var apierr *apierror.ApiError
logger.Verbosef("get new download url for worker: %d\n", wer.ID())
durl, apierr := wer.panClient.OpenapiPanClient().GetFileDownloadUrl(&aliyunpan.GetFileDownloadUrlParam{DriveId: wer.driveId, FileId: wer.fileId})
if apierr != nil {
logger.Verbosef("get new download url for worker: %d, error: %+v\n", wer.ID(), apierr)
wer.status.statusCode = StatusCodeTooManyConnections
return
}
wer.url = durl.Url
logger.Verbosef("get new download url for worker: %d, new url: %s\n", wer.ID(), wer.url)
}
// Canceled 是否已经取消
@@ -266,6 +269,7 @@ func (wer *Worker) Err() error {
// Execute 执行任务
func (wer *Worker) Execute() {
logger.Verbosef("Execute worker: %d\n", wer.ID())
wer.lazyInit()
wer.execMu.Lock()
+7 -6
View File
@@ -16,12 +16,13 @@ package uploader
import "fmt"
var (
UploadUrlExpired = fmt.Errorf("UrlExpired")
UploadPartNotSeq = fmt.Errorf("PartNotSequential")
UploadNoSuchUpload = fmt.Errorf("NoSuchUpload")
UploadTerminate = fmt.Errorf("UploadErrorTerminate")
UploadPartAlreadyExist = fmt.Errorf("PartAlreadyExist")
UploadHttpError = fmt.Errorf("HttpError")
UploadUrlExpired = fmt.Errorf("UrlExpired")
UploadPartNotSeq = fmt.Errorf("PartNotSequential")
UploadNoSuchUpload = fmt.Errorf("NoSuchUpload")
UploadTerminate = fmt.Errorf("UploadErrorTerminate")
UploadPartAlreadyExist = fmt.Errorf("PartAlreadyExist")
UploadHttpError = fmt.Errorf("HttpError")
UploadLocalFileAlreadyClosedError = fmt.Errorf("LocalFileAlreadyClosedError")
)
type (
+8 -5
View File
@@ -150,9 +150,10 @@ func (muer *MultiUploader) upload() (uperr error) {
}()
wg.Wait()
if uperr != nil {
if errors.Is(uperr, UploadPartNotSeq) || errors.Is(uperr, UploadNoSuchUpload) {
// 分片出现乱序,停止上传
// 清空数据,准备重新上传
if errors.Is(uperr, UploadPartNotSeq) ||
errors.Is(uperr, UploadNoSuchUpload) ||
errors.Is(uperr, UploadLocalFileAlreadyClosedError) {
// 清空数据,停止上传
uploadDeque = lane.NewDeque() // 清空待上传列表
}
}
@@ -165,8 +166,10 @@ func (muer *MultiUploader) upload() (uperr error) {
// 释放链路
uploadClient.CloseIdleConnections()
// 返回错误,通知上层客户端
if errors.Is(uperr, UploadPartNotSeq) || errors.Is(uperr, UploadNoSuchUpload) {
// 返回错误,通知上层客户端进行处理
if errors.Is(uperr, UploadPartNotSeq) ||
errors.Is(uperr, UploadNoSuchUpload) ||
errors.Is(uperr, UploadLocalFileAlreadyClosedError) {
return uperr
}
@@ -15,6 +15,15 @@ package pandownload
import (
"fmt"
"io"
"os"
"path"
"path/filepath"
"sort"
"strconv"
"strings"
"time"
"github.com/tickstep/aliyunpan-api/aliyunpan"
"github.com/tickstep/aliyunpan-api/aliyunpan/apierror"
"github.com/tickstep/aliyunpan/cmder/cmdtable"
@@ -26,19 +35,12 @@ import (
"github.com/tickstep/aliyunpan/internal/log"
"github.com/tickstep/aliyunpan/internal/plugins"
"github.com/tickstep/aliyunpan/internal/taskframework"
"github.com/tickstep/aliyunpan/internal/ui"
"github.com/tickstep/aliyunpan/internal/utils"
"github.com/tickstep/aliyunpan/library/requester/transfer"
"github.com/tickstep/library-go/converter"
"github.com/tickstep/library-go/logger"
"github.com/tickstep/library-go/requester/rio/speeds"
"io"
"os"
"path"
"path/filepath"
"sort"
"strconv"
"strings"
"time"
)
type (
@@ -58,7 +60,6 @@ type (
// 可选项
VerbosePrinter *logger.CmdVerbose
PrintFormat string
IsPrintStatus bool // 是否输出各个下载线程的详细信息
IsExecutedPermission bool // 下载成功后是否加上执行权限
IsOverwrite bool // 是否覆盖已存在的文件
@@ -74,6 +75,8 @@ type (
// 下载文件记录器
FileRecorder *log.FileRecorder
UI *ui.DashboardPanel // 下载统计面板UI
}
)
@@ -110,6 +113,26 @@ func (dtu *DownloadTaskUnit) verboseInfof(format string, a ...interface{}) {
}
}
// logf 打印文本日志
func (dtu *DownloadTaskUnit) logf(format string, a ...interface{}) {
if dtu.UI != nil {
// 使用UI面板显示下载日志
dtu.UI.Logf(format, a...)
return
}
// 使用传统单行文本显示下载日志,直接输出到控制台
fmt.Printf(format, a...)
}
// updateUITaskState 更新UI下载面板的任务状态
func (dtu *DownloadTaskUnit) updateUITaskState(state ui.TaskState, message string) {
if dtu.UI == nil {
return
}
dtu.UI.MarkTaskState(dtu.taskInfo.Id(), state, message)
}
// download 执行下载文件(非目录)
func (dtu *DownloadTaskUnit) download() (err error) {
var (
@@ -181,14 +204,17 @@ func (dtu *DownloadTaskUnit) download() (err error) {
return apierror.NewFailedApiError("")
})
// 检查输出格式
if dtu.PrintFormat == "" {
dtu.PrintFormat = DefaultPrintFormat
}
// 这里用共享变量的方式
isComplete := false
der.OnDownloadStatusEvent(func(status transfer.DownloadStatuser, workersCallback func(downloader.RangeWorkerFunc)) {
// 方式1:使用UI统计面板显示下载进度
if dtu.UI != nil {
// 更新UI面板任务状态
dtu.UI.UpdateTaskProgress(dtu.taskInfo.Id(), status.Downloaded(), status.TotalSize(), status.SpeedsPerSecond(), status.TimeLeft())
return
}
// 方式2:使用传统单行显示下载进度
// 这里可能会下载结束了, 还会输出内容
builder := &strings.Builder{}
if dtu.IsPrintStatus {
@@ -236,7 +262,7 @@ func (dtu *DownloadTaskUnit) download() (err error) {
})
der.OnExecute(func() {
fmt.Printf("[%s] 下载开始\n", dtu.taskInfo.Id())
dtu.logf("[%s] 下载开始\n", dtu.taskInfo.Id())
})
err = der.Execute()
@@ -253,7 +279,7 @@ func (dtu *DownloadTaskUnit) download() (err error) {
if removeErr != nil {
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
} else {
// 下载发生错误
@@ -278,10 +304,10 @@ func (dtu *DownloadTaskUnit) download() (err error) {
if dtu.IsExecutedPermission {
err = file.Chmod(0766)
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)
dtu.logf("\n[%s] 下载完成, 保存位置: %s\n", dtu.taskInfo.Id(), dtu.SavePath)
return nil
}
@@ -321,7 +347,7 @@ func (dtu *DownloadTaskUnit) checkFileValid(result *taskframework.TaskUnitRunRes
if dtu.fileInfo.FileSize >= 128*converter.MB {
// 大文件, 输出一句提示消息
fmt.Printf("[%s] 开始检验文件有效性, 请稍候...\n", dtu.taskInfo.Id())
dtu.logf("[%s] 开始检验文件有效性, 请稍候...\n", dtu.taskInfo.Id())
}
// 就在这里处理校验出错
@@ -334,7 +360,7 @@ func (dtu *DownloadTaskUnit) checkFileValid(result *taskframework.TaskUnitRunRes
// 文件不支持校验
result.ResultMessage = "检验文件有效性"
result.Err = err
fmt.Printf("[%s] 检验文件有效性: %s\n", dtu.taskInfo.Id(), err)
dtu.logf("[%s] 检验文件有效性: %s\n", dtu.taskInfo.Id(), err)
return true
case ErrDownloadFileBanned:
// 违规文件
@@ -352,7 +378,7 @@ func (dtu *DownloadTaskUnit) checkFileValid(result *taskframework.TaskUnitRunRes
}
}
fmt.Printf("[%s] 检验文件有效性成功: %s\n", dtu.taskInfo.Id(), dtu.SavePath)
dtu.logf("[%s] 检验文件有效性成功: %s\n", dtu.taskInfo.Id(), dtu.SavePath)
return true
}
@@ -360,16 +386,19 @@ func (dtu *DownloadTaskUnit) OnRetry(lastRunResult *taskframework.TaskUnitRunRes
// 输出错误信息
if lastRunResult.Err == nil {
// 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
}
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) {
// 执行插件
dtu.pluginCallback("success")
// 更新UI面板任务状态
dtu.updateUITaskState(ui.TaskSuccess, "")
// 下载文件数据记录
if config.Config.FileRecordConfig == "1" {
if dtu.fileInfo.IsFile() {
@@ -389,13 +418,20 @@ func (dtu *DownloadTaskUnit) OnFailed(lastRunResult *taskframework.TaskUnitRunRe
// 失败
dtu.pluginCallback("fail")
// 更新UI面板
failedMessage := lastRunResult.ResultMessage
if lastRunResult.Err != nil {
failedMessage = fmt.Sprintf("%s, %s", lastRunResult.ResultMessage, lastRunResult.Err)
}
dtu.updateUITaskState(ui.TaskFailed, failedMessage)
// 失败
if lastRunResult.Err == nil {
// result中不包含Err, 忽略输出
fmt.Printf("[%s] %s\n", dtu.taskInfo.Id(), lastRunResult.ResultMessage)
dtu.logf("[%s] %s\n", dtu.taskInfo.Id(), lastRunResult.ResultMessage)
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) {
@@ -428,7 +464,7 @@ func (dtu *DownloadTaskUnit) OnComplete(lastRunResult *taskframework.TaskUnitRun
}
func (dtu *DownloadTaskUnit) OnCancel(lastRunResult *taskframework.TaskUnitRunResult) {
dtu.updateUITaskState(ui.TaskCanceled, "取消: "+dtu.FilePanPath)
}
func (dtu *DownloadTaskUnit) RetryWait() time.Duration {
@@ -455,8 +491,8 @@ func (dtu *DownloadTaskUnit) Run() (result *taskframework.TaskUnitRunResult) {
}
// 输出文件信息
fmt.Print("\n")
fmt.Printf("[%s] ----\n%s\n", dtu.taskInfo.Id(), dtu.fileInfo.String())
dtu.logf("\n")
dtu.logf("[%s] ----\n%s\n", dtu.taskInfo.Id(), dtu.fileInfo.String())
// 调用插件
ft := "file"
@@ -481,7 +517,8 @@ func (dtu *DownloadTaskUnit) Run() (result *taskframework.TaskUnitRunResult) {
if downloadFilePrepareResult, er := plugin.DownloadFilePrepareCallback(plugins.GetContext(config.Config.ActiveUser()), pluginParam); er == nil && downloadFilePrepareResult != nil {
if strings.Compare("yes", downloadFilePrepareResult.DownloadApproved) != 0 {
// skip download this file
fmt.Printf("插件取消了该文件下载: %s\n", dtu.fileInfo.Path)
dtu.logf("插件取消了该文件下载: %s\n", dtu.fileInfo.Path)
dtu.updateUITaskState(ui.TaskCanceled, "插件取消了该文件下载: "+dtu.fileInfo.Path)
result.Succeed = false
result.Cancel = true
return
@@ -490,7 +527,7 @@ func (dtu *DownloadTaskUnit) Run() (result *taskframework.TaskUnitRunResult) {
targetSaveRelativePath := strings.TrimPrefix(downloadFilePrepareResult.LocalFilePath, "/")
targetSaveRelativePath = strings.TrimPrefix(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 +600,7 @@ func (dtu *DownloadTaskUnit) Run() (result *taskframework.TaskUnitRunResult) {
// 是否排除下载
if utils.IsExcludeFile(fileList[k].Path, &dtu.Cfg.ExcludeNames) {
fmt.Printf("排除文件: %s\n", fileList[k].Path)
dtu.logf("排除文件: %s\n", fileList[k].Path)
continue
}
@@ -583,7 +620,11 @@ func (dtu *DownloadTaskUnit) Run() (result *taskframework.TaskUnitRunResult) {
// 加入父队列,按照队列调度进行下载
info := dtu.ParentTaskExecutor.Append(&subUnit, dtu.taskInfo.MaxRetry())
fmt.Printf("[%s] 加入下载队列: %s\n", info.Id(), fileList[k].Path)
dtu.logf("[%s] 加入下载队列: %s\n", info.Id(), fileList[k].Path)
// UI面板注册任务
if dtu.UI != nil {
dtu.UI.RegisterTask(info.Id(), fileList[k].Path, fileList[k].FileSize, fileList[k].IsFile())
}
}
// 本下载任务执行成功
@@ -591,7 +632,7 @@ func (dtu *DownloadTaskUnit) Run() (result *taskframework.TaskUnitRunResult) {
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) {
// fmt.Printf("[%s] 文件已经存在: %s, 跳过...\n", dtu.taskInfo.Id(), dtu.SavePath)
@@ -600,12 +641,15 @@ func (dtu *DownloadTaskUnit) Run() (result *taskframework.TaskUnitRunResult) {
//}
// 支持符号文件,逻辑和注释代码一致
if !dtu.IsOverwrite && SymlinkFileExist(dtu.SavePath, dtu.OriginSaveRootPath) {
fmt.Printf("[%s] 文件已经存在: %s, 跳过...\n", dtu.taskInfo.Id(), dtu.SavePath)
dtu.updateUITaskState(ui.TaskSkipped, "文件已存在,跳过: "+dtu.SavePath)
dtu.logf("[%s] 文件已经存在: %s, 跳过...\n", dtu.taskInfo.Id(), dtu.SavePath)
result.Succeed = true // 执行成功
return
}
fmt.Printf("[%s] 将会下载到路径: %s\n", dtu.taskInfo.Id(), dtu.SavePath)
dtu.logf("[%s] 将会下载到路径: %s\n", dtu.taskInfo.Id(), dtu.SavePath)
// 更新UI面板任务状态
dtu.updateUITaskState(ui.TaskRunning, "")
var ok bool
er := dtu.download()
+108 -17
View File
@@ -4,23 +4,40 @@ import (
"encoding/hex"
"encoding/json"
"fmt"
"github.com/tickstep/aliyunpan/internal/global"
"github.com/tickstep/aliyunpan/internal/utils"
"github.com/tickstep/library-go/crypto"
"github.com/tickstep/library-go/getip"
"github.com/tickstep/library-go/logger"
"github.com/tickstep/library-go/requester"
"net/url"
"regexp"
"runtime"
"strings"
"time"
"github.com/tickstep/aliyunpan/internal/global"
"github.com/tickstep/aliyunpan/internal/utils"
"github.com/tickstep/library-go/crypto"
"github.com/tickstep/library-go/logger"
"github.com/tickstep/library-go/requester"
)
type LoginHelper struct {
webHost string
}
const (
// SUCCESS 成功
SUCCESS = 0
// ERROR 失败
ERROR = 500
// ERROR_TOO_MANY_REQUESTS 请求太频繁,已被限流
ERROR_TOO_MANY_REQUESTS = 429
// ERROR_NEED_LOGIN_AGAIN Token已经过期,需要重新登录
ERROR_NEED_LOGIN_AGAIN = 100004
)
// CommonResultError 通用接口错误
type CommonResultError struct {
Code int `json:"code"`
Msg string `json:"msg"`
}
type LoginHttpResult struct {
Code int `json:"code"`
Msg string `json:"msg"`
@@ -51,6 +68,13 @@ type CommonTokenEntity struct {
Webapi *LoginTokenResult `json:"webapi"`
}
func NewCommonResultError(msg string) *CommonResultError {
return &CommonResultError{
Code: 500,
Msg: msg,
}
}
func NewLoginHelper(webHost string) *LoginHelper {
return &LoginHelper{
webHost: webHost,
@@ -63,7 +87,7 @@ func (h *LoginHelper) GetQRCodeLoginUrl(keyStr string) (*QRCodeUrlResult, error)
keyStr = utils.GetUniqueKeyStr()
}
fullUrl := strings.Builder{}
ipAddr, err := getip.IPInfoFromTechainBaidu()
ipAddr, err := utils.GetPublicIP()
if err != nil {
ipAddr = "127.0.0.1"
}
@@ -86,6 +110,16 @@ func (h *LoginHelper) GetQRCodeLoginUrl(keyStr string) (*QRCodeUrlResult, error)
return nil, err
}
// 通用接口错误
commonResult := &CommonResultError{}
if err1 := json.Unmarshal(body, commonResult); err1 != nil {
logger.Verboseln("parse openapi result json error ", err1)
return nil, err1
}
if commonResult.Code != 0 {
return nil, fmt.Errorf(commonResult.Msg)
}
errResp := &LoginHttpResult{}
if err1 := json.Unmarshal(body, errResp); err1 != nil {
logger.Verboseln("parse qrcode result json error ", err1)
@@ -137,6 +171,16 @@ func (h *LoginHelper) GetQRCodeLoginResult(tokenId string) (*QRCodeLoginResult,
return nil, err
}
// 通用接口错误
commonResult := &CommonResultError{}
if err1 := json.Unmarshal(body, commonResult); err1 != nil {
logger.Verboseln("parse openapi result json error ", err1)
return nil, err1
}
if commonResult.Code != 0 {
return nil, fmt.Errorf(commonResult.Msg)
}
errResp := &LoginHttpResult{}
if err1 := json.Unmarshal(body, errResp); err1 != nil {
logger.Verboseln("parse qrcode result json error ", err1)
@@ -177,6 +221,16 @@ func (h *LoginHelper) GetRefreshToken(tokenId string) (*QRCodeLoginResult, error
return nil, err
}
// 通用接口错误
commonResult := &CommonResultError{}
if err1 := json.Unmarshal(body, commonResult); err1 != nil {
logger.Verboseln("parse openapi result json error ", err1)
return nil, err1
}
if commonResult.Code != 0 {
return nil, fmt.Errorf(commonResult.Msg)
}
errResp := &LoginHttpResult{}
if err1 := json.Unmarshal(body, errResp); err1 != nil {
logger.Verboseln("parse qrcode result json error ", err1)
@@ -233,7 +287,7 @@ func (h *LoginHelper) ParseSecureRefreshToken(keyStr, secureRefreshToken string)
}
// GetWebapiNewToken 获取Webapi Token
func (h *LoginHelper) GetWebapiNewToken(ticketId, userId, oldAccessToken string) (*LoginTokenResult, error) {
func (h *LoginHelper) GetWebapiNewToken(ticketId, userId, oldAccessToken string) (*LoginTokenResult, *CommonResultError) {
fullUrl := strings.Builder{}
fmt.Fprintf(&fullUrl, "%s/auth/tickstep/aliyunpan/token/webapi/%s/refresh?userId=%s",
h.webHost, ticketId, userId)
@@ -251,16 +305,29 @@ func (h *LoginHelper) GetWebapiNewToken(ticketId, userId, oldAccessToken string)
body, err := client.Fetch("GET", fullUrl.String(), nil, header)
if err != nil {
logger.Verboseln("get web token result error ", err)
return nil, err
return nil, NewCommonResultError(err.Error())
}
// 通用接口错误
commonResult := &CommonResultError{}
if err1 := json.Unmarshal(body, commonResult); err1 != nil {
logger.Verboseln("parse openapi result json error ", err1)
return nil, NewCommonResultError(err1.Error())
}
if commonResult.Code != 0 {
return nil, commonResult
}
errResp := &LoginHttpResult{}
if err1 := json.Unmarshal(body, errResp); err1 != nil {
logger.Verboseln("parse result json error ", err1)
return nil, err1
return nil, NewCommonResultError(err1.Error())
}
if errResp.Code != 0 {
return nil, fmt.Errorf(errResp.Msg)
return nil, &CommonResultError{
Code: errResp.Code,
Msg: errResp.Msg,
}
}
// parse result
@@ -268,13 +335,13 @@ func (h *LoginHelper) GetWebapiNewToken(ticketId, userId, oldAccessToken string)
r.Data = &LoginTokenResult{}
if err2 := json.Unmarshal(body, r); err2 != nil {
logger.Verboseln("parse web token result json error ", err2)
return nil, err2
return nil, NewCommonResultError(err2.Error())
}
return r.Data.(*LoginTokenResult), nil
}
// GetOpenapiNewToken 获取Openapi Token
func (h *LoginHelper) GetOpenapiNewToken(ticketId, userId, oldAccessToken string) (*LoginTokenResult, error) {
func (h *LoginHelper) GetOpenapiNewToken(ticketId, userId, oldAccessToken string) (*LoginTokenResult, *CommonResultError) {
fullUrl := strings.Builder{}
fmt.Fprintf(&fullUrl, "%s/auth/tickstep/aliyunpan/token/openapi/%s/refresh?userId=%s",
h.webHost, ticketId, userId)
@@ -292,16 +359,30 @@ func (h *LoginHelper) GetOpenapiNewToken(ticketId, userId, oldAccessToken string
body, err := client.Fetch("GET", fullUrl.String(), nil, header)
if err != nil {
logger.Verboseln("get openapi token result error ", err)
return nil, err
return nil, NewCommonResultError(err.Error())
}
// 通用接口错误
commonResult := &CommonResultError{}
if err1 := json.Unmarshal(body, commonResult); err1 != nil {
logger.Verboseln("parse openapi result json error ", err1)
return nil, NewCommonResultError(err1.Error())
}
if commonResult.Code != 0 {
return nil, commonResult
}
// 解析结果
errResp := &LoginHttpResult{}
if err1 := json.Unmarshal(body, errResp); err1 != nil {
logger.Verboseln("parse result json error ", err1)
return nil, err1
return nil, NewCommonResultError(err1.Error())
}
if errResp.Code != 0 {
return nil, fmt.Errorf(errResp.Msg)
return nil, &CommonResultError{
Code: errResp.Code,
Msg: errResp.Msg,
}
}
// parse result
@@ -309,7 +390,7 @@ func (h *LoginHelper) GetOpenapiNewToken(ticketId, userId, oldAccessToken string
r.Data = &LoginTokenResult{}
if err2 := json.Unmarshal(body, r); err2 != nil {
logger.Verboseln("parse openapi token result json error ", err2)
return nil, err2
return nil, NewCommonResultError(err2.Error())
}
return r.Data.(*LoginTokenResult), nil
}
@@ -335,6 +416,16 @@ func (h *LoginHelper) GetLoginToken(ticketId string) (*CommonTokenEntity, error)
return nil, err
}
// 通用接口错误
commonResult := &CommonResultError{}
if err1 := json.Unmarshal(body, commonResult); err1 != nil {
logger.Verboseln("parse openapi result json error ", err1)
return nil, err1
}
if commonResult.Code != 0 {
return nil, fmt.Errorf(commonResult.Msg)
}
errResp := &LoginHttpResult{}
if err1 := json.Unmarshal(body, errResp); err1 != nil {
logger.Verboseln("parse result json error ", err1)
+45 -10
View File
@@ -16,13 +16,18 @@ package panupload
import (
"context"
"encoding/xml"
"errors"
"io"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"time"
"github.com/tickstep/aliyunpan/internal/config"
"github.com/tickstep/library-go/logger"
"github.com/tickstep/library-go/requester"
"io"
"net/http"
"strconv"
"time"
"github.com/tickstep/aliyunpan-api/aliyunpan"
"github.com/tickstep/aliyunpan-api/aliyunpan/apierror"
@@ -72,6 +77,11 @@ func (pu *PanUpload) Precreate() (err error) {
}
func (pu *PanUpload) UploadFile(ctx context.Context, partseq int, partOffset int64, partEnd int64, r rio.ReaderLen64, uploadClient *requester.HTTPClient) (uploadDone bool, uperr error) {
if len(pu.uploadOpEntity.PartInfoList) == 0 {
logger.Verbosef("网盘上传参数错误,分片%d => uploadOpEntity.PartInfoList为空\n", partseq+1)
return true, nil
}
pu.lazyInit()
// check url expired or not
@@ -102,6 +112,7 @@ func (pu *PanUpload) UploadFile(ctx context.Context, partseq int, partOffset int
}
var respErr *uploader.MultiError
// 分片上传回调函数,供阿里云盘API接口使用
uploadFunc := func(httpMethod, fullUrl string, headers map[string]string) (*http.Response, error) {
var resp *http.Response
var respError error = nil
@@ -124,10 +135,10 @@ func (pu *PanUpload) UploadFile(ctx context.Context, partseq int, partOffset int
if blen > 0 {
buf := make([]byte, blen)
resp.Body.Read(buf)
logger.Verbosef("分片上传出错: 分片%d => %s\n", partseq+1, string(buf))
logger.Verbosef("分片上传出错: 分片%d, 错误信息: %s\n", partseq+1, string(buf))
errResp := &apierror.ErrorXmlResp{}
if err := xml.Unmarshal(buf, errResp); err == nil {
if errXml := xml.Unmarshal(buf, errResp); errXml == nil {
if errResp.Code != "" {
if "PartNotSequential" == errResp.Code {
respError = uploader.UploadPartNotSeq
@@ -184,10 +195,31 @@ func (pu *PanUpload) UploadFile(ctx context.Context, partseq int, partOffset int
}
}
} else {
// 默认错误
respError = uploader.UploadTerminate
respErr = &uploader.MultiError{
Terminated: true,
}
// 解析HTTP请求出现的错误信息
if err != nil {
var netErr *url.Error
if errors.As(err, &netErr) {
var pathErr *os.PathError
if errors.As(netErr.Err, &pathErr) { // 本地文件读取异常
if strings.Contains(pathErr.Err.Error(), "file already closed") {
// 本地文件流触发"file already closed"错误
// 一般是文件上传过程中,文件被删除了,或者文件所在的硬盘异常导致文件流被系统关闭
respError = uploader.UploadLocalFileAlreadyClosedError
respErr = &uploader.MultiError{
Terminated: true,
NeedStartOver: false,
Err: uploader.UploadLocalFileAlreadyClosedError,
}
}
}
}
}
}
return resp, respError
}
@@ -197,7 +229,7 @@ func (pu *PanUpload) UploadFile(ctx context.Context, partseq int, partOffset int
apiError := pu.panClient.OpenapiPanClient().UploadFileData(uploadUrl, uploadFunc)
if respErr != nil {
if respErr.Err == uploader.UploadUrlExpired {
if errors.Is(respErr.Err, uploader.UploadUrlExpired) {
// URL过期,获取新的URL
guur, er := pu.panClient.OpenapiPanClient().GetUploadUrl(&aliyunpan.GetUploadUrlParam{
DriveId: pu.driveId,
@@ -213,16 +245,19 @@ func (pu *PanUpload) UploadFile(ctx context.Context, partseq int, partOffset int
// 获取新的上传URL重试一次
pu.uploadOpEntity.PartInfoList[partseq] = guur.PartInfoList[0]
uploadUrl := pu.uploadOpEntity.PartInfoList[partseq].UploadURL
uploadUrl = pu.uploadOpEntity.PartInfoList[partseq].UploadURL
apiError = pu.panClient.OpenapiPanClient().UploadFileData(uploadUrl, uploadFunc)
} else if respErr.Err == uploader.UploadPartAlreadyExist {
} else if errors.Is(respErr.Err, uploader.UploadPartAlreadyExist) {
// already upload
// success
return true, nil
} else if respErr.Err == uploader.UploadPartNotSeq {
} else if errors.Is(respErr.Err, uploader.UploadPartNotSeq) {
// 上传分片乱序了
// 返回错误,然后重传
return false, respErr
} else if errors.Is(respErr.Err, uploader.UploadLocalFileAlreadyClosedError) {
// 本地文件读取错误
return false, respErr
} else {
return false, respErr
}
@@ -16,9 +16,6 @@ package panupload
import (
"errors"
"fmt"
"github.com/tickstep/aliyunpan/internal/log"
"github.com/tickstep/aliyunpan/internal/plugins"
"github.com/tickstep/library-go/logger"
"os"
"path"
"path/filepath"
@@ -26,6 +23,11 @@ import (
"sync"
"time"
"github.com/tickstep/aliyunpan/internal/log"
"github.com/tickstep/aliyunpan/internal/plugins"
"github.com/tickstep/aliyunpan/internal/ui"
"github.com/tickstep/library-go/logger"
"github.com/tickstep/aliyunpan/internal/utils"
"github.com/tickstep/library-go/requester/rio/speeds"
@@ -79,6 +81,8 @@ type (
// 上传文件记录器
FileRecorder *log.FileRecorder
UI *ui.DashboardPanel // 上传统计面板UI
}
)
@@ -97,6 +101,26 @@ const (
StrUploadFailed = "上传文件失败"
)
// logf 打印文本日志
func (utu *UploadTaskUnit) logf(format string, a ...interface{}) {
if utu.UI != nil {
// 使用UI面板显示日志
utu.UI.Logf(format, a...)
return
}
// 使用传统单行文本显示下载日志,直接输出到控制台
fmt.Printf(format, a...)
}
// updateUITaskState 更新UI面板的任务状态
func (utu *UploadTaskUnit) updateUITaskState(state ui.TaskState, message string) {
if utu.UI == nil {
return
}
utu.UI.MarkTaskState(utu.taskInfo.Id(), state, message)
}
func (utu *UploadTaskUnit) SetTaskInfo(taskInfo *taskframework.TaskInfo) {
utu.taskInfo = taskInfo
}
@@ -127,7 +151,7 @@ func (utu *UploadTaskUnit) prepareFile() {
}
//if utu.LocalFileChecksum.Length > MaxRapidUploadSize {
// fmt.Printf("[%s] 文件超过20GB, 无法使用秒传功能, 跳过秒传...\n", utu.taskInfo.Id())
// utu.logf("[%s] 文件超过20GB, 无法使用秒传功能, 跳过秒传...\n", utu.taskInfo.Id())
// utu.Step = StepUploadUpload
// return
//}
@@ -142,13 +166,13 @@ func (utu *UploadTaskUnit) rapidUpload() (isContinue bool, result *taskframework
// 是否可以秒传
result = &taskframework.TaskUnitRunResult{}
fmt.Printf("[%s] %s 检测秒传中, 请稍候...\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"))
utu.logf("[%s] %s 检测秒传中, 请稍候...\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"))
if utu.LocalFileChecksum.UploadOpEntity.RapidUpload {
fmt.Printf("[%s] %s 秒传成功, 保存到网盘路径: %s\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"), utu.SavePath)
utu.logf("[%s] %s 秒传成功, 保存到网盘路径: %s\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"), utu.SavePath)
result.Succeed = true
return false, result
} else {
fmt.Printf("[%s] %s 秒传失败,开始正常上传文件\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"))
utu.logf("[%s] %s 秒传失败,开始正常上传文件\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"))
result.Succeed = false
result.ResultMessage = "文件未曾上传,无法秒传"
return true, result
@@ -170,6 +194,13 @@ func (utu *UploadTaskUnit) upload() (result *taskframework.TaskUnitRunResult) {
MaxRate: config.Config.MaxUploadRate,
}, utu.LocalFileChecksum.UploadOpEntity, utu.PanClient, utu.GlobalSpeedsStat)
// 模拟 file already closed 错误
//go func(f *os.File) {
// time.Sleep(10 * time.Second)
// f.Close()
// utu.logf("关闭文件流: %s\n", f.Name())
//}(utu.LocalFileChecksum.GetFile())
// 设置断点续传
if utu.state != nil {
muer.SetInstanceState(utu.state)
@@ -183,6 +214,14 @@ func (utu *UploadTaskUnit) upload() (result *taskframework.TaskUnitRunResult) {
default:
}
// 方式1:使用UI统计面板显示进度
if utu.UI != nil {
// 更新UI面板任务状态
utu.UI.UpdateTaskProgress(utu.taskInfo.Id(), status.Uploaded(), status.TotalSize(), status.SpeedsPerSecond(), status.TimeLeft())
return
}
// 方式2:使用传统单行显示下载进度
if utu.ShowProgress {
// 如果上传速度为0, 剩余时间未知, 则用 - 代替
var leftStr string
@@ -192,8 +231,11 @@ func (utu *UploadTaskUnit) upload() (result *taskframework.TaskUnitRunResult) {
} else {
leftStr = left.String()
}
uploadedPercentage := fmt.Sprintf("%.2f%%", float64(status.Uploaded())/float64(status.TotalSize())*100)
fmt.Printf("\r[%s] ↑ %s/%s(%s) %s/s(%s/s) in %s, left %s ............", utu.taskInfo.Id(),
uploadedPercentage := "0.00%"
if status.TotalSize() > 0 {
uploadedPercentage = fmt.Sprintf("%.2f%%", float64(status.Uploaded())/float64(status.TotalSize())*100)
}
utu.logf("\r[%s] ↑ %s/%s(%s) %s/s(%s/s) in %s, left %s ............", utu.taskInfo.Id(),
converter.ConvertFileSize(status.Uploaded(), 2),
converter.ConvertFileSize(status.TotalSize(), 2),
uploadedPercentage,
@@ -208,8 +250,8 @@ func (utu *UploadTaskUnit) upload() (result *taskframework.TaskUnitRunResult) {
// result
result = &taskframework.TaskUnitRunResult{}
muer.OnSuccess(func() {
fmt.Printf("\n")
fmt.Printf("[%s] %s 上传文件成功, 保存到网盘路径: %s\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"), utu.SavePath)
utu.logf("\n")
utu.logf("[%s] %s 上传文件成功, 保存到网盘路径: %s\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"), utu.SavePath)
// 统计
utu.UploadStatistic.AddTotalSize(utu.LocalFileChecksum.Length)
utu.UploadingDatabase.Delete(&utu.LocalFileChecksum.LocalFileMeta) // 删除
@@ -254,16 +296,19 @@ func (utu *UploadTaskUnit) OnRetry(lastRunResult *taskframework.TaskUnitRunResul
// 输出错误信息
if lastRunResult.Err == nil {
// result中不包含Err, 忽略输出
fmt.Printf("[%s] %s, 重试 %d/%d\n", utu.taskInfo.Id(), lastRunResult.ResultMessage, utu.taskInfo.Retry(), utu.taskInfo.MaxRetry())
utu.logf("[%s] %s, 重试 %d/%d\n", utu.taskInfo.Id(), lastRunResult.ResultMessage, utu.taskInfo.Retry(), utu.taskInfo.MaxRetry())
return
}
fmt.Printf("[%s] %s, %s, 重试 %d/%d\n", utu.taskInfo.Id(), lastRunResult.ResultMessage, lastRunResult.Err, utu.taskInfo.Retry(), utu.taskInfo.MaxRetry())
utu.logf("[%s] %s, %s, 重试 %d/%d\n", utu.taskInfo.Id(), lastRunResult.ResultMessage, lastRunResult.Err, utu.taskInfo.Retry(), utu.taskInfo.MaxRetry())
}
func (utu *UploadTaskUnit) OnSuccess(lastRunResult *taskframework.TaskUnitRunResult) {
// 执行插件
utu.pluginCallback("success")
// 更新UI面板状态
utu.updateUITaskState(ui.TaskSuccess, "")
// 上传文件数据记录
if config.Config.FileRecordConfig == "1" {
utu.FileRecorder.Append(&log.FileRecordItem{
@@ -278,6 +323,7 @@ func (utu *UploadTaskUnit) OnSuccess(lastRunResult *taskframework.TaskUnitRunRes
func (utu *UploadTaskUnit) OnFailed(lastRunResult *taskframework.TaskUnitRunResult) {
// 失败
utu.pluginCallback("fail")
utu.updateUITaskState(ui.TaskCanceled, "取消: "+utu.LocalFileChecksum.Path.LogicPath)
}
func (utu *UploadTaskUnit) pluginCallback(result string) {
@@ -309,7 +355,12 @@ func (utu *UploadTaskUnit) OnComplete(lastRunResult *taskframework.TaskUnitRunRe
// 任务结束,可能成功也可能失败
}
func (utu *UploadTaskUnit) OnCancel(lastRunResult *taskframework.TaskUnitRunResult) {
// 更新UI面板
failedMessage := lastRunResult.ResultMessage
if lastRunResult.Err != nil {
failedMessage = fmt.Sprintf("%s, %s", lastRunResult.ResultMessage, lastRunResult.Err)
}
utu.updateUITaskState(ui.TaskCanceled, failedMessage)
}
func (utu *UploadTaskUnit) RetryWait() time.Duration {
return functions.RetryWait(utu.taskInfo.Retry())
@@ -318,7 +369,7 @@ func (utu *UploadTaskUnit) RetryWait() time.Duration {
func (utu *UploadTaskUnit) Run() (result *taskframework.TaskUnitRunResult) {
err := utu.LocalFileChecksum.OpenPath()
if err != nil {
fmt.Printf("[%s] 文件不可读, 错误信息: %s, 跳过...\n", utu.taskInfo.Id(), err)
utu.logf("[%s] 文件不可读, 错误信息: %s, 跳过...\n", utu.taskInfo.Id(), err)
return
}
defer utu.LocalFileChecksum.Close() // 关闭文件
@@ -326,7 +377,8 @@ func (utu *UploadTaskUnit) Run() (result *taskframework.TaskUnitRunResult) {
timeStart := time.Now()
result = &taskframework.TaskUnitRunResult{}
fmt.Printf("[%s] %s 准备上传: %s => %s\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"), utu.LocalFileChecksum.Path.LogicPath, utu.SavePath)
utu.logf("[%s] %s 准备上传: %s => %s\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"), utu.LocalFileChecksum.Path.LogicPath, utu.SavePath)
utu.updateUITaskState(ui.TaskRunning, "")
defer func() {
var msg string
@@ -337,7 +389,7 @@ func (utu *UploadTaskUnit) Run() (result *taskframework.TaskUnitRunResult) {
} else {
msg = result.ResultMessage
}
fmt.Printf("[%s] %s 文件上传结果: %s 耗时 %s\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"), msg, utils.ConvertTime(time.Now().Sub(timeStart)))
utu.logf("[%s] %s 文件上传结果: %s 耗时 %s\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"), msg, utils.ConvertTime(time.Now().Sub(timeStart)))
}()
// 准备文件
@@ -373,7 +425,7 @@ StepUploadPrepareUpload:
saveFilePath = path.Dir(utu.SavePath)
if saveFilePath != "/" {
utu.FolderCreateMutex.Lock()
fmt.Printf("[%s] %s 正在检测和创建云盘文件夹: %s\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"), saveFilePath)
utu.logf("[%s] %s 正在检测和创建云盘文件夹: %s\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"), saveFilePath)
fe, apierr1 := utu.PanClient.OpenapiPanClient().FileInfoByPath(utu.DriveId, saveFilePath)
time.Sleep(1 * time.Second)
needToCreateFolder := false
@@ -422,7 +474,8 @@ StepUploadPrepareUpload:
if efi != nil && efi.FileId != "" {
result.Succeed = true
result.Extra = efi
fmt.Printf("[%s] %s 检测到同名文件,跳过上传: %s\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"), utu.SavePath)
utu.logf("[%s] %s 检测到同名文件,跳过上传: %s\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"), utu.SavePath)
utu.updateUITaskState(ui.TaskSkipped, "")
return
}
}
@@ -447,7 +500,7 @@ StepUploadPrepareUpload:
if preHashMatch { // preHashMatch为true,代表该文件可能已经被上传过,能够支持秒传,所以需要进一步计算完整SHA1进行检测是否能秒传
// 计算完整文件SHA1
fmt.Printf("[%s] %s 正在计算文件SHA1: %s\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"), utu.LocalFileChecksum.Path.LogicPath)
utu.logf("[%s] %s 正在计算文件SHA1: %s\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"), utu.LocalFileChecksum.Path.LogicPath)
utu.LocalFileChecksum.Sum(localfile.CHECKSUM_SHA1)
sha1Str = utu.LocalFileChecksum.SHA1
if utu.LocalFileChecksum.Length == 0 {
@@ -467,7 +520,7 @@ StepUploadPrepareUpload:
checkNameMode = "auto_rename"
}
} else {
fmt.Printf("[%s] %s 已经禁用秒传检测,直接上传\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"))
utu.logf("[%s] %s 已经禁用秒传检测,直接上传\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"))
sha1Str = ""
contentHashName = ""
checkNameMode = "auto_rename"
@@ -480,7 +533,7 @@ StepUploadPrepareUpload:
if strings.ToUpper(efi.ContentHash) == strings.ToUpper(sha1Str) {
result.Succeed = true
result.Extra = efi
fmt.Printf("[%s] %s 检测到同名文件,文件内容完全一致,无需重复上传: %s\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"), utu.SavePath)
utu.logf("[%s] %s 检测到同名文件,文件内容完全一致,无需重复上传: %s\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"), utu.SavePath)
return
}
// existed, delete it
@@ -493,7 +546,7 @@ StepUploadPrepareUpload:
return
}
time.Sleep(time.Duration(500) * time.Millisecond)
fmt.Printf("[%s] %s 检测到同名文件,文件内容不一致,已将旧文件移动到回收站: %s\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"), utu.SavePath)
utu.logf("[%s] %s 检测到同名文件,文件内容不一致,已将旧文件移动到回收站: %s\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"), utu.SavePath)
}
}
@@ -544,7 +597,7 @@ StepUploadPrepareUpload:
// 重试
result.NeedRetry = true
} else if apierr.Code == apierror.ApiCodeUploadPayloadTooLarge {
fmt.Printf("[%s] %s 上传文件的大小超出限制,你可能需要开通阿里云盘三方权益包以便上传大文件\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"))
utu.logf("[%s] %s 上传文件的大小超出限制,你可能需要开通阿里云盘三方权益包以便上传大文件\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"))
}
return
}
@@ -572,7 +625,7 @@ stepUploadUpload:
if ee := utu.amendFileUploadPartNum(); ee != nil {
// 修正分片乱序失败,先令上传任务直接失败
logger.Verboseln("WARNING! amend uploaded parts num failed")
fmt.Printf("[%s] %s 无法修正上传分片乱序的错误,建议重新上传\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"))
utu.logf("[%s] %s 无法修正上传分片乱序的错误,建议重新上传\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"))
uploadResult = &taskframework.TaskUnitRunResult{
Succeed: false,
NeedRetry: false,
@@ -588,18 +641,34 @@ stepUploadUpload:
}
if errors.Is(uploadResult.Err, uploader.UploadNoSuchUpload) {
// 上传任务过期
fmt.Printf("[%s] %s 网盘上传任务不存在,创建新任务重新上传文件\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"))
utu.logf("[%s] %s 网盘上传任务不存在,创建新任务重新上传文件\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"))
// 需要重新从0开始上传
uploadResult = nil
utu.LocalFileChecksum.UploadOpEntity = nil
utu.state = nil
goto StepUploadPrepareUpload
}
if errors.Is(uploadResult.Err, uploader.UploadLocalFileAlreadyClosedError) {
// 本地文件读取错误
utu.logf("[%s] %s 本地文件读取错误,建议检查文件是否存在,是否有文件读取权限,或者硬盘是否有异常\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"))
uploadResult = &taskframework.TaskUnitRunResult{
Succeed: false,
NeedRetry: false,
Cancel: false,
Err: uploadResult.Err,
ResultCode: 0,
ResultMessage: "",
Extra: nil,
}
return uploadResult
}
// 处理阿里云盘API接口错误
var apier *apierror.ApiError
if errors.As(uploadResult.Err, &apier) {
// 上传任务过期
if apier.Code == apierror.ApiCodeUploadIdNotFound {
fmt.Printf("[%s] %s 网盘上传任务已失效,创建新任务重新上传文件\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"))
utu.logf("[%s] %s 网盘上传任务已失效,创建新任务重新上传文件\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"))
uploadResult = nil
utu.LocalFileChecksum.UploadOpEntity = nil
utu.state = nil
+9 -9
View File
@@ -16,15 +16,15 @@ package panupload
import (
"crypto/sha1"
"encoding/hex"
"github.com/tickstep/aliyunpan/internal/config"
"github.com/tickstep/library-go/converter"
"github.com/tickstep/library-go/logger"
"net/url"
"os"
"path"
"strconv"
"strings"
"time"
"github.com/tickstep/aliyunpan/internal/config"
"github.com/tickstep/aliyunpan/internal/utils"
"github.com/tickstep/library-go/converter"
"github.com/tickstep/library-go/logger"
)
const (
@@ -55,16 +55,16 @@ func getBlockSize(fileSize int64) int64 {
// IsUrlExpired 上传链接是否已过期。过期返回True
func IsUrlExpired(urlStr string) bool {
u, err := url.Parse(urlStr)
expiredTimeSec, err := utils.GetExpiredTimeSecFromOSSUrl(urlStr)
if err != nil {
// 解析错误,默认过期
return true
}
expiredTimeSecStr := u.Query().Get("x-oss-expires")
expiredTimeSec, _ := strconv.ParseInt(expiredTimeSecStr, 10, 64)
if (expiredTimeSec - time.Now().Unix()) <= 300 { // 小于5分钟
// expired
return true
}
// 返回未过期
return false
}
@@ -87,7 +87,7 @@ func IsVideoFile(fileName string) bool {
// CalcFilePreHash 计算文件 PreHash
func CalcFilePreHash(filePath string) string {
localFile, _ := os.Open(filePath)
localFile, _ := os.OpenFile(filePath, os.O_RDONLY, 0)
defer localFile.Close()
bytes := make([]byte, 1024)
localFile.ReadAt(bytes, 0)
+30 -28
View File
@@ -17,6 +17,16 @@ import (
"archive/zip"
"bytes"
"fmt"
"io/ioutil"
"net/http"
"path/filepath"
"regexp"
"runtime"
"strconv"
"strings"
"sync"
"time"
jsoniter "github.com/json-iterator/go"
"github.com/tickstep/aliyunpan/cmder/cmdliner"
"github.com/tickstep/aliyunpan/cmder/cmdutil"
@@ -27,19 +37,9 @@ import (
"github.com/tickstep/library-go/cachepool"
"github.com/tickstep/library-go/checkaccess"
"github.com/tickstep/library-go/converter"
"github.com/tickstep/library-go/getip"
"github.com/tickstep/library-go/jsonhelper"
"github.com/tickstep/library-go/logger"
"github.com/tickstep/library-go/requester"
"io/ioutil"
"net/http"
"path/filepath"
"regexp"
"runtime"
"strconv"
"strings"
"sync"
"time"
)
const (
@@ -58,16 +58,16 @@ type tsResp struct {
Msg string `json:"msg"`
}
func getReleaseFromTicstep(client *requester.HTTPClient, showPrompt bool) *ReleaseInfo {
func getReleaseFromTickstep(client *requester.HTTPClient, showPrompt bool) *ReleaseInfo {
tsReleaseInfo := &ReleaseInfo{}
tsRespObj := &tsResp{Data: tsReleaseInfo}
fullUrl := strings.Builder{}
ipAddr, err := getip.IPInfoFromTechainBaidu()
ipAddr, err := utils.GetPublicIP()
if err != nil {
ipAddr = "127.0.0.1"
}
fmt.Fprintf(&fullUrl, "http://api.tickstep.com/update/tickstep/aliyunpan/releases/latest?ip=%s&os=%s&arch=%s&version=%s",
ipAddr, runtime.GOOS, runtime.GOARCH, global.AppVersion)
fmt.Fprintf(&fullUrl, "http://api.tickstep.com/update/tickstep/aliyunpan/releases/latest?ip=%s&os=%s&arch=%s&version=%s", ipAddr, runtime.GOOS, runtime.GOARCH, global.AppVersion)
//fmt.Fprintf(&fullUrl, "http://localhost:8997/update/tickstep/aliyunpan/releases/latest?ip=%s&os=%s&arch=%s&version=%s", ipAddr, runtime.GOOS, runtime.GOARCH, global.AppVersion)
resp, err := client.Req(http.MethodGet, fullUrl.String(), nil, nil)
if resp != nil {
defer resp.Body.Close()
@@ -136,19 +136,12 @@ func GetLatestReleaseInfo(showPrompt bool) *ReleaseInfo {
client.SetTimeout(time.Duration(0) * time.Second)
client.SetKeepAlive(true)
// check tickstep srv
var tsReleaseInfo *ReleaseInfo = nil
for idx := 0; idx < 3; idx++ {
tsReleaseInfo = getReleaseFromTicstep(client, showPrompt)
if tsReleaseInfo != nil {
break
}
time.Sleep(time.Duration(5) * time.Second)
}
// tickstep server
var tsReleaseInfo = getReleaseFromTickstep(client, showPrompt)
// github
// github server
var ghReleaseInfo *ReleaseInfo = nil
for idx := 0; idx < 3; idx++ {
for idx := 0; idx < 2; idx++ {
ghReleaseInfo = getReleaseFromGithub(client, showPrompt)
if ghReleaseInfo != nil {
break
@@ -158,10 +151,11 @@ func GetLatestReleaseInfo(showPrompt bool) *ReleaseInfo {
var releaseInfo *ReleaseInfo = nil
if config.Config.UpdateCheckInfo.PreferUpdateSrv == "tickstep" {
// theoretically, tickstep server will be more faster at mainland
// theoretically, tickstep server will be faster in China mainland
releaseInfo = tsReleaseInfo
} else {
releaseInfo = ghReleaseInfo
// 如果github的版本信息获取不到(例如网络不稳定),可以尝试使用tickstep服务进行升级
if ghReleaseInfo == nil || ghReleaseInfo.TagName == "" {
releaseInfo = tsReleaseInfo
}
@@ -381,7 +375,8 @@ func CheckUpdate(version string, yes bool) {
return
}
execPath := cmdutil.ExecutablePath()
//execPath := cmdutil.ExecutablePath()
configPath := configurationPath()
var fileNum, errTimes int
for _, zipFile := range reader.File {
@@ -405,9 +400,11 @@ func CheckUpdate(version string, yes bool) {
name := zipFile.Name[strings.Index(zipFile.Name, "/")+1:]
if name == ReleaseName {
// 执行文件
err = update(cmdutil.Executable(), rc)
} else {
err = update(filepath.Join(execPath, name), rc)
// 配置文件
err = update(filepath.Join(configPath, name), rc)
}
if err != nil {
@@ -424,3 +421,8 @@ func CheckUpdate(version string, yes bool) {
fmt.Printf("更新完毕, 请重启程序\n")
}
// ConfigurationPath 获取程序配置所在目录
func configurationPath() string {
return config.GetConfigDir()
}
+9
View File
@@ -528,6 +528,15 @@ func (f *FileActionTask) uploadFile(ctx context.Context) error {
return nil
}
} else {
if len(f.syncItem.UploadEntity.PartInfoList) == 0 {
// finished
f.syncItem.Status = SyncFileStatusSuccess
f.syncItem.StatusUpdateTime = utils.NowTimeStr()
f.syncFileDb.Update(f.syncItem)
PromptPrintln("上传完毕:" + f.syncItem.getPanFileFullPath())
return nil
}
// 检测链接是否过期
// check url expired or not
uploadUrl := f.syncItem.UploadEntity.PartInfoList[f.syncItem.UploadPartSeq].UploadURL
+833
View File
@@ -0,0 +1,833 @@
// Copyright (c) 2020 tickstep.
//
// 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/aliyunpan/internal/utils"
"github.com/tickstep/library-go/converter"
"github.com/tickstep/library-go/requester/rio/speeds"
)
type TaskState int
type DashboardType int
const (
TaskQueued TaskState = iota
TaskRunning
TaskSuccess
TaskFailed
TaskSkipped
TaskCanceled
// DashboardPanelDownload 下载面板类型
DashboardPanelDownload DashboardType = 1
// DashboardPanelUpload 上传面板类型
DashboardPanelUpload DashboardType = 2
)
type DashboardOptions struct {
Title string
MaxHistory int
ActiveSlots int
Output io.Writer
}
// DashboardPanel 统计面板使用了很多ANSI转义序列。并不是所有的终端都支持ANSI转义序列,所以需要做兼容性处理。
type DashboardPanel struct {
dashboardType DashboardType
mu sync.Mutex
tasks map[string]*dashboardTask
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 dashboardTask struct {
id string
path string
name string
total int64
downloaded int64
speed int64
eta time.Duration
state TaskState
message string
isFile bool
}
// NewDashboardPanel 创建下载任务统计面板
func NewDashboardPanel(dashboardType DashboardType, parallel int, globalSpeeds *speeds.Speeds, opts *DashboardOptions) *DashboardPanel {
if parallel < 1 {
parallel = 1
}
db := &DashboardPanel{
dashboardType: dashboardType,
tasks: make(map[string]*dashboardTask),
order: make([]string, 0, 128),
history: make([]string, 0, 64),
maxHistory: 8,
parallel: parallel,
activeSlots: 5,
globalSpeeds: globalSpeeds,
title: "aliyunpan统计面板",
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
}
// Start 启动面板
func (db *DashboardPanel) 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:
// 延迟1秒
time.Sleep(1 * time.Second)
// 渲染
db.render()
return
case <-ticker.C:
db.render()
}
}
}()
})
}
// Close 关闭面板
func (db *DashboardPanel) Close() {
if db == nil {
return
}
db.closeOnce.Do(func() {
close(db.stopCh)
<-db.doneCh
fmt.Fprint(db.out, "\x1b[?25h") // 显示光标
})
}
// RegisterTask 注册任务信息
func (db *DashboardPanel) 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.isFile = isFile
}
// UpdateTaskInfo 更新任务信息
func (db *DashboardPanel) 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.isFile = isFile
}
// UpdateTaskProgress 更新任务进度
func (db *DashboardPanel) 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
}
// MarkTaskState 标记任务状态
func (db *DashboardPanel) 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))
}
}
// Logf 增加日志
func (db *DashboardPanel) Logf(format string, a ...interface{}) {
if db == nil {
return
}
msg := fmt.Sprintf(format, a...)
msg = strings.TrimRight(msg, "\r\n")
if msg == "" {
return
}
// 去掉开头的换行符
msg = strings.TrimPrefix(msg, "\n")
// 去掉回车换行符,UI面板的日志只能单行显示
msg = strings.ReplaceAll(msg, "\n", " ")
db.mu.Lock()
defer db.mu.Unlock()
db.appendHistoryLocked(msg)
}
func (db *DashboardPanel) getOrCreateTaskLocked(id string) *dashboardTask {
if task, ok := db.tasks[id]; ok {
return task
}
task := &dashboardTask{
id: id,
state: TaskQueued,
}
db.tasks[id] = task
db.order = append(db.order, id)
return task
}
func (db *DashboardPanel) 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 *DashboardPanel) defaultHistoryMessage(task *dashboardTask, 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
}
}
// render 渲染面板
func (db *DashboardPanel) render() {
if db == nil {
return
}
width, height := terminalSize()
if width < 60 {
width = 60
}
innerWidth := width - 2
snapshot := db.snapshot()
// 统计总览
headerLines := db.headerLines(snapshot, innerWidth)
// 下载任务队列详情
//activeTitle := db.activeTitle(snapshot)
activeLines := db.activeLines(snapshot, innerWidth)
// 下载日志
headerHeight := len(headerLines) + 2
activeHeight := len(activeLines) + 1
historyBase := 3
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(renderBox(headerLines, width))
builder.WriteString(renderTitledBox("ActiveDownloads", activeLines, width))
builder.WriteString(renderTitledBox("History", historyBody, width))
content := builder.String()
// 使用双缓冲技术
var buf strings.Builder
// 构建完整内容
buf.WriteString("\x1b[H\x1b[2J") // 清屏操作
buf.WriteString(content)
// 一次性输出
fmt.Fprint(db.out, buf.String())
}
type dashboardSnapshot struct {
tasks []*dashboardTask
history []string
}
func (db *DashboardPanel) snapshot() dashboardSnapshot {
db.mu.Lock()
defer db.mu.Unlock()
tasks := make([]*dashboardTask, 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("\u2502") // 左边框
//builder.WriteString("\u2514") // 左下角
builder.WriteString(strings.Repeat("\u2500", innerWidth))
builder.WriteString("\u2502") // 右边框
//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{}
for _, line := range body {
builder.WriteString("\n")
builder.WriteString("\u2502") // 左边框
builder.WriteString(fitLine(line, innerWidth)) // 内容,使用空格补足宽度
builder.WriteString("\u2502") // 右边框
}
builder.WriteString("\n")
if strings.HasPrefix(title, "History") {
builder.WriteString("\u2514") // 左下角
} 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()
}
func (db *DashboardPanel) 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 := "进行中"
if db.dashboardType == DashboardPanelDownload {
status = "下载中"
} else if db.dashboardType == DashboardPanelUpload {
status = "上传中"
}
if totalFiles > 0 && doneFiles >= totalFiles {
if failedFiles > 0 {
status = "已完成 (有错误)"
} else {
status = "已完成"
}
}
line1 := fmt.Sprintf("总速度: %s | 文件: %d/%d | 失败: %d | 状态: %s | 已用时间: %s | 剩余时间: %s",
utils.FormatSpeedFixedWidth(speed, 11), doneFiles, totalFiles, failedFiles, status, formatDurationShort(elapsed), left)
line1 = fitLine(line1, 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 := "总进度: "
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)
line2 := fmt.Sprintf("%s[%s]%s", label, bar, suffix)
line2 = fitLine(line2, innerWidth)
return []string{line1, line2}
}
func (db *DashboardPanel) activeTitle(snapshot dashboardSnapshot) string {
running := 0
queued := 0
for _, task := range snapshot.tasks {
if !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 *DashboardPanel) 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 := &dashboardTask{name: "[空闲...]", state: TaskQueued}
lines = append(lines, renderTaskLine(placeholder, i, innerWidth))
}
return lines
}
func (db *DashboardPanel) 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))
}
// 倒序数组
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 {
result = append(result, "")
}
return result[:lines]
}
func (db *DashboardPanel) fileCounts(tasks []*dashboardTask) (total int, done int, failed int) {
for _, task := range tasks {
if !task.isFile {
continue
}
total++
switch task.state {
case TaskSuccess, TaskSkipped, TaskFailed, TaskCanceled:
done++
}
if task.state == TaskFailed {
failed++
}
}
return
}
func (db *DashboardPanel) totalProgress(tasks []*dashboardTask) (downloaded int64, total int64) {
for _, task := range tasks {
if !task.isFile {
continue
}
if task.total > 0 {
total += task.total
if task.state == TaskSuccess || task.state == TaskSkipped {
// 成功、跳过的文件,统计上当做已经下载完成
downloaded += task.total
} else {
if task.downloaded >= task.total {
downloaded += task.total
} else {
downloaded += task.downloaded
}
}
}
}
return
}
func (db *DashboardPanel) totalSpeed(tasks []*dashboardTask) int64 {
if db.globalSpeeds != nil {
return db.globalSpeeds.GetSpeeds()
}
var sum int64
for _, task := range tasks {
sum += task.speed
}
return sum
}
func (db *DashboardPanel) pickActiveTasks(tasks []*dashboardTask) []*dashboardTask {
slotCap := db.activeSlots
if slotCap < 1 {
slotCap = 1
}
active := make([]*dashboardTask, 0, slotCap)
queued := make([]*dashboardTask, 0, slotCap)
for _, task := range tasks {
if !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 *dashboardTask, 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)) // 将浮点数格式化为:总宽度 5 个字符,包含 1 位小数,后面加百分号
} else {
// 空闲队列左边多一个空格,对齐活跃队列
percent = " --.-%"
}
// 速度
rate := "-"
// 剩余时间
eta := "-"
switch task.state {
case TaskQueued:
rate = "等待中"
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 /*1个空格分隔*/ + 2 /*进度条左右[]括号*/ + barWidth /*进度条长度*/ + 1 /*1个空格分隔*/ + percentWidth + 1 /*1个空格分隔*/ + rateWidth + 1 /*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)
}
// renderBar 绘制进度条
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)))
// 已经下载的部分使用 => 表示,没有下载的部分使用 空格 表示
if filled >= 1 {
return strings.Repeat("=", filled-1) + ">" + strings.Repeat(" ", width-filled)
}
return strings.Repeat(" ", 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
}
+28
View File
@@ -0,0 +1,28 @@
// Copyright (c) 2020 tickstep.
//
// 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"
// IsTerminal 判断输出环境,是否是输出到终端界面,如果重定向到文件则不用进行下载统计打印输出了
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
}
+32
View File
@@ -0,0 +1,32 @@
//go:build !windows
// Copyright (c) 2020 tickstep.
//
// 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() {}
+46
View File
@@ -0,0 +1,46 @@
//go:build windows
// Copyright (c) 2020 tickstep.
//
// 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)
}
+147 -4
View File
@@ -16,15 +16,13 @@ package utils
import (
"compress/gzip"
"crypto/md5"
"encoding/json"
"flag"
"fmt"
jsoniter "github.com/json-iterator/go"
uuid "github.com/satori/go.uuid"
"github.com/tickstep/aliyunpan-api/aliyunpan"
"github.com/tickstep/library-go/ids"
"io"
"io/ioutil"
"math"
"net/http"
"net/http/cookiejar"
"net/url"
"os"
@@ -34,6 +32,11 @@ import (
"strconv"
"strings"
"time"
jsoniter "github.com/json-iterator/go"
uuid "github.com/satori/go.uuid"
"github.com/tickstep/aliyunpan-api/aliyunpan"
"github.com/tickstep/library-go/ids"
)
// TrimPathPrefix 去除目录的前缀
@@ -301,3 +304,143 @@ func UnixTime2LocalFormatStr(unixTime int64) string {
cz := time.FixedZone("CST", 8*3600) // 东8区
return t.In(cz).Format("2006-01-02T15:04:05.000Z")
}
// FormatSpeedFixedWidth 格式化速度显示
// speed: 速度值,单位 Byte/s
// 返回格式: " xxx.xx MB/s" (固定长度,左侧用空格补齐)
func FormatSpeedFixedWidth(speed int64, totalWidth int) string {
const (
kb = 1024.0
mb = kb * 1024
gb = mb * 1024
tb = gb * 1024
)
var value float64
var unit string
speedFloat := float64(speed)
switch {
case speedFloat >= tb:
value = speedFloat / tb
unit = "TB/s"
case speedFloat >= gb:
value = speedFloat / gb
unit = "GB/s"
case speedFloat >= mb:
value = speedFloat / mb
unit = "MB/s"
case speedFloat >= kb:
value = speedFloat / kb
unit = "KB/s"
default:
value = speedFloat
unit = "B/s"
}
// 格式化为两位小数
formatted := fmt.Sprintf("%.2f %s", value, unit)
// 如果不足指定宽度,左侧用空格补齐
if len(formatted) < totalWidth {
spaces := strings.Repeat(" ", totalWidth-len(formatted))
return spaces + formatted
}
return formatted
}
func GetPublicIP() (string, error) {
type IPResponse struct {
Origin string `json:"origin"`
}
resp, err := http.Get("https://httpbin.org/ip")
if err != nil {
return "", fmt.Errorf("请求失败: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("HTTP 状态码异常: %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("读取响应体失败: %w", err)
}
var ipResp IPResponse
if err := json.Unmarshal(body, &ipResp); err != nil {
return "", fmt.Errorf("解析 JSON 失败: %w", err)
}
if ipResp.Origin == "" {
return "", fmt.Errorf("响应中无 IP 信息")
}
// 取第一个 IP(处理代理/X-Forwarded-For 情况)
ip := strings.TrimSpace(strings.Split(ipResp.Origin, ",")[0])
return ip, nil
}
// GetExpiredTimeSecFromOSSUrl 从OSS URL中解析出统一的过期时间戳(秒)
func GetExpiredTimeSecFromOSSUrl(rawURL string) (int64, error) {
parsedURL, err := url.Parse(rawURL)
if err != nil {
return 0, fmt.Errorf("failed to parse URL: %w", err)
}
query := parsedURL.Query()
sigVersion := query.Get("x-oss-signature-version")
var expires int64 = 0
// 检测版本标识(不区分大小写)
if strings.Contains(sigVersion, "OSS4") {
// V4 签名:x-oss-expires 为相对秒数,需要结合 x-oss-date
expiresStr := query.Get("x-oss-expires")
if expiresStr == "" {
return 0, fmt.Errorf("V4 signature: 'x-oss-expires' parameter not found")
}
relSec, err := strconv.ParseInt(expiresStr, 10, 64)
if err != nil {
return 0, fmt.Errorf("V4 signature: invalid 'x-oss-expires' value: %w", err)
}
dateStr := query.Get("x-oss-date")
if dateStr == "" {
return 0, fmt.Errorf("V4 signature: 'x-oss-date' parameter not found")
}
// 解析 ISO8601 UTC 时间,例如 "20231220T083018Z"
t, err := time.Parse("20060102T150405Z", dateStr)
if err != nil {
return 0, fmt.Errorf("V4 signature: failed to parse 'x-oss-date': %w", err)
}
expires = t.Unix() + relSec
} else if strings.Contains(sigVersion, "OSS3") || strings.Contains(sigVersion, "OSS2") {
// V2 / V3:x-oss-expires 为绝对时间戳
expiresStr := query.Get("x-oss-expires")
if expiresStr == "" {
return 0, fmt.Errorf("V2/V3 signature: 'x-oss-expires' parameter not found")
}
expires, err = strconv.ParseInt(expiresStr, 10, 64)
if err != nil {
return 0, fmt.Errorf("V2/V3 signature: invalid 'x-oss-expires' value: %w", err)
}
} else {
// 默认为 V1 签名(没有 x-oss-signature-version 或为空)
expiresStr := query.Get("Expires")
if expiresStr == "" {
return 0, fmt.Errorf("V1 signature: 'Expires' parameter not found")
}
expires, err = strconv.ParseInt(expiresStr, 10, 64)
if err != nil {
return 0, fmt.Errorf("V1 signature: invalid 'Expires' value: %w", err)
}
}
return expires, nil
}
+36 -39
View File
@@ -15,6 +15,18 @@ package main
import (
"fmt"
"io/ioutil"
"os"
"os/exec"
"path"
"path/filepath"
"runtime"
"sort"
"strconv"
"strings"
"time"
"unicode"
"github.com/olekukonko/tablewriter"
"github.com/peterh/liner"
"github.com/tickstep/aliyunpan/cmder"
@@ -33,17 +45,6 @@ import (
"github.com/tickstep/library-go/converter"
"github.com/tickstep/library-go/logger"
"github.com/urfave/cli"
"io/ioutil"
"os"
"os/exec"
"path"
"path/filepath"
"runtime"
"sort"
"strconv"
"strings"
"time"
"unicode"
)
const (
@@ -53,7 +54,7 @@ const (
var (
// Version 版本号
Version = "v0.3.7"
Version = "v0.4.0"
// 命令历史文件
historyFilePath = filepath.Join(config.GetConfigDir(), "aliyunpan_command_history.txt")
@@ -84,16 +85,14 @@ func init() {
func checkLoginExpiredAndRelogin() {
command.ReloadConfigFunc(nil)
activeUser := config.Config.ActiveUser()
if activeUser == nil || activeUser.UserId == "" {
// maybe expired, try to login
command.TryLogin()
} else {
// 尝试登录
activeUser := command.TryLogin()
if activeUser != nil && activeUser.UserId != "" {
// 刷新过期Token并保存到配置文件
command.RefreshWebTokenInNeed(activeUser, config.Config.DeviceName)
command.RefreshWebTokenInNeed(activeUser)
command.RefreshOpenTokenInNeed(activeUser)
command.SaveConfigFunc(nil)
}
command.SaveConfigFunc(nil)
}
func main() {
@@ -103,20 +102,7 @@ func main() {
checkLoginExpiredAndRelogin()
// check token expired task
go func() {
for {
// Token刷新进程,不管是CLI命令行模式,还是直接命令模式,本刷新任务都会执行
time.Sleep(time.Duration(1) * time.Minute)
//time.Sleep(time.Duration(5) * time.Second)
for _, u := range config.Config.UserList {
if u.UserId == config.Config.ActiveUID {
if u.PanClient() != nil {
checkLoginExpiredAndRelogin()
}
}
}
}
}()
command.AutomaticallyRefreshTokenTask() // Token刷新进程,不管是CLI命令行模式,还是直接命令模式,本刷新任务都会执行
app := cli.NewApp()
cmder.SetApp(app)
@@ -124,7 +110,7 @@ func main() {
app.Name = "aliyunpan"
app.Version = Version
app.Author = "tickstep/aliyunpan: https://github.com/tickstep/aliyunpan"
app.Copyright = "(c) 2021-2025 tickstep."
app.Copyright = "(c) 2021-2026 tickstep."
app.Usage = "阿里云盘客户端 for " + runtime.GOOS + "/" + runtime.GOARCH
app.Description = `aliyunpan 是一款阿里云盘命令行客户端工具, 为操作阿里云盘, 提供实用功能。
支持同步备份功能,支持备份本地文件到云盘,备份云盘文件到本地。
@@ -253,8 +239,10 @@ func main() {
fmt.Printf("提示: Ctrl + A / E 跳转命令 首 / 尾.\n")
fmt.Printf("提示: 输入 help 获取帮助.\n")
// check update
// 刷新配置文件
command.ReloadConfigFunc(c)
// check update
if config.Config.UpdateCheckInfo.LatestVer != "" {
if utils.ParseVersionNum(config.Config.UpdateCheckInfo.LatestVer) > utils.ParseVersionNum(global.AppVersion) {
fmt.Printf("\n当前的软件版本为:%s, 现在有新版本 %s 可供更新,强烈推荐进行更新!(可以输入 update 命令进行更新)\n\n",
@@ -262,6 +250,7 @@ func main() {
}
}
go func() {
// 新版本检测异步任务
latestCheckTime := config.Config.UpdateCheckInfo.CheckTime
nowTime := time.Now().Unix()
secsOf12Hour := int64(43200)
@@ -279,14 +268,22 @@ func main() {
}
}()
for {
for { // 命令行交互进程,每一次命令执行都会进入到这个for循环
var (
prompt string
activeUser = config.Config.ActiveUser()
)
if activeUser == nil {
// 尝试重新登录并获取新的token
activeUser = command.TryLogin()
// 自动登录成功
if activeUser != nil {
logger.Verboseln("自动重新登录成功")
// 保存新token到配置文件
command.SaveConfigFunc(c)
}
}
if activeUser != nil && activeUser.Nickname != "" {
@@ -309,14 +306,14 @@ func main() {
prompt = app.Name + " > "
}
commandLine, err := line.State.Prompt(prompt)
switch err {
commandLine, err1 := line.State.Prompt(prompt)
switch err1 {
case liner.ErrPromptAborted:
return
case nil:
// continue
default:
fmt.Println(err)
fmt.Println(err1)
return
}
Binary file not shown.
Binary file not shown.
+7 -7
View File
@@ -2,14 +2,14 @@
"FixedFileInfo": {
"FileVersion": {
"Major": 0,
"Minor": 3,
"Patch": 7,
"Minor": 4,
"Patch": 0,
"Build": 0
},
"ProductVersion": {
"Major": 0,
"Minor": 3,
"Patch": 7,
"Minor": 4,
"Patch": 0,
"Build": 0
},
"FileFlagsMask": "3f",
@@ -22,14 +22,14 @@
"Comments": "",
"CompanyName": "tickstep",
"FileDescription": "阿里云盘客户端",
"FileVersion": "v0.3.7",
"FileVersion": "v0.4.0",
"InternalName": "",
"LegalCopyright": "© 2021-2025 tickstep.",
"LegalCopyright": "© 2021-2026 tickstep.",
"LegalTrademarks": "",
"OriginalFilename": "",
"PrivateBuild": "",
"ProductName": "aliyunpan",
"ProductVersion": "v0.3.7",
"ProductVersion": "v0.4.0",
"SpecialBuild": ""
},
"VarFileInfo": {