mirror of
https://gitee.com/zhangsken/aliyunpan.git
synced 2026-10-16 10:29:42 +08:00
Compare commits
47 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d05d19acd8 | |||
| 5179f914ed | |||
| 2feaa3cf82 | |||
| cedd43d402 | |||
| e62bdbb9f1 | |||
| acc408f0a0 | |||
| db28ea6732 | |||
| a9bc1076a5 | |||
| e20ea479c4 | |||
| 42f044ba38 | |||
| 88f1ff2688 | |||
| ca09039e48 | |||
| cf38ad51fd | |||
| 30cbced50c | |||
| 0946939325 | |||
| da13b046b8 | |||
| 268e4a30d1 | |||
| a0ca0f8089 | |||
| 2d99a39606 | |||
| 2965179ae8 | |||
| 0565482121 | |||
| 1822d3c2b2 | |||
| 6672aa4cd4 | |||
| c69a27c9b1 | |||
| 4feacbcdbf | |||
| 1916dda8f8 | |||
| 8b5eb14eb6 | |||
| d3d9690d55 | |||
| 89a6ef326c | |||
| 35dfb35429 | |||
| 3c7ebb5f62 | |||
| f796f40664 | |||
| 0d9700c84c | |||
| 09ee162ba4 | |||
| ff0e3dbfa9 | |||
| 4c5f45e5ab | |||
| 02326b0bd7 | |||
| 350fec4275 | |||
| 70a14dd1a6 | |||
| 23dad72914 | |||
| 9fa547ddc8 | |||
| 2898037ec5 | |||
| 70c14fd66f | |||
| 8913597716 | |||
| 9b276f2caf | |||
| e8bda159fa | |||
| e1ac6a95b9 |
@@ -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
|
||||

|
||||
|
||||
```
|
||||
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,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
@@ -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命令行程序,任何云盘命令都有类似如下日志输出
|
||||

|
||||

|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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))
|
||||
|
||||
// 输出失败的文件列表
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -54,7 +54,7 @@ const (
|
||||
DefaultFileDownloadParallelNum = 5
|
||||
|
||||
// MaxFileDownloadParallelNum 最大文件下载并发数量。过大会被阿里云盘风控,导致无法下载
|
||||
MaxFileDownloadParallelNum = 20
|
||||
MaxFileDownloadParallelNum = 3
|
||||
|
||||
// DefaultTokenServiceWebHost 默认的token服务
|
||||
DefaultTokenServiceWebHost = "https://api.tickstep.com"
|
||||
|
||||
@@ -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代表不限制"},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: // 下载遇到限流报错
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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() {}
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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": {
|
||||
|
||||
Reference in New Issue
Block a user